linux网络编程之socket(五):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、包尾加\r\n(ftp) 3、包头加上包体长度

4、更复杂的应用层协议

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

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

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函数更改如下:

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\n");
            break;
        }

        n = ntohl(recvbuf.len);
        ret = readn(conn, recvbuf.buf, n);
        if (ret == -1)
            ERR_EXIT("read error");
        if (ret < n)   //客户端关闭
        {
            printf("client close\n");
            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));

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ytkah

群用户通过微信小程序可以更好地协作了

  今天,小程序向开发者开放了群ID的接口能力。简单地说,就是当你把小程序分享在群聊中,被点击后开发者可获取群ID和群名称,也方便更好地针对群场景提供个性化服务...

3725
来自专栏草根专栏

用ASP.NET Core 2.0 建立规范的 REST API -- GET 和 POST

本文所需的一些预备知识可以看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblog...

1081
来自专栏Java 技术分享

Ajax 学习总结

3367
来自专栏大内老A

使命必达: 深入剖析WCF的可靠会话[编程篇](上)

在《实例篇》给出的例子中,我实际上是通过对终结点的绑定进行相应的配置让整个消息的交换过程在一个可靠会话中进行,进而实现可靠消息传输的目的。由于整个可靠会话的机制...

1715
来自专栏程序员互动联盟

【专业技术】Android webkit处理汉字编码问题

在XX项目中解决android webkit处理汉字编码问题的总结 1.问题: 服务器通过302重定向方式发送给客户端重定向地址,地址中的汉字采用原数据方式发送...

3166
来自专栏Linyb极客之路

RPC框架设计和调用详解

RPC是远程调用过程的简写,是一个协议,处于网络通信协议的第五层:会话层,其下就是TCP/IP协议,在建立在其基础上的通信会话协议。RPC定义了交互的模式,而...

1002
来自专栏互联网杂技

判断图片是否加载完成

有时需要获取图片的尺寸,这需要在图片加载完成以后才可以。有三种方式实现,下面一一介绍。 1、load事件 <!DOCTYPE HTML> <html> <hea...

3117
来自专栏HTML5学堂

2016.06 第二周 群问题分享

HTML+CSS display:none与visibility:hidden相同点与不同点 2016.06.06~2016.06.10 核心概念 displa...

3328
来自专栏javathings

Spring Boot 中,过滤器和拦截器的区别是什么?

过滤器和拦截器有相似之处,都能对 Servlet 请求二次加工。但是过滤器并不是 SpringBoot 规范中的概念,事实上,过滤器是 Servlet 规范中的...

1032
来自专栏程序员的知识天地

新鲜出炉的8月前端面试题

题目的答案提供了一个思考的方向,答案不一定正确全面,有错误的地方欢迎大家请在评论中指出,共同进步。

612

扫码关注云+社区