開發gRPC總共分三步
highlight: a11y-dark theme: Chinese-red
本文為掘金社區首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!
前言
上一篇文章我們介紹了ProtoBuf的使用,不瞭解ProtoBuf的同學建議先讀這篇文章:# 一文帶你玩轉ProtoBuf,會用protobuf是學習gRPC的基礎。
之前我也有寫過RPC相關的文章:# Go RPC入門指南:RPC的使用邊界在哪裏?如何實現跨語言調用?,詳細介紹了RPC是什麼,使用邊界在哪裏?並且用Go和php舉例,實現了跨語言調用。不瞭解RPC的同學建議先讀這篇文章補補課。
上面提到的這些基礎知識,不是本文的重點。
所以建議小夥伴們先讀上面兩篇,再讀這篇,體驗更好哦。
這篇文章將重點介紹在微服務中gRPC的使用:
開發流程
在微服務分佈式架構中開發gRPC其實非常簡單,不要畏難畏煩,沒有什麼心智負擔的。
開發gRPC的流程和宋丹丹把大象裝冰箱是一樣的:
- 把冰箱門打開
- 把大象裝進去
- 把冰箱門關上
開發gRPC的流程;
- 寫proto文件定義服務和消息
- 使用protoc工具生成代碼
- 編寫業務邏輯代碼提供服務
就是這麼簡單。
下面我仍然以Go語言舉例,其他語言的實現思路也是一樣的。
入門實踐
為了讓大家更好理解,我參考gRPC官方文檔,寫了一個helloword示例。
下圖是使用Go實現gRPC開發的目錄結構圖,先讓大家有個整體的認識:
歡迎大家按照我的步驟進行復刻實踐:
看文章是學不會編程的,但是一邊看文章一邊敲代碼可以!
1. 寫proto文件定義服務和消息
service Greeter {} 是我們定義的服務
rpc SayHello (HelloRequest) returns (HelloReply) {} 是在服務中定義的方法
protoc工具集,會根據我們定義的服務、方法、和消息生成指定語言的代碼。
```Go syntax = "proto3";
option go_package = "./;hello";
package hello;
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} }
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; } ```
如果小夥伴們看上面代碼有不懂的地方,那就是protobuf基礎不牢了,請看這篇:一文帶你玩轉protobuf,回顧一下知識點。
2. 使用protoc工具生成代碼
切換到proto文件所在目錄下
cd protos/helloword/
生成Go代碼
protoc --go_out=. helloworld.proto
小技巧之同步依賴:當你生成Go代碼後,發現生成的文件飄紅報錯,不要緊張,多數情況是因為依賴不存在導致的。
執行下面的命令,同步依賴就可以了:
go mod tidy
3. 編寫業務邏輯代碼 提供服務
下面是今天的重點,我們用Go實現業務邏輯的編寫,注意看:
在微服務架構開發gRPC時,一定有兩個端:服務端和客户端。
我們的習慣是,在搞定protobuf之後,先寫服務端邏輯,暴露端口,提供服務;再寫客户端邏輯,連接服務,發送請求,處理響應。
小提示:PHP和Objective-C只能實現gRPC中的客户端,不能實現服務端。
3.1 編寫服務端業務邏輯
編寫服務端非常簡單,我們只需要實現在proto中定義的rpc方法。
小技巧:在我們實際開發中,我們導入protos服務的時候,默認是一個比較長的名字,建議結合自己項目,改成比較短又容易理解的名字。
```go package greeter_server
import "context"
//導入我們在protos文件中定義的服務 import pb "juejin/rpc/protos/helloworld"
//定義一個結構體,作用是實現helloworld中的GreeterServer type server struct{}
// SayHello implements helloworld.GreeterServer func (s server) SayHello(ctx context.Context, in pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil } ```
以上就完成了服務端的業務邏輯編寫:
- 用我們在proto中定義的消息,構建並填充了一個我們在接口定義的
HelloReply
應答對象。 - 將
HelloReply
對象返回給客户端。
到這裏業務功能是實現了,但是服務端的業務如何讓客户端調用呢?
下面我們繼續編寫:暴露端口,提供服務
3.2 暴露端口,提供服務
踩坑分享:我在編碼的過程中使用了錯誤的gRPC依賴,浪費了不少時間。應該用下面這個依賴包:
go
go get google.golang.org/grpc
注意:下面的代碼是在 3.1的基礎上添加的,並不是另外創建一個新的Go文件。
關鍵代碼註釋已經在代碼段中寫清楚了,建議大家參考步驟,手敲一遍。
```go package main
import ( "context" "flag" "fmt" "google.golang.org/gRPC" "log" "net" )
//導入我們在protos文件中定義的服務 import pb "juejin/rpc/protos/helloworld"
//定義一個結構體,作用是實現helloworld中的GreeterServer type server struct { pb.UnimplementedGreeterServer }
// SayHello implements helloworld.GreeterServer func (s server) SayHello(ctx context.Context, in pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil }
//定義端口號 支持啟動的時候輸入端口號 var ( port = flag.Int("port", 50051, "The server port") )
func main() { //解析輸入的端口號 默認50051 flag.Parse() //tcp協議監聽指定端口號 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } //實例化gRPC服務 s := gRPC.NewServer() //服務註冊 pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) //啟動服務 if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } ```
啟動成功,普天同慶:
到這裏我們就完成了gRPC服務端的編寫:我們實現了將 Greeter
服務綁定到一個端口,我們啟動這個服務時,服務端已準備好從 Greeter
服務的客户端接收請求了。
我們接下來再編寫客户端:
3.3 編寫客户端邏輯代碼
客户端的 gRPC 更簡單!
我們將用protoc生成的代碼寫一個簡單的客户端程序,來訪問我們在創建的 Greeter
服務端。
小技巧:在 gRPC Go 我們使用一個特殊的 Dial() 方法來創建頻道,實現和服務端的連接。
關鍵代碼已添加註釋,編寫客户端邏輯代碼,強烈建議大家和我一起手敲一遍。
“編程要有工匠精神,做的多了手感就出來了。”
```go package main
import ( "context" "flag" "google.golang.org/gRPC" //這個依賴不要搞錯 "google.golang.org/gRPC/credentials/insecure" pb "juejin/rpc/protos/helloworld" "log" "time" )
//默認數據 也支持在控制枱自定義 const ( defaultName = "world" )
//監聽地址和傳入的name var ( addr = flag.String("addr", "localhost:50051", "the address to connect to") name = flag.String("name", defaultName, "Name to greet") )
func main() { flag.Parse() //通過gRPC.Dial()方法建立服務連接 conn, err := gRPC.Dial(addr, gRPC.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("did not connect: %v", err) } //連接要記得關閉 defer func(conn gRPC.ClientConn) { err := conn.Close() if err != nil {
}
}(conn) //實例化客户端連接 c := pb.NewGreeterClient(conn)
//設置請求上下文,因為是網絡請求,我們需要設置超時時間 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() //客户端調用在proto中定義的SayHello()rpc方法,發起請求,接收服務端響應 r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) } ```
到這裏我們就已經完成了服務端和客户端業務邏輯的編寫,下面就是見證奇蹟的時刻了:
3.4 調用gRPC 兩端互通
如何實現兩端的消息互通?
- 我們之前已經打開了一個終端,啟動了服務端的服務。
- 我們再打開一個新的終端,運行客户端,看下服務端是否給我們返回了數據:
和我們預想中的結果一樣:
服務端給我們返回了“Hello world”,其中Hello是服務端設置的,world是客户端傳給服務端的參數,服務端進行拼接之後給客户端返回了。
至此,一個經典的gRPC通信示例就搞定了!
擴展:自定義輸入
沒用過go flag自定義輸入的小夥伴重點看一下,這部分是為你寫的:
客户端和服務端代碼中的flag.Parse的作用是:支持我們在終端控制枱自定義輸入參數,如果沒有輸入的話,使用程序中設置的默認參數,比如客户端的name,在代碼中是這麼定義的:
go
name = flag.String("name", "world", "Name to greet")
我們在終端輸入如下命令:
shell
go run main.go --name 王中陽
效果是這樣的:
好了,咱們再接着聊進階的內容:
gRPC另外一個特點就是和語言無關,我們可以使用不同的語言定義客户端和服務端。
下面咱們再進階實戰一下,用gRPC實現跨語言的調用。
進階實戰:跨語言調用
入門實戰我給出了詳細的示例代碼,甚至連目錄結構都分享給大家了,相信大家只要按照步驟復刻,一定也能運行成功。
關於進階實戰的跨語言調用:服務端不重複編寫了,我們仍然使用上面用Go編寫的服務端。
客户端我將用我熟悉的PHP語言來編寫,實現兩端的rpc通信。
建議大家回顧一下“大象裝冰箱”的步驟,用自己擅長的語言開發客户端,像我一樣實現gRPC的跨語言調用。
1. 編寫proto文件
和入門實戰是一樣的
2. 根據proto文件生成代碼
和入門實戰思路一樣,區別指定生成代碼語言不一樣:
php
protoc-gen-php -i . -o . ./helloworld.proto
3. 編寫業務邏輯代碼
3.1 先寫服務端
服務端使用Go實現的服務端,不進行編寫。
確定服務端是開啟狀態:
再次提醒一下:
PHP和Objective-C只能實現gRPC中的客户端,不能實現服務端。
3.2 再寫客户端
我用PHP實現客户端的編寫,你擅長什麼語言呢?有沒有踩到坑,歡迎大家在評論區討論。
```php <?php //命名空間 namespace Helloworld;
//定義PHP客户端 class GreeterClient extends \gRPC\BaseStub {
//定義構造方法 public function __construct($hostname, $opts, $channel = null) { parent::__construct($hostname, $opts, $channel); }
/* * 實現proto文件中定義的SayHello()方法 * Sends a greeting * @param \Helloworld\HelloRequest $argument input argument * @param array $metadata metadata * @param array $options call options * @return \gRPC\UnaryCall / public function SayHello(\Helloworld\HelloRequest $argument, $metadata = [], $options = []) { return $this->_simpleRequest('/helloworld.Greeter/SayHello', $argument, ['\Helloworld\HelloReply', 'decode'], $metadata, $options); }
} ```
3.3 啟動服務,進行調用
編寫PHP腳本文件:
連接50051
端口(Go實現的gRPC服務端對外暴露的端口)
```php <?php require dirname(FILE).'/vendor/autoload.php';
function greet($hostname, $name) { $client = new Helloworld\GreeterClient($hostname, [ 'credentials' => gRPC\ChannelCredentials::createInsecure(), ]); $request = new Helloworld\HelloRequest(); $request->setName($name); list($response, $status) = $client->SayHello($request)->wait(); if ($status->code !== gRPC\STATUS_OK) { echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL; exit(1); } echo $response->getMessage() . PHP_EOL; }
$name = !empty($argv[1]) ? $argv[1] : 'world'; $hostname = !empty($argv[2]) ? $argv[2] : 'localhost:50051'; greet($hostname, $name); ```
通過終端,啟動PHP客户端:
我們發現,PHP的客户端通過gRPC成功的連接了Go服務端提供的50051服務,併成功調用了SayHello()方法,獲得了返回值:Hello world
實操技巧
紙上得來終覺淺,絕知此事要躬行。
強烈建議大家動手實操,使用自己熟悉的語言完成gRPC跨語言調用,可以參考:gRPC 各種語言教程詳解。這篇技術博客更適合小白入門gRPC的開發,有個整體的理解和概念。
進階知識點安利大家看官方文檔進行實踐:
本文總結
通過這篇文章我們已經掌握了gRPC相關的知識點,可以獨立用Go實現客户端和服務端的編寫,並且通過服務註冊對外提供服務,實現可客户端和服務端的gRPC通信。
為了驗證gRPC支持跨語言調用的特性,在進階實戰中又使用PHP開發了客户端,實現了PHP客户端和Go服務端的遠程跨語言調用。
養成良好的編程習慣有助於減少奇奇怪怪的問題,強烈建議大家嚴格按照“大象裝冰箱”的順序進行gRPC的開發:
1. 寫proto文件定義服務和消息
2. 使用protoc工具生成代碼
3. 編寫業務邏輯代碼提供服務
關注我,下一篇帶大家玩轉Go微服務。
最後:萬事起於忽微,量變引起質變,相信堅持的力量。
關於專欄
近期會更新一系列Go實戰進階的文章,歡迎大家關注我的簽約專欄 :# Go語言進階實戰。
這是近期會更新文章的知識脈絡圖,感興趣的小夥伴可以關注一波,歡迎日常催更。
已完成
《Go WEB進階實戰:基於GoFrame搭建的電商前後台API系統》
小夥伴們還想看哪些內容,歡迎在評論區留言。
- Go異步任務處理解決方案:Asynq
- 一天約了4個面試,覆盤一下面試題和薪資福利
- 世界上最健康的程序員作息表!「值得一看」
- 8千字詳解Go1.20穩定版
- 不愧是微軟出品的工具,逆天!
- 【視頻 源碼】登錄鑑權的三種方式:token、jwt、session實戰分享
- 程序員副業接單做私活避坑指南
- Git操作不規範,戰友提刀來相見!
- 【簡歷優化】如何寫好項目的亮點難點?項目經歷怎麼寫最好?
- 技術男的春天:小姐姐求助&暖男分析
- 【簡歷優化】如何在簡歷中最大化體現出自己的學習能力?
- 如何快速學一門新語言?關鍵問題是什麼?
- Go WEB進階實戰:GoFrame結合電商項目深入理解Go知識點
- Go容易搞錯的知識點彙總
- 開發gRPC總共分三步
- 【答讀者問】把Go基礎學完後,是學web方向還是區塊鏈方向?
- Go WEB進階實戰:基於GoFrame搭建的電商前後台API系統
- 給想轉Go或者Go進階同學的一些建議
- 聽了大佬們的直播,我決定卷掘金小冊了。| Flag永不倒
- 爆肝兩千字整理《Go學習路線圖》| 文末投稿送投影