TCP/IP中的“粘包”与“拆包”
确实,我也认为这是个伪命题,tcp这种双工面向流的协议,本来就没有粘拆包的说法;tcp只保证可靠数据传输,至于包的界限问题应该需要由上层的应用处理。
粘包与拆包指的是什么?
应用程序单次读到了不完整的包属于拆包
比如socket.read(buf)时,读到的buf不完整
应用程序单次读到了预期之外的数据,属于粘包
比如socket.read(buf)时,读到的buf内包含其他数据
但为什么会有粘拆包问题呢?
TCP发送报文使用的是滑动窗口(Sliding Window)的方式,发送端和接收端都会维护一个buffer,发送的数据首先会存至buffer,然后通过网络发送给接收端的buffer中
无论发送方发了多少数据,发了几次数据,对于接收方来说,都是从buffer中读取数据
接收方的socket.read操作,只是从recv buffer中读取数据,至于这个数据是多少组IP数据报,是发送端几次socket.write发送的,应用层无法感知
所以接收方读取buffer的界限一定是由应用层来控制,如果应用层不考虑包界限问题直接读取,那么就一定会出现一次socket.read读取了一段错误区间的报文,从而发生“粘包”和“拆包”问题
如果接收端读取数据时,还存在上次没有读取完成的buffer,那么就会错读,出现“粘包”问题;如果读取时,发送端一次发送的报文的所有IP数据报还没有到达recv buffer,那么就会少读,出现“拆包”问题
在应用层角度来观察粘拆包
写一个简易版TCP Server
//接收4k*1000大小的数据
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9527));
SocketChannel socketChannel = serverSocketChannel.accept();
int size = 4096*1000;
ByteBuffer byteBuffer = ByteBuffer.allocate(size);
while (byteBuffer.hasRemaining()){
int read = socketChannel.read(byteBuffer);
System.out.println(read);
}
简易版TCP Client
//发送4k*1000大小的数据
int size = 4096*1000;
ByteBuffer byteBuffer = ByteBuffer.allocate(size);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1",9527));
for (int i = 0; i < size; i++) {
byteBuffer.put((byte) 1);
}
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
int write = socketChannel.write(byteBuffer);
System.out.println(write);
}
当前环境下,MSS是1380,,IP层分片默认是禁用的(Don`t fragment),忽略即可
看一下执行结果:
//服务端打印结果
39672
2736
2736
2736
13680
2736
19152
2736
10944
2736
5472
2736
16416
2736
2736
12312
1368
5472
1368
从日志上看,每次读取的报文最小值是1368,刚好比mss小一点点(mss只是最大报文段长度,实际可读取的值需要减去各层协议的首部大小,所以最小值是1368)。每次读取的长度值虽然有波动,但都是1368的整数倍。
由此可见,每次可读取的报文大小(tcp buffer中),都是以ip数据报为单位的。接收端每次接收的报文大小也都是以ip数据报为单位。
所以在读取报文(tcp buffer)时,也是以ip数据报为单位,绝对不会出现读取到半个ip包的问题(忽略因mtu大小导致的ip层分片)。
那么粘包拆包里的这个“包”的最小单位也是一个IP数据报
问题解决
解决这个问题也很简单,只需要限制包的界限就可以。比如定长读取,如果不够就等待或者继续读取,直到读够为止。一次定长读取完成后,再开始下一次的读取。
Netty中的粘拆包处理
Netty中并没有直接说粘包拆包这个问题,但《Netty权威指南》这本书上倒解释了粘包拆包,不用纠结这个名词,跟着大多数人叫也没错,错的人多了也就是对的。
Netty的请求处理是一个Pipeline结构,通过handler接口,可以定义不同的encoder/decoder,从而解决粘包拆包(处理包界限)问题,当然也可以自己处理,原理都是相同的。
Netty中内置了几个编解码器,可以很简单的处理包界限问题。
LengthFieldBasedFrameDecoder
通过在包头增加消息体长度的解码器,解析数据时首先获取首部长度,然后定长读取socket中的数据。
LineBasedFrameDecoder
换行符解码器,报文尾部增加固定换行符rn,解析数据时以换行符作为报文结尾。
DelimiterBasedFrameDecoder
分隔符解码器,使用特定分隔符作为报文的结尾,解析数据时以定义的分隔符作为报文结尾
FixedLengthFrameDecoder
定长解码器,这个最简单,消息体固定长度,解析数据时按长度读取即可