当前位置: 首页 > news >正文

protobuf原理和使用

一、protobuf原理

1.协议概述

        什么是协议:协议是一种约定,通过约定,不同的进程可以对一段数据产生相同的理解,从而相互协作,存在进程间通信的程序就一定需要协议。

2.判断消息的完整性-区分消息的边界

        为了让对端知道如何结束消息分界,通常有以下几种做法:

1. 固定大小字节数拆分

按每个消息固定字节数来划分,例如每个消息 100 个字节,对端每接收 100 个字节,就当成一个消息来解析。

2. 特定符号分界

每个消息以特定的字符结尾(如 \r\n),当在字符流中读取到该字符时,则表明上一个消息到此为止。例如 Redis 协议(key-value 结构,如 get teacher\r\n),服务端读到 \r\n 就认定一个消息结束。

3. 固定消息头 + 消息体结构

消息头是固定字节长度的结构,其中包含一个特定字段来规定消息体的大小。接收消息时,先接收固定字节数的头部,解析出这个消息中消息体的长度,再按此长度接收消息体。这是目前各种网络应用用得最多的一种消息格式(header + body)。

4. 字符流宽度 + 消息总长度判断

        在序列化后的 buffer 前面增加一个固定宽度的头部,其中有个字段存储消息总长度。接收时,先判断已收到的数据中是否包含结束符,收到结束符后解析消息头,得出这个消息的完整长度,再按此长度接收消息体。例如 HTTP 和 Redis 采用类似方式(需结合具体协议细节)。

        这些方式的核心是让接收方能够明确识别消息的边界,避免因黏包、拆包等问题导致消息解析错误,确保通信双方对消息完整性的认知一致。

3.协议设计

3.1 协议设计范例

3.1.1 范例 1 - IM 即时通讯

即时通讯的协议设计如下:

字段类型长度(字节)说明
lengthunsigned int4整个消息的长度,包括协议头 + BODY
versionunsigned short2通信协议的版本号
appidunsigned short2对外 SDK 提供服务时,用来识别不同的客户
service_idunsigned short2对应命令的分组类,比如 login 和 msg 是不同分组
command_idunsigned short2分组里面的子命令,比如 login 和 login response
seq_numunsigned short2消息序号
reserveunsigned short2预留字节
bodyunsigned char[]n具体的协议数据
3.1.2 范例 2 - 云平台节点服务器
字段类型长度(字节)说明
STAGunsigned short2通信协议数据包的开始标志,如 0xff 0xfe
versionunsigned short2通信协议的版本号,目前为 0x01
checksumunsigned char1计算协议数据校验和,若为加密数据,则计算密文校验和。校验和计算范围:协议头 CheckSum 字段后数据,协议体全部数据
typeunsigned char10 表示协议体是 json 格式,其它值未定义。设备心跳消息类型的值为 0xA0
seqnounsigned int4通信数据报文的序列号,应答报文序列号必须与请求报文序列号相同
Lengthunsigned short4报文内容长度,即从该字段后报文内容长度
reserveunsigned int4预留字节,设备心跳消息类型的值为 devid
bodyunsigned char[]n数据
具体参考《云平台节点服务器设计说明书_v0.4.10.pdf》。
3.1.4 范例 4 - HTTP 协议

HTTP 协议的请求由请求行、请求头、空白行和请求体组成:

  • 请求行[Method] [url] [Version] \r\n
  • 请求头:多个 [Key]: [Value] \r\n 形式的参数。
  • 空白行\r\n,表示请求头结束。
  • 请求体(Body):实际数据。

不适合作为互联网后台协议的原因

  1. HTTP 协议只是一个框架,没有指定包体的序列化方式,还需配合其他序列化方式(如 JSON)才能传递业务逻辑数据。
  2. HTTP 协议解析效率低,且相对复杂(并非协议本身简单,而是因其广泛使用让人熟悉)。

适用情况

  1. 对公网用户开放 API,HTTP 协议的穿透性好。
  2. 效率要求没那么高的场景。
  3. 希望提供更多人熟悉的接口,如新浪微博、腾讯微博提供的开放接口。
3.1.5 范例 5 - Redis 协议

基本原理:先发送一个字符串表示参数个数,再逐个发送参数。每个参数发送时,先发送一个字符串表示参数的数据长度,再发送参数内容。

在 RESP 中,数据类型通过第一个字节判断:

  • 单行(Simple String)回复:第一个字节是 +
  • 错误(Error)信息:第一个字节是 -
  • 整形数字(Integers):第一个字节是 :
  • 多行字符串(Bulk Strings):第一个字节是 $
  • 数组(Arrays):第一个字节是 *

        此外,RESP 可使用特殊变体表示 NULL 值。协议的不同部分始终以 \r\n(CRLF)结束。具体参考《Redis 协议规范 - 20221011.pdf》。

3.2 序列化方法

序列化和反序列化概念
  • 序列化:把对象转换为字节序列的过程。
  • 反序列化:把字节序列恢复为对象的过程。
什么情况下需要序列化
  • 需将内存中的对象状态保存到文件或数据库时。
  • 需通过套接字在网络上传送对象时。
常见的序列化方法
  • TLV 编码及其变体:如 Protobuf(tag, length, value 的缩写)。
  • 文本流编码:如 XML/JSON。
  • 固定结构编码:协议约定传输字段类型和含义,无 tag 和 len,只有 value(如 TCP/IP)。
  • 内存 dump:直接输出内存数据,反序列化时直接还原内存,不做序列化操作。
3.2.1 常见序列化方法(主流序列化协议)
  1. XML(可扩展标记语言)
    • 通用且重量级的数据交换格式,以文本方式存储。
  2. JSON(JS 对象简谱)
    • 通用且轻量级的数据交换格式,以文本结构存储。
  3. Protocol Buffer
    • Google 开发的独立、轻量级数据交换格式,以二进制结构存储。
类型XMLJSONProtobuf
通用性通用通用通用(需编译器生成对应语言代码)
数据长度重量级轻量级轻量级
格式文本格式文本格式二进制格式

范例:一个 Person 对象从客户端发送给服务器,包含以下字段:

  • name:姓名(变长字段),如 “湖南长沙零声教育 darren”。
  • age:年龄,如 80。
  • languages:熟悉技能(普通数组,变长字段),如 [“C/C++”, “Java”, “python”, “go”, “js”]
  • phone:电话号码(嵌套对象),如 {“number”: “18570368134”, “type”: “home”}
  • books:对象数组(变长字段),每本书包含 name 和 price 属性。
  • vip:是否为 vip,如 true
  • address:地址,如 “yageguoji”。

        文本格式(如 JSON)方便调试,二进制格式(如 Protobuf)则在效率和数据紧凑性上更有优势,不同序列化方式对这些字段的处理结构和效率各有差异。

3.2.2序列化结果数据对⽐

                                    xml

 3.2.3 序列化、反序列化速度对⽐ 测试10万次序列化

库 / 场景序列化耗时(默认)序列化耗时(-O1)序列化后字节数反序列化耗时(默认)反序列化耗时(-O1)
cJSON(C)488ms452ms297284ms251ms
jsoncpp(C++)871ms709ms255786ms709ms
rapidjson(C++)701ms113ms1171288ms953ms
tinyxml2(XML)---1781ms-
protobuf241ms83ms239190ms80ms

说明

  • JSON 序列化后字节数差异与格式排列有关,例如:
    • {"name":"darren"}(紧凑格式) vs {\n"name": "darren"\n}(带换行缩进)。
  • Protobuf 作为二进制协议,在序列化后字节数和耗时上均表现最优。

3.3 协议安全

1. XXTEA 加密算法(固定密钥)
  • 简介:XXTEA 是 TEA 的改进版,属于分组密码算法,密钥长度为 128 位(4 个 32 位无符号整数),适合嵌入式系统或资源受限环境。
  • 核心代码(C 语言):
    #define DELTA 0x9e3779b9
    #define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))void btea(uint32_t *v, int n, uint32_t const key[4]) {uint32_t y, z, sum;unsigned p, rounds, e;if (n > 1) { // 加密rounds = 6 + 52/n;sum = 0;z = v[n-1];do {sum += DELTA;e = (sum >> 2) & 3;for (p=0; p<n-1; p++) {y = v[p+1];z = v[p] += MX;}y = v[0];z = v[n-1] += MX;} while (--rounds);} else if (n < -1) { // 解密n = -n;rounds = 6 + 52/n;sum = rounds*DELTA;y = v[0];do {e = (sum >> 2) & 3;for (p=n-1; p>0; p--) {z = v[p-1];y = v[p] -= MX;}z = v[n-1];y = v[0] -= MX;sum -= DELTA;} while (--rounds);}
    }
    
  • 使用示例
    uint32_t v[2] = {1, 2};
    uint32_t key[4] = {2, 2, 3, 4};
    btea(v, 2, key); // 加密
    btea(v, -2, key); // 解密
    
4. Signal Protocol(端到端加密协议)
  • 核心特性

    • 前向安全:即使某消息密钥泄露,无法解密历史消息。
    • 后向安全:无法通过泄露的密钥预测未来消息密钥。
    • 双棘轮算法:结合 KDF 链棘轮(生成消息密钥)和 DH 棘轮(保证密钥随机性)。
  • 关键流程

    1. X3DH 密钥协商
      • 双方各生成 3 对密钥:身份密钥(长期)、已签名预共享密钥(中期)、一次性预共享密钥(短期)。
      • 通过 DH 协议计算初始密钥:
        DH = DH(IPK-A, SPK-B) || DH(EPK-A, IPK-B) || DH(EPK-A, SPK-B) || DH(IPK-A, OPK-B)
        S = KDF(DH)  // 衍生消息密钥
        
    2. 双棘轮算法
      • KDF 链棘轮:每次迭代生成新消息密钥,前半部分用于下一次迭代,后半部分用于加密消息。
      • DH 棘轮:每次消息轮回后更新临时密钥对,确保盐值随机性,防止后向泄露。
    3. 群组聊天
      • 每个成员生成独立的 KDF 链密钥和签名密钥对。
      • 消息加密后通过服务器转发,成员离开时需重新生成所有密钥并同步。
  • 应用场景:WhatsApp、Signal App 等实时通讯应用,确保通讯内容仅收发双方可解密。

3.4 数据压缩

  • 适用场景:文本数据(如 JSON/XML)压缩,二进制数据(如图像、视频)压缩收益较低。
  • 常用算法
    1. deflate:结合 LZ77 和哈夫曼编码,常用于 HTTP 压缩(如 Nginx 默认支持)。
    2. gzip:基于 deflate,压缩率更高,适合文件压缩。
    3. LZW:适用于重复性高的文本(如日志),但存在专利限制。
  • 实施建议:仅在带宽受限场景启用压缩,避免过度消耗 CPU 资源。

3.5 协议升级

  1. 版本号机制
    • 在协议头部添加 version 字段(如 2 字节无符号整数),明确协议版本。
    • 示例:
      | 4字节 length | 2字节 version | 2字节 service_id | ... |
      
  2. 头部可扩展性
    • 在头部添加 extend_length 字段,标识扩展头部长度,支持新增字段而不破坏旧协议。
    • 结构:
      固定头部(12字节) + 扩展头部(extend_length 字节) + 消息体
      
    • 例:
      固定头部:| magic(4) | version(2) | extend_len(2) | ... |
      扩展头部:| new_field1(4) | new_field2(4) | ... |

4. Protobuf 的使用

        Protocol Buffers(简称 Protobuf)是 Google 开发的语言中立、平台无关的高效序列化格式,广泛用于通信协议、数据存储等场景。其核心优势包括:

  • 高效性:二进制格式,序列化后体积小、解析速度快。
  • 强类型:通过 .proto 文件定义数据结构,类型安全且易于维护。
  • 扩展性:支持字段新增和旧版本兼容,无需重新部署系统。

4.1 Protobuf 协议的工作流程

  1. 定义数据结构:通过 .proto 文件(IDL,接口描述语言)定义消息类型和字段。
    syntax = "proto3";
    package example;
    message Person {string name = 1;int32 age = 2;repeated string emails = 3;
    }
    
  2. 生成代码:使用 protoc 编译器根据 .proto 文件生成对应语言的序列化 / 反序列化代码(如 C++ 的 .pb.cc 和 .pb.h)。
  3. 序列化 / 反序列化:通过生成的代码对消息对象进行编码(写入文件或网络传输)和解码(读取解析)。

4.2 Protobuf 的编译安装(以 C++ 为例)

1. 下载与解压
  • 官方仓库:https://github.com/protocolbuffers/protobuf
tar zxf protobuf-cpp-3.19.6.tar.gz
cd protobuf-3.19.6
2. 编译与安装
./configure  # 配置编译选项
make         # 编译(耗时较长,需耐心等待)
sudo make install  # 安装到系统路径
sudo ldconfig  # 更新动态链接库缓存
3. 验证安装
protoc --version  # 应输出版本号(如 3.19.6)
4. 生成代码
  • -I:指定 .proto 文件所在路径(可多次指定,按顺序查找)。
  • --cpp_out:指定生成的 C++ 代码输出路径。
5. 编译示例程序
g++ -std=c++11 -o my_app my_app.cc message.pb.cc -lprotobuf -lpthread -L/usr/local/lib
  • -lprotobuf:链接 Protobuf 库。
  • -L/usr/local/lib:指定库文件路径(若安装在其他路径需调整)。

4.3 优化选项(option optimize_for

.proto 文件可通过 option optimize_for 配置生成代码的优化级别(文件级选项):

选项特点适用场景
SPEED生成代码运行效率高,编译后体积较大(默认选项)。高性能场景(如服务器端)
CODE_SIZE生成代码体积小,运行效率较低。资源受限场景(如嵌入式设备)
LITE_RUNTIME兼顾效率和体积,但移除反射功能,仅需链接 libprotobuf-lite 库。移动端或对体积敏感的场景

示例

option optimize_for = LITE_RUNTIME;  // 启用轻量级运行时

4.4 标量数值类型与编码规则

Protobuf 支持多种标量类型,不同类型在序列化时采用不同的编码方式,影响存储效率和性能。

.proto 类型C++ 类型编码方式适用场景
doubledouble定长(8 字节)浮点数值,对精度要求高的场景(如坐标、金额)
floatfloat定长(4 字节)浮点数值,精度要求较低时(如概率、比例)
int32int32变长非负数或小范围整数(若存在负数,优先使用 sint32
uint32uint32变长无符号整数
int64int64变长大范围整数(若存在负数,优先使用 sint64
sint32int32变长(ZigZag)有符号整数(负数编码效率高于 int32
sint64int64变长(ZigZag)大范围有符号整数
fixed32uint32定长(4 字节)数值常大于 228 的无符号整数(如哈希值、版本号)
fixed64uint64定长(8 字节)数值常大于 256 的无符号整数(如时间戳、文件大小)
boolbool变长(0 或 1)布尔值
stringstd::string变长(UTF-8)文本数据(如用户名、地址)
bytesstd::string变长(二进制)任意字节数据(如图像、压缩数据)

编码规则说明

  • 变长编码(Varint):适用于小数值,每个字节最高位为标志位(0 表示结束,1 表示继续)。例如,数值 1 编码为 0x01,数值 300 编码为 0xAC 0x02
  • 定长编码:无论数值大小,固定占用字节数(如 fixed32 始终占 4 字节),适合存储大数值或需要快速读取的场景。
  • ZigZag 编码:将有符号整数映射为无符号数,避免负数在变长编码中占用更多字节(如 -1 编码为 0xFFFFFFFF,即 0xFF 0xFF 0xFF 0xFF)。

4.5 反射机制(高级特性)

Protobuf 支持运行时反射,允许通过代码动态操作消息字段(如根据字段名读写值),适用于通用框架或插件系统。

核心类与接口
  1. google::protobuf::Message:所有消息的基类,提供 GetDescriptor() 和 GetReflection() 接口获取元信息。
  2. google::protobuf::Descriptor:描述消息的结构(字段名、类型、标签等)。
  3. google::protobuf::Reflection:提供动态读写字段的方法(如 GetInt32()SetString())。
应用场景
  • 通用日志系统:动态记录不同类型的消息字段。
  • 网络传输:通过消息名动态解析接收到的数据包。
  • 配置系统:根据配置文件动态生成消息对象。

注意:反射会增加运行时开销,且启用 LITE_RUNTIME 优化时反射功能不可用。

 

5 Protobuf 编码原理

Protobuf 采用高效的二进制编码,核心目标是减少数据存储空间并提升解析速度。以下是其核心编码原理的详细解析:

5.1 Varints 编码(变长整型)

核心思想
  • 动态分配字节:根据数值大小动态分配字节数,小数值占用少字节,大数值占用多字节。
  • Base128 编码:每个字节的最高位(MSB)为标志位,剩余 7 位存储数据:
    • 标志位为 1:后续还有字节。
    • 标志位为 0:当前字节是最后一个字节。
  • 小端序存储:低字节在前,高字节在后。
编码示例
  1. 正数示例:数字 1

    • 二进制:00000001(仅 1 字节,标志位为 0)。
    • 编码结果:0x01
  2. 较大数示例:数字 666

    • 十进制转二进制:666 = 256×2 + 154 = 00000010 10011010(8 位)。
    • 按 7 位拆分:10011010(低 7 位)、0000001(高 1 位补前导 0)。
    • 标志位补位:10011010(标志位 1,后续有字节)、00000010(标志位 0,最后一个字节)。
    • 编码结果(十六进制):0xAA 0x02
  3. 大数示例:0xFFFFFFFF(4 字节整数)

    • 二进制:32 位全 1,按 7 位拆分需 5 字节(32/7≈4.57,向上取整)。
    • 编码结果:5 字节,每个字节标志位均为 1(前 4 字节),最后一字节标志位为 0。
优缺点与适用场景
  • 优点:小数值(≤28bit)存储效率极高,节省空间。
  • 缺点:大数值(>28bit)可能比固定长度编码更占空间(如 32bit 整数需 5 字节 vs. fixed32 的 4 字节)。
  • 适用场景:非负数、小整数(如枚举值、计数器)。

5.2 Zigzag 编码(负数优化)

问题背景
  • 负数在 Varints 中的低效性
    • int32 类型的负数(如 -5)在 Protobuf 中会被视为无符号数,高字节全为 1,导致 Varints 编码需 10 字节。
    • 示例:-5 的补码为 0xFFFFFFFF...FB,编码后为 0xFB 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01(10 字节)。
解决方案:Zigzag 映射
  • 核心思想:将负数映射为正数,使高位全为 0,再使用 Varints 编码。
  • 正向变换(编码)
    // 32位:n << 1 ^ (n >> 31)
    // 64位:n << 1 ^ (n >> 63)
    sint32 n = -5;
    uint32 encoded = (n << 1) ^ (n >> 31); // n >> 31 对负数为 0xFFFFFFFF(32个1)
    
    • 例:-5 → 编码后为 0xA(二进制 1010),Varints 编码仅需 1 字节。
  • 逆向变换(解码)
    // 32位:(n >> 1) ^ -(n & 1)
    int32 decoded = (encoded >> 1) ^ (-(encoded & 1));
    
效果对比
类型数值编码后字节数说明
int32-510 字节直接 Varints 编码
sint32-52 字节Zigzag + Varints 编码
适用场景
  • 有符号整数(尤其是负数),如温度差值、账户余额变动等。
  • 推荐使用 sint32/sint64 替代 int32/int64 存储负数。

5.3 固定长度编码(Fixed32/Fixed64)

原理
  • 定长存储:无论数值大小,固定占用 4 字节(fixed32/sfixed32/float)或 8 字节(fixed64/sfixed64/double)。
  • 适用场景
    • 大数值(>28bit 的无符号数,如哈希值、时间戳)。
    • 需快速读取的场景(如索引字段)。
对比 Varints
场景Varints(int32)fixed32
数值 ≤ 28bit1-4 字节(高效)4 字节(低效)
数值 > 28bit 且为正5 字节4 字节(高效)
负数10 字节不适用

5.4 长度分隔类型(Length-Delimited)

原理
  • TLV 结构
    • T(Type=2):标识字段类型为长度分隔。
    • L(Length):4 字节变长整数,标识值(V)的字节数。
    • V(Value):具体数据(如字符串、字节数组、嵌套消息)。
  • 示例:字符串 "hello"(UTF-8 编码,长度 5)
    • 编码:0xA(Type=2,Length=5) + "hello" 的字节数据。
适用场景
  • 变长数据类型:stringbytes、嵌套消息(message)、打包的重复字段(packed repeated)。

5.5 编码类型总结

类型Protobuf 类型编码方式适用场景
Varintsint32/uint32/int64/uint64/bool/enumBase128 变长小整数、非负数、枚举值
Zigzagsint32/sint64Zigzag + Varints有符号整数(尤其是负数)
固定 32fixed32/sfixed32/float4 字节定长大整数(>28bit)、浮点数
固定 64fixed64/sfixed64/double8 字节定长极大整数(>56bit)、双精度浮点数
长度分隔string/bytes/message/repeatedTLV(Type=2)变长字符串、二进制数据、嵌套消息

5.6 编码选择建议

  1. 优先使用 Varints:对于非负数、小整数(如用户 ID、计数器)。
  2. 负数用 sint32/sint64:避免 int32/int64 对负数的低效编码。
  3. 大整数用 fixed32/fixed64:如时间戳(秒级时间戳用 fixed32 仅需 4 字节)。
  4. 字符串 / 嵌套消息用长度分隔:确保数据边界清晰,支持动态长度。

二、protobuf语法详解

1. 定义消息类型基础

1.1 语法声明
  • 语法版本:proto3 语法需在文件首行声明 syntax = "proto3";,否则默认使用 proto2。
  • 示例
    syntax = "proto3";
    message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
    }
    
1.2 字段组成
  • 字段结构类型 字段名 = 标识号;
    • 类型:标量类型(如 stringint32)或自定义消息类型。
    • 标识号:唯一整数,用于二进制编码,不可修改。范围:1~536,870,911,禁止使用 19000~19999(预留段)。

2. 字段类型与规则

2.1 标量类型映射
.proto 类型C++ 类型Java 类型说明
doubledoubledouble64 位浮点数
int32int32int变长编码,负数效率低,建议用sint32
stringstringStringUTF-8 或 ASCII 编码文本
bytesstringByteString任意二进制数据
2.2 字段规则
  • singular:0 或 1 个值(默认规则,不可显式声明)。
  • repeated:0 或多个值,顺序保留,标量类型默认启用packed压缩。
    repeated int32 scores = 4; // 可重复字段
    

3. 枚举类型(Enum)

3.1 定义与规则
  • 语法
    message SearchRequest {enum Corpus {UNIVERSAL = 0; // 必须以0开头(默认值)WEB = 1;IMAGES = 2;}Corpus corpus = 4; // 使用枚举字段
    }
    
  • 注意
    • 枚举值需唯一,首值必须为 0(作为默认值)。
    • 允许别名(allow_alias = true),但需显式声明。
3.2 兼容性
  • 未识别的枚举值在反序列化时保留,不同语言处理方式不同(如 C++ 存储为整数,Java 通过特殊类型表示)。

4. 消息定义进阶

4.1 嵌套类型
  • 语法:在消息内部定义其他消息类型。
    message SearchResponse {message Result { // 嵌套消息string url = 1;string title = 2;}repeated Result results = 1; // 外部使用:ParentType.NestedType
    }
    
4.2 Any 类型(动态消息)
  • 用途:无需预先定义即可存储任意消息类型,需导入google/protobuf/any.proto
    import "google/protobuf/any.proto";
    message ErrorStatus {repeated google.protobuf.Any details = 2; // 存储任意消息
    }
    
  • 实现:通过 URL 标识类型(如type.googleapis.com/packagename.messagename),支持动态打包 / 解包。

5. 数据结构与特殊字段

5.1 Oneof(互斥字段)
  • 用途:确保同一时间只有一个字段被设置,节省内存。
    message SampleMessage {oneof test_oneof {string name = 4;SubMessage sub_message = 9; // 设置其中一个会清除另一个}
    }
    
  • 注意:不支持repeated,反射 API 可用,需避免内存泄漏(如 C++ 中需注意指针管理)。
5.2 Map(键值对)
  • 语法
    map<string, Project> projects = 3; // 键为string,值为自定义消息
    
  • 特性
    • 序列化顺序不确定,文本格式按键排序。
    • 重复键以最后一个为准,兼容旧版本(视为repeated MapFieldEntry)。

6. 版本兼容性与更新规则

6.1 安全更新操作
  • 允许操作
    • 添加新字段(旧版本解析时忽略,默认值处理)。
    • 删除字段(标记为deprecated,保留标识号)。
    • 兼容类型转换(如int32uint32boolint32)。
6.2 禁止操作
  • 修改已有字段的标识号。
  • 混用sint32与其他整数类型(不兼容)。
  • 删除repeated字段并替换为非repeated字段。

7. 代码生成与语言特性

7.1 生成文件结构
  • C++:生成.h.cc,每个消息对应一个类。
  • Java:生成.javaBuilder类,支持流式构建。
  • Python:生成模块,通过描述符动态创建类。
  • Go:生成.pb.go,支持结构体和方法绑定。
7.2 优化选项(optimize_for
  • SPEED:默认,生成高性能代码(序列化 / 解析最快)。
  • CODE_SIZE:代码体积小,适用于大量消息定义的场景。
  • LITE_RUNTIME:轻量级运行时,不含反射功能,适合移动端。
    option optimize_for = LITE_RUNTIME; // 启用轻量级模式
    

8. 高级特性与其他

8.1 包声明(package
  • 用途:避免命名冲突,影响生成代码的命名空间 / 包名。
    package foo.bar; // C++对应命名空间foo::bar,Java对应包com.foo.bar(需配合java_package)
    
8.2 服务定义(RPC 支持)
  • 语法:定义 RPC 服务接口,配合 gRPC 等框架生成代码。
    service SearchService {rpc Search(SearchRequest) returns(SearchResponse); // 定义远程方法
    }
    
8.3 JSON 映射
  • 规则:proto3 支持自动 JSON 编码 / 解码,字段名映射为驼峰式(如page_numberpageNumber)。
    • bytes→Base64 字符串,enum→枚举名,map→JSON 对象。

9. 选项(Options)与保留字段

9.1 常用选项
  • 文件选项
    • java_package:指定 Java 包名。
    • cc_enable_arenas:启用 C++ 内存池优化。
  • 字段选项
    • deprecated=true:标记字段废弃,生成代码中添加警告。
9.2 保留字段(reserved
  • 用途:防止字段标识号或名称被重用,避免版本冲突。
    message Foo {reserved 2, 15, 9 to 11; // 保留标识号reserved "foo", "bar"; // 保留字段名(影响JSON序列化)
    }

三、具体使用案例

1. 定义 .proto 文件(user.proto)

syntax = "proto3";package demo; // 包名,影响生成的命名空间// 定义用户消息类型
message User {string name = 1;   // 姓名(字符串)int32 age = 2;     // 年龄(整数)string email = 3;  // 邮箱(字符串)
}

2. 生成 C++ 代码

安装 Protobuf 编译器(protoc)
# Ubuntu/Debian
sudo apt-get install protobuf-compiler libprotobuf-dev# 验证安装
protoc --version  # 应输出版本号(如 3.19.6)
生成代码
protoc --cpp_out=. user.proto  # 生成 user.pb.h 和 user.pb.cc

3. C++ 代码实现(main.cpp)

#include <iostream>
#include "user.pb.h"  // 包含生成的头文件using namespace std;
using namespace demo;  // 使用定义的包名// 序列化消息到文件
bool SerializeToFile(const User& user, const string& filename) {fstream output(filename, ios::out | ios::binary);if (!user.SerializeToOstream(&output)) {cerr << "Failed to serialize User." << endl;return false;}cout << "Serialized data to " << filename << endl;return true;
}// 从文件反序列化消息
bool ParseFromFile(User& user, const string& filename) {fstream input(filename, ios::in | ios::binary);if (!user.ParseFromOstream(&input)) {cerr << "Failed to parse User from file." << endl;return false;}cout << "Parsed data from " << filename << endl;return true;
}int main() {// 创建 User 对象并设置字段User user;user.set_name("Alice");user.set_age(30);user.set_email("alice@example.com");// 序列化到文件SerializeToFile(user, "user.bin");// 反序列化从文件User parsed_user;if (ParseFromFile(parsed_user, "user.bin")) {// 验证字段值cout << "Parsed Name: " << parsed_user.name() << endl;cout << "Parsed Age: " << parsed_user.age() << endl;cout << "Parsed Email: " << parsed_user.email() << endl;}return 0;
}

4. 编译与运行

编译命令
g++ -std=c++11 main.cpp user.pb.cc -lprotobuf -o protobuf_demo
  • -lprotobuf:链接 Protobuf 库。
  • user.pb.cc:生成的实现文件。
运行结果
./protobuf_demo

输出

 
Serialized data to user.bin
Parsed data from user.bin
Parsed Name: Alice
Parsed Age: 30
Parsed Email: alice@example.com

5. 代码解析

关键步骤说明
  1. 消息定义

    • .proto 文件中声明 User 消息,包含三个字段(nameageemail)。
    • 每个字段有唯一标识号(123),用于二进制编码。
  2. 代码生成

    • protoc 编译器根据 .proto 生成 user.pb.h 和 user.pb.cc,包含消息类的定义和序列化 / 反序列化方法。
  3. 序列化

    • SerializeToOstream:将 User 对象序列化为二进制流,写入文件。
    • Protobuf 使用高效的二进制格式(如 Varints 编码整数,长度分隔字符串)。
  4. 反序列化

    • ParseFromOstream:从文件读取二进制数据,解析为 User 对象。
    • 自动处理字段的类型转换和默认值(如未设置的字段使用默认值)。
  5. 字段访问

    • 通过生成的访问器(如 set_namename())操作字段,类型安全且高效。

6. 扩展说明

  • 嵌套消息:若需定义复杂结构,可在 .proto 中嵌套消息:
    message Address {string city = 1;string street = 2;
    }
    message User {string name = 1;Address address = 2; // 嵌套消息字段
    }
    
  • 枚举类型:用于限制字段取值范围:
    enum Gender {GENDER_UNKNOWN = 0;GENDER_MALE = 1;GENDER_FEMALE = 2;
    }
    message User {Gender gender = 4; // 使用枚举字段
    }
    
  • 重复字段:处理数组或列表:
    message User {repeated string hobbies = 5; // 可重复字段(列表)
    }

0voice · GitHub  

相关文章:

  • 网络基础知识
  • vue2实现【瀑布流布局】
  • 推一帧,通一气:跨平台RTMP推流的内家功夫
  • Mysql面经
  • SpringBoot3+Vue3(1)-后端 请求头校验,jwt退出登录,mybaits实现数据库用户校验
  • SAGE:基于SAM进行二级蒸馏的多模态图像融合新方法,CVPR2025
  • 如何处理 collation 导致的索引失效 | OceanBase SQL调优实践
  • 信奥赛-刷题笔记-栈篇-T3-P4387验证栈序列0520
  • 13 分钟讲解所有知名 Python 库/模块
  • Linux探秘:驾驭开源,解锁高效能——基础指令
  • 数据仓库是什么?常见问题解答
  • 彭博社聚焦Coinbase数据泄露,CertiK联创顾荣辉警示私钥风险与物理攻击
  • Java从入门到精通 - 案例专题
  • 【RK3588嵌入式图形编程】-Cairo-形状与填充
  • 瑞萨单片机笔记
  • JS 中 var、let、const 的区别联系
  • 奥威BI:打破AI数据分析伪场景,赋能企业真实决策价值
  • CesiumEarth v1.15 更新
  • 虚拟主机:经济便捷的 “云端小屋”
  • MySQL 8.0窗口函数详解
  • 王楚钦球拍受损,乒乓球裁判揭秘大赛球拍检测
  • 美国前驻华大使携美大学生拜访中联部、外交部
  • 上海文化馆服务宣传周启动,为市民提供近2000项活动
  • 半年不到再换岗:伊春市委常委、政法委书记方春彪任伊春森工集团党委书记
  • 国家外汇管理局:4月货物贸易项下跨境资金净流入649亿美元
  • 优质文化资源下基层,上海各区优秀群文团队“文化走亲”