Protobuf协议
序列化协议
1. 什么是序列化?
序列化(Serialization)就是将“内存中的对象”转化为“可以传输或存储的格式”的过程。在计算机中,内存里的对象是一块复杂的数据结构(包含指针、引用、嵌套等)。网络传输的时候,网络电缆或硬盘只认识连续的字节流(0和1)。
- 内存状态: 它是立体的、分散的。比如一个
User对象,它的名字在内存 A 区。 - 传输状态: 它是线性的。像一排排队过海关的人,必须一个接一个地变成字节(Bytes)。
想象你用积木搭了一个精美的城堡(这就是内存中的对象/Object)。序列化: 你想把这个城堡送给远方的朋友。你不能直接把一整个城堡扔进快递盒,它太占空间且容易散架。于是,你对照着图纸,把它拆成了一块块零件,并装进袋子。这个“拆解并装袋”的过程就是序列化。反序列化: 你的朋友收到袋子后,对照着同样的图纸,把零件重新拼装成了一模一样的城堡。这个“重组”的过程就是反序列化。
序列化的用途除了上述说到的网络传输之外,还有其他用途比如持久化之类的。。。。
具体可以看这篇文章:https://zhuanlan.zhihu.com/p/1948482140254741383
2. 序列化种类
第一个就是文本类序列化协议:这类协议将对象转化为人类可读的字符串。具体代表就有:
JSON (JavaScript Object Notation):
-
优点:当之无愧的互联网“通用语”。跨语言能力极强,肉眼可读,调试方便。
-
缺点: 冗余信息多(大量的引号、大括号),解析时会消耗 CPU 和内存。
XML:
- 优点: 格式严谨,支持复杂的 Schema 验证。
- 缺点: 标签比数据还长,序列化后的体积非常臃肿,现代开发中已逐渐退居幕后(多用于配置或老系统接口)。
第二个就是二进制类的序列化协议了:这类协议将对象转化为机器友好的二进制流,牺牲了可读性,换取了极致的性能。具体代表就有:
Protobuf (Protocol Buffers): 【本文要探讨的】
- 特点:Google 开发。 需要先编写
.proto定义文件。它通过 Varint 压缩算法和 Tag-Value 存储方式,让数据包小到惊人。 - 适用: 微服务间通信(gRPC)、移动端与后端通讯。
Apache Thrift:
- 特点: Facebook 开发。不仅仅是序列化协议,还自带了完整的 RPC 框架。支持的语言极其丰富。
3. 对比实验
这一小节就以使用Json序列化协议和protobuf序列化协议进行网络传输为例子,来看一下二者的性能。
场景就很简单,先启动服务器,然后客户端发送消息、然后服务器回显给客户端,这样一个来回。
采用的技术栈就是Netty。
然后传输的消息长下面这个样子:
public class JsonBusinessMessage implements Serializable {
............
private Long msgId;
private String content;
private Long timestamp;
...........
}
3.1 Json序列化协议
首先编写一下json的服务端:
public class Server {
// json形式的序列化
public void startJsonSerializerServer() {
ServerBootstrap s = new ServerBootstrap();
EventLoopGroup boss = null;
EventLoopGroup worker = null;
try {
boss = new NioEventLoopGroup(1);
worker = new NioEventLoopGroup();
s.group(boss, worker)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))
// 自定义JSON解码器
.addLast(new JsonDecoder())
// 长度字段编码器:在消息前添加4字节长度
.addLast(new LengthFieldPrepender(4))
// 自定义JSON编码器
.addLast(new JsonEncoder())
// 业务处理器
.addLast(new JsonServerHandler());
}
});
ChannelFuture channelFuture = s.bind(8888).sync();
log.info("启动成功");
channelFuture.channel().closeFuture().sync();
} catch ( Exception e ) {
log.error("start server error", e);
} finally {
if ( boss != null ) {
boss.shutdownGracefully();
} if ( worker != null ) {
worker.shutdownGracefully();
}
}
}
}
服务端的自定义json编解码器,和业务handler:
/**
* JSON解码器:将JSON字节流反序列化为JsonBusinessMessage对象
*/
public class JsonDecoder extends ByteToMessageDecoder {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 读取所有字节(长度字段已由LengthFieldBasedFrameDecoder处理,此处为完整的JSON字节流)
byte[] jsonBytes = new byte[in.readableBytes()];
in.readBytes(jsonBytes);
// 反序列化为目标对象
JsonBusinessMessage message = OBJECT_MAPPER.readValue(jsonBytes, JsonBusinessMessage.class);
out.add(message);
}
}
/**
* @Description: json形式的编码器
*/
public class JsonEncoder extends MessageToByteEncoder<Object> {
// 单例ObjectMapper,避免重复创建
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// 将对象序列化为JSON字节数组
byte[] jsonBytes = OBJECT_MAPPER.writeValueAsBytes(msg);
// 写入字节流
out.writeBytes(jsonBytes);
}
}
/**
* JSON服务端处理器:接收消息→统计体积→直接回显
*/
public class JsonServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(JsonServerHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
JsonBusinessMessage message = (JsonBusinessMessage) msg;
// 统计JSON序列化后的体积
int msgSize = OBJECT_MAPPER.writeValueAsBytes(message).length;
log.info("【JSON服务端】接收消息 | 消息ID:{} | 内容:{} | 体积:{}字节",
message.getMsgId(), message.getContent(), msgSize);
// 回显消息
ctx.writeAndFlush(message);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("JSON服务端异常", cause);
ctx.close();
}
}
服务端就很简单了,接下来是json客户端的代码:
/**
* JSON客户端:连接8888端口,支持单次/高并发测试(和Protobuf客户端配置完全一致)
*/
public class NettyJsonClient {
private static final Logger log = LoggerFactory.getLogger(NettyJsonClient.class);
private static final String HOST = "127.0.0.1";
private static final int PORT = 8888;
private static final NioEventLoopGroup GROUP = new NioEventLoopGroup();
private static Channel getChannel() throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(GROUP)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4))
.addLast(new JsonDecoder()) // 这个就是复用上面的
.addLast(new LengthFieldPrepender(4))
.addLast(new JsonEncoder()) // 同理
.addLast(new JsonClientHandler());
}
});
return bootstrap.connect(HOST, PORT).sync().channel();
}
// 构建JSON消息(和Protobuf内容完全一致)
private static JsonBusinessMessage buildMessage(long msgId, String content) {
long timestamp = System.currentTimeMillis();
return new JsonBusinessMessage(msgId, content, timestamp);
}
public static void main(String[] args) throws InterruptedException {
Channel channel = getChannel();
log.info("【JSON客户端】连接服务端成功:{}:{}", HOST, PORT);
// 【重要】和Protobuf客户端使用完全相同的测试配置,保证对比公平
long testCount = 10000; // 发送次数
String msgContent = "Netty整合Protobuf协议测试,对比JSON传输效率!这是一段测试内容,越长越能体现Protobuf的体积优势!"; // 消息内容
for (long i = 1; i <= testCount; i++) {
JsonBusinessMessage message = buildMessage(i, msgContent);
JsonClientHandler.SEND_TIMESTAMP.set(System.currentTimeMillis());
JsonClientHandler.SEND_MESSAGE = message;
channel.writeAndFlush(message);
if (testCount == 1) {
Thread.sleep(1000);
}
}
if (testCount > 1) {
Thread.sleep(5000);
log.info("【JSON客户端】高并发测试完成,共发送{}条消息", testCount);
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
GROUP.shutdownGracefully();
log.info("JSON客户端优雅关闭");
}));
}
}
客户端的handler:
/**
* JSON客户端处理器:接收回显→统计耗时+体积
*/
public class JsonClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(JsonClientHandler.class);
public static final AtomicLong SEND_TIMESTAMP = new AtomicLong(0);
public static JsonBusinessMessage SEND_MESSAGE;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
JsonBusinessMessage message = (JsonBusinessMessage) msg;
long cost = System.currentTimeMillis() - SEND_TIMESTAMP.get();
// 统计体积
int msgSize = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsBytes(message).length;
log.info("【JSON客户端】接收回显 | 消息ID:{} | 耗时:{}ms | 体积:{}字节", message.getMsgId(), cost, msgSize);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("JSON客户端异常", cause);
ctx.close();
}
}
运行查看测试结果,客户端发送一万条消息:


可以从图中看到消息的体积是183字节,然后平均耗时在800ms左右。
这个是json的
3.2 protobuf序列化
官网下载的proto编译工具版本是:protoc-33.4-win64
然后proto-javamaven依赖是:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>4.33.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>4.33.4</version>
<scope>compile</scope>
</dependency>
首先要编写一下proto文件:
syntax = "proto3"; // 指定proto3版本
package com.feng.demo.msg; // 生成Java代码的包名
// 通用业务消息:Protobuf协议使用
message BusinessMessage {
int64 msg_id = 1; // 消息唯一ID
string content = 2; // 消息内容(核心传输数据)
int64 timestamp = 3; // 发送时间戳(毫秒)
}
然后使用proto官网给的编译器,将这个文件搞一下,生成java类,生成的类特别长,这里就不放了。
服务端启动器:
// protobuf形式的序列化
public void startProtobufSerializerServer() {
ServerBootstrap s = new ServerBootstrap();
EventLoopGroup boss = null;
EventLoopGroup worker = null;
try {
boss = new NioEventLoopGroup(1);
worker = new NioEventLoopGroup();
s.group(boss, worker)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder()) // Protobuf粘包拆包解码器
// Protobuf解码器:指定目标消息类型
.addLast(new ProtobufDecoder(Message.BusinessMessage.getDefaultInstance()))
.addLast(new ProtobufVarint32LengthFieldPrepender()) // Protobuf粘包拆包编码器
.addLast(new ProtobufEncoder()) // Protobuf编码器
.addLast(new ProtobufHandler());
}
});
ChannelFuture channelFuture = s.bind(9999).sync();
log.info("启动成功");
channelFuture.channel().closeFuture().sync();
} catch ( Exception e ) {
log.error("start server error", e);
} finally {
if ( boss != null ) {
boss.shutdownGracefully();
} if ( worker != null ) {
worker.shutdownGracefully();
}
}
}
protobuf的编解码器netty框架自带了,非常地迅捷好用。我们只需要看一下自定义的服务端业务处理器了:
public class ProtobufHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(ProtobufHandler.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 强制转换为Protobuf消息对象
Message.BusinessMessage message = (Message.BusinessMessage) msg;
// 统计序列化后的消息体积(字节数):Protobuf原生支持获取字节数组
int msgSize = message.toByteArray().length;
log.info("【Protobuf服务端】接收消息 | 消息ID:{} | 内容:{} | 体积:{}字节",
message.getMsgId(), message.getContent(), msgSize);
// 直接回显消息给客户端(Netty自动通过编码器序列化)
ctx.writeAndFlush(message);
}
// 异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("Protobuf服务端异常", cause);
ctx.close();
}
}
接下来就是客户端的测试代码了:
public class ProtobufClient {
private static final Logger log = LoggerFactory.getLogger(ProtobufClient.class);
private static final String HOST = "127.0.0.1";
private static final int PORT = 9999;
private static final NioEventLoopGroup GROUP = new NioEventLoopGroup();
// 获取客户端Channel(建立连接)
private static Channel getChannel() throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(GROUP)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufDecoder(Message.BusinessMessage.getDefaultInstance()))
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder())
.addLast(new ProtobufClientHandler());
}
});
return bootstrap.connect(HOST, PORT).sync().channel();
}
// 构建Protobuf消息
private static Message.BusinessMessage buildMessage(long msgId, String content) {
long timestamp = System.currentTimeMillis();
return Message.BusinessMessage.newBuilder()
.setMsgId(msgId)
.setContent(content)
.setTimestamp(timestamp)
.build();
}
// 测试方法:可选择单次/高并发
public static void main(String[] args) throws InterruptedException {
Channel channel = getChannel();
log.info("【Protobuf客户端】连接服务端成功:{}:{}", HOST, PORT);
// 测试配置:可修改
long testCount = 10000; // 发送次数:1=单次测试,100000=高并发测试
String msgContent = "Netty整合Protobuf协议测试,对比JSON传输效率!这是一段测试内容,越长越能体现Protobuf的体积优势!"; // 消息内容
// 循环发送消息
for (long i = 1; i <= testCount; i++) {
Message.BusinessMessage message = buildMessage(i, msgContent);
// 记录发送时间戳(用于计算耗时)
ProtobufClientHandler.SEND_TIMESTAMP.set(System.currentTimeMillis());
ProtobufClientHandler.SEND_MESSAGE = message;
// 发送消息
channel.writeAndFlush(message);
// 单次测试休眠,高并发测试注释(避免控制台刷屏)
if (testCount == 1) {
Thread.sleep(1000);
}
}
// 高并发测试时,休眠等待所有响应接收完成
if (testCount > 1) {
Thread.sleep(5000);
log.info("【Protobuf客户端】高并发测试完成,共发送{}条消息", testCount);
}
// 关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
GROUP.shutdownGracefully();
log.info("Protobuf客户端优雅关闭");
}));
}
}
客户端的handler:
public class ProtobufClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(ProtobufClientHandler.class);
// 发送时间戳(用于计算耗时):原子类保证线程安全
public static final AtomicLong SEND_TIMESTAMP = new AtomicLong(0);
// 发送的消息对象(用于统计体积)
public static Message.BusinessMessage SEND_MESSAGE;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message.BusinessMessage message = (Message.BusinessMessage) msg;
// 计算耗时:当前时间 - 发送时间
long cost = System.currentTimeMillis() - SEND_TIMESTAMP.get();
// 统计体积
int msgSize = message.toByteArray().length;
log.info("【Protobuf客户端】接收回显 | 消息ID:{} | 耗时:{}ms | 体积:{}字节",
message.getMsgId(), cost, msgSize);
// 关闭连接(单次测试),高并发测试时注释
// ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("Protobuf客户端异常", cause);
ctx.close();
}
}
然后运行查看测试结果:


对比json:json的消息的体积是183字节,然后平均耗时在800ms左右
protobuf的消息体积是143字节,平均耗时是348ms。
体积上只用了json的78%,耗时只占json的43.5%。这还只是消息体比较小的情况下。可以看出protobuf将性能进一步提升了。
4. Protobuf解析
为什么protobuf性能这么好?
Protobuf 的设计目标就是极致的性能和紧凑性,而 JSON/XML 为了 “人类可读性”、Java 原生序列化为了 “Java 对象完整还原”,都牺牲了性能;Protobuf 则从存储结构、编码方式、编解码机制三个核心层面做了极致优化,最终实现 “更小体积、更快速度、更低开销” 的性能优势。
首先,protobuf存储结构很精炼,序列化的本质是 “用字节表示数据”,但不同协议的 “字节利用率” 天差地别:Protobuf 只存 “字段编号 + 值”,而其他协议会携带大量 “描述性冗余信息”,这些冗余既增加体积,又增加解析开销。
| 协议 | 存储结构特点(冗余来源) | 性能损耗点 |
|---|---|---|
| Protobuf | 仅存「字段编号(1-2 字节)+ 值」 | 无冗余,字节 100% 用于业务数据;解析时按编号直接映射字段,无需处理冗余 |
| JSON | 字段名(如 "topic")+ 分隔符(:、,、{})+ 引号 | 字段名占比 30-50%(比如 "topic":"order" 中,"topic" 是纯冗余);解析时要分词 / 校验语法 |
| XML | 标签( |
冗余占比 60-70%(比如<topic>order</topic>,标签占比远超实际值);解析逻辑极复杂 |
| Java 原生序列化 | 类名、包名、继承关系、序列化 ID、字段类型 | 元信息占比 80% 以上(比如序列化一个简单对象,类元信息比业务数据还大);反序列化要反射加载类 |
拿一个例子来说,(MQ 消息:topic=order,timestamp=1710000000000,body=test):
- Protobuf 存储:
1(编号)+order(值) + 2(编号)+1710000000000(值) + 3(编号)+test(值)→ 仅约 30 字节; - JSON 存储:
{"topic":"order","timestamp":1710000000000,"body":"test"}→ 约 60 字节(冗余的字段名 / 分隔符占一半); - Java 原生序列化:约 150 字节(包含类名、包名、序列化 ID 等元信息);
- XML 存储:
<msg><topic>order</topic><timestamp>1710000000000</timestamp><body>test</body></msg>→ 约 100 字节。
第二个,编码方式的压缩:
- 整数编码:Varint(可变长度编码)—— 小数字省 75% 空间
日常开发中 90% 的整数是小数字(如 ID<1000、状态码 0-10),Protobuf 的 Varint 让小数字仅占 1-2 字节,而其他协议用固定长度:
- Protobuf:整数 100 → Varint 编码后1 字节;整数 256 → 2 字节;
- JSON/XML:100 → 转字符 "100" → 3 字节(UTF-8);
- Java 原生序列化:int 类型不管数值大小 → 固定 4 字节;long → 固定 8 字节。
- 其他类型优化:无额外转换开销
- 布尔值:Protobuf 用 1 位存储(8 个布尔值占 1 字节);JSON 转 "true"/"false"(4-5 字节);Java 原生占 1 字节;
- 字符串 / 二进制:Protobuf 用 UTF-8 编码(无 BOM、无冗余),bytes 类型直接存二进制;JSON 字符串要转义(如 "\n"→"\n");XML 要转义特殊字符(如 <→<);
- 无字节对齐:紧凑排列
Java 原生序列化会做 “8 字节对齐”(哪怕数据只有 1 字节,也填充到 8 字节),Protobuf 则按实际编码长度紧凑排列,无任何填充字节,进一步减少体积。
5. 总结
Protobuf 性能好的核心是 “极致的字节利用率”+“零冗余的编解码逻辑”:
- 存储上:用字段编号替代字段名,砍掉所有格式冗余,字节 100% 服务于业务数据;
- 编码上:Varint 等定制化压缩编码,用最小字节表示数据,避免固定长度 / 文本编码的浪费;
- 编解码上:编译生成的硬编码逻辑,无反射 / 文本解析开销,CPU 效率拉满;
- 连锁优势:小体积带来网络 / IO / 内存的全方位性能提升。
而 JSON/XML 为了 “人类能看懂”,Java 原生序列化为了 “Java 对象完整还原”,都在性能上做了妥协 —— 这也是为什么 Protobuf 成为微服务、RPC、MQ、物联网等高性能场景的首选序列化协议。

浙公网安备 33010602011771号