第五章Netty第一节 粘包和半包

粘包与半包

粘包

现象:发送abc def,接受到abcdef

原因:

  • 应用层:接收方ByteBuf设置太大(Netty默认是1024)
  • 传输层滑动窗口: 假设发送方256 bytes表示一个完整的报文,接收方的滑动窗口来不及处理且滑动窗口足够大,导致多个报文在一个滑动窗口中,导致粘包。(TCP协议)
  • Nagle算法:会造成粘包。Nagle算法是TCP协议中的一种机制,有时候发送一个字节,也需要加入TCP头和IP头,有点浪费网络。为了提高网络利用率,会将少量数据进行延迟发送,积攒起来一起发送。会造成粘包现象。

半包

现象:发送abcdef,接受abc def

原因:

  • 应用层:接收方的ByteBuf小于实际发送的数据,导致一个数据报文被拆分了。
  • 传输层滑动窗口:假设接收方的滑动窗口大小为128bytes,这时候发送方发了256bytes,滑动窗口接受不过来,让发送方只能先发128bytes,等待ACK后,才能发送剩余的数据。造成一个完整的报文被拆分开。(传输层TCP协议)
  • MSS(max segment size)限制:当发送的数据超出MSS限制后,会将数据切分后发送,会造成半包。
    本质上是因为TCP协议是基于字节流的(首部没有长度),消息没有边界,所以会造成粘包和半包现象。UDP是面向报文的(首部有长度),所以不会有粘包和半包现象。

解决方案

方法1 短链接

java 复制代码
public class HelloWorldClient {
    static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);

    public static void main(String[] args) {
        // 分 10 次发送
        for (int i = 0; i < 10; i++) {
            send();
        }
    }

    private static void send() {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("conneted...");
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            ByteBuf buffer = ctx.alloc().buffer();
                            buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
                            ctx.writeAndFlush(buffer);
                            // 发完即关
                            ctx.close();
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            log.error("client error", e);
        } finally {
            worker.shutdownGracefully();
        }
    }
}

短链接相当于发送一个数据就断开一次。只能解决粘包。接受的ByteBuf还是有限的,还是会有半包问题。

方法2 固定长度

客户端和服务端商量好发送的数据包具有固定长度。如果不够长,就用占位符占位。

让所有的数据包长度固定(假设长度为10字节),服务端加入FixedLengthFrameDecoder进行处理。

java 复制代码
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));

客户端测试代码,注意, 采用这种方法后,客户端什么时候 flush 都可以

java 复制代码
public class HelloWorldClient {
    static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);

    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connetted...");
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 发送内容随机的数据包
                            Random r = new Random();
                            char c = 'a';
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                byte[] bytes = new byte[8];
                                for (int j = 0; j < r.nextInt(8); j++) {
                                    bytes[j] = (byte) c;
                                }
                                c++;
                                buffer.writeBytes(bytes);
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            log.error("client error", e);
        } finally {
            worker.shutdownGracefully();
        }
    }
}

缺点是数据包的大小不好把握。长度只能定的太大,占位符就会多。浪费空间。因此长度最好定位最大的数据包的长度。仅仅适用于提前知道数据包的长度的情况。

方法3 固定分隔符

客户端和服务端商量好发送的数据包具有特定的分隔符。服务端用LineBasedFrameDecoder处理,自动将接受到的消息用\n进行分隔。

java 复制代码
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

客户端在每条消息之后,加入\n分隔符。

java 复制代码
public class HelloWorldClient {
    static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);

    public static void main(String[] args) {
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(worker);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connetted...");
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            Random r = new Random();
                            char c = 'a';
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                for (int j = 1; j <= r.nextInt(16)+1; j++) {
                                    buffer.writeByte((byte) c);
                                }
                                buffer.writeByte(10);
                                c++;
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            log.error("client error", e);
        } finally {
            worker.shutdownGracefully();
        }
    }
}

如果本身传输的数据中有分隔符,会解析错误

方法4 预设长度(推荐)

在发送消息前,先约定用定长字节表示接下来数据的长度。服务端用LengthFieldBasedFrameDecoder来处理。

java 复制代码
// 最大长度,长度偏移,长度占用字节,长度调整(长度占用字节后偏移多少个才是真正的数据),剥离字节数(用于接受的数据去除长度占用的字节信息)
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

测试代码:

java 复制代码
package cn.itcast.advance.c1;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class TestLengthFieldDecoder {
    public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                new LengthFieldBasedFrameDecoder(
                        1024, 0, 4, 1,4),
                new LoggingHandler(LogLevel.DEBUG)
        );

        //  4 个字节的内容长度, 实际内容
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        send(buffer, "Hello, world");
        send(buffer, "Hi!");
        channel.writeInbound(buffer);
    }

    private static void send(ByteBuf buffer, String content) {
        byte[] bytes = content.getBytes(); // 实际内容
        int length = bytes.length; // 实际内容长度
        buffer.writeInt(length);
        buffer.writeByte(1);
        buffer.writeBytes(bytes);
    }
}
相关推荐
网络安全Jack21 分钟前
什么是网络安全等级保护?
网络·安全·web安全
Dovir多多1 小时前
Python数据处理——re库与pydantic的使用总结与实战,处理采集到的思科ASA防火墙设备信息
网络·python·计算机网络·安全·网络安全·数据分析
VVVVWeiYee2 小时前
项目2路由交换
运维·服务器·网络·网络协议·信息与通信
小伍_Five4 小时前
透视网络世界:计算机网络习题的深度解析与总结【前3章】
服务器·网络·计算机网络
网络安全(king)6 小时前
网络安全攻防学习平台 - 基础关
网络·学习·web安全
李白你好6 小时前
家用无线路由器的 2.4GHz 和 5GHz
运维·网络
嵌入(师)6 小时前
嵌入式驱动开发详解21(网络驱动开发)
网络·驱动开发
柒烨带你飞7 小时前
路由器的原理
网络·智能路由器·php
xserver27 小时前
ensp 基于EASY IP的公司出口链路配置
网络·tcp/ip·智能路由器
枫零NET7 小时前
学习思考:一日三问(学习篇)之匹配VLAN
网络·学习·交换机