TCP粘包问题

1. 问题背景:

tcp是以流动的方式传输数据,没有边界的一段数据。像打开自来水管一样,连成一片,没有边界。传输的最小单位为一个报 文段(segment)。tcp Header中有个Options标识位,常见的标识为mss(Maximum Segment Size)指的是:连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。换算成字节, 也就是180多字节。tcp为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。 同理,接收方也有缓冲区这样的机制,来接收数据。

发现,如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况,这就是TCP协议中经常会遇到的粘包以及拆包的问题。
我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会。UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

2. 粘包、拆包表现形式
现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

 

 

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

3.粘包、拆包发生原因
发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
等等。

4.粘包、拆包解决办法
通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
等等。

 

5. 构造粘包问题

SERVER:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <errno.h>
 6 #include <sys/types.h>
 7 #include <sys/socket.h>
 8 #include <netinet/in.h>
 9 #include <arpa/inet.h>
10 #define ERR_EXIT(m) \
11     do { \
12         perror(m);\
13         exit(EXIT_FAILURE);\
14     }while(0)
15 
16 void do_service(int sockfd);
17 
18 int main(int argc, const char *argv[])
19 {
20     int listenfd = socket(PF_INET, SOCK_STREAM, 0);
21     if(listenfd == -1)
22         ERR_EXIT("socket");
23 
24     //地址复用
25     int on = 1;
26     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
27         ERR_EXIT("setsockopt");
28 
29     struct sockaddr_in addr;
30     memset(&addr, 0, sizeof addr);
31     addr.sin_family = AF_INET;
32     addr.sin_addr.s_addr = inet_addr("127.0.0.1");
33     addr.sin_port = htons(8976);
34     if(bind(listenfd, (struct sockaddr*)&addr, sizeof addr) == -1)
35         ERR_EXIT("bind");
36 
37     if(listen(listenfd, SOMAXCONN) == -1)
38         ERR_EXIT("listen");
39 
40     int peerfd = accept(listenfd, NULL, NULL);
41     do_service(peerfd);
42 
43     close(peerfd);
44     close(listenfd);
45 
46     return 0;
47 }
48 
49 
50 
51 void do_service(int sockfd)
52 {
53     int cnt = 0;
54     char recvbuf[1024000] = {0};
55     while(1)
56     {
57         int nread = read(sockfd, recvbuf, sizeof recvbuf);
58         if(nread == -1)
59         {
60             if(errno == EINTR)
61                 continue;
62             ERR_EXIT("read");
63         }
64         else if(nread == 0)
65         {
66             printf("close ...\n");
67             exit(EXIT_SUCCESS);
68         }
69 
70         printf("count = %d, receive size = %d\n", ++cnt, nread);
71         //write(sockfd, recvbuf, strlen(recvbuf));
72         memset(recvbuf, 0, sizeof recvbuf);
73     }
74 }

注意, server端的接收缓冲区应该足够大,否则无法接收 “黏在一块的数据包”

CLIENT:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <errno.h>
 6 #include <sys/types.h>
 7 #include <sys/socket.h>
 8 #include <netinet/in.h>
 9 #include <arpa/inet.h>
10 #define ERR_EXIT(m) \
11     do { \
12         perror(m);\
13         exit(EXIT_FAILURE);\
14     }while(0)
15 
16 void do_service(int sockfd);
17 void nano_sleep(double val);
18 
19 int main(int argc, const char *argv[])
20 {
21     int peerfd = socket(PF_INET, SOCK_STREAM, 0);
22     if(peerfd == -1)
23         ERR_EXIT("socket");
24 
25     struct sockaddr_in addr;
26     memset(&addr, 0, sizeof addr);
27     addr.sin_family = AF_INET;
28     addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //localhost
29     addr.sin_port = htons(8976);
30     socklen_t len = sizeof addr;
31     if(connect(peerfd, (struct sockaddr*)&addr, len) == -1)
32         ERR_EXIT("Connect");
33 
34     do_service(peerfd);
35 
36 
37     return 0;
38 }
39 
40 
41 
42 void do_service(int sockfd)
43 {
44     //const int kSize = 1024;
45     #define SIZE 1024
46     char sendbuf[SIZE + 1] = {0};
47     int i;
48     for(i = 0; i < SIZE; ++i)
49         sendbuf[i] = 'a';
50 
51     int cnt = 0; //次数
52     while(1)
53     {
54         int i;
55         for(i = 0; i < 10; ++i)
56         {
57             write(sockfd, sendbuf, SIZE);
58             printf("count = %d, write %d bytes\n", ++cnt, SIZE);
59         }
60         nano_sleep(4);
61 
62         memset(sendbuf, 0, sizeof sendbuf);
63     }
64 }
65 
66 void nano_sleep(double val)
67 {
68     struct timespec tv;
69     tv.tv_sec = val; //取整
70     tv.tv_nsec = (val - tv.tv_sec) * 1000 * 1000 * 1000;
71 
72     int ret;
73     do
74     {
75         ret = nanosleep(&tv, &tv);
76     }while(ret == -1 && errno == EINTR);
77 }

客户端应该 短时间发送 大量的数据, 使server端 处理接收时 造成粘包;

可以看到我们连续发送了 10次 长度为1024 的全是a的 字符串;  看下server端打印如何

clint:

count = 1, write 1024 bytes
count = 2, write 1024 bytes
count = 3, write 1024 bytes
count = 4, write 1024 bytes
count = 5, write 1024 bytes
count = 6, write 1024 bytes
count = 7, write 1024 bytes
count = 8, write 1024 bytes
count = 9, write 1024 bytes
count = 10, write 1024 bytes
count = 11, write 1024 bytes
count = 12, write 1024 bytes
count = 13, write 1024 bytes
count = 14, write 1024 bytes
count = 15, write 1024 bytes
count = 16, write 1024 bytes
count = 17, write 1024 bytes
count = 18, write 1024 bytes
count = 19, write 1024 bytes
count = 20, write 1024 bytes
count = 21, write 1024 bytes
count = 22, write 1024 bytes
count = 23, write 1024 bytes
count = 24, write 1024 bytes
count = 25, write 1024 bytes
count = 26, write 1024 bytes
count = 27, write 1024 bytes
count = 28, write 1024 bytes
count = 29, write 1024 bytes
count = 30, write 1024 bytes
count = 31, write 1024 bytes
count = 32, write 1024 bytes
count = 33, write 1024 bytes
count = 34, write 1024 bytes
count = 35, write 1024 bytes
count = 36, write 1024 bytes
count = 37, write 1024 bytes
count = 38, write 1024 bytes
count = 39, write 1024 bytes
count = 40, write 1024 bytes
count = 41, write 1024 bytes
count = 42, write 1024 bytes
count = 43, write 1024 bytes
count = 44, write 1024 bytes
count = 45, write 1024 bytes
count = 46, write 1024 bytes
count = 47, write 1024 bytes
count = 48, write 1024 bytes
count = 49, write 1024 bytes
count = 50, write 1024 bytes
count = 51, write 1024 bytes
count = 52, write 1024 bytes
count = 53, write 1024 bytes
count = 54, write 1024 bytes
count = 55, write 1024 bytes
count = 56, write 1024 bytes
count = 57, write 1024 bytes
count = 58, write 1024 bytes
count = 59, write 1024 bytes
count = 60, write 1024 bytes

server:

count = 1, receive size = 10240
count = 2, receive size = 1024
count = 3, receive size = 4096
count = 4, receive size = 5120
count = 5, receive size = 1024
count = 6, receive size = 2048
count = 7, receive size = 3072
count = 8, receive size = 2048
count = 9, receive size = 2048
count = 10, receive size = 1024
count = 11, receive size = 5120
count = 12, receive size = 3072
count = 13, receive size = 1024
count = 14, receive size = 1024
count = 15, receive size = 3072
count = 16, receive size = 3072
count = 17, receive size = 3072
count = 18, receive size = 1024
count = 19, receive size = 2048
count = 20, receive size = 4096
count = 21, receive size = 3072

服务器收到的数据包大小不一,出现粘包问题

 

5. 构造粘包问题解决

1. 每当我们发送数据时, 先行将4个字节的 将要发送的数据的 长度信息发送过去

 同理, 通过约定, 接收方也先行接收长度信息, 按照长度信息来接收 后面的 字节流; 这样可以防止数据粘包的问题;

server端

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <errno.h>
 6 #include <sys/types.h>
 7 #include <sys/socket.h>
 8 #include <netinet/in.h>
 9 #include <arpa/inet.h>
10 #include "sysutil.h"
11 #define ERR_EXIT(m) \
12     do { \
13         perror(m);\
14         exit(EXIT_FAILURE);\
15     }while(0)
16 
17 void do_service(int sockfd);
18 
19 int main(int argc, const char *argv[])
20 {
21     int listenfd = socket(PF_INET, SOCK_STREAM, 0);
22     if(listenfd == -1)
23         ERR_EXIT("socket");
24 
25     //地址复用
26     int on = 1;
27     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
28         ERR_EXIT("setsockopt");
29 
30     struct sockaddr_in addr;
31     memset(&addr, 0, sizeof addr);
32     addr.sin_family = AF_INET;
33     addr.sin_addr.s_addr = inet_addr("127.0.0.1");
34     addr.sin_port = htons(8976);
35     if(bind(listenfd, (struct sockaddr*)&addr, sizeof addr) == -1)
36         ERR_EXIT("bind");
37 
38     if(listen(listenfd, SOMAXCONN) == -1)
39         ERR_EXIT("listen");
40 
41     int peerfd = accept(listenfd, NULL, NULL);
42     do_service(peerfd);
43 
44     close(peerfd);
45     close(listenfd);
46 
47     return 0;
48 }
49 
50 
51 
52 void do_service(int sockfd)
53 {
54     int cnt = 0;
55     char recvbuf[1024000] = {0};
56     while(1)
57     {
58         //先接收报文长度
59         int32_t len = recv_int32(sockfd);
60         //接收len长度的报文
61         int nread = readn(sockfd, recvbuf, len);
62         if(nread == -1)
63             ERR_EXIT("readn");
64         else if(nread == 0 || nread < len)
65         {
66             printf("client close ....\n");
67             exit(EXIT_FAILURE);
68         }
69 
70         printf("count = %d, receive size = %d\n", ++cnt, nread);
71         //write(sockfd, recvbuf, strlen(recvbuf));
72         memset(recvbuf, 0, sizeof recvbuf);
73     }
74 }

client:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <errno.h>
 6 #include <sys/types.h>
 7 #include <sys/socket.h>
 8 #include <netinet/in.h>
 9 #include <arpa/inet.h>
10 #include "sysutil.h"
11 #define ERR_EXIT(m) \
12     do { \
13         perror(m);\
14         exit(EXIT_FAILURE);\
15     }while(0)
16 
17 void do_service(int sockfd);
18 
19 int main(int argc, const char *argv[])
20 {
21     int listenfd = socket(PF_INET, SOCK_STREAM, 0);
22     if(listenfd == -1)
23         ERR_EXIT("socket");
24 
25     //地址复用
26     int on = 1;
27     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
28         ERR_EXIT("setsockopt");
29 
30     struct sockaddr_in addr;
31     memset(&addr, 0, sizeof addr);
32     addr.sin_family = AF_INET;
33     addr.sin_addr.s_addr = inet_addr("127.0.0.1");
34     addr.sin_port = htons(8976);
35     if(bind(listenfd, (struct sockaddr*)&addr, sizeof addr) == -1)
36         ERR_EXIT("bind");
37 
38     if(listen(listenfd, SOMAXCONN) == -1)
39         ERR_EXIT("listen");
40 
41     int peerfd = accept(listenfd, NULL, NULL);
42     do_service(peerfd);
43 
44     close(peerfd);
45     close(listenfd);
46 
47     return 0;
48 }
49 
50 
51 
52 void do_service(int sockfd)
53 {
54     int cnt = 0;
55     char recvbuf[1024000] = {0};
56     while(1)
57     {
58         //先接收报文长度
59         int32_t len = recv_int32(sockfd);
60         //接收len长度的报文
61         int nread = readn(sockfd, recvbuf, len);
62         if(nread == -1)
63             ERR_EXIT("readn");
64         else if(nread == 0 || nread < len)
65         {
66             printf("client close ....\n");
67             exit(EXIT_FAILURE);
68         }
69 
70         printf("count = %d, receive size = %d\n", ++cnt, nread);
71         //write(sockfd, recvbuf, strlen(recvbuf));
72         memset(recvbuf, 0, sizeof recvbuf);
73     }
74 }

这种方式 的关键是 在收发送数据前的 send_int32 和 recv_int32 用于发收 4字节长度的 数据长度信息

相当于发送方 先告诉 收方,  我要发送多长的信息, 你按照这个长度收 , 这样 每条信息之间就会条理清晰 不至于“粘包”

两个函数代码如下  (原理相当简答, 不过是一个包装过的writenn 和readn)

 1 void send_int32(int sockfd, int32_t val)
 2 {
 3     //先转化为网络字节序
 4     int32_t tmp = htonl(val);
 5     if(writen(sockfd, &tmp, sizeof(int32_t)) != sizeof(int32_t))
 6         ERR_EXIT("send_int32");
 7 }
 8 
 9 int32_t recv_int32(int sockfd)
10 {
11     int32_t tmp;
12     if(readn(sockfd, &tmp, sizeof(int32_t)) != sizeof(int32_t))
13         ERR_EXIT("recv_int32");
14     return ntohl(tmp); //转化为主机字节序
15 }

2. 另外一种防止 粘包的处理方式更加简答 , 通过以\n当做每条信息之间的 标志;

处理方式在逻辑上更加明了,  事实上各大网络公司也是通过这种方式处理 粘包问题的

 

下面只用修改几行代码即可

把 server端和 client 端中的 do_service逻辑稍加修改即可

client 每次发送的数据缓冲区末尾加一个 \n做标示

 1 void do_service(int sockfd)
 2 {
 3     //const int kSize = 1024;
 4     #define SIZE 1024
 5     char sendbuf[SIZE + 1] = {0};
 6     int i;
 7     for(i = 0; i < SIZE-1; ++i)
 8         sendbuf[i] = 'a';
 9     sendbuf[SIZE - 1] = '\n';
10     // aaaaaa....aaaaa\n
11 
12     int cnt = 0; //次数
13     while(1)
14     {
15         int i;
16         for(i = 0; i < 10; ++i)
17         {
18             //write(sockfd, sendbuf, SIZE);
19             //我们每次发送的报文均以\n作为结尾
20             if(writen(sockfd, sendbuf, SIZE) != SIZE)
21                 ERR_EXIT("writen");
22             
23             printf("count = %d, write %d bytes\n", ++cnt, SIZE);
24         }
25         nano_sleep(4);
26 
27         //memset(sendbuf, 0, sizeof sendbuf);
28     }
29 }

server用 readline即可  因为readline 遇到\n便返回了

 1 void do_service(int sockfd)
 2 {
 3     int cnt = 0;
 4     char recvbuf[1024000] = {0};
 5     while(1)
 6     {
 7         int nread = readline(sockfd, recvbuf, sizeof recvbuf);
 8         if(nread == -1)
 9             ERR_EXIT("readn");
10         else if(nread == 0)
11         {
12             printf("client close ....\n");
13             exit(EXIT_FAILURE);
14         }
15 
16         printf("count = %d, receive size = %d\n", ++cnt, nread);
17         //write(sockfd, recvbuf, strlen(recvbuf));
18         memset(recvbuf, 0, sizeof recvbuf);
19     }
20 }

 

posted @ 2020-02-13 22:52  坚持,每天进步一点点  阅读(673)  评论(0编辑  收藏  举报