1. 概述

1.1 粘包

发送 abc def,接收 abcdef

原因

  • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
  • Nagle 算法:会造成粘包

1.2 半包

现象,发送 abcdef,接收 abc def

原因

  • 应用层:接收方 ByteBuf 小于实际发送数据量
  • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
  • 滑动窗口: 发送方维护一个发送窗口,而接收方维护一个接收窗口。发送窗口的大小取决于接收方通知的窗口大小,而接收窗口的大小取决于系统资源和当前状态。
  • Nagle 算法: 发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由; 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送

MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数,链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同, MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

1.3 解决方案

  • 短链接 发送完报文 就断开 然后重连 在发, 缺点是性能不好
  • 每一条消息采用固定长度,缺点是浪费空间
  • 每一条消息采用分隔符,例如 \n,缺点是不是分隔符的\n字符需要转义
  • 每一条消息发消息长度+消息,根据消息中的长度读取后面的消息

2 代码实例

2.1 粘包/半包实例

2.1.1 服务器
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FrameDecoderServer {
    void start() {
        // 创建两个EventLoopGroup,一个用于接收连接(boss),一个用于处理连接(worker)
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置服务器引导程序
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class); // 指定使用的通道类型
            serverBootstrap.group(boss, worker); // 关联EventLoopGroup

            // 配置子通道处理器
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 添加处理日志的处理器
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    // 添加自定义的处理连接激活和断开的处理器
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected {}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect {}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                    // 添加字符串解码器
                    ch.pipeline().addLast(new StringDecoder());
                    // 添加处理实际业务逻辑的处理器
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                            System.out.println("Client received: " + msg);
                        }
                    });
                }
            });

            // 绑定端口并等待服务器启动
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            log.debug("{} binding...", channelFuture.channel());
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());

            // 等待服务器关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            // 关闭EventLoopGroup
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("stopped");
        }
    }

    /**
     * 程序入口点。
     * 创建FrameDecoderServer实例并启动服务器。
     *
     * @param args 命令行参数
     */
    public static void main(String[] args)   {
        new FrameDecoderServer().start();
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
2.1.2 粘包客户端
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FrameDecoderClient {
    public static void main(String[] args) {
        // 创建一个 NIO 事件循环组,用于处理客户端的 I/O 事件。
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置客户端启动器,设置通道类型、事件循环组和处理器。
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class); // 指定使用 NIO Socket 通道。
            bootstrap.group(worker); // 设置事件循环组。
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connected...");
                    // 添加自定义的入站处理程序,负责在通道激活时发送数据。
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        /**
                         * 当通道变得可写时,发送数据到服务器。
                         * @param ctx 通道上下文,用于分配缓冲区和写入数据。
                         * @throws Exception 如果操作过程中发生异常。
                         */
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 循环10次,发送相同的数据。
                            for (int i = 0; i < 10; i++) {
                                // 分配一个缓冲区,并写入固定长度的数据。
                                ByteBuf buffer = ctx.alloc().buffer();
                                buffer.writeBytes("11111111".getBytes());
                                // 写入缓冲区并刷新通道,确保数据被发送。
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            // 连接到服务器并等待连接完成。
            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();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

运行截图:

Netty 粘包/拆包、解码工具类_数据

2.1.2 半包客户端
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FrameDecoderClient {
    public static void main(String[] args) {
        // 创建一个 NIO 事件循环组,用于处理客户端的 I/O 事件。
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置客户端启动器,设置通道类型、事件循环组和处理器。
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class); // 指定使用 NIO Socket 通道。
            bootstrap.group(worker); // 设置事件循环组。
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connected...");
                    // 添加自定义的入站处理程序,负责在通道激活时发送数据。
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        /**
                         * 当通道变得可写时,发送数据到服务器。
                         * @param ctx 通道上下文,用于分配缓冲区和写入数据。
                         * @throws Exception 如果操作过程中发生异常。
                         */
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 循环10次,发送相同的数据。
                            for (int i = 0; i < 1; i++) {
                                // 分配一个缓冲区,并写入固定长度的数据。
                                ByteBuf buffer = ctx.alloc().buffer();
                                StringBuilder str = new StringBuilder();
                                for (int j = 0; j < 10000; j++) {
                                    str.append("1");
                                }
                                buffer.writeBytes(str.toString().getBytes());
                                // 写入缓冲区并刷新通道,确保数据被发送。
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            // 连接到服务器并等待连接完成。
            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();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.

Netty 粘包/拆包、解码工具类_网络_02

3. 解决方案

3.1 固定长度

每一条消息采用固定长度,缺点是浪费空间

3.1.1 服务端代码

ch.pipeline().addLast(new FixedLengthFrameDecoder(8)); 固定长度8字节

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FixedLengthFrameDecoderServer {
    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected {}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect {}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                    ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                            System.out.println("Client received: " + msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            log.debug("{} binding...", channelFuture.channel());
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("stoped");
        }
    }

    public static void main(String[] args)   {
        new FixedLengthFrameDecoderServer().start();
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
3.1.2 客户端

客户端代码同2.1.2

3.1.3

运行截图:

Netty 粘包/拆包、解码工具类_bootstrap_03

3.1.3 FixedLengthFrameDecoder 源码

FixedLengthFrameDecoder decode 解码时,每次读取固定长度

protected Object decode(
            @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        if (in.readableBytes() < frameLength) {
            return null;
        } else {
            return in.readRetainedSlice(frameLength);
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

看看怎么调用的

io.netty.handler.codec.ByteToMessageDecoder#channelRead
  • 1.

Netty 粘包/拆包、解码工具类_.net_04

callDecode(ctx, cumulation, out);
  • 1.

Netty 粘包/拆包、解码工具类_数据_05

io.netty.handler.codec.ByteToMessageDecoder#callDecode

// 读取所有数据
while (in.isReadable())

// 可读数据小于定长 break
if (outSize == out.size()) {
	if (oldInputLength == in.readableBytes()) {
    	break;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

Netty 粘包/拆包、解码工具类_.net_06

io.netty.handler.codec.ByteToMessageDecoder#decodeRemovalReentryProtection
  • 1.

调用

io.netty.handler.codec.FixedLengthFrameDecoder#decode`
  • 1.

Netty 粘包/拆包、解码工具类_数据_07

3.2 分隔符

每一条消息采用分隔符,例如 \n,缺点是不是分隔符的\n字符需要转义

ch.pipeline().addLast(new LineBasedFrameDecoder(1024));

3.2.1 服务端代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LineBasedFrameDecoderServer {
    void start() {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected {}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect {}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                            System.out.println("Client received: " + msg);
                        }
                    });
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            log.debug("{} binding...", channelFuture.channel());
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("stoped");
        }
    }

    public static void main(String[] args)   {
        new LineBasedFrameDecoderServer().start();
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
3.2.2 客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FrameDecoderClient {
    public static void main(String[] args) {
        // 创建一个 NIO 事件循环组,用于处理客户端的 I/O 事件。
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置客户端启动器,设置通道类型、事件循环组和处理器。
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class); // 指定使用 NIO Socket 通道。
            bootstrap.group(worker); // 设置事件循环组。
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connected...");
                    // 添加自定义的入站处理程序,负责在通道激活时发送数据。
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        /**
                         * 当通道变得可写时,发送数据到服务器。
                         * @param ctx 通道上下文,用于分配缓冲区和写入数据。
                         * @throws Exception 如果操作过程中发生异常。
                         */
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 循环10次,发送相同的数据。
                            for (int i = 0; i < 10; i++) {
                                // 分配一个缓冲区,并写入固定长度的数据。
                                ByteBuf buffer = ctx.alloc().buffer();
                                StringBuilder str = new StringBuilder();
                                for (int j = 0; j < 9; j++) {
                                    str.append("1");
                                }
                                str.append("\n");
                                buffer.writeBytes(str.toString().getBytes());
                                // 写入缓冲区并刷新通道,确保数据被发送。
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            // 连接到服务器并等待连接完成。
            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();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
3.2.3 运行截图

Netty 粘包/拆包、解码工具类_bootstrap_08

3.2.4 LineBasedFrameDecoder#decode

ByteProcessor.FIND_LF 就是 ‘\n’

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        final int eol = findEndOfLine(buffer);
        if (!discarding) {
            if (eol >= 0) {
                final ByteBuf frame;
                final int length = eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;

                if (length > maxLength) {
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }

                if (stripDelimiter) {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    frame = buffer.readRetainedSlice(length + delimLength);
                }

                return frame;
            } else {
                final int length = buffer.readableBytes();
                if (length > maxLength) {
                    discardedBytes = length;
                    buffer.readerIndex(buffer.writerIndex());
                    discarding = true;
                    offset = 0;
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                return null;
            }
        } else {
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    fail(ctx, length);
                }
            } else {
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
                // We skip everything in the buffer, we need to set the offset to 0 again.
                offset = 0;
            }
            return null;
        }
    }

    private void fail(final ChannelHandlerContext ctx, int length) {
        fail(ctx, String.valueOf(length));
    }

    private void fail(final ChannelHandlerContext ctx, String length) {
        ctx.fireExceptionCaught(
                new TooLongFrameException(
                        "frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')'));
    }

    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
3.3 长度域解码器

每一条消息发消息长度+消息,根据消息中的长度读取后面的消息

// 添加基于长度字段的帧解码器,用于解码客户端发送的数据
            // 解码器配置:最大帧长度1000,长度字段起始位置0,长度字段长度2字节,不进行长度调整,跳过2个字节
            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
                    1000, 0, 2, 0, 2));
  • 1.
  • 2.
  • 3.
  • 4.
3.3.1 服务端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

@Slf4j
/**
 * 该类实现了基于长度字段的帧解码器服务器。
 */
public class LengthFieldBasedFrameDecoderServer {
    /**
     * 启动服务器。
     */
    void start() {
        // 创建Boss线程组和Worker线程组
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置服务器引导程序
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    // 添加日志处理器
                    ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                    // 添加自定义的ChannelInboundHandlerAdapter,处理连接激活和不活跃事件
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("connected {}", ctx.channel());
                            super.channelActive(ctx);
                        }

                        @Override
                        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("disconnect {}", ctx.channel());
                            super.channelInactive(ctx);
                        }
                    });
                    // 添加基于长度字段的帧解码器,用于解码客户端发送的数据
                    // 解码器配置:最大帧长度1000,长度字段起始位置0,长度字段长度2字节,不进行长度调整,跳过2个字节
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
                            1000, 0, 2, 0, 2));
                    // 添加字符串解码器,将字节转换为字符串
                    ch.pipeline().addLast(new StringDecoder());
                    // 添加自定义的ChannelInboundHandler,处理读取到的字符串消息
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                            System.out.println("Client received: " + msg);
                        }
                    });
                }
            });
            // 绑定端口并等待连接
            ChannelFuture channelFuture = serverBootstrap.bind(8080);
            log.debug("{} binding...", channelFuture.channel());
            channelFuture.sync();
            log.debug("{} bound...", channelFuture.channel());
            // 等待服务器关闭
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            // 关闭线程组
            boss.shutdownGracefully();
            worker.shutdownGracefully();
            log.debug("stopped");
        }
    }

    /**
     * 程序入口。
     * @param args 命令行参数
     */
    public static void main(String[] args)   {
        new LengthFieldBasedFrameDecoderServer().start();
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
3.3.2 客户端代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FrameDecoderClient {
    public static void main(String[] args) {
        // 创建一个 NIO 事件循环组,用于处理客户端的 I/O 事件。
        NioEventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 配置客户端启动器,设置通道类型、事件循环组和处理器。
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class); // 指定使用 NIO Socket 通道。
            bootstrap.group(worker); // 设置事件循环组。
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    log.debug("connected...");
                    // 添加自定义的入站处理程序,负责在通道激活时发送数据。
                    ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        /**
                         * 当通道变得可写时,发送数据到服务器。
                         * @param ctx 通道上下文,用于分配缓冲区和写入数据。
                         * @throws Exception 如果操作过程中发生异常。
                         */
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 循环10次,发送相同的数据。
                            for (int i = 0; i < 10; i++) {
                                // 分配一个缓冲区,并写入固定长度的数据。
                                ByteBuf buffer = ctx.alloc().buffer();
                                StringBuilder str = new StringBuilder();
                                for (int j = 0; j < 9; j++) {
                                    str.append("1");
                                }
                                buffer.writeShort(9);
                                buffer.writeBytes(str.toString().getBytes());
                                // 写入缓冲区并刷新通道,确保数据被发送。
                                ctx.writeAndFlush(buffer);
                            }
                        }
                    });
                }
            });
            // 连接到服务器并等待连接完成。
            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();
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
3.3.3 运行截图

Netty 粘包/拆包、解码工具类_.net_09