Netty应用(二) ---- 粘包半包的处理

一、粘包与半包现象

1.1 粘包现象

server端

public class StickyBagServer {
    static final Logger log = LoggerFactory.getLogger(StickyBagServer.class);

    public static void main(String[] args) {

        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            // 添加netty的日志处理器
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                                @Override
                                public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                    log.debug("connect {}", ctx.channel());
                                    super.channelActive(ctx);
                                }

                                @Override
                                public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                                    log.debug("disconnect {}", ctx.channel());
                                    super.channelInactive(ctx);
                                }
                            });
                        }
                    });
            ChannelFuture channelFuture = bootstrap.bind(8080);
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            log.debug("error");
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("shutdown");
        }
    }
}

client端

public class StickyBagClient {
    static final Logger log = LoggerFactory.getLogger(StickyBagClient.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("connected...");
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 每次发送16个字节的数据,共发送10次
                            for (int i = 0; i < 10; i++) {
                                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);
                            }
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
            channelFuture.channel().closeFuture().sync();

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

控制台

00:29:39.428 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] REGISTERED
00:29:39.429 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] ACTIVE
00:29:39.438 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
00:29:39.439 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.delayedQueue.ratio: 8
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkAccessible: true
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.checkBounds: true
00:29:39.451 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@144e1053
00:29:39.455 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] READ: 160B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000030| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000040| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000050| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000060| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000070| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000080| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000090| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
00:29:39.455 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 160, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
00:29:39.456 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, StickyBagServer$1$1#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028].
00:29:39.456 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x94f6b6b1, L:/127.0.0.1:8080 - R:/127.0.0.1:58028] READ COMPLETE

可见虽然客户端是分别以16字节为单位,通过channel向服务器发送了10次数据,可是服务器端却只接收了一次,接收数据的大小为160B,即客户端发送的数据总大小,这就是粘包现象。

1.2 半包现象

我们可以调节server端接收缓冲区的大小,设置小一点,当client端一次发送的包size超过设置最大值的时候,就会发生半包现象。
在server端添加一行此代码,client端发送18个字节,分别为0-17,ChannelOption.SO_RCVBUF方法不生效。

            bootstrap.channel(NioServerSocketChannel.class)
//                    .option(ChannelOption.SO_RCVBUF, 10)
                .childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16))
                .group(boss, worker)

控制台
在这里插入图片描述

1.3 原因分析

TCP协议中,每发送一次数据就需要进行一次ack确认,但是这样就意味着数据的发送将是串行的。于是TCP协议中引入了一个叫做滑动窗口的概念。滑动窗口其实就是一个缓冲区,在滑动窗口内发送的数据,无需接收响应,可以继续发送。当第一个数据ack确认之后,滑动窗口就会向下移动一个单位。当接收方的滑动窗口设置足够大,并且接收方处理不及时的情况下,发送方发过来的数据就会在接收方的滑动窗口中缓冲多个报文,最终导致粘包。当接收方的滑动窗口设置小于实际发送量,就只能先处理一部分数据,等ack确认后再处理后续的,就导致了半包的情况。

除了TCP层之外,Nagle算法也会造成粘包,网卡的MSS限制也会造成半包。

二、粘包半包解决–帧解码器

2.1 分隔符解码器

  • LineBasedFrameDecoder行解码器,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常。
  • DelimiterBasedFrameDecoder自定义分隔符解码器,支持自定义
  • 方法入参:
    maxLength – 最大长度
    stripDelimiter – 解码的帧是否应该去掉分隔符,默认为true,去掉分隔符
    failFast – 超过最大长度,true或者false都会抛出TooLongFrameException异常,没感觉此参数有什么区别
    delimiter – 分隔符

LineBasedFrameDecoder

public class LineBaseFrameDecoderTest {

    public static void main(String[] args) {

        // 行解码器,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常,设置最大长度1024
        LineBasedFrameDecoder frameDecoder = new LineBasedFrameDecoder(1024,false,false);
        EmbeddedChannel embeddedChannel =
            new EmbeddedChannel(frameDecoder, new LoggingHandler(LogLevel.INFO));

        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        buffer.writeBytes("hello world\n".getBytes());
        buffer.writeBytes("the time is running\n".getBytes());
        embeddedChannel.writeInbound(buffer);
    }
}

在这里插入图片描述

DelimiterBasedFrameDecoder

public class DelimiterBaseFrameDecoderTest {

    public static void main(String[] args) {

        // 自定义分隔符解码器,设置最大长度1024
        DelimiterBasedFrameDecoder frameDecoder = new DelimiterBasedFrameDecoder(256, true, false,
            ByteBufAllocator.DEFAULT.buffer().writeBytes("||".getBytes()));
        EmbeddedChannel embeddedChannel = new EmbeddedChannel(frameDecoder, new LoggingHandler(LogLevel.INFO));

        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        buffer.writeBytes("hello world||".getBytes());
        buffer.writeBytes("the time is running||".getBytes());
        embeddedChannel.writeInbound(buffer);
    }
}

在这里插入图片描述

缺点:

  • 处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
  • 效率低,因为是按照传过来的字节一个一个去找换行符的

2.2 LTC解码器

有参构造

        /**
         * @param maxFrameLength  最大帧长度
         * @param lengthFieldOffset 长度字段的偏移量,即长度字段从第几个字节开始
         * @param lengthFieldLength 长度字段所占的字节数
         * @param lengthAdjustment  以长度字段为基准,还有几个字节是内容
         * @param initialBytesToStrip 从头剥离多少字节,即解码后直接去掉头部我们不想要的字节
         */
        LengthFieldBasedFrameDecoder fieldBasedFrameDecoder = new LengthFieldBasedFrameDecoder(1024, 1, 2, 1, 1);

官方示例一:

在这里插入图片描述

官方示例二:
在这里插入图片描述

官方示例三:
在这里插入图片描述

官方示例四:
在这里插入图片描述
代码

public class LineBaseFrameDecoderTest {
    public static void main(String[] args) {
        LengthFieldBasedFrameDecoder fieldBasedFrameDecoder = new LengthFieldBasedFrameDecoder(1024, 2, 2, 1, 5);

        EmbeddedChannel embeddedChannel =
            new EmbeddedChannel(fieldBasedFrameDecoder, new LoggingHandler(LogLevel.INFO));

        byte[] content = "hello, world".getBytes();
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
        // 魔数,占两个字节
        buffer.writeByte(0XCA);
        buffer.writeByte(0XFE);
        // 长度字段,占两个字节
        buffer.writeShort(content.length);
        // 版本号,占一个字节
        buffer.writeByte(1);
        // 内容占12个字节
        buffer.writeBytes(content);
        embeddedChannel.writeInbound(buffer);
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值