搞定Protocol Buffers (下)- 原來你是這樣的pb
凡事知其然 更要知其所以然。本文僅拋磚引玉,閱讀完本文,也許你也可以試著實(shí)現(xiàn)一個(gè)自己的protoc-gen-xxx。

totalTime表示一個(gè)對(duì)象操作的整個(gè)時(shí)間,包括創(chuàng)建一個(gè)對(duì)象、序列化以及反序列化總共的耗時(shí)。
上圖是從官網(wǎng)找的一個(gè)protocol buffers的序列化壓測(cè)對(duì)比圖,從圖上來看protocol buffers表現(xiàn)相對(duì)還是比較優(yōu)異的。
OK,書接上回。上一篇我們熟悉了protocol buffers安裝使用以及proto3的語法,本篇繼續(xù)來聊聊其實(shí)現(xiàn)原理。
protocol buffers 主要分編譯器編譯部分和運(yùn)行時(shí)部分。編譯器編譯主要是利用protoc命令來將你書寫的proto代碼編譯為指定語言的數(shù)據(jù)訪問類,從而對(duì)Protobuf數(shù)據(jù)進(jìn)行序列化和反序列化。運(yùn)行時(shí)部分主要是將要傳輸?shù)臄?shù)據(jù)進(jìn)行序列化和反序列化的過程。如下圖:

protocol buffers原理
了解一個(gè)組件的原理,沒有比看源碼更好的方式了。傳送門:(https://github.com/protocolbuffers/protobuf),因?yàn)槭褂?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">protocol buffers我們編寫完.proto文件就接觸的是protoc命令了,那先來看看編譯器是怎么工作的吧。
編譯期
編譯器一瞥
通常使用protocol buffers都是先寫好.proto文件,在用protocol buffers編譯器生成目標(biāo)語言所需要的源代碼文件。然后將生成的代碼和應(yīng)用程序一起編譯。所以要了解protocol buffers的源碼,可能需要簡(jiǎn)單了解下編譯器的知識(shí)。編譯器一般分為前端和后端,實(shí)際的流程比較復(fù)雜,主要的步驟包括:詞法分析、語法分析、語義分析、中間代碼生成、優(yōu)化、目標(biāo)代碼生成等步驟。

編譯器前端主要是根據(jù)輸入的.proto文件進(jìn)行詞法、語法、語義分析得到抽象語法樹。

拿到AST,編譯器后端就可以生成中間代碼,這里是直接生成目標(biāo)代碼,生成目標(biāo)代碼的過程可以選擇自帶的生成器,又或者是第三方插件形式提供的Code Generator能力。實(shí)際源代碼如何工作,接著看protoc指令執(zhí)行流程
protoc執(zhí)行流程
既然是命令執(zhí)行那必然有執(zhí)行入口。打開src/google/protobuf/compiler/main.cc很容易就能找打命令執(zhí)行的入口。精簡(jiǎn)下執(zhí)行流程如下:
int main(int argc, char* argv[]) {
CommandLineInterface cli;
cli.AllowPlugins("protoc-");
... ...
// Proto2 Java 官方提供的Java編譯器后端實(shí)現(xiàn)
java::JavaGenerator java_generator;
cli.RegisterGenerator("--java_out", "--java_opt", &java_generator,
"Generate Java source file.");
... ...
return cli.Run(argc, argv);
}
mian函數(shù)里主要完成了內(nèi)置的一些不同語言代碼生成器的注冊(cè),具體protoc命令的參數(shù)解析,proto文件解析交給了CommandLineInterface::Run:
int CommandLineInterface::Run(int argc, const char* const argv[]) {
// 1.參數(shù)解析
// 2.proto文件解析為FileDescriptor
... ...
// 3.調(diào)用Generator生成代碼
if (mode_ == MODE_COMPILE) {
for (int i = 0; i < output_directives_.size(); i++) {
std::string output_location = output_directives_[i].output_location;
if (!HasSuffixString(output_location, ".zip") &&
!HasSuffixString(output_location, ".jar") &&
!HasSuffixString(output_location, ".srcjar")) {
AddTrailingSlash(&output_location);
}
auto& generator = output_directories[output_location];
if (!generator) {
// First time we've seen this output location.
generator.reset(new GeneratorContextImpl(parsed_files));
}
if (!GenerateOutput(parsed_files, output_directives_[i],
generator.get())) {
return 1;
}
}
}
//4.將所有輸出寫入磁盤
... ...
return 0
}
CommandLineInterface::Run()主要干了幾件事:
protoc參數(shù)解析校驗(yàn) proto文件解析為由 FileDescriptor、Descriptor等等組成的抽象語法樹調(diào)用具體的Generator,并根據(jù)傳入的 FileDescriptor生成代碼將所有輸出寫入磁盤
我們這里主要關(guān)注下利用Generator生成代碼的流程:
bool CommandLineInterface::GenerateOutput(
const std::vector<const FileDescriptor*>& parsed_files,
const OutputDirective& output_directive,
GeneratorContext* generator_context) {
// Call the generator.
std::string error;
if (output_directive.generator == NULL) {//插件模式
// This is a plugin.
GOOGLE_CHECK(HasPrefixString(output_directive.name, "--") &&
HasSuffixString(output_directive.name, "_out"))
<< "Bad name for plugin generator: " << output_directive.name;
std::string plugin_name = PluginName(plugin_prefix_, output_directive.name);
std::string parameters = output_directive.parameter;
if (!plugin_parameters_[plugin_name].empty()) {
if (!parameters.empty()) {
parameters.append(",");
}
parameters.append(plugin_parameters_[plugin_name]);
}
if (!GeneratePluginOutput(parsed_files, plugin_name, parameters,
generator_context, &error)) {
std::cerr << output_directive.name << ": " << error << std::endl;
return false;
}
} else {
// Regular generator. 內(nèi)置的生成器
... ...
if (!output_directive.generator->GenerateAll(parsed_files, parameters,
generator_context, &error)) {
// Generator returned an error.
std::cerr << output_directive.name << ": " << error << std::endl;
return false;
}
}
return true;
}
在使用protoc命令時(shí)一般這么執(zhí)行protoc --proto_path=. --objc_out=.*.proto,protoc會(huì)根據(jù)--xxx_out來識(shí)別xxx對(duì)應(yīng)的代碼生成器(當(dāng)前protoc默認(rèn)支持cpp、csharp、java、js、objectivec、php、python、ruby)。如果protoc識(shí)別不了xxx,則會(huì)在PATH路徑下尋找protoc-gen-xxx的可執(zhí)行文件,對(duì)應(yīng)的protoc-gen-xxx是你需要實(shí)現(xiàn)的插件。那么protocol buffers是如何跟protoc-gen-xxx交互的呢?
bool CommandLineInterface::GeneratePluginOutput(
const std::vector<const FileDescriptor*>& parsed_files,
const std::string& plugin_name, const std::string& parameter,
GeneratorContext* generator_context, std::string* error) {
CodeGeneratorRequest request;
CodeGeneratorResponse response;
std::string processed_parameter = parameter;
// Build the request.
if (!processed_parameter.empty()) {
request.set_parameter(processed_parameter);
}
std::set<const FileDescriptor*> already_seen;
for (int i = 0; i < parsed_files.size(); i++) {
request.add_file_to_generate(parsed_files[i]->name());
GetTransitiveDependencies(parsed_files[i],
true, // Include json_name for plugins.
true, // Include source code info.
&already_seen, request.mutable_proto_file());
}
... ...
// 調(diào)用插件
Subprocess subprocess;
if (plugins_.count(plugin_name) > 0) {
subprocess.Start(plugins_[plugin_name], Subprocess::EXACT_NAME);
} else {
subprocess.Start(plugin_name, Subprocess::SEARCH_PATH);
}
std::string communicate_error;
//與插件進(jìn)行交互
if (!subprocess.Communicate(request, &response, &communicate_error)) {
*error = strings::Substitute("$0: $1", plugin_name, communicate_error);
return false;
}
// Write the files. We do this even if there was a generator error in order
// to match the behavior of a compiled-in generator.
std::unique_ptr<io::ZeroCopyOutputStream> current_output;
for (int i = 0; i < response.file_size(); i++) {
const CodeGeneratorResponse::File& output_file = response.file(i);
if (!output_file.insertion_point().empty()) {
std::string filename = output_file.name();
// Open a file for insert.
// We reset current_output to NULL first so that the old file is closed
// before the new one is opened.
current_output.reset();
current_output.reset(
generator_context->OpenForInsertWithGeneratedCodeInfo(
filename, output_file.insertion_point(),
output_file.generated_code_info()));
} else if (!output_file.name().empty()) {
// Starting a new file. Open it.
// We reset current_output to NULL first so that the old file is closed
// before the new one is opened.
current_output.reset();
current_output.reset(generator_context->Open(output_file.name()));
} else if (current_output == NULL) {
*error = strings::Substitute(
"$0: First file chunk returned by plugin did not specify a file "
"name.",
plugin_name);
return false;
}
// Use CodedOutputStream for convenience; otherwise we'd need to provide
// our own buffer-copying loop.
io::CodedOutputStream writer(current_output.get());
writer.WriteString(output_file.content());
}
// 檢查reponse.error()錯(cuò)誤
... ...
return true;
}
對(duì)于采用插件支持生成protobuf代碼的方式,顯然需要將protobuf編譯器前端生成的抽象語法樹等信息交給插件,那這是怎么做的呢?配合src/google/protobuf/compiler/plugin.proto定義就一目了然了
syntax = "proto2";
package google.protobuf.compiler;
option java_package = "com.google.protobuf.compiler";
option java_outer_classname = "PluginProtos";
message Version {
optional int32 major = 1;
optional int32 minor = 2;
optional int32 patch = 3;
optional string suffix = 4;
}
message CodeGeneratorRequest {
repeated string file_to_generate = 1;
optional string parameter = 2;
repeated FileDescriptorProto proto_file = 15;
optional Version compiler_version = 3;
}
message CodeGeneratorResponse {
optional string error = 1;
optional uint64 supported_features = 2;
enum Feature {
FEATURE_NONE = 0;
FEATURE_PROTO3_OPTIONAL = 1;
}
message File {
optional string name = 1;
optional string insertion_point = 2;
optional string content = 15;
optional GeneratedCodeInfo generated_code_info = 16;
}
repeated File file = 15;
}
CodeGeneratorRequest request; CodeGeneratorResponse response;實(shí)際也是利用聲明的proto文件生成的,用于protocol buffers跟插件之間交換信息,關(guān)于具體插件如何利用傳遞的信息生成代碼,感興趣的同學(xué)可以翻翻源碼,不是很復(fù)雜??偨Y(jié)起來:

運(yùn)行時(shí)
關(guān)于運(yùn)行時(shí),我們主要來聊聊protocol buffers編碼的知識(shí),具體各個(gè)語言的具體源碼實(shí)現(xiàn),感興趣的童鞋可以自行翻閱。
編碼
一個(gè)簡(jiǎn)單的消息類型
假設(shè)你定義了如下消息結(jié)構(gòu):
message Test1 {
optional int32 a = 1;
}
然后你在應(yīng)用程序里,創(chuàng)建了一個(gè)Test1消息對(duì)象,并設(shè)置a=150,序列化消息到輸出流。如果你可以檢查編碼后消息,則會(huì)看到如下三個(gè)字節(jié):
08 96 01
這些數(shù)字咋一看可能一臉懵逼,它們到底意味著什么呢?
了解protocol buffers的編碼,你首先需要理解varints。Varints是一種使用一個(gè)或多個(gè)字節(jié)序列化整數(shù)的方法。較小的數(shù)字占用較少的字節(jié)數(shù)。
除了最后一個(gè)字節(jié)外,varint中的每個(gè)字節(jié)都設(shè)置了最高有效位(msb) 用來表示還有其他字節(jié)。每個(gè)字節(jié)的低7位用于以7位為一組存儲(chǔ)數(shù)字的二進(jìn)制補(bǔ)碼表示,最低有效組在前,即采用了大端字節(jié)序。
比如數(shù)字1,占用一個(gè)字節(jié),所以msb沒有設(shè)置:
0000 0001
如果是數(shù)字300,就有點(diǎn)兒復(fù)雜了:
1010 1100 0000 0010
如何確定上面這串?dāng)?shù)字表示的就是300?首先,從每個(gè)字節(jié)中刪除msb,因?yàn)檫@個(gè)是用來告訴我們是否已到達(dá)數(shù)字的末尾。(如果被設(shè)置,表示vaint里有多個(gè)字節(jié))
1010 1100 0000 0010
→ 010 1100 000 0010
反轉(zhuǎn)兩組7bits,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">varint存儲(chǔ)的數(shù)字是低位在前。然后,將它們連接起來從而獲得最終值:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
消息結(jié)構(gòu)
正如你所知道的,protocol buffers的消息是一系列key-value對(duì)組成。消息的二進(jìn)制版本僅使用字段的編號(hào)作為關(guān)鍵字,每個(gè)字段的名稱和聲明的類型只能在解碼端通過引用消息類型定義(即.proto文件)來確定。
對(duì)消息進(jìn)行編碼時(shí),鍵和值被串聯(lián)到一個(gè)字節(jié)流中。在對(duì)消息進(jìn)行解碼時(shí),解析器需要能夠跳過無法識(shí)別的字段。這樣,可以將新字段添加到消息中,而不會(huì)破壞不知道它們的舊程序。故而,wire格式的消息中沒對(duì)key-value中的key實(shí)際上是兩個(gè)值:
.proto文件中的字段編號(hào)提供足夠信息確定value值長(zhǎng)度的 wire type
在大多數(shù)語言實(shí)現(xiàn)中,這個(gè)key稱為tag。
可用的wire type如下
| Type | Meaning | Used For |
|---|---|---|
| 0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | 64-bit | fixed64, sfixed64, double |
| 2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
| 3 | Start group | groups (deprecated) |
| 4 | End group | groups (deprecated) |
| 5 | 32-bit | fixed32, sfixed32, float |
流式消息的每個(gè)key的值格式均為(field_number << 3) | wire_type,也就是說,數(shù)字的后三位存儲(chǔ)了wire type。
現(xiàn)在,讓我們?cè)賮砜匆粋€(gè)簡(jiǎn)單的例子?,F(xiàn)在,你知道流中的第一個(gè)數(shù)字始終是varint鍵,這里是08,或者(刪除了msb):
000 1000
根據(jù)key的規(guī)則,使用最后三位來獲得wire type為(0),然后右移三位來獲得字段編號(hào)(1)。因此,你現(xiàn)在知道字段號(hào)為1,并且key對(duì)應(yīng)的值為varint。使用上面varint解碼知識(shí),你可以看到接下來的兩個(gè)字節(jié)存儲(chǔ)值150。
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (刪除msb并且反轉(zhuǎn)7bits組)
→ 10010110
→ 128 + 16 + 4 + 2 = 150

更多的值類型
有符號(hào)整數(shù)
如之前所說,與wire type0關(guān)聯(lián)的所有protocol buffers類型都被編碼為varint。但是,當(dāng)對(duì)負(fù)數(shù)進(jìn)行編碼時(shí),帶符號(hào)的int類型(sint32和sint64)與"標(biāo)準(zhǔn)"int類型(int32和int64)之間存在重要區(qū)別。若將int32或int64用作負(fù)數(shù)的類型,則varint編碼的結(jié)果需要占用10個(gè)字節(jié)的長(zhǎng)度。實(shí)際上,它被視為一個(gè)非常大的無符號(hào)整數(shù)。若使用帶符號(hào)類型,則生成的varint使用ZigZag編碼,效率更高。
ZigZag編碼將有符號(hào)整數(shù)映射為無符號(hào)整數(shù),這樣較小絕對(duì)值(比如,-1)的負(fù)數(shù),也具有較小的varint編碼值。這樣做的方式是通過正整數(shù)和負(fù)整數(shù)來回"曲折",以便將-1編碼為1,將1編碼為2,將-2編碼為3,依次類推,如下表:
| Signed Original | Encoded As |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2147483647 | 4294967294 |
| -2147483648 | 4294967295 |
也就是,sint32編碼使用:
(n << 1) ^ (n >> 31)
sint64編碼使用:
(n << 1) ^ (n >> 63)
非varint數(shù)值
非varint數(shù)值類型比較簡(jiǎn)單,double和fixed64的wire type為1,它告訴解析器期望固定的64位數(shù)據(jù)塊;同樣,float和fixed32的wire type為5,表明其需要使用32位的數(shù)據(jù)塊。在這兩種情況下,這些值都以小端字節(jié)序存儲(chǔ)。
字符串
字符串類型的數(shù)據(jù)wire type的值為2(length-delimited)。表示該值varint編碼的長(zhǎng)度,后跟指定數(shù)量的字節(jié)數(shù)據(jù)。舉個(gè)例子
message Test2 {
optional string b = 2;
}
將b設(shè)置為testing,序列化數(shù)據(jù)為:
12 07 [74 65 73 74 69 6e 67]
中括號(hào)中的內(nèi)容為testing的UTF8編碼,key的值為0x12,解析下:
0x12
→ 0001 0010 (binary representation)
→ 00010 010 (regroup bits)
→ field_number = 2, wire_type = 2
0x12后的07表示varint值長(zhǎng)度為7。即07后跟著7字節(jié)的字符串。
內(nèi)嵌消息
假設(shè)你有下面這樣一個(gè)內(nèi)嵌消息結(jié)構(gòu):
message Test3 {
optional Test1 c = 3;
}
將c.a設(shè)置為150,得到編碼后的數(shù)據(jù):
1a 03 08 96 01
0001 1010 0000 0011 0000 1000 1001 0110 0000 0001
-> 00011 010 0000 0011 0000 1000 1001 0110 0000 0001
最后三個(gè)字節(jié)跟上面例子中單獨(dú)設(shè)置Test1.a=150的序列化數(shù)據(jù)一致。根據(jù)varint解析,field_number=3 wire_type=2,故而內(nèi)嵌消息編碼處理跟字符串完全一致。
optional和repeated元素
如果proto2消息定義了重復(fù)的元素(沒有定義[packed=ture]選項(xiàng)),則編碼消息具有零個(gè)或多個(gè)具有相同字段編號(hào)的鍵值對(duì)。這些重復(fù)的值不必連續(xù)出現(xiàn),它們也可能跟其他字段交錯(cuò)出現(xiàn),元素之間的順序會(huì)保留下來,盡管其他字段的順序會(huì)丟失。在proto3中,重復(fù)字段使用了壓縮編碼。
對(duì)于proto3中任何非重復(fù)字段,或proto2中的optional字段,編碼后的消息可能有也可能沒有該字段編號(hào)的鍵值對(duì)。
通常一個(gè)編碼的消息永遠(yuǎn)不會(huì)有一個(gè)以上非重復(fù)字段的實(shí)例。但解析器會(huì)根據(jù)實(shí)際情況進(jìn)行處理。對(duì)于數(shù)字類型和字符串類型,如果同一字段出現(xiàn)多次,解析器將接受它看到的最后一個(gè)值。對(duì)于內(nèi)嵌消息字段,解析器合并同一字段的多個(gè)實(shí)例,就像使用Message::MergeFrom方法一樣:也就是說,后面的實(shí)例中所有單值標(biāo)量字段將替換前面的實(shí)例中的單值標(biāo)量字段,并合并單值內(nèi)嵌消息,連接重復(fù)字段。這些規(guī)則的作用是,解析兩個(gè)已編碼消息的串聯(lián)聯(lián)產(chǎn)生的結(jié)果與你分別解析兩個(gè)消息并合并的結(jié)果完全相同。也即:
MyMessage message;
message.ParseFromString(str1 + str2);
等價(jià)于:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
該屬性有時(shí)很有用,因?yàn)榧词鼓悴恢浪鼈兊念愋?,它也允許你合并兩個(gè)消息。
壓縮可重復(fù)字段
2.1.0版本引入了打包可重復(fù)字段的功能,在proto2中聲明為重復(fù)字段,但具有特殊的[packed=true]選項(xiàng)。在proto3中,重復(fù)的標(biāo)量數(shù)字類型默認(rèn)會(huì)被打包。這些功能類似于重復(fù)字段,但編碼方式不同。包含零元素的壓縮重復(fù)字段不會(huì)出現(xiàn)在編碼的消息中。否則,該字段的所有元素都將打包為wire type為2(length-delimited)的單個(gè)鍵值對(duì)。每個(gè)元素的編碼方式與通常相同,不同之處在于之前沒有key。
例如:
message Test4 {
repeated int32 d = 4 [packed=true];
}
現(xiàn)在,假設(shè)你構(gòu)造一個(gè)Test4,為重復(fù)的字段d提供值3、270和86942。然后,編碼形式為:
22 // key (field number 4, wire type 2)
06 // 數(shù)據(jù)長(zhǎng)度 (6 bytes)
03 // 第一個(gè)元素 (varint 3)
8E 02 // 第二個(gè)元素 (varint 270)
9E A7 05 // 第三個(gè)元素 (varint 86942)
只有原始數(shù)字類型(使用varint,32位或64位wire的類型)的重復(fù)字段可以聲明為“打包”。
請(qǐng)注意,盡管通常沒有理由為一個(gè)打包的重復(fù)字段編碼多個(gè)鍵值對(duì),但編碼器必須準(zhǔn)備好接受多個(gè)鍵值對(duì)。在這種情況下,應(yīng)將有效負(fù)載串聯(lián)在一起。每對(duì)必須包含整數(shù)個(gè)元素。
protocol buffers解析器必須能夠解析被編譯為packed的重復(fù)字段,就好像它們沒有packed一樣,反之亦然。這允許以向前和向后兼容的方式將[packed = true]添加到現(xiàn)有字段。
字段順序
字段編號(hào)可以在.proto文件中以任何順序使用。順序的選擇對(duì)消息的序列化方式?jīng)]有影響。
序列化消息時(shí),對(duì)于已知字段或未知字段的寫入沒有保證順序。序列化順序是一個(gè)實(shí)現(xiàn)細(xì)節(jié),將來任何特定實(shí)現(xiàn)的細(xì)節(jié)都可能更改。因此,protocol buffers解析器必須能夠以任何順序解析字段。
含義
不要假定序列化消息的字節(jié)輸出是穩(wěn)定的。對(duì)于消息中具有傳遞表示其他序列化的 protocol buffers消息的字節(jié)字段的場(chǎng)景尤其如此。默認(rèn)情況下,在同一 protocol buffers消息實(shí)例上重復(fù)調(diào)用序列化方法時(shí),可能不會(huì)返回相同的字節(jié)輸出。即默認(rèn)序列化不是確定性的。確定性序列化僅可確保特定二進(jìn)制文件的字節(jié)輸出相同。字節(jié)輸出可能會(huì)在二進(jìn)制的不同版本之間發(fā)生變化。 對(duì)于 protocol buffers消息實(shí)例foo,以下檢查可能會(huì)失敗。foo.SerializeAsString()== foo.SerializeAsString() Hash(foo.SerializeAsString())==Hash(foo.SerializeAsString()) CRC(foo.SerializeAsString())== CRC(foo.SerializeAsString()) FingerPrint(foo.SerializeAsString())== FingerPrint(foo.SerializeAsString()) 這是一些示例場(chǎng)景,其中邏輯等效的 protocol buffers消息foo和bar可能序列化為不同的字節(jié)輸出。bar由一臺(tái)舊服務(wù)器序列化,該服務(wù)器將某些字段視為未知字段。bar由以不同編程語言實(shí)現(xiàn)的服務(wù)器序列化,并以不同順序序列化字段。bar有一個(gè)以不確定性方式序列化的字段。bar有一個(gè)字段,用于存儲(chǔ)protocol buffers消息的序列化字節(jié)輸出,該消息以不同的順序進(jìn)行序列化。bar由新服務(wù)器序列化,該服務(wù)器因?yàn)閷?shí)現(xiàn)更改而以不同順序序列化字段。foo和bar都是單個(gè)消息的串聯(lián),但是順序不同。
總結(jié)
老生常談總結(jié)下,Protocol Buffers優(yōu)缺點(diǎn):
優(yōu)點(diǎn):
跨平臺(tái)、語言無關(guān)
使用簡(jiǎn)單protoc編譯器自動(dòng)幫助進(jìn)行數(shù)據(jù)的序列化和反序列化
維護(hù)成本較低,只需要維護(hù)
.proto文件即可向后兼容性較好,不必破壞已部署、依賴舊有結(jié)構(gòu)的程序即可完成對(duì)數(shù)據(jù)結(jié)構(gòu)的更新升級(jí)
安全性較好,都是以字節(jié)數(shù)組進(jìn)行傳輸
數(shù)據(jù)序列化后體積較小且速度也相比xml和json快20-100倍
缺點(diǎn)
功能簡(jiǎn)單,無法用來表示復(fù)雜的概念 不像XML成為行業(yè)標(biāo)準(zhǔn),Protobuf只是Google內(nèi)部使用的工具,通用性較差 自解釋性較差,只能通過 .proto了解數(shù)據(jù)結(jié)構(gòu)
關(guān)于編碼,了解protocol buffers編碼原理對(duì)于合理使用protocol buffers很有必要,比如我們知道int32 int64較小數(shù)時(shí)確實(shí)序列化結(jié)果更加緊湊,但是int32 int64存儲(chǔ)負(fù)數(shù)的話卻需要10個(gè)字節(jié)的長(zhǎng)度。所以了解原理選擇適合的數(shù)據(jù)類型,從而發(fā)揮protocol buffers的最大威力。對(duì)于有性能潔癖的你來說,值得擁有。
如果閱讀過程中發(fā)現(xiàn)本文存疑或錯(cuò)誤的地方,可以關(guān)注公眾號(hào)留言。如果覺得還可以 幫忙點(diǎn)個(gè)在看??
