現如今微服務很流行,而微服務很有可能是使用不同語言進行構建的。而微服務之間通常需要相互通訊,所以微服務之間必須在以下幾個方面達成共識:
- 需要使用某種API
- 資料格式
- 錯誤的模式
- 負載均衡
- 。。。
現在最流行的一種API風格可能是REST,它主要是通過HTTP協議來傳輸JSON資料。
但是現在我們可以看看gRPC(https://grpc.io/),它來自Google,並且支援眾多主流的語言包括Go,Dart,C#,C/C++,Nodejs,Python等等。
下面就學習一下gRPC。
gRPC能解決哪些問題?
構建(Web)API是挺麻煩的,因為構建API時我們得考慮:
- 資料的格式是JSON、XML還是二進位制的;
- 端點地址以及GET還是POST等;
- 如何呼叫API以及對異常的處理規則;
- API的效率:一次呼叫讀取多少資料?是否太多了或太少了?太少的話可能會導致多次API的呼叫;
- 延遲;
- 擴充套件性,是否能支援成上千個客戶端
- 負載均衡
- 與其他語言的互操作性
- 如何處理身份認證、監控、日誌等等
以上這些問題據說gRPC都能解決。。?
再次介紹一下gRPC
之前說了gRPC來自Google,它是一個開源的框架;它同時也是Cloud Native Computation基金會(CNCF)的一部分,就像Docker和Kubernetes一樣。
gRPC允許你為RPC(Remote Procedure Call)定義請求和響應,然後gRPC會幫你處理一切剩餘問題。
它速度快,執行效率高,基於HTTP/2構建,低延遲,支援流,與開發語言無關,並且可以很簡單的插入身份認證、負載均衡、日誌和監控等功能。
RPC是啥
RPC是(Remote Procedure Call)遠端過程呼叫。
在客戶端程式碼使用RPC呼叫的時候,就像直接呼叫了服務端的一個函式一樣。
例如在伺服器端程式碼是這樣的:
而在“遙遠”的客戶端它是這樣呼叫伺服器端的邏輯的,就像呼叫本地方法一樣:
而實際上客戶端在呼叫這個方法的時候,是要走網路通訊的。
RPC它不是一個新的概念,很早它就出現了。但是它存在很多的問題。而gRPC它是對RPC一種非常簡潔的實現並且解決了很多RPC的問題。
如何學習gRPC?
首先,你得學習Protocol Buffers(https://developers.google.com/protocol-buffers/),簡單的說,它可以用來定義訊息和服務。
然後,你只需要實現服務即可,剩餘的gRPC程式碼將會自動為你生成。
.proto這個檔案可以適用於十幾種開發語言(包括服務端和客戶端),並且它允許你使用同一個框架來支援每秒百萬級以上的RPC呼叫。
gPRC使用的是合約優先的API開發模式,它預設使用Protocol buffers (protobuf) 作為介面設計語言(IDL),這個.proto檔案包括兩部分:
- gRPC服務的定義
- 服務端和客戶端之間傳遞的訊息
看一個官網的例子(protobuf):
在這裡定義了一個Greeter服務,它裡面定義了一個SayHello的rpc呼叫。SayHello會傳送HelloRequest這個訊息,接收HelloReply這個訊息。
為什麼使用Protocol Buffers?
因為:
- 它和開發語言無關
- 可以生成所有主流開發語言的程式碼
- 資料是二進位制格式的,序列化的效率高,Payload比較小
- 也很適合傳遞大量的資料
- 通過設定某些規則,是的API的進化也很簡單
Protocol Buffer
開發環境:
- IDE: VSCode
- VSCode的擴充套件外掛:vscode-proto3和Clang-Format這兩個擴充套件
- Windows還需要安裝Clang,Windows 64位系統的地址如下:Clang for Windows (64-bit);Mac:
brew install clang-format
。
第一個例子
選個資料夾,建立一個名叫first.proto的檔案:
1. 這行程式碼表示我們使用的是語法是proto3,之前還有一個proto2;如果你不寫這一行,那麼protocol buffer編譯器會認為你採用的是proto2。這個必須是檔案的第一個非空非註釋行。
2. 這裡是定義了一個訊息名稱為FirstMessage,型別是message。它裡面定義了三個欄位,它們都是標量型別(Scalar Type),你也可以定義複合型別,這個以後再說。
3. 是指欄位(Field)的型別
4. 欄位的名稱
5. 欄位的數值(也叫Tag),這個數字是唯一的。它們是用來在資訊格式裡識別你的欄位的,一旦該型別被使用了,那麼這個數字就不要再改變了。
標量型別
數值型
數值型有很多種形式:double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64。
根據需要選擇對應的數值型別。
布林型
bool型可以有True和False兩個值。
字串
string表示任意長度的文字,但是它必須包含的是UTF-8編碼或7位ASCII的文字,長度不可超過232。
位元組型
bytes可表示任意的byte陣列序列,但是長度也不可以超過232 ,最後是由你來決定如何解釋這些bytes。例如你可以使用這個型別來表示一個圖片。
做個例子
可以自己做一個例子,需求是這樣的:這個資訊表示的是一個人Person,使用proto3語法,欄位如下:ID,姓名,身高,體重,頭像,電子郵件,郵件是否已驗證。
這個應該沒有什麼難度,不過要注意一下別忘記標點符號。
欄位的數值(Tag)
在Protocol Buffers裡面,欄位的名其實沒那麼重要,但是寫C#程式碼的時候,欄位名還是很重要的。
對於protobuf來說,這個tag是更為重要的。
可以使用的最小的tag數值是1,最大值是229 - 1, 或者 536,870,911。但是你不可以使用19000到19999之間的數,這部分數是保留的。
還有一點值得注意的是:
從1到15的Tag數只佔用1個位元組的空間,所以它們應該被用在頻繁使用的欄位上。而從16到2047,則佔用兩個位元組,它們可以用在不頻繁使用的欄位上。
欄位規則
protobuf的欄位必須滿足以下兩個規則之一:
單數字段(Singular)
大概意思就是指這個欄位只能出現0或1次(不能超過一次),這也是proto3的預設欄位規則。
重複欄位(Repeated)
與singular相對的就是repeated。如果你想做一個list或陣列的話,你可以使用重複欄位這個概念。這個list可以有任何數量(包括0)的元素。它裡面的值的順序將會得到保留。
Repeated Fields 例子
還是使用前面的Person這個例子,我們在裡面新增一個repeated欄位(電話號碼):
就是在前面加上repeated這個關鍵字即可。
在proto3裡面,標量型別的repeated欄位採用的是packed編碼。
註釋
proto檔案裡可以新增註釋。它們通常被當作你定義的這些訊息的文件。
註釋很簡單,還是兩種形式,直接看程式碼就明白了:
保留的欄位
如果你對你定義的訊息型別進行了更新,例如刪除某個欄位或者註釋掉某個欄位,那麼其它開發者在以後更新這個訊息型別的時候可能會重新使用被你刪除/註釋掉的欄位的數值(tag)。如果以後還需要使用這個訊息型別的老版本的proto檔案,那麼這將會引起嚴重的問題,例如資料損壞、隱私漏洞等等。
那麼一種避免此類事情發生的解決辦法就是將你刪除/註釋掉的這些欄位的數值(或/並且包括欄位名,因為欄位名可引起JSON序列化的問題)標記為reserved,如果其他人再使用這個數值作為欄位識別符號,那麼編譯器就會有錯誤提示:
注意,不可以把reserved數值和欄位名放在同一個reserved語句裡。
欄位的預設值
當訊息被解析的時候,如果編碼的訊息裡不含有特定的一個singular元素,那麼在被解析物件裡相應的欄位就會被設為預設值。
常用型別的預設值如下:
- string:空字串
- bytes:空的byte陣列
- bool:false
- 數值型:0
- 列舉enum:列舉裡定義的第一個列舉值,值必須是0
- repeated:通常是相應開發語言裡的空list
- 還有個訊息型別的欄位,它的預設值和開發語言有關,這個以後再說。
列舉
之前說了,列舉裡面定義的第一個值就是這個列舉的預設值。
Enum的tag必須從0開始,所以0就是列舉的數值預設值。
繼續上個例子
我們對Person新增一個列舉型別的欄位:性別 Gender:
首先需要定義列舉型別,這裡定義了一個列舉,名稱是Gender,裡面有3個值,預設值是NOT_SPECIFIED,數值預設值就是0。
然後使用這個列舉型別定義了一個欄位,名稱為gender,tag數為10。
為列舉值起別名
列舉值是可以起別名的,起別名的作用就是允許兩個列舉值擁有同一個數值。
要想起別名,首先需要設定allow_alias這個option為true:
然後我們為FEMALE這個列舉值起了一個別名叫做WOMAN,它們的數值是一樣的。同樣的MAN是MALE的數值也是一樣的。
列舉裡面的常量的值必須不能超過32位整型的數值,不建議使用負數。
列舉可以定義在message裡面,也可以在外邊單獨定義以便複用。如果另一個訊息想使用Person裡面這個Gender列舉,那麼可以使用Person.Gender這種形式。
針對列舉值被刪除/註釋掉這種情況,它也可以使用reserved:
數值和常量名也必須分開使用兩個reserved語句。
其中max表示可能的最大的值。
使用其它的資訊型別
可以使用其它的資訊型別作為欄位的型別。
我們可以在同一個proto檔案裡定義多個資訊型別(為了截圖方便,我去掉了Person的一些欄位):
在這個檔案裡,除了Person資訊型別外,我還定義了Date資訊型別。
所以,我可以在Person裡面使用Date作為它的欄位型別:
引入定義
如果想要使用的資訊型別已經在其它的proto檔案定義好了呢?這個時候就需要引入資訊型別的定義。
現在我把Date定義移動到了date.proto這個檔案裡面:
然後在person.proto裡面我們可以引用date.proto:
巢狀型別
Protocol Buffer允許在資訊型別裡面定義其它的資訊型別。
直接看例子:
如果想在Person外邊使用Address這個型別,那麼就需要這樣用:Person.Address。
打包
你可以向proto檔案新增可選的打包(package)說明符,以避免訊息型別間的名稱衝突。
所以說打包是很必要的。
打包之後生成的C#程式碼就會使用名稱空間來對應proto裡面的package,但是命名方式會改為Pascal Case(每個單詞首字母大寫)。
上面的程式碼在C#裡面的情況就是:Person類在My.Project這個名稱空間下。
但是如果你在proto檔案裡設定了option csharp_namespace這個選項,那麼在C#裡的名稱空間就是該選項指定的名稱空間了:
這時候,C#裡面Perosn類的名稱空間就是My.WebApis了,但是在proto檔案裡它的包還是my.project。
設定Protocol Buffers編譯器
protoc編譯器主要就是用來生成程式碼的,它的下載地址目前是:https://github.com/protocolbuffers/protobuf/releases/
在裡面選擇你使用的作業系統的版本:
下載後解壓縮到某個路徑,然後把解壓目錄下的bin目錄新增到系統的環境變數裡。
然後開啟命令列,輸入protoc,如果有類似下面的東西出現,說明安裝成功了:
這裡面的--proto_path=PATH這個引數比較常用,它用來指定到哪個檔案見來查詢引入。
再有就這個引數很常用:
--csharp_out=OUT_DIR用來指定存放生成的C#程式碼的目錄。
我們先試驗一下,生成Person的C#程式碼:
執行成功後就沒有任何提示,開啟csharp目錄,可以看到Person.cs這個檔案:
而Person.cs檔案裡面的程式碼就比較多了:
千萬不要去修改這個檔案!
第一篇文章先到這。