2018-06-13

炮打TCP – 关于一而再再而三的粘包拆包问题的大字报

Views: 16415 | 20 Comments

TCP 所谓的粘包和拆包问题,是技术圈里最奇葩的问题之一!

一而再,再而三,就跟傻逼的中国球迷支持中国足球队一样,前赴后继。有时候同一个人多次在犯同一个错误,有时候是前脚一个犯错了后脚又来一个还犯同样的错。即使是最优秀的程序员,也会在这个问题上面栽跟头,思维甚至很难转过弯,很久才能意识到自己的错误。而低水平的程序员就更不用说了,很多人到死都没有理解这个错误并解决掉,只是逃掉了而已。

我们固然可以认为原因是某些人学艺不精,但那么多的人,其中包括无数的优秀程序员在 TCP 粘包和拆包问题在犯错误,难道我们不能说,这其实是 TCP 自身的原因吗?

在我看来,这个问题的出现,原因就在于 TCP 协议是有原罪的 -- 也就是 TCP 协议所谓的“流式”协议。所以,我要炮轰 TCP!

经过几十年的验证,除了几数几个网络协议会用到 TCP 所谓的流式特性之外,没有任何应用协议使用流式特性。我们必须承认,所有的应用层协议都是基于报文的协议,而不是流式协议。而某些名字中带有”流(Stream)"字样的协议,如 RTP,流媒体等,其本质是无数小体积的报文按顺序拼接而成,根本就和 TCP 的流式没有任何关系!

那么我们就可以确定,数据的本质是报文,流数据是某类报文数据的一种伪称。事实,TCP 的流就是基于 IP 报文的。

因为“流”是一个伪抽象的概念,所以流式协议是违反人的天性和事物的内在逻辑的。万物的本质是报文。这因为如此,”流”所引出的粘包拆包问题,就必然会一而再,再而三,大量地出现。

炮轰之后,我们要怎么解决问题呢?

由于 TCP 协议已经成为事实上的基础,所以淘汰掉 TCP 是不可想象的。我们要做的是,找到正确的编程代码,解决粘包拆包问题。经过无数人的探索,以及无数人一次又一次重复的愚蠢错误的反证,我发现了解决 TCP 粘包和拆包问题只有一条路径,没有第二条!我断言,所有和我的解决方案不同的代码,都是错误的。

彻底解决 TCP 粘包和拆包问题的代码架构如下:

char tmp[];
Buffer buffer;
// 网络循环:必须在一个循环中读取网络,因为网络数据是源源不断的。
while(1){
    // 从TCP流中读取不定长度的一段流数据,不能保证读到的数据是你期望的长度
    tcp.read(tmp);
    // 将这段流数据和之前收到的流数据拼接到一起
    buffer.append(tmp);
    // 解析循环:必须在一个循环中解析报文,应对所谓的粘包
    while(1){
        // 尝试解析报文
        msg = parse(buffer);
        if(!msg){
            // 报文还没有准备好,糟糕,我们遇到拆包了!跳出解析循环,继续读网络。
            break;
        }
        // 将解析过的报文对应的流数据清除
        buffer.remove(msg.length);
        // 业务处理
        process(msg);
    }
}

这段代码是终极地解决 TCP 粘包和拆包问题的代码!

这段代码之所以正确,是因为它包含了两个循环:网络循环和解析循环。

网络循环用于从 TCP socket 中读取流式数据,每一次读取到的数据的长度是不可预期,也就是,读取到的数据长短不一,无法保证,这就是所谓“流式”引出的问题。

而解析循环的功能是从拼接后流数据中,尝试解析出多个报文。注意,是多个报文,不是一个。因为所谓的粘包问题存在,所以可能是多个,而不是一个。如果解析不成功,那说明是遇到了拆包问题,我们继续读网络数据。

你只需要死记硬背上面的正确代码即可。不死记硬背也一样,最终你还是要得出和我相同的结论写出和我一样的代码。那么,何不现在就死记硬背呢?

最后,附上经典的错误代码:

tcp.read(tmp, HEADER_LEN);
header = parse_header(tmp);
tcp.read(tmp, header.body_len);
body = parse(tmp);

这样的代码当然是错误的,这么简单代码怎么可能是对的?如果对了,TCP 还是 TCP 吗?

如果你认为本文有用,请关注这个 GitHub 项目:https://github.com/ideawu/FUCK_TCP 让更多人一起炮打 TCP!

该项目还提供了模拟粘包和拆包的代码,你如果不信邪,可以写一个自己的 client 试试。

Related posts:

  1. 关于TCP粘包和拆包的终极解答
  2. 经典的 TCP socket 读取报文错误
  3. 通过 HTTP POST 发送二进制数据
  4. 在Linux进行IO的正确姿势
  5. 使用 Channel 进行可靠传输
Posted by ideawu at 2018-06-13 15:05:11

20 Responses to "炮打TCP – 关于一而再再而三的粘包拆包问题的大字报"

  • 流式协议保证数据段在发送端和接受端的顺序,上层协议也是基于这个前提 Reply
  • 客户端tcp不这么写还能怎么写。还以为是服务器端发送和接收的技巧。 Reply
  • TCP 本身是“字节流”协议,不存在“包”的概念。

    兵无常势,水无常形;能因敌变化而取胜者,谓之神。

    水没有自己的形状,你把他放杯子里,它就是杯子的形状,你把它放碗里,它就是碗的形状。

    TCP也一样,你用什么样的“分包”协议,TCP就是什么样的“包” Reply
  • 看看golang的 bufio.SplitFunc 函数吧!!!!哈哈哈
    你再Fuck_TCP,但是你也没有通用解决方案啊! 这个SplitFunc 接口就是通用解决方案,切包!

    其实 流不流的,我都不管 ,我只要 切到一定规则的就可以了,比如http 就是 \n\n 前是http-header,里面有body的长度,读取完毕,整个http就读取完毕,一个packet的长度还是不固定的; Reply
  • 不能这么说,HTTP/FTP/SMTP/POP3/IMAP,更不要说TELNET/SSH,这些都用到了流特性。
    只是自己开发的应用协议设计时使用错了。
    RTP/RTSP本来就是为了减少TCP的开销,一般更适合udp更好。 Reply
    @黄亮: HTTP真的没有用到流特性,只是使用了TCP协议而已。估计你说的“用到了”就是这个意思吧? Reply
  • 发送方发送数据:
    abc#f1ndwh7#efg#f1ndwh7#hij#f1ndwh7#
    接收方读取数据:
    tmp = abc#
    无法解析数据(解析规则为末尾必须为#f1ndwh7#),等待下一次读取
    读取数据:
    tmp = abc#f1ndwh7#e
    解析数据:
    得到符合要求数据abc,进行业务处理
    删除已使用数据abc#f1ndwh7#:
    tmp = e
    读取数据:
    tmp = efg#f1ndwh7#hij#f1ndwh7#
    解析数据:
    得到符合要求数据efg和hij,进行业务处理
    删除已使用数据efg#f1ndwh7#hij#f1ndwh7#
    tmp =
    等待读取数据:

    是不是可以理解为上诉这样一个例子? Reply
    @f1ndwh7: 是的。 Reply
    @ideawu:
    tcp.read(tmp, LEN)的问题在于无法精确的读出相应长度的数据(http://www.ideawu.net/blog/archives/891.html),所以如果数据不完整那么解析时必然会出现异常。

    为什么错误代码能正常使用,是不是因为通常情况下需要解析处理的数据并不多,导致每次读取都能"恰好"读取到完整数据,从而可以正确解析执行后续业务逻辑。

    看了你的博文学到挺多,感谢 Reply
  • Buffer buffer;缓存也要有最大的限制,不然网络攻击,势必造成内存溢出,

    既然buffer已经有最大限制了,意味着需要结构化的字节流也要限制,
    即:业务`包` < buffer大小 < MAX_BUFFER_SIZE && socket buffer大小 < buffer大小

    关于那些TCP粘包、黏包的说法,我觉他们就是这些东西认识不清楚。 Reply
  • 关键问题是解析部分需要写成状态机或者用一个coroutine或promise或者某种monad之类的结构包起来 Reply
  • 你的喷点是啥?TCP不能说是流协议么?

    人家RFC 793原文描述是
    The TCP is able to transfer a continuous stream of octets in each direction between its users by packaging some number of octets into segments for transmission through the internet system

    注意这里的词用的是 “is able to” 能处理的意思,不是"TCP is stream protocol"吧? Reply
    @tkboy: 你看不懂吗? Reply
    @ideawu: 这句话给人的感觉不是像讨论技术,而是就是为了喷而喷哇。如果是这样那讨论结束了。 Reply
    @ideawu: 你这话说的,我上面的描述是看不懂么?是哪里描述的不对么?为啥讨论技术的方式不是直入话题说出自己的说法以及反驳点,而是说这种没分量的话? Reply
  • 为什么使用循环解析报文能避免粘包? Reply
    @Levi: 这个问题的本质是: 每一个packet的包长度是不固定的,所以你要确定你读取到一定长度的就行了. 其实问题很简单,但是犯错误的人太多, golang挺好,把这底层封装起来,给一个通用接口,你来实现就好了. 避免更多人犯错误. Reply
    @Levi: 更正:不是避免,是应对。 Reply

« [1][2] » 1/2

Leave a Comment