TCP粘包与拆包——基于Netty

本文出自:【InTheWorld的博客】

netty_log

粘包与拆包的概念

在TCP/IP协议中,由于传输层并不了解应用层数据的含义,发送端传输层可能会对应用层数据进行拆分或者合并,在接收端也同样如此。由此而产生的问题就是常常会听说的“粘包与拆包”的问题。“粘包拆包”的问题在“短报文”和“一问一答”的场景下其实并不会出现。短报文是指报文长度远小于MSS的情况,应用层的报文在TCP报文中完全可以放下。另一方面,“一问一答”的通信模式可以保证报文会以单一的TCP包发送出去。在这两个条件下都满足时,我们不需要考虑“粘包拆包”问题。

反之,如果这两个条件不同时满足,就很可能会出现“粘包拆包”问题。

  • 粘包的示意图

220649_ivTz_1759553

  • 拆包的示意图

221054_Pifu_1759553

粘包拆包的问题的原因大概有以下几个方面:

  1. 应用程序要写入的报文字节数大于套接字缓存区大小,这时候会发生拆包问题。
  2. 应用程序的报文字节数小于套接字缓冲区大小,但应用程序连续写入多个数据包,这会导致粘包问题。
  3. TCP缓冲区的数据比较多,传输层根据MSS对缓冲区的数据进行分片发送。

image

《Netty权威指南》以及一些其他资料认为IP分片也会导致“粘包拆包”问题,这一点我不认同。因为IP分片出报文会在目标设备上进行组合,组合完成后才会传递给传输层。换言之,IP分片对传输层是透明的,IP层不应该影响到TCP层。这个问题,到底是我错了还是资料错了,还需要进一步验证。

粘包与拆包的解决

对于粘包和拆包问题,一般有以下几种解决方法:

  1. 使用固定长度的消息,报文长度不够的时候用无效数据填充。
  2. 使用换行字符来分割不同的报文。
  3. 把消息分为消息头和消息体两个部分,在消息头中添加消息长度的字段。
  4. 其他综合多种方法的消息协议。

对于上面几种方法,在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也对这种情况进行了处理。

看完这段代码,是不是觉得粘包和拆包的问题其实很简单?

参考资料:

  1. 《Netty权威指南》

  2. Netty精粹之TCP粘包拆包问题

仅有1条评论 发表评论

  1. Wu you /

    最近在看TCP/IP

发表评论