gRPC趁現在還沒大火,搶先了解一下

Code綜藝圈發表於2021-06-28

前言

系統分散式已經成為程式設計師的家常,將大型單體劃分為相對簡單的小模組,分散系統能力,提升系統擴充套件性、功能模組複用性等;各功能模組之間肯定會有很多資料共享和互動的應用場景,那就避免不了各模組之間的通訊;目前用的比較多的方式是HTTP(Restful API)介面、訊息佇列等,而HTTP(Restful API)介面應該是目前應用比較廣泛的,相對之前的webservice和WCF都顯得比較輕量級,而且實用;

隨著微服務的盛行,對服務間的通訊要求也越來越高,比如傳輸方式、傳輸速率、傳輸內容大小等,而HTTP(Restful API)方式有較重的頭資訊、無狀態、不能複用連線等缺點,所以優化是早晚的事;那麼gRPC的出現就顯得情有可原啦(gRPC不侷限於服務間通訊,只要符合Server/Client場景就可以),所以接下來我們們一起來探究學習一下。

正文

1. 認識一下gRPC

gRPC 由 google 開發,最前面的g就代表google。進入gRPC官網,一句話描述了gRPC的主要特色,先上張圖:

A high performance, open source universal RPC framework

翻譯:一個高效能開源通用RPC框架

  • 高效能:gRPC遵循HTTP/2協議,解決並優化了HTTP1.1的一些缺陷;預設使用谷歌開源的 protocol buffers(類似於XML、JSON的資料序列化結構協議),傳輸速率、解析速度都很快、壓縮率高,效能整體都比XML和JSON好(後續專門寫個程式來比較比較);
  • 開源:原始碼地址請進傳送門
  • 通用:各種流行語言(C++、C#、Java、Go、Python等)都能用,輕鬆實現跨語言通訊;本身不限於任何平臺。
  • RPC:遠端過程呼叫(Remote Procedure Call),通俗一點理解,就是分散式中各服務之間呼叫的一種技術。

總而言之,gRPC是一個現代的開源高效能遠端過程呼叫(RPC)框架,可以在任何環境中執行。它可以有效地連線資料中心內和跨資料中心的服務,並支援可插拔的負載均衡、跟蹤、健康檢查和身份認證(後續會一一舉例演示)。

算啦,估計到這有些小夥伴還是有點懵圈(語言組織能力還有待提高),那就先將其理解為一個類似於WebAPI的呼叫框架,只是效能更高,使用更簡單,就像呼叫本地方法一樣;它使客戶端和伺服器應用程式能夠透明地通訊,隱藏了遠端呼叫的細節,大概過程如下:

上圖簡析:各語言之間可以互相呼叫,只要客戶端按照約定(Protocol Buffer)傳遞對應的請求引數,呼叫服務端對應的方法,最後就會返回約定好(Protocol Buffer)的響應資料。

不說那麼多啦,直接開幹吧,一邊擼碼一邊說理論。

2. 初體驗 gRPC

2.1 從0開始寫服務端
  • 建立一個空的Web專案(基於.NetCore3.1),引入包Grpc.AspNetCore

  • 編寫proto檔案(重點),因為gRPC是使用protocol Buffer作為介面定義語言,內容包含以下兩部分:

    傳遞的訊息:請求和響應時的資料資訊,類似於現在用的DTO類。

    gRPC服務的定義:定義gRPC服務方法,可以理解為現在寫的Restful 介面。

    這裡模擬使用者維護的場景,包含增、刪、改、查方法,這裡新建protos目錄專門用來存放proto檔案,user.proto內容如下:

    // 使用的是proto3版本
    syntax = "proto3";
    // 定義名稱空間,後續生成程式碼時就會生成對應的名稱空間
    option csharp_namespace = "gRPC.Demo.Server.protos";
    
    /*
    每一句需要用分號結尾
    message 用來定義請求和返回資料格式
    tag message後面的值數字代表是欄位的標識(tag),不是賦值,
    */
    
    // 新增使用者時需要傳遞資料訊息, 可理解為一個類
    message AddUserReuqest{
    	string name=1;
    	int32 age=2;
    	bool isBoy=3;
    }
    // 新增時返回的訊息格式
    message ResultResponse {
    	int32 code=1;
    	string msg =2;
    }
    //傳遞的查詢條件資訊格式,可理解為平時傳入的查詢條件物件
    message QueryUserReuqest{
    	string name=1;
    }
    //查詢返回的使用者資訊格式,可理解為返回的類
    message UserInfoResponse {
    	string name=1;
    	int32 age=2;
    	string gender=3;
    }
    
    // service 用標識定義服務的,裡面寫對應的方法
    service UserService{
    	// 新增使用者
    	rpc AddUser(AddUserReuqest) returns (ResultResponse);
    	// 查詢使用者
    	rpc GetAllUser(QueryUserReuqest) returns (UserInfoResponse);
    }
    

    proto檔案,各種程式語言是通用的,都可以根據定義好的proto檔案生成對應程式語言的程式碼,重寫業務即可。對於自動生成程式碼,其他程式語言需要藉助protoc 工具(點這裡去下載),而.Net微軟已經封裝好了,只要引入了Grpc相關的包,直接編譯即可。先簡單設定一下proto檔案屬性,大概步驟如下:

    注:如果不想按照上面步驟設定檔案屬性,也可以直接編輯專案檔案也行,如下增加紅框部分:

    最後編譯程式就可以自動生成對應的服務端程式碼了,如下路徑:

  • 重寫生成的方法,增加業務邏輯

    proto檔案生成的程式碼只是一個約定,不包含業務邏輯,關於具體的業務需要自己處理,如下:

  • 在Startup.cs檔案中開啟gRPC的功能,如下:

    到這服務端就完成了,現在就去寫一個客戶端連線一下;

2.2 搞一個客戶端訪問服務
  • 建立一個控制檯程式

    引入Google.Protobuf、Grpc.Net.Client、Grpc.Tools三個包,然後將服務端寫好的proto檔案拷貝到專案中,按照上面說的步驟設定一下proto檔案屬性為Client only,編譯即可,專案結構如下:

    如果不想通過設定屬性的方式,也可以直接修改專案檔案,然後編譯就可自動生成程式碼:

  • 現在可以寫業務啦

    新增使用者呼叫遠端服務邏輯:

    查詢使用者呼叫遠端服務邏輯:

    從上面程式碼來看,使用是不是比較簡單,只需要知道遠端地址即可,遠端呼叫就像呼叫本地方法一樣,也不用判斷狀態碼,不用自己去解析內容,而是按照約定直接返回結果。

  • 先執行服務端,再執行客戶端,效果如下:

    除錯時注意:

    由於gRPC使用SSL/TLS保護服務,在除錯的時候儘管指定為https連線也會報錯,這是因為系統沒有信任微軟開發證照,執行以下命令即可:

    dotnet dev-certs https --trust
    

    然後就可以正常除錯了;當然在生產環境中,還是需要配置真實證照的,如果不想用證照也是可以滴,這個後續單獨會說到。

2.3 使用.NetCore模板快速生成服務端

.NetCore已經將gRPC服務端封裝成一個模板了,可以通過模板快速建立一個gRPC專案,如下:

建立出來的服務端專案基本和剛剛建立的一樣,編譯執行就能啟動;小夥伴後續可以直接用模板建立,在裡面新增對應的業務程式碼即可。

本來打算把gRPC的四種模式接著演示的,但考慮到proto檔案的編寫是一個重點,所以總結了一些常用的寫法,先熟悉熟悉proto檔案的編寫,後續再通過案例演示加深印象就更好啦。

3. Protocol Buffer 是重點

通過上面示例演示,proto檔案用於約定服務介面,各程式語言用其可以生成對應的程式碼,然後進行業務邏輯的編寫,可見proto檔案扮演了很重要的角色。在效能方便是以二進位制格式進行解析,內容小,傳輸效率高,適合傳遞大量資料的場景。
proto檔案中每一個message代表了一類結構化的資料,message 裡面定義了每一個屬性的型別和名字,並指定一個Tag(每個屬性後面的數字)。在gRPC傳輸過程中是通過Tag這個數字進行標識屬性的,不是用屬性名

3.1 資料型別

由於protocol Buffer 不限於程式語言,所以在編寫proto檔案時,指定的型別和不同程式語言的型別是一一對應的,這樣在根據proto檔案生成對應程式碼時,約束力就比較強,大概的型別對照表如下(只整理了比較常用的語言,詳細請進官網):

每一種型別在沒有指定值時都對應有預設值:

  • string: 空字串
  • bytes:空byte陣列
  • bool:false
  • 數值型:0
  • 列舉enum: 預設第一個列舉值,第一個值必須是0

預設值其實基本上和對應的程式語言預設值差不多一樣。

3.2 常用編寫方式
  • 一般形式

    通過message定義一類結構化資料,如下:

    使用message定義一類資料時,其實就可以理解為在編寫一個類,裡面的欄位就是類裡面的屬性。需要注意的是message中指定欄位的後面數值不是欄位值,而是欄位名稱對應的一個標識(Tag)。

  • 巢狀自定義型別

    經常會遇到這種巢狀自定義型別的場景,比如一個班級有多個學生,班級型別就需要巢狀學生,如下:

    這裡用到repeated來表示一個班級有多個學生,可以理解為List.

  • 使用列舉

    列舉在程式設計過程中肯定少不了,這裡把上面的性別改為列舉,如下:

    列舉型別需要注意的是第一項對映的常量值必須為0,方便設定其作為預設項,同時也是為了相容proto2。

    如果想要一個數字對應多個列舉項,可以開啟允許別名的方式進行設定,如下:

  • 檔案引入

    在實際開發場景中,定義的資料型別很多,放在一個檔案肯定不太合適,可讀性差,所以都會將其放在多個proto檔案,然後通過引用多個檔案即可;接下來把學生單獨出來作為一個proto檔案,如下:

    注:在建立proto檔案時,按照2.1那個步驟設定檔案屬性,否則不會自動生成類,如果有檔案相互引用,還會編譯報錯。

  • 引用第三方proto

    在平時開發的時候,有些機構、社群或是大佬將常用的類進行封裝,然後打包好, 我們只需要下載引用就可以愉快的使用啦;同樣,proto也可以這樣,如下在Student中加一個入學時間,如下:

    image-20210627002228358

    其他型別進傳送門,需要哪個,就照著上面方式使用即可。

3.3 引數欄位變更注意

需求變動對於開發來說是家常便飯,針對介面增加欄位或修改欄位都是常有的事,如果打算在原有方法修改欄位,那就需要注意啦; 在通過protocol Buffer進行序列化時,是通過類似於字典的形式約定好的,其中欄位後面的數字Tag至關重要,用其進行屬性欄位標識,學生舉例如下:

// 在解析時,都是用後面的數字tag進行約定解析的
message Student{
	string name=1; // 1 就代表 姓名
	int32 age=2;     // 2 就代表 年齡
	Gender gender=3; // 3 就代表 性別
	string addr=4; // 4 就代表 地址
	google.protobuf.Timestamp enrollmentDate=5; //5 就代表 入學時間
}

只要已經上線互相呼叫,如果有引數欄位變動,最好是通過新增的方式,因為這樣會避免雙方呼叫因為沒有同時更改引數欄位造成對業務的影響。比如上面學生欄位要變動,需求是:新增一個分數字段,刪除年齡欄位,通常會有以下方式:

  • 方式1:將年齡欄位直接改成分數字段即可

    如果服務端將proto檔案改成這樣,客戶端還沒來的及改,就會造成如下情況:

    2這個tag標識在服務端代表分數,在傳遞資料的時候就將分數賦值給該欄位;但對於客戶端來說,還是認為2這個tag標識為年齡,接收的時候還是以年齡欄位進行處理,最終就會影響原有解析邏輯,並影響到業務。

  • 方式2:刪除年齡欄位並將其對應tag標識設定為保留欄位,然後新增一個分數字段

    通過reserved將原有引數欄位和對應的tag標識設為保留值,這樣不允許用作其他業務,就會避免業務邏輯處理異常的情況,最多是就接受方沒有收到值,按預設值處理即可。

關於欄位的更新注意先暫時說這麼多,型別之間互相相容的細節小夥伴去官網好好看看。 這裡記住一個重點:tag 標識不能隨便進行重用,不然proto檔案沒有及時同步就有可能導致業務邏輯處理有問題。

總結

.NetCore對於gRPC已經是封裝得比較方便啦,引入對應的包,編譯自動根據.proto檔案生成對應程式碼,然後直接寫業務即可;下一篇說說關於gRPC四種模式及gRPC服務的認證、授權。

一個被程式搞醜的帥小夥,關注"Code綜藝圈",和我一起學~~~

相關文章