本节摘要:LengthFieldBasedFrameDecoder 的五个参数是自定义协议的黄金标准,本节逐个参数画图讲透;随后装配 HTTP 与 WebSocket 的现成工序组,最后把 Protobuf 序列化嵌入流水线,给出"传输帧 + 序列化"的正交组合观。
LengthFieldBasedFrameDecoder(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip) 是 Netty 里参数最"迷"的类,值得用图说话。设帧格式为 [魔数 2B][版本 1B][长度 4B][载荷 NB]:
// 帧头 7 字节中,长度域从第 3 字节开始、宽 4 字节、长度值只算载荷 new LengthFieldBasedFrameDecoder( 1024 * 1024, // maxFrameLength:单帧上限,安全阀 3, // lengthFieldOffset:长度域在帧内的偏移 4, // lengthFieldLength:长度域本身占 4 字节(int) 0, // lengthAdjustment:长度值之后还有多少字节属于"帧长的一部分" 0); // initialBytesToStrip:解码后丢弃帧头多少字节(0 = 全保留)
五个参数的分工:offset 与 length 定位长度域;adjustment 弥补"长度值含义"与"帧总长"的差——若长度值=总帧长,adjustment 要填 -7(减去帧头);strip 决定交给下游的 ByteBuf 是否还带帧头。调这五个参数的心法:先在纸上画出字节布局,标出长度域覆盖范围,再对应填表。
选型时补一句:长度域宽度按业务上限选——1 字节最多 255,2 字节 64KB,4 字节 2GB;绝大多数协议选 4 字节 int,超大文件传输则该走分片协议而不是无限加宽。
自定义协议之外,Netty 对主流协议提供了"工序全家桶"。HTTP 服务端的典型装配:
ch.pipeline() .addLast(new HttpServerCodec()) // 编解码一体:请求解析 + 响应编码 // 可选工序:聚合分块的 HTTP 消息、压缩、限制内容长度 .addLast(new HttpObjectAggregator(65536)) // 把分块聚成 FullHttpRequest .addLast(new HttpContentCompressor()) // gzip 压缩 .addLast(new SimpleChannelInboundHandler<FullHttpRequest>() { @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) { byte[] body = "hello from netty workshop".getBytes(StandardCharsets.UTF_8); FullHttpResponse resp = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(body)); resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, body.length); resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=utf-8"); ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE); } });
要点有二:HTTP 消息可能分块到达(HttpRequest + 多个 HttpContent),不加 Aggregator 就得自己拼;响应务必带 Content-Length,否则客户端不知道何时算完(除非用 chunked 编码)。
WebSocket 的握手是一个 HTTP Upgrade 请求,之后同一条连接切换为帧协议。Netty 用一个"协议切换工位"完成变身:
ch.pipeline() .addLast(new HttpServerCodec()) .addLast(new HttpObjectAggregator(65536)) .addLast(new WebSocketServerProtocolHandler("/ws")) // 握手、心跳、帧处理全包 .addLast(new TextWebSocketFrameHandler()); public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) { ctx.writeAndFlush(new TextWebSocketFrame("echo: " + frame.text())); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { // 握手完成事件:此刻起流水线上 HTTP 工位已经退役 if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { System.out.println("ws 握手完成 协议切换"); } } }
注意第三章的热插拔在这里的真实演出:握手完成后,WebSocketServerProtocolHandler 会把 HTTP 相关工位从流水线上撤下,后续全是 WebSocket 帧——协议升级就是流水线改造,不是魔法。
边界与协议解决"切",载荷里放什么则是序列化的领地。Protobuf 是 Netty 生态的常客:
ch.pipeline() .addLast(new ProtobufVarint32LengthPrepender()) // 出站:补 varint 长度域 .addLast(new ProtobufDecoder(User.Cmd.getDefaultInstance())) // 入站:字节转 Protobuf 对象 .addLast(new ProtobufEncoder()) // 出站:对象转字节 .addLast(new BizHandler()); // 此时收到的已是强类型对象
对比三种常见序列化的取舍:Java 原生序列化能跑但体积大、跨语言无望;JSON 可读、调试友好,但解析慢、数值类型松散;Protobuf 体积小、强 schema、跨语言,代价是要维护 IDL 与生成工具链。判断式很直接——对外开放接口选 JSON(可读优先),内部高频 RPC 选 Protobuf(性能优先),原型验证随意。
⚠️ 常见坑:Protobuf 解码器装配顺序错(Prepender 必须在 Encoder 之前对应出站方向),现象是对方收到裸字节流解析失败。装完先用 EmbeddedChannel 打个来回再联调,五分钟省两小时。
💡 关键直觉:协议栈是正交的两层——传输帧(边界怎么切)与序列化(载荷放什么)。LengthField + Protobuf、LineBased + JSON,自由组合,互不干涉。设计新协议时分层考虑,别把两件事搅在一起。