【NIO与Netty】网络编程:netty中粘包、半包现象展示,分析及解决

一、粘包现象

服务端

public static void main(String[] args) {
    NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);
    NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);
    try {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(bossGroup,workerGroup);
        serverBootstrap.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 (InterruptedException e) {
        log.debug("Server erro {}",e);
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

客户端:希望发送 10 个消息,每个消息是 16 字节

public static void main(String[] args) {
    NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(workerGroup);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        for (int i = 0; i < 10; i++) {
                            ByteBuf buffer = ctx.alloc().buffer(16);
                            buffer.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,0,'a','b','c','d','e','f'});
                            ctx.writeAndFlush(buffer);
                        }
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        workerGroup.shutdownGracefully();
    }
}

服务端输出:一次就接收了 160 个字节,而非分 10 次接收

09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] REGISTERED
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] ACTIVE
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] READ: 160B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000010| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000020| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000030| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000040| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000050| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000060| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000070| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000080| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000090| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
+--------+-------------------------------------------------+----------------+
09:54:51 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xc88b198b, L:/127.0.0.1:8080 - R:/127.0.0.1:55393] READ COMPLETE

二、半包现象

服务端

  • serverBootstrap.option(ChannelOption.SO_RCVBUF, 10) :影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍
public static void main(String[] args) {
    NioEventLoopGroup bossGroup=new NioEventLoopGroup(1);
    NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);
    try {
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        //调整系统底层接收缓冲区(即滑动窗口)大小
        serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(bossGroup,workerGroup);
        serverBootstrap.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 (InterruptedException e) {
        log.debug("Server erro {}",e);
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

客户端:希望发送 1 个消息,这个消息是 160 字节

public static void main(String[] args) {
    NioEventLoopGroup workerGroup=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(workerGroup);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        ByteBuf buffer = ctx.alloc().buffer();
                        for (int i = 0; i < 10; i++) {
                            buffer.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,0,'a','b','c','d','e','f'});
                        }
                        ctx.writeAndFlush(buffer);
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1",8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        workerGroup.shutdownGracefully();
    }
}

服务端输出:接收的消息被分为两节,第一次 20 字节,第二次 140 字节

10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] REGISTERED
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] ACTIVE
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ: 20B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 00 61 62 63 64 65 66 |..........abcdef|
|00000010| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ COMPLETE
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ: 140B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000010| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000020| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000030| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000040| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000050| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000060| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000070| 05 06 07 08 09 00 61 62 63 64 65 66 01 02 03 04 |......abcdef....|
|00000080| 05 06 07 08 09 00 61 62 63 64 65 66             |......abcdef    |
+--------+-------------------------------------------------+----------------+
10:06:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xba1ffac6, L:/127.0.0.1:8080 - R:/127.0.0.1:55530] READ COMPLETE

三、现象分析

粘包

  • 现象,发送 abc def,接收 abcdef
  • 原因
    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle 算法:延迟发送,因为数据比协议头还小

半包

  • 现象,发送 abcdef,接收 abc def
  • 原因
    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
    • MSS 限制:当发送的数据超过 链路层MSS 限制后,会将数据切分发送,就会造成半包

本质是因为 TCP 是流式协议,消息无边界

滑动窗口

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

    在这里插入图片描述

  • 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

    在这里插入图片描述

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用

    • 图中深色的部分即要发送的数据,高亮的部分即窗口
    • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
    • 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动
    • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

Nagle 算法:粘包发送端

  • 即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
    • 如果 TCP_NODELAY = true,则需要发送
    • 已发送的数据都收到 ack 时,则需要发送
    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送
    • 除上述情况,延迟发送

MSS 限制:半包

  • 链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如

  • 以太网的 MTU 是 1500

  • FDDI(光纤分布式数据接口)的 MTU 是 4352

  • 本地回环地址的 MTU 是 65535 - 本地测试不走网卡

  • MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数

  • ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460

  • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送

  • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

在这里插入图片描述

四、解决方案

💡 处理消息的边界

在这里插入图片描述

  • 第一种思路是固定消息长度:数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  • 第二种思路是按分隔符拆分:缺点是效率低
  • 第三种思路是TLV 格式:即 Type 类型、Length 长度、Value 数据。类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    • Http 1.1 是 TLV 格式
    • Http 2.0 是 LTV 格式

方案1:短链接解决粘包

  • 发一次消息建立一次连接,这样连接建立到连接断开之间就是消息的边界
    • 缺点:效率太低
    • 缺点:还会存在半包问题

1.1 解决粘包

客户端

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) { //建立10次连接发送10次数据
        connect();
    }
}
private static void connect() {
    NioEventLoopGroup group=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(group);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                log.debug("连接成功!");
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        ByteBuf buffer = ctx.alloc().buffer();
                        buffer.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18});
                        ctx.writeAndFlush(buffer);
                        //发送完关闭连接
                        ctx.close();
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        group.shutdownGracefully();
    }
}

服务端

public static void main(String[] args) {
    NioEventLoopGroup boss=new NioEventLoopGroup(1);
    NioEventLoopGroup worker=new NioEventLoopGroup(2);

    try {
        ServerBootstrap serverBootstrap=new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(boss,worker);
        serverBootstrap.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 (InterruptedException e) {
        log.debug("Server error {}",e);
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

输出:以下内容输出10次,解决粘包问题

13:24:21 [DEBUG] [nioEventLoopGroup-3-2] i.n.h.l.LoggingHandler : [id: 0x045cf95e, L:/127.0.0.1:8080 - R:/127.0.0.1:57708] READ: 18B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 |................|
|00000010| 11 12                                           |..              |
+--------+-------------------------------------------------+----------------+

1.2 存在半包问题

  • 半包用这种办法还是不好解决,因为接收方的缓冲区大小是有限的

服务端:改变接收缓冲区的大小

public static void main(String[] args) {
    NioEventLoopGroup boss=new NioEventLoopGroup(1);
    NioEventLoopGroup worker=new NioEventLoopGroup(2);

    try {
        ServerBootstrap serverBootstrap=new ServerBootstrap();
        //调整系统底层接收缓冲区(即滑动窗口)大小
        //serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
        //调整netty接收缓冲区的大小:最小取16
        serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(boss,worker);
        serverBootstrap.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 (InterruptedException e) {
        log.debug("Server error {}",e);
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

输出:以下内容输出10次,出现半包问题

13:25:58 [DEBUG] [nioEventLoopGroup-3-2] i.n.h.l.LoggingHandler : [id: 0x816efb49, L:/127.0.0.1:8080 - R:/127.0.0.1:57782] READ: 16B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 |................|
+--------+-------------------------------------------------+----------------+
13:25:58 [DEBUG] [nioEventLoopGroup-3-2] i.n.h.l.LoggingHandler : [id: 0x816efb49, L:/127.0.0.1:8080 - R:/127.0.0.1:57782] READ: 2B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 11 12                                           |..              |
+--------+-------------------------------------------------+----------------+

方案2:协商固定长度

  • 固定长度:应该考虑业务场景中可能出现的最长消息

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

    • 长度定得太大:浪费空间
    • 长度定得太小:对某些数据包又显得不够

客户端:一次性发送8条信息,每条信息长度固定为10。长度不够的信息用’-'分隔符补位

  • 客户端什么时候 flush 都可以
public static void main(String[] args) {
    NioEventLoopGroup group=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(group);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                log.debug("连接成功!");
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        ByteBuf buffer = ctx.alloc().buffer();
                        char c='a';
                        for (int i = 0; i < 8; i++) {
                            buffer.writeBytes(fillBuf(c++, (int) (Math.random()*10)));
                        }
                        //一次性发送8条信息
                        ctx.writeAndFlush(buffer);
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        group.shutdownGracefully();
    }
}
//返回长度10的ByteBuf:其中c字符length个,不足的补'-'
private static ByteBuf fillBuf(char c,int time){
    byte[] buf=new byte[10];
    if(time >10){
        for (int i = 0; i < 10; i++) {
            buf[i]= (byte) c;
        }
    }else{
        for (int i = 0; i < time; i++) {
            buf[i]= (byte) c;
        }
        for (int i = 9; i >= time; i--) {
            buf[i]= '-';
        }
    }
    return ByteBufAllocator.DEFAULT.buffer().writeBytes(buf);
}

服务端:每次只取通道中的10个字节

public static void main(String[] args) {
    NioEventLoopGroup boss=new NioEventLoopGroup(1);
    NioEventLoopGroup worker=new NioEvntLoopGroup(2);

    try {
        ServerBootstrap serverBootstrap=new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(boss,worker);
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                //每次固定接收10个字节
                ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            }
        });
        ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Server error {}",e);
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

输出:每次只取通道中的10个字节

14:13:59 [DEBUG] [nioEventLoopGroup-3-2] i.n.h.l.LoggingHandler : [id: 0xf3308d0c, L:/127.0.0.1:8080 - R:/127.0.0.1:58830] READ: 10B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 2d 2d 2d 2d 2d 2d 2d 2d 2d                   |a---------      |
+--------+-------------------------------------------------+----------------+
14:13:59 [DEBUG] [nioEventLoopGroup-3-2] i.n.h.l.LoggingHandler : [id: 0xf3308d0c, L:/127.0.0.1:8080 - R:/127.0.0.1:58830] READ: 10B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 62 62 62 62 62 62 62 2d                   |bbbbbbbbb-      |
+--------+-------------------------------------------------+----------------+

方案3:协商分隔符

  • LineBasedFrameDecoder:以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常
  • DelimiterBasedFrameDecoder:除了指定长度外,可以自定义指定ByteBuf类型的分隔符
  • 缺点
    • 处理字符数据比较合适。但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误。
    • 需要转义

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

public static void main(String[] args) {
    NioEventLoopGroup group=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(group);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                log.debug("连接成功!");
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        ByteBuf buffer = ctx.alloc().buffer();
                        char c='a';
                        for (int i = 0; i < 3; i++) {
                            //拼接长度 1-32 的随机信息
                            buffer.writeBytes(prodStr(c++, (int) (Math.random()*32)).toString().getBytes());
                        }
                        //一次性发送8条信息
                        ctx.writeAndFlush(buffer);
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        group.shutdownGracefully();
    }
}
//返回由 c 组成的长度为 length 的信息
private static StringBuilder prodStr(char c,int length){
    StringBuilder sb=new StringBuilder();
    for (int i = 0; i < length; i++) {
        sb.append(c);
    }
    sb.append("\n");
    return sb;
}

服务端:使用 \n 解码器 LineBasedFrameDecoder

public static void main(String[] args) {
    NioEventLoopGroup boss=new NioEventLoopGroup(1);
    NioEventLoopGroup worker=new NioEventLoopGroup(2);

    try {
        ServerBootstrap serverBootstrap=new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(boss,worker);
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                //以 \n 或 \r\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常
                ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            }
        });
        ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Server error {}",e);
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

输出

  • 客户端发送的数据包
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 61 61 61 61 0a 62 62 62 62 62 62 62 62 62 |aaaaaa.bbbbbbbbb|
|00000010| 62 62 62 62 62 62 62 62 62 62 62 0a 63 63 63 63 |bbbbbbbbbbb.cccc|
|00000020| 63 63 63 63 63 63 63 63 63 63 63 63 63 63 0a    |cccccccccccccc. |
+--------+-------------------------------------------------+----------------+
  • 服务接收的消息
14:58:33 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0x8da58f26, L:/127.0.0.1:8080 - R:/127.0.0.1:59463] READ: 6B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 61 61 61 61 61                               |aaaaaa          |
+--------+-------------------------------------------------+----------------+
14:58:33 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0x8da58f26, L:/127.0.0.1:8080 - R:/127.0.0.1:59463] READ: 20B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 |bbbbbbbbbbbbbbbb|
|00000010| 62 62 62 62                                     |bbbb            |
+--------+-------------------------------------------------+----------------+
14:58:33 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0x8da58f26, L:/127.0.0.1:8080 - R:/127.0.0.1:59463] READ: 18B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 |cccccccccccccccc|
|00000010| 63 63                                           |cc              |
+--------+-------------------------------------------------+----------------+

方案4:协商预设长度

在发送消息前,先约定消息中 什么位置、多长字节 表示接下来数据的长度

  • LengthFieldBasedFrameDecoder:最大长度,长度偏移,长度占用字节,长度调整,剥离字节数

  • 每一条消息分为 head 和 body,head 中包含 body 的长度

客户端

public static void main(String[] args) {
    NioEventLoopGroup group=new NioEventLoopGroup(2);
    try {
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(group);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                log.debug("连接成功!");
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        ByteBuf buffer = ctx.alloc().buffer();
                        char c='a';
                        for (int i = 0; i < 3; i++) {
                            ByteBuf buf=ctx.alloc().buffer();
                            byte[] bytes = prodStr(c++, (int) (Math.random() * 31)+1).toString().getBytes();
                            //在信息头加入信息长度
                            buf.writeInt(bytes.length);
                            buf.writeBytes(bytes);
                            buffer.writeBytes(buf);
                        }
                        //一次性发送3条信息
                        ctx.writeAndFlush(buffer);
                    }
                });
            }
        });
        ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8080)).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Client erro {}",e);
    } finally {
        group.shutdownGracefully();
    }
}
//返回由 c 组成的长度为 length 的信息
private static StringBuilder prodStr(char c,int length){
    StringBuilder sb=new StringBuilder();
    for (int i = 0; i < length; i++) {
        sb.append(c);
    }
    return sb;
}

服务端

public static void main(String[] args) {
    NioEventLoopGroup boss=new NioEventLoopGroup(1);
    NioEventLoopGroup worker=new NioEventLoopGroup(2);

    try {
        ServerBootstrap serverBootstrap=new ServerBootstrap();
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.group(boss,worker);
        serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                //在消息中存放此消息的长度,协商格式
                // 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
                ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,0,4,0,0));
                ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
            }
        });
        ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        log.debug("Server error {}",e);
    } finally {
        boss.shutdownGracefully();
        worker.shutdownGracefully();
    }
}

输出

  • 客户端发送的数据包
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 1c 61 61 61 61 61 61 61 61 61 61 61 61 |....aaaaaaaaaaaa|
|00000010| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
|00000020| 00 00 00 01 62 00 00 00 03 63 63 63             |....b....ccc    |
+--------+-------------------------------------------------+----------------+
  • 服务端接收的消息
15:07:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xdd9a24d1, L:/127.0.0.1:8080 - R:/127.0.0.1:59595] READ: 32B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 1c 61 61 61 61 61 61 61 61 61 61 61 61 |....aaaaaaaaaaaa|
|00000010| 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 |aaaaaaaaaaaaaaaa|
+--------+-------------------------------------------------+----------------+
15:07:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xdd9a24d1, L:/127.0.0.1:8080 - R:/127.0.0.1:59595] READ: 5B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 01 62                                  |....b           |
+--------+-------------------------------------------------+----------------+
15:07:06 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler : [id: 0xdd9a24d1, L:/127.0.0.1:8080 - R:/127.0.0.1:59595] READ: 7B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 03 63 63 63                            |....ccc         |
+--------+-------------------------------------------------+----------------+
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愿你满腹经纶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值