一文吃透 Netty 处理粘包拆包的核心原理与实践

520 阅读15分钟

大家好!今天来一起聊聊 Netty 中如何解决 TCP 粘包拆包问题。这个问题算是 Java 网络编程中的一个"障碍",不少小伙伴在这里栽过跟头,甚至有经验的开发者也会在这个问题上踩坑。本文从问题本质出发,结合具体案例,手把手教你搞定 Netty 粘包拆包问题。

一、什么是 TCP 粘包拆包问题?

在深入 Netty 的解决方案前,先得搞清楚问题到底是什么。

1.1 问题描述

想象一下这个场景:你用 TCP 发送了两条消息"Hello"和"World",但接收方可能收到的是:

  • "HelloWorld"(粘包)
  • "Hel"和"loWorld"(拆包)
  • "HelloWor"和"ld"(粘包+拆包)

这就是所谓的 TCP 粘包拆包现象。就像快递员把两个包裹捆在一起送(粘包),或者把一个大包裹分成几次送(拆包)。

1.2 为什么会发生?

这个问题主要有几个原因:

  1. TCP 是流协议:TCP 只关心字节流的传输,不会在数据包之间保留边界信息,就像水管中的水流,你无法区分哪些水是上一秒的,哪些是这一秒的。

  2. 操作系统的缓冲区机制:TCP 为提高网络效率,会将数据放入缓冲区。这时 Nagle 算法可能会将多个小数据包合并成一个再发送。Nagle 算法主要优化小分组传输,通过延迟发送减少网络小包数量,可通过设置TCP_NODELAY选项关闭它。

  3. MSS 限制:如果数据超过最大分段大小(Maximum Segment Size,简称 MSS),TCP 会将其拆分成多个包发送。MSS = MTU(最大传输单元,通常为 1500 字节)- IP 头(20 字节)- TCP 头(20 字节),所以标准以太网的 MSS 通常为 1460 字节。

二、Netty 解决粘包拆包的核心思路

Netty 解决这个问题的核心思想很简单:在消息中添加边界信息。这就像在信件中加上页码和总页数,让收信人知道这是第几页,总共有几页。

具体实现有以下几种方式:

2.1 基于分隔符的解码器

这种方式类似我们在文本处理中使用分隔符分割数据。比如我们用 Excel 导出 CSV 文件时,用逗号分隔每个字段,用换行符分隔每一行。

Netty 提供了:

  • LineBasedFrameDecoder:使用换行符作为分隔符,就像聊天记录一样,一行一条消息
  • DelimiterBasedFrameDecoder:使用自定义分隔符,可以自己决定用什么符号来区分消息

2.2 基于长度字段的解码器

这种方式在消息头部包含"长度字段",明确指出消息体的长度。类似于快递包裹上的重量标签,告诉你这个包裹有多重。

Netty 提供了:

  • LengthFieldBasedFrameDecoder:通过解析长度字段确定消息边界,灵活性最高

2.3 基于固定长度的解码器

如果所有消息都是固定长度,可以直接按固定长度切分,就像固定规格的信封,每个都一样大。

Netty 提供了:

  • FixedLengthFrameDecoder:将入站数据按固定长度切分

三、四种解码器详解与使用案例

下面通过具体案例详解四种解码器的使用方法。

3.1 LineBasedFrameDecoder(基于行分隔符)

这个解码器使用\n\r\n作为分隔符,非常适合基于文本的协议,比如聊天应用、日志传输等。

工作原理

使用案例

public class LineBasedServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 添加行解码器,最大行长度为8192
                     pipeline.addLast(new LineBasedFrameDecoder(8192));
                     // 将ByteBuf转为String
                     pipeline.addLast(new StringDecoder());
                     // 业务处理器
                     pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                             System.out.println("收到消息: " + msg);
                             // 回复消息
                             ctx.writeAndFlush("已收到: " + msg + "\n");
                         }
                     });
                 }
             });

            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

客户端测试

public class LineBasedClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();
                     pipeline.addLast(new LineBasedFrameDecoder(8192));
                     pipeline.addLast(new StringDecoder());
                     // 明确添加StringEncoder避免依赖默认行为
                     pipeline.addLast(new StringEncoder());
                 }
             });

            Channel ch = b.connect("localhost", 8888).sync().channel();

            // 发送三条消息,每条消息以\n结尾
            ch.writeAndFlush("你好,Netty!\n");
            ch.writeAndFlush("这是第二条消息\n");
            ch.writeAndFlush("这是最后一条消息\n");

            ch.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

注意事项

  • 如果消息内容中本身包含\n,会导致误判为多条消息
  • 必须确保发送端添加正确的换行符
  • 8192 是最大行长度,超过会抛出异常,可根据业务需求调整

3.2 DelimiterBasedFrameDecoder(基于自定义分隔符)

这个解码器允许我们使用自定义的分隔符,比如$_$##END##等,更加灵活。但注意,分隔符越长,扫描查找的开销就越大,对性能影响也越明显。对于长分隔符(超过 10 字节),建议优先使用长度字段解码器,避免扫描时间线性增长导致性能下降。

使用案例:使用1745479423701.jpg作为分隔符

public class DelimiterBasedServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 定义分隔符
                     ByteBuf delimiter = Unpooled.copiedBuffer("$_$", CharsetUtil.UTF_8);

                     // 添加基于分隔符的解码器
                     pipeline.addLast(new DelimiterBasedFrameDecoder(8192, delimiter));
                     pipeline.addLast(new StringDecoder());
                     pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                             System.out.println("收到消息: " + msg);
                             ctx.writeAndFlush("已收到: " + msg + "$_$");
                         }
                     });
                 }
             });

            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

解决分隔符冲突问题: 如果分隔符可能出现在消息内容中,有两种常见的解决方法:

1745480114575.jpg

  1. 使用二进制分隔符:选择极少出现在原始数据中的字节序列,如 0xFF,0xFF

实际应用场景

  • 自定义文本协议
  • 遗留系统集成,需要兼容已有的协议格式

3.3 FixedLengthFrameDecoder(固定长度解码器)

这个解码器适用于所有消息都是固定长度的场景,比如有些老旧工控系统的通信协议。由于不需要扫描查找分隔符,它的性能通常是最高的,内存占用也最稳定。

使用案例:假设所有消息长度为 20 字节

public class FixedLengthServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 固定长度为20字节的解码器
                     pipeline.addLast(new FixedLengthFrameDecoder(20));
                     // 增加ReadTimeoutHandler处理接收不足情况
                     pipeline.addLast(new ReadTimeoutHandler(60)); // 60秒超时
                     pipeline.addLast(new StringDecoder());
                     pipeline.addLast(new SimpleChannelInboundHandler<String>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                             System.out.println("收到固定长度消息: " + msg);
                         }
                     });
                 }
             });

            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

客户端发送固定长度消息示例

// 填充消息到固定长度的示例
public void sendFixedLengthMessage(Channel channel, String message) {
    byte[] content = message.getBytes();
    // 创建20字节的缓冲区
    ByteBuf buf = Unpooled.buffer(20);
    // 写入原始内容
    buf.writeBytes(content);
    // 如果不足20字节,用0填充
    // 注意:填充字符(0或空格)必须与服务端解析逻辑一致
    buf.writeZero(20 - content.length);
    channel.writeAndFlush(buf);
}

注意事项

  • 必须确保发送的消息正好是 20 字节,不足需要填充(通常用空格或 0 填充)
  • 填充字符的选择(0 或空格)需要客户端和服务端达成一致,避免解析错误
  • 如果接收不到完整的 20 字节,解码器会一直等待,可能导致超时
  • 添加ReadTimeoutHandler可以防止永久等待导致的资源泄露

3.4 LengthFieldBasedFrameDecoder(基于长度字段的解码器)

这是最灵活也是最常用的解码器,适用于各种复杂协议。它通过解析消息中的长度字段来确定消息体的长度,算法时间复杂度为 O(1),不需要像分隔符解码器那样扫描整个缓冲区(O(n)),所以性能更高。

工作原理

graph TD
    A[接收字节流] --> B[定位长度字段]
    B --> C[解析长度值]
    C --> D[根据长度提取消息体]
    D --> E[传递给下一个处理器]

参数说明

参数名含义如何理解(生活类比)
maxFrameLength最大帧长度快递公司规定单个包裹最大重量,超过不收
lengthFieldOffset长度字段偏移量快递单上"重量"字段前面有收件人、地址等信息,需要跳过这些才能找到重量
lengthFieldLength长度字段长度重量标签占用多少空间(1 位数、2 位数还是更多)
lengthAdjustment长度调整值重量标签表示的是净重还是毛重,可能需要加上或减去包装重量
initialBytesToStrip解码后跳过的字节数收件人拿到快递后是否需要撕掉快递单后才能用内容

常见协议格式

| 长度字段(4字节) | 实际内容 |

使用案例

public class LengthFieldServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 添加基于长度字段的解码器
                     // 参数分别是:最大帧长度,长度字段偏移,长度字段长度,长度调整值,跳过的初始字节数
                     pipeline.addLast(new LengthFieldBasedFrameDecoder(
                         1024 * 1024, // 设置1MB的最大帧长度,防止内存溢出
                         0, 4, 0, 4
                     ) {
                         // 增加异常处理
                         @Override
                         protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
                             try {
                                 return super.decode(ctx, in);
                             } catch (TooLongFrameException e) {
                                 System.err.println("消息太长,关闭连接: " + e.getMessage());
                                 ctx.close();
                                 return null;
                             }
                         }
                     });

                     // 添加自定义处理器
                     pipeline.addLast(new SimpleChannelInboundHandler<ByteBuf>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
                             byte[] bytes = new byte[msg.readableBytes()];
                             msg.readBytes(bytes);
                             System.out.println("收到消息: " + new String(bytes, CharsetUtil.UTF_8));
                         }
                     });
                 }
             });

            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

客户端发送示例

public class LengthFieldClient {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     // LengthFieldPrepender自动添加长度字段
                     ch.pipeline().addLast(new LengthFieldPrepender(4));
                 }
             });

            Channel ch = b.connect("localhost", 8888).sync().channel();

            // 方式1:通过LengthFieldPrepender自动处理长度字段
            String message = "Hello, Netty with LengthField!";
            ByteBuf buf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
            ch.writeAndFlush(buf);

            // 方式2:手动构造包含长度字段的二进制消息
            String message2 = "Manual length field message";
            byte[] content = message2.getBytes(CharsetUtil.UTF_8);
            ByteBuf buf2 = Unpooled.buffer();
            buf2.writeInt(content.length); // 4字节长度字段(大端)
            buf2.writeBytes(content);
            ch.writeAndFlush(buf2);

            ch.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

字节序处理

长度字段在网络传输中通常使用大端序(网络字节序),这是 TCP/IP 协议的标准约定,Java 默认也使用大端序。大端序指高位字节在前,低位字节在后,便于跨平台兼容。如果需要兼容一些使用小端序的老系统,可以明确指定使用小端序方法:

// 读取小端序长度字段
int length = in.readIntLE();

// 编码小端序长度字段
out.writeIntLE(contentLength);

一个完整的 LengthFieldBasedFrameDecoder 应用实例

下面是一个更复杂但更实用的例子,我们设计一个简单的协议:

| 魔数(2字节) | 版本(1字节) | 长度字段(4字节) | 消息类型(1字节) | 消息内容 | CRC校验(4字节) |

协议结构图:

这里选择0x9527作为魔数,而不是0x00000xFFFF这样的常见值,是为了降低与其他数据误判的概率。很多知名协议都有自己的魔数,如 HTTP/2 使用"PRI "(ASCII 码为 0x50524920)作为魔数,Elasticsearch 使用 0xEs 开头的魔数。版本号放在魔数之后,便于将来协议升级时,根据版本号选择不同的解码逻辑。

LengthFieldBasedFrameDecoder 参数计算表:

参数名含义计算方式当前协议值备注
lengthFieldOffset长度字段偏移魔数(2)+版本(1)3跳过前 3 字节才是长度字段
lengthFieldLength长度字段长度固定大小4长度字段占 4 字节(int)
lengthAdjustment长度调整值长度字段后其他头部字段长度1长度值仅表示内容长度,不包括消息类型(1 字节)
initialBytesToStrip跳过字节数0 表示保留完整消息0保留完整消息体,包括头部

服务端代码:

public class ComplexProtocolServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline pipeline = ch.pipeline();

                     // 魔数(2字节) + 版本(1字节) = 3字节偏移
                     // 长度字段为4字节
                     // 长度字段后还有消息类型(1字节),所以调整值为1
                     // 我们保留协议头,所以不跳过任何字节
                     pipeline.addLast(new LengthFieldBasedFrameDecoder(
                         65535, 3, 4, 1, 0));

                     // 添加自定义协议解码器
                     pipeline.addLast(new ProtocolDecoder());

                     // 业务处理器
                     pipeline.addLast(new SimpleChannelInboundHandler<ProtocolMessage>() {
                         @Override
                         protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) {
                             System.out.println("收到消息: " + msg);
                             // 业务处理...
                         }
                     });

                     // 添加心跳检测(可选)
                     pipeline.addLast(new IdleStateHandler(30, 0, 0));
                     pipeline.addLast(new ChannelInboundHandlerAdapter() {
                         @Override
                         public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
                             if (evt instanceof IdleStateEvent) {
                                 System.out.println("30秒未收到数据,关闭连接");
                                 ctx.close();
                             }
                         }
                     });
                 }
             });

            ChannelFuture f = b.bind(8888).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    // 协议消息类
    static class ProtocolMessage {
        private short magic; // 魔数
        private byte version; // 版本
        private int length; // 长度
        private byte type; // 消息类型
        private byte[] content; // 消息内容
        private int crc; // CRC校验

        // 省略getter/setter和toString
    }

    // 协议解码器
    static class ProtocolDecoder extends ByteToMessageDecoder {
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
            // 步骤1: 确保有足够的字节可读
            if (in.readableBytes() < 12) { // 头部固定部分长度
                return;
            }

            // 步骤2: 记录读索引,便于重置
            in.markReaderIndex();

            // 步骤3: 解析头部各字段
            short magic = in.readShort();
            byte version = in.readByte();
            int length = in.readInt(); // 默认大端序(网络字节序)
            byte type = in.readByte();

            // 步骤4: 校验魔数
            if (magic != 0x9527) {
                in.resetReaderIndex();
                throw new CorruptedFrameException("非法的魔数: " + magic);
            }

            // 步骤5: 协议版本处理
            if (version != 0x01) {
                // 根据版本号选择不同的解码逻辑
                if (version == 0x02) {
                    System.out.println("使用V2版本解码器");
                    // V2版本解码逻辑
                } else {
                    throw new CorruptedFrameException("不支持的协议版本: " + version);
                }
            }

            // 步骤6: 确保消息体和CRC都可读
            if (in.readableBytes() < length + 4) {
                in.resetReaderIndex();
                return;
            }

            // 步骤7: 读取消息体
            byte[] content = new byte[length];
            in.readBytes(content);

            // 步骤8: 读取和验证CRC
            int crc = in.readInt();

            // 计算CRC
            CRC32 crc32 = new CRC32();
            crc32.update(content);
            int calculatedCrc = (int) crc32.getValue();

            if (crc != calculatedCrc) {
                throw new CorruptedFrameException("CRC校验失败");
            }

            // 步骤9: 创建消息对象并传递
            ProtocolMessage message = new ProtocolMessage();
            message.magic = magic;
            message.version = version;
            message.length = length;
            message.type = type;
            message.content = content;
            message.crc = crc;

            // 将消息传递给下一个处理器
            out.add(message);
        }
    }
}

客户端编码示例:

// 协议编码器
class ProtocolEncoder extends MessageToByteEncoder<ProtocolMessage> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf out) {
        // 写入魔数
        out.writeShort(msg.getMagic());
        // 写入版本
        out.writeByte(msg.getVersion());
        // 写入长度
        out.writeInt(msg.getContent().length);
        // 写入类型
        out.writeByte(msg.getType());
        // 写入内容
        out.writeBytes(msg.getContent());

        // 计算并写入CRC
        CRC32 crc32 = new CRC32();
        crc32.update(msg.getContent());
        out.writeInt((int) crc32.getValue());
    }
}

这个例子展示了如何使用 LengthFieldBasedFrameDecoder 处理复杂的自定义协议,实现了可靠的消息边界识别,同时通过 CRC 校验增强了数据完整性验证。

四、解码器选型对比

在实际项目中,如何选择合适的解码器?下面是一个简单的对比:

各解码器使用场景对比:

解码器类型适用场景优点缺点内存占用字节序处理支持协议升级推荐消息格式GC 压力代表性应用
LineBasedFrameDecoder基于行的文本协议简单直观分隔符冲突中等(扫描缓冲区)不涉及有限文本HTTP 头解析、日志传输
DelimiterBasedFrameDecoder自定义分隔符协议灵活可定制分隔符冲突中等(扫描缓冲区)不涉及有限文本高(多次扫描)自定义文本协议
FixedLengthFrameDecoder固定长度消息性能最佳,简单浪费空间,不灵活低(固定缓冲区)不涉及文本/二进制低(固定缓冲区)古老工控协议、银行交易系统
LengthFieldBasedFrameDecoder带长度字段的协议灵活高效配置复杂中(按需分配)需指定大端/小端是(通过版本号)二进制中(按需分配)RPC 框架、大多数二进制协议

五、解码器性能与异常处理

5.1 性能对比

解码器性能差异主要体现在处理算法复杂度上:

  • FixedLengthFrameDecoder: O(1),直接按固定长度切分,性能最高
  • LengthFieldBasedFrameDecoder: O(1),直接读取长度字段,性能高
  • DelimiterBasedFrameDecoder/LineBasedFrameDecoder: O(n),需要扫描整个缓冲区查找分隔符,性能较低

在相同硬件条件下,基于长度字段的解码器处理 100 字节消息的吞吐量通常比基于分隔符的高出 40%-60%。特别是在高并发场景下,这种差异会更加明显。

5.2 异常处理示例

解码器可能抛出的异常及处理方式:

// 异常处理示例
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4) {
    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
        try {
            return super.decode(ctx, in);
        } catch (TooLongFrameException e) {
            // 帧太长异常
            logger.error("消息长度超过限制: {}", e.getMessage());
            // 可以返回错误响应
            ctx.writeAndFlush(Unpooled.copiedBuffer("消息长度超出限制", CharsetUtil.UTF_8));
            // 关闭连接防止攻击
            ctx.close();
            return null;
        } catch (CorruptedFrameException e) {
            // 数据损坏异常
            logger.error("数据格式错误: {}", e.getMessage());
            ctx.close();
            return null;
        }
    }
});

5.3 内存分配与流量控制

不同解码器的内存分配方式有很大差异:

  • FixedLengthFrameDecoder:使用固定大小缓冲区,内存占用稳定,适合资源受限环境
  • LengthFieldBasedFrameDecoder:根据长度字段按需分配内存,适合变长消息
  • DelimiterBasedFrameDecoder:在搜索过程中可能产生内存碎片,增加 GC 压力

在高流量场景下,可以结合 Netty 的流量控制机制避免内存溢出:

// 设置高水位线和低水位线,单位为字节
b.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024); // 64KB
b.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024);  // 32KB

// 对于使用池化内存的场景,可以调整JVM参数
// -Dio.netty.allocator.type=pooled
// -XX:MaxDirectMemorySize=256m

六、总结

解码器类型适用场景优点缺点代表性应用实现难度扩展性
LineBasedFrameDecoder基于行的文本协议简单直观分隔符冲突HTTP 头解析
DelimiterBasedFrameDecoder自定义分隔符协议灵活可定制分隔符冲突自定义文本协议
FixedLengthFrameDecoder固定长度消息性能最佳,实现简单浪费空间,不灵活固定格式传输最低最低
LengthFieldBasedFrameDecoder带长度字段的协议灵活性最高,性能好配置复杂RPC 框架,大多数二进制协议