百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

ProtoBuf应用

bigegpt 2024-11-24 12:00 10 浏览

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);
}

今天这篇文章就分享到这里,欢迎关注,点赞,评论,转发。

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...