自学内容网 自学内容网

网络编程 - - TCP套接字通信及编程实现

概述

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的传输层协议。在网络编程中,TCP常用于实现客户端和服务器之间的可靠数据传输。本文将基于C语言实现TCP服务端和客户端建立通信的过程。

三次握手

在 TCP 连接建立之前,客户端和服务器之间需要进行三次握手来同步双方的序列号,并确认双方都准备好进行数据传输

  • 第一次握手:客户端向服务器发送一个 SYN(同步序列编号)报文段,表示请求建立连接。客户端进入 SYN_SENT 状态
  • 第二次握手:服务器收到 SYN 报文段后,回复一个 SYN-ACK(同步序列编号 + 确认)报文段,表示同意建立连接。服务器进入 SYN_RCVD 状态
  • 第三次握手:客户端收到 SYN-ACK 报文段后,回复一个 ACK(确认)报文段,表示确认收到服务器的响应。客户端和服务器都进入 ESTABLISHED 状态,连接正式建立

在这里插入图片描述

图片来源:https://img-blog.csdnimg.cn/39bb4f4da21a4513b9506ecdf6a40cf3.png

四次挥手

通信结束时,客户端或服务器可以发起断开连接的请求。断开连接的过程称为四次挥手,以确保双方都能正确关闭连接并释放资源

  • 第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(终止)报文段,表示不再发送数据。主动关闭方进入 FIN_WAIT_1 状态
  • 第二次挥手:被动关闭方(通常是服务器)收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方进入 FIN_WAIT_2 状态
  • 第三次挥手:被动关闭方在处理完所有未完成的数据后,发送一个 FIN 报文段,表示自己也不再发送数据。被动关闭方进入 LAST_ACK 状态
  • 第四次挥手:主动关闭方收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。主动关闭方进入 TIME_WAIT 状态,等待一段时间(通常为2倍的最大报文段生命周期,即2MSL),以确保被动关闭方收到了最后的 ACK。之后,主动关闭方进入 CLOSED 状态,连接完全关闭

2MSL:MSL 的默认值是 30 秒,基于经验选择的一个保守估计,用来确保大多数网络环境下的数据包都能被接收或者超时。

大部分操作系统都允许用户调整 MSL 的值,从而改变 TIME_WAIT 状态的持续时间

在这里插入图片描述

图片来源:https://i-blog.csdnimg.cn/blog_migrate/843f121dd50cd8458daf1fa834bc1f36.png

TCP保证可靠传输方式

  • 序列号:每个TCP报文段都有一个序列号,表示该报文段中的第一个字节在整个数据流中的位置。接收方可以根据序列号重新排序接收到的报文段,确保数据按顺序传递
  • 确认应答:接收方在收到报文段后,会发送一个确认应答,告诉发送方哪些数据已经成功接收。发送方根据确认应答判断是否需要重传丢失或损坏的报文段
  • 超时重传:如果发送方在一定时间内没有收到确认应答,它会认为报文段可能丢失或延迟,并重新发送该报文段。TCP使用动态调整的超时机制来优化重传策略
  • 流量控制:TCP使用滑动窗口机制来控制发送方的发送速率,确保接收方不会被过多的数据淹没。接收方会在确认应答中告知发送方当前可用的接收窗口大小,发送方根据这个信息调整自己的发送速率
  • 拥塞控制:TCP通过多种算法(如慢启动、拥塞避免、快速重传和快速恢复)来动态调整发送方的发送速率,避免网络拥塞。这些算法旨在在网络负载较高时减小发送速率,在网络条件改善时逐渐增加发送速率

TCP通信实现流程

涉及到的库方法

  • 创建套接字(Socket)

    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
    
  • 绑定(Bind)套接字到指定的IP地址和端口

    #include <sys/socket.h>
    int bind(int socket, const struct sockaddr *address, socklen_t address_len);
    
  • 监听(Listen)客户端的连接请求

    #include <sys/socket.h>
    int listen(int socket, int backlog)
    
  • 接受(Accept)客户端的连接

    #include <sys/socket.h>
    int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
    
  • 连接、发送和接收数据(Send/Recv)

    #include <sys/socket.h>
    int connect(int socket, const struct sockaddr *address, socklen_t address_len);
    ssize_t send(int socket, const void *buffer, size_t length, int flags);
    ssize_t recv(int socket, void *buffer, size_t length, int flags);
    
  • 关闭连接(Close)

    #include <unistd.h>
    int close(int fildes);
    

代码实现

服务器代码(Linux)

tcp_server.h

#ifndef TCP_SERVER_H
#define TCP_SERVER_H

#include <pthread.h>
#include <netinet/in.h>
#include "cJson.h"
#include "common_base.h"

// 定义常量
#define PORT 18888
#define BUFFER_SIZE 1024
#define TCP_IP "127.0.0.1"

// 外部函数声明
extern void getSerialNoStr(char *buf);

/**
 * 启动服务器主循环,等待客户端连接
 */
void start_serve_tcp(void *arg);

/**
 * 解析接收到的消息
*/
void parse_message(char *data, size_t data_size);

#endif // TCP_SERVER_H

tcp_server.c

#include "tcp_server.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

/**
 * 启动TCP服务器,等待客户端连接并处理接收到的数据。
 *
 * @param arg 传递给线程的参数,通常为NULL。
 */
void start_serve_tcp(void *arg) 
{
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE] = {0};

    // 创建 Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("Socket 创建失败");
        return;
    }

    // 配置服务器地址
    memset_s(&server_addr, sizeof(server_addr), 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;

    // 配置端口号和IP
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(TCP_IP);
    
   // 绑定 Socket
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) 
    {
        printf("绑定失败\n");
        close(server_fd);
        return;
    }

    // 开始监听
    if (listen(server_fd, 5) == -1)
    {
        printf("监听失败\n");
        close(server_fd);
        return;
    }

    printf("服务器已启动,监听地址:%s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    // 等待客户端连接
    while (1)
    {
        printf("等待客户端连接...\n");

        if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) 
        {
            printf("接受客户端连接失败");
            continue;
        }

        printf("客户端已连接:%s:%d\n",
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 接收数据
        int received_size;
        printf("receive before client_fd: %d\n", client_fd);
        while ((received_size = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) 
        {
            printf("收到数据,大小: %d 字节\n", received_size);
            parse_message(buffer, received_size);

            printf("jsonString: %s\n", jsonString);
            sleep(1);
            
            if(jsonString)
            {              
                printf("receive after client_fd: %d\n", client_fd);
                // 发送 JSON 数据到客户端
                ssize_t sent_size = send(client_fd, jsonString, strlen(jsonString), 0);
                if (sent_size == -1)
                {
                    printf("发送数据失败\n");
                } 
                else 
                {
                    printf("成功发送 %zd 字节到客户端\n", sent_size);
                }

                free(jsonString);
                jsonString = NULL;
            }
            else 
            {
                printf("生成 JSON 数据失败\n");
            }
        }

        if (received_size == 0)
        {
            printf("客户端已断开连接\n");
        } 
        else if (received_size == -1)
        {
            printf("接收数据失败");
        }

        close(client_fd);
    }

    close(server_fd);
}

void parse_message(char *data, size_t data_size) 
{
    cJSON *rootMsg = NULL;  
cJSON *serialNo = NULL; // 序列号
cJSON *netCmd = NULL;   // 操作行为
    MANUAL_TRIG_PARAM manualTrParam;    // 手动触发抓拍接口传参结构体

    // 确保消息头部完整性(消息类型:4字节,数据长度:4字节)
    if (data_size < 8)
    {
        printf("数据长度不足,无法解析\n");
        return;
    }

     // 解析消息类型和数据长度
    int message_type = ntohl(*(int *)data);
    int data_length = ntohl(*(int *)(data + 4));

    printf("消息类型: %d\n", message_type);
    printf("数据长度: %d\n", data_length);

    // 检查数据长度是否匹配
    if (data_size < 8 + data_length) 
    {
        printf("数据长度与实际内容不匹配\n");
        return;
    }

     // 解析消息内容
    char *message_content = (char *)malloc(data_length + 1);
    if (!message_content) 
    {
        printf("内存分配失败\n");
        return;
    }
    memcpy(message_content, data + 8, data_length);
    message_content[data_length] = '\0'; // 确保字符串以 \0 结尾

    printf("消息内容: %s\n", message_content);

    rootMsg = cJSON_Parse(message_content);
    if (NULL == rootMsg)
{
printf("rootMsg is not json tpye\n");
        free(message_content);
        return;
}
    printf("解析成功\n");

    serialNo = cJSON_GetObjectItem(rootMsg, "serialNo");
    netCmd = cJSON_GetObjectItem(rootMsg, "netCmd");

    if (serialNo && netCmd)
    {
        printf("serialNo: %s\n", serialNo->valuestring);
        printf("netCmd: %s\n", netCmd->valuestring);
    } 
    else
    {
        printf("缺少必要的 JSON 字段\n");
        cJSON_Delete(rootMsg);
        free(message_content);
        return;
    }

    char serial[64] = {0};
    getSerialNoStr(serial);

    printf("getSerialNoStr: %s\n", serial);
    printf("serialNo: %s\n", serialNo->valuestring);
    
    if(strncmp(serial, serialNo->valuestring, 9) == 0)
    {
        // TODO:业务处理
    }
    else
    {
        printf("serialNo is not equal\n");
    }

    cJSON_Delete(rootMsg);
    free(message_content);
}


客户端代码(Windows)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

void send_message(const char *ip, int port, int message_type, const char *content) {
    WSADATA wsa;
    SOCKET sock;
    struct sockaddr_in server_addr;
    char buffer[1024];
    int recv_size;
    char recv_buffer[1024];

    // 初始化 Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        printf("Winsock 初始化失败,错误代码: %d\n", WSAGetLastError());
        exit(EXIT_FAILURE);
    }

    // 创建 Socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Socket 创建失败,错误代码: %d\n", WSAGetLastError());
        WSACleanup();
        exit(EXIT_FAILURE);
    }

    // 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = inet_addr(ip);

    // 连接服务器
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) {
        printf("连接服务器失败,错误代码: %d\n", WSAGetLastError());
        closesocket(sock);
        WSACleanup();
        exit(EXIT_FAILURE);
    }
    // 准备消息
    int data_length = strlen(content);
    int net_message_type = htonl(message_type);
    int net_data_length = htonl(data_length);


    memcpy(buffer, &net_message_type, 4);
    memcpy(buffer + 4, &net_data_length, 4);
    memcpy(buffer + 8, content, data_length);

    // 发送消息
    send(sock, buffer, 8 + data_length, 0);

     // 接收服务器返回的数据
    if ((recv_size = recv(sock, recv_buffer, sizeof(recv_buffer) - 1, 0)) == SOCKET_ERROR) {
        printf("接收数据失败,错误代码: %d\n", WSAGetLastError());
    } else {
        recv_buffer[recv_size] = '\0'; // 确保以 null 结尾
        printf("服务器返回: %s\n", recv_buffer);
    }

    closesocket(sock);
    WSACleanup();
}

int main() {
    send_message("127.0.0.1", 18888, 1, "{id: 'FS123456', command: 'NET_CAP'}");
    return 0;
}

原文地址:https://blog.csdn.net/qq_40949254/article/details/145158938

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!