1. 程式人生 > >grpc系列- protobuf詳解

grpc系列- protobuf詳解

Protocol Buffers 是一種與語言、平臺無關,可擴充套件的序列化結構化資料的方法,常用於通訊協議,資料儲存等等。相較於 JSON、XML,它更小、更快、更簡單,因此也更受開發人員的青眯。 ![](https://img2020.cnblogs.com/blog/342595/202012/342595-20201228170030370-1500430342.png) 基本語法 ``` syntax = “proto3”; package model; service MyServ { rpc Query(Request) returns(Reply); } message Student { int64 id = 1; string name = 2; int32 age = 3; } ``` 定義完 proto檔案後,生成相應語言的程式碼 ``` protoc --proto_path=. --go_out=plugins=grpc,paths=source_relative:. xxxx.proto ``` `--proto_path` 或者 `-I` 引數用以指定所編譯原始碼(包括直接編譯的和被匯入的 proto 檔案)的搜尋路徑 `--go_out` 引數之間用逗號隔開,最後用冒號來指定程式碼目錄架構的生成位置 ,`--go_out=plugins=grpc`引數來生成gRPC相關程式碼,如果不加`plugins=grpc`,就只生成`message`資料 eg:--go_out=plugins=grpc,paths=import:. 。注意一下 paths 引數,他有兩個選項,import 和 source_relative 。預設為 import ,代表按照生成的 go 程式碼的包的全路徑去建立目錄層級,source_relative 代表按照 proto 原始檔的目錄層級去建立 go 程式碼的目錄層級,如果目錄已存在則不用建立 `protoc`是通過外掛機制實現對不同語言的支援。比如 `--xxx_out` 引數,那麼protoc將首先查詢是否有內建的xxx外掛,如果沒有內建的xxx外掛那麼將繼續查詢當前系統中是否存在protoc-gen-xxx命名的可執行程式。 例如,生成 c++程式碼 ``` protoc -I . --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` --cpp_out=. *.proto ``` ## 匯入依賴的`proto`檔案 為了方便,會把公共的一些欄位放到一個`proto`檔案裡,如果有需要,就把這個`proto`檔案`impot`進去,比如,我現在的組織結構好下 ![](https://img2020.cnblogs.com/blog/342595/202012/342595-20201228171801715-1524033716.png) `common.proto` 檔案裡只有個簡單的`message` ``` syntax = "proto3"; package protos; option go_package = "protos"; option java_package = "com.proto"; message Result { string code = 1; string desc = 2; bytes data = 3; } ``` 目錄`api`裡`student_api.proto` 在這個檔案裡,我們匯入了`common.proto`,還有其他需要的檔案 ``` syntax = "proto3"; package api; option go_package = "protos/api"; option java_package = "com.proto.api"; import "protos/common.proto"; import "protos/model/students.proto"; import "google/protobuf/empty.proto"; service StudentSrv { rpc NewStudent(model.Student) returns (protos.Result); rpc StudentByID(QueryStudent) returns (QueryStudentResponse); rpc AllStudent(google.protobuf.Empty) returns(stream QueryStudentResponse); rpc StudentInfo(stream QueryStudent) returns(stream QueryStudentResponse); } message QueryStudent { int64 id = 1; } message QueryStudentResponse { repeated model.Student studentList = 1; } ``` 在執行`protoc`的時候,我們要指定這些需檔案的查詢路徑,在專案的根目錄裡執行`protoc`進行程式碼生成 ``` protoc -I=. --go_out=plugins=grpc:. --go_opt=paths=source_relative protos/api/*.proto ``` 上面的`-I`指定了當前目錄,就是說可以從當前目錄開始找`proto`檔案 ## protoc 生成了什麼 以 `student.proto`為例 ``` syntax = "proto3"; package model; option go_package = "protos/model"; option java_package = "com.proto.model"; message Student { int64 id = 1; string name = 2; int32 age = 3; } message StudentList { string class = 1; repeated Student students = 2; string teacher = 3; repeated int64 score = 4; } ``` 執行完`protoc`後,大概看一下生成的的`go`檔案 ``` type Student struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Age int32 `protobuf:"varint,3,opt,name=age,proto3" json:"age,omitempty"` } ``` `state` 儲存 proto檔案的反射資訊 `sizeCache`序列化的資料總長度 `unknownFields` 不能解析的欄位 剩下的欄位是我們`message`裡定義的資訊,主要看一下`tag`資訊 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`,說明這個欄位是protobuf的`varint`型別,`index`為`1` name為`id`,使用`proto3`協議 還有一個`byte`陣列的`file_protos_model_students_proto_rawDesc` ![](https://img2020.cnblogs.com/blog/342595/202012/342595-20201229150532116-827556817.png) 一眼看上去就有點蒙,這一坨是什麼?開源的好處就是,我可以很清楚的看清他是做什麼的, ![](https://img2020.cnblogs.com/blog/342595/202012/342595-20201230112850009-257671933.png) 這個`file_protos_model_students_proto_rawDesc`是`proto`裡資料的描述資訊。如 `proto`的路徑、包名稱,`message`資訊等等。 `file_protos_model_students_proto_rawDesc`描述資訊有什麼用呢? 當我們在執行`proto.Marshal`的時候,會對傳入的引數`Message`進行驗證,比如每個`message`欄位的`index`、資料型別,是否和`file_protos_model_students_proto_rawDesc`一致。如果不一致就說明是有問題的。 ## protobuf支援的資料型別 ![](https://img2020.cnblogs.com/blog/342595/202012/342595-20201230132157802-1560391435.png) `protobuf`目前支援這5種資料型別,還有2個是已經廢棄了。`protobuf`是語言無關的,也就是說,無論具體的語言支援哪些資料型別,在`marshal`的時候都要轉換成這幾種,在`unmarshal`的時候再轉換成具體語言的型別 我們把一個結構轉換成json ``` Student { Id: 1, Name: "孫悟空", Age: 300, } ``` ``` { "id": 1, "name": "孫悟空", "age": 300 } ``` 轉換成 `protobuf` 資料格式 ``` 1000 1 10010 1001 11100101 10101101 10011001 11100110 10000010 10011111 11100111 10101001 10111010 11000 10101100 10 ``` 轉換成十進位制 ``` 8 1 8 9 229 173 153 230 130 159 231 169 186 24 172 2 ``` `json`一眼就能看懂是什麼 ,`protobuf`資料格式看不明白,下面來解釋這些資料都是什麼。 ### index 和型別 先說一下第一個byte `1000` 這個表示的是欄位的`index`和型別, `protobuf` 把一個欄位的 index 和型別放在了一起 ``` (field_number << 3) | wire_type ``` 最後3個bit為型別,前面的bit為index `0000 1000` 首位為標識位,index為 1 後三位為wire_type:0(Varint型別)再比如 `10010` index: 2 wire_type: 2(Length-delimited型別) ### Varint型別 `Varint`資料型別,最高位(msb)標誌位,為1說明後面還有byte,0說明後面沒有byte,使用後面的7個Bit位儲存數值 `Id: 1` protobuf對應的資料是`0000 0001` 這個很好理解 `Age: 300` protobuf對應的資料是`1010 1100 0000 0010`,這個是怎麼計算的呢? ``` protobuf資料 1010 1100 0000 0010 去掉最高位 010 1100 000 0010 連線剩餘 0100101100 計算 256 + 32 + 8 + 4 = 300 ``` ### Length-delimited 型別 字串記憶體的表現形式`protobuf`一個漢字佔3個byte “孫悟空”記憶體的資料 ``` 11100101 10101101 10011001 11100110 10000010 10011111 11100111 10101001 10111010 ``` “孫悟空”前面的一個byte:`1001` 這個數值有什麼意義?對,字串長度 9 `Length-delimited`型別的資料第一個byte是資料的長度,後面是具體的資料資訊,資料大同小異