一、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 即时通讯

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

字段 类型 长度(字节) 说明
length unsigned int 4 整个消息的长度,包括协议头 + BODY
version unsigned short 2 通信协议的版本号
appid unsigned short 2 对外 SDK 提供服务时,用来识别不同的客户
service_id unsigned short 2 对应命令的分组类,比如 login 和 msg 是不同分组
command_id unsigned short 2 分组里面的子命令,比如 login 和 login response
seq_num unsigned short 2 消息序号
reserve unsigned short 2 预留字节
body unsigned char[] n 具体的协议数据
3.1.2 范例 2 - 云平台节点服务器
字段 类型 长度(字节) 说明
STAG unsigned short 2 通信协议数据包的开始标志,如 0xff 0xfe
version unsigned short 2 通信协议的版本号,目前为 0x01
checksum unsigned char 1 计算协议数据校验和,若为加密数据,则计算密文校验和。校验和计算范围:协议头 CheckSum 字段后数据,协议体全部数据
type unsigned char 1 0 表示协议体是 json 格式,其它值未定义。设备心跳消息类型的值为 0xA0
seqno unsigned int 4 通信数据报文的序列号,应答报文序列号必须与请求报文序列号相同
Length unsigned short 4 报文内容长度,即从该字段后报文内容长度
reserve unsigned int 4 预留字节,设备心跳消息类型的值为 devid
body unsigned 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 开发的独立、轻量级数据交换格式,以二进制结构存储。
类型 XML JSON Protobuf
通用性 通用 通用 通用(需编译器生成对应语言代码)
数据长度 重量级 轻量级 轻量级
格式 文本格式 文本格式 二进制格式

范例:一个 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) 488ms 452ms 297 284ms 251ms
jsoncpp(C++) 871ms 709ms 255 786ms 709ms
rapidjson(C++) 701ms 113ms 117 1288ms 953ms
tinyxml2(XML) - - - 1781ms -
protobuf 241ms 83ms 239 190ms 80ms

说明

  • 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. 下载与解压
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++ 类型 编码方式 适用场景
double double 定长(8 字节) 浮点数值,对精度要求高的场景(如坐标、金额)
float float 定长(4 字节) 浮点数值,精度要求较低时(如概率、比例)
int32 int32 变长 非负数或小范围整数(若存在负数,优先使用 sint32
uint32 uint32 变长 无符号整数
int64 int64 变长 大范围整数(若存在负数,优先使用 sint64
sint32 int32 变长(ZigZag) 有符号整数(负数编码效率高于 int32
sint64 int64 变长(ZigZag) 大范围有符号整数
fixed32 uint32 定长(4 字节) 数值常大于 228 的无符号整数(如哈希值、版本号)
fixed64 uint64 定长(8 字节) 数值常大于 256 的无符号整数(如时间戳、文件大小)
bool bool 变长(0 或 1) 布尔值
string std::string 变长(UTF-8) 文本数据(如用户名、地址)
bytes std::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 -5 10 字节 直接 Varints 编码
sint32 -5 2 字节 Zigzag + Varints 编码
适用场景
  • 有符号整数(尤其是负数),如温度差值、账户余额变动等。
  • 推荐使用 sint32/sint64 替代 int32/int64 存储负数。

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

原理
  • 定长存储:无论数值大小,固定占用 4 字节(fixed32/sfixed32/float)或 8 字节(fixed64/sfixed64/double)。
  • 适用场景
    • 大数值(>28bit 的无符号数,如哈希值、时间戳)。
    • 需快速读取的场景(如索引字段)。
对比 Varints
场景 Varints(int32) fixed32
数值 ≤ 28bit 1-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 类型 编码方式 适用场景
Varints int32/uint32/int64/uint64/bool/enum Base128 变长 小整数、非负数、枚举值
Zigzag sint32/sint64 Zigzag + Varints 有符号整数(尤其是负数)
固定 32 fixed32/sfixed32/float 4 字节定长 大整数(>28bit)、浮点数
固定 64 fixed64/sfixed64/double 8 字节定长 极大整数(>56bit)、双精度浮点数
长度分隔 string/bytes/message/repeated TLV(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 类型 说明
double double double 64 位浮点数
int32 int32 int 变长编码,负数效率低,建议用sint32
string string String UTF-8 或 ASCII 编码文本
bytes string ByteString 任意二进制数据
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  

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐