Protocol Buffer技术规范

2024-02-04 15:18

本文主要是介绍Protocol Buffer技术规范,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

该系列Blog的内容主体主要源自于Protocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo。这样做的

目的主要在于不仅可以保持Google文档的良好风格和系统性,同时再结合一些比较实用和通用的用例,这样就更加便于公司内部的培训,以

及和广大网友的技术交流。需要说明的是,Blog的内容并非line by line的翻译,其中包含一些经验性总结,与此同时,对于一些不是非常常

用的功能并未予以说明,有兴趣的开发者可以直接查阅Google的官方文档。

      一、为什么使用Protocol Buffer?

      在回答这个问题之前,我们还是先给出一个在实际开发中经常会遇到的系统场景。比如:我们的客户端程序是使用Java开发的,可能运

行自不同的平台,如:Linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序

之间进行数据通讯时存在多种方式用于设计消息格式,如:

      1. 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了,

仅需将接收到的数据按照结构体类型强行转换即可。事实上对于变长结构体也不会非常麻烦。在发送数据时,也只需定义一个结构体变量并

设置各个成员变量的值之后,再以char*的方式将该二进制数据发送到远端。反之,该方式对于Java开发者而言就会非常繁琐,首先需要将

接收到的数据存于ByteBuffer之中,再根据约定的字节序逐个读取每个字段,并将读取后的值再赋值给另外一个值对象中的域变量,以便于

程序中其他代码逻辑的编写。对于该类型程序而言,联调的基准是必须客户端和服务器双方均完成了消息报文构建程序的编写后才能展开,

而该设计方式将会直接导致Java程序开发的进度过慢。即便是Debug阶段,也会经常遇到Java程序中出现各种域字段拼接的小错误。

      2. 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的XML描述信

息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整

体运行性能明显下降。

      对于以上两种方式所产生的问题,Protocol Buffer均可以很好的解决,不仅如此,Protocol Buffer还有一个非常重要的优点就是可以

保证同一消息报文新旧版本之间的兼容性。至于具体的方式我们将会在后续的博客中给出。

      二、定义第一个Protocol Buffer消息。

      创建扩展名为.proto的文件,如:MyMessage.proto,并将以下内容存入该文件中。

      message LogonReqMessage {
          required int64 acctID = 1;
          required string passwd = 2;
      }

      这里将给出以上消息定义的关键性说明。
      

1. message是消息定义的关键字,等同于C++中的struct/class,或是Java中的class。

   2. LogonReqMessage为消息的名字,等同于结构体名或类名。

        3. required前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在

另外两个类似的关键字,optional和repeated,带有这两种限定符的消息字段则没有required字段这样的限制。相比于optional,repeated

主要用于表示数组字段。具体的使用方式在后面的用例中均会一一列出。

       4. int64和string分别表示长整型和字符串型的消息字段,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类

型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给

出。

      5. acctID和passwd分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。

      6. 标签数字12则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,passwd字段编码后的数据一定位于acctID之

后。需要注意的是该值在同一message中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既

标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes,而Protocol Buffer可以支持的字段数量则为2的29次方减

一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节

数量。

      三、定义第二个(含有枚举字段)Protocol Buffer消息。

      //在定义Protocol Buffer的消息时,可以使用和C++/Java代码同样的方式添加注释。

      enum UserStatus {
          OFFLINE = 0;  //表示处于离线状态的用户
          ONLINE = 1;   //表示处于在线状态的用户
      }
      message UserInfo {
          required int64 acctID = 1;
          required string name = 2;
          required UserStatus status = 3;
      }

      这里将给出以上消息定义的关键性说明(仅包括上一小节中没有描述的)。

      1. enum是枚举类型定义的关键字,等同于C++/Java中的enum。

      2. UserStatus为枚举的名字。

      3. 和C++/Java中的枚举不同的是,枚举值之间的分隔符是分号,而不是逗号。

      4. OFFLINE/ONLINE为枚举值。

      5. 0和1表示枚举值所对应的实际整型值,和C/C++一样,可以为枚举值指定任意整型值,而无需总是从0开始定义。如:

      enum OperationCode {

          LOGON_REQ_CODE = 101;

          LOGOUT_REQ_CODE = 102;

          RETRIEVE_BUDDIES_REQ_CODE = 103;
    
          LOGON_RESP_CODE = 1001;

          LOGOUT_RESP_CODE = 1002;

          RETRIEVE_BUDDIES_RESP_CODE = 1003;
      }

      四、定义第三个(含有嵌套消息字段)Protocol Buffer消息。

      我们可以在同一个.proto文件中定义多个message,这样便可以很容易的实现嵌套消息的定义。如:

      enum UserStatus {
          OFFLINE = 0;
          ONLINE = 1;
      }

      message UserInfo {
          required int64 acctID = 1;
          required string name = 2;
          required UserStatus status = 3;
      }
      message LogonRespMessage {
          required LoginResult logonResult = 1;
          required UserInfo userInfo = 2;
      }

      这里将给出以上消息定义的关键性说明(仅包括上两小节中没有描述的)。

      1. LogonRespMessage消息的定义中包含另外一个消息类型作为其字段,如UserInfo userInfo。

      2. 上例中的UserInfo和LogonRespMessage被定义在同一个.proto文件中,那么我们是否可以包含在其他.proto文件中定义的

message呢?Protocol Buffer提供了另外一个关键字import,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他

消息定义文件可以通过import的方式将该文件中定义的消息包含进来,如:

      import "myproject/CommonMessages.proto"

     五、限定符(required/optional/repeated)的基本规则。

      1. 在每个消息中必须至少留有一个required类型的字段。 

      2. 每个消息中可以包含0个或多个optional类型的字段。

      3. repeated表示的字段可以包含0个或多个数据。需要说明的是,这一点有别于C++/Java中的数组,因为后两者中的数组必须包含至少一个元素。

      4. 如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional

或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。


      六、类型对照表。

.proto Type Notes C++ Type Java Type
double   double  double
float   float  float
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.  int32  int
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.  int64  long
uint32Uses variable-length encoding.  uint32  int
uint64Uses variable-length encoding.  uint64  long
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.  int32  int
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.   int64  long
fixed32Always four bytes. More efficient than uint32 if values are often greater than 228  uint32  int
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 256.  uint64  long
sfixed32Always four bytes.  int32  int
sfixed64Always eight bytes.  int64  long
bool   bool  boolean
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.  string  String
bytesMay contain any arbitrary sequence of bytes. string ByteString


      七、Protocol Buffer消息升级原则。
 

     在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用

程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规

则如下:

      1. 不要修改已经存在字段的标签号。

      2. 任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。

      3. 在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须

被保留,不能被新的字段重用。

      4. int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和

sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类

型兼容的类型,否则就将打破新老消息格式的兼容性。

      5. optional和repeated限定符也是相互兼容的。

      八、Packages。

      我们可以在.proto文件中定义包名,如:

      package ourproject.lyphone;

      该包名在生成对应的C++文件时,将被替换为名字空间名称,既namespace ourproject { namespace lyphone。而在生成的Java

代码文件中将成为包名。

      九、Options。

      Protocol Buffer允许我们在.proto文件中定义一些常用的选项,这样可以指示Protocol Buffer编译器帮助我们生成更为匹配的目标语

言代码。Protocol Buffer内置的选项被分为以下三个级别:

      1. 文件级别,这样的选项将影响当前文件中定义的所有消息和枚举。

      2. 消息级别,这样的选项仅影响某个消息及其包含的所有字段。

      3. 字段级别,这样的选项仅仅响应与其相关的字段。

      下面将给出一些常用的Protocol Buffer选项。

      1. option java_package = "com.companyname.projectname";

      java_package是文件级别的选项,通过指定该选项可以让生成Java代码的包名为该选项值,如上例中的Java代码包名为

com.companyname.projectname。与此同时,生成的Java文件也将会自动存放到指定输出目录下的

com/companyname/projectname子目录中。如果没有指定该选项,Java的包名则为package关键字指定的名称。该选项对于生成

C++代码毫无影响。

      2. option java_outer_classname = "LYPhoneMessage";

      java_outer_classname是文件级别的选项,主要功能是显示的指定生成Java代码的外部类名称。如果没有指定该选项,Java代码

的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式,如:my_project.proto,那么该文件的默认外部类名称将为

MyProject。该选项对于生成C++代码毫无影响。

      注:主要是因为Java中要求同一个.java文件中只能包含一个Java外部类或外部接口,而C++则不存在此限制。因此在.proto文件中定

义的消息均为指定外部类的内部类,这样才能将这些消息生成到同一个Java文件中。在实际的使用中,为了避免总是输入该外部类限定符,

可以将该外部类静态引入到当前Java文件中,如:import static com.company.project.LYPhoneMessage.*

      3. option optimize_for = LITE_RUNTIME;

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

SPEED。

      SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。

      CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如

Mobile。

      LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功

能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-

java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。

      注:对于LITE_MESSAGE选项而言,其生成的代码均将继承自MessageLite,而非Message。    

      4. [pack = true]: 因为历史原因,对于数值型的repeated字段,如int32、int64等,在编码时并没有得到很好的优化,然而在新近版

本的Protocol Buffer中,可通过添加[pack=true]的字段选项,以通知Protocol Buffer在为该类型的消息对象编码时更加高效。如:

      repeated int32 samples = 4 [packed=true]。

      注:该选项仅适用于2.3.0以上的Protocol Buffer。

      5. [default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么

在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支

持自定义的缺省值,如:

      optional int32 result_per_page = 3 [default = 10]。


     十、命令行编译工具。

      protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR 

path/to/file.proto

      这里将给出上述命令的参数解释。

      1. protoc为Protocol Buffer提供的命令行编译工具。

      2. --proto_path等同于-I选项,主要用于指定待编译的.proto消息定义文件所在的目录,该选项可以被同时指定多个。

      3. --cpp_out选项表示生成C++代码,--java_out表示生成Java代码,--python_out则表示生成Python代码,其后的目录为生成后

的代码所存放的目录。

      4. path/to/file.proto表示待编译的消息定义文件。

      注:对于C++而言,通过Protocol Buffer编译工具,可以将每个.proto文件生成出一对.h和.cc的C++代码文件。生成后的文件可以直

接加载到应用程序所在的工程项目中。如:MyMessage.proto生成的文件为MyMessage.pb.h和MyMessage.pb.cc。

这篇关于Protocol Buffer技术规范的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/677929

相关文章

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Java 文件读写最好是用buffer对于大文件可以加快速度

参考例子: FileReader fileReader = new FileReader(filename);BufferedReader bufferedReader = new BufferedReader(fileReader);List<String> lines = new ArrayList<String>();String line = null;while ((line =

【Node】Buffer 与 Stream

node 为什么会出现 Buffer 这个模块 在最初的时候,JavaScript 只运行在浏览器端, 对于处理 Unicode 编码的字符串很容易,但是对于处理二进制以及非 Unicode 编码的数据便无能为力。 不过对于 Server 端操作来说 网络I/O 以及 文件I/O 的处理是必须的,所以 Node 中便提供了 Buffer 类处理二进制的数据。 二进制缓冲区 Buffer

【0324】Postgres内核 Shared Buffer Access Rules (共享缓冲区访问规则)说明

0. 章节内容 1. 共享磁盘缓冲区访问机制 (shared disk buffers) 共享磁盘缓冲区有两套独立的访问控制机制:引用计数(a/k/a pin 计数)和缓冲区内容锁。(实际上,还有第三级访问控制:在访问任何属于某个关系表的页面之前,必须持有该关系表的适当类型的锁。这里不讨论关系级锁。) Pins 在对缓冲区做任何操作之前,必须“对缓冲区pin”(即增加其引用计数, re

Netty源码解析1-Buffer

大数据成神之路系列: 请戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData 上一篇文章我们概要介绍了Netty的原理及结构,下面几篇文章我们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer机制,这部分也相对独立,我们就先从buffer讲起。 What:buffer简介 buffer中文

WebDriver与Chrome DevTools Protocol:如何在浏览器自动化中提升效率

介绍 随着互联网数据的爆炸式增长,爬虫技术成为了获取信息的重要工具。在实际应用中,如何提升浏览器自动化的效率是开发者常常面临的挑战。Chrome DevTools Protocol(CDP)与Selenium WebDriver相结合,为浏览器自动化提供了强大的控制能力,并允许用户直接与浏览器的底层交互。本文将通过使用CDP优化Selenium的效率,结合代理IP技术,实现对微博数据的高效采

Pencils Protocol生态新进展,即将上线 Vault 产品

“极高的盈利预期、通证的持续回购与销毁,Vault产品的推出正在成为Pencils Protocol生态发展的重磅利好。” Pencils Protocol是目前Scroll生态TVL最高的DeFi平台 ,即便是行情整体较为平淡,其仍旧能够保持在3亿美元左右的锁仓价值,并拥有超过247,000名活跃用户。 而进入到9月,Pencils Protocol陆续迎来了

【LINUX】“dmesg: read kernel buffer failed: Operation not permitted“ 错误

出现 “dmesg: read kernel buffer failed: Operation not permitted” 错误通常是因为当前用户没有权限读取内核日志缓冲区 这可以通过修改内核参数 kernel.dmesg_restrict 来解决。 你可以尝试以下命令来允许非特权用户读取内核日志: sudo sysctl -w kernel.dmesg_restrict=0 这个命令

Kafka【五】Buffer Cache (缓冲区缓存)、Page Cache (页缓存)和零拷贝技术

【1】Buffer Cache (缓冲区缓存) 在Linux操作系统中,Buffer Cache(缓冲区缓存)是内核用来优化对块设备(如磁盘)读写操作的一种机制(故而有一种说法叫做块缓存)。尽管在较新的Linux内核版本中,Buffer Cache和Page Cache已经被整合在一起,但在理解历史背景和功能时,了解Buffer Cache仍然很有帮助。 Buffer Cache 的历史和定义

ios开发之Protocol

一.特点: 1.Protocol有点类似于java中的接口(interface),只有申明没有实现 2.任何类都可以实现协议 3.实现了协议的类,不一定需要实现协议中定义的所有方法,只有在协议中定义的方法有@required修饰的时候,才表示这个方法必须被实现.采用@optional修饰的方法表示,可以实现也可以不实现. 二.利用Xcode创建协议 1.new file-->iOS