Linux socket编程(一):客户端服务端通信、解决TCP粘包

一、服务端程序

服务端程序工作流程: 创建socket → \rightarrow 绑定监听的IP地址和端口 → \rightarrow 监听客户端连接 → \rightarrow 接受/发送数据。对应到系统API的调用就是socket() → \rightarrow bind() → \rightarrow listen() → \rightarrow accept() → \rightarrow recv()/send()

socket()创建socket,返回一个文件描述符,这个文件描述符只用于监听客户端的连接,不负责与客户端通信。调用accept()函数后,会返回一个与当前客户端通信的文件描述符。

TCP建立连接的过程: 调用listen()后,服务器监听指定的端口。收到客户端发来的SYN报文后(第一次握手),将这个连接请求放入半连接队列,并返回SYN+ACK报文(第二次握手)。当收到客户端发来的ACK报文后(第三次握手),服务器将这个连接请求从半连接队列中拿出,放入全连接队列中,等待accpet()取出。

listen()和accpet()的区别: 调用listen()后就已经可以与客户端建立连接了。accpet()只是从全连接队列中拿出一个已经建立的连接

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int open_socket() {
    int sockfd = 0;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
        perror("socket wrong\n");
    }
    // 在 struct sockaddr_in 中设置监听信息,三个属性:
    // 1. sin_family:协议类型
    // 2. sin_port:端口号
    // 3. sin_addr:一个struct,其中只包含一个 unsigned long 字段 s_addr,存储 ip 地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    // INADDR_ANY:监听所有网卡的 ip 地址
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // htons: 将主机字节序(通常为小端)转换为网络字节序(大端)
    server_addr.sin_port = htons(5005);
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {
        perror("bind wrong\n");
    }
    if (listen(sockfd, 4096) != 0) {
        perror("listen wrong\n");
    }
    return sockfd;
}

int main() {
    int sockfd = open_socket();
    int clientfd = 0;
    struct sockaddr_in client_addr;
    int size = sizeof(struct sockaddr_in);
    clientfd = accept(sockfd, (struct sockaddr*)&client_addr, (socklen_t*)&size);
    char buffer[1024];
    int ret = 0;
    while (true) {
        memset(buffer, 0, sizeof(buffer));
        ret = recv(clientfd, buffer, sizeof(buffer), 0);
        if (ret == 0) {
            printf("client disconnect\n");
            break;
        } else if (ret < 0) {
            perror("recv error\n");
        } else {
            printf("msg size = %d, msg = %s\n", ret, buffer);
        }
    }
    close(sockfd);
    close(clientfd);
    return 0;
}

全连接队列满了会怎样: listen()的第二个参数由用户指定全连接队列最大长度,但实际长度由这个参数和内核参数共同决定。实际长度=min(backlog, /proc/sys/net/core/somaxconn)。backlog约等于listen()的第二个参数,但略有不同。比如我指定backlog = 2,实际的全连接队列最大长度为3。
全连接队列长度
当全连接队列满后,服务端不会再接受第三次握手的ACK报文,一定时间后会重发第二次握手的SYN+ACK报文,因此没有进入全连接队列的客户端会回到SYN_SENT状态并重新发送ACK报文,直到超时。比如服务端程序不进行accept(),启动10个客户端连接,用ss -nt命令查看连接状态,发现只有三个客户端可以进入ESTABLISH状态,其它客户端处于SYN_SENT状态。
Linux socket编程(一):客户端服务端通信、解决TCP粘包_第1张图片

二、客户端程序

客户端工作流程: 创建socket → \rightarrow 连接服务端 → \rightarrow 接受/发送数据

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int open_connect(const char* ip, uint16_t port) {
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket wrong\n");
        return sockfd;
    }
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    // struct hostent:保存 ip 地址,以网络字节序存储
    struct hostent* h = NULL;
    // gethostbyname:将 ip 地址或域名转化为网络字节序,返回一个 struct hostent*
    if ((h = gethostbyname(ip)) == NULL) {
        printf("wrong host name\n");
    }
    memcpy(&server_addr.sin_addr, h->h_addr, h->h_length);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {
        perror("connect wrong\n");
    }
    return sockfd;
}
int main() {
    int sockfd = open_connect("127.0.0.1", 5005);
    char buffer[1024];
    int ret;
    for (int i = 0; i < 1000; i++) {
        memset(buffer, 0, sizeof(buffer));
        sprintf(buffer, "this is message %d", i);
        // strlen(buffer) + 1 是额外计算'\0'所占的内存
        if ((ret = send(sockfd, buffer, strlen(buffer) + 1, 0)) <= 0) {
            perror("send error\n");
        }
        printf("send message: %s\n", buffer);
    }
    close(sockfd);
    return 0;
}

send()函数: 调用send()函数后,会将数据拷贝到内核缓冲区中(只拷贝,不负责真的发送)。如果要发送的数据超过了缓冲区大小,则返回错误。如果发送数据超过了缓冲区当前剩余大小,则只拷贝缓冲区能容下的部分数据。

三、TCP粘包

上述程序运行后,服务端接收到的数据为:
Linux socket编程(一):客户端服务端通信、解决TCP粘包_第2张图片
客户端每次发送18或19个字节的数据,即字符串“this is message xx”,但服务端每次接受到的数据长度不一样,说明有多条数据拼接在一起(实际打印的数据会截断到’\0’的位置),这就是TCP粘包问题。

TCP粘包: TCP是面向字节流的协议,一个TCP报文中携带的数据并不由应用程序决定。上面的客户端程序每次send一个字符串到内核缓冲区,但每次send的数据不一定是一个TCP报文,一个报文中可能包含多个send过来的数据。服务端在读取时,会在buffer大小范围内,将缓冲区的数据全都读进来。

解决TCP粘包问题: 每次发送数据之前,先发送一个固定长度的自定义包头,包头中定义了这一次数据的长度。服务端先按照包头长度接受包头数据,再按照包头数据中指定的数据长度接受这一次通信的数据。

在下面代码中,我们使用一个int类型作为“包头”,代表发送数据的长度。而int类型固定4字节,因此服务端每次先接受4字节的数据x,再接受x字节的字符串数据。

// 替代 recv() 函数
bool receive_message(int clientfd, char* buf) {
    int one_size = 0;
    int msg_size = 0;
    // 先接受 msg_size
    one_size = recv(clientfd, &msg_size, sizeof(int), 0);
    if (one_size == 0) {
        printf("client disconnect\n");
        return false;
    }
    if (one_size < 0) {
        perror("recv wrong\n");
        return false;
    }
    int pos = 0;
    while (msg_size > 0) {
        one_size = recv(clientfd, buf + pos, msg_size, 0);
        if (one_size == 0) {
            printf("client disconnect\n");
            return false;
        }
        if (one_size < 0) {
            perror("recv wrong\n");
            return false;
        }
        pos += one_size;
        msg_size -= one_size;
    }
    printf("message size = %d, message: %s\n", pos, buf);
    return true;
}

客户端调用send()函数时,由于可能当前缓冲区剩余空间不足以放下所有要发送的数据,因此也需要用send()函数的返回值来判断是否完成发送

// 替代 send() 函数
void send_message(int sockfd, const char* msg) {
    int msg_size = strlen(msg);
    int send_size = 0;
    send_size = send(sockfd, &msg_size, sizeof(int), 0);
    if (send_size < 0) {
        perror("send wrong\n");
        return;
    }
    int pos = 0;
    while (msg_size > 0) {
        send_size = send(sockfd, msg + pos, strlen(msg) - pos, 0);
        if (send_size < 0) {
            perror("send wrong\n");
            return;
        }
        pos += send_size;
        msg_size -= send_size;
    }
    printf("send message: %s\n", msg);
}

你可能感兴趣的:(socket,linux,tcp/ip,网络)