開發gRPC總共分三步

語言: CN / TW / HK

highlight: a11y-dark theme: Chinese-red


本文為掘金社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

上一篇文章我們介紹了ProtoBuf的使用,不瞭解ProtoBuf的同學建議先讀這篇文章:# 一文帶你玩轉ProtoBuf,會用protobuf是學習gRPC的基礎。

之前我也有寫過RPC相關的文章:# Go RPC入門指南:RPC的使用邊界在哪裡?如何實現跨語言呼叫?,詳細介紹了RPC是什麼,使用邊界在哪裡?並且用Go和php舉例,實現了跨語言呼叫。不瞭解RPC的同學建議先讀這篇文章補補課。

上面提到的這些基礎知識,不是本文的重點。

所以建議小夥伴們先讀上面兩篇,再讀這篇,體驗更好哦。

這篇文章將重點介紹在微服務中gRPC的使用:

開發流程

在微服務分散式架構中開發gRPC其實非常簡單,不要畏難畏煩,沒有什麼心智負擔的。

開發gRPC的流程和宋丹丹把大象裝冰箱是一樣的:

  1. 把冰箱門開啟
  2. 把大象裝進去
  3. 把冰箱門關上

開發gRPC的流程;

  1. 寫proto檔案定義服務和訊息
  2. 使用protoc工具生成程式碼
  3. 編寫業務邏輯程式碼提供服務

就是這麼簡單。

下面我仍然以Go語言舉例,其他語言的實現思路也是一樣的。

入門實踐

為了讓大家更好理解,我參考gRPC官方文件,寫了一個helloword示例。

下圖是使用Go實現gRPC開發的目錄結構圖,先讓大家有個整體的認識:

image.png

歡迎大家按照我的步驟進行復刻實踐:

看文章是學不會程式設計的,但是一邊看文章一邊敲程式碼可以!

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

image.png

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 } ```

以上就完成了服務端的業務邏輯編寫:

  1. 用我們在proto中定義的訊息,構建並填充了一個我們在介面定義的 HelloReply 應答物件。
  2. 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) } } ```

啟動成功,普天同慶:

image.png

到這裡我們就完成了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 兩端互通

如何實現兩端的訊息互通?

  1. 我們之前已經打開了一個終端,啟動了服務端的服務。
  2. 我們再開啟一個新的終端,執行客戶端,看下服務端是否給我們返回了資料:

image.png

和我們預想中的結果一樣:

服務端給我們返回了“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 王中陽

效果是這樣的:

image.png

好了,咱們再接著聊進階的內容:

gRPC另外一個特點就是和語言無關,我們可以使用不同的語言定義客戶端和服務端。

下面咱們再進階實戰一下,用gRPC實現跨語言的呼叫。

進階實戰:跨語言呼叫

入門實戰我給出了詳細的示例程式碼,甚至連目錄結構都分享給大家了,相信大家只要按照步驟復刻,一定也能執行成功。

關於進階實戰的跨語言呼叫:服務端不重複編寫了,我們仍然使用上面用Go編寫的服務端。

客戶端我將用我熟悉的PHP語言來編寫,實現兩端的rpc通訊。

建議大家回顧一下“大象裝冰箱”的步驟,用自己擅長的語言開發客戶端,像我一樣實現gRPC的跨語言呼叫。

1. 編寫proto檔案

和入門實戰是一樣的

2. 根據proto檔案生成程式碼

和入門實戰思路一樣,區別指定生成程式碼語言不一樣:

php protoc-gen-php -i . -o . ./helloworld.proto

3. 編寫業務邏輯程式碼

3.1 先寫服務端

服務端使用Go實現的服務端,不進行編寫。

確定服務端是開啟狀態:

image.png

再次提醒一下:

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客戶端:

image.png

我們發現,PHP的客戶端通過gRPC成功的連線了Go服務端提供的50051服務,併成功呼叫了SayHello()方法,獲得了返回值:Hello world

實操技巧

紙上得來終覺淺,絕知此事要躬行。

強烈建議大家動手實操,使用自己熟悉的語言完成gRPC跨語言呼叫,可以參考:gRPC 各種語言教程詳解這篇技術部落格更適合小白入門gRPC的開發,有個整體的理解和概念。

進階知識點安利大家看官方文件進行實踐:

gRPC 官方文件中文版

gRPC 官方示例GitHub

本文總結

通過這篇文章我們已經掌握了gRPC相關的知識點,可以獨立用Go實現客戶端和服務端的編寫,並且通過服務註冊對外提供服務,實現可客戶端和服務端的gRPC通訊。

為了驗證gRPC支援跨語言呼叫的特性,在進階實戰中又使用PHP開發了客戶端,實現了PHP客戶端和Go服務端的遠端跨語言呼叫。

養成良好的程式設計習慣有助於減少奇奇怪怪的問題,強烈建議大家嚴格按照“大象裝冰箱”的順序進行gRPC的開發:

1. 寫proto檔案定義服務和訊息

2. 使用protoc工具生成程式碼

3. 編寫業務邏輯程式碼提供服務

關注我,下一篇帶大家玩轉Go微服務。

最後:萬事起於忽微,量變引起質變,相信堅持的力量。

關於專欄

近期會更新一系列Go實戰進階的文章,歡迎大家關注我的簽約專欄# Go語言進階實戰

這是近期會更新文章的知識脈絡圖,感興趣的小夥伴可以關注一波,歡迎日常催更。

image.png

已完成

《一文玩轉ProtoBuf》

《開發gRPC總共分三步》

《Go WEB進階實戰:基於GoFrame搭建的電商前後臺API系統》

小夥伴們還想看哪些內容,歡迎在評論區留言。