zoukankan      html  css  js  c++  java
  • tcp流协议产生的粘包问题和解决方案

    我们在前面曾经说过,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区,所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。


    一、粘包问题可以用下图来表示:


    假设主机A send了两条消息M1和M2 各10k 给主机B,由于主机B一次提取的字节数是不确定的,接收方提取数据的情况可能是:

    • 一次性提取20k 数据
    • 分两次提取,第一次5k,第二次15k
    • 分两次提取,第一次15k,第二次5k
    • 分两次提取,第一次10k,第二次10k
    • 分三次提取,第一次6k,第二次8k,第三次6k
    • 其他任何可能


    二、粘包问题的解决方案

    本质上是要在应用层维护消息与消息的边界(下文的“包”可以认为是“消息”)
    1、定长包
    2、包尾加 (ftp)
    3、包头加上包体长度

    4、更复杂的应用层协议


    对于条目2,缺点是如果消息本身含有 字符,则也分不清消息的边界。

    对于条目1,即我们需要发送和接收定长包。因为TCP协议是面向流的,read和write调用的返回值往往小于参数指定的字节数。对于read调用(套接字标志为阻塞),如果接收缓冲区中有20字节,请求读100个字节,就会返回20。对于write调用,如果请求写100个字节,而发送缓冲区中只有20个字节的空闲位置,那么write会阻塞,直到把100个字节全部交给发送缓冲区才返回。为避免这些情况干扰主程序的逻辑,确保读写我们所请求的字节数,我们实现了两个包装函数readn和writen,如下所示。

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
     
    ssize_t readn(int fd, void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char *)buf;

        while (nleft > 0)
        {

            if ((nread = read(fd, bufp, nleft)) < 0)
            {

                if (errno == EINTR)
                    continue;
                return -1;
            }

            else if (nread == 0) //对方关闭或者已经读到eof
                return count - nleft;

            bufp += nread;
            nleft -= nread;
        }

        return count;
    }

    ssize_t writen(int fd, const void *buf, size_t count)
    {
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char *)buf;

        while (nleft > 0)
        {

            if ((nwritten = write(fd, bufp, nleft)) < 0)
            {

                if (errno == EINTR)
                    continue;
                return -1;
            }

            else if (nwritten == 0)
                continue;

            bufp += nwritten;
            nleft -= nwritten;
        }

        return count;

    }


    需要注意的是一旦在我们的客户端/服务器程序中使用了这两个函数,则每次读取和写入的大小应该是一致的,比如设置为1024个字节,但定长包的问题在于不能根据实际情况读取数据,可能会造成网络阻塞,比如现在我们只是敲入了几个字符,却还是得发送1024个字节,造成极大的空间浪费。

    此时条目3是比较好的解决办法,其实也可以算是自定义的一种简单应用层协议。比如我们可以自定义一个包体结构

    struct packet {
        int len;
        char buf[1024];
    };

    先接收固定的4个字节,从中得知实际数据的长度n,再调用readn 读取n个字符,这样数据包之间有了界定,且不用发送定长包浪费网络资源,是比较好的解决方案。服务器端在前面的fork程序的基础上把do_service函数更改如下:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    void do_service(int conn)
    {
        struct packet recvbuf;
        int n;
        while (1)
        {
            memset(&recvbuf, 0, sizeof(recvbuf));
            int ret = readn(conn, &recvbuf.len, 4);
            if (ret == -1)
                ERR_EXIT("read error");
            else if (ret < 4)   //客户端关闭
            {
                printf("client close ");
                break;
            }

            n = ntohl(recvbuf.len);
            ret = readn(conn, recvbuf.buf, n);
            if (ret == -1)
                ERR_EXIT("read error");
            if (ret < n)   //客户端关闭
            {
                printf("client close ");
                break;
            }

            fputs(recvbuf.buf, stdout);
            writen(conn, &recvbuf, 4 + n);
        }
    }


    注意:客户端是直接将整个结构体发送过来,能这样分步解包的前提是结构体没有填充字段。


    客户端程序的修改与上类似,不再赘述。

    对于条目4,举例如 如TLV 编解码格式

    struct TLV
    {
        uint8_t tag;
        uint16_t len;
        char value[0];
    }__attribute__((packed));


    度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式

    空间使用情况。

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    int main(void)
    {
        char *szMsg = "aaaaaaaaa";
        cout << sizeof(TLV) << endl; //the size of TLV
        uint16_t len = strlen(szMsg) + 1;
        struct TLV *pTLV;
        pTLV = (struct TLV *)malloc(sizeof(struct TLV) + sizeof(char) * len);
        pTLV->tag = 0x2;
        pTLV->len = len;
        memcpy(pTLV->value, szMsg, len);
        cout << pTLV->value << endl;
        free(pTLV);
        pTLV = NULL;
        return 0;
    }


    参考:

    《Linux C 编程一站式学习》

    《TCP/IP详解 卷一》

    《UNP》

    http://www.cppblog.com/aa19870406/archive/2012/06/14/178803.html

    如何定义变长的TLV结构体?

    TLV是一种常用的用于通信的结构体格式。T表示tag,L表示length,V表示value。其中T和L是固定大小的,V是可变大小,L表示的是V的长度。通常用于结构化网络通信中的数据流。如0x3 3 'aa',0x3 5 'aaaa',其中0x3表示tag的值,3 or 5表示的是后面的字符串的长度。由于V是可变长度的,所以在定义TLV结构时,需要将V定义成为可变大小。可定义如下:

    struct TLV
    {
        uint8_t tag;
        uint16_t len;
        char value[0];
    }__attribute__((packed));


    注意value分配的是0大小,最后一个成员为可变长的数组,对于TLV(Type-Length-Value)形式的结构,或者其他需要变长度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式访问,释放时,直接把整个结构体free掉就可以了。__attribute__(packed)用来强制不对struct TLV进行4字节对齐,目的是为了获取真实的TLV的空间使用情况。

    int main()
    {
        char *szMsg = "aaaaaaaaa";
        cout << sizeof(TLV) << endl; //the size of TLV
        uint16_t len = strlen(szMsg) + 1;
        struct TLV *pTLV;
        pTLV = (struct TLV*)malloc(sizeof(struct TLV) + sizeof(char)*len);
        pTLV->tag = 0x2;
        pTLV->len = len;
        memcpy(pTLV->value, szMsg, len);
        cout << pTLV->value << endl;
        free(pTLV);
        pTLV = NULL;
        return 0;
    }


    这里有关于设置变长TLV的详细说明:http://www.douban.com/note/213324857/
    这里有一个问题,如何实现嵌套TLV结构呢?大家有什么好的思路吗?欢迎交流
    简单实现了一下嵌套TLV,不知道有没有问题。

    #include <iostream>
    using namespace std;

    struct TLVNODE
    {
        uint8_t tag;
        uint16_t len;
        char value[0];
    }__attribute__ ((packed));

    struct TLV
    {
        int hei;
        uint8_t tag;
        uint16_t len;
        struct TLVNODE value[0];
    } __attribute__ ((packed));

    int main()
    {
        //char *szMsg = "aaaaaaaaaaa";
        cout << sizeof(TLV) << endl;
        //uint16_t len = strlen(szMsg) + 1;

        char *szNodeMsg = "bbbbbbbbbb";
        uint16_t nodelen = strlen(szNodeMsg) + 1;
        struct TLVNODE *pNode = (struct TLVNODE *) malloc(sizeof(struct TLVNODE) + sizeof(char)*nodelen); 
        pNode->tag = 0x3;
        pNode->len = nodelen;
        memcpy(pNode->value, szNodeMsg, nodelen);


        struct TLV *pTlv;
        uint16_t nodeSize = sizeof(struct TLVNODE) + sizeof(char)*nodelen;
        pTlv = (struct TLV*)malloc(sizeof(struct TLV) + nodeSize);
        pTlv->tag = 0x2;
        pTlv->len = nodeSize;
    //    pTlv->value[0] = (struct TLVNODE)*pNode;
        memcpy(pTlv->value, pNode, nodeSize);
        free(pNode);
        pNode = NULL;
        cout << sizeof(*pTlv) << endl;
        /*for (int i = 0; i < len; ++i)
        {
            pTlv->value[i] = szMsg[i]; 
        }*/

        /*memcpy(pTlv->value, szMsg, len);*/
        //cout << pTlv->value << endl;
        free(pTlv);
        pTlv = NULL;
        return 0;
    }
  • 相关阅读:
    日志
    JAVA字符串类
    JAVA包
    JAVA面向对象
    JAVA数组
    JAVA循环结构
    JAVA程序调试
    JAVA条件判断
    JAVA算术运算符
    JAVA数据类型
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8472872.html
Copyright © 2011-2022 走看看