NIO与Netty篇-(1)-粘包拆包问题

1.1 什么是粘包拆包

client 连续发送 server 的数据包,server 接收到数据会出现数据包粘在一起的情况

比如 client发送了 数据"123456"和"78910"

server收到却是: "12345" 和 "678910"

1.2 为何出现粘包?

TCP报文格式 如下,

image.png

在 TCP首部并未指明数据包的长度

  1. TCP首部中有20bytes的固定长度;
  2. 可于第1个报文中指明: 最大报文段长度MSS(Maximum Segment Size); 但是它是选项 部分, 非必有的;

1.3 如何解决粘包问题

一般解决粘包问题的四种方案:

1.3.1 固定发包长度

客户端发送数据包时, 固定长度, 比如 1024字节, 如果某次发包不足 1024字节, 空格补足;

1.3.2 使用固定分隔符

客户端发包时, 每个包末尾使用固定分隔符, 比如"\r\n";

如果数据包粘包了, 拆包时, 就等下一个数据包直到拿到"\r\n";

拆后的头部部分前一个包的剩余部分合并; 这样就得到一个完整的包.

1.3.3 消息头部保存消息长度len字段

消息分为 头部 和 消息体; 然后在头部增设一个 消息长度的字段; 接收时, 读到够len长度的数据, 才算读完完整数据!

1.3.4 自定义协议

自己定制自己的发包协议; 指定数据长度和分拆合并逻辑;

1.4 Netty如何解决粘包问题

1.4.1 固定发包长度

FixedLengthFrameDecoder:

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    private final int frameLength;
    ...
}

将接收到的字节按固定字节数分割。例如,如果你收到以下四个片段化的数据包:

第1个packet第2个packet第3个packet第4个packet
ABCDEFGHI

FixedLengthFrameDecoder(3) 将其解码为以下3个固定长度的数据包:

第1个packet第2个packet第3个packet
ABCDEFGHI

发包前编码:

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class FixedLengthFrameEncoder extends MessageToByteEncoder<String> {
    private int len;

    public FixedLengthFrameEncoder(int len) {
        this.len = len;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        // 超长直接抛出异常
        if (msg.length() > len) {
            throw new UnsupportedOperationException("message too large, limited " + len);
        }

        // 不足长补全
        if (msg.length() < len) {
            msg = appendSpace(msg);
        }

        ctx.writeAndFlush(Unpooled.wrappedBuffer(msg.getBytes()));
    }  // 进行空格补全


  /**
   * 补空格
   * @param msg
   * @return
   */
  private String appendSpace(String msg) {
        StringBuilder builder = new StringBuilder(msg);
        for (int i = 0; i < len - msg.length(); i++) {
            builder.append(" ");
        }

        return builder.toString();
    }
}

1.4.2 固定分隔符

1.4.2.1 回车换行符-LineBasedFrameDecoder

public class LineBasedFrameDecoder extends ByteToMessageDecoder {}

1.4.2.2 自定义分隔符-DelimiterBasedFrameDecoder

public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {}

1.4.3 消息保存长度len字段

LengthFieldBasedFrameDecoder & LengthFieldPrepender

增加长度字段, 标明数据长度;

maxFrameLength:指定包所传递的最大数据包大小;
lengthFieldOffset:指定length字段在字节码中的偏移量;
lengthFieldLength:指定length字段所占用的字节长度;
lengthAdjustment:对含消息头和消息体的, 我们有时需进行消息头的长度调整,方便只取消息体: 此字段就是消息头长;
initialBytesToStrip:对于length字段在消息头中间的情况,可以通过此字段, 忽略消息头及length字段所占的字节。

收包后解码:

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {

    private final ByteOrder byteOrder;
    private final int maxFrameLength;
    private final int lengthFieldOffset;
    private final int lengthFieldLength;
    private final int lengthFieldEndOffset;
    private final int lengthAdjustment;
    private final int initialBytesToStrip;
    private final boolean failFast;
    private boolean discardingTooLongFrame;
    private long tooLongFrameLength;
    private long bytesToDiscard;
    ...
}

发包前编码:

public class LengthFieldPrepender extends MessageToMessageEncoder<ByteBuf> {

    private final ByteOrder byteOrder;
    private final int lengthFieldLength;
    private final boolean lengthIncludesLengthFieldLength;
    private final int lengthAdjustment;
    ...
}

1.4.4 自定义粘包拆包器

1.4.4.1 继承 LengthFieldBasedFrameDecoder和LengthFieldPrepender

继承这两个类, 然后复写自己的逻辑

1.4.4.2 自己扩展MessageToByteEncoder和ByteToMessageDecoder

public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter{}

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {

1.5 小结

1. 发包固定长度

2. 发包指定分隔符

3. 发包头部加数据长度

4. 自定义协议

阅读 292

推荐阅读