Netty学习笔记(3) Netty进阶1 - 粘包和半包


前言

笔记基于黑马的Netty教学讲义加上自己的一些理解,感觉这是看过的视频中挺不错的,基本没有什么废话,视频地址:黑马Netty


粘包和半包

1. 粘包现象

由于粘包在前面 Nio 概念中介绍过了,这里不多说,直接看测试:这里测试一段代码,分10次,每次发送16个字节

//服务端
@Slf4j
public class HelloWorldServer {
    public static void main(String[] args) {
        start();
    }

    static void start(){
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class).group(boss, worker)
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            //日志处理器
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("server error", e);
            e.printStackTrace();
        } finally {
            //优雅地关闭
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }
}

//客户端
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).group(worker);
            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        //会在连接上channel后,会触发channelActive事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            for (int i = 0; i < 10; i++) {
                                //创建了10个
                                ByteBuf buf = ctx.alloc().buffer(16);
                                buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,});
                                ctx.writeAndFlush(buf);
                            }
                        }
                    });
                }
            });

            ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            log.error("client error", e);
        }finally {
            //优雅地关闭
            worker.shutdownGracefully();
        }
    }

}
//结果
DEBUG [nioEventLoopGroup-3-1] (12:53:38,710) (AbstractInternalLogger.java:145) - [id: 0x14bde79c, L:/127.0.0.1:8080 - R:/127.0.0.1:61860] 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 |................|
+--------+-------------------------------------------------+----------------+
DEBUG [nioEventLoopGroup-3-1] (12:53:38,710) (DefaultChannelPipeline.java:1182) - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 160, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
DEBUG [nioEventLoopGroup-3-1] (12:53:38,712) (DefaultChannelPipeline.java:1198) - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x14bde79c, L:/127.0.0.1:8080 - R:/127.0.0.1:61860].
DEBUG [nioEventLoopGroup-3-1] (12:53:38,712) (AbstractInternalLogger.java:145) - [id: 0x14bde79c, L:/127.0.0.1:8080 - R:/127.0.0.1:61860] READ COMPLETE

Process finished with exit code -1


结果是:在结果中发现 READ: 160B,意思是一次性输入了 160 个字节,所以我们可以看到不是预想的分10次发送,一次发送16个字节,而是发生了粘包一次发送160个字节



2. 半包现象

调用 serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) 这个方法把服务器的接收缓冲区设置小一点。

结果:

DEBUG [nioEventLoopGroup-3-1] (13:02:59,781) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ: 36B
         +-------------------------------------------------+
         |  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                                     |....            |
+--------+-------------------------------------------------+----------------+
DEBUG [nioEventLoopGroup-3-1] (13:02:59,781) (DefaultChannelPipeline.java:1182) - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 36, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
DEBUG [nioEventLoopGroup-3-1] (13:02:59,782) (DefaultChannelPipeline.java:1198) - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092].
DEBUG [nioEventLoopGroup-3-1] (13:02:59,782) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ COMPLETE
DEBUG [nioEventLoopGroup-3-1] (13:02:59,783) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ: 50B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000030| 04 05                                           |..              |
+--------+-------------------------------------------------+----------------+
DEBUG [nioEventLoopGroup-3-1] (13:02:59,783) (DefaultChannelPipeline.java:1182) - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 50, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
DEBUG [nioEventLoopGroup-3-1] (13:02:59,783) (DefaultChannelPipeline.java:1198) - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092].
DEBUG [nioEventLoopGroup-3-1] (13:02:59,783) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ COMPLETE
DEBUG [nioEventLoopGroup-3-1] (13:02:59,783) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ: 50B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |................|
|00000010| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |................|
|00000020| 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 |................|
|00000030| 06 07                                           |..              |
+--------+-------------------------------------------------+----------------+
DEBUG [nioEventLoopGroup-3-1] (13:02:59,784) (DefaultChannelPipeline.java:1182) - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 50, cap: 512) that reached at the tail of the pipeline. Please check your pipeline configuration.
DEBUG [nioEventLoopGroup-3-1] (13:02:59,784) (DefaultChannelPipeline.java:1198) - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092].
DEBUG [nioEventLoopGroup-3-1] (13:02:59,784) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ COMPLETE
DEBUG [nioEventLoopGroup-3-1] (13:02:59,784) (AbstractInternalLogger.java:145) - [id: 0x13b94382, L:/127.0.0.1:8080 - R:/127.0.0.1:62092] READ: 24B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 04 05 06 07 |................|
|00000010| 08 09 0a 0b 0c 0d 0e 0f                         |........        |
+--------+-------------------------------------------------+----------------+

可以发现原来一次性写出的数据现在分成了4个部分:36 - 50- 50 - 24,发生了半包现象。serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) 影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍其实使用TCP协议进行处理的数据都会有半包粘包的现象。



3. 现象分析(看视频)

粘包:组合拼接

  • 现象:发送 abc def,接收 abcdef
  • 原因:
    1. 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024),底层帮我们把所有的数据都合并了
    2. 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    3. Nagle 算法:会造成粘包(TCP层面的,计算机网络的知识),用来尽可能多的发送数据

半包:分拆

  • 现象,发送 abcdef,接收 abc def
  • 原因
    1. 应用层:接收方 ByteBuf 小于实际发送数据量,发送的消息只能被拆开逐次发送
    2. 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    3. MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包(计算机网络的知识,传输层的知识)

本质是因为 TCP 是流式协议,消息无边界,我们必须自己去找边界

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差
  • 在下面这张图可以看出来,客户端每发一次请求给服务器,需要服务器响应之后返回给客户端才可以继续执行,缺点就是性能差,如果在其中有一个请求要处理的数据很多,那么就会影响到后面的执行
  • 为了解决这个,TCP 使用了滑动窗口法

在这里插入图片描述

  • 滑动窗口:一开始定义一个固定大小的窗口,窗口的大小是用来决定有多少的数据可以不用等待服务器返回就立刻发出去的
  • 通过下面这张图可以看到,一开始窗口大小是4,那么当发出一个数据之后并接到了就可以继续往下滑,比如:一开始窗口是在1,2,3,4这里的,后来1发送数据并且服务器响应 5 回来了,这时候窗口就变成了 2,3,4,5
    在这里插入图片描述

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
  • 发出请求之后,在响应没有回来之前,窗口是不可以滑动的
  • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收



4. 粘包解决方法 - 短链接

短链接只能解决 粘包现象,不能解决半包现象。思路就是客户端发一次就断开一次,再发完数据之后调用 ctx.channel().close() 方法进行断开,客户端感觉到服务器断开了,自然就会把缓冲区的数据写出来。所以只要缓冲区够大这种方法是可以解决粘包的。

  • 发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低,我们只需要对客户端修改一下,加入ctx.channel().close();
private static void send(){
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try{
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class).group(worker);
            bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
                @Override
                protected void initChannel(NioSocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                        //会在连接上channel后,会触发channelActive事件
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            for (int i = 0; i < 10; i++) {
                                //创建了10个
                                ByteBuf buf = ctx.alloc().buffer(16);
                                buf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,});
                                ctx.writeAndFlush(buf);
                                //发一次就关闭一次
                                ctx.channel().close();
                            }
                        }
                    });
                }
            });

            ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            log.error("client error", e);
        }finally {
            //优雅地关闭
            worker.shutdownGracefully();
        }
    }
  • 对于服务端,把 serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) 这行代码注释掉。因为这行代码是设置系统接收缓冲器大小(滑动窗口)




我们测试半包:调整netty的接收器缓冲区为16,客户端一次性发送17个字节

//调整netty的接收器缓冲区
//AdaptiveRecvByteBufAllocator:参数1:最小容量 参数2:初始容量 参数3:最大容量
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16))

现象:
在这里插入图片描述



5. 半包解决方法 - 定长解码器

让所有数据包长度固定(假设长度为 8 字节),服务器端加入解码器

  • 注意这个解码器要放在其他消息处理的处理器比如LoggingHandler之前,因为要先解码才可以处理消息
  • FixedLengthFrameDecoder 指定解码长度,但是我们必须要自己手动去算出可能发送的消息的最大长度,这样才可以保证每次发送的时候能把数据都写入到ByteBuf中
  • 这种方法类似于 CPU 刷新周期的定长周期那样,利用最大的周期作为所有周期的刷新时间
  • 给个例子,比如有三个数据 111,2222,3333要发送的,这时候我们设置最大的解码数为5,然后把111填充成 111–,2222填充成 2222- ,我们把 111-- 和 2222- 和 33333 一起发送到服务端,服务端按照5个字节的大小进行解码,每5个字节就作为一条消息。这样就解决了半包的现象,虽然 客户端发送的时候还会出现粘包现象,但是不影响服务端的接收
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));

这种方法其实是固定了长度,比如数据包固定长度是10,那么在客户端发生数据的时候,服务端接收消息 接收到 10个字节才考虑完成一次接收。

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();
        }
    }
}

客户端:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 61 61 00 00 00 00 62 00 00 00 00 00 00 00 |aaaa....b.......|
|00000010| 63 63 00 00 00 00 00 00 64 00 00 00 00 00 00 00 |cc......d.......|
|00000020| 00 00 00 00 00 00 00 00 66 66 66 66 00 00 00 00 |........ffff....|
|00000030| 67 67 67 00 00 00 00 00 68 00 00 00 00 00 00 00 |ggg.....h.......|
|00000040| 69 69 69 69 69 00 00 00 6a 6a 6a 6a 00 00 00 00 |iiiii...jjjj....|
+--------+-------------------------------------------------+----------------+

服务端:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 61 61 00 00 00 00                         |aaaa....        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 00 00 00 00 00 00 00                         |b.......        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 63 00 00 00 00 00 00                         |cc......        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 64 00 00 00 00 00 00 00                         |d.......        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 00 00 00                         |........        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 66 66 66 66 00 00 00 00                         |ffff....        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 67 67 67 00 00 00 00 00                         |ggg.....        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 00 00 00 00 00 00 00                         |h.......        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 69 69 69 69 69 00 00 00                         |iiiii...        |
+--------+-------------------------------------------------+----------------+
12:07:00 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0xd739f137, L:/192.168.0.103:9090 - R:/192.168.0.103:53155] READ: 8B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 6a 6a 6a 6a 00 00 00 00                         |jjjj....        |
+--------+-------------------------------------------------+----------------+



缺点: 数据包的大小不好把握

  • 长度定的太大,浪费
  • 长度定的太小,对某些数据包又显得不够
  • 我们得一个一个去测出最大值



6. 粘包半包解决 - 行解码器

服务端加入,默认以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常,我们在构造的时候需要指定一个最大长度。为了防止出现消息一直没有换行符的情况。

//设置最大长度是1024,以换行符作为消息结束的标志
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

服务端:设置消息超过1024没有换行符就抛出异常

@Slf4j
public class HelloWorldServer {
    public static void main(String[] args) {
        start();
    }

    static void start(){
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class).group(boss, worker)
            //我们把系统接收的缓冲区(滑动窗口)设置小一点
            //serverBootstrap.option(ChannelOption.SO_RCVBUF, 10)
            //我们也不用去刻意调整滑动窗口大小,服务器和客户端在连接建立之后会自动帮我们根据吞吐量这些去设置的

                    //调整netty的接收器缓冲区
                    .childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16))
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            //日志处理器
                            ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                            ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("server error", e);
            e.printStackTrace();
        } finally {
            //优雅地关闭
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }
}

客户端:随机发送10条数据过去,以 \n 结尾

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

    public static StringBuilder makeString(char c, int len){
        StringBuilder sb = new StringBuilder(len + 2);
        for (int i = 0; i < len; i++) {
            sb.append(c);
        }
        sb.append("\n");
        return sb;
    }

    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 {
                           ByteBuf buf = ctx.alloc().buffer();
                           char c = '0';
                           Random r = new Random();
                            for (int i = 0; i < 10; i++) {
                                //结尾都以一个换行符结束
                                StringBuilder stringBuilder = makeString(c, r.nextInt(256) + 1);
                                c++;
                                buf.writeBytes(stringBuilder.toString().getBytes());
                            }
                            ctx.writeAndFlush(buf);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
            channelFuture.channel().closeFuture().sync();

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

结果:客户端输入,点表示换行符
在这里插入图片描述
服务端接收的:按\n分隔开了
在这里插入图片描述


缺点:

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



7. 粘包半包解决 - LTC解码器

1. 概念

在发送消息前,先约定用定长字节表示接下来数据的长度

// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

参数:
在这里插入图片描述


我们到源码中看官方给出的例子并进行解释:

  1. 从0开始,length长度为2个字节,Adjustment是0,表示从2开始之后就是全部内容了,initial = 0表示结果去掉从头开始的0个字节
    在这里插入图片描述

  2. 从0开始,length长度为2个字节,Adjustment是-2,表示从2开始之后往前-2就是全部内容了,initial = 0表示结果去掉从头开始的0个字节。Adjustment是负数是因为某些时候长度字段表示整个消息的长度,比如这里的E表示了14,是整个消息的,而不是内容的,这时候长度总是比内容长度大2,所以用一个Adjustment来调整。
    在这里插入图片描述

  3. 表示从2开始,之后三个字节是长度,调整长度为0,保持不变,求出结果后不需要减
    在这里插入图片描述

  4. 表示从1开始,之后的2个字节是长度,然后正数表示再往前数1个字节(过了HDR2)才是内容,最终的结果去掉从头开始的3个字节(HDR1和Length),剩下的就是HDR2和内容 13个字节了
    在这里插入图片描述



2. 代码

  • 设置 LengthFieldBasedFrameDecoder 的时候,如果我们出了长度和内容之外还加了一些其他的内容比如版本号(在接下来这个代码中有体现),那么我们就得动态调整Adjustment的字节大小,使得能够把内容完整读出来。下面这个例子如果不调整为1,就会把版本号(1)位和内容(12)位组合,然后再读取12位,这样内容就少了一位
public class TestLengthFieldDecoder {
    public static void main(String[] args) {
        EmbeddedChannel channel = new EmbeddedChannel(
                //长度是4,从头开始,版本号占了1个,所以Adjustment设置为1向后移一位再读取内容,结果剥离从头开始的4个字节
                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);

        /**
             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
             +--------+-------------------------------------------------+----------------+
             |00000000| 48 69 21                                        |Hi!             |
             +--------+-------------------------------------------------+----------------+

             +-------------------------------------------------+
             |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
             +--------+-------------------------------------------------+----------------+
             |00000000| 48 65 6c 6c 6f 20 77 6f 72 6c 64                |Hello world     |
             +--------+-------------------------------------------------+----------------+
         */


    }

    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);  //内容
    }
}





如有错误,欢迎指出!!!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值