- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
导读
在 Netty 应用级开发中,字符串、JSON 与 XML 是常见的数据载体,但当业务需要以自定义 Java 对象直接在客户端与服务端之间传输时,直接使用 Java 原生序列化会在性能上付出较大代价。本文基于 CodeGuide 仓库中的 Netty4.1 中级拓展篇三《Netty 传输 Java 对象》 展开,讲解如何借助 protostuff-core 工具包将自定义 POJO 以二进制形式编码传输,并手写ObjEncoder/ObjDecoder编解码器挂载到 Netty Pipeline。读完本文你将掌握:为什么弃用 Java 原生序列化、protostuff 与原生 protobuf 的差异、序列化工具类的完整实现、编解码器与粘包半包的处理逻辑,以及该方案在仓库手写 RPC 框架中的复用形态。
一、为什么传输 Java 对象要选择 protostuff
Netty 的ChannelHandler链路中,writeAndFlush写入的对象最终都要落成字节流,因此"对象 -> 字节 -> 对象"的序列化方案直接决定通信的性能与通用性。
1.1 Java 原生序列化的痛点
Java 自带的ObjectOutputStream/ObjectInputStream序列化会把完整的类描述信息、继承体系、访问修饰符等元数据一并写入字节流,序列化结果体积大、速度慢,且要求目标类实现Serializable接口。在高并发、长连接的 IM、RPC、网关等场景中,这种开销会被放大为明显的性能损耗。仓库 分布式 IM 即时通信系统 与 手写 RPC 框架 都明确采用了 protostuff 二进制流来保证传输性能。
1.2 protostuff 是什么
protostuff 基于 Google protobuf 设计,但提供了更简易的用法与更丰富的功能:
- 支持 protostuff-compiler 生成的 message;
- 支持现有的 POJO 对象(无需编写 .proto 文件);
- 支持现有的 protoc 生成的 Java 消息;
- 具备与移动平台(Android、Kindle、j2me)的互操作能力;
- 支持转码(可按照 protobuf 配置序列化成 JSON / YAML / XML 等格式)。
其中最关键的是protostuff-runtime模块:它实现了无需预编译即可对 Java Bean 进行 protobuf 序列化 / 反序列化的能力,原理是在运行时通过RuntimeSchema.createFrom(cls)反射生成对象的 Schema。这与原生 protobuf 必须先写.proto文件再用protoc编译出代码的方式形成鲜明对比。
1.3 protostuff 的两点局限(务必牢记)
- 序列化前需预先传入 schema:
RuntimeSchema的生成有成本,不能每个对象都现算,必须做缓存; - 反序列化不负责对象的创建,只负责复制字段:也就是说目标类必须提供默认构造函数,否则无法完成反序列化。这一点在定义传输对象(如
MsgInfo)时必须保留无参构造器。
关于性能:文档说明 protostuff 在性能上不输原生 protobuf,甚至可能反超,但本文不引用外部基准数据,仅以方案选型逻辑说明其适用性。
二、开发环境
本文案例的运行前提(以仓库文档与案例源码为准):
- jdk1.8(jdk1.7 以下只能部分支持 netty);
- Netty 4.1.36.Final(netty3.x / 4.x / 5.x 每次变化较大,接口与类名也随之变化,务必锁定版本);
- 依赖 protostuff-core(序列化)、protostuff-runtime(运行时 Schema)、objenesis(免构造器实例化)。
对应的 Maven 核心依赖示意:
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.36.Final</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>org.objenesis</groupId> <artifactId>objenesis</artifactId> <version>2.6</version> </dependency>三、案例工程结构
案例模块名为itstack-demo-netty-2-03,包路径org.itstack.demo.netty,完整的工程树如下:
itstack-demo-netty-2-03 └── src ├── main │ └── java │ └── org.itstack.demo.netty │ ├── client │ │ ├── MyChannelInitializer.java │ │ ├── MyClientHandler.java │ │ └── NettyClient.java │ ├── codec │ │ ├── ObjDecoder.java │ │ └── ObjEncoder.java │ ├── domain │ │ └── MsgInfo.java │ ├── server │ │ ├── MyChannelInitializer.java │ │ ├── MyServerHandler.java │ │ └── NettyServer.java │ └── util │ ├── MsgUtil.java │ └── SerializationUtil.java │ └── test └── java └── org.itstack.demo.test └── ApiTest.java各模块职责划分:
| 目录 | 类 | 职责 |
|---|---|---|
| domain | MsgInfo | 自定义传输 POJO,包含 channelId 与 msgContent |
| codec | ObjEncoder / ObjDecoder | 基于 ByteBuf 的对象编解码器,挂载到 Pipeline |
| util | SerializationUtil | 基于 protostuff 的序列化 / 反序列化工具类 |
| util | MsgUtil | 消息对象构建工具 |
| client / server | ChannelInitializer、Handler、启动类 | 通信两端完整启动与收发逻辑 |
四、核心实现:SerializationUtil 序列化工具类
序列化的正确性全部集中在 SerializationUtil.java 中,它是整个方案的基石,核心要点有三个:Schema 缓存、LinkedBuffer 复用、Objenesis 无构造器实例化。
public class SerializationUtil { // Schema 缓存:避免每次序列化都反射生成 Schema private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<>(); // Objenesis:反序列化时绕过构造器创建对象实例 private static Objenesis objenesis = new ObjenesisStd(); private SerializationUtil() { } /** * 序列化(对象 -> 字节数组) */ public static <T> byte[] serialize(T obj) { Class<T> cls = (Class<T>) obj.getClass(); LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); try { Schema<T> schema = getSchema(cls); return ProtostuffIOUtil.toByteArray(obj, schema, buffer); } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } finally { buffer.clear(); } } /** * 反序列化(字节数组 -> 对象) */ public static <T> T deserialize(byte[] data, Class<T> cls) { try { T message = objenesis.newInstance(cls); Schema<T> schema = getSchema(cls); ProtostuffIOUtil.mergeFrom(data, message, schema); return message; } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } } private static <T> Schema<T> getSchema(Class<T> cls) { Schema<T> schema = (Schema<T>) cachedSchema.get(cls); if (schema == null) { schema = RuntimeSchema.createFrom(cls); cachedSchema.put(cls, schema); } return schema; } }4.1 逐点拆解
cachedSchema(Schema 缓存):RuntimeSchema.createFrom(cls)会通过反射遍历类的字段构建 Schema,成本较高。这里用ConcurrentHashMap做类级别缓存,保证同一个类全局只生成一次 Schema,同时天然线程安全。LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE):protostuff 序列化时需要一个可增长的缓冲区,LinkedBuffer用链表式分块缓冲,DEFAULT_BUFFER_SIZE为默认分块大小;每次使用后在finally中buffer.clear()以便复用,避免频繁分配内存。ProtostuffIOUtil.toByteArray:将对象按 schema 写入 buffer,返回紧凑的 protobuf 二进制字节数组,体积远小于 Java 原生序列化产物。objenesis.newInstance(cls):反序列化时先创建目标类实例。protostuff 的mergeFrom只做"字段复制",不负责 new 对象,因此这一步由 Objenesis 完成——它甚至可以绕过构造器直接分配实例。ProtostuffIOUtil.mergeFrom(data, message, schema):把字节数组按 schema 解析并填充到已创建实例的字段上。
由此可以明确一个使用约束:传输的 POJO 必须提供默认构造函数(尽管 Objenesis 能绕过构造器,但按文档说明,反序列化只负责复制,稳妥起见目标类应保留无参构造)。同时不要对没有无参构造器的复杂对象直接使用本工具。
五、核心实现:自定义编解码器 ObjEncoder 与 ObjDecoder
protostuff 只负责"对象 <-> 字节数组",而字节数组要放进 Netty 的ByteBuf才能通过 Pipeline 传输。由于 TCP 是字节流,多帧数据可能粘连或拆散,编解码器还必须处理半包 / 粘包问题。
5.1 自定义传输帧格式
本案例采用经典的"4 字节长度头 + 数据体"帧结构:
+----------------+-----------------------------+ | dataLength(4) | data(protobuf 二进制字节) | +----------------+-----------------------------+- 发送(编码):先写入
data.length(int,4 字节),再写入序列化后的字节数组; - 接收(解码):先读 4 字节长度,再按长度截取完整数据体,最后交给
SerializationUtil.deserialize还原对象。
这一设计与仓库 手写 RPC 框架第二章 中的RpcDecoder/RpcEncoder完全一致,属于该系列案例统一的传输协议规范。
5.2 编码器 ObjEncoder(出站)
继承MessageToByteEncoder,在出站时把对象编码为"长度 + 数据":
public class ObjEncoder extends MessageToByteEncoder { private Class<?> genericClass; public ObjEncoder(Class<?> genericClass) { this.genericClass = genericClass; } @Override protected void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) { // 只处理指定类型的对象,避免误编码其他出站消息 if (genericClass.isInstance(in)) { byte[] data = SerializationUtil.serialize(in); out.writeInt(data.length); // 先写 4 字节长度 out.writeBytes(data); // 再写数据体 } } }注意genericClass.isInstance(in)的类型校验:Pipeline 中除了业务对象,还可能经过其他出站消息(如心跳、关闭指令等),编码器只对MsgInfo类型的对象生效,起到隔离作用。
5.3 解码器 ObjDecoder(入站)
继承ByteToMessageDecoder,入站时处理半包、粘包:
public class ObjDecoder extends ByteToMessageDecoder { private Class<?> genericClass; public ObjDecoder(Class<?> genericClass) { this.genericClass = genericClass; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 1. 可读字节不足 4 字节(连长度头都不完整),等待下一次数据到达 if (in.readableBytes() < 4) { return; } // 2. 标记当前读位置,便于长度不足时回退 in.markReaderIndex(); int dataLength = in.readInt(); // 3. 数据体尚未收全,回退读指针,等待剩余数据 if (in.readableBytes() < dataLength) { in.resetReaderIndex(); return; } // 4. 数据完整,读取数据体并反序列化 byte[] data = new byte[dataLength]; in.readBytes(data); out.add(SerializationUtil.deserialize(data, genericClass)); } }5.4 半包粘包处理逻辑拆解
- 半包:一个对象的数据被拆成多次到达。解码器先判断
readableBytes() < 4(长度头不完整)或readableBytes() < dataLength(数据体不完整),直接return,剩余数据会缓存在 ByteBuf 中,等下一次数据到达后继续处理;markReaderIndex()/resetReaderIndex()保证在数据不足时读指针能回退到安全位置。 - 粘包:多个对象的数据粘连在一次到达。
ByteToMessageDecoder的循环调用机制保证解码器会反复触发decode,直到缓冲区中的字节不足以组成一个完整帧为止,从而把粘连的多帧数据逐个拆出。
这两个类与 RPC 框架中的 RpcDecoder.java、RpcEncoder.java逻辑完全同构,可以互相印证这套编解码方案的通用性。
六、传输对象 MsgInfo 与消息构建
6.1 传输对象定义
public class MsgInfo { private String channelId; // 通道标识(客户端连接 ID) private String msgContent; // 消息内容 public MsgInfo() { // 必须保留默认构造器(protostuff 反序列化要求) } public MsgInfo(String channelId, String msgContent) { this.channelId = channelId; this.msgContent = msgContent; } public String getChannelId() { return channelId; } public void setChannelId(String channelId) { this.channelId = channelId; } public String getMsgContent() { return msgContent; } public void setMsgContent(String msgContent) { this.msgContent = msgContent; } }6.2 消息构建工具 MsgUtil
public class MsgUtil { public static MsgInfo buildMsg(String channelId, String msgContent) { return new MsgInfo(channelId, msgContent); } }channelId携带发送方通道 ID,msgContent携带业务内容。服务端和客户端都通过MsgUtil.buildMsg构造消息,保持两端构造逻辑一致。
七、Pipeline 装配与服务端 / 客户端实现
7.1 ChannelInitializer:编解码器的挂载点
无论是客户端还是服务端,Pipeline 的装配逻辑一致:先挂编解码器,再挂业务 Handler。
客户端 MyChannelInitializer.java:
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel channel) throws Exception { // 对象传输处理:解码器在前、编码器在后,最后是业务处理器 channel.pipeline().addLast(new ObjDecoder(MsgInfo.class)); channel.pipeline().addLast(new ObjEncoder(MsgInfo.class)); channel.pipeline().addLast(new MyClientHandler()); } }服务端 MyChannelInitializer.java:
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel channel) { // 对象传输处理 channel.pipeline().addLast(new ObjDecoder(MsgInfo.class)); channel.pipeline().addLast(new ObjEncoder(MsgInfo.class)); // 在管道中添加我们自己的接收数据实现方法 channel.pipeline().addLast(new MyServerHandler()); } }Pipeline 中 Handler 的添加顺序是有讲究的:入站时先经过
ObjDecoder完成字节 -> 对象转换,再进入业务ChannelInboundHandlerAdapter;出站时对象先经过ObjEncoder编码成字节,再写回对端。这也是为什么业务 Handler 中channelRead收到的msg已经是MsgInfo类型,不再需要自己解码。
7.2 业务 Handler:链接报告与消息接收
客户端与服务端的 Handler 结构一致,以客户端 MyClientHandler.java 为例:
public class MyClientHandler extends ChannelInboundHandlerAdapter { /** * 当客户端主动链接服务端的链接后,这个通道就是活跃的了 */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { SocketChannel channel = (SocketChannel) ctx.channel(); System.out.println("链接报告开始"); System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id()); System.out.println("链接报告IP:" + channel.localAddress().getHostString()); System.out.println("链接报告Port:" + channel.localAddress().getPort()); System.out.println("链接报告完毕"); // 通知客户端链接建立成功 String str = "通知服务端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString(); ctx.writeAndFlush(MsgUtil.buildMsg(channel.id().toString(), str)); } /** * 当客户端主动断开服务端的链接后,这个通道就是不活跃的 */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("断开链接" + ctx.channel().localAddress().toString()); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 接收msg消息{与上一章节相比,此处已经不需要自己进行解码} System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息类型:" + msg.getClass()); System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + JSON.toJSONString(msg)); } /** * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); System.out.println("异常信息:\r\n" + cause.getMessage()); } }这里有一处非常直观的对比:channelRead中接收到的msg已经被ObjDecoder还原成了MsgInfo对象,日志打印的"消息类型"为class org.itstack.demo.netty.domain.MsgInfo,并可直接用JSON.toJSONString(msg)输出对象结构——相比字符串章节需要手工解码,使用对象编解码器后业务层完全无感。
7.3 客户端启动器 NettyClient
public class NettyClient { public static void main(String[] args) { new NettyClient().connect("127.0.0.1", 7397); } private void connect(String inetHost, int inetPort) { EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.AUTO_READ, true); b.handler(new MyChannelInitializer()); ChannelFuture f = b.connect(inetHost, inetPort).sync(); System.out.println("itstack-demo-netty client start done."); // 连续发送多条消息,验证二进制对象传输 f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(), "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(), "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(), "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(), "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(), "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。")); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); } } }要点说明:
- 连接地址固定为
127.0.0.1:7397,与NettyServer的绑定端口保持一致; - 建立连接后连续
writeAndFlush五条内容相同的消息,channelActive还会自动发送一条"链接建立成功"通知,共 6 条MsgInfo对象数据流; ChannelOption.AUTO_READ设为true开启自动读取;- 关闭后
shutdownGracefully()优雅释放线程组资源。
7.4 服务端启动器 NettyServer
public class NettyServer { public static void main(String[] args) { new NettyServer().bing(7397); } private void bing(int port) { // 配置服务端NIO线程组 EventLoopGroup parentGroup = new NioEventLoopGroup(); // NioEventLoopGroup extends MultithreadEventLoopGroup Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)) EventLoopGroup childGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(parentGroup, childGroup) .channel(NioServerSocketChannel.class) //非阻塞模式 .option(ChannelOption.SO_BACKLOG, 128) .childHandler(new MyChannelInitializer()); ChannelFuture f = b.bind(port).sync(); System.out.println("itstack-demo-netty server start done."); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { childGroup.shutdownGracefully(); parentGroup.shutdownGracefully(); } } }parentGroup/childGroup双线程组模型:前者负责接收客户端连接,后者负责处理已建立连接的 IO 事件;线程数默认取Math.max(1, 可用处理器数 * 2)(见代码注释中的NettyRuntime.availableProcessors() * 2);SO_BACKLOG = 128设置服务端连接请求等待队列长度;- 服务端 Handler 与客户端对称,
channelActive时向客户端回发"通知客户端链接建立成功",channelRead接收并打印客户端的MsgInfo对象。
八、运行验证与测试结果
按以下顺序运行即可完成验证:
- 先启动
NettyServer(监听 7397 端口); - 再启动
NettyClient(连接 127.0.0.1:7397); - 观察两端的控制台输出。
服务端执行结果(节选,来自原文档实测输出):
itstack-demo-netty server start done. 链接报告开始 链接报告信息:有一客户端链接到本服务端。channelId:eaa23c73 链接报告IP:127.0.0.1 链接报告Port:7397 链接报告完毕 2019-08-04 16:25:48 接收到消息类型:class org.itstack.demo.netty.domain.MsgInfo 2019-08-04 16:25:48 接收到消息内容:{"channelId":"e0a8c2f0","msgContent":"通知服务端链接建立成功 Sun Aug 04 16:25:48 CST 2019 127.0.0.1"} 2019-08-04 16:25:48 接收到消息类型:class org.itstack.demo.netty.domain.MsgInfo 2019-08-04 16:25:48 接收到消息内容:{"channelId":"e0a8c2f0","msgContent":"你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。"} ...(连续 5 条同类消息) 异常信息: 远程主机强迫关闭了一个现有的连接。 客户端断开链接/127.0.0.1:7397客户端执行结果:
链接报告开始 itstack-demo-netty client start done. 链接报告信息:本客户端链接到服务端。channelId:e0a8c2f0 链接报告IP:127.0.0.1 链接报告Port:60886 链接报告完毕 2019-08-04 16:25:48 接收到消息类型:class org.itstack.demo.netty.domain.MsgInfo 2019-08-04 16:25:48 接收到消息内容:{"channelId":"eaa23c73","msgContent":"通知客户端链接建立成功 Sun Aug 04 16:25:48 CST 2019 127.0.0.1\r\n"}结果解读:
- 两端互相收到的消息类型均为
MsgInfo,证明对象经过序列化、二进制传输、反序列化全链路后完整还原; - 服务端连续收到 6 条
MsgInfo(1 条连接建立通知 + 5 条业务消息),说明编码器/解码器在连续多帧数据下工作正常,粘包拆包逻辑正确; - 消息内容中的
channelId与对端打印的 channelId 一一对应(如服务端打印 channelIdeaa23c73,客户端收到消息的 channelId 正是eaa23c73),链路信息完整; - 客户端主动关闭后,服务端捕获到"远程主机强迫关闭了一个现有的连接"异常并打印"客户端断开链接",属于
exceptionCaught与channelInactive的正常表现。
九、从案例到框架:protostuff 方案在仓库中的延伸应用
这套"长度头 + protostuff 二进制"的传输方案并不是孤立案例,而是仓库中多个实战项目的公共基础设施,可以从源码结构上印证其通用性:
9.1 手写 RPC 框架
在 手写RPC框架第二章《netty通信》 中:
RpcDecoder(继承ByteToMessageDecoder)与RpcEncoder(继承MessageToByteEncoder)的decode/encode实现与本案例的ObjDecoder/ObjEncoder逐行一致;SerializationUtil同样基于com.dyuproject.protostuff的LinkedBuffer、ProtostuffIOUtil、RuntimeSchema实现,并同样使用ConcurrentHashMap缓存 Schema。
这说明:本文的编码器与序列化工具天然适合直接搬到 RPC 通信层复用,只需把genericClass换成 RPC 的Request/Response对象即可。
9.2 分布式 IM 即时通信系统
在 给学习加点实践,开发一个分布式IM即时通信系统 的协议工程中,agreement模块同样包含codec/ObjDecoder.java、codec/ObjEncoder.java与util/SerializationUtil.java,并在此基础上进一步引入Packet抽象类与Command指令映射:packetType用ConcurrentHashMap把指令字节映射到具体的请求/响应类,从而支持登录、消息、加好友、群聊、断线重连等多种协议对象的统一编码传输。
从该结构可以推断:当系统需要传输多种 Java 对象时,仅靠单个
genericClass的编解码器是不够的,业界惯用做法是给每个对象类型分配一个"帧标识"(指令字节),解码端先读指令再反射出对应类,这正是 IM 协议模块的设计思路,可作为本文方案的进阶演进方向。
十、注意事项与使用约束汇总
- 必须提供默认构造函数:protostuff 反序列化只负责字段复制,目标 POJO 需要默认构造器(
MsgInfo中的无参构造不能删除); - 泛型类型要匹配:
ObjEncoder/ObjDecoder构造时传入的genericClass必须与 Pipeline 中实际传输的对象类型一致,否则编码器会因isInstance校验而静默跳过; - Pipeline 顺序:解码器应放在业务入站 Handler 之前,编码器应放在业务出站 Handler 之前;
- 帧长度用 int:4 字节长度头决定了单帧数据体上限约 2GB,足够日常业务;若传输超大对象需自行调整帧结构;
- 不要给无字段的接口或抽象类直接序列化:
RuntimeSchema需要从具体类的字段构建,传输对象应为具体 POJO; - 版本兼容性:案例基于 Netty 4.1.36.Final,Netty 3.x/4.x/5.x 的 API 差异较大,生产环境请锁定与案例一致的 Netty 版本族。
十一、总结
本文完整还原了 CodeGuide 仓库中"Netty 传输 Java 对象"案例的实现链路:从"为什么不用 Java 原生序列化"的选型分析,到SerializationUtil的 Schema 缓存与 Objenesis 实例化,再到ObjEncoder/ObjDecoder的长度头帧协议与半包粘包处理,最后给出完整的服务端、客户端与 Pipeline 装配代码,并提供了仓库 RPC 框架与 IM 系统中的延伸应用证据。这套"4 字节长度头 + protostuff 二进制"方案无需编写.proto文件即可实现高性能的 Java 对象传输,可作为自研 RPC、网关、IM 等通信类中间件的基础编码层直接复用。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
grpc-gateway 二进制文件上传实战:基于 ServeMux.HandlePath 自定义路由的完整方案
grpc gateway 二进制文件上传实战:基于 ServeMux.HandlePath 自定义路由的完整方案 本指南围绕 grpc gateway 官方文档
后端API网关开发工具gRPCNetty4.1 ChunkedStream 数据流切块传输实战:基于 CodeGuide 中级拓展篇十一的源码级解析
Netty4.1 ChunkedStream 数据流切块传输实战:基于 CodeGuide 中级拓展篇十一的源码级解析 本篇技术指南围绕小傅哥 CodeGuid
文档教程后端CodeGuide 开源仓库 Netty 实战入门:基于 Netty4.1 从零搭建第一个 NettyServer
CodeGuide 开源仓库 Netty 实战入门:基于 Netty4.1 从零搭建第一个 NettyServer 本篇技术指南以开源仓库 CodeGuide
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考