ProtoBuf应用 protobuf 工具
liebian365 2024-10-24 14:38 19 浏览 0 评论
1.设计协议目标
(1)解析效率
解析效率决定了使用协议的CPU成本。编码长度,即信息编码的长度,决定了使用协议的网络带宽及存储成本。
(2)易实现
需要轻量级协议,而非大而全
(3)可读性
编码后的数据的可读性决定了使用协议的调试及维护成本,不同序列化协议具有不同应用场景。
(4)兼容性
使用协议双方是否能够独立升级协议,增减协议中的字段是非常重要。
(5)跨平台
不同操作系统,不同开发语言,比如Windows用C++,Android用Java,Web用Js,IOS用object-c。
(6)安全
协议安全加密,意味着信息的安全。
2.协议涉及最核心问题
(1)序列化/反序列化
序列化:把对象转换为字节序列的过程称为对象序列化。
反序列化:把字节序列转换为对象的过程称为对象的反序列化。
TLV(TLV是tag, length和value的缩写)编码及其变体,如protobuf。
文本流编码,如XML/JSON。
固定结构编码,协议约定了传输字段类型和字段含义,没有tag,len,只有value,比如TCP/IP。
内存dump,把内存中的数据直接输出,不做任何序列化操作,反序列化,直接还原内存。
(2)数据包的完整性
包与包之间是有边界,一般有一些方案去分界,如以下方法:
以固定大小字节数目来分界,如每个包50个字节,对端每收齐50个字节,就当成一个包来解析;
以特定符号分界,每个包以特定字符来结尾(如\r\n),如果读到这个分界线符,表面上一个包到此为止。
固定包头+包体结构,包头部分是一个固定字节长度的结构,会有一个特定的字段指定包体的大小。接收包,先接收固定字节数的头部,解析出这个包的完整度,按照此长度接收包体。这也是目前应用最多的一种方案。
在序列化的buffer前面增加一个字符流的头部,其中有个字段存储包长度,根据特殊字符(比如根据\n 或者\0)判断头部的完整性。如HTTP和REDIS采用的就是这种方式,收包,先判断已收到的数据中是否包含结束符,收到结束符后解析包头,解出这个包完整长度,按此长度接收包体。
如下协议设计参考:
报文头结构:
协议头字段说明:
(3)协议升级
通过版本号指明协议版本,即通过版本号辨别不同类型协议。支持协议头部可扩展,在设计协议头部时用一个字段来指明头部长度。
(4)协议安全
xxtea 固定key。
AES 固定key。
openssl。
Signal protocol 端到端的通讯加密协议。
(5)数据压缩
参考这篇文章,聊聊字符集编码与数据压缩
deflate
gzip
lzw
主流序列化协议:xml、json、protobuf,其中XML与json前面有文章讲过了,可以参考前面的文章。
XML主要是以文本方式存储,JSON以文本结构进行存储。
protobuf是Google的一种独立和轻量级的数据交换格式,以二进制结构进行存储。
对比如下图:
序列化、反序列化效率对比
测试10万次序列化
从下图可以看出,protobuf的效率,在这些方案中,还是最高。
测试10万次反序列化
常用协议设计,HTTP协议
HTTP不适合后台协议,主要有以下原因:
(1)HTTP协议只是一个框架,没有指定包体的序列化方式,必须配合其它序列化方式才能传递业务逻辑数据。
(2)解析效率低,协议本身复杂。
HTTP适合的场景:
(1)有些公网用户api,协议穿透性好,所以最适合。
(2)效率要求没那么高的场景
3.protobuf的编译安装
开源工具:https://github.com/protocolbuffers/protobuf
1. 解压
tar zxvf protobuf-cpp-3.8.0.tar.gz
2.编译
cd protobuf-3.8.0/
./configure
make
sudo make install
3. 显示版本信息
protoc –version
4. 编写proto文件
5. 将proto文件生成对应的.cc和.h文件
定义一个消息类型
定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。如下格式:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。
分配标识号
在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符用来识别各个字段,一旦开始使用,就不再改变。[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。频繁出现的消息元素保留在[1,15]之内的标识号,一定要为这些频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999](FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的标识号,Protobuf协议实现中对这些进行了预留。如果使用了这些预留标识号,编译时就会报警。
指定字段规则
singular:一个格式良好的消息应该有0个或者1个这种字段(但是不能超过1个)
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
添加更多消息类型
定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
//添加注释
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
保留标识符(Reserved)
当删除或注释所有域,以后的用户重用标记号,当重新更新类型的时候,如果使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。这就要通过指定保留标识符,protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。
注意:不要在同一行reserved声明中同时声明域名字和标识号
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
1
2
3
4
从.proto文件生成了什么?
当protocol buffer编译器来运行.proto文件时,编译器生成所选择语言代码,可以在.proto文件中定义消息类型,包括获取、设置字段值,把消息序列化到一个输出流中,以及从一个输入流中解析消息。
对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
默认值
当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
对于strings,默认是一个空string。
对于bytes,默认是一个空的bytes。
对于bools,默认是false。
对于数值类型,默认是0。
对于枚举,默认是第一个定义的枚举值,必须为0?
对于消息类型(message),域没有被设置,确切的消息是根据语言确定的。
对于可重复域的默认值是空。
枚举
通过向消息定义中添加一个枚举(enum)并且为每个可能的值定义一个常量就可以了。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
Corpus枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:必须有有一个0值,我们可以用这个0值作为默认值。这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
在反序列化的过程中,无法识别的枚举值会被保存在消息中,虽然这种表示方式需要依据所使用语言而定。在那些支持开放枚举类型超出指定范围之外的语言中(例如C++和Go),为识别的值会被表示成所支持的整型。
嵌套类型
可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type的形式使用它,如:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
更新一个消息类型
更新规则
(1)不要更改任何已有的字段的数值标识。
(2)记住元素默认值,这样新代码就可以和旧代码产生数据交互。通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉,如果再传递,新的字段还是不可用。
(3)非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用。
(4)int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样,可能数据精度就会丢失。
(5)sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
(6)string和bytes是兼容的——只要bytes是有效的UTF-8编码。
(7)fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
(8)枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留。
Any
Any类型消息允许你在没有指定proto定义的情况下使用消息作为一个嵌套类型。。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。相当于是有个自动型。为了使用Any类型,你需要导入import google/protobuf/any.proto
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename。
在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()‐>PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
Oneof
消息中有很多可选字段, 并且同时至多一个字段会被设置,可以使用这个属性,使用oneof特性可以节省内存。可以使用case()或者WhichOneof() 方法检查哪个oneof字段被设置, 看使用什么语言。可以增加任意类型的字段, 但是不能使用repeated 关键字。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
设置多次后,只有最后一次设置的字段有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
如果解析器遇到同一个oneof中有多个成员,只有最后一个会被解析成消息。
oneof不支持repeated;
反射API对oneof 字段有效;
使用C++,需确保代码不会导致内存泄漏. 下面的代码会崩溃, 因为sub_message 已经通过set_name()删除了
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message‐>set_... // Crashes here
使用Swap()两个oneof消息,每个消息,两个消息将拥有对方的值,例如在下面的例子中,msg1会拥有sub_message并且msg2会有name。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
兼容性
增加或者删除oneof字段时一定要小心. 如果检查oneof的值返回None/NOT_SET,表示oneof没有被复制或者在一个不同的版本中赋值。没有办法判断如果未识别的字段是一个oneof字段
Map
protocol buffer提供了一种快捷的语法:
map<key_type, value_type> map_field = N?
key_type可以是任意Integer或者string类型(所以,除了floating和bytes的任意标量类型都是可以的)value_type可以是任意类型。
创建一个project的映射,每个Projecct使用一个string作为key,可以像下面这样定义:
map<string, Project> projects = 3?
Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以不要期望以固定顺序处理Map。
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key,生成map的API现在对于所有proto3支持的语言都可用。
即使是不支持map语法的protocol buffer实现也是可以处理你的数据。
message MapFieldEntry {
key_type key = 1?
value_type value = 2?
}
repeated MapFieldEntry map_field = N?
包
为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:
package foo.bar;
message Open { ... }
其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo {
。。。
required foo.bar.Open open = 1;
...
}
包的声明符会根据使用语言的不同影响生成的代码。
对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package;
对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。
服务
将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
今天这篇文章就分享到这里,欢迎关注,点赞,评论,转发。
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)