<p id="m2nkj"><option id="m2nkj"><big id="m2nkj"></big></option></p>
    <strong id="m2nkj"></strong>
    <ruby id="m2nkj"></ruby>

    <var id="m2nkj"></var>
  • 搞定Protocol Buffers (下)- 原來你是這樣的pb

    共 20887字,需瀏覽 42分鐘

     ·

    2021-04-06 12:44

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


    protobuf benchmark

    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)行序列化和反序列化的過程。如下圖:

    protobuf

    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)代碼生成等步驟。

    compiler

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

    abstract syntax tree

    拿到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 charconst 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()主要干了幾件事:

    1. protoc參數(shù)解析校驗(yàn)
    2. proto文件解析為由FileDescriptorDescriptor等等組成的抽象語法樹
    3. 調(diào)用具體的Generator,并根據(jù)傳入的FileDescriptor生成代碼
    4. 將所有輸出寫入磁盤

    我們這里主要關(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、javajs、objectivecphp、pythonruby)。如果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é)起來:

    protobuf compile

    運(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è)值:

    1. .proto文件中的字段編號(hào)
    2. 提供足夠信息確定value值長(zhǎng)度的wire type

    在大多數(shù)語言實(shí)現(xiàn)中,這個(gè)key稱為tag。

    可用的wire type如下

    TypeMeaningUsed For
    0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
    164-bitfixed64, sfixed64, double
    2Length-delimitedstring, bytes, embedded messages, packed repeated fields
    3Start groupgroups (deprecated)
    4End groupgroups (deprecated)
    532-bitfixed32, 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

    varints
    更多的值類型
    有符號(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 OriginalEncoded As
    00
    -11
    12
    -23
    21474836474294967294
    -21474836484294967295

    也就是,sint32編碼使用:

    (n << 1) ^ (n >> 31)

    sint64編碼使用:

    (n << 1) ^ (n >> 63)
    非varint數(shù)值

    varint數(shù)值類型比較簡(jiǎn)單,doublefixed64wire type為1,它告訴解析器期望固定的64位數(shù)據(jù)塊;同樣,floatfixed32wire 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消息foobar可能序列化為不同的字節(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)更改而以不同順序序列化字段。
      • foobar都是單個(gè)消息的串聯(lián),但是順序不同。

    總結(jié)

    老生常談總結(jié)下,Protocol Buffers優(yōu)缺點(diǎn):

    優(yōu)點(diǎn):

    1. 跨平臺(tái)、語言無關(guān)

    2. 使用簡(jiǎn)單protoc編譯器自動(dòng)幫助進(jìn)行數(shù)據(jù)的序列化和反序列化

    3. 維護(hù)成本較低,只需要維護(hù).proto文件即可

    4. 向后兼容性較好,不必破壞已部署、依賴舊有結(jié)構(gòu)的程序即可完成對(duì)數(shù)據(jù)結(jié)構(gòu)的更新升級(jí)

    5. 安全性較好,都是以字節(jié)數(shù)組進(jìn)行傳輸

    6. 數(shù)據(jù)序列化后體積較小且速度也相比xml和json快20-100倍

    缺點(diǎn)

    1. 功能簡(jiǎn)單,無法用來表示復(fù)雜的概念
    2. 不像XML成為行業(yè)標(biāo)準(zhǔn),Protobuf只是Google內(nèi)部使用的工具,通用性較差
    3. 自解釋性較差,只能通過.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è)在看??




    瀏覽 116
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    <p id="m2nkj"><option id="m2nkj"><big id="m2nkj"></big></option></p>
    <strong id="m2nkj"></strong>
    <ruby id="m2nkj"></ruby>

    <var id="m2nkj"></var>
  • 日产无码久久久久久久久精英 | 黑人大屌视频 | 丁香五月综合久久 | 一区免费在线 | 美女干逼免费的 | 日韩av男人天堂 日韩mv国产视频 | 黄色片一级免费 | 99视频亚洲 | 成人网站WWW污污污网址 | 操123|