找回密码
 立即注册
查看: 188|回复: 0

Linux基础组件之protobuf的原理和使用

[复制链接]
发表于 2022-11-3 09:09 | 显示全部楼层 |阅读模式
protobuf 概述

这篇主要说明protobuf的原理和使用。
Protocol buffers 是种语中,平台关,可扩展的序列化数据的格式,可于通信协议,数据存储 等。Protocol buffers 在序列化数据具有灵活、效的特点。
相于 XML 来说,Protocol buffers 更加 巧,更加快速,更加简单。旦定义了要处理的数据的数据结构之后,就可以利 Protocol buffers 的 代码成具成相关的代码。甚可以在需重新部署程序的情况下更新数据结构。只需使 Protobuf 对数据结构进次描述,即可利各种不同语或从各种不同数据流中对你的结构化数据轻松 读写。
Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可于通讯协议、数据存储等领域的语 关、平台关、可扩展的序列化结构数据格式。
Protocol buffers在游戏和即时通信用的比较多。使用常见分析:
协议场景举例
xml主要在本地使用UI,游戏信息
sjonhttp 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文件。




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文件生成相应的.http://pb.cc文件和.pb.h。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/*.protoSRC_DIR是.proto所在的路径。DST_DIR是.cc和.h生成的位置。
示例:
将指定proto件成.http://pb.cc和.pb.h 。
protoc -I=./ --cpp_out=./ test.proto 将对应录的所有proto件成.http://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注意要有-lprotobuf来指定库。
相关视频推荐
C++高性能服务器通信协议设计的奥秘,xml、json、protobuf性能对比分析
高并发之protobuf通信协议设计
c++后端绕不开的7个开源项目,每一个源码值得深入研究
学习地址:c/c++ linux服务器开发/后台架构师
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享


protobuf option部分选项

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

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

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


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

主要说明varints和zigzag。
只讲解重点原理;把上面的各种变量类型归为6大类,除去官方不再推荐的deprecated还有四大类。
protobuf的高效表现在:
(1)解析高效。
(2)字节数占用少。


总结下来就是:
(1)变长编码类型Varints。
(2)固定32 bits类型。
(3)固定64 bits类型。
(4)有长度标记类型。
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字节的类型。
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表示。
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 编码。
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 是种常紧密的数 据组织格式,其不需要特别地加额外的分隔符来分割个消息字段,这可提升通信的效率, 规避 冗余的数据传输。wire_type=0 的时候
二进制结构为:Tag-Value。

value的编码也采Varints编码式,故不需要额外的位来表示整个value的度。因为Varint的msb位标 识下个字节是否是有效的就起到了指示度的作。
例如:
// 666 int1Size = 3 六进制:
08 9a 05
// 0x1 int1Size = 2 六进制:
08 01wire_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 ffwire_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;  }


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次方)
当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工程经验


  • proto件命名规则;
  • proto命名空间;
  • 引件;
  • 多个平台使同份proto件。
总结

Protobuf 采 Varints 编码和 Zigzag 编码来编码数据, 其中 Varints 编码的思想是移除数字 位的 0, 变的进制位来描述个数字, 对于数字, 其编码度短, 可提数据传输效率, 但 由于它在每个字节的最位额外采了个标志位来标记其后是否还跟有有效字节, 因此对于 的正数, 它会使普通的定格式占更多的空间, 另外对于负数, 直接采 Varints 编码将恒 定占 10 个字节, Zigzag 编码可将负数映射为符号的正数, 然后采 Varints 编码进数据 压缩, 在各种语的 Protobuf 实现中, 对于 int32 类型的数据, Protobuf 都会转为 uint64 后 使 Varints 编码来处理, 因此当字段可能为负数时, 我们应使 sint32 或 sint64, 这样 Protobuf 会按照 Zigzag 编码将数据变换后再采 Varints 编码进压缩, 从缩短数据的进 制位数
Protobuf 不是完全描述的信息描述格式, 接收端需要有相应的解码器(即 proto 定义)才可解析 数据格式, 序列化后的 Protobuf 数据不携带字段名, 只使字段编号来标识个字段, 因此更改 proto 的字段名不会影响数据解析(但这显然不是种好的为), 字段编号会被编码进进制的 消息结构中, 因此我们应尽可能地使字段编号
protobuf 是种紧密的消息结构, 编码后字段之间没有间隔, 每个字段头由两部分组成: 字段编 号和 wire type, 字段头可确定数据段的度, 因此其字段之前需加间隔, 也需引特定的 数据来标记字段末尾, 因此 Protobuf 的编码度短, 传输效率。
协议设计的边界问题、版本号放在哪里、command id需要与否,要考虑清楚。
熟悉protocol、json、xml的序列化和反序列化。特别是json。
熟悉proto文件编写。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 22:54 , Processed in 0.065336 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表