博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux学习: TCP粘包问题
阅读量:6471 次
发布时间:2019-06-23

本文共 10507 字,大约阅读时间需要 35 分钟。

TCP协议下:

当发送数据过长过短, 或缓冲区大小问题, 导致出现了所谓的 TCP“粘包”问题, 这是我们的俗称, TCP是流模式,并不是包;

 

现象解释:

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。    

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。 

 

好了, 根据上述的理论 我们自己人为制造一起 ”粘包“

 

server

1 #include 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
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 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
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"); //localhost29 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 102446 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 do74 {75 ret = nanosleep(&tv, &tv);76 }while(ret == -1 && errno == EINTR);77 }

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

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

count = 1, receive size = 1024count = 2, receive size = 1024count = 3, receive size = 1024count = 4, receive size = 1024count = 5, receive size = 1024count = 6, receive size = 5120count = 7, receive size = 10240count = 8, receive size = 10240count = 9, receive size = 10240

可以看到, 当第6次读取时便出现了粘包; 数据出现了相连的问题;

 

而我们的客户端 是均匀的每次发送1024字节的数据

count = 1, write 1024 bytescount = 2, write 1024 bytescount = 3, write 1024 bytescount = 4, write 1024 bytescount = 5, write 1024 bytescount = 6, write 1024 bytescount = 7, write 1024 bytescount = 8, write 1024 bytescount = 9, write 1024 bytescount = 10, write 1024 bytescount = 11, write 1024 bytescount = 12, write 1024 bytescount = 13, write 1024 bytescount = 14, write 1024 bytescount = 15, write 1024 bytescount = 16, write 1024 bytescount = 17, write 1024 bytescount = 18, write 1024 bytescount = 19, write 1024 bytescount = 20, write 1024 bytescount = 21, write 1024 bytescount = 22, write 1024 bytescount = 23, write 1024 bytescount = 24, write 1024 bytescount = 25, write 1024 bytescount = 26, write 1024 bytescount = 27, write 1024 bytescount = 28, write 1024 bytescount = 29, write 1024 bytescount = 30, write 1024 bytescount = 31, write 1024 bytescount = 32, write 1024 bytescount = 33, write 1024 bytescount = 34, write 1024 bytescount = 35, write 1024 bytescount = 36, write 1024 bytescount = 37, write 1024 bytescount = 38, write 1024 bytescount = 39, write 1024 bytescount = 40, write 1024 bytes

显然不是我们发送数据时造成的问题, 而是TCP本身的缺陷。

 

下面有两种解决“粘包的问题” 我们来介绍一下

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

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

server端

1 #include 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
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 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
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做标示

void do_service(int sockfd){    //const int kSize = 1024;    #define SIZE 1024    char sendbuf[SIZE + 1] = {
0}; int i; for(i = 0; i < SIZE-1; ++i) sendbuf[i] = 'a'; sendbuf[SIZE - 1] = '\n'; // aaaaaa....aaaaa\n int cnt = 0; //次数 while(1) { int i; for(i = 0; i < 10; ++i) { //write(sockfd, sendbuf, SIZE); //我们每次发送的报文均以\n作为结尾 if(writen(sockfd, sendbuf, SIZE) != SIZE) ERR_EXIT("writen"); printf("count = %d, write %d bytes\n", ++cnt, SIZE); } nano_sleep(4); //memset(sendbuf, 0, sizeof sendbuf); }}

 

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 }

 

 

以上代码均通过测试无误, 暂时解决了 粘包问题, 上述代码依然存在缺陷, readline的效率问题。

转载于:https://www.cnblogs.com/DLzhang/p/4025180.html

你可能感兴趣的文章
【学习笔记】阿里云Centos7.4下配置Nginx
查看>>
VuePress手把手一小時快速踩坑
查看>>
dnsmasq安装使用和体验
查看>>
学习constructor和instanceof的区别
查看>>
Vijos P1881 闪烁的星星
查看>>
ABP理论学习之领域服务
查看>>
Qt 控制watchdog app hacking
查看>>
让所有IE支持HTML5的解决方案
查看>>
RDD之五:Key-Value型Transformation算子
查看>>
Windows 搭建Hadoop 2.7.3开发环境
查看>>
python操作mysql数据库实现增删改查
查看>>
percona 5.7.11root初始密码设置
查看>>
Cognitive Security的异常检测技术
查看>>
Impress.js上手 - 抛开PPT、制作Web 3D幻灯片放映
查看>>
生活杂事--度过十一中秋
查看>>
Pyrex也许是一个好东西
查看>>
Java内部类总结
查看>>
WINFORM WPF字体颜色相互转换
查看>>
能力不是仅靠原始积累(三)
查看>>
实战:使用终端服务网关访问终端服务
查看>>