語言指導(dǎo)(proto3)

語言指導(dǎo)(proto3)

翻譯自:https://developers.google.com/protocol-buffers/docs/proto3

關(guān)注 紅牛慕課镊绪,發(fā)送 proto3 獲取該文檔的 PDF 版本寒亥。

本指導(dǎo)描述了如何使用 protocol buffer 語言來構(gòu)建 protocol buffer 數(shù)據(jù),包括 .proto 文件語法和如何基于該 .proto 文件生成數(shù)據(jù)訪問類挂捅。本文是涵蓋 protocol buffer 語言 proto3 版本的內(nèi)容芹助,若需要 proto2 版本的信息,請參考 Proto2 Language Guide 闲先。

本文是語言指導(dǎo)——關(guān)于文中描述內(nèi)容的分步示例状土,請參考所選編程語言的對應(yīng) tutorial (當前僅提供了 proto2,更多 proto3 的內(nèi)容會持續(xù)更新)伺糠。

定義一個消息類型

我們先看一個簡單示例蒙谓。比如說我們想定義個關(guān)于搜索請求的消息,每個搜索請求包含一個查詢字符串训桶,一個特定的頁碼累驮,和每頁的結(jié)果數(shù)量酣倾。下面是用于定義消息類型的 .proto 文件:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指明了我們使用的是 proto3 語法:若不指定該行 protocol buffer 編譯器會認為是 proto2 。該行必須是文件的第一個非空或非注釋行谤专。
  • SearchRequest 消息定義了三個字段(名稱/值對)躁锡,字段就是每個要包含在該類型消息中的部分數(shù)據(jù)。每個字段都具有名稱和類型 置侍。

指定字段類型

上面的例子中映之,全部字段都是標量類型:兩個整型(page_numberresult_per_page)和一個字符串型(query)。同樣蜡坊,也可以指定復(fù)合類型的字段杠输,包括枚舉型和其他消息類型。

分配字段編號

正如你所見秕衙,消息中定義的每個字段都有一個唯一編號蠢甲。字段編號用于在消息二進制格式中標識字段,同時要求消息一旦使用字段編號就不應(yīng)該改變据忘。注意一點 1 到 15 的字段編號需要用 1 個字節(jié)來編碼鹦牛,編碼同時包括字段編號和字段類型( 獲取更多信息請參考 Protocol Buffer Encoding )。16 到 2047 的字段變化使用 2 個字節(jié)勇吊。因此應(yīng)將 1 到 15 的編號用在消息的常用字段上能岩。注意應(yīng)該為將來可能添加的常用字段預(yù)留字段編號。

最小的字段編號為 1萧福,最大的為 2^29 - 1,或 536,870,911辈赋。注意不能使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber)的字段編號鲫忍,因為是 protocol buffer 內(nèi)部保留的——若在 .proto 文件中使用了這些預(yù)留的編號 protocol buffer 編譯器會發(fā)出警告。同樣也不能使用之前預(yù)留的字段編號钥屈。

指定字段規(guī)則

消息的字段可以是一下規(guī)則之一:

  • singular 悟民, 格式良好的消息可以有 0 個或 1 個該字段(但不能多于 1 個)。這是 proto3 語法的默認字段規(guī)則篷就。
  • repeated 射亏,格式良好的消息中該字段可以重復(fù)任意次數(shù)(包括 0 次)。重復(fù)值的順序?qū)⒈槐A簟?/li>

在 proto3 中竭业,標量數(shù)值類型的重復(fù)字段默認會使用 packed 壓縮編碼智润。

更多關(guān)于 packed 壓縮編碼的信息請參考 Protocol Buffer Encoding

增加更多消息類型

單個 .proto 文件中可以定義多個消息類型未辆。這在定義相關(guān)聯(lián)的多個消息中很有用——例如要定義與搜索消息SearchRequest 相對應(yīng)的回復(fù)消息 SearchResponse窟绷,則可以在同一個 .proto 文件中增加它的定義:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

增加注釋

使用 C/C++ 風(fēng)格的 ///* ... */ 語法在 .proto 文件添加注釋。

/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}

保留字段

在采取徹底刪除或注釋掉某個字段的方式來更新消息類型時咐柜,將來其他用戶再更新該消息類型時可能會重用這個字段編號兼蜈。后面再加載該 .ptoto 的舊版本時會引發(fā)好多問題攘残,例如數(shù)據(jù)損壞,隱私漏洞等为狸。一個防止該問題發(fā)生的辦法是將刪除字段的編號(或字段名稱歼郭,字段名稱會導(dǎo)致在 JSON 序列化時產(chǎn)生問題)設(shè)置為保留項 reserved。protocol buffer 編譯器在用戶使用這些保留字段時會發(fā)出警告辐棒。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注意病曾,不能在同一條 reserved 語句中同時使用字段編號和名稱。

.proto 文件會生成什么涉瘾?

當 protocol buffer 編譯器作用于一個 .proto 文件時知态,編輯器會生成基于所選編程語言的關(guān)于 .proto 文件中描述消息類型的相關(guān)代碼 ,包括對字段值的獲取和設(shè)置立叛,序列化消息用于輸出流负敏,和從輸入流解析消息。

  • 對于 C++秘蛇, 編輯器會針對于每個 .proto 文件生成.h.cc 文件其做,對于每個消息類型會生成一個類。
  • 對于 Java, 編譯器會生成一個 .java 文件和每個消息類型對應(yīng)的類赁还,同時包含一個特定的 Builder類用于構(gòu)建消息實例妖泄。
  • Python 有些不同 – Python 編譯器會對于 .proto 文件中每個消息類型生成一個帶有靜態(tài)描述符的模塊,以便于在運行時使用 metaclass 來創(chuàng)建必要的 Python 數(shù)據(jù)訪問類艘策。
  • 對于 Go蹈胡, 編譯器會生成帶有每種消息類型的特定數(shù)據(jù)類型的定義在.pb.go 文件中。
  • 對于 Ruby朋蔫,編譯器會生成帶有消息類型的 Ruby 模塊的 .rb 文件罚渐。
  • 對于Objective-C,編輯器會針對于每個 .proto 文件生成pbobjc.hpbobjc.m. 文件驯妄,對于每個消息類型會生成一個類荷并。
  • 對于 C#,編輯器會針對于每個 .proto 文件生成.cs 文件青扔,對于每個消息類型會生成一個類源织。
  • 對于 Dart,編輯器會針對于每個 .proto 文件生成.pb.dart 文件微猖,對于每個消息類型會生成一個類谈息。

可以參考所選編程語言的教程了解更多 API 的信息。更多 API 詳細信息凛剥,請參閱相關(guān)的 API reference 黎茎。

標量數(shù)據(jù)類型

消息標量字段可以是以下類型之一——下表列出了可以用在 .proto 文件中使用的類型,以及在生成代碼中的相關(guān)類型:

.proto Type Notes C++ Type Java Type Python Type[2] Go Type Ruby Type C# Type PHP Type Dart Type
double double double float float64 Float double float double
float float float float float32 Float float float double
int32 使用變長編碼当悔。負數(shù)的編碼效率較低——若字段可能為負值傅瞻,應(yīng)使用 sint32 代替踢代。 int32 int int int32 Fixnum or Bignum (as required) int integer int
int64 使用變長編碼。負數(shù)的編碼效率較低——若字段可能為負值嗅骄,應(yīng)使用 sint64 代替胳挎。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
uint32 使用變長編碼。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
uint64 使用變長編碼溺森。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sint32 使用變長編碼慕爬。符號整型。負值的編碼效率高于常規(guī)的 int32 類型屏积。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sint64 使用變長編碼医窿。符號整型。負值的編碼效率高于常規(guī)的 int64 類型炊林。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
fixed32 定長 4 字節(jié)姥卢。若值常大于2^28 則會比 uint32 更高效。 uint32 int[1] int/long[3] uint32 Fixnum or Bignum (as required) uint integer int
fixed64 定長 8 字節(jié)渣聚。若值常大于2^56 則會比 uint64 更高效独榴。 uint64 long[1] int/long[3] uint64 Bignum ulong integer/string[5] Int64
sfixed32 定長 4 字節(jié)。 int32 int int int32 Fixnum or Bignum (as required) int integer int
sfixed64 定長 8 字節(jié)奕枝。 int64 long int/long[3] int64 Bignum long integer/string[5] Int64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool
string 包含 UTF-8 和 ASCII 編碼的字符串棺榔,長度不能超過 2^32 。 string String str/unicode[4] string String (UTF-8) string string String
bytes 可包含任意的字節(jié)序列但長度不能超過 2^32 隘道。 string ByteString str []byte String (ASCII-8BIT) ByteString string List<int>

可以在 Protocol Buffer Encoding 中獲取更多關(guān)于消息序列化時類型編碼的相關(guān)信息症歇。

[1] Java 中,無符號 32 位和 64 位整數(shù)使用它們對應(yīng)的符號整數(shù)表示谭梗,第一個 bit 位僅是簡單地存儲在符號位中当船。

[2] 所有情況下,設(shè)置字段的值將執(zhí)行類型檢查以確保其有效默辨。

[3] 64 位或無符號 32 位整數(shù)在解碼時始終表示為 long,但如果在設(shè)置字段時給出 int苍息,則可以為 int缩幸。在所有情況下,該值必須適合設(shè)置時的類型竞思。見 [2]表谊。

[4] Python 字符串在解碼時表示為 unicode,但如果給出了 ASCII 字符串盖喷,則可以是 str(這條可能會發(fā)生變化)爆办。

[5] Integer 用于 64 位機器,string 用于 32 位機器课梳。

默認值

當解析消息時,若消息編碼中沒有包含某個元素,則相應(yīng)的會使用該字段的默認值揪利。默認值依據(jù)類型而不同:

  • 字符串類型杭抠,空字符串
  • 字節(jié)類型,空字節(jié)
  • 布爾類型舌缤,false
  • 數(shù)值類型,0
  • 枚舉類型,第一個枚舉元素
  • 內(nèi)嵌消息類型步势,依賴于所使用的編程語言。參考 generated code guide 獲取詳細信息背犯。

對于可重復(fù)類型字段的默認值是空的( 通常是相應(yīng)語言的一個空列表 )坏瘩。

注意一下標量字段,在消息被解析后是不能區(qū)分字段是使用默認值(例如一個布爾型字段是否被設(shè)置為 false )賦值還是被設(shè)置為某個值的漠魏。例如你不能通過對布爾值等于 false 的判斷來執(zhí)行一個不希望在默認情況下執(zhí)行的行為倔矾。同時還要注意若一個標量字段設(shè)置為默認的值,那么是不會被序列化以用于傳輸?shù)摹?/p>

查看 generated code guide 來獲得更多關(guān)于編程語言生成代碼的內(nèi)容蛉幸。

枚舉

定義消息類型時破讨,可能需要某字段值是一些預(yù)設(shè)值之一。例如當需要在 SearchRequest 消息類型中增加一個 corpus 字段奕纫, corpus 字段的值可以是 UNIVERSAL提陶, WEBIMAGES匹层, LOCAL隙笆, NEWSPRODUCTSVIDEO升筏。僅僅需要在消息類型中定義帶有預(yù)設(shè)值常量的 enum 類型即可完成上面的定義撑柔。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}

如你所見,Corpus 枚舉類型的第一個常量映射到 0 :每個枚舉的定義必須包含一個映射到 0 的常量作為第一個元素您访。原因是:

  • 必須有一個 0 值铅忿,才可以作為數(shù)值類型的默認值。
  • 0 值常量必須作為第一個元素灵汪,是為了與 proto2 的語義兼容就是第一個元素作為默認值檀训。

將相同的枚舉值分配給不同的枚舉選項常量可以定義別名。要定義別名需要將 allow_alisa 選項設(shè)置為 true享言,否則 protocol 編譯器當發(fā)現(xiàn)別名定義時會報錯峻凫。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}

枚舉的常量值必須在 32 位整數(shù)的范圍內(nèi)。因為枚舉值在傳輸時采用的是 varint 編碼览露,同時負值無效因而不建議使用荧琼。可以如上面例子所示,將枚舉定義在消息類型內(nèi)命锄,也可以將其定義外邊——這樣該枚舉可以用在 .proto 文件中定義的任意的消息類型中以便重用堰乔。還可以使用 MessageType.EnumType 語法將枚舉定義為消息字段的某一數(shù)據(jù)類型。

使用 protocol buffer 編譯器編譯 .proto 中的枚舉時累舷,對于 Java 或 C 會生成相應(yīng)的枚舉類型浩考,對于 Python 會生成特定的 EnumDescriptor 類用于在運行時創(chuàng)建一組整型值符號常量即可。

反序列化時被盈,未識別的枚舉值會被保留在消息內(nèi)析孽,但如何表示取決于編程語言。若語言支持開放枚舉類型允許范圍外的值時只怎,這些未識別的枚舉值簡單的以底層整型進行存儲袜瞬,就像 C++ 和 Go。若語言支持封閉枚舉類型例如 Java身堡,一種情況是使用特殊的訪問器(譯注:accessors)來訪問底層的整型邓尤。無論哪種語言,序列化時的未識別枚舉值都會被保留在序列化結(jié)果中贴谎。

更多所選語言中關(guān)于枚舉的處理汞扎,請參考 generated code guide

保留值

在采取徹底刪除或注釋掉某個枚舉值的方式來更新枚舉類型時擅这,將來其他用戶再更新該枚舉類型時可能會重用這個枚舉數(shù)值澈魄。后面再加載該 .ptoto 的舊版本時會引發(fā)好多問題,例如數(shù)據(jù)損壞仲翎,隱私漏洞等痹扇。一個防止該問題發(fā)生的辦法是將刪除的枚舉數(shù)值(或名稱,名稱會導(dǎo)致在 JSON 序列化時產(chǎn)生問題)設(shè)置為保留項 reserved溯香。protocol buffer 編譯器在用戶使用這些特定數(shù)值時會發(fā)出警告鲫构。可以使用 max 關(guān)鍵字來指定保留值的范圍到最大可能值玫坛。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注意不能在 reserved 語句中混用字段名稱和數(shù)值结笨。

使用其他消息類型

消息類型也可作為字段類型。例如湿镀,我們需要在 SearchResponse 消息中包含 Result 消息——想要做到這一點炕吸,可以將 Result 消息類型的定義放在同一個 .proto 文件中同時在 SearchResponse 消息中指定一個 Result 類型的字段:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}   

導(dǎo)入定義

前面的例子中,我們將 Result 消息定義在了與 SearchResponse 相同的文件中——但若我們需要作為字段類型使用的消息類型已經(jīng)定義在其他的 .proto 文件中了呢肠骆?

可以通過導(dǎo)入操作來使用定義在其他 .proto 文件中的消息定義。在文件的頂部使用 import 語句完成導(dǎo)入其他 .proto 文件中的定義:

import "myproject/other_protos.proto";

默認情況下僅可以通過直接導(dǎo)入 .proto 文件來使用這些定義塞耕。然而有時會需要將 .proto 文件移動位置蚀腿。可以通過在原始位置放置一個偽 .proto 文件使用 import public 概念來轉(zhuǎn)發(fā)對新位置的導(dǎo)入,而不是在發(fā)生一點更改時就去更新全部對舊文件的導(dǎo)入位置莉钙。任何導(dǎo)入包含 import public 語句的 proto 文件就會對其中的 import public 依賴產(chǎn)生傳遞依賴廓脆。例如:

// new.proto
// 全部定義移動到該文件
// old.proto
// 這是在客戶端中導(dǎo)入的偽文件
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 可使用 old.proto 和 new.proto 中的定義,但不能使用 other.proto 中的定義

protocol 編譯器會使用命令行參數(shù) -I/--proto_path 所指定的目錄集合中檢索需要導(dǎo)入的文件磁玉。若沒有指定停忿,會在調(diào)用編譯器的目錄中檢索。通常應(yīng)該將 --proto_path 設(shè)置為項目的根目錄同時在 import 語句中使用全限定名蚊伞。

使用 proto2 類型

可以在 proto3 中導(dǎo)入 proto2 定義的消息類型席赂,反之亦然。然而时迫,proto2 中的枚舉不能直接用在 proto3 語法中(但導(dǎo)入到 proto2 中 proto3 定義的枚舉是可用的)颅停。

嵌套類型

可以在一個消息類型中定義和使用另一個消息類型,如下例所示—— Result 消息類型定義在了 SearchResponse 消息類型中:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

使用 Parent.Type 語法可以在父級消息類型外重用內(nèi)部定義消息類型:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

支持任意深度的嵌套:

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;
    }
  }
}

消息類型的更新

如果現(xiàn)有的消息類型不再滿足您的所有需求——例如掠拳,需要擴展一個字段——同時還要繼續(xù)使用已有代碼癞揉,別慌! 在不破壞任何現(xiàn)有代碼的情況下更新消息類型非常簡單溺欧。僅僅遵循如下規(guī)則即可:

  • 不要修改任何已有字段的字段編號
  • 若是添加新字段喊熟,舊代碼序列化的消息仍然可以被新代碼所解析。應(yīng)該牢記新元素的默認值以便于新代碼與舊代碼序列化的消息進行交互姐刁。類似的芥牌,新代碼序列化的消息同樣可以被舊代碼解析:舊代碼解析時會簡單的略過新字段。參考未知字段獲取詳細信息龙填。
  • 字段可被移除胳泉,只要不再使用移除字段的字段編號即可⊙乙牛可能還會對字段進行重命名扇商,或許是增加前綴 OBSOLETE_ ,或保留字段編號以保證后續(xù)不能重用該編號宿礁。
  • int32案铺, uint32int64梆靖, uint64控汉, 和 bool 是完全兼容的——意味著可以從這些字段其中的一個更改為另一個而不破壞前后兼容性。若解析出來的數(shù)值與相應(yīng)的類型不匹配返吻,會采用與 C++ 一致的處理方案(例如姑子,若將 64 位整數(shù)當做 32 位進行讀取,則會被轉(zhuǎn)換為 32 位)测僵。
  • sint32sint64 相互兼容但不與其他的整型兼容街佑。
  • string and bytes 在合法 UTF-8 字節(jié)前提下也是兼容的谢翎。
  • 嵌套消息與 bytes 在 bytes 包含消息編碼版本的情況下也是兼容的。
  • fixed32sfixed32 兼容沐旨, fixed64sfixed64兼容森逮。
  • enumint32uint32磁携, int64褒侧,和 uint64 兼容(注意若值不匹配會被截斷)。但要注意當客戶端反序列化消息時會采用不同的處理方案:例如谊迄,未識別的 proto3 枚舉類型會被保存在消息中闷供,但是當消息反序列化時如何表示是依賴于編程語言的。整型字段總是會保持其的值鳞上。
  • 將一個單獨值更改為新 oneof 類型成員之一是安全和二進制兼容的这吻。 若確定沒有代碼一次性設(shè)置多個值那么將多個字段移入一個新 oneof 類型也是可行的。將任何字段移入已存在的 oneof 類型是不安全的篙议。

未知字段

未知字段是解析結(jié)構(gòu)良好的 protocol buffer 已序列化數(shù)據(jù)中的未識別字段的表示方式唾糯。例如,當舊程序解析帶有新字段的數(shù)據(jù)時鬼贱,這些新字段就會成為舊程序的未知字段移怯。

本來,proto3 在解析消息時總是會丟棄未知字段这难,但在 3.5 版本中重新引入了對未知字段的保留機制以用來兼容 proto2 的行為舟误。在 3.5 或更高版本中,未知字段在解析時會被保留同時也會包含在序列化結(jié)果中姻乓。

Any 類型

Any 類型允許我們將沒有 .proto 定義的消息作為內(nèi)嵌類型來使用嵌溢。一個 Any 包含一個類似 bytes 的任意序列化消息,以及一個 URL 來作為消息類型的全局唯一標識符蹋岩。要使用 Any 類型赖草,需要導(dǎo)入 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 剪个。

不同的語言實現(xiàn)會支持運行時的助手函數(shù)來完成類型安全地 Any 值的打包和拆包工作——例如秧骑,Java 中,Any 類型會存在特定的 pack()unpack() 訪問器扣囊,而 C++ 中會是 PackFrom()UnpackTo() 方法:

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

當前處理 Any 類型的運行庫正在開發(fā)中

若你已經(jīng)熟悉了 proto2 語法乎折,Any 類型的位于 extensions 部分。

Oneof

若一個含有多個字段的消息同時大多數(shù)情況下一次僅會設(shè)置一個字段侵歇,就可以使用 oneof 特性來強制該行為同時節(jié)約內(nèi)存骂澄。

Oneof 字段除了全部字段位于 oneof 共享內(nèi)存以及大多數(shù)情況下一次僅會設(shè)置一個字段外與常規(guī)字段類似。對任何oneof 成員的設(shè)置會自動清除其他成員惕虑》爻澹可以通過 case()WhichOneof() 方法來檢測 oneof 中的哪個值被設(shè)置了士修,這個需要基于所選的編程語言。

使用 oneof

使用 oneof 關(guān)鍵字在 .proto 文件中定義 oneof樱衷,同時需要跟隨一個 oneof 的名字,就像本例中的 test_oneof

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

然后將字段添加到 oneof 的定義中酒唉【毓穑可以增加任意類型的字段,但不能使用 repeated 字段痪伦。

在生成的代碼中侄榴,oneof 字段和常規(guī)字段一致具有 getters 和 setters 。同時也會獲得一個方法以用于檢測哪個值被設(shè)置了网沾。更多所選編程語言中關(guān)于 oneof 的 API 可以參考 API reference 癞蚕。

Oneof 特性

  • 設(shè)置 oneof 的一個字段會清除其他字段。因此入設(shè)置了多次 oneof 字段辉哥,僅最后設(shè)置的字段生效桦山。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // 會清理 name 字段
CHECK(!message.has_name());
  • 若解析器在解析得到的數(shù)據(jù)時碰到了多個 oneof 的成員,最后一個碰到的是最終結(jié)果醋旦。
  • oneof 不能是 repeated恒水。
  • 反射 API 可作用于 oneof 字段。
  • 若將一個 oneof 字段設(shè)為了默認值(就像為 int32 類型設(shè)置了 0 )饲齐,那么 oneof 字段會被設(shè)置為 "case"钉凌,同時在序列化編碼時使用。
  • 若使用 C++ 捂人,確認代碼不會造成內(nèi)存崩潰御雕。以下的示例代碼就會導(dǎo)致崩潰,因為 sub_message 在調(diào)用 set_name() 時已經(jīng)被刪除了滥搭。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // 會刪除 sub_message
sub_message->set_...            // 此處會崩潰
  • 同樣在 C++ 中酸纲,若 Swap() 兩個 oneof 消息,那么消息會以另一個消息的 oneof 的情況:下例中论熙,msg1會是 sub_message1msg2 中會是 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());

向后兼容問題

在添加或刪除 oneof 字段時要當心。若檢測到 oneof 的值是 None/NOT_SET脓诡,這意味著 oneof 未被設(shè)置或被設(shè)置為一個不同版本的 oneof 字段无午。沒有方法可以區(qū)分,因為無法確定一個未知字段是否是 oneof 的成員祝谚。

標記重用問題

  • 移入或移出 oneof 字段: 消息序列化或解析后宪迟,可能會丟失一些信息(某些字段將被清除)。然而交惯,可以安全地將單個字段移入新的 oneof 中次泽,同樣若確定每次操作只有一個字段被設(shè)置則可以移動多個字段穿仪。
  • 刪除一個 oneof 字段并又將其加回: 消息序列化和解析后,可能會清除當前設(shè)置的 oneof 字段意荤。
  • 拆分或合并 oneof:這與移動常規(guī)字段有類似的問題啊片。

Map 映射表

若需要創(chuàng)建關(guān)聯(lián)映射表作為定義的數(shù)據(jù)的一部分,protocol buffers 提供了方便的快捷語法:

map<key_type, value_type> map_field = N;

key_type 處可以是整型或字符串類型(其實是除了 float 和 bytes 類型外任意的標量類型)玖像。注意枚舉不是合法的 key_type 紫谷。value_type 是除了 map 外的任意類型。

例如捐寥,若需要創(chuàng)建每個項目與一個字符串 key 相關(guān)聯(lián)的映射表笤昨,可以采用下面的定義:

map<string, Project> projects = 3;
  • 映射表字段不能為 repeated
  • 映射表的編碼和迭代順序是未定義的,因此不能依賴映射表元素的順序來操作握恳。
  • 當基于 .proto 生成文本格式時瞒窒,映射表的元素基于 key 來排序。數(shù)值型的 key 基于數(shù)值排序乡洼。
  • 當解析或合并時崇裁,若出現(xiàn)沖突的 key 以最后一個 key 為準。當從文本格式解析時束昵,若 key 沖突則會解析失敗寇壳。
  • 若僅僅指定了映射表中某個元素的 key 而沒有指定 value,當序列化時的行為是依賴于編程語言妻怎。在 C++壳炎,Java,和 Python 中使用類型的默認值來序列化逼侦,但在有些其他語言中可能不會序列化任何東西匿辩。

生成的映射表 API 當前可用于全部支持 proto3 的編程語言。在 API reference 中可以獲取更多關(guān)于映射表 API 的內(nèi)容榛丢。

向后兼容問題

映射表語法與以下代碼是對等的铲球,因此 protocol buffers 的實現(xiàn)即使不支持映射表也可以正常處理數(shù)據(jù):

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

repeated MapFieldEntry map_field = N;

任何支持映射表的 protocol buffers 實現(xiàn)都必須同時處理和接收上面代碼的數(shù)據(jù)定義。

可以在 .proto 文件中使用 package 指示符來避免 protocol 消息類型間的命名沖突晰赞。

package foo.bar;
message Open { ... }

這樣在定義消息的字段類型時就可以使用包指示符來完成:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

包指示符的處理方式是基于編程語言的:

  • C++ 中生成的類位于命名空間中稼病。例如,Open 會位于命名空間 foo::bar 中掖鱼。
  • Java 中然走,使用 Java 的包,除非在 .proto 文件中使用 option java_pacakge 做成明確的指定戏挡。
  • Python 中芍瑞,package 指示符被忽略,這是因為 Python 的模塊是基于文件系統(tǒng)的位置來組織的褐墅。
  • Go 中拆檬,作為 Go 的包名來使用洪己,除非在 .proto 文件中使用 option java_pacakge 做成明確的指定。
  • Ruby 中竟贯,生成的類包裹于 Ruby 的命名空間中答捕,還要轉(zhuǎn)換為 Ruby 所需的大小寫風(fēng)格(首字母大寫;若首字符不是字母屑那,則使用 PB_ 前綴)噪珊。例如,Open 會位于命名空間 Foo::Bar 中齐莲。
  • C# 中作為命名空間來使用,同時需要轉(zhuǎn)換為 PascalCase 風(fēng)格磷箕,除非在 .proto 使用 option csharp_namespace 中明確的指定选酗。例如,Open 會位于命名空間 Foo.Bar 中岳枷。

包和名稱解析

protocol buffer 中類型名稱解析的工作機制類似于 C++ :先搜索最內(nèi)層作用域芒填,然后是次內(nèi)層,以此類推空繁,每個包被認為是其外部包的內(nèi)層殿衰。前導(dǎo)點(例如,.foo.bar.Baz)表示從最外層作用域開始盛泡。

protocol buffer 編譯器會解析導(dǎo)入的 .proto 文件中的全部類型名稱闷祥。基于編程語言生成的代碼也知道如何去引用每種類型傲诵,即使編程語言有不同的作用域規(guī)則凯砍。

定義服務(wù)

若要在 RPC (Remote Procedure Call,遠程過程調(diào)用)系統(tǒng)中使用我們定義的消息類型拴竹,則可在 .proto 文件中定義這個 RPC 服務(wù)接口悟衩,同時 protocol buffer 編譯器會基于所選編程語言生成該服務(wù)接口代碼。例如栓拜,若需要定義一個含有可以接收 SearchRequest 消息并返回 SearchResponse 消息方法的 RPC 服務(wù)座泳,可以在 .proto 文件中使用如下代碼定義:

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

最直接使用 protocal buffer 的 RPC 系統(tǒng)是 gRPC :一款 Google 開源,語言和平臺無關(guān)的 RPC 系統(tǒng)幕与。gRPC 對 protocol buffer 的支持非常好同時允許使用特定的 protocol buffer 編譯器插件來基于 .proto 文件生成相關(guān)的代碼挑势。

若不想使用 gRPC,同樣可以在自己的 RPC 實現(xiàn)上使用 protocol buffer啦鸣⊙Τ埽可以在 Proto2 Language Guide 處獲得更多關(guān)于這方面的信息。

同樣也有大量可用的第三方使用 protocol buffer 的項目赏陵。對于我們了解的相關(guān)項目列表饼齿,請參考 third-party add-ons wiki page 饲漾。

JSON 映射

Proto3 支持 JSON 的規(guī)范編碼,這使得系統(tǒng)間共享數(shù)據(jù)變得更加容易缕溉。下表中考传,將逐類型地描述這些編碼。

若 JSON 編碼中不存在某個值或者值為 null证鸥,當將其解析為 protocol buffer 時會解析為合適的默認值僚楞。若 procol buffer 中使用的是字段的默認值,則默認情況下 JSON 編碼會忽略該字段以便于節(jié)省空間枉层。實現(xiàn)上應(yīng)該提供一個選項以用來將具有默認值的字段生成在 JSON 編碼中泉褐。

proto3 JSON JSON 示例 說明
message object {"fooBar": v, "g": null,…} 生成 JSON 對象。消息字段名映射為對象的 lowerCamelCase(譯著:小駝峰) 的 key鸟蜡。若指定了 json_name 選項膜赃,則使用該選項值作為 key。解析器同時支持 lowerCamelCase 名稱(或 json_name 指定名稱)和原始 proto 字段名稱揉忘。全部類型都支持 null 值跳座,是當做對應(yīng)類型的默認值來對待的。
enum string "FOO_BAR" 使用 proto 中指定的枚舉值的名稱泣矛。解析器同時接受枚舉名稱和整數(shù)值疲眷。
map<K,V> object `{"k": v, …} 所有的 key 被轉(zhuǎn)換為字符串類型。
repeated V array [v, …] null 被解釋為空列表 []您朽。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON 值是使用標準邊界 base64 編碼的字符串狂丝。不論標準或 URL 安全還是攜帶邊界與否的 base64 編碼都支持。
int32, fixed32, uint32 number 1, -10, 0 JSON 值是 10 進制數(shù)值哗总。數(shù)值或字符串都可以支持美侦。
int64, fixed64, uint64 string "1", "-10" JSON 值是 10 進制字符串。數(shù)值或字符串都支持魂奥。
float, double number 1.1, -10.0, 0, "NaN","Infinity" JSON 值是數(shù)值或特定的字符串之一:"NaN"菠剩,"Infinity" 和 "-Infinity" 。數(shù)值和字符串都支持耻煤。指數(shù)表示法同樣支持具壮。
Any object {"@type": "url", "f": v, … } 若 Any 類型包含特定的 JSON 映射值,則會被轉(zhuǎn)換為下面的形式: {"@type": xxx, "value": yyy}哈蝇。否則棺妓,會被轉(zhuǎn)換到一個對象中,同時會插入一個 "@type" 元素用以指明實際的類型炮赦。
Timestamp string "1972-01-01T10:00:20.021Z" 采用 RFC 3339 格式怜跑,其中生成的輸出總是 Z規(guī)范的,并使用 0、3性芬、6 或 9 位小數(shù)峡眶。除 “Z” 以外的偏移量也可以。
Duration string "1.000340012s", "1s" 根據(jù)所需的精度植锉,生成的輸出可能會包含 0辫樱、3、6 或 9 位小數(shù)俊庇,以 “s” 為后綴狮暑。只要滿足納秒精度和后綴 “s” 的要求,任何小數(shù)(包括沒有)都可以接受辉饱。
Struct object { … } 任意 JSON 對象搬男。參見 struct.proto.
Wrapper types various types 2, "2", "foo", true,"true", null, 0, … 包裝器使用與包裝的原始類型相同的 JSON 表示,但在數(shù)據(jù)轉(zhuǎn)換和傳輸期間允許并保留 null彭沼。
FieldMask string "f.fooBar,h" 參見field_mask.proto缔逛。
ListValue array [foo, bar, …]
Value value Any JSON value
NullValue null JSON null
Empty object {} 空 JSON 對象

JSON 選項

proto3 的 JSON 實現(xiàn)可以包含如下的選項:

  • 省略使用默認值的字段:默認情況下,在 proto3 的 JSON 輸出中省略具有默認值的字段溜腐。該實現(xiàn)可以使用選項來覆蓋此行為,來在輸出中保留默認值字段瓜喇。
  • 忽略未知字段:默認情況下挺益,proto3 的 JSON 解析器會拒絕未知字段,同時提供選項來指示在解析時忽略未知字段乘寒。
  • 使用 proto 字段名稱代替 lowerCamelCase 名稱: 默認情況下望众,proto3 的 JSON 編碼會將字段名稱轉(zhuǎn)換為 lowerCamelCase(譯著:小駝峰)形式。該實現(xiàn)提供選項可以使用 proto 字段名代替伞辛。Proto3 的 JSON 解析器可同時接受 lowerCamelCase 形式 和 proto 字段名稱烂翰。
  • 枚舉值使用整數(shù)而不是字符串表示: 在 JSON 編碼中枚舉值是使用枚舉值名稱的。提供了可以使用枚舉值數(shù)值形式來代替的選項蚤氏。

選項

.proto 文件中的單個聲明可以被一組選項來設(shè)置甘耿。選項不是用來更改聲明的含義,但會影響在特定上下文下的處理方式竿滨。完整的選項列表定義在 google/protobuf/descriptor.proto 中佳恬。

有些選項是文件級的,意味著可以卸載頂級作用域于游,而不是在消息毁葱、枚舉、或服務(wù)的定義中贰剥。有些選項是消息級的倾剿,意味著需寫在消息的定義中。有些選項是字段級的蚌成,意味著需要寫在字段的定義內(nèi)前痘。選項還可以寫在枚舉類型凛捏,枚舉值,服務(wù)類型际度,和服務(wù)方法上葵袭;然而,目前還沒有任何可用于以上位置的選項乖菱。

下面是幾個最常用的選項:

  • java_package (文件選項):要用在生成 Java 代碼中的包坡锡。若沒有在 .proto 文件中對 java_package 選項做設(shè)置,則會使用 proto 作為默認包(在 .proto 文件中使用 "package" 關(guān)鍵字設(shè)置)窒所。 然而鹉勒,proto 包通常不是合適的 Java 包,因為 proto 包通常不以反續(xù)域名開始吵取。若不生成 Java 代碼禽额,則此選項無效。
option java_package = "com.example.foo";
  • java_multiple_files (文件選項):導(dǎo)致將頂級消息皮官、枚舉脯倒、和服務(wù)定義在包級,而不是在以 .proto 文件命名的外部類中捺氢。
option java_multiple_files = true;
  • java_outer_classname(文件選項):想生成的最外層 Java 類(也就是文件名)藻丢。若沒有在 .proto 文件中明確指定 java_outer_classname 選項,類名將由 .proto 文件名轉(zhuǎn)為 camel-case 來構(gòu)造(因此 foo_bar.proto 會變?yōu)?FooBar.java)摄乒。若不生成 Java 代碼悠反,則此選項無效。
option java_outer_classname = "Ponycopter";
  • optimize_for (文件選項): 可被設(shè)為 SPEED馍佑, CODE_SIZE斋否,或 LITE_RUNTIME。這會影響 C++ 和 Java 代碼生成器(可能包含第三方生成器) 的以下幾個方面:
  • SPEED (默認): protocol buffer 編譯器將生成用于序列化拭荤、解析和消息類型常用操作的代碼茵臭。生成的代碼是高度優(yōu)化的。
  • CODE_SIZE :protocol buffer 編譯器將生成最小化的類舅世,并依賴于共享的笼恰、基于反射的代碼來實現(xiàn)序列化、解析和各種其他操作歇终。因此社证,生成的代碼將比 SPEED 模式小的多,但操作將變慢评凝。類仍將實現(xiàn)與 SPEED 模式相同的公共 API追葡。這種模式在處理包含大量 .proto 文件同時不需要所有操作都要求速度的應(yīng)用程序中最有用。
  • LITE_RUNTIME :protocol buffer 編譯器將生成僅依賴于 “l(fā)ite” 運行庫的類(libprotobuf-lite 而不是libprotobuf)。lite 運行時比完整的庫小得多(大約小一個數(shù)量級)宜肉,但會忽略某些特性匀钧,比如描述符和反射。這對于在受限平臺(如移動電話)上運行的應(yīng)用程序尤其有用谬返。編譯器仍然會像在 SPEED 模式下那樣生成所有方法的快速實現(xiàn)之斯。生成的類將僅用每種語言實現(xiàn) MessageLite 接口,該接口只提供 Message 接口的一個子集遣铝。
option optimize_for = CODE_SIZE;    
  • cc_enable_arenas(文件選項):為生成的 C++ 代碼啟用 arena allocation 佑刷。
  • objc_class_prefix (文件選項): 設(shè)置當前 .proto 文件生成的 Objective-C 類和枚舉的前綴。沒有默認值酿炸。你應(yīng)該使用 recommended by Apple 的 3-5 個大寫字母作為前綴瘫絮。注意所有 2 個字母前綴都由 Apple 保留。
  • deprecated (字段選項):若設(shè)置為 true填硕, 指示該字段已被廢棄麦萤,新代碼不應(yīng)使用該字段。在大多數(shù)語言中扁眯,這沒有實際效果壮莹。在 Java 中,這變成了一個 @Deprecated 注釋姻檀。將來命满,其他語言的代碼生成器可能會在字段的訪問器上生成棄用注釋,這將導(dǎo)致在編譯試圖使用該字段的代碼時發(fā)出警告施敢。如果任何人都不使用該字段周荐,并且您希望阻止新用戶使用它狭莱,那么可以考慮使用保留語句替換字段聲明僵娃。
int32 old_field = 6 [deprecated=true];

自定義選項

protocol buffer 還允許使用自定義選項。大多數(shù)人都不需要此高級功能腋妙。若確認要使用自定義選項默怨,請參閱 Proto2 Language Guide 了解詳細信息。注意使用 extensions 來創(chuàng)建自定義選項少态,只允許用于 proto3 中说订。

生成自定義類

若要生成操作 .proto 文件中定義的消息類型的 Java送讲、Python、C++痕檬、Go、Ruby送浊、Objective-C 或 C# 代碼梦谜,需要對 .proto 文件運行 protocol buffer 編譯器 protoc。若還沒有安裝編譯器,請 download the package 并依據(jù) README 完成安裝唁桩。對于 Go 闭树,還需要為編譯器安裝特定的代碼生成器插件:可使用 GitHub 上的 golang/protobuf 庫。

Protocol buffer 編譯器的調(diào)用方式如下:

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_PATHimport 指令檢索 .proto 文件的目錄荒澡。若未指定报辱,使用當前目錄。多個導(dǎo)入目錄可以通過多次傳遞 --proto_path 選項實現(xiàn)单山;這些目錄會依順序檢索碍现。 -I=*IMPORT_PATH* 可作為 --proto_path 的簡易格式使用。

  • 可以提供一個或多個輸出指令:

  • --cpp_outDST_DIR目錄 生成 C++ 代碼饥侵。參閱 C++ generated code reference 獲取更多信息鸵赫。

  • --java_outDST_DIR目錄 生成 Java 代碼。參閱 Java generated code reference 獲取更多信息躏升。

  • --python_outDST_DIR目錄 生成 Python代碼辩棒。參閱 Python generated code reference 獲取更多信息。

  • --go_outDST_DIR目錄 生成 Go 代碼膨疏。參閱 Go generated code reference 獲取更多信息一睁。

  • --ruby_outDST_DIR目錄 生成 Ruby 代碼。 coming soon!

  • --objc_outDST_DIR目錄 生成 Objective-C 代碼佃却。參閱 Objective-C generated code reference 獲取更多信息者吁。

  • --csharp_outDST_DIR目錄 生成 C# 代碼。參閱 C# generated code reference 獲取更多信息饲帅。

  • --php_outDST_DIR目錄 生成 PHP代碼复凳。參閱 PHP generated code reference 獲取更多信息。

作為額外的便利灶泵,若 DST_DIR 以 .zip.jar 結(jié)尾育八,編譯器將會寫入給定名稱的 ZIP 格式壓縮文件,.jar 還將根據(jù) Java JAR 的要求提供一個 manifest 文件赦邻。請注意髓棋,若輸出文件已經(jīng)存在,它將被覆蓋惶洲;編譯器還不夠智能按声,無法將文件添加到現(xiàn)有的存檔中。

  • 必須提供一個或多個 .proto 文件作為輸入恬吕∏┰颍可以一次指定多個 .proto 文件。雖然這些文件是相對于當前目錄命名的铐料,但是每個文件必須駐留在 IMPORT_PATHs 中渐裂,以便編譯器可以確定它的規(guī)范名稱侨颈。

關(guān)注 紅牛慕課,發(fā)送 proto3 獲取該文檔的 PDF 版本芯义。


subscribe-proto3.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哈垢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子扛拨,更是在濱河造成了極大的恐慌耘分,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绑警,死亡現(xiàn)場離奇詭異求泰,居然都是意外死亡,警方通過查閱死者的電腦和手機计盒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門渴频,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人北启,你說我怎么就攤上這事卜朗。” “怎么了咕村?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵场钉,是天一觀的道長。 經(jīng)常有香客問我懈涛,道長逛万,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任批钠,我火速辦了婚禮宇植,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘埋心。我一直安慰自己指郁,他們只是感情好,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布踩窖。 她就那樣靜靜地躺著坡氯,像睡著了一般晨横。 火紅的嫁衣襯著肌膚如雪洋腮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天手形,我揣著相機與錄音啥供,去河邊找鬼。 笑死库糠,一個胖子當著我的面吹牛伙狐,可吹牛的內(nèi)容都是我干的涮毫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贷屎,長吁一口氣:“原來是場噩夢啊……” “哼罢防!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起唉侄,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咒吐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后属划,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恬叹,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年同眯,在試婚紗的時候發(fā)現(xiàn)自己被綠了绽昼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡须蜗,死狀恐怖硅确,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情明肮,我是刑警寧澤疏魏,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站晤愧,受9級特大地震影響大莫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜官份,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一只厘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舅巷,春花似錦羔味、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至飒房,卻和暖如春搁凸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狠毯。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工护糖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嚼松。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓嫡良,卻偏偏與公主長得像锰扶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子寝受,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容