開發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系統》

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