- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
本篇技术指南以 CodeGuide 开源仓库中 itstack-demo-netty 中级拓展篇二的完整案例为蓝本,讲解如何在 Netty 4.1 服务端与客户端之间使用 Google Protocol Buffers(Protobuf)作为数据传输格式,涵盖MsgInfo.proto协议定义、protoc 编译、ProtobufDecoder/ProtobufEncoder/ProtobufVarint32FrameDecoder/ProtobufVarint32LengthFieldPrepender四个编解码器的管道装配,以及 Protobuf 与 JSON 的互转。读完本文,你将掌握一套"定义 proto → 编译生成类 → 服务端/客户端对称装配编解码器 → 二进制传输"的完整实战方案,并理解它与仓库中字符串解码、自定义编解码器、protostuff 等其他传输方案的差异与适用场景。
一、为什么选择 Protobuf 作为 Netty 的数据传输格式
在 Netty 数据传输过程中可以有很多选择,比如字符串、JSON、XML、Java 对象。但为了保证传输的数据具备良好的通用性、方便的操作性和传输的高性能,可以选择 Protobuf 作为数据传输格式。
Protobuf 是 Google 提出的语言中立、平台中立、可扩展的序列化结构化数据机制——可以把它理解为"更小、更快、更简单"的 XML:只需定义一次数据结构,就可以用生成的源代码在多种数据流、多种语言之间轻松读写这些结构化数据。目前 Protobuf 可以支持 C++、C#、Dart、Go、Java、Python 等语言,也可以在 JS 里使用。
在仓库的基础入门篇章中,作者已经梳理过 Netty 处理半包粘包的常用解码器。参考 基础入门篇三《NettyServer字符串解码器》 的问答可以知道:字符串格式下常用的解码器有三种——LineBasedFrameDecoder(基于换行)、DelimiterBasedFrameDecoder(基于指定字符串)、FixedLengthFrameDecoder(基于字符串长度);而在 String 之外,protobuf 数据格式同样是 Netty 原生支持处理半包粘包的序列化方案之一,这正是本案例存在的意义。
本章节涉及的知识点有:ProtobufDecoder、ProtobufEncoder、ProtobufVarint32FrameDecoder、ProtobufVarint32LengthFieldPrepender。其中前两个负责 Protobuf 消息对象与字节流之间的编解码,后两个负责以 varint32 前缀承载消息长度,从而在传输层划定消息边界、解决半包粘包问题。
二、开发环境与工程结构
环境要求
- jdk1.8:jdk1.7 以下只能部分支持 netty;
- Netty4.1.36.Final:netty3.x、4.x、5.x 每次的变化较大,接口类名也随着变化;
- protoc-3.5.0-win32:用于编译 proto 文件(
protoc -I=源地址 --java_out=目标地址 源地址/xxx.proto),源码中已经提供,其他开发环境可以自行下载对应版本。
需要说明的是:本案例运行在 JDK 8 + Netty 4.1.36 组合下,若你使用的 JDK 或 Netty 版本不同,需要注意接口与依赖版本的兼容性。
工程结构
itstack-demo-netty-2-02 └── src ├── main │ └── java │ └── org.itstack.demo.netty │ ├── client │ │ ├── MyChannelInitializer.java │ │ ├── MyClientHandler.java │ │ └── NettyClient.java │ ├── domain │ │ ├── MsgBody.java │ │ ├── MsgBodyOrBuilder.java │ │ └── MsgInfo.java │ ├── proto │ │ └── MsgInfo.proto │ ├── server │ │ ├── MyChannelInitializer.java │ │ ├── MyServerHandler.java │ │ └── NettyServer.java │ └── util │ └── MsgUtil.java │ └── test └── java └── org.itstack.demo.test └── ApiTest.javadomain包下的MsgBody.java、MsgBodyOrBuilder.java、MsgInfo.java都是由proto/MsgInfo.proto经过 protoc 编译自动生成的 Java 类,不需要手写;MsgBody即本案例的通信消息体。
三、定义消息协议:MsgInfo.proto
通信双方(服务端与客户端)必须使用同一份协议定义,才能保证编解码一致。本案例的协议文件位于src/main/java/org/itstack/demo/netty/proto/MsgInfo.proto,内容如下:
syntax = "proto3"; package org.itstack.demo.netty.domain; option java_package = "org.itstack.demo.netty.domain"; option java_multiple_files = true; option java_outer_classname = "MsgInfo"; message MsgBody { string channelId = 1; string msgInfo = 2; }各配置项与字段说明:
| 配置 / 字段 | 含义 |
|---|---|
syntax = "proto3" | 声明使用 proto3 语法(相比 proto2 更简洁,字段默认值由语言侧处理) |
package | proto 文件的逻辑包名,用于避免命名冲突 |
java_package | 指定生成的 Java 类所在包名,这里为org.itstack.demo.netty.domain |
java_multiple_files = true | 每个 message 生成独立的 Java 文件(对应工程结构中的MsgBody.java、MsgBodyOrBuilder.java等) |
java_outer_classname = "MsgInfo" | 外层类名为MsgInfo(与文件名对应) |
message MsgBody | 定义一个消息类型,包含channelId(字段编号 1)与msgInfo(字段编号 2)两个 string 字段 |
字段编号(= 1、= 2)是 Protobuf 二进制编码中实际写入字节流的标识,一旦发布使用后不宜随意变更。
编译 proto 文件
在 IDEA 的 Terminal 下执行编译命令,命令格式为:
protoc -I=源地址 --java_out=目标地址 源地址/xxx.proto案例中的实际执行命令(Windows 环境,protoc-3.5.0-win32位于工程根目录的protoc-3.5.0-win32/bin下):
protoc.exe -I=E:\itstack\GIT\itstack.org\itstack-demo-netty\itstack-demo-netty-2-02\src\main\java\org\itstack\demo\netty\proto --java_out=E:\itstack\GIT\itstack.org\itstack-demo-netty\itstack-demo-netty-2-02\src\main\java MsgInfo.proto其中-I=指定 proto 源文件所在目录,--java_out=指定生成 Java 代码的输出目录(这里输出到src\main\java,与java_package组合后最终落在org.itstack.demo.netty.domain包下)。编译完成后即可在domain包中看到生成的MsgBody、MsgBodyOrBuilder、MsgInfo等类。
四、服务端:管道装配 Protobuf 编解码器
server/MyChannelInitializer.java
服务端在ChannelInitializer中按顺序向管道添加 4 个 Protobuf 相关的编解码器,再添加业务处理器:
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel channel) { //protobuf 处理 channel.pipeline().addLast(new ProtobufVarint32FrameDecoder()); channel.pipeline().addLast(new ProtobufDecoder(MsgBody.getDefaultInstance())); channel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); channel.pipeline().addLast(new ProtobufEncoder()); // 在管道中添加我们自己的接收数据实现方法 channel.pipeline().addLast(new MyServerHandler()); } }从这四个组件的分工看,可以这样理解整条入站/出站链路:
ProtobufVarint32FrameDecoder(入站):负责按 varint32 长度前缀切割字节流,把网络上连续的字节流还原成一个个完整的消息帧,从源头处理半包、粘包问题;ProtobufDecoder(MsgBody.getDefaultInstance())(入站):把切好的帧数据反序列化为MsgBody消息对象,构造时传入的MsgBody.getDefaultInstance()是 Protobuf 解码器要求的默认实例(用于获取消息的描述信息);ProtobufVarint32LengthFieldPrepender(出站):与ProtobufVarint32FrameDecoder对称,在发送的 Protobuf 二进制数据前补上 varint32 格式的长度前缀,让对端能够据此切帧;ProtobufEncoder(出站):把MsgBody消息对象序列化为字节流写出。
由于ProtobufVarint32LengthFieldPrepender在出站方向、ProtobufVarint32FrameDecoder在入站方向各自生效,因此只要客户端与服务端采用完全一致的装配顺序,双端就能完成对称的"加长度前缀 → 编码发送"与"解码接收 → 按长度切帧"。
server/NettyServer.java
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(); } } }服务端要点:
- 配置服务端 NIO 线程组,
parentGroup用于接受连接,childGroup用于处理已建立连接的 I/O; NioServerSocketChannel开启非阻塞模式;ChannelOption.SO_BACKLOG, 128设置服务端可排队的连接数上限;childHandler(new MyChannelInitializer())为每个新接入的客户端 SocketChannel 装配管道(即上文包含 Protobuf 编解码器的管道);- 端口固定为
7397,与客户端连接地址保持一致。
server/MyServerHandler.java
public class MyServerHandler 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() + "\r\n"; 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()) + " 接收到消息内容:" + JsonFormat.printToString((MsgBody) msg)); } /** * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); System.out.println("异常信息:\r\n" + cause.getMessage()); } }处理器的核心变化体现在channelRead:入站的msg已经被管道中的解码器处理为MsgBody对象,因此可以直接(MsgBody) msg强转,并通过JsonFormat.printToString以 JSON 形式打印消息内容——与字符串传输章节相比,这里已经不需要自己进行解码。
五、客户端:与服务端对称的编解码管线
client/MyChannelInitializer.java
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel channel) throws Exception { //protobuf 处理 channel.pipeline().addLast(new ProtobufVarint32FrameDecoder()); channel.pipeline().addLast(new ProtobufDecoder(MsgBody.getDefaultInstance())); channel.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); channel.pipeline().addLast(new ProtobufEncoder()); // 在管道中添加我们自己的接收数据实现方法 channel.pipeline().addLast(new MyClientHandler()); } }客户端管道与服务端完全一致,这正是 Protobuf 通信成立的前提:通信双方必须保持同样的半包粘包处理、编码解码处理与收发数据方式。这一点与仓库 基础入门篇八《NettyClient半包粘包处理、编码解码处理、收发数据方式》 中强调的原则一致,只是把字符串解码器换成了 Protobuf 编解码器组合。
client/NettyClient.java
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博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(),"你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(),"你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(),"你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。")); f.channel().writeAndFlush(MsgUtil.buildMsg(f.channel().id().toString(),"你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。")); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); } } }客户端要点:
- 使用
Bootstrap引导,NioSocketChannel非阻塞模式,ChannelOption.AUTO_READ, true表示通道自动读取数据; connect("127.0.0.1", 7397)与服务端bing(7397)的端口对应;- 连接成功后连续
writeAndFlush5 条由MsgUtil.buildMsg构建的MsgBody消息——这些消息经过出站方向的ProtobufVarint32LengthFieldPrepender与ProtobufEncoder处理后,以带长度前缀的二进制形式发往服务端。
client/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()) + " 接收到消息内容:" + JsonFormat.printToString((MsgBody) msg)); } /** * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接 */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); System.out.println("异常信息:\r\n" + cause.getMessage()); } }客户端处理器与服务端处理器结构对称:连接建立后立即通知服务端并携带本端channelId与连接信息;channelRead中收到的同样已是MsgBody对象,直接强转后以 JSON 打印。
六、消息构建工具与 Protobuf/JSON 互转
util/MsgUtil.java
为了避免在业务代码里反复写 Builder 装配逻辑,案例把"构建 Protobuf 消息体"封装成工具方法:
public class MsgUtil { /** * 构建protobuf消息体 */ public static MsgBody buildMsg(String channelId, String msgInfo) { MsgBody.Builder msg = MsgBody.newBuilder(); msg.setChannelId(channelId); msg.setMsgInfo(msgInfo); return msg.build(); } }MsgBody.newBuilder()返回 Protobuf 生成的 Builder,通过链式的setChannelId、setMsgInfo设置字段后调用build()得到不可变的MsgBody实例。服务端、客户端的channelActive与客户端主流程中发送的所有消息,都是经由这个工具方法构建的。
ApiTest.java:Protobuf 与 JSON 互转验证
public class ApiTest { public static void main(String[] args) throws JsonFormat.ParseException { MsgBody.Builder msg = MsgBody.newBuilder(); msg.setChannelId("abD01223"); msg.setMsgInfo("hi helloworld"); MsgBody msgBody = msg.build(); //protobuf转Json 需要引入protobuf-java-format String msgBodyStr = JsonFormat.printToString(msgBody); System.out.println(msgBodyStr); //json转protobuf 需要引入protobuf-java-format JsonFormat.merge("{\"channelId\": \"HBdhi993\",\"msgInfo\": \"hi bugstack虫洞栈\"}", msg); msgBody = msg.build(); System.out.println(msgBody.getChannelId()); System.out.println(msgBody.getMsgInfo()); } }ApiTest验证了两个方向的数据转换:
- Protobuf 转 JSON:
JsonFormat.printToString(msgBody)把MsgBody序列化为 JSON 字符串输出; - JSON 转 Protobuf:
JsonFormat.merge(jsonString, msg)把 JSON 字符串反向合并进已有的 Builder,再build()后通过getChannelId()、getMsgInfo()读取字段。
需要说明的是:代码中使用的JsonFormat来自protobuf-java-format依赖,原案例在使用时需要额外引入该工具包(Protobuf 官方仓库自身不内置此 JSON 格式转换器)。这个工具在业务侧的典型用途是:把channelRead收到的MsgBody转成 JSON 字符串打印日志或对接 JSON 接口。
七、编译与运行测试
编译 proto
在 IDEA 的 Terminal 下执行编译命令,把MsgInfo.proto编译为 Java 类(具体命令见第三节,或直接使用工程内提供的protoc-3.5.0-win32/bin/protoc.exe)。
启动与执行
- 先启动
NettyServer(main 方法,监听 7397 端口); - 再启动
NettyClient(main 方法,连接 127.0.0.1:7397)。
服务端执行结果
itstack-demo-netty server start done. 链接报告开始 链接报告信息:有一客户端链接到本服务端。channelId:807679da 链接报告IP:127.0.0.1 链接报告Port:7397 链接报告完毕 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "通知服务端链接建立成功 Sun Aug 04 14:06:01 CST 2019 127.0.0.1"} 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。"} 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。"} 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。"} 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。"} 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "abc14b89","msgInfo": "你好,使用protobuf通信格式的服务端,我是https://bugstack.cn博主,付政委。这是我的公众号<bugstack虫洞栈>,关注我获取案例源码。"} 异常信息: 远程主机强迫关闭了一个现有的连接。 客户端断开链接/127.0.0.1:7397 Process finished with exit code -1客户端执行结果
itstack-demo-netty client start done. 链接报告开始 链接报告信息:本客户端链接到服务端。channelId:abc14b89 链接报告IP:127.0.0.1 链接报告Port:51218 链接报告完毕 2019-08-04 14:06:01 接收到消息类型:class org.itstack.demo.netty.domain.MsgBody 2019-08-04 14:06:01 接收到消息内容:{"channelId": "807679da","msgInfo": "通知客户端链接建立成功 Sun Aug 04 14:06:01 CST 2019 127.0.0.1\r\n"} Process finished with exit code -1结果分析
- 服务端连续 5 次收到客户端在
connect后批量发送的消息,接收消息类型均为class org.itstack.demo.netty.domain.MsgBody,说明入站数据被ProtobufVarint32FrameDecoder正确切帧、被ProtobufDecoder正确反序列化为协议对象,而非原始的字节流; - 双端通过
JsonFormat.printToString打印出的内容都是合法 JSON 结构({"channelId": "...","msgInfo": "..."}),验证了 Protobuf 对象与 JSON 的可读转换; - 客户端主动结束进程后,服务端触发
channelInactive打印"客户端断开链接",随后exceptionCaught捕获到连接被远端关闭的异常并ctx.close()释放连接,属于预期的收尾行为。
八、深入理解:Protobuf 编解码在 Netty 管道中的定位
帧边界与编解码的对称组合
从管道装配顺序可以推断出本案例的核心设计思路:Netty 的入站与出站处理是两条独立方向,四个 Protobuf 组件两两配对——入站方向用ProtobufVarint32FrameDecoder先按 varint32 长度前缀切出完整帧,再用ProtobufDecoder把帧解码为MsgBody;出站方向用ProtobufEncoder把MsgBody编码为字节,再由ProtobufVarint32LengthFieldPrepender补上长度前缀。长度前缀让接收方能够从 TCP 流中精确还原每条消息的边界,从而规避半包、粘包带来的解析错乱。
这与仓库 基础入门篇九《自定义编码解码器,处理半包、粘包数据》 中"通过实现ByteToMessageDecoder、MessageToByteEncoder处理字节码传输并控制半包、粘包"的思路一脉相承:自定义解码器需要自己维护切帧状态,而 Protobuf 方案由 Netty 官方提供的四个组件直接完成,业务只需保证双端管道一致。
与其他传输方案的横向对比
在本仓库的 itstack-demo-netty 系列中,传输格式的选择形成了清晰的演进脉络:
- 字符串方案(如 中级拓展篇一《Netty与SpringBoot整合》):使用
LineBasedFrameDecoder+StringDecoder/StringEncoder,简单直观,但传输的是文本,需要自行约定换行等边界符,序列化效率与可扩展性有限; - Protobuf 方案(本案例):通过 proto 文件定义强类型协议,跨语言通用,二进制序列化体积小、编解码性能高,适合对传输效率与协议规范性要求高的场景;
- protostuff 方案(见 中级拓展篇三《Netty传输Java对象》):同样基于 Google protobuf,但不需要定义 proto 文件、不需要预编译,可以直接对现有 POJO 做序列化/反序列化,代价是序列化前需预先传入 schema、反序列化要求对象提供默认构造函数,适合快速传输自定义 Java 对象而不想维护协议文件的场景。
三者对比可以归纳为:字符串方案上手最快但边界与性能一般;Protobuf 需要先定义协议并编译,换来的是强类型、跨语言与高性能;protostuff 则是在"不写 proto"和"高性能"之间取平衡。实际选型时应结合团队技术栈、跨语言需求与协议维护成本综合决定。
本案例在仓库中的定位
本案例源码对应仓库文档 docs/md/netty/expand/2019-08-17-netty案例,netty4.1中级拓展篇二《Netty使用Protobuf传输数据》.md,属于 itstack-demo-netty 中级拓展篇的第二讲。它建立在基础篇(NettyServer 收发数据、字符串编解码、客户端半包粘包处理、自定义编解码器等)之上,又为后续的 Java 对象传输、WebSocket、文件传输、心跳断线重连、集群部署、SSL 加密等拓展篇章奠定了"自定义传输协议 + 编解码器装配"的方法论。沿着 docs/md/netty 目录下的 base → expand → application → source-code 顺序阅读,可以完整掌握从入门到源码级的 Netty 通信能力。
- 文档
- 教程
- 后端
【免费下载链接】CodeGuide
:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
相关推荐
Netty 4.1 入门实战:ChannelOutboundHandlerAdapter 出站处理器原理与使用(CodeGuide 篇十)
Netty 4.1 入门实战:ChannelOutboundHandlerAdapter 出站处理器原理与使用(CodeGuide 篇十) 本篇是 CodeGu
文档教程后端Netty4.1 ChunkedStream 数据流切块传输实战:基于 CodeGuide 中级拓展篇十一的源码级解析
Netty4.1 ChunkedStream 数据流切块传输实战:基于 CodeGuide 中级拓展篇十一的源码级解析 本篇技术指南围绕小傅哥 CodeGuid
文档教程后端Netty 实战:SpringBoot + Netty + Elasticsearch 搭建日志数据收集存储管道
Netty 实战:SpringBoot + Netty + Elasticsearch 搭建日志数据收集存储管道 本篇技术指南基于小傅哥 Netty 中级拓展案
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考