Linux学习: TCP粘包问题
TCP协议下:
当发送数据过长过短, 或缓冲区大小问题, 导致出现了所谓的 TCP“粘包”问题, 这是我们的俗称, TCP是流模式,并不是包;
现象解释:
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
好了, 根据上述的理论 我们自己人为制造一起 ”粘包“
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端打印如何
count = 1, receive size = 1024 count = 2, receive size = 1024 count = 3, receive size = 1024 count = 4, receive size = 1024 count = 5, receive size = 1024 count = 6, receive size = 5120 count = 7, receive size = 10240 count = 8, receive size = 10240 count = 9, receive size = 10240
可以看到, 当第6次读取时便出现了粘包; 数据出现了相连的问题;
而我们的客户端 是均匀的每次发送1024字节的数据
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
显然不是我们发送数据时造成的问题, 而是TCP本身的缺陷。
下面有两种解决“粘包的问题” 我们来介绍一下
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做标示
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的效率问题。