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的效率问题。

  • 相关阅读:
    leetcode 576. Out of Boundary Paths 、688. Knight Probability in Chessboard
    leetcode 129. Sum Root to Leaf Numbers
    leetcode 542. 01 Matrix 、663. Walls and Gates(lintcode) 、773. Sliding Puzzle 、803. Shortest Distance from All Buildings
    leetcode 402. Remove K Digits 、321. Create Maximum Number
    leetcode 139. Word Break 、140. Word Break II
    leetcode 329. Longest Increasing Path in a Matrix
    leetcode 334. Increasing Triplet Subsequence
    leetcode 403. Frog Jump
    android中webView加载H5,JS不能调用问题的解决
    通过nginx中转获取不到IP的问题解决
  • 原文地址:https://www.cnblogs.com/DLzhang/p/4025180.html
Copyright © 2011-2022 走看看