
上一篇【第08篇】LengthFieldBasedFrameDecoder——Netty最强帧解码器全攻略下一篇【第10篇】Netty私有协议栈开发——从零设计一套企业级通信协议摘要如果您用过Java的ObjectOutputStream做网络传输大概率遇到过这样的崩溃场景加了transient的字段被序列化了、不同版本JDK序列化不兼容、一个简单对象序列化后竟然500字节……Java默认序列化有三大缺陷性能差、体积大、跨语言难。Netty提供了完善的编解码框架支持Protobuf最高效、JSON最可读、JBoss Marshalling最兼容等多种序列化方案。本文详解各种编解码器的使用方法并给出自定义编解码器的最佳模板。一、Java序列化——为什么Netty不用它1.1 Java默认序列化的三大缺陷【Java序列化 vs 其他方案对比】 指标 Java序列化 Protobuf JSON JBoss ──────────────────────────────────────────────────────────── 性能序列化速度 ❌ 最慢 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ 体积序列化后大小❌ 最大 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ 跨语言支持 ❌ 仅Java ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ❌ 仅Java 兼容性 ⚠️ 弱 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐三大缺陷详解性能差反射 递归遍历对象速度是Protobuf的10倍以上慢体积大序列化后的字节流包含完整的类名、字段名体积通常是Protobuf的3-5倍跨语言难只有Java能反序列化其他语言Python/Go/C无法解析1.2 Netty为什么废弃了ObjectEncoder/ObjectDecoderNetty 4.x之前内置了ObjectEncoder和ObjectDecoder基于Java序列化但在Netty 4.1.x中标记为Deprecated。// ❌ 不推荐用法已废弃pipeline.addLast(newObjectEncoder());pipeline.addLast(newObjectDecoder(ClassResolvers.cacheDisabled()));pipeline.addLast(newBusinessHandler());// 处理Java对象结论生产环境不要用Java序列化用Protobuf或JSON替代。二、Protobuf——性能之王Google ProtobufProtocol Buffers是Netty中最推荐的序列化方案被Dubbo/gRPC等顶级框架采用。2.1 Protobuf的核心优势【Protobuf编码原理】 Java对象 User { int id 123; String name Alice; int age 25; } Java序列化后约80字节 [流开始][类名][字段类型][字段名][字段值]...包含大量元数据 Protobuf序列化后约15字节 [字段1标签][字段1值][字段2标签][字段2值]...紧凑的二进制格式核心优势体积小比Java序列化小3-5倍速度快比Java序列化快10倍以上跨语言支持Java/C/Python/Go等20种语言向后兼容新增字段不影响老版本解析2.2 Protobuf集成步骤Step 1定义.proto文件// user.proto syntax proto3; package com.example.netty.protobuf; option java_package com.example.netty.protobuf; option java_outer_classname UserProto; message User { int32 id 1; string name 2; int32 age 3; }Step 2编译.proto文件# 下载protoc编译器$ protoc--java_out./src/main/java user.proto# 生成 UserProto.javaStep 3引入Netty的Protobuf编解码器依赖!-- pom.xml --dependencygroupIdcom.google.protobuf/groupIdartifactIdprotobuf-java/artifactIdversion3.25.0/version/dependencyStep 4在Pipeline中配置Protobuf编解码器// 服务端/客户端Pipeline配置pipeline.addLast(newProtobufVarint32FrameDecoder());// 解决粘包基于varint长度pipeline.addLast(newProtobufDecoder(UserProto.User.getDefaultInstance()));// 解码器pipeline.addLast(newProtobufVarint32LengthFieldPrepender());// 编码器加长度字段pipeline.addLast(newProtobufEncoder());// 编码器pipeline.addLast(newBusinessHandler());// 业务Handler处理User对象2.3 完整示例Protobuf通信// 服务端HandlerpublicclassProtobufServerHandlerextendsChannelInboundHandlerAdapter{OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){UserProto.Useruser(UserProto.User)msg;System.out.println(服务端收到Useruser.getId(), user.getName());// 构造响应UserProto.UserresponseUserProto.User.newBuilder().setId(user.getId()).setName(Hello, user.getName()).setAge(user.getAge()1).build();ctx.writeAndFlush(response);}}// 客户端发送消息UserProto.UseruserUserProto.User.newBuilder().setId(123).setName(Alice).setAge(25).build();channel.writeAndFlush(user);三、JSON编解码——最人类可读的方案如果您需要调试方便、跨语言且人类可读JSON是最好的选择。3.1 JSON的优缺点优点缺点人类可读调试方便体积比Protobuf大跨语言支持极好性能比Protobuf差生态成熟Jackson/Fastjson没有强类型约束3.2 使用Jackson编解码依赖配置dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion2.17.0/version/dependency自定义JSON编解码器// JsonDecoderByteBuf → Java对象publicclassJsonDecoderextendsByteToMessageDecoder{privatefinalObjectMappermappernewObjectMapper();privatefinalClass?clazz;publicJsonDecoder(Class?clazz){this.clazzclazz;}Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){if(in.readableBytes()4)return;in.markReaderIndex();intlengthin.readInt();// 假设前面有4字节长度字段if(in.readableBytes()length){in.resetReaderIndex();return;}byte[]bytesnewbyte[length];in.readBytes(bytes);try{Objectobjmapper.readValue(bytes,clazz);out.add(obj);}catch(Exceptione){thrownewCodecException(JSON解码失败,e);}}}// JsonEncoderJava对象 → ByteBufpublicclassJsonEncoderextendsMessageToByteEncoderObject{privatefinalObjectMappermappernewObjectMapper();Overrideprotectedvoidencode(ChannelHandlerContextctx,Objectmsg,ByteBufout){try{byte[]bytesmapper.writeValueAsBytes(msg);out.writeInt(bytes.length);// 写入长度字段out.writeBytes(bytes);}catch(Exceptione){thrownewCodecException(JSON编码失败,e);}}}Pipeline配置pipeline.addLast(newLengthFieldBasedFrameDecoder(1024*1024,0,4,0,4));pipeline.addLast(newJsonDecoder(User.class));pipeline.addLast(newJsonEncoder());pipeline.addLast(newBusinessHandler());四、JBoss Marshalling——高性能的Java专用方案如果您确定只在Java环境间通信JBoss Marshalling是比Java序列化更快、更省空间的方案。4.1 JBoss Marshalling集成依赖配置dependencygroupIdorg.jboss.marshalling/groupIdartifactIdjboss-marshalling/artifactIdversion2.0.12.Final/version/dependencydependencygroupIdorg.jboss.marshalling/groupIdartifactIdjboss-marshalling-serial/artifactIdversion2.0.12.Final/version/dependencyNetty内置支持// 创建JBoss Marshalling编解码器MarshallerFactoryfactoryMarshalling.getProvidedMarshallerFactory(serial);MarshallingConfigurationconfignewMarshallingConfiguration();config.setVersion(5);// Pipeline配置pipeline.addLast(newMarshallingDecoder(factory,config,1024*1024));pipeline.addLast(newMarshallingEncoder(factory,config));pipeline.addLast(newBusinessHandler());五、自定义编解码器——最佳模板如果您有特殊的协议需求可以自定义编解码器。以下是经过生产验证的最佳模板。5.1 自定义解码器模板继承ByteToMessageDecoder/** * 自定义消息解码器模板 * 协议格式[魔数(4字节)][版本(1字节)][类型(1字节)][长度(4字节)][消息体(N字节)] */publicclassMyMessageDecoderextendsByteToMessageDecoder{// 协议常量privatestaticfinalintMAGIC_NUMBER0x12345678;privatestaticfinalintHEADER_SIZE10;// 魔数4 版本1 类型1 长度4 10字节Overrideprotectedvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){// 1. 检查是否有足够的字节读取消息头if(in.readableBytes()HEADER_SIZE){return;// 数据不够等待更多数据}// 2. 标记当前读取位置如果消息体不完整要回滚in.markReaderIndex();// 3. 读取消息头intmagicin.readInt();if(magic!MAGIC_NUMBER){thrownewCodecException(魔数不匹配Integer.toHexString(magic));}byteversionin.readByte();if(version!1){thrownewCodecException(不支持的协议版本version);}bytetypein.readByte();intlengthin.readInt();// 4. 检查消息体是否完整if(in.readableBytes()length){in.resetReaderIndex();// 回滚读取位置return;// 消息体不完整等待更多数据}// 5. 读取消息体byte[]bodynewbyte[length];in.readBytes(body);// 6. 构造消息对象并交给下一个HandlerMyMessagemsgnewMyMessage();msg.setVersion(version);msg.setType(type);msg.setBody(body);out.add(msg);}OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){// 解码异常关闭连接System.err.println(解码异常cause.getMessage());ctx.close();}}5.2 自定义编码器模板继承MessageToByteEncoder/** * 自定义消息编码器模板 */publicclassMyMessageEncoderextendsMessageToByteEncoderMyMessage{Overrideprotectedvoidencode(ChannelHandlerContextctx,MyMessagemsg,ByteBufout){// 1. 写入魔数out.writeInt(MyMessage.MAGIC_NUMBER);// 2. 写入版本out.writeByte(msg.getVersion());// 3. 写入类型out.writeByte(msg.getType());// 4. 写入长度 消息体byte[]bodymsg.getBody();out.writeInt(body.length);out.writeBytes(body);}}5.3 Pipeline配置pipeline.addLast(newLengthFieldBasedFrameDecoder(1024*1024,6,4,0,10));pipeline.addLast(newMyMessageDecoder());pipeline.addLast(newMyMessageEncoder());pipeline.addLast(newBusinessHandler());六、编解码框架的性能对比与选型建议6.1 性能基准测试JMH【序列化性能对比越小越好】 序列化方案 序列化速度ops/ms 反序列化速度ops/ms 序列化后体积字节 ────────────────────────────────────────────────────────────── Java序列化 120 150 580 JSON(Jackson) 350 400 280 JBoss 800 900 320 Protobuf 1500 1800 1506.2 选型建议场景推荐方案理由高性能RPC如DubboProtobuf体积小、速度快、跨语言调试方便如管理后台JSON人类可读、调试方便Java专用系统JBoss Marshalling比Java序列化快、兼容性好特殊协议需求自定义编解码器完全控制协议格式总结Java序列化三大缺陷性能差、体积大、跨语言难生产环境不要用Protobuf是性能之王体积小、速度快、跨语言推荐在高性能RPC中使用JSON最人类可读调试方便适合管理后台等场景自定义编解码器继承ByteToMessageDecoder和MessageToByteEncoder参考本文模板下一步学习如何设计一套完整的企业级私有协议栈第010篇上一篇【第08篇】LengthFieldBasedFrameDecoder——Netty最强帧解码器全攻略下一篇【第10篇】Netty私有协议栈开发——从零设计一套企业级通信协议