今天和大家讲一下socket网络编程中粘包和拆包的问题。
1、出现粘包拆包的原因
假设一个这样的场景,客户端要利用send()函数发送字符“asd”到服务端,连续发送3次,但是服务端休眠10秒之后再去缓冲池中接收。那么请问10秒之后服务端从缓冲区接收到的信息是“asd”还是“asdasdasd”呢?如果大家有去做实验的话,可以知道服务端收到的是“asdasdasd”,为什么会这样呢?按正常的话,服务端收到的应该是“asd”,剩下的两个asd要不就是收不到要不就是下次循环收到,怎么会一次性收到“asdasdasd”呢?如果要说罪魁祸首的话就是那个休眠10秒,导致数据粘包了!
服务端代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 512 #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) int main() { //创建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //初始化socket元素 struct sockaddr_in server_addr; int server_len = sizeof(server_addr); memset(&server_addr, 0, server_len); server_addr.sin_family = AF_INET; //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以 server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(39002); //绑定文件描述符和服务器的ip和端口号 int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len); if (m_bindfd < 0) { ERR_EXIT("bind ip and port fail"); } //进入监听状态,等待用户发起请求 int m_listenfd = listen(m_sockfd, 20); if (m_listenfd < 0) { ERR_EXIT("listen client fail"); } //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); //这里休眠了10秒 sleep(10); //接收客户端数据 char buffer[BUF_SIZE]; recv(m_connfd, buffer, sizeof(buffer)-1, 0); printf("server recv:%s ", buffer); strcat(buffer, "+ACK"); send(m_connfd, buffer, strlen(buffer), 0); //关闭套接字 close(m_connfd); close(m_sockfd); return 0; }
客户端代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 512 #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) int main() { //创建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //服务器的ip为本地,端口号 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("81.68.140.74"); server_addr.sin_port = htons(39002); //向服务器发送连接请求 int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (m_connectfd < 0) { ERR_EXIT("connect server fail"); } //发送并接收数据 char buffer[BUF_SIZE] = "asd"; int datasize = strlen(buffer); send(m_sockfd, buffer, datasize, 0); send(m_sockfd, buffer, datasize, 0); send(m_sockfd, buffer, datasize, 0); recv(m_sockfd, buffer, sizeof(buffer)-1, 0); printf("client recv:%s ", buffer); //断开连接 close(m_sockfd); return 0; }
以上代码在Linux平台上运行之后就会出现粘包现象,大家可以把以上代码复制去验证看看。
2、粘包拆包的几种情况
这个问题在socket网络编程中非常的常见,数据不仅会粘包,还会被拆包,就是一段数据被拆成两部分。那么拆包、粘包问题产生的原因都有哪些呢
- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
而数据之所以会发送粘包拆包的根本原因是TCP的数据包是流的方式传输的,就像水流一样,没有一个分界的东西。
3、处理粘包拆包的方法
处理拆包、粘包问题的方法:
那么最关键的就是我们该怎么处理粘包拆包问题呢?因为这个问题在socket无法很好的处理,所以必须要在应用层上面处理,所以就需要要求大家在封装网络通信接口的时候要自己实现粘包拆包的处理方法。解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:
- 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
- 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
- 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
第1种和第2种方法都会存在一些误差,没有办法很好处理好粘包拆包,所以一般的方法都是采用第3种。以下我先给出代码,然后再结合代码分析第3种粘包拆包的处理方式。
3.1、服务端代码
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include "protocol.h" #define BUF_SIZE 512 #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) int main() { //创建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //初始化socket元素 struct sockaddr_in server_addr; int server_len = sizeof(server_addr); memset(&server_addr, 0, server_len); server_addr.sin_family = AF_INET; //server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以 server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(39002); //绑定文件描述符和服务器的ip和端口号 int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len); if (m_bindfd < 0) { ERR_EXIT("bind ip and port fail"); } //进入监听状态,等待用户发起请求 int m_listenfd = listen(m_sockfd, 20); if (m_listenfd < 0) { ERR_EXIT("listen client fail"); } //定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); //接收客户端数据 char recv_buffer[10000]; //接收数据的buffer memset(recv_buffer, 0, sizeof(recv_buffer)); //初始化接收buffer while (1) { if (m_connfd < 0) { m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len); printf("client accept success again!!! "); } //休眠10秒才能有粘包现象出现 sleep(10); int nrecvsize = 0; //一次接收到的数据大小 int sum_recvsize = 0; //总共收到的数据大小 int packersize; //数据包长度 int disconn = false; //先从缓存池取出包头 while (sum_recvsize != sizeof(NetPacketHeader)) { nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, sizeof(NetPacketHeader) - sum_recvsize, 0); if (nrecvsize == 0) { close(m_connfd); m_connfd = -1; printf("client lose connection!!! "); disconn = true; break; } sum_recvsize += nrecvsize; } if (disconn) { continue; } NetPacketHeader *phead = (NetPacketHeader *)recv_buffer; packersize = phead->wDataSize; //客户端发过来的数据包长度(包含包头) //从缓冲池中取出数据(不包含包头) while (sum_recvsize != packersize) { nrecvsize = recv(m_connfd, recv_buffer + sum_recvsize, packersize - sum_recvsize, 0); if (nrecvsize == 0) { close(m_connfd); m_connfd = -1; printf("client lose connection!!! "); disconn = true; break; } else if (nrecvsize < 0) { ERR_EXIT("recv fail"); } printf("server recv:%s, size:%d ", recv_buffer + sum_recvsize, nrecvsize); sum_recvsize += nrecvsize; } if (disconn) { continue; } } //关闭套接字 close(m_connfd); close(m_sockfd); return 0; }
3.2、客户端代码
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include "protocol.h" #define BUF_SIZE 512 #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) int main() { //创建套接字 int m_sockfd = socket(AF_INET, SOCK_STREAM, 0); if (m_sockfd < 0) { ERR_EXIT("create socket fail"); } //服务器的ip为本地,端口号 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("81.68.140.74"); server_addr.sin_port = htons(39002); //向服务器发送连接请求 if (connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ERR_EXIT("connect server fail"); } //发送并接收数据 char data_buffer[BUF_SIZE] = "asd"; int datasize = strlen(data_buffer); NetPacket send_packet; //数据包 send_packet.Header.wDataSize = datasize + sizeof(NetPacketHeader); //数据包大小 memcpy(send_packet.Data, data_buffer, datasize); //数据拷贝 send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); send(m_sockfd, &send_packet, send_packet.Header.wDataSize, 0); //断开连接 close(m_sockfd); return 0; }
3.3、公用的部分
//protocol.h #ifndef _PROTOCOL_H #define _PROTOCOL_H #define NET_PACKET_DATA_SIZE 5000 /// 网络数据包包头 struct NetPacketHeader { unsigned short wDataSize; ///< 数据包大小,包含包头的长度和数据长度 }; /// 网络数据包 struct NetPacket { NetPacketHeader Header; /// 包头 unsigned char Data[NET_PACKET_DATA_SIZE]; /// 数据 }; #endif
首先定义一个新的文件protocol.h,主要是客户端和服务端共用的部分,包含数据包和包头的结构体定义。
然后客户端发送的时候记得发送数据体的长度是数据加包头的长度。
而在接收端的代码则稍微要花点心思了。首先接收端需要分两次来从缓冲池中接收数据,先取出长度为包头的数据,然后去取数据体的部分的时候一定要记得每次从缓冲区取数据的偏移量。
这样子就可以正确的处理好粘包拆包的问题了。当然从服务端向客户端发送数据的话,两者则是颠倒过来,这里就不在说明了。最后希望大家可以从这边文章获得一点收获,有什么疑问欢迎在下方评论说明。
更多精彩内容,请关注同名公众:一点月光(alittle-moon)