百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

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前面有文章讲过了,可以参考前面的文章。

Json与XML实战

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的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: