程序员行业有一些奇怪的错误的观点(误解),这些误解非常之流行,而且持有这些错误观点的人经常言之凿凿,打死也不相信自己有错,实在让人啼笑皆非。究其原因,还是因为这些错误观点所对应的正确观点不符合人的正常思维习惯,是扭曲人的直观感受的。
有两个错误观点非常之经典,一而再,再而三的出现,就跟韭菜一样,割不完,还越长越多。一是经典的"服务器最多65536个连接"误解,打开链接看介绍。另一个就是这里要讲的TCP"粘包"和"拆包"问题。
基于前面的思路,我们先介绍人的正常思维习惯,然后再介绍扭曲的正确观点,这样你就会印象深刻了。
首先,人的正常思维习惯是,数据传输应该是基于报文的,人们不论是对话还是写信,肯定是一句一句的话或者一封一封的信,这就是所谓的报文。而 TCP 是什么东西呢?TCP 是一种流式协议,简单说,你使用它的时候,根本就没有所谓的报文,无论是聊天说的话还是发的图片,对于 TCP 来说,通通都是没有边界的,TCP 根本不认识这些东西,不知道你按换行时是一句话,你发的图片是一张图片,所有的东西都是数据流没有任何边界。这显然不符合人的正常思维,是扭曲的。
而无论是人的思维,还是实际的东西,一定是要有边界的。这就有矛盾了。
所以,对于程序员,在使用 TCP 之前,你必须进行报文协议设计。要么你使用别人设计好的报文协议,要么你自己设计。如果你自己不设计,也不使用别人设计好的,那么你肯定错了,一定会遇到所谓的粘包和拆包。
关于报文协议设计,这篇文章有介绍 http://www.ideawu.net/blog/archives/429.html
一般的程序员当然不愿意自己设计,一是没能力,二是不需要重新发明。那么设计完之后呢,必然是实现。因为自身水平的问题,还是会遇到 TCP 粘包和拆包问题。所以,我的建议是,不要自己去实现,应该去使用别人已经写好的代码。
接下来讲一讲如果自己直接使用 TCP 的接口,为什么会遇到粘包和拆包问题呢?TCP 的接口简单说只有两个:
send(data, length); recv(buff);
不懂的人会基于人的正常思维想当然地认为 send() 就是发送报文,recv() 就是接收报文。但是,前面已经提到了,TCP 是没有所谓的报文边界的,所以你错了。为什么错?因为 send() 和 recv() 不是一一对应的。也就是说,send() 的调用次数和 recv() 的调用次数是独立的,有时是相等的(你在自己机器上和内网测试时基本都是相等的),有时是不相等的(互联网上非常容易出现)。
现在明白了吧,TCP 的 send() 和 recv() 不是一一对应的,理所当然会粘应用层的包,拆应用层的包。明白了之后怎么解决?简单,用别人封装好的代码。或者,你自己慢慢琢磨去吧!
正确地从TCP socket中读取报文的代码必须长这个样子,如果不长这个样子,就是错的!
char recv_buf[]; Buffer buffer; // 网络循环:必须在一个循环中读取网络,因为网络数据是源源不断的。 while(1){ // 从TCP流中读取不定长度的一段流数据,不能保证读到的数据是你期望的长度 tcp.read(recv_buf); // 将这段流数据和之前收到的流数据拼接到一起 buffer.append(recv_buf); // 解析循环:必须在一个循环中解析报文,避免所谓的粘包 while(1){ // 尝试解析报文 msg = parse(buffer); if(!msg){ // 报文还没有准备好,糟糕,我们遇到拆包了!跳出解析循环,继续读网络。 break; } // 将解析过的报文对应的流数据清除 buffer.remove(msg.length); // 业务处理 process(msg); } }
这段代码包含两个循环:网络循环和解析循环。
注意了!如果你的代码不长这个样子,绝对是错的!一定是错的!不要狡辩!正确的做法只有一种,错误的做法成千上万。
如果你想真正的解决所谓的TCP粘包和拆包问题,你写的代码必须和我的一样。如果不一样,就是错的,说明你根本就没弄懂。