protobuf idl

protobuf的IDL都是保存爲*.proto的文件中,proto文件中數據類型能夠分爲兩大類:複合數據類型和標準數據類型。複合數據類型包括:枚舉和message類型,標準數據類型包含:整型,浮點,字符串等。php

定義一個消息類型(message)html

最經常使用的數據格式就是message,假設你想定義一個「搜索請求」的消息格式,每個請求含有一個查詢字符串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。能夠採用以下的方式來定義消息類型的.proto文件了:java

message SearchRequest {

  required string query = 1;

  optional int32 page_number = 2;

  optional int32 result_per_page = 3;

}

SearchRequest消息格式有3個字段,在消息中承載的數據分別對應於每個字段。其中每一個字段都有一個名字和一種類型。python

Ø  指定字段類型設計模式

在上面的例子中,全部字段都是標量類型:兩個整型(page_number和result_per_page),一個string類型(query)。固然,你也能夠爲字段指定其餘的合成類型,包括枚舉(enumerations)或其餘消息類型。api

Ø   分配標識號安全

正如上述文件格式,在消息定義中,每一個字段都有惟一的一個標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不可以再改 變。注:[1,15]以內的標識號在編碼的時候會佔用一個字節。[16,2047]以內的標識號則佔用2個字節。因此應該爲那些頻繁出現的消息元素保留 [1,15]以內的標識號。切記:要爲未來有可能添加的、頻繁出現的標識號預留一些標識號。服務器

最小的標識號能夠從1開始,最大到229 - 1, or 536,870,911。不可使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。若是非要在.proto文件中使用這些預留標識號,編譯時就會報警。ide

Ø  指定字段規則函數

所指定的消息字段修飾符必須是以下之一:

²  required:一個格式良好的消息必定要含有1個這種字段。表示該值是必需要設置的;

²  optional:消息格式中該字段能夠有0個或1個值(不超過1個)。

²  repeated:在一個格式良好的消息中,這種字段能夠重複任意屢次(包括0次)。重複的值的順序會被保留。表示該值能夠重複,至關於java中的List。

因爲一些歷史緣由,基本數值類型的repeated的字段並無被儘量地高效編碼。在新的代碼中,用戶應該使用特殊選項[packed=true]來保證更高效的編碼。如:

repeated int32 samples = 4 [packed=true];

required是永久性的:在將一個字段標識爲required的時候,應該特別當心。若是在某些狀況下不想寫入或者發送一個required的 字段,將原始該字段修飾符更改成optional可能會遇到問題——舊版本的使用者會認爲不含該字段的消息是不完整的,從而可能會無目的的拒絕解析。在這 種狀況下,你應該考慮編寫特別針對於應用程序的、自定義的消息校驗函數。Google的一些工程師得出了一個結論:使用required弊多於利;他們更 願意使用optional和repeated而不是required。固然,這個觀點並不具備廣泛性。

Ø   添加更多消息類型

在一個.proto文件中能夠定義多個消息類型。在定義多個相關的消息的時候,這一點特別有用——例如,若是想定義與SearchResponse消息類型對應的回覆消息格式的話,你能夠將它添加到相同的.proto文件中,如:

message SearchRequest {

  required string query = 1;

  optional int32 page_number = 2;

  optional int32 result_per_page = 3;

}

 

message SearchResponse {

 …

}

Ø  添加註釋

向.proto文件添加註釋,可使用C/C++/java風格的雙斜槓(//) 語法格式,如:

message SearchRequest {

  required string query = 1;

  optional int32 page_number = 2;// 最終返回的頁數

  optional int32 result_per_page = 3;// 每頁返回的結果數

}

Ø  從.proto文件生成了什麼?

當用protocolbuffer編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,這些代碼能夠操做在.proto文件中定義的消息類型,包括獲取、設置字段值,將消息序列化到一個輸出流中,以及從一個輸入流中解析消息。

  •  對C++來講,編譯器會爲每一個.proto文件生成一個.h文件和一個.cc文件,.proto文件中的每個消息有一個對應的類。
  •  對Java來講,編譯器爲每個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來建立消息類接口的)。
  • 對Python來講,有點不太同樣——Python編譯器爲.proto文件中的每一個消息類型生成一個含有靜態描述符的模塊,,該模塊與一個元類(metaclass)在運行時(runtime)被用來建立所需的Python數據訪問類。

l  標量數值類型

一個標量消息字段能夠含有一個以下的類型——該表格展現了定義於.proto文件中的類型,以及與之對應的、在自動生成的訪問類中定義的類型:

.proto類型

Java 類型

C++類型

備註

double

double

double

 

float

float

float

 

int32

int

int32

使用可變長編碼方式。編碼負數時不夠高效——若是你的字段可能含有負數,那麼請使用sint32。

int64

long

int64

使用可變長編碼方式。編碼負數時不夠高效——若是你的字段可能含有負數,那麼請使用sint64。

uint32

int[1]

uint32

Uses variable-length encoding.

uint64

long[1] uint64 Uses variable-length encoding.

sint32

int

int32

使用可變長編碼方式。有符號的整型值。編碼時比一般的int32高效。

sint64

long

int64

使用可變長編碼方式。有符號的整型值。編碼時比一般的int64高效。

fixed32

int[1]

uint32

老是4個字節。若是數值老是比老是比228大的話,這個類型會比uint32高效。

fixed64

long[1]

uint64

老是8個字節。若是數值老是比老是比256大的話,這個類型會比uint64高效。

sfixed32

int

int32

老是4個字節。

sfixed64

long

int64

老是8個字節。

bool

boolean

bool

 

string

String

string

一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本。

bytes

ByteString

string

可能包含任意順序的字節數據。

l  Optional的字段和默認值

如上所述,消息描述中的一個元素能夠被標記爲「可選的」(optional)。一個格式良好的消息能夠包含0個或一個optional的元素。當解 析消息時,若是它不包含optional的元素值,那麼解析出來的對象中的對應字段就被置爲默認值。默認值能夠在消息描述文件中指定。例如,要爲 SearchRequest消息的result_per_page字段指定默認值10,在定義消息格式時以下所示:

optional int32 result_per_page = 3 [default = 10];

 若是沒有爲optional的元素指定默認值,就會使用與特定類型相關的默認值:對string來講,默認值是空字符串。對bool來講,默認值是false。對數值類型來講,默認值是0。對枚舉來講,默認值是枚舉類型定義中的第一個值。

l 枚舉

當須要定義一個消息類型的時候,可能想爲一個字段指定某「預約義值序列」中的一個值。例如,假設要爲每個SearchRequest消息添加一個 corpus字段,而corpus的值多是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個。 其實能夠很容易地實現這一點:經過向消息定義中添加一個枚舉(enum)就能夠了。一個enum類型的字段只能用指定的常量集中的一個值做爲其值(若是嘗 試指定不一樣的值,解析器就會把它看成一個未知的字段來對待)。在下面的例子中,在消息格式中添加了一個叫作Corpus的枚舉類型——它含有全部可能的值 ——以及一個類型爲Corpus的字段:

message SearchRequest {

  required string query = 1;

  optional int32 page_number = 2;

  optional int32 result_per_page = 3 [default = 10];

  enum Corpus {

    UNIVERSAL = 0;

    WEB = 1;

    IMAGES = 2;

    LOCAL = 3;

    NEWS = 4;

    PRODUCTS = 5;

    VIDEO = 6;

  }

  optional Corpus corpus = 4 [default = UNIVERSAL];

}

枚舉常量必須在32位整型值的範圍內。由於enum值是使用可變編碼方式的,對負數不夠高效,所以不推薦在enum中使用負數。如上例所示,能夠在 一個消息定義的內部或外部定義枚舉——這些枚舉能夠在.proto文件中的任何消息定義裏重用。固然也能夠在一個消息中聲明一個枚舉類型,而在另外一個不一樣 的消息中使用它——採用MessageType.EnumType的語法格式。

當對一個使用了枚舉的.proto文件運行protocol buffer編譯器的時候,生成的代碼中將有一個對應的enum(對Java或C++來講),或者一個特殊的EnumDescriptor類(對 Python來講),它被用來在運行時生成的類中建立一系列的整型值符號常量(symbolic constants)。

l  使用其餘消息類型

你能夠將其餘消息類型用做字段類型。例如,假設在每個SearchResponse消息中包含Result消息,此時能夠在相同的.proto文件中定義一個Result消息類型,而後在SearchResponse消息中指定一個Result類型的字段,如:

message SearchResponse {

  repeated Result result = 1;

}

message Result {

  required string url = 1;

  optional string title = 2;

  repeated string snippets = 3;

}

Ø  導入定義

在上面的例子中,Result消息類型與SearchResponse是定義在同一文件中的。若是想要使用的消息類型已經在其餘.proto文件中已經定義過了呢?

 你能夠經過導入(importing)其餘.proto文件中的定義來使用它們。要導入其餘.proto文件的定義,你須要在你的文件中添加一個導入聲明,如:

import "myproject/other_protos.proto";

protocol編譯器就會在一系列目錄中查找須要被導入的文件,這些目錄經過protocol編譯器的命令行參數-I/–import_path指定。若是不提供參數,編譯器就在其調用目錄下查找。

l  嵌套類型

你能夠在其餘消息類型中定義、使用消息類型,在下面的例子中,Result消息就定義在SearchResponse消息內,如:

message SearchResponse {

  message Result {

    required string url = 1;

    optional string title = 2;

    repeated string snippets = 3;

  }

  repeated Result result = 1;

}

若是你想在它的父消息類型的外部重用這個消息類型,你須要以Parent.Type的形式使用它,如:

message SomeOtherMessage {

  optional SearchResponse.Result result = 1;

}

固然,你也能夠將消息嵌套任意多層,如:

message Outer {                  // Level 0

  message MiddleAA {  // Level 1

    message Inner {   // Level 2

      required int64 ival = 1;

      optional bool  booly = 2;

    }

  }

  message MiddleBB {  // Level 1

    message Inner {   // Level 2

      required int32 ival = 1;

      optional bool  booly = 2;

    }

  }

}

l  更新一個消息類型

若是一個已有的消息格式已沒法知足新的需求——如,要在消息中添加一個額外的字段——可是同時舊版本寫的代碼仍然可用。不用擔憂!更新消息而不破壞已有代碼是很是簡單的。在更新時只要記住如下的規則便可。

  • 不要更改任何已有的字段的數值標識。
  •  所添加的任何字段都必須是optional或repeated的。這就意味着任何使用「舊」的消息格式的代碼序列化的消息能夠被新的代碼所解析,由於它們 不會丟掉任何required的元素。應該爲這些元素設置合理的默認值,這樣新的代碼就可以正確地與老代碼生成的消息交互了。相似地,新的代碼建立的消息 也能被老的代碼解析:老的二進制程序在解析的時候只是簡單地將新字段忽略。然而,未知的字段是沒有被拋棄的。此後,若是消息被序列化,未知的字段會隨之一 起被序列化——因此,若是消息傳到了新代碼那裏,則新的字段仍然可用。注意:對Python來講,對未知字段的保留策略是無效的。
  • 非required的字段能夠移除——只要它們的標識號在新的消息類型中再也不使用(更好的作法多是重命名那個字段,例如在字段前添加「OBSOLETE_」前綴,那樣的話,使用的.proto文件的用戶未來就不會無心中從新使用了那些不應使用的標識號)。
  • 一個非required的字段能夠轉換爲一個擴展,反之亦然——只要它的類型和標識號保持不變。
  • int32, uint32, int64, uint64,和bool是所有兼容的,這意味着能夠將這些類型中的一個轉換爲另一個,而不會破壞向前、 向後的兼容性。若是解析出來的數字與對應的類型不相符,那麼結果就像在C++中對它進行了強制類型轉換同樣(例如,若是把一個64位數字看成int32來 讀取,那麼它就會被截斷爲32位的數字)。
  • sint32和sint64是互相兼容的,可是它們與其餘整數類型不兼容。
  • string和bytes是兼容的——只要bytes是有效的UTF-8編碼。
  • 嵌套消息與bytes是兼容的——只要bytes包含該消息的一個編碼過的版本。
  • fixed32與sfixed32是兼容的,fixed64與sfixed64是兼容的。

l  擴展

經過擴展,能夠將一個範圍內的字段標識號聲明爲可被第三方擴展所用。而後,其餘人就能夠在他們本身的.proto文件中爲該消息類型聲明新的字段,而沒必要去編輯原始文件了。看個具體例子:

message Foo {

  // …

  extensions 100 to 199;

}

這個例子代表:在消息Foo中,範圍[100,199]以內的字段標識號被保留爲擴展用。如今,其餘人就能夠在他們本身的.proto文件中添加新字段到Foo裏了,可是添加的字段標識號要在指定的範圍內——例如:

extend Foo {

  optional int32 bar = 126;

}

這個例子代表:消息Foo如今有一個名爲bar的optional int32字段。

當用戶的Foo消息被編碼的時候,數據的傳輸格式與用戶在Foo裏定義新字段的效果是徹底同樣的。

然而,要在程序代碼中訪問擴展字段的方法與訪問普通的字段稍有不一樣——生成的數據訪問代碼爲擴展準備了特殊的訪問函數來訪問它。例如,下面是如何在C++中設置bar的值:

Foo foo;
foo.SetExtension(bar, 15);

相似地,Foo類也定義了模板函數 HasExtension(),ClearExtension(),GetExtension(),MutableExtension(),以及 AddExtension()。這些函數的語義都與對應的普通字段的訪問函數相符。要查看更多使用擴展的信息,請參考相應語言的代碼生成指南。注:擴展可 以是任何字段類型,包括消息類型。

l  嵌套的擴展

能夠在另外一個類型的範圍內聲明擴展,如:

message Baz {

  extend Foo {

    optional int32 bar = 126;

  }

  …

}

在此例中,訪問此擴展的C++代碼以下:

Foo foo;

foo.SetExtension(Baz::bar, 15);

一個一般的設計模式就是:在擴展的字段類型的範圍內定義該擴展——例如,下面是一個Foo的擴展(該擴展是Baz類型的),其中,擴展被定義爲了Baz的一部分:

message Baz {

  extend Foo {

    optional Baz foo_ext = 127;

  }

  …

}

然而,並無強制要求一個消息類型的擴展必定要定義在那個消息中。也能夠這樣作:

message Baz {

  …

}



extend Foo {

  optional Baz foo_baz_ext = 127;

}

 

事實上,這種語法格式更能防止引發混淆。正如上面所提到的,嵌套的語法一般被錯誤地認爲有子類化的關係——尤爲是對那些還不熟悉擴展的用戶來講。

Ø  選擇可擴展的標符號

在同一個消息類型中必定要確保兩個用戶不會擴展新增相同的標識號,不然可能會致使數據的不一致。能夠經過爲新項目定義一個可擴展標識號規則來防止該狀況的發生。

若是標識號須要很大的數量時,能夠將該可擴展標符號的範圍擴大至max,其中max是229 - 1, 或536,870,911。以下所示:

message Foo {

  extensions 1000 to max;

}

一般狀況下在選擇標符號時,標識號產生的規則中應該避開[19000-19999]之間的數字,由於這些已經被Protocol Buffers實現中預留了。

l  包(Package)

固然能夠爲.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模塊是按照其在文件系統中的位置進行組織的。

 

Ø  包及名稱的解析

Protocol buffer語言中類型名稱的解析與C++是一致的:首先從最內部開始查找,依次向外進行,每一個包會被看做是其父類包的內部類。固然對於 (foo.bar.Baz)這樣以「.」分隔的意味着是從最外圍開始的。ProtocolBuffer編譯器會解析.proto文件中定義的全部類型名。 對於不一樣語言的代碼生成器會知道如何來指向每一個具體的類型,即便它們使用了不一樣的規則。

l  定義服務(Service)

若是想要將消息類型用在RPC(遠程方法調用)系統中,能夠在.proto文件中定義一個RPC服務接口,protocol buffer編譯器將會根據所選擇的不一樣語言生成服務接口代碼及存根。如,想要定義一個RPC服務並具備一個方法,該方法可以接收 SearchRequest並返回一個SearchResponse,此時能夠在.proto文件中進行以下定義:

service SearchService {

  rpc Search (SearchRequest) returns (SearchResponse);

}

protocol編譯器將產生一個抽象接口SearchService以及一個相應的存根實現。存根將全部的調用指向RpcChannel,它是一 個抽象接口,必須在RPC系統中對該接口進行實現。如,能夠實現RpcChannel以完成序列化消息並經過HTTP方式來發送到一個服務器。換句話說, 產生的存根提供了一個類型安全的接口用來完成基於protocolbuffer的RPC調用,而不是將你限定在一個特定的RPC的實現中。C++中的代碼 以下所示:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;
  

// The protocol compiler generates the SearchService class based on the
  // definition given above.


service = new SearchService::Stub(channel);
  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, request, response, protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

 

全部service類都必須實現Service接口,它提供了一種用來調用具體方法的方式,即在編譯期不須要知道方法名及它的輸入、輸出類型。在服務器端,經過服務註冊它能夠被用來實現一個RPC Server。

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

l  生成訪問類

能夠經過定義好的.proto文件來生成Java、Python、C++代碼,須要基於.proto文件運行protocol buffer編譯器protoc。運行的命令以下所示:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto

·        IMPORT_PATH聲明瞭一個.proto文件所在的具體目錄。若是忽略該值,則使用當前目錄。若是有多個目錄則能夠 對--proto_path 寫屢次,它們將會順序的被訪問並執行導入。-I=IMPORT_PATH是它的簡化形式。

·        固然也能夠提供一個或多個輸出路徑:

o   --cpp_out 在目標目錄DST_DIR中產生C++代碼,能夠在 http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference /cpp-generated.html中查看更多。

o   --java_out 在目標目錄DST_DIR中產生Java代碼,能夠在 http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference /java-generated.html中查看更多。

o   --python_out 在目標目錄 DST_DIR 中產生Python代碼,能夠在http://code.google.com/intl/zh-CN/apis/protocolbuffers /docs/reference/python-generated.html中查看更多。

     做爲一種額外的使得,若是DST_DIR 是以.zip或.jar結尾的,編譯器將輸出結果打包成一個zip格式的歸檔文件。.jar將會輸出一個 Java JAR聲明必須的manifest文件。注:若是該輸出歸檔文件已經存在,它將會被重寫,編譯器並無作到足夠的智能來爲已經存在的歸檔文件添加新的文 件。

·        你必須提供一個或多個.proto文件做爲輸入。多個.proto文件可以一次所有聲明。雖然這些文件是相對於當前目錄來命名的,每一個文件必須在一個IMPORT_PATH中,只有如此編譯器才能夠決定它的標準名稱。

相關文章
相關標籤/搜索