天天看點

深入Protobuf源碼-Descriptor、Message、RPC架構

對非optimize_for為lite_runtime的proto檔案,protobuf編譯器會在編譯出的java代碼檔案末尾添加一個filedescriptor靜态字段以描述該proto檔案定義時的所有中繼資料資訊、為每個message對象定義一個descriptor靜态字段以描述該message定義時的中繼資料資訊、為每個message對象定義一個fieldaccessortable靜态字段用于使用反射讀取/設定某個字段的值等(以提供generatedmessage中方法的反射實作):    

private static descriptor inter-nal_static_levin_protobuf_result_descriptor;

private static fieldaccessortable inter-nal_static_levin_protobuf_result_fieldaccessortable;

private static descriptor inter-nal_static_levin_protobuf_searchresponse_descriptor;

private static fieldaccessortable inter-nal_static_levin_protobuf_searchresponse_fieldaccessortable;

private static filedescriptor descriptor;

在protobuf中存在多種類型的中繼資料描述類:

1.     filedescriptor:對一個proto檔案的描述,它包含檔案名、包名、選項(如java_package、java_outer_classname等)、檔案中定義的所有message、檔案中定義的所有enum、檔案中定義的所有service、檔案中所有定義的extension、檔案中定義的所有依賴檔案(import)等。在filedescriptor中還存在一個descriptorpool執行個體,它儲存了所有的dependencies(依賴檔案的filedescriptor)、name到genericdescriptor的映射、字段到fielddescriptor的映射、枚舉項到enumvaluedescriptor的映射,進而可以從該descriptorpool中查找相關的資訊,因而可以通過名字從filedescriptor中查找message、enum、service、extensions等。

2.   descriptor:對一個message定義的描述,它包含該message定義的名字、所有字段、内嵌message、内嵌enum、關聯的filedescriptor等。可以使用字段名或字段号查找fielddescriptor。

3.   fielddescriptor:對一個字段或擴充字段定義的描述,它包含字段名、字段号、字段類型、字段定義(required/optional/repeated/packed)、預設值、是否是擴充字段以及和它關聯的descriptor/filedescriptor等。

4.   enumdescriptor:對一個enum定義的描述,它包含enum名、全名、和它關聯的filedescriptor。可以使用枚舉項或枚舉值查找enumvaluedescriptor。

5.   enumvaluedescriptor:對一個枚舉項定義的描述,它包含枚舉名、枚舉值、關聯的enumdescriptor/filedescriptor等。

6.   servicedescriptor:對一個service定義的描述,它包含service名、全名、關聯的filedescriptor等。

7.   methoddescriptor:對一個在service中的method的描述,它包含method名、全名、參數類型、傳回類型、關聯的filedescriptor/servicedescriptor等。

最後,protobuf編譯生成的代碼末尾還有一個descriptordata字元串數組,它是序列化後的filedescriptorproto資料,在靜态初始化塊中可以調用filedescriptor.internalbuildgeneratedfilefrom()方法構造整個filedescriptor執行個體,在完成filedescriptor的構造後,還會回調傳入的internaldescriptorassigner執行個體以初始化其他的靜态字段,如以上提到的所有的靜态字段。

在protobuf中descriptor的類圖:

深入Protobuf源碼-Descriptor、Message、RPC架構

序列化和反序列化是protobuf最基礎的架構,它使用messagelite/message接口來抽象一個可序列化的執行個體,并且使用builder從位元組數組或輸入位元組流中建構messagelite/message執行個體,messagelite和message内部都定義了自己的builder類,他們個字繼承自messageliteorbuilder以及messageorbuiler,它們定義了messagelite/message和它們各自builder類的共同接口。

messageliteorbuilder接口隻定義了messagelite和messagelite.builder兩個接口共有的兩個方法:getdefaultinstancefortype()方法擷取一個目前還未初始化的目前message執行個體(沒有字段被指派,因而所有字段傳回預設值,對repeat字段傳回空,在目前protobuf 2.5.0的實作中,它傳回的是一個單例,和每個生成的靜态方法getdefaultinstance()傳回相同的執行個體);isinitialized()方法用來判斷是否所有required字段已經被指派。messagelite接口中定義了兩個writeto()方法分别将目前執行個體序列化并寫入輸出位元組流中,而另一個writedelimitedto()方法則在寫入之前将目前執行個體的總長度寫入輸出位元組流中(以可變長32位int編碼方式),進而可以同時向一個輸出位元組流中寫入多個message執行個體;messagelite中還定義了擷取目前messagelite在序列化成位元組流後的總位元組數的方法getserializedsize(),兩個直接傳回位元組數組的tobytearray()/tobytestring()方法,以及擷取它的parser執行個體(getparserfortype())和傳回它的builder執行個體(tobuilder()-建立一個新的builder執行個體/newbuilderfortype()-用目前messagelite類初始化一個新的builder執行個體并傳回)方法。其中builder接口用于從位元組流或位元組數組中解析并構造messagelite對象(各種版本的mergefrom()方法,如果發送端寫入了messagelite位元組長度,則使用mergedelimitedfrom()方法),最後builder使用build()方法構造messagelite對象,此時如果有required字段還未被設定,會抛出uninitializedmessageexception,為了避免抛出異常,可以使用buildpartial()方法;另外builder還定義了clone()和clear()方法;在生成的每個message對象中都定義了一個newbuilder()靜态方法,一般使用該靜态方法初始化一個builder執行個體。parser接口也定義了各個版本的parsefrom()/parsepartialfrom()/parsedelimitedfrom()/parsepartialdelimitedfrom()方法用來從位元組數組或位元組流中解析出message執行個體,在生成的代碼中,builder的實作直接調用parser實作類中的方法。

在大部分情況下,messagelite已經能完成所有的序列化和反序列化操作了,特别是一些資源有限額手持裝置,它如果運作整個protobuf庫會顯得太耗資源;可以在.proto檔案中加入一下指令來告訴protobuf編譯器隻需要生成實作messagelite的類:

option optimize_for = lite_runtime

然而對一般的server程式來說,我們并不在乎這點資源的損耗,因而會選擇實作message接口,它相比messagelite,添加了descriptors相關的支援,即支援使用fielddescriptor來建構message.builder執行個體并最終建構message執行個體。

messageorbuilder接口繼承自messageliteorbuilder接口,它定義了message和message.builder共有的接口,即添加了descriptor、fielddescriptor等相關的擴充。由于實作message和message.builder接口的類儲存了所有message定義時具有的資訊(檔案名、包名、字段清單等,使用各種descriptor類來抽象),因而我們可以使用message/message.builder類擷取到更多的資訊,如一個message/message.builder沒有指派所有required的字段,可以使用findinitializationerrors()方法來擷取所有未指派的字段清單(字段的全路徑名,getinitializationerrorstring()是這個清單的字元串形式表達,為了提升性能,建議使用isinitialized()方法先做初步判斷,因為它更快);另外在messageorbuilder中還定義了目前message對應的descriptor執行個體:getdescriptorfortype()方法,擷取所有已經指派的fielddescriptor到其值的一個map:getallfields(),通過fielddescriptor取得其值:getfield(),判斷一個字段是否已經被指派:hasfield(),擷取repeated字段的count:getrepeatedfieldcount(),通過fielddescriptor以及index擷取repeated字段在index處的值:getrepeatedfield(),擷取未知的字段:getunknownfields()。message接口除了繼承自messageorbuilder接口的方法,并沒有定義多餘的方法,隻是添加了equals、hashcode、tostring方法的定義。而message.builder接口除了繼承自messageorbuilder接口以外,它還定義了基于fielddescriptor的方法,如通過fielddescriptor建立/擷取builder執行個體:newbuilderforfileld()/getfieldbuilder(),通過fielddescriptor設定/清除字段的值:setfield()/clearfield()/setrepeatedfield()/addrepeatedfield(),以及設定unknownfields:setunknownfields()/mergeunknownfields()。

messagelite/message類圖如下:

深入Protobuf源碼-Descriptor、Message、RPC架構

除了序列化架構,protobuf還定義了一套簡單的rpc架構。之是以說簡單是因為它定義的service層接口的協定,而沒有具體和傳輸相關的實作,而隻是将傳輸相關的邏輯抽象成rpcchannel和blockingrpcchannel分别用于表示同步和一步方式的service方法調用,而至于底層用什麼樣的協定和架構,由使用者自己決定并實作。

所謂rpc架構,從使用者角度上最基本的就是定義用戶端和伺服器端的協定,即伺服器端暴露出什麼樣的接口供用戶端調用,這個接口定義了伺服器在一個host的某個(些)端口上接收某些請求資料,并期望能傳回的響應。其中伺服器和端口号屬于傳輸實作的範疇,protobuf隻是用rpcchannel/blockingrpcchannel的概念做了抽象,而沒有給出具體實作;而接收某個請求資料以及期待的響應資料,在protobuf使用service/blockingservice抽象來定義,并且這也是protobuf中rpc架構的定義部分,其中service和rpcchannel共同構成異步方式的rpc架構,而blockingservice和blockingrpcchannel共同構成了同步(阻塞)方式的rpc架構。

從底層實作的角度,一個rpc調用就是用戶端發送一些請求資料給伺服器,伺服器解析并處理這些請求資料,然後将響應資料傳回給用戶端。為了隐藏内部實作細節,提升寫代碼的效率,rpc将這一過程封裝成方法調用,即不同的請求用不同的方法表達,這就是protobuf中rpc的定義。在protobuf中,定義一個prc接口比較簡單:首先開啟rpc功能,然後用service關鍵字定義一個接口,在接口中使用rpc關鍵字定義一個方法,方法包含方法名、方法參數、傳回值,其中方法參數和傳回值都必須是一個message類型,并且隻能有一個:

option java_generic_services = true;

service myservice {

    rpc request(searchrequest) returns(searchresponse);

}

在protobuf編譯生成的代碼中,它會生成一個myservice抽象類實作了service接口,一般它隻是作為一個命名空間,它内部定義了兩個接口:interface和blockinginterface本别繼承自service接口和blockingservice接口,用于抽象異步和同步方式的rpc方法調用;這兩個接口有兩個實作類:stub和blockingstub,他們分别接收rpcchannel和blockingrpcchannel執行個體作為構造函數參數,可以使用myservice中的靜态方法newstub()和newblockingstub()方法擷取他們各自執行個體,他們主要用于用戶端的調用。在生成的request方法中,除了request本身的參數,還有一個rpccontroller參數,它用于處理在rpcchannel/blockingrpcchannel調用中的狀态處理,如錯誤處理等,使用它可以獲知此次調用是否出錯,錯誤資訊是什麼等。在myservice中還定義了兩個靜态方法newreflectiveservice/newreflectiveblockingservice,他們接收interface/blockinginterface執行個體,并傳回service/blockingservice的實作執行個體(暫時還沒有想到使用他們的場景)。

深入Protobuf源碼-Descriptor、Message、RPC架構

在myservice的rpc架構實作中,在伺服器端,實作myservice.interface/myservice.blockinginterface接口,然後将它注冊到對rpcchannel/blockingrpcchannel架構的實作中;在用戶端則建立一個rpcchannel/blockingrpcchannel執行個體,傳入myservice.newstub()/myservice.newblockingstub()方法擷取對應的執行個體,然後使用這個stub/blockingstub執行個體調用相應的方法即可。