粘包与半包
滑动窗口
为了提高 TCP 传输的效率,传输端 & 接收端 会维护一个数组动态储存维护一定数量的 TCP 连接。
现象分析
粘包
-
现象:发送 abc def,接收 abcdef
-
原因:
-
应用层:接收方 ByteBuf 设置较大(Netty 默认 1024)
-
滑动窗口:接收方未及时处理数据且窗口大小足够大,滑动窗口中缓存了多个报文就会粘包
-
Nagle 算法1:会造成粘包
-
半包
-
现象:发送 abcdef,接收 abc def
-
原因:
-
应用层:接收方 ByteBuf 小于实际发送的数据量
-
滑动窗口:窗口大小较小且发送方报文较大,窗口容量不足只能部分发送数据造成半包
-
MSS 限制:当发送到数据超过 MSS 限制后会将数据切分发送,造成半包
-
本质原因:TCP 无边界
解决方案
-
客户端每次发送完数据后及时关闭连接
@Override // Channel 连接建立成功后触发 public void channelActive(ChannelHandlerContext ctx) { ByteBuf buf = ctx.alloc().buffer(16); buf.writeBytes(new byte[] {'1','2','3','4','5','6','7','8','9','0','a','s','d','f','g','h'}); ctx.writeAndFlush(buf); ctx.channel().close(); }
-
服务端采用固定长度接收
@Override protected void initChannel(NioSocketChannel channel) { channel.pipeline().addLast(new StringDecoder()) .addLast(new FixedLengthFrameDecoder(16)) // 每 16 字节解码一次 .addLast(new LoggingHandler(LogLevel.DEBUG)); }
-
服务端采用换行符分割接收
@Override protected void initChannel(NioSocketChannel channel) { channel.pipeline().addLast(new StringDecoder()) .addLast(new LineBasedFrameDecoder(1024)) // 最大寻找长度限制 .addLast(new LoggingHandler(LogLevel.DEBUG)); }
-
服务端采用协议解析
数据写入时先声明数据长度,服务端根据协议内容直接进行解析
public static void main(String[] args) { EmbeddedChannel channel = new EmbeddedChannel( new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4/*剥离数据的长度*/), new StringDecoder(StandardCharsets.UTF_8), new LoggingHandler(LogLevel.DEBUG) ); ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(); for (int i = 0; i < 3; ++ i) { byte[] bytes = "你好IllTamer".getBytes(StandardCharsets.UTF_8);