原文转载自:http://zgc168.iteye.com/blog/1880620,感谢原作者。
为什么TCP 会粘包
前几天,调试mina的TCP通信, 第一个协议包解析正常,第二个数据包不完整。为什么会这样吗,我们用mina这样通信框架,还会出现这种问题?带者问题,我们先分析一下问题。
提到通信, 我们面临都通信协议,数据协议的选择。 通信协议我们可选择TCP/UDP:
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
由于TCP无消息保护边界, 需要在消息接收端处理消息边界问题。也就是为什么我们以前使用UDP没有此问题。反而使用TCP后,出现少包的现象。
粘包的分析
上面说了原理,但可能有人使用TCP通信会出现多包/少包,而一些人不会。那么我们具体分析一下,少包,多包的情况。
正常情况,发送及时每消息发送,接收也不繁忙,及时处理掉消息。像UDP一样.
发送粘包,多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包. 这种情况和客户端处理繁忙,接收缓存区积压,用户一次从接收缓存区多个数据包的接收端处理一样。
发送粘包或接收缓存区积压,但用户缓冲区大于接收缓存区数据包总大小。此时需要考虑处理一次处理多数据包的情况,但每个数据包都是完整的。
发送粘包或接收缓存区积压, 用户缓存区是数据包大小的整数倍。此时需要考虑处理一次处理多数据包的情况,但每个数据包都是完整的。
发送粘包或接收缓存区积压, 用户缓存区不是数据包大小的整数倍。此时需要考虑处理一次处理多数据包的情况,同时也需要考虑数据包不完整。
我们的情况就属于最后一种,发生了数据包不完整的情况。
啰嗦了这么多,总结 一下, 就两种情况下会发生粘包。
发送端需要等缓冲区满才发送出去,造成粘包
接收方不及时接收缓冲区的包,造成多个包接收
如何应对
先卖个关子, 不是所有的粘包都需要处理。 我们先列举一下,免得在编码过程中,因为知道了粘包的情况下,都处理粘包。
连续的数据流不需要处理。如一个在线视频,它是一个连续不断的流, 不需要考虑分包。
每发一个消息,建一次连接的情况。
发送端使用了TCP强制数据立即传送的操作指令push。
UDP, 前面已说明白了。在这在强调一下,UDP不需要处理,免的忘记了。
如果用socket编写编程的话, 我就不多说我, 可参考下面的资料:
Grizzly: http://grizzly.java.net/nonav/docs/docbkx2.0/html/coreframework-samples.html User Guide 第二章的样例:解析收到的消息。
xSocket:http://xsocket.sourceforge.net/core/tutorial/V2/TutorialCore.htm 第 18 节。
Netty: http://netty.io/docs/3.2.6.Final/api/org/jboss/netty/handler/codec/frame/FrameDecoder.html FrameDecoder 的API 文档。Netty 抽象了一个“消息桢解码器”的类来处理这些。
Mina 2:http://mina.apache.org/chapter-11-codec-filter.html
Mina 2:如果En文不好的话, 可参考http://freemart.iteye.com/blog/836654。 它在判断包是否完整时,有个小缺陷,它没使用IOBuffer的prefixedDataAvailable。但注释写的比较好。
把官网上的代码,也在这展示一下。
- public class ImageResponseDecoder extends CumulativeProtocolDecoder {
- /**
- * 返回值的解释:
- * 1、false, 继续接收下一批数据,有两种情形,如缓冲区数据刚刚就是一个完整消息,或不够一条消息时。如果不够一条消息,那么会将下一批数据和剩余消息进行合并
- * 2、true, 当缓冲区的消息多于一条消息时,剩余消息会再会推送至doDecode
- */
- protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)throws Exception {
- //发送数据时,头四个字节记录了消息的长度。 此方法会读四个字节,并和实现流长度对比。返回前,将流reset.
- if (in.prefixedDataAvailable(4)) {
- int length = in.getInt();
- byte [] bytes =newbyte[length];
- in.get(bytes);
- ByteArrayInputStream bais =new ByteArrayInputStream(bytes);
- BufferedImage image = ImageIO.read(bais);
- out.write(image);
- return true;//如果读取内容后还粘了包,系统会自动处理。
- }else{
- returnfalse;//继续接收数据,以待数据完整
- }
- }
- }
再总结一下处理流程: 就发送数据时,包开始写入消息长度n, 当接收到的缓存区数据m,各处理流程如下:
1)若n<m,则表明数据流包含多包数据,从其头部截取n个字节存入临时缓冲区,剩余部分数据依此继续循环处理,直至结束。或n>m
2)若n=m,则表明数据流内容恰好是一完整结构数据,直接将其存入临时缓冲区即可。
3)若n >m,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。
参考
http://blog.csdn.net/binghuazh/article/details/4222516
http://www.cnblogs.com/alon/archive/2009/04/16/1437600.html
http://hi.baidu.com/chongerfeia/blog/item/b1e572f631dd7e28bd310965.html
http://freemart.iteye.com/blog/836654
http://blianchen.blog.163.com/blog/static/1310562992010101891522100/
http://mina.apache.org/chapter-11-codec-filter.html