深入protobuf(Protocol Buffers)原理:简化你的数据序列化

一、protobuf 概述

经过上一篇对通信消息协议设计的分析,这篇主要说明protobuf的原理和使用。

Protocol buffers 是⼀种语⾔中⽴,平台⽆关,可扩展的序列化数据的格式,可⽤于通信协议,数据存储 等。Protocol buffers 在序列化数据具有灵活、⾼效的特点。
相⽐于 XML 来说,Protocol buffers 更加 ⼩巧,更加快速,更加简单。⼀旦定义了要处理的数据的数据结构之后,就可以利⽤ Protocol buffers 的 代码⽣成⼯具⽣成相关的代码。甚⾄可以在⽆需重新部署程序的情况下更新数据结构。只需使⽤ Protobuf 对数据结构进⾏⼀次描述,即可利⽤各种不同语⾔或从各种不同数据流中对你的结构化数据轻松 读写。

Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可⽤于通讯协议、数据存储等领域的语⾔⽆ 关、平台⽆关、可扩展的序列化结构数据格式。
在这里插入图片描述

Protocol buffers在游戏和即时通信用的比较多。使用常见分析:

协议场景举例
xml主要在本地使用UI,游戏信息
jsonhttp apiHTTP网页注册账户
protobuf服务与服务的远程调用rpc,游戏,即时通讯,tars brpc

二、protobuf 协议的⼯作流程

要使用protobuf序列化方式,要先编写proto文件。

syntax="proto3"; 					// 版本,proto2和proto3
package IM.Login;					// 类似CPP的命名空间
import "IM.BaseDefine.proto";		// 引用其他的proto文件
option optimize_for = LITE_RUNTIME;	// 编译优化

// 一个类
message IMLoginReq{
	// 各种字段
	string user_name=1;
	string password=2;
	IM.BaseDefine.UserStatType online_status=3;
	IM.BaseDefine.ClientType client_type=4;
	string client_version=5;
}

然后利用工具生成.cc和.h文件。

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.proto

最后让程序调用。

proto文件在发送端和接收端是公用的,及发送端和接收端使用的是同样的proto文件。

deserialize
serialize
deserialize
serialize
newwork
skeleton
Server
protocol stack
compiler
idl
Client
skeleton
protocol stack

protocol_buffers
IDL是Interface description language的缩写,指接⼝描述语⾔。

可以看到,对于序列化协议来说,使⽤⽅只需要关注业务对象本身,即IDL定义(.proto),序列化和反序 列化的代码只需要通过⼯具⽣成即可。

protobuf不能完全替代json,比如对外注册,json只需要把格式提供给对方,而protobuf还需要一些复杂的流程,会降低可读性。

同一个proto文件可以生成不同的语言。
在这里插入图片描述

三、protobuf 的编译安装及使用

⾕歌开源的协议标准+⼯具
安装⼯具 —> 根据编写的proto⽂件产⽣c++代码。

(1)下载。

wget https://github.com/protocolbuffers/protobuf/releases/download/v21.7/protobuf-cpp-3.21.7.tar.gz

(2)解压。

tar zxvf protobuf-cpp-3.21.7.tar.gz

(3)编译。时间可能会有点长。

cd protobuf-3.21.7/ 
./configure 
make 
sudo make install
sudo ldconfig

(4)查看版本信息。

protoc --version

(5)编写proto文件。
示例:

syntax="proto3"; 					// 版本,proto2和proto3
package IM.Login;					// 类似CPP的命名空间
//import "IM.BaseDefine.proto";		// 引用其他的proto文件
option optimize_for = LITE_RUNTIME;	// 编译优化

// 一个类
message IMLoginReq{
	// 各种字段
	string user_name=1;
	string password=2;
	//IM.BaseDefine.UserStatType online_status=3;
	//IM.BaseDefine.ClientType client_type=4;
	string client_version=5;
}

(6)将proto文件生成相应的.pb.cc文件和.pb.h。

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.proto

SRC_DIR是.proto所在的路径。DST_DIR是.cc和.h生成的位置。

示例:
将指定proto⽂件⽣成.pb.cc和.pb.h 。

protoc -I=./ --cpp_out=./ test.proto 

将对应⽬录的所有proto⽂件⽣成.pb.cc和.pb.h

protoc -I=./ --cpp_out=./ *.proto

(7)编译范例。

g++ -std=c++11 -o list_people list_people.cc addressbook.pb.cc -lprotobuf - lpthread

注意要有-lprotobuf来指定库。

protobuf option部分选项:

option optimize_for = LITE_RUNTIME; 

optimize_for是⽂件级别的选项,Protocol Buffer定义三种优化级别 :PEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。

  1. SPEED: 表示⽣成的代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。
  2. CODE_SIZE: 和SPEED恰恰相反,代码运⾏效率较低,但是由此⽣成的代码编译后会占⽤更少的空 间,通常⽤于资源有限的平台,如Mobile。
  3. LITE_RUNTIME: ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接 libprotobuf-lite,⽽⾮libprotobuf。
-rw-r--r--  1 root root   89501198 1013 16:02 libprotobuf.a
-rwxr-xr-x  1 root root        986 1013 16:02 libprotobuf.la*
-rw-r--r--  1 root root   15320786 1013 16:02 libprotobuf-lite.a
-rwxr-xr-x  1 root root       1021 1013 16:02 libprotobuf-lite.la*
lrwxrwxrwx  1 root root         26 1013 16:02 libprotobuf-lite.so -> libprotobuf-lite.so.32.0.7*
lrwxrwxrwx  1 root root         26 1013 16:02 libprotobuf-lite.so.32 -> libprotobuf-lite.so.32.0.7*
-rwxr-xr-x  1 root root    5827448 1013 16:02 libprotobuf-lite.so.32.0.7*
lrwxrwxrwx  1 root root         21 1013 16:02 libprotobuf.so -> libprotobuf.so.32.0.7*
lrwxrwxrwx  1 root root         21 1013 16:02 libprotobuf.so.32 -> libprotobuf.so.32.0.7*
-rwxr-xr-x  1 root root   33984952 1013 16:02 libprotobuf.so.32.0.7*
-rw-r--r--  1 root root  130421776 1013 16:02 libprotoc.a
-rwxr-xr-x  1 root root       1002 1013 16:02 libprotoc.la*
lrwxrwxrwx  1 root root         19 1013 16:02 libprotoc.so -> libprotoc.so.32.0.7*
lrwxrwxrwx  1 root root         19 1013 16:02 libprotoc.so.32 -> libprotoc.so.32.0.7*
-rwxr-xr-x  1 root root   43255928 1013 16:02 libprotoc.so.32.0.7*

四、protobuf 标量数值类型

⼀个标量消息字段可以含有⼀个如下的类型——该表格展示了定义于.proto⽂件中的类型,以及与之对应 的、在⾃动⽣成的访问类中定义的类型:

.proto TypeNotesC++ TypeJava TypeGo Type
doubledoubledoublefloat64
floatfloatfloatfloat32
int32使⽤变⻓编码,对于负值的效率很低,如果你的域 有可能有负值,请使⽤sint64替代int32intint32
uint32使⽤变⻓编码uint32intuint32
uint64使⽤变⻓编码uint64longuint64
sint32使⽤变⻓编码,这些编码在负值时⽐int32⾼效的多int32intint32
sint64使⽤变⻓编码,有符号的整型值。编码时⽐通常的 int64⾼效。int64longint64
fixed32总是4个字节,如果数值总是⽐总是⽐ 2 28 2^{28} 228⼤的 话,这个类型会⽐uint32⾼效。uint32intuint32
fixed64总是8个字节,如果数值总是⽐总是⽐2^56⼤的 话,这个类型会⽐uint64⾼效。uint64longuint64
sfixed32总是4个字节int32intint32
sfixed64总是8个字节int64longint64
boolboolbooleanbool
string⼀个字符串必须是UTF-8编码或者7-bit ASCII编 码的⽂本。stringStringstring
bytes可能包含任意顺序的字节数据。stringByteString[]byte

变长编码:值小的时候,减少表示字节数。

五、protobuf的编码原理

主要说明varints和zigzag。

只讲解重点原理;把上面的各种变量类型归为6大类,除去官方不再推荐的deprecated还有四大类。

protobuf的高效表现在:
(1)解析高效。
(2)字节数占用少。

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length- delimited (⻓度分割)string, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

总结下来就是:
(1)变长编码类型Varints。
(2)固定32 bits类型。
(3)固定64 bits类型。
(4)有长度标记类型。

5.1、Varints 编码(变⻓的类型才使⽤)

为什么设计变长编码: 普通的int数据类型,无论其值的大小,所占用的存储空间都是相等的。⽐如 不管是0x12345678 还是0x12都占⽤4字节,那能否让0x12在表示的时候只占⽤1个字节呢? 是否可以根据数值的⼤⼩来动态地占⽤存储空间, 使得值⽐较⼩的数字占⽤较少的字节数, 值相对⽐ 较⼤的数字占⽤较多的字节数, 这即是变⻓整型编码的基本思想。

采⽤变⻓整型编码的数字, 其占⽤的字节数不是完全⼀致的, Varints 编码使⽤每个字节的最⾼有效 位作为标志位, ⽽剩余的 7 位以⼆进制补码的形式来存储数字值本身, 当最⾼有效位为 1 时, 代表其 后还跟有字节, 当最⾼有效位为 0 时, 代表已经是该数字的最后的⼀个字节。

在 Protobuf 中, 使⽤的是 Base128 Varints 编码, 在这种⽅式中, 使⽤ 7 bit (即7的2次⽅为128) 来存储数字, 在 Protobuf 中, Base128 Varints 采⽤的是⼩端序(即数字的低位存放在⾼地址)。
举例 来看, 对于数字 1, 假设 int 类型占 4 个字节, 以标准的整型存储, 其⼆进制表示应为:

00000000 00000000 00000000 00000001

可⻅, 只有最后⼀个字节存储了有效数值, 前 3 个字节都是 0, 若采⽤ Varints 编码, 其⼆进制形式为:

00000001

因为其没有后续字节, 因此其最⾼有效位为 0, 其余的 7 位以补码形式存放 1。

再⽐如数字 666, 其以 标准的整型存储, 其⼆进制表示为:

00000000 00000000 00000010 10011010

若采⽤ Varints 编码, 其⼆进制形式为:

10011010 00000101

还原可以得到正确的值:

00000010 10011010

从上⾯的编码解码过程可以看出, 可变⻓整型编码对于不同⼤⼩的数字, 其所占⽤的存储空间是 不同的。

通俗的说:
每个字节用7bit表示数值的信息,用1 bit标记结束(1表示没有结束,0表示结束,也就是最后一个字节的位置)。编码时从低位开始取7bit,放在高位。还原时从高位取,放到低位。

那么,如果一个值很大,比如0xFFFFFFFF,需要多少字节存储呢?
0xFFFFFFFF需要分配32个bit,使⽤base128 Varints 编码需要的字节数: 32/7=4.57, 只要 不整除就要进位, 就是需要5个字节存储。 从这⾥看得出来,小于等于28bit的整数适合使⽤变⻓编码, 如果整数都是32bit>= 变量 >28bit可以考虑使⽤fixed32, sfixed32等固定4字节的类型。

5.2、ZigZag 编码(针对负数的)

Varints 编码的实质在于去掉数字开头的 0, 因此可缩短数字所占的存储字节数, 在上⾯的例⼦ 中, 只举例说明了正数的 Varints 编码, 但如果数字为负数, 则采⽤ Varints 编码会恒定占⽤ 10 个字 节, 原因在于负数的符号位为 1, 对于负数其从符号位开始的⾼位均为 1, 在 Protobuf 的具体实现中, 会将此视为⼀个很⼤的⽆符号数, 以C++语⾔的实现为例, 对于 int32 类型的 pb 字段, 对于如下 定义的 proto:

message Tint32{
 	int32 n1 = 1; 
 }

Request 中包含类型为 int32 类型的字段, 当 a 为负数时, 其序列化之后将恒定占⽤ 10 个字节。
比如 对于 int32 类型的数字 -5, 其序列化之后的⼆进制为:

11111011 11111111 11111111 1111111 11111111 11111111 11111111 00000001

其原因在于 Protobuf 的内部将 int32 类型的负数转换为 uint64 来处理。

// 取 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 编码、 解码的速度。

sint32 = Zigzag 编码 +varints编码合起来。
sint32 序列化: 负数 -> Zigzag 编码 -> varints编码。
sint32 反序列化: varints解码 -> Zigzag 解码 -> 负数 。

重点在于:同样是表示-5,sint32只需要2个字节,int32需要11字节。
对于Zigzag的算法不必太细究。其⽬的是把多个1转成多个0表示。

5.3、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 进⼀步将通信线路上消息类型做了划分, 如下表所示:

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length- delimited (⻓度分割)string, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

对于 int32, int64, uint32 等数据类型在序列化之后都会转为 Varints 编码。
Protobuf 除了存储字段的值之外, 还存储了字段的编号以及字段在通信线路上的格式类型(wire- type),具体的存储⽅式为:
field_num << 3 | wire type
即将字段标号逻辑左移 3 位, 然后与该字段的 wire type 的编号按位或。接收端可以利⽤这些信息,结合 proto ⽂件来解码消息结构体。

上例子中,假设 age 为 5,由于 age 在 proto ⽂件中定义 的是 int32 类型, 因此序列化之后它的 wire type 为 0,其字段编号为 1,因此按照上⾯的计算⽅式, 即 1 << 3 | 0, 所以其类型和字段编号的信息只占 1 个字节, 即 00001000, 后⾯跟上字段值 5 的 Varints 编码, 所以整个结构体序列化之后为:

00001000 00000101

有了字段编号和 wire type,其后所跟的数据的⻓度便是确定的,因此 Protobuf 是⼀种⾮常紧密的数 据组织格式,其不需要特别地加⼊额外的分隔符来分割⼀个消息字段,这可⼤⼤提升通信的效率, 规避 冗余的数据传输。

(1)wire_type=0 的时候。 二进制结构为:Tag-Value。
value的编码也采⽤Varints编码⽅式,故不需要额外的位来表示整个value的⻓度。因为Varint的msb位标 识下⼀个字节是否是有效的就起到了指示⻓度的作⽤。

例如:

// 666 int1Size = 3 ⼗六进制:
08 9a 05
// 0x1 int1Size = 2 ⼗六进制:
08 01

(2)wire_type=1、5 的时候。⼆进制结构也为: Tag-Value。
因为都是取固定32位或者64位,因此也不需要额外的位来表示整个value的⻓度。

例如:

// 0x12 n1Size = 9 ⼗六进制:
09 12 00 00 00 00 00 00 00
// -5 n1Size = 9 ⼗六进制:
09 fb ff ff ff ff ff ff ff

// 0x12 n1Size = 5 ⼗六进制:
0d 12 00 00 00
// -5 n1Size = 5 ⼗六进制:
0d fb ff ff ff

(3)wire_type=2 的时候。⼆进制结构为: Tag-[Length]-Value 。
因为表示的是可变⻓度的值,需要有额外的位来指示⻓度。

例如:

// 1 str1Size = 3 ⼗六进制:
0a 01 31	//(这里1表示长度)
// 1234 str1Size = 6 ⼗六进制:
0a 04 31 32 33 34 //(这⾥4表示⻓度) 
//⽼师 str1Size = 8 ⼗六进制:
0a 06 e8 80 81 e5 b8 88 //(这⾥6表示⻓度)

repeat也是这种模式,此时length代表元素个数。

message TRepeatedfields{
	repeated int32 n1 = 1;
	repeated Tbytes n2 = 2; 
}

message Tbytes{
 	bytes n1 = 1; 
 }

tag_length_value_repeated
(4)filed_num范围:

  1. 1到15,仅使⽤1bytes。每个byte包含两个部分:⼀个是field_number,⼀个是tag,其中field-number就是protobuf中每个值后等号后的数字。可以认为这个field_number是必须的。那么⼀个byte⽤来表达这个值就是 000000000,其中bit 8表示是否有后续字节,如果为0表示没有也就是这是⼀个字节,bit 3~bit 7部分表示 field-number,bit 0 ~ bit 2部分则是wire_type部分,表示数据类型。也就是(field_number << 3) | wire_type。其中wire_type只有3位,表示数据类型。那么能够表示field_number的就是bit 3 ~ bit 7的数 字,能够表达的最⼤范围就是1-15(其中0是⽆效的)。

  2. 16到2047,与上⾯的规则其实类似(类似base128的⽅式)。
    以2bytes为例⼦,那么就有 10000000 00000000,其中bit7和bit15依然是符号位,因为每个byte的第⼀位都⽤来表示下⼀byte是否 和⾃⼰有关,那么对于>1byte的数据,bit15⼀定是1,因为这⾥假设是2byte。那么bit7是0表示结束,刨除这两位,再扣掉3个wire_type位,剩下11位(2*8-2-3),能够表达的数字范围 就是2047( 2 11 2^{11} 211 )。

  3. 当filed_num > 15时,依次类推。

六、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)

protobuf⼯程经验:

  1. proto⽂件命名规则;
  2. proto命名空间;
  3. 引⽤⽂件;
  4. 多个平台使⽤同⼀份proto⽂件。

总结

  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 的编码⻓度短, 传输效率⾼。
  4. 协议设计的边界问题、版本号放在哪里、command id需要与否,要考虑清楚。
  5. 熟悉protocol、json、xml的序列化和反序列化。特别是json。
  6. 熟悉proto文件编写。

在这里插入图片描述

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值