一個簡單的案例入門 gRPC
這篇文章本來要在年前和小伙伴們見面,但是因為我之前的 Mac 系統版本是 10.13.6,這個版本比較老,時至今天在運行一些新鮮玩意的時候有時候會有一些 BUG(例如運行最新版的 Nacos 等),運行 gRPC 的插件也有 BUG,代碼總是生成有問題,但是因為系統升級是一個大事,所以一直等到過年放假,在家才慢慢折騰將 Mac 升級到目前的 13.1 版本,之前這些問題現在都沒有了,gRPC 的案例現在也可以順利跑起來了。
所以今天就來和小伙伴們簡單聊一聊 gRPC。
1. 緣起
我為什么想寫一篇 gRPC 的文章呢?其實本來我是想和小伙伴們梳理一下在微服務中都有哪些跨進城調用的方式,在梳理的過程中想到了 gRPC,發現還沒寫文章和小伙伴們聊過 gRPC,因此打算先來幾篇文章和小伙伴們詳細介紹一下 gRPC,然后再梳理微服務中的跨進程方案。
2. 什么是 gRPC
了解 gRPC 之前先來看看什么是 RPC。
RPC 全稱是 Remote Procedure Call,中文一般譯作遠程過程調用。RPC 是一種進程間的通信模式,程序分布在不同的地址空間里。簡單來說,就是兩個進程之間互相調用的一種方式。
gRPC 則是一個由 Google 發起的開源的 RPC 框架,它是一個高性能遠程過程調用 (RPC) 框架,可以在任何環境中運行。gRPC 通過對負載均衡、跟蹤、健康檢查和身份驗證的可插拔支持,有效地連接數據中心內和數據中心之間的服務。
在 gRPC 中,客戶端應用程序可以直接調用部署在不同機器上的服務端應用程序中的方法,就好像它是本地對象一樣,使用 gRPC 可以更容易地創建分布式應用程序和服務。與許多 RPC 系統一樣,gRPC 基于定義服務的思想,指定基于參數和返回類型遠程調用的方法。在服務端側,服務端實現接口,運行 gRPC 服務,處理客戶端調用。在客戶端側,客戶端擁有存根(Stub,在某些語言中稱為客戶端),它提供與服務端相同的方法。
gRPC 客戶端和服務端可以在各種環境中運行和相互通信 – 從 Google 內部的服務器到你自己的桌面 – 并且可以使用 gRPC 支持的任何語言編寫。因此,你可以輕松地用 Java 創建 gRPC 服務端,使用 Go、Python 或 Ruby 創建客戶端。此外,最新的 Google API 將包含 gRPC 版本的接口,使你輕松地將 Google 功能構建到你的應用程序中。
gRPC 支持的語言版本:
說了這么多,還是得整兩個小案例小伙伴們可能才會清晰,所以我們也不廢話了,上案例。
3. 實踐
先來看下我們的項目結構:
大家看下,這里首先有一個 grpc-api,這個模塊用來放我們的公共代碼;grpc-server 是我們的服務端,grpc-client 則是我們的客戶端,這些都是普通的 maven 項目。
3.1 grpc-api
在 grpc-api 中,我們首先引入項目依賴,如下:
除了這些常規的依賴之外,還需要一個插件:
我來說一下這個插件的作用。
默認情況下,gRPC 使用 Protocol Buffers,這是 Google 提供的一個成熟的開源的跨平臺的序列化數據結構的協議,我們編寫對應的 proto 文件,通過上面這個插件可以將我們編寫的 proto 文件自動轉為對應的 Java 類。
多說一句,使用 Protocol Buffers 并不是必須的,也可以使用 JSON 等,但是目前來說這個場景更常用的還是 Portal Buffers。
接下來我們在 main 目錄下新建 proto 文件夾,如下:
注意,這個文件夾位置是默認的。如果我們的 proto 文件不是放在 src/main/proto 位置,那么在配置插件的時候需要指定 proto 文件的位置,咱們本篇文章主要是入門,我這里就使用默認的位置。
在 proto 文件夾中,我們新建一個 product.proto 文件,內容如下:
這段配置算是一個比較核心的配置了,這里主要說明了負責進程傳輸的類、方法等到底是個啥樣子:
- syntax = "proto3";:這個是 protocol buffers 的版本。
- option java_multiple_files = true;:這個字段是可選的,如果設置為 true,表示每一個 message 文件都會有一個單獨的 class 文件;否則,message 全部定義在 outerclass 文件里。
- option java_package = "org.javaboy.grpc.demo";:這個字段是可選的,用于標識生成的 java 文件的 package。如果沒有指定,則使用 proto 里定義的 package,如果package 也沒有指定,那就會生成在根目錄下。
- option java_outer_classname = "ProductProto";:這個字段是可選的,用于指定 proto 文件生成的 java 類的 outerclass 類名。什么是 outerclass?簡單來說就是用一個 class 文件來定義所有的 message 對應的 Java 類,這個 class 就是 outerclass;如果沒有指定,默認是 proto 文件的駝峰式;
- package product;:這個屬性用來定義 message 的包名。包名的含義與平臺語言無關,這個 package 僅僅被用在 proto 文件中用于區分同名的 message 類型。可以理解為 message 全名的前綴,和 message 名稱合起來唯一標識一個 message 類型。當我們在 proto 文件中導入其他 proto 文件的 message,需要加上 package 前綴才行。所以包名是用來唯一標識 message 的。
- service:我們定義的跨平臺方法都寫在 service 中,上面的案例中我們定義了兩個方法:addProduct 表示添加一件商品,參數是一個 Product 對象,返回值則是剛剛添加成功的商品的 ID;getProduct 則表示根據 ID 查詢一個商品,參數是一個商品 ID,返回值則是查詢到的商品對象。這里的定義相當于一個接口,將來我們要在 Java 代碼中實現這個接口。
- message:這里有點像我們在 Java 中定義類,上文中我們定義了兩個類,分別是 Product 和 ProductId 兩個類。這兩個類在 service 中被使用。
message 中定義的有點像我們 Java 中定義的類,但是不能直接使用 Java 中的數據類型,畢竟這是 Protocol buffers,這個是和語言無關的,將來可以據此生成不同語言的代碼,這里我們可以使用的類型和我們 Java 類型之間的對應關系如下:
另外我們在 message 中定義的屬性的時候,都會給一個數字,例如 id=1,name=2 等,這個數字將來會在二進制消息中標識我們的字段,并且一旦我們的消息類型被使用就不應更改,這個有點像序列化的感覺。
實際上,這個 message 編譯后的字節內容大概像下面這樣:
這里的標簽中的內容包含兩部分,字段索引和字段類型,字段索引其實就是我們上面定義的數字。
定義完成之后,接下來我們就需要使用插件來生成對應的 Java 代碼了,插件我們在前面已經引入了,現在只需要執行了,如下圖:
注意,compile 和 compile-custom 兩個指令都需要執行。其中 compile 用來編譯消息對象,compile-custom 則依賴消息對象,生成接口服務。
首先我們點擊 compile 看看生成的代碼,如下:
再看 compile-custom 生成的代碼,如下:
好了,這樣我們的準備工作就算完成了。
有的小伙伴生成的代碼文件夾顏色不對勁,此時有兩種解決辦法:1.選中目標文件夾,右鍵單擊,選擇 Mark Directory as-> Generated Sources root;2.選中工程,右鍵單擊,選擇 Maven->Reload project。推薦使用第二種方案。
3.2 grpc-server
接下來我們創建 grpc-server 項目,并使該項目依賴 grpc-api,然后在 grpc-server 中,提供 ProductInfo 的具體實現:
ProductInfoGrpc.ProductInfoImplBase 是根據我們在 proto 文件中定義的 service 自動生成的,我們的 ProductInfoImpl 繼承自該類,并且提供了我們給出的方法的具體實現。
以 addProduct 方法為例,參數 request 就是將來客戶端調用的時候傳來的 Product 對象,返回結果則通過 responseObserver 來完成。我們的方法邏輯很簡單,我就把參數傳來的 Product 對象打印出來,然后構建一個 ProductId 對象并返回,最后調用 responseObserver.onCompleted(); 表示數據返回完畢。
剩下的 getProduct 方法邏輯就很好懂了,我這里就不再贅述了。
最后,我們再把這個 grpc-server 項目啟動起來:
由于我們這里是一個 JavaSE 項目,為了避免項目啟動之后就停止,我們這里調用了 server.awaitTermination(); 方法,就是讓服務啟動成功之后不要停止。
3.3 grpc-client
最后再來看看客戶端的調用。首先 grpc-client 項目也是需要依賴 grpc-api 的,然后直接進行方法調用,如下:
小伙伴們看到,這里首先需要和服務端建立連接,給出服務端的地址和端口號即可,usePlaintext() 方法表示不使用 TLS 對連接進行加密(默認情況下會使用 TLS 對連接進行加密),生產環境建議使用加密連接。
剩下的代碼就比較好懂了,創建 Product 對象,調用 addProduct 方法進行添加;創建 ProductId 對象,調用 getProduct。Product 對象和 ProductId 對象都是根據我們在 proto 中定義的 message 自動生成的。
4. 總結
好啦,一個簡單的例子,小伙伴們先對 gRPC 入個門,后面松哥會再整幾篇文章跟大家介紹這里邊的一些細節。