700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Linux socket编程(一):客户端服务端通信 解决TCP粘包

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

时间:2023-02-25 22:20:48

相关推荐

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 <sys/socket.h>#include <sys/types.h>#include <netinet/in.h>#include <stdio.h>#include <string.h>#include <stdbool.h>#include <unistd.h>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状态。

二、客户端程序

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

#include <sys/socket.h>#include <sys/types.h>#include <netinet/in.h>#include <netdb.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>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粘包

上述程序运行后,服务端接收到的数据为:

客户端每次发送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_sizeone_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);}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。