zoukankan      html  css  js  c++  java
  • 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 ...
    ");
    67             exit(EXIT_SUCCESS);
    68         }
    69 
    70         printf("count = %d, receive size = %d
    ", ++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
    ", ++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 ....
    ");
    67             exit(EXIT_FAILURE);
    68         }
    69 
    70         printf("count = %d, receive size = %d
    ", ++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 ....
    ");
    67             exit(EXIT_FAILURE);
    68         }
    69 
    70         printf("count = %d, receive size = %d
    ", ++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. 另外一种防止 粘包的处理方式更加简答 , 通过以 当做每条信息之间的 标志;

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

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

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

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

    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] = '
    ';
        // aaaaaa....aaaaa
    
    
        int cnt = 0; //次数
        while(1)
        {
            int i;
            for(i = 0; i < 10; ++i)
            {
                //write(sockfd, sendbuf, SIZE);
                //我们每次发送的报文均以
    作为结尾
                if(writen(sockfd, sendbuf, SIZE) != SIZE)
                    ERR_EXIT("writen");
                
                printf("count = %d, write %d bytes
    ", ++cnt, SIZE);
            }
            nano_sleep(4);
    
            //memset(sendbuf, 0, sizeof sendbuf);
        }
    }

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

     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 ....
    ");
    13             exit(EXIT_FAILURE);
    14         }
    15 
    16         printf("count = %d, receive size = %d
    ", ++cnt, nread);
    17         //write(sockfd, recvbuf, strlen(recvbuf));
    18         memset(recvbuf, 0, sizeof recvbuf);
    19     }
    20 }

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

  • 相关阅读:
    CodeForces 58C Trees
    【转】二分匹配题集
    HDU2604 Queuing
    HDU1281 棋盘游戏
    HDU3360 National Treasures
    HDU2444 The Accomodation of Students
    HDU1498 50 years, 50 colors
    HDU1068 Girls and Boys
    【转】常用的latex宏包
    【转】网络流题集
  • 原文地址:https://www.cnblogs.com/DLzhang/p/4025180.html
Copyright © 2011-2022 走看看