粘包与拆包的概念
在TCP/IP协议中,由于传输层并不了解应用层数据的含义,发送端传输层可能会对应用层数据进行拆分或者合并,在接收端也同样如此。由此而产生的问题就是常常会听说的“粘包与拆包”的问题。“粘包拆包”的问题在“短报文”和“一问一答”的场景下其实并不会出现。短报文是指报文长度远小于MSS的情况,应用层的报文在TCP报文中完全可以放下。另一方面,“一问一答”的通信模式可以保证报文会以单一的TCP包发送出去。在这两个条件下都满足时,我们不需要考虑“粘包拆包”问题。
反之,如果这两个条件不同时满足,就很可能会出现“粘包拆包”问题。
- 粘包的示意图
- 拆包的示意图
粘包拆包的问题的原因大概有以下几个方面:
- 应用程序要写入的报文字节数大于套接字缓存区大小,这时候会发生拆包问题。
- 应用程序的报文字节数小于套接字缓冲区大小,但应用程序连续写入多个数据包,这会导致粘包问题。
- TCP缓冲区的数据比较多,传输层根据MSS对缓冲区的数据进行分片发送。
《Netty权威指南》以及一些其他资料认为IP分片也会导致“粘包拆包”问题,这一点我不认同。因为IP分片出报文会在目标设备上进行组合,组合完成后才会传递给传输层。换言之,IP分片对传输层是透明的,IP层不应该影响到TCP层。这个问题,到底是我错了还是资料错了,还需要进一步验证。
粘包与拆包的解决
对于粘包和拆包问题,一般有以下几种解决方法:
- 使用固定长度的消息,报文长度不够的时候用无效数据填充。
- 使用换行字符来分割不同的报文。
- 把消息分为消息头和消息体两个部分,在消息头中添加消息长度的字段。
- 其他综合多种方法的消息协议。
对于上面几种方法,在Netty都有对应的一些支持。在Netty中,这些功能以Decoder的形式存在,常见的Decoder有LineBasedFramDecoder,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder等等。这些解码器,基本都是顾名思义的。
解码器的使用也非常简单,由于Netty使用了责任链的设计模式,所以只要在消息接收的Pipeline添加对应Decoder即可。大概就是下面这样的形式:
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
}
});
从前文也可以看出来,粘包拆包的解决方法其实也挺简单的。这里以这个LineBasedFrameDecoder为例,简单的看看Netty的实现方式。LineBasedFrameDecoder比较重要的实现代码如下:
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
final int eol = findEndOfLine(buffer);
if (!discarding) {
if (eol >= 0) {
final ByteBuf frame;
final int length = eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
if (length > maxLength) {
buffer.readerIndex(eol + delimLength);
fail(ctx, length);
return null;
}
if (stripDelimiter) {
frame = buffer.readBytes(length);
buffer.skipBytes(delimLength);
} else {
frame = buffer.readBytes(length + delimLength);
}
return frame;
} else {
final int length = buffer.readableBytes();
if (length > maxLength) {
discardedBytes = length;
buffer.readerIndex(buffer.writerIndex());
discarding = true;
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null;
}
} else {
if (eol >= 0) {
final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
discardedBytes = buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
}
return null;
}
}
在上面的代码片段中,主要的逻辑就是根据换行符以及行的最大长度来读取消息。如果成功的读取到了一行数据,就从消息缓存中跳过对应的长度,然后传递给下一个pipeline。其中可以看到,针对不同系统的换行符不同,Netty也对这种情况进行了处理。
看完这段代码,是不是觉得粘包和拆包的问题其实很简单?
参考资料:
- 《Netty权威指南》
- Netty精粹之TCP粘包拆包问题
最近在看TCP/IP