Netty处理半包粘包问题

无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制。

TCP粘包/拆包发生的根本原因【流式协议,消息无边界】

粘包主要原因

  • 发送方每次写入数据 < 套接字缓冲区大小
  • 接收方读取套接字缓冲区数据不够及时

半包的主要原因:

  • 发送方写入数据 > 套接字缓冲区大小
  • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包

拆包、粘包示例

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下5种情况。

  • (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

  • (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

  • (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

  • (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

  • (5)如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

解决问题的根本手段:流式协议无边界,解决问题就是找出消息的边界:

(1)方式一:消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;【空间浪费,不推荐】

(2)方式二:在包尾增加回车换行符进行分割,例如FTP协议;【推荐】

(3)方式三:将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度;【推荐+】

Netty处理粘包、半包

Netty针对三种封帧方式提供了对应的处理器:

  • LineBasedFrameDecoder:通过换行符来区分每个包
  • DelimiterBasedFrameDecoder:通过特殊分隔符来区分每个包
  • FixedLengthFrameDecoder:通过定长的报文来分包
  • LengthFieldBasedFrameDecoder:跟据包头部定义的长度来区分包

服务端添加解码器:DelimiterBasedFrameDecoder

ByteBuf delimiter = Unpooled.copiedBuffer("&".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
//1024表示单条消息的最大长度,解码器在查找分隔符的时候,达到该长度还没找到的话会抛异常
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());

Netty处理粘包、半包的源码解析:

核心原理:利用ByteBuf类型的数据积累器,若为定长,读取的数据不满足长度则进行缓存积累,下一次读取数据拼接解码

处理器都拥有一个共同的父类:ByteToMessageDecoder

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		//msg相当于我们的消息数据
        if (msg instanceof ByteBuf) {
            CodecOutputList out = CodecOutputList.newInstance();
            boolean var10 = false;

            try {
                var10 = true;
				
				//1、把数据转换成data,,
                ByteBuf data = (ByteBuf)msg;
				//2、cumulation是数据积累器,用来积累数据。判断是否为空。
				//第一次的时候肯定为null  所以first是true,直接把data数据给了cumulation。再接下来就是解码:
                this.first = this.cumulation == null;
                if (this.first) {
                    this.cumulation = data;
                } else {
					//3、具体数据解码。底层调用对应的解码器的decode方法
                    this.cumulation = this.cumulator.cumulate(ctx.alloc(), this.cumulation, data);
                }

                this.callDecode(ctx, this.cumulation, out);
                var10 = false;
            } catch (DecoderException var11) {
                throw var11;
            } catch (Exception var12) {
                throw new DecoderException(var12);
            } finally {
                if (var10) {
                    if (this.cumulation != null && !this.cumulation.isReadable()) {
                        this.numReads = 0;
                        this.cumulation.release();
                        this.cumulation = null;
                    } else if (++this.numReads >= this.discardAfterReads) {
                        this.numReads = 0;
                        this.discardSomeReadBytes();
                    }

                    int size = out.size();
                    this.decodeWasNull = !out.insertSinceRecycled();
                    fireChannelRead(ctx, out, size);
                    out.recycle();
                }
            }

            if (this.cumulation != null && !this.cumulation.isReadable()) {
                this.numReads = 0;
                this.cumulation.release();
                this.cumulation = null;
            } else if (++this.numReads >= this.discardAfterReads) {
                this.numReads = 0;
                this.discardSomeReadBytes();
            }

            int size = out.size();
            this.decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();
        } else {
            ctx.fireChannelRead(msg);
        }

    }
    
}

FixedLengthFrameDecoder的decode为例

protected Object decode(
        @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
    if (in.readableBytes() < frameLength) {
        return null;
    } else {
        return in.readRetainedSlice(frameLength);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值