设计⽬标
解析效率:互联⽹业务具有⾼并发的特点,解析效率决定了使⽤协议的CPU成本;
编码⻓度:信息编 码出来的⻓度,编码⻓度决定了使⽤协议的⽹络带宽及存储成本;
易于实现:互联⽹业务需要⼀个轻量级的协议,⽽不是⼤⽽全的;
可读性:编码后的数据的可读性决定了使⽤协议的调试及维护成本(不同的序列化协议是有不同的应⽤的场景);
兼容性: 互联⽹的需求具有灵活多变的特点,协议会经常升级,使⽤协议的双⽅是否可以独⽴升级协议、增减协议中的字段是⾮常重要的;
跨平台跨语⾔:互联⽹的的业务涉及到不同的平台和语⾔,⽐如Windows⽤C++,Android⽤Java, Web⽤Js,IOS⽤object-c。
安全可靠:防⽌数据被破解
1 协议概述
什么协议:协议是⼀种约定,通过约定,不同的进程可以对⼀段数据产⽣相同的理解,从⽽可以相互协作,存在进程间通信的程序就⼀定需要协议。为什么说进程间通信就需要协议?⽽不是说客户端和服务器端之前?为什么需要⾃⼰设计协议?
⽐如不同表的插头,还需要进⾏各种转换,如果我们两端进⾏通信没有约定好协议,那彼此是不知道对⽅发送的数据是什么意义。
2 判断消息的完整性
为了能让对端知道如何给消息帧分界,⽬前⼀般有以下做法:
1. 以特定符号来分界,如每个消息都以特定的字符来结尾(如\r\n),当在字节流中读取到该字符时, 则表明上⼀个消息到此为⽌
2. 固定消息头+消息体结构,这种结构中⼀般消息头部分是⼀个固定字节⻓度的结构,并且消息头中会有⼀个特定的字段指定消息体的⼤⼩。收消息时,先接收固定字节数的头部,解出这个消息完整⻓度, 按此⻓度接收消息体。这是⽬前各种⽹络应⽤⽤的最多的⼀种消息格式;header + body
3. 在序列化后的buffer前⾯增加⼀个字符流的头部,其中有个字段存储消息总⻓度,根据特殊字符(⽐如根据\n或者\0)判断头部的完整性。这样通常⽐3要麻烦⼀些,HTTP和REDIS采⽤的是这种⽅式。收消息的时候,先判断已收到的数据中是否包含结束符,收到结束符后解析消息头,解出这个消息完整⻓度,按此⻓度接收消息体。
3 协议设计
重点: 消息边界 版本区分 消息类型区分
3.1 协议设计范例
3.1.1 范例1-IM即时通讯
3.1.2 范例2-云平台节点服务器
3.1.3 范例3-nginx
3.1.4 范例4-HTTP协议
HTTP协议是我们最常⻅的协议,我们是否可以采⽤HTTP协议作为互联⽹后台的协议呢?这个⼀般是不适 当的,主要是考虑到以下2个原因:
1) HTTP协议只是⼀个框架,没有指定包体的序列化⽅式,所以还需要配合其他序列化的⽅式使⽤才能传 递业务逻辑数据。 2) HTTP协议解析效率低,⽽且⽐较复杂(不知道有没有⼈觉得HTTP协议简单,其实不是http协议简单, ⽽是HTTP⼤家⽐较熟悉⽽已)
有些情况下是可以使⽤HTTP协议的:
1) 对公⽹⽤户api,HTTP协议的穿透性最好,所以最适合;
2) 效率要求没那么⾼的场景;
3) 希望提供更多⼈熟悉的接⼝,⽐如新浪微、腾讯博提供的开放接⼝;
⽐如图床项⽬:
3.2 序列化⽅法
TLV编码及其变体(TLV是tag, length和value的缩写):⽐如Protobuf。
⽂本流编码:⽐如XML/JSON
固定结构编码: 基本原理是,协议约定了传输字段类型和字段含义,和TLV的⽅式类似,但是没有了 tag和len,只有value,⽐如TCP/IP
内存dump:基本原理是,把内存中的数据直接输出,不做任何序列化操作。反序列化的时候,直接还原内存。
3.2.1 常⻅序列化⽅法
主流序列化协议:xml、json、protobuf
1. XML指可扩展标记语⾔(eXtensible Markup Language)。是⼀种通⽤和重量级的数据交换格式。 以⽂本⽅式存储。
2. JSON(JavaScript ObjectNotation, JS 对象简谱) 是⼀种通⽤和轻量级的数据交换格式。以⽂本结构 进⾏存储。
3. protocol buffer是Google的⼀种独⽴和轻量级的数据交换格式。以⼆进制结构进⾏存储。
3.2.2序列化结果数据对⽐
xml
j s o n protobuf
3.2.3 序列化、反序列化速度对⽐
测试10万次序列化
测试10万次反序列化
3.3 协议安全
1. xxtea 固定key (https://blog.csdn.net/gsls200808/article/details/48243019)
2. AES 固定key(https://www.yuque.com/docs/share/e1cc0245-5daf-466b-ad25- 32f9be9dff06?# 《AES详解》)
3. openssl
4. Signal protocol 端到端的通讯加密协议(https://www.yuque.com/docs/share/d8305ed5- 53ba-4533-b4ac-f1cbe52b7c58?# 《Signal protocol 开源协议理解》)
4 protobuf的使⽤
Protocol buffers 是⼀种语⾔中⽴,平台⽆关,可扩展的序列化数据的格式,可⽤于通信协议,数据存储等。Protocol buffers 在序列化数据⽅⾯,它是灵活的,⾼效的。相⽐于 XML 来说,Protocol buffers 更加⼩巧,更加快速,更加简单。⼀旦定义了要处理的数据的数据结构之后,就可以利⽤ Protocol buffers 的代码⽣成⼯具⽣成相关的代码。甚⾄可以在⽆需重新部署程序的情况下更新数据结构。只需使⽤Protobuf对数据结构进⾏⼀次描述,即可利⽤各种不同语⾔或从各种不同数据流中对你的结构化数据轻松读写。
Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可⽤于通讯协议、数据存储等领域的语⾔⽆关、平台⽆关、可扩展的序列化结构数据格式。tars brpc
4.1 ProtoBuf 协议的⼯作流程
IDL是Interface description language的缩写,指接⼝描述语⾔。
可以看到,对于序列化协议来说,使⽤⽅只需要关注业务对象本身,即 idl 定义(.proto),序列化和反序列化的代码只需要通过⼯具⽣成即可。
4.2 protobuf的编译安装
https://github.com/protocolbuffers/protobuf ⾕歌开源的 协议标准+⼯具
编译安装
安装⼯具->根据编写的proto⽂件产⽣c++代码
1. 解压 tar zxvf protobuf-cpp-3.8.0.tar.gz
2. 编译(make的时间有点⻓)
cd protobuf-3.8.0/
./configure
make
sudo make install
sudo ldconfig
3. 显示版本信息
protoc --version ⽣成各种版本的代码
4. 编写proto⽂件
5. 将proto⽂件⽣成对应的.cc和.h⽂件
protoc -I=/路径1 --cpp_out=./路径2 /路径1/addressbook.proto
路径1为.proto所在的路径
路径2为.cc和.h⽣成的位置
将指定proto⽂件⽣成.pb.cc和.pb.h
protoc -I=./ --cpp_out=./ test.proto
将对应⽬录的所有proto⽂件⽣成.pb.cc和.pb.h
protoc -I=./ --cpp_out=./ *.proto
6. 编译范例
g++ -std=c++11 -o list_people list_people.cc addressbook.pb.cc -lprotobuf -lpthread
protobuf option部分选项
option optimize_for = LITE_RUNTIME;optimize_for是⽂件级别的选项,Protocol Buffer定义三种优化级别 SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。
1. SPEED: 表示⽣成的代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。
2. CODE_SIZE: 和SPEED恰恰相反,代码运⾏效率较低,但是由此⽣成的代码编译后会占⽤更少的空间,通常⽤于资源有限的平台,如Mobile。
3. LITE_RUNTIME: ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接 libprotobuf-lite,⽽⾮libprotobuf。
protobuf的反射参考(实际开发中C++使⽤反射的场景⽐较少):
1. https://blog.csdn.net/JMW1407/article/details/107223287
2. https://blog.csdn.net/chengangdzzd/article/details/50446367
4.3 标量数值类型
⼀个标量消息字段可以含有⼀个如下的类型——该表格展示了定义于.proto⽂件中的类型,以及与之对应的、在⾃动⽣成的访问类中定义的类型:
4.4 protobuf的使⽤实例-和范例1-即时通讯同代码
1. pb协议设计 2. 登录 3. 发送消息 4. 代码验证
4.5 protobuf⼯程经验
1. proto⽂件命名规则; 2. proto命名空间; 3. 引⽤⽂件; 4. 多个平台使⽤同⼀份proto⽂件
5 protobuf的编码原理
varints 和zigzag protobuf的编码原理 参考:高效的数据压缩编码方式 Protobuf · 语雀 (yuque.com)
把4.3 节的各种变量类型归为6⼤类。出去deprecated的还有4个⼤类。从下图我们可以简单总结:
变⻓编码类型Varints
固定32bit类型
固定64bit类型
有⻓度标记类型
5.1 Varints 编码(变⻓的类型才使⽤)
为什么设计变⻓编码:普通的 int 数据类型, ⽆论其值的⼤⼩, 所占⽤的存储空间都是相等的,⽐如 不管是0x12345678 还是0x12都占⽤4字节,那能否让0x12在表示的时候只占⽤1个字节呢? 是否可以根据数值的⼤⼩来动态地占⽤存储空间, 使得值⽐较⼩的数字占⽤较少的字节数, 值相对⽐较⼤的数字占⽤较多的字节数, 这即是变⻓整型编码的基本思想。
采⽤变⻓整型编码的数字, 其占⽤的字节数不是完全⼀致的, Varints 编码使⽤每个字节的最⾼有效 位作为标志位, ⽽剩余的 7 位以⼆进制补码的形式来存储数字值本身, 当最⾼有效位为 1 时, 代表其 后还跟有字节, 当最⾼有效位为 0 时, 代表已经是该数字的最后的⼀个字节。
在 Protobuf 中, 使⽤的是 Base128 Varints 编码, 在这种⽅式中, 使⽤ 7 bit (即2的7次⽅为128) 来存储数字, 在 Protobuf 中, Base128 Varints 采⽤的是⼩端序, 即数字的低位存放在⾼地址, 举例 来看, 对于数字 1, 我们假设 int 类型占 4 个字节, 以标准的整型存储, 其⼆进制表示应为
可⻅, 只有最后⼀个字节存储了有效数值, 前 3 个字节都是 0, 若采⽤ Varints 编码, 其⼆进制形式为
因为其没有后续字节, 因此其最⾼有效位为 0, 其余的 7 位以补码形式存放 1, 再⽐如数字 666, 其以 标准的整型存储, 其⼆进制表示为
从上⾯的编码解码过程可以看出, 可变⻓整型编码对于不同⼤⼩的数字, 其所占⽤的存储空间是 不同的。
问: 如果⼀个值为0xff ff ff ff那需要多少个字节存储? 答:0xff ff ff ff需要分配32个bit,使⽤base128 Varints 编码需要的字节数: 32/7=4.57, 只要 不整除就要进位, 就是需要5个字节存储。 从这⾥看得出来,= 变量 >28bit可以考虑使⽤fixed32, sfixed32等固定4字节的类型。
5.2 Zigzag 编码(针对负数的)
这⾥重点:
主要理解为什么负数不建议使⽤int类型
对于Zigzag的算法不必太细究。其⽬的是把多个1转成多个0表示。
Varints 编码的实质在于去掉数字开头的 0, 因此可缩短数字所占的存储字节数, 在上⾯的例⼦ 中, 只举例说明了正数的 Varints 编码, 但如果数字为负数, 则采⽤ Varints 编码会恒定占⽤ 10 个字 节, 原因在于负数的符号位为 1, 对于负数其从符号位开始的⾼位均为 1, 在 Protobuf 的具体实现中, 会将此视为⼀个很⼤的⽆符号数, 以C++语⾔的实现为例, 对于 int32 类型的 pb 字段, 对于如 定义的 proto
Request 中包含类型为 int32 类型的字段, 当 a 为负数时, 其序列化之后将恒定占⽤ 10 个字节, 我们可以 使⽤如下的测试代码(⻅2-protobuf\5-codec代码的Tint32()函数)
std::string strPb; uint8_t *szData; Codec::Tint32 int1; int1.set_n1(-5); int1Size = int1.ByteSize(); // 序列化后的⼤⼩, 占⽤11字节 std::cout << "-5 int1Size = " << int1Size << std::endl; strPb.clear(); strPb.resize(int1Size); szData = (uint8_t *)strPb.c_str(); int1.SerializeToArray(szData, int1Size); // 拷⻉序列化后的数据 printHex(szData, int1Size); // 08 fb ff ff ff ff ff ff ff ff 01
对于 int32 类型的数字 -5, 其序列化之后的⼆进制为
究其原因在于 Protobuf 的内部将 int32 类型的负数转换为 uint64 来处理。⽐如整数-5:
取 5 的 原 码 : 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101,
得 反 码 : 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111010,
对 反 码 加 1 最 后 得 补 码 : 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111011,
即 -5 在 计 算 机 ⾥ ⽤ ⼆ 进 制 表 示 结 果 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111011。
转成每7bit占⽤1个字节,
( ⾼ 位 ) 1 1111111 1111111 1111111 1111111 1111111 1111111 1111111 1111111 1111011(低位) 然后⾼地址存储到低地址,并且不是结束字节最⾼位为1,即是
(低位)11111011 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 00000001(⾼位)
转成16进制:fb ff ff ff ff ff ff ff ff 01 数据本身就占⽤了10字节。
转换后的 uint64 数值的⾼位全为 1, 相当于是⼀个 8 字节的很⼤的⽆符号数, 因此采⽤ Base128 Varints 编码后将恒定占⽤ 10 个字节的空间, 可⻅ Varints 编码对于表示负数毫⽆优势, 甚⾄⽐普通 的固定 32 位存储还要多占 4 个字节。Varints 编码的实质在于设法移除数字开头的 0 ⽐特, ⽽对于 负数, 由于其数字⾼位都是 1, 因此 Varints 编码在此场景下失效, Zigzag 编码便是为了解决这个问 题, Zigzag 编码的⼤致思想是⾸先对负数做⼀次变换, 将其映射为⼀个正数, 变换以后便可以使⽤ Varints 编码进⾏压缩, 这⾥关键的⼀点在于变换的算法, ⾸先算法必须是可逆的, 即可以根据变换后 的值计算出原始值, 否则就⽆法解码, 同时要求变换算法要尽可能简单, 以避免影响 Protobuf 编码、 解码的速度, 我们假设 n 是⼀个 32 位类型的数字, 则 Zigzag 编码的计算⽅式为
要注意这⾥左边是逻辑移位, 右边是算术移位, 右边的含义实际是得到⼀个全 1 (对于负数) 或全 0 (对 于正数)的⽐特序列, 因为对于任意⼀个位数为 η 的有符号数 n, 其最⾼位为符号位, 剩下的 η - 1 位为数字位, 将其算术右移 η - 1 位, 由于是算术移位, 因此右移时左边产⽣的空位将由符号位来填充, 进⾏ η - 1 次算术右移之后便得到 η 位与原先的符号位相等的序列, 然后对两边按位异或便得到 Zigzag 编码, 我们⽤⼀个图示来直观地说明 Zigzag 编码的设计思想, 为了简化, 我们假定数字是 16 位的, 先来看负数的情形, 假设数字为 -5, 其在内存中的形式为
⾸先对其进⾏⼀次逻辑左移, 移位后空出的⽐特位由 0 填充
然后对原数字进⾏ 15 次算术右移, 得到 16 位全为原符号位(即 1)的数字
然后对逻辑移位和算术移位的结果按位异或, 便得到最终的 Zigzag 编码
可以看到, 对负数使⽤ Zigzag 编码以后, 其⾼位的 1 全部变成了 0, 这样以来我们便可以使⽤ Varints 编码进⾏进⼀步地压缩, 再来看正数的情形, 对于 16 位的正数 5, 其在内存中的存储形式为
我们按照与负数相同的处理⽅法, 可以得到其 Zigzag 编码为
从上⾯的结果来看, ⽆论是正数还是负数, 经过 Zigzag 编码以后, 数字⾼位都是 0, 这样以来, 便可以 进⼀步使⽤ Varints 编码进⾏数据压缩, 即 Zigzag 编码在 Protobuf 中并不单独使⽤, ⽽是配合 Varints 编码共同来进⾏数据压缩, Google 在 Protobuf 的官⽅⽂档中写道:
If you use int32 or int64 as the type for a negative number, the resulting varint is always ten bytes long – it is, effectively, treated like a very large unsigned integer. If you use one of the signed types, the resulting varint uses ZigZag encoding, which is much more efficient。
逆向原理不做进⼀步引申了,ZigZag的逆函数 = (n>>1)^ -(n&1),有兴趣参考:《⼩⽽巧的数字压缩算法:zigzag》 https://blog.csdn.net/zgwangbo/article/details/51590 186
sint32 = Zigzag 编码 +varints编码合起来 (负数使用这个)
sint32 序列化: 负数 -> Zigzag 编码 -> varints编码
sint32 反序列化: varints解码 -> Zigzag 解码 -> 负数
重点在于:同样是表示-5,sint32只需要2个字节,int32需要11字节。
5.3 Protobuf 的数据组织
在上⾯的讨论中, 我们了解了 Protobuf 所使⽤的 Varints 编码和 Zigzag 编码的编码原理, 本节我 们来讨论 Protobuf 的数据组织⽅式, ⾸先来看⼀个例⼦, 假设客户端和服务端使⽤ protobuf 作为数 据交换格式, proto 的具体定义为
syntax = "proto3"; package pbTest; message Request { int32 age = 1; }
Request 中包含了⼀个名称为 age 的字段, 客户端和服务端双⽅都⽤同⼀份相同的 proto ⽂件是没有任何问题的, 假设客户端⾃⼰将 proto ⽂件做了修改, 修改后的 proto ⽂件如下
syntax = "proto3"; package pbTest; message Request { int32 age_test = 1; }
在这种情形下, 服务端不修改应⽤程序仍能够正确地解码, 原因在于序列化后的 Protobuf 没有使⽤ 字段名称, ⽽仅仅采⽤了字段编号, 与 json xml 等相⽐, Protobuf 不是⼀种完全⾃描述的协议格 式, 即接收端在没有 proto ⽂件定义的前提下是⽆法解码⼀个 protobuf 消息体的, 与此相对的, json xml 等协议格式是完全⾃描述的, 拿到了 json 消息体, 便可以知道这段消息体中有哪些字段, 每 个字段的值分别是什么, 其实对于客户端和服务端通信双⽅来说, 约定好了消息格式之后完全没有必 要在每⼀条消息中都携带字段名称, Protobuf 在通信数据中移除字段名称, 这可以⼤⼤降低消息的⻓ 度, 提⾼通信效率, Protobuf 进⼀步将通信线路上消息类型做了划分, 如下表所示
对于 int32, int64, uint32 等数据类型在序列化之后都会转为 Varints 编码, 除去两种已标记 为 deprecated 的类型, ⽬前 Protobuf 在序列化之后的消息类型(wire-type) 总共有 4 种, Protobuf 除了存储字段的值之外, 还存储了字段的编号以及字段在通信线路上的格式类型(wire-type), 具体的存储⽅式为 field_num << 3 | wire type
即将字段标号逻辑左移 3 位, 然后与该字段的 wire type 的编号按位或, 在上表中可以看到, wire type 总共有 6 种类型, 因此可以⽤ 3 位⼆进制来标识, 所以低 3 位实际上存储了其后所跟的数据的 wire type, 接收端可以利⽤这些信息, 结合 proto ⽂件来解码消息结构体, 我们以上⾯ proto 为例来 看⼀段 Protobuf 实际序列化之后的完整⼆进制数据, 假设 age 为 5, 由于 age 在 proto ⽂件中定义 的是 int32 类型, 因此序列化之后它的 wire type 为 0, 其字段编号为 1, 因此按照上⾯的计算⽅式, 即 1 << 3 | 0, 所以其类型和字段编号的信息只占 1 个字节, 即 00001000, 后⾯跟上字段值 5 的 Varints 编码, 所以整个结构体序列化之后为
有了字段编号和 wire type, 其后所跟的数据的⻓度便是确定的, 因此 Protobuf 是⼀种⾮常紧密的数 据组织格式, 其不需要特别地加⼊额外的分隔符来分割⼀个消息字段, 这可⼤⼤提升通信的效率, 规避 冗余的数据传输。
1.当wire_type等于0的时候整个⼆进制结构为: Tag-Value
value的编码也采⽤Varints编码⽅式,故不需要额外的位来表示整个value的⻓度。因为Varint的msb位标 识下⼀个字节是否是有效的就起到了指示⻓度的作⽤。 ⽐如:
Tint32 test into 666 int1Size = 3 ⼗六进制:08 9a 05 0x1 int1Size = 2 ⼗六进制:08 01
2.当wire_type等于1、5的时候整个⼆进制结构也为: Tag-Value
因为都是取固定32位或者64位,因此也不需要额外的位来表示整个value的⻓度。
Tfixed64 test into 0x12 n1Size = 9 ⼗六进制:09 12 00 00 00 00 00 00 00 -5 n1Size = 9 ⼗六进制:09 fb ff ff ff ff ff ff ff Tfixed32 test into 0x12 n1Size = 5 ⼗六进制:0d 12 00 00 00 -5 n1Size = 5 ⼗六进制:0d fb ff ff ff
3.当wire_type等于2的时候整个⼆进制结构为: Tag-[Length]-Value 因为表示的是可变⻓度的值,需要有额外的位来指示⻓度。
Tstring test into --------------------> 1 str1Size = 3 ⼗六进制:0a 01 31 1234 str1Size = 6 ⼗六进制:0a 04 31 32 33 34 (这⾥4表示⻓度) ⽼师 str1Size = 8 ⼗六进制:0a 06 e8 80 81 e5 b8 88 (这⾥8表示⻓度)
repeat也是这种模式,此时length代表元素个数。
message TRepeatedfields{ repeated int32 n1 = 1; repeated Tbytes n2 = 2; } message Tbytes{ bytes n1 = 1; }
filed_num范围:
1. 1到15,仅使⽤1bytes。每个byte包含两个部分,⼀个是field_number⼀个是tag,其中field-number就是protobuf中每个值后等号后的数字(在C++和Java中,如果不设置这个值,则它是随机 的,如果在Python中,不设置,它则不被处理(这个在message binary format中的Field Order⼀节 中有提到)。那么我们可以认为这个field_number是必须的。那么⼀个byte⽤来表达这个值就是 000000000,其中红⾊表示是否有后续字节,如果为0表示没有也就是这是⼀个字节,蓝⾊部分表示 field-number,绿⾊部分则是wire_type部分,表示数据类型。也就是(field_number << 3) | wire_type。其中wire_type只有3位,表示数据类型。那么能够表示field_number的就是4位蓝⾊的数 字,4位数字能够表达的最⼤范围就是1-15(其中0是⽆效的)。
2. 16到2047,与上⾯的规则其实类似(类似base128的⽅式),下⾯以2bytes为例⼦,那么就有 10000000 00000000,其中红⾊部分依然是符号位,因为每个byte的第⼀位都⽤来表示下⼀byte是否 和⾃⼰有关,那么对于>1byte的数据,第⼀位⼀定是1,因为这⾥假设是2byte,那么第⼆个byte的第 ⼀位也是红⾊,刨除这两位,再扣掉3个wire_type位,剩下11位(2*8-2-3),能够表达的数字范围 就是2047(2 )。
当filed_num > 15时, 以16、64、65为例,value值为1
int32 n1_int32 = 16;
5.4 编码总结
1. Protobuf 采⽤ Varints 编码和 Zigzag 编码来编码数据, 其中 Varints 编码的思想是移除数字⾼ 位的 0, ⽤变⻓的⼆进制位来描述⼀个数字, 对于⼩数字, 其编码⻓度短, 可提⾼数据传输效率, 但 由于它在每个字节的最⾼位额外采⽤了⼀个标志位来标记其后是否还跟有有效字节, 因此对于⼤ 的正数, 它会⽐使⽤普通的定⻓格式占⽤更多的空间, 另外对于负数, 直接采⽤ Varints 编码将恒 定占⽤ 10 个字节, Zigzag 编码可将负数映射为⽆符号的正数, 然后采⽤ Varints 编码进⾏数据 压缩, 在各种语⾔的 Protobuf 实现中, 对于 int32 类型的数据, Protobuf 都会转为 uint64 ⽽后 使⽤ Varints 编码来处理, 因此当字段可能为负数时, 我们应使⽤ sint32 或 sint64, 这样 Protobuf 会按照 Zigzag 编码将数据变换后再采⽤ Varints 编码进⾏压缩, 从⽽缩短数据的⼆进制位数
2. Protobuf 不是完全⾃描述的信息描述格式, 接收端需要有相应的解码器(即 proto 定义)才可解析 数据格式, 序列化后的 Protobuf 数据不携带字段名, 只使⽤字段编号来标识⼀个字段, 因此更改 proto 的字段名不会影响数据解析(但这显然不是⼀种好的⾏为), 字段编号会被编码进⼆进制的消息结构中, 因此我们应尽可能地使⽤⼩字段编号
3. Protobuf 是⼀种紧密的消息结构, 编码后字段之间没有间隔, 每个字段头由两部分组成: 字段编 号和 wire type, 字段头可确定数据段的⻓度, 因此其字段之前⽆需加⼊间隔, 也⽆需引⼊特定的 数据来标记字段末尾, 因此 Protobuf 的编码⻓度短, 传输效率⾼。
6. protobuf协议消息升级
如果后⾯发现之前定义 message 需要增加字段了,这个时候就体现出 Protocol Buffer 的优势了,不需要改动之前的代码。不过需要满⾜以下 10 条规则:
1. 不要改动原有字段的数据结构。
2. 如果您添加新字段,则任何由代码使⽤“旧”消息格式序列化的消息仍然可以通过新⽣成的代码进⾏分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码⽣成的消息进⾏交互。同样,由新代码创建的消息可以由旧代码解析:旧的⼆进制⽂件在解析时会简单地忽略新字段。(具体原因⻅未知字段这⼀章节)
3. 只要字段号在更新的消息类型中不再使⽤,字段可以被删除。您可能需要重命名该字段,可能会添加前缀“OBSOLETE_”,或者标记成保留字段号 reserved ,以便将来的 .proto ⽤户不会意外重复使⽤该号码。
syntax "proto3"; message Stock { reserved 3, 4; //通过,隔开 int32 id = 1; string symbol = 2; } message Info { reserved 2, 9 to 11, 15; // 可以通过to指定连续返回 // ... }
4. int32,uint32,int64,uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之⼀更改为另⼀个字段⽽不破坏向前或向后兼容性。如果⼀个数字从不适合相应类型的线路中解析出来,则会得到与在 C++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。
5. sint32 和 sint64 相互兼容,但与其他整数类型不兼容。6. 只要字节是有效的UTF-8,string 和 bytes 是兼容的。
7. 嵌⼊式 message 与 bytes 兼容,如果 bytes 包含 message 的 encoded version。
8. fixed32与sfixed32兼容,⽽fixed64与sfixed64兼容。
9. enum 就数组⽽⾔,是可以与 int32,uint32,int64 和 uint64 兼容(请注意,如果它们不适合,值将被截断)。但是请注意,当消息反序列化时,客户端代码可能会以不同的⽅式对待它们:例如,未识别的 proto3 枚举类型将保留在消息中,但消息反序列化时如何表示是与语⾔相关的。(这点和语⾔相关,上⾯提到过了)Int 域始终只保留它们的值。
10. 将单个值更改为新的成员是安全和⼆进制兼容的。如果您确定⼀次没有代码设置多个字段,则将多个字段移⾄新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。(注意字段和值的区 别,字段是 field,值是 value)