2024年Go最新高效数据编码-protobuf_protocol buffer在线编码(1),2024年最新音视频学习指南来咯

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


可以随意嵌套消息



message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}



### 2.5 更新消息


如果后面发现之前定义 message 需要增加字段了,这个时候就体现出 Protocol Buffer 的优势了,不需要改动之前的代码。不过需要满足以下 10 条规则:  



1. 不要改动原有字段的数据结构。
2. 如果您添加新字段,则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。
3. 只要字段号在更新的消息类型中不再使用,字段可以被删除。您可能需要重命名该字段,可能会添加前缀“OBSOLETE\_”,或者标记成保留字段号 reserved,以便将来的 .proto 用户不会意外重复使用该号码。
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)


### 2.6 未知字段


未知数字段是 protocol buffers 序列化的数据,表示解析器无法识别的字段。例如,当一个旧的二进制文件解析由新的二进制文件发送的新数据的数据时,这些新的字段将成为旧的二进制文件中的未知字段。  
Proto3 实现可以成功解析未知字段的消息,但是,实现可能会或可能不会支持保留这些未知字段。你不应该依赖保存或删除未知域。对于大多数 Google protocol buffers 实现,未知字段在 proto3 中无法通过相应的 proto 运行时访问,并且在反序列化时被丢弃和遗忘。这是与 proto2 的不同行为,其中未知字段总是与消息一起保存并序列化。  
 


### 2.7 Any


该Any消息类型,可以使用邮件作为嵌入式类型,而不必自己.proto定义。AnAny包含一个任意序列化的消息 as bytes,以及一个 URL,该 URL 充当全局唯一标识符并解析为该消息的类型。要使用该Any类型,您需要导入 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*。  
​


不同的语言实现将支持运行时库助手以类型安全的方式打包和解包 Any 值——例如,在 Java 中,Any 类型将具有特殊的pack()andunpack()访问器,而在 C++ 中有PackFrom()andUnpackTo()方法:



// 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 network_error;
detail.UnpackTo(&network_error);
… processing network_error …
}
}


  
 **目前,用于处理 Any 类型的运行时库正在开发中**。  
 


### Once


如果您的消息包含多个字段并且最多同时设置一个字段,则可以强制执行此行为并使用 oneof 功能节省内存。  
oneof字段和普通字段一样,除了oneof共享内存中的所有字段外,最多可以同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。您可以使用特殊case()或WhichOneof()方法检查 oneof 中设置的值(如果有),具体取决于您选择的语言。  
要在您的 oneof 中定义 oneof,您.proto可以使用oneof后跟 oneof 名称的关键字,在这种情况下test\_oneof:



message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}


* 设置 oneof 字段将自动清除 oneof 的所有其他成员。因此,如果您设置了多个 oneof 字段,则只有您设置的\_最后一个\_字段仍然具有值。



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 字段。
* 如果将 oneof 字段设置为默认值(例如将 int32 oneof 字段设置为 0),则将设置该 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


* 同样在 C++ 中,如果你的Swap()两条消息带有 oneofs,则每条消息都会以另一个的 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());


* 向后兼容性: 添加或删除Once字段时要小心。如果检查 oneof 的值返回None/ NOT\_SET,则可能意味着尚未设置 oneof 或已将其设置为不同版本的 oneof 中的字段。无法区分,因为无法知道未知字段是否是 oneof 的成员。


### 2.8 Map


如果你想创建一个关联映射作为数据定义的一部分,protocol buffers 提供了一个方便的快捷语法:



map<key_type, value_type> map_field = N;


* key类型可以是任何整数或字符串类型。
* enum不能作为key。
* map字段不能是repeated
* 线性数组和map迭代顺序是不确定的,所以你不能依靠你的map是在一个 特定顺序。
* 为proto生成文本格式时,map按key排序,数字的key按数字排序
* 从数组中解析或合并时,如果有重复的key,则使用所看到的最后一个key。从文本格式解析映射时,如果有重复的key,解析可能会失败。


可以自己实现maps



message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;



### 2.9 Json映射


Proto3 支持 JSON 中的规范编码,使系统之间共享数据变得更加容易。编码在下表中按类型逐个描述。  
如果 JSON 编码数据中缺少值或其值为空,则在解析为 protocol buffer 时,它将被解释为适当的默认值。如果一个字段在协议缓冲区中具有默认值,默认情况下它将在 JSON 编码数据中省略以节省空间。具体 Mapping 的实现可以提供选项决定是否在 JSON 编码的输出中发送具有默认值的字段。




| proto | Json | Json示例 | 说明 |
| --- | --- | --- | --- |
| message | object | {“fooBar”, v, “g”: null} | 生成 JSON 对象。消息字段名称映射到小写字母并成为 JSON 对象键。如果指定了字段选项,则指定的值将用作键。解析器接受lowerCamelCase 名称(或选项指定的名称)和原始原型字段名称。是所有字段类型的可接受值,并被视为相应字段类型的默认值。​ |
| enum | string | **"FOO\_BAR"** | 使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。 |
| map | object | {“k”: v,…} | 键值转化成json对象 |
| repeated V | array | {v, …} | Json值将是json数组 |
| string | string | “Hello” |  |
| bytes | base64string | “YWJHASDFAFGQWRWR+” | JSON 值将是使用带填充的标准 base64 编码编码为字符串的数据。接受带/不带填充的标准或 URL 安全 base64 编码。 |
| int32fixed32uint32 | number | 1,-10,0 | JSON 值将是一个十进制数。接受数字或字符串。 |
| int64fixed64uint64 | string | “1”, “-10” | JSON 值将是一个十进制字符串。接受数字或字符串。 |
| float,double | number | 1.1, -10.0, “NaN”,“Infinity” | JSON 值将是数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”之一。接受数字或字符串。指数符号也被接受。-0 被认为等价于 0。 |
| Any | object | {"@type": “url”, “f”:v} | 如果 Any 包含一个具有特殊 JSON 映射的值,它将按如下方式转换:. 否则,该值将转换为 JSON 对象,并插入该字段以指示实际数据类型。{"@type": xxx, “value”: yyy}"@type" |
| Timestamp | string | “1972-0101T10:00:20.021Z” | 使用 RFC 3339,其中生成的输出将始终是 Z 规范化的,并使用 0、3、6 或 9 个小数位。也接受除“Z”以外的偏移量。 |
| Duration | string | “1.000340012s”, “1s” | 生成的输出始终包含 0、3、6 或 9 个小数位,具体取决于所需的精度,后跟后缀“s”。接受任何小数位数(也没有),只要它们适合纳秒精度并且需要后缀“s”。 |
| Struct | object |  | json对象 |
| Wrapper types | various type | 2,“2”,“foo”,true | 包装器在 JSON 中使用与包装基元类型相同的表示,除了null在数据转换和传输期间允许和保留。 |
| FileMask | string | “f.fooBar,h” |  |
| ListValue | array | [foo, bar,…] |  |
| NullValue | null |  |  |
| Empty | object | {} |  |



### 2.10 选项


选项不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表在 中定义`google/protobuf/descriptor.proto`。  
有些选项是文件级选项,这意味着它们应该写在顶级范围内,而不是在任何消息、枚举或服务定义中。一些选项是消息级别的选项,这意味着它们应该写在消息定义中。有些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、字段之一、服务类型和服务方法上;


* java\_package(文件选项):要用于生成的 Java/Kotlin 类的包。如果文件中未java\_package给出显式选项.proto,则默认情况下将使用 proto 包(使用文件中的“package”关键字指定.proto)。然而,proto 包通常不会成为好的 Java 包,因为 proto 包不会以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。



option java_package = “com.example.foo”;


* java\_outer\_classname(文件选项):要生成的包装 Java 类的类名(以及文件名)。如果文件中没有明确java\_outer\_classname指定.proto,类名将通过将.proto文件名转换为驼峰式大小写来构造(因此foo\_bar.proto变为FooBar.java)。如果该java\_multiple\_files选项被禁用,则所有其他类/枚举/等。为.proto文件生成的将\_在此外部\_包装器 Java 类中生成为嵌套类/枚举/等。如果不生成 Java 代码,则此选项无效。



option java_outer_classname = “Ponycopter”;


* java\_multiple\_files(文件选项):如果为 false,则只.java为该.proto文件生成一个文件,所有 Java 类/枚举/等。为顶级消息、服务和枚举生成的消息将嵌套在外部类中(请参阅 参考资料java\_outer\_classname)。如果为 true,.java将为每个 Java 类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto文件生成的包装器 Java 类将不包含任何嵌套类/枚举/等。这是一个布尔选项,默认为false。如果不生成 Java 代码,则此选项无效。



option java_multiple_files = true;


* optimize\_for(文件选项):可以设置为SPEED、CODE\_SIZE、 或LITE\_RUNTIME。这会通过以下方式影响 C++ 和 Java 代码生成器(以及可能的第三方生成器):
	+ SPEED(默认):协议缓冲区编译器将生成用于序列化、解析和对您的消息类型执行其他常见操作的代码。这段代码是高度优化的。
	+ CODE\_SIZE:protocol buffer 编译器将生成最少的类,并将依赖共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此生成的代码将比 with 小得多SPEED,但操作会更慢。类仍将实现与它们在SPEED模式中完全相同的公共 API 。此模式在包含大量.proto文件且不需要所有文件都非常快的应用程序中最有用。
	+ LITE\_RUNTIME:protocol buffer 编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite而不是libprotobuf)的类。lite 运行时比完整库小得多(大约小一个数量级),但省略了某些功能,如描述符和反射。这对于在手机等受限平台上运行的应用程序特别有用。编译器仍然会像在SPEEDmode 中那样生成所有方法的快速实现。生成的类只会实现MessageLite每种语言的接口,它只提供完整Message接口的方法的一个子集。



option optimize_for = CODE_SIZE;


* cc\_enable\_arenas(文件选项):为 C++ 生成的代码启用[arena 分配](https://bbs.csdn.net/topics/618658159)。
* objc\_class\_prefix(文件选项):设置 Objective-C 类前缀,该前缀添加到所有 Objective-C 生成的类和此 .proto 中的枚举。没有默认值。您应该使用[Apple 推荐的](https://bbs.csdn.net/topics/618658159)3-5 个大写字符之间的前缀。请注意,所有 2 个字母前缀都由 Apple 保留。
* deprecated(字段选项):如果设置为true,则表示该字段已弃用,不应由新代码使用。在大多数语言中,这没有实际效果。在 Java 中,这变成了一个@Deprecated注解。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段未被任何人使用并且您希望阻止新用户使用它,请考虑使用[保留](https://bbs.csdn.net/topics/618658159)语句替换该字段声明。



int32 old_field = 6 [deprecated = true];


* go\_package: 可以指明生成的文件应该放在那个目录之下,可以指定包名。  
如何使用?



会在当前目录下创建目录hello/proto/v1,然后在v1下生成pb的go源文件,并且包名为v1

option go_package=“hello/proto/v1”;

会在当前目录的上级目录创建hello/proto/v1,然后在v1下生成pb的go源文件,并且包名为v1

option go_package=“…/hello/proto/v1”;



### 2.11 命令说明



protoc --proto_path=IMPORT_PATH
–cpp_out=DST_DIR
–java_out=DST_DIR
–python_out=DST_DIR \
–go_out=DST_DIR
–ruby_out=DST_DIR \
–objc_out=DST_DIR \
–csharp_out=DST_DIR
path/to/file.proto


* IMPORT\_PATH指定.proto解析import指令时查找文件的目录,如果省略则使用当前目录。通过–proto\_path多次传递该选项可以指定多个导入目录,并按顺序搜索他们 -I=IMPORT\_PATH可以当做–proto\_path
* 输出指定的代码
	+ –cpp\_out生成C++代码的输出目录
	+ –java\_out生成java代码的输出目录
	+ –kotlin\_out生成kotlin代码的输出目录
	+ –python\_out生成python代码的输出目录
	+ –go\_out生成go代码的输出目录
	+ –ruby\_out生成ruby的输出目录
	+ –objc\_out生成objectc代码的输出目录
	+ –csharp\_out生成的C#代码的输出目录
	+ –php\_out生成的php代码的输出目录
* 您必须提供一个或多个.proto文件作为输入。.proto可以一次指定多个文件。尽管文件是相对于当前目录命名的,但每个文件都必须驻留在IMPORT\_PATHs之一中,以便编译器可以确定其规范名称。


#### 2.11.1 生成go的pb源文件



protoc -I . helloworld.proto --go_out=plugins=grpc:.



## 三、proto3定义Services


如果要使用 RPC(远程过程调用)系统的消息类型,可以在 `.proto` 文件中定义 RPC 服务接口,protocol buffer 编译器将使用所选语言生成服务接口代码和 stubs。所以,例如,如果你定义一个 RPC 服务,入参是 SearchRequest 返回值是 SearchResponse,你可以在你的 `.proto` 文件中定义它,如下所示:



service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}


与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC:在谷歌开发的语言和平台中立的开源 RPC 系统。gRPC 在 protocol buffer 中工作得非常好,并且允许你通过使用特殊的 protocol buffer 编译插件,直接从 `.proto` 文件中生成 RPC 相关的代码。  
如果你不想使用 gRPC,也可以在你自己的 RPC 实现中使用 protocol buffers。您可以在 Proto2 语言指南中找到更多关于这些相关的信息。  
还有一些正在进行的第三方项目为 Protocol Buffers 开发 RPC 实现。  
 


## 四、规范



### 4.1 标准文件格式


* 保持行长度为 80 个字符。
* 使用 2 个空格的缩进。
* 最好对字符串使用双引号。


### 4.2 文件结构


文件应该命名 lower\_snake\_case.proto  
所有文件应按以下方式排序:


1. 许可证
2. 文件简介
3. Syntax
4. Package
5. Import
6. File options
7. Everythins else


​


message 采用驼峰命名法。message 首字母大写开头。字段名采用下划线分隔法命名。



message SongServerRequest {
required string song_name = 1;
}


枚举类型采用驼峰命名法。枚举类型首字母大写开头。每个枚举值全部大写,并且采用下划线分隔法命名。



enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}


**每个枚举值用分号结束,不是逗号**。  
服务名和方法名都采用驼峰命名法。并且首字母都大写开头。



service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}



## 五、编码原理



### 5.1 Base 128 Varints 编码


Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。  
Varint 中的每个字节(最后一个字节除外)都设置了最高有效位(msb),这一位表示还会有更多字节出现。每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示,最低有效组首位。  
如果用不到 1 个字节,那么最高有效位设为 0 ,如下面这个例子,1 用一个字节就可以表示,所以 msb 为 0.



0000 0001


如果需要多个字节表示,msb 就应该设置为 1 。例如 300,如果用 Varint 表示的话:



1010 1100 0000 0010


如果按照正常的二进制计算的话,这个表示的是 88068(65536 + 16384 + 4096 + 2048 + 4)。  
那 Varint 是怎么编码的呢?  
下面代码是 Varint int 32 的编码计算方法。



char* EncodeVarint32(char* dst, uint32_t v) {
// Operate on characters as unsigneds
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
static const int B = 128;
if (v < (1<<7)) {
*(ptr++) = v;
} else if (v < (1<<14)) {
*(ptr++) = v | B;
*(ptr++) = v>>7;
} else if (v < (1<<21)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = v>>14;
} else if (v < (1<<28)) {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = v>>21;
} else {
*(ptr++) = v | B;
*(ptr++) = (v>>7) | B;
*(ptr++) = (v>>14) | B;
*(ptr++) = (v>>21) | B;
*(ptr++) = v>>28;
}
return reinterpret_cast<char*>(ptr);
}



300 = 100101100


由于 300 超过了 7 位(Varint 一个字节只有 7 位能用来表示数字,最高位 msb 用来表示后面是否有更多字节),所以 300 需要用 2 个字节来表示。  
Varint 的编码,以 300 举例:



if (v < (1<<14)) {
*(ptr++) = v | B;
*(ptr++) = v>>7;
}

  1. 100101100 | 10000000 = 1 1010 1100
  2. 110101100 取出末尾 7 位 = 010 1100
  3. 100101100 >> 7 = 10 = 0000 0010
  4. 1010 1100 0000 0010 (最终 Varint 结果)

Varint 的解码算法应该是这样的:(实际就是编码的逆过程)


1. 如果是多个字节,先去掉每个字节的 msb(通过逻辑或运算),每个字节只留下 7 位。
2. 逆序整个结果,最多是 5 个字节,排序是 1-2-3-4-5,逆序之后就是 5-4-3-2-1,字节内部的二进制位的顺序不变,变的是字节的相对位置。


解码过程调用 GetVarint32Ptr 函数,如果是大于一个字节的情况,会调用 GetVarint32PtrFallback 来处理。



inline const char* GetVarint32Ptr(const char* p,
const char* limit,
uint32_t* value) {
if (p < limit) {
uint32_t result = *(reinterpret_cast<const unsigned char*>§);
if ((result & 128) == 0) {
*value = result;
return p + 1;
}
}
return GetVarint32PtrFallback(p, limit, value);
}
const char* GetVarint32PtrFallback(const char* p,
const char* limit,
uint32_t* value) {
uint32_t result = 0;
for (uint32_t shift = 0; shift <= 28 && p < limit; shift += 7) {
uint32_t byte = *(reinterpret_cast<const unsigned char*>§);
p++;
if (byte & 128) {
// More bytes are present
result |= ((byte & 127) << shift);
} else {
result |= (byte << shift);
*value = result;
return reinterpret_cast<const char*>§;
}
}
return NULL;
}


至此,Varint 处理过程读者应该都熟悉了。上面列举出了 Varint 32 的算法,64 位的同理,只不过不再用 10 个分支来写代码了,太丑了。(32位 是 5 个 字节,64位 是 10 个字节)  
64 位 Varint 编码实现:



char* EncodeVarint64(char* dst, uint64_t v) {
static const int B = 128;
unsigned char* ptr = reinterpret_cast<unsigned char*>(dst);
while (v >= B) {
*(ptr++) = (v & (B-1)) | B;
v >>= 7;
}
*(ptr++) = static_cast(v);
return reinterpret_cast<char*>(ptr);
}


原理不变,只不过用循环来解决了。  
64 位 Varint 解码实现:



const char* GetVarint64Ptr(const char* p, const char* limit, uint64_t* value) {
uint64_t result = 0;
for (uint32_t shift = 0; shift <= 63 && p < limit; shift += 7) {
uint64_t byte = *(reinterpret_cast<const unsigned char*>§);
p++;
if (byte & 128) {
// More bytes are present
result |= ((byte & 127) << shift);
} else {
result |= (byte << shift);
*value = result;
return reinterpret_cast<const char*>§;
}
}
return NULL;
}


读到这里可能有读者会问了,Varint 不是为了紧凑 int 的么?那 300 本来可以用 2 个字节表示,现在还是 2 个字节了,哪里紧凑了,花费的空间没有变啊?!  
Varint 确实是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。  
300 如果用 int32 表示,需要 4 个字节,现在用 Varint 表示,只需要 2 个字节了。缩小了一半!  
​  
  
 


### 5.2 message struct编码


protocol buffer 中 message 是一系列键值对。message 的二进制版本只是使用字段号(field’s number 和 wire\_type)作为 key。每个字段的名称和声明类型只能在解码端通过引用消息类型的定义(即 `.proto` 文件)来确定。这一点也是人们常常说的 protocol buffer 比 JSON,XML 安全一点的原因,如果没有数据结构描述 `.proto` 文件,拿到数据以后是无法解释成正常的数据的。  
![image.png](https://img-blog.csdnimg.cn/img\_convert/991847e07d2b68d79f48caac41480b28.png#clientId=u20f7bf7f-74ec-4&from=paste&height=443&id=ub35781c1&margin=[object Object]&name=image.png&originHeight=443&originWidth=934&originalType=binary&ratio=1&size=111307&status=done&style=none&taskId=u242aa481-1c96-44f0-8c85-c19b152d7bc&width=934)  
由于采用了 tag-value 的形式,所以 option 的 field 如果有,就存在在这个 message buffer 中,如果没有,就不会在这里,这一点也算是压缩了 message 的大小了。  
当消息编码时,键和值被连接成一个字节流。当消息被解码时,解析器需要能够跳过它无法识别的字段。这样,可以将新字段添加到消息中,而不会破坏不知道它们的旧程序。这就是所谓的 “向后”兼容性。  
为此,线性的格式消息中每对的“key”实际上是两个值,其中一个是来自`.proto`文件的字段编号,加上提供正好足够的信息来查找下一个值的长度。在大多数语言实现中,这个 key 被称为 tag。




| Type | Meaning | 使用 |
| --- | --- | --- |
| 0 | Varint | int32,int64,uint32,uint64,sint32,sint64,bool,enum |
| 1 | 64-bit | fixed64,sfixed64,double |
| 2 | Length-delimiter | string,bytes,embedded message, packed repeated fields |
| 3 | 32-bit | fixed32,sfixed32,float |


key 的计算方法是 `(field_number << 3) | wire_type`,换句话说,key 的最后 3 位表示的就是 `wire_type`。  
举例,一般 message 的字段号都是 1 开始的,所以对应的 tag 可能是这样的:



000 1000


末尾 3 位表示的是 value 的类型,这里是 000,即 0 ,代表的是 varint 值。右移 3 位,即 0001,这代表的就是字段号(field number)。tag 的例子就举这么多,接下来举一个 value 的例子,还是用 varint 来举例:



96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits)
→ 10010110
→ 128 + 16 + 4 + 2 = 150


可以 96 01 代表的数据就是 150 。



message Test1 {
required int32 a = 1;
}


如果存在上面这样的一个 message 的结构,如果存入 150,在 Protocol Buffer 中显示的二进制应该为 08 96 01 。  
额外说一句,type 需要注意的是 type = 2 的情况,tag 里面除了包含 field number 和 wire\_type ,还需要再包含一个 length,决定 value 从那一段取出来。  
 


### 5.3 Signed Integers 编码


从上面的表格里面可以看到 wire\_type = 0 中包含了无符号的 varints,但是如果是一个无符号数呢?  
一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 10 个 byte 长度。



> 
> 为何 32 位和 64 位的负数都需要 10 个 byte 长度呢?
> 
> 
> 



inline void CodedOutputStream::WriteVarint32SignExtended(int32 value) {
WriteVarint64(static_cast(value));
}



> 
> 因为源码里面是这么规定的。32 位的有符号数都会转换成 64 位无符号来处理。至于源码为什么要这么规定呢,猜想可能是怕 32 位的负数转换会有溢出的可能。(只是猜想)
> 
> 
> 


为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。**将所有整数映射成无符号整数,然后再采用 varint 编码方式编码**,这样,绝对值小的整数,编码后也会有一个较小的 varint 编码值。  
Zigzag 映射函数为:



Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时
Zigzag(n) = (n << 1) ^ (n >> 63), n 为 sint64 时


按照这种方法,-1 将会被编码成 1,1 将会被编码成 2,-2 会被编码成 3,如下表所示:  
![image.png](https://img-blog.csdnimg.cn/img\_convert/ceffc59cfd8786c6ae173046a362b2b3.png#clientId=u20f7bf7f-74ec-4&from=paste&height=336&id=ubd978331&margin=[object Object]&name=image.png&originHeight=336&originWidth=805&originalType=binary&ratio=1&size=56500&status=done&style=none&taskId=u01cd78a1-a788-4b6a-bcd1-fd37553c4e7&width=805)  
需要注意的是,第二个转换 `(n >> 31)` 部分,是一个算术转换。所以,换句话说,移位的结果要么是一个全为0(如果n是正数),要么是全部1(如果n是负数)。  
当 sint32 或 sint64 被解析时,它的值被解码回原始的带符号的版本。  
 


### 5.4 Non-varint Numbers


Non-varint 数字比较简单,double 、fixed64 的 wire\_type 为 1,在解析时告诉解析器,该类型的数据需要一个 64 位大小的数据块即可。同理,float 和 fixed32 的 wire\_type 为5,给其 32 位数据块即可。两种情况下,都是高位在后,低位在前。  
**说 Protocol Buffer 压缩数据没有到极限,原因就在这里,因为并没有压缩 float、double 这些浮点类型**。  
 


### 5.5 字符串


![](https://img-blog.csdnimg.cn/img\_convert/523a8b8976cb00755458bce294ec667a.webp?x-oss-process=image/format,png#from=url&id=eQWFT&margin=[object Object]&originHeight=502&originWidth=1200&originalType=binary&ratio=1&status=done&style=none)  
wire\_type 类型为 2 的数据,是一种指定长度的编码方式:key + length + content,key 的编码方式是统一的,length 采用 varints 编码方式,content 就是由 length 指定长度的 Bytes。  


![img](https://img-blog.csdnimg.cn/img_convert/a4ee6081d7ce9e2f595212f4067346a5.png)
![img](https://img-blog.csdnimg.cn/img_convert/c25c53112db9c11b15d4df2664cdb068.png)
![img](https://img-blog.csdnimg.cn/img_convert/d18f91f8613f7329e53c61ef2f867026.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

double 这些浮点类型**。  
 


### 5.5 字符串


![](https://img-blog.csdnimg.cn/img\_convert/523a8b8976cb00755458bce294ec667a.webp?x-oss-process=image/format,png#from=url&id=eQWFT&margin=[object Object]&originHeight=502&originWidth=1200&originalType=binary&ratio=1&status=done&style=none)  
wire\_type 类型为 2 的数据,是一种指定长度的编码方式:key + length + content,key 的编码方式是统一的,length 采用 varints 编码方式,content 就是由 length 指定长度的 Bytes。  


[外链图片转存中...(img-k2JV0uHz-1715390470197)]
[外链图片转存中...(img-7MlPHqCU-1715390470197)]
[外链图片转存中...(img-sWT1j7hR-1715390470198)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值