ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CodeGuide Netty 实战:基于 Protostuff 实现 Netty4.1 Java 对象二进制传输(免 proto 文件的自定义编解码方案)

CodeGuide Netty 实战:基于 Protostuff 实现 Netty4.1 Java 对象二进制传输(免 proto 文件的自定义编解码方案) 文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载导读在 Netty 应用级开发中字符串、JSON 与 XML 是常见的数据载体但当业务需要以自定义 Java 对象直接在客户端与服务端之间传输时直接使用 Java 原生序列化会在性能上付出较大代价。本文基于 CodeGuide 仓库中的 Netty4.1 中级拓展篇三《Netty 传输 Java 对象》 展开讲解如何借助 protostuff-core 工具包将自定义 POJO 以二进制形式编码传输并手写ObjEncoder/ObjDecoder编解码器挂载到 Netty Pipeline。读完本文你将掌握为什么弃用 Java 原生序列化、protostuff 与原生 protobuf 的差异、序列化工具类的完整实现、编解码器与粘包半包的处理逻辑以及该方案在仓库手写 RPC 框架中的复用形态。一、为什么传输 Java 对象要选择 protostuffNetty 的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 的两点局限务必牢记序列化前需预先传入 schemaRuntimeSchema的生成有成本不能每个对象都现算必须做缓存反序列化不负责对象的创建只负责复制字段也就是说目标类必须提供默认构造函数否则无法完成反序列化。这一点在定义传输对象如MsgInfo时必须保留无参构造器。关于性能文档说明 protostuff 在性能上不输原生 protobuf甚至可能反超但本文不引用外部基准数据仅以方案选型逻辑说明其适用性。二、开发环境本文案例的运行前提以仓库文档与案例源码为准jdk1.8jdk1.7 以下只能部分支持 nettyNetty 4.1.36.Finalnetty3.x / 4.x / 5.x 每次变化较大接口与类名也随之变化务必锁定版本依赖 protostuff-core序列化、protostuff-runtime运行时 Schema、objenesis免构造器实例化。对应的 Maven 核心依赖示意dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.36.Final/version /dependency dependency groupIdcom.dyuproject.protostuff/groupId artifactIdprotostuff-core/artifactId version1.1.3/version /dependency dependency groupIdcom.dyuproject.protostuff/groupId artifactIdprotostuff-runtime/artifactId version1.1.3/version /dependency dependency groupIdorg.objenesis/groupId artifactIdobjenesis/artifactId version2.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各模块职责划分目录类职责domainMsgInfo自定义传输 POJO包含 channelId 与 msgContentcodecObjEncoder / ObjDecoder基于 ByteBuf 的对象编解码器挂载到 PipelineutilSerializationUtil基于 protostuff 的序列化 / 反序列化工具类utilMsgUtil消息对象构建工具client / serverChannelInitializer、Handler、启动类通信两端完整启动与收发逻辑四、核心实现SerializationUtil 序列化工具类序列化的正确性全部集中在 SerializationUtil.java 中它是整个方案的基石核心要点有三个Schema 缓存、LinkedBuffer 复用、Objenesis 无构造器实例化。public class SerializationUtil { // Schema 缓存避免每次序列化都反射生成 Schema private static MapClass?, Schema? cachedSchema new ConcurrentHashMap(); // Objenesis反序列化时绕过构造器创建对象实例 private static Objenesis objenesis new ObjenesisStd(); private SerializationUtil() { } /** * 序列化(对象 - 字节数组) */ public static T byte[] serialize(T obj) { ClassT cls (ClassT) obj.getClass(); LinkedBuffer buffer LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); try { SchemaT 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, ClassT cls) { try { T message objenesis.newInstance(cls); SchemaT schema getSchema(cls); ProtostuffIOUtil.mergeFrom(data, message, schema); return message; } catch (Exception e) { throw new IllegalStateException(e.getMessage(), e); } } private static T SchemaT getSchema(ClassT cls) { SchemaT schema (SchemaT) cachedSchema.get(cls); if (schema null) { schema RuntimeSchema.createFrom(cls); cachedSchema.put(cls, schema); } return schema; } }4.1 逐点拆解cachedSchemaSchema 缓存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 与 ObjDecoderprotostuff 只负责对象 - 字节数组而字节数组要放进 Netty 的ByteBuf才能通过 Pipeline 传输。由于 TCP 是字节流多帧数据可能粘连或拆散编解码器还必须处理半包 / 粘包问题。5.1 自定义传输帧格式本案例采用经典的4 字节长度头 数据体帧结构--------------------------------------------- | dataLength(4) | data(protobuf 二进制字节) | ---------------------------------------------发送编码先写入data.lengthint4 字节再写入序列化后的字节数组接收解码先读 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, ListObject 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 消息构建工具 MsgUtilpublic class MsgUtil { public static MsgInfo buildMsg(String channelId, String msgContent) { return new MsgInfo(channelId, msgContent); } }channelId携带发送方通道 IDmsgContent携带业务内容。服务端和客户端都通过MsgUtil.buildMsg构造消息保持两端构造逻辑一致。七、Pipeline 装配与服务端 / 客户端实现7.1 ChannelInitializer编解码器的挂载点无论是客户端还是服务端Pipeline 的装配逻辑一致先挂编解码器再挂业务 Handler。客户端 MyChannelInitializer.javapublic class MyChannelInitializer extends ChannelInitializerSocketChannel { 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.javapublic class MyChannelInitializer extends ChannelInitializerSocketChannel { 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 客户端启动器 NettyClientpublic 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 服务端启动器 NettyServerpublic 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() * 2SO_BACKLOG 128设置服务端连接请求等待队列长度服务端 Handler 与客户端对称channelActive时向客户端回发通知客户端链接建立成功channelRead接收并打印客户端的MsgInfo对象。八、运行验证与测试结果按以下顺序运行即可完成验证先启动NettyServer监听 7397 端口再启动NettyClient连接 127.0.0.1:7397观察两端的控制台输出。服务端执行结果节选来自原文档实测输出itstack-demo-netty server start done. 链接报告开始 链接报告信息有一客户端链接到本服务端。channelIdeaa23c73 链接报告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. 链接报告信息本客户端链接到服务端。channelIde0a8c2f0 链接报告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 条MsgInfo1 条连接建立通知 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 之前帧长度用 int4 字节长度头决定了单帧数据体上限约 2GB足够日常业务若传输超大对象需自行调整帧结构不要给无字段的接口或抽象类直接序列化RuntimeSchema需要从具体类的字段构建传输对象应为具体 POJO版本兼容性案例基于 Netty 4.1.36.FinalNetty 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核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐grpc-gateway 二进制文件上传实战基于 ServeMux.HandlePath 自定义路由的完整方案grpc gateway 二进制文件上传实战基于 ServeMux.HandlePath 自定义路由的完整方案 本指南围绕 grpc gateway 官方文档后端API网关开发工具gRPCNetty4.1 ChunkedStream 数据流切块传输实战基于 CodeGuide 中级拓展篇十一的源码级解析Netty4.1 ChunkedStream 数据流切块传输实战基于 CodeGuide 中级拓展篇十一的源码级解析 本篇技术指南围绕小傅哥 CodeGuid文档教程后端CodeGuide 开源仓库 Netty 实战入门基于 Netty4.1 从零搭建第一个 NettyServerCodeGuide 开源仓库 Netty 实战入门基于 Netty4.1 从零搭建第一个 NettyServer 本篇技术指南以开源仓库 CodeGuide文档教程后端上一篇Apache PredictionIO 代码贡献实战指南从 JIRA 工单、开发环境搭建到 Pull Request 的完整流程下一篇Nx 23 迁移指南将 createNodesV2 导入重命名为 createNodes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表