此篇不會說明有關 gRPC 與 protocol buffers 的詳細概念,如果要瞭解可以到 grpc.io 閱讀相關文件。 今天會筆記,怎麼從無到用 Go 建立好 gRPC 的 Server 與 Client,當作是學習 gRPC 的起點。

安裝

開始之前先把環境建立好,在 Compiling your protocol buffers 文件裡面有詳細說明如何安裝。

1. 首先要先到 protobuf-github release 下載 protobuf compiler 執行檔案。

現在最新的版本是 Protocol Buffers v3.11.4 ,而我電腦是 win64,所以我就選擇 protoc-3.11.4-win64.zip 下載。

Protocol Buffers v3.11.4Protocol Buffers v3.11.4
win64win64

2. 將 protobuf compiler 執行檔加入環境變數

將下載的檔案解壓縮後,有 bin 資料夾,將裡面的 protoc.exe 加入環境變數。

protocprotoc

如果你的電腦有安裝 Go 的話,可以把檔案放在 ${GOROOT}/bin 底下,以我電腦為例,我就把 protoc.exe 放到 C:\Go\bin 底下,因為 ${GOROOT}/bin 已經加入環境變數了。 完成後打開命令提示字元輸入 $ protoc --version 就可以看到版本號了。

versionversion

3. 安裝 Go protocol buffers plugin

這是用來產生 Go Code 的工具,請執行下列指令安裝,

install go protocol buffers plugin
  • bash
1
2
$ go get google.golang.org/protobuf/cmd/protoc-gen-go
$ go install google.golang.org/protobuf/cmd/protoc-gen-go

安裝好後,就會看到 %{GOPATH}/bin 底下會有 protoc-gen-go.exe 執行檔案。

protoc-gen-goprotoc-gen-go

根據 Go Quick Start 的說明,我以為就這樣安裝結束了。 但發現要使用時,出現了此錯誤

根據 protocolbuffers/protobuf-go release note 的說明,這是因為 v1.20 以後 protoc-gen-go 不再支援產生 gRPC service definitions,未來要改用 protoc-gen-go-grpc

但是我更改使用 protoc-gen-go-grpc 卻又產生 'protoc-gen-go-grpc' is not recognized as an internal or external command 的錯誤訊息。

根據 google.golang.org/grpc: move protoc-gen-go-grpc into grpc module 的說明,這是因為 google.golang.org/protobuf 還沒有把 protoc-gen-go-grpc 的工具從 github.com/golang/protobuf 移植過來,所以要修正這個的問題,我們改從 github.com/golang/protobuf 取得 protoc-gen-go.exe,安裝方式請執行下面兩行程式。

install go protocol buffers plugin
  • bash
1
2
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ go install github.com/golang/protobuf/protoc-gen-go

上敘是我在寫此篇文章當下遇到的問題,我相信過不久 Goolge 會把文件跟這個錯誤修正好。

4. 最後要安裝 Golang 的 grpc package ,這是要用來寫 grpc code 的 package

grpc package
  • bash
1
$ go get -u google.golang.org/grpc

這樣我們就把環就都弄好啦。接下來進入簡單的概念說明與開始寫 code 了。

概念解說

我從 Mastering Go - Second Edition 這本書擷取一段 gRPC 的說明。

Strictly speaking, gRPC is a protocol built on HTTP/2 that allows you to create services
easily. gRPC can use protocol buffers to specify an interface definition language, as well as
to specify the format of the interchanged messages. gRPC clients and servers can be written
in any programming language without the need to have clients written in the same
programming language as their servers.

上敘的翻譯為,「gRPC 是建立在 HTTP/2 上的通訊協定,gRPC 可以使用 protocol buffers 定義訊息的格式,gRPC 可以使用任何語言實作。」

而我從此課程 Enhancing Application Communication with gRPC 擷取一張圖片,說明 Client 與 Server 使用 protocol buffers 的格式來傳送資料。

gRPC Concepts - Basic ComponentsgRPC Concepts - Basic Components

根據上圖的架構就是我們這篇文章要做的事情

  1. 使用 protocol buffers 定義 message type 產生程式碼框架。
  2. 實作 Server Code,執行 gRPC Server 。
  3. 實作 Client Code。

定義 Message Type

我們用 Define Message Type 範例來看 Message Type 的長相

define message type
  • proto
1
2
3
4
5
6
7
syntax = "proto3";

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}

這邊要特別說的是為什麼每一個欄位後面會有一個數字呢? 根據 Assigning Field Numbers 的說明

Assigning Field Numbers
As you can see, each field in the message definition has a unique number. These field numbers are used to identify your fields in the message binary format, and should not be changed once your message type is in use. Note that field numbers in the range 1 through 15 take one byte to encode, including the field number and the field’s type (you can find out more about this in Protocol Buffer Encoding). Field numbers in the range 16 through 2047 take two bytes. So you should reserve the numbers 1 through 15 for very frequently occurring message elements. Remember to leave some room for frequently occurring elements that might be added in the future.

這是說,當資料傳送的時候是用 message binary format ,該數字是用來 定位 該欄位。 而數字1-15 是只用了 1 byte encode, 16-2047 用了 2 btyes encode ,所以建議常用的欄位可以放在前 15 個欄位,可以多少減少一些資料的傳送量。

為了今天的 Demo 能夠簡單,我將使用 Message Definition 提供的範例,當作是我們這篇的範例。

define message
  • proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";
option go_package = "pb";

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}

上敘 Message Definition 非常簡單,就定義兩個 Message Type 一個是 HelloRequest 一個是 HelloReply ,然後還有定義一個 RPC 的服務 rpc SayHello (HelloRequest) returns (HelloReply) {},而等等的實作程式碼,就是要實作 SayHello 這個方法。

定義好 Message 之後,我們就使用指令 $ protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/message.proto 產生程式碼,如下範例,

gen code
  • bash
1
$ protoc -I pb/ pb/messages.proto --go_out=plugins=grpc:pb

也可以使用另一種寫法 $ protoc --go_out=plugins=grpc:$DST_DIR $SRC_DIR,例如 $ protoc --go_out=plugins=grpc:. pb/*.proto,參考:gRPC Support

執行完後,就會看到有一份 messages.pb.go 的程式碼被產生,這就是我們定義好的訊息介面,等等會要用到這份程式碼來實作 Server Code 與 Client Code。

gen-codegen-code

實作 Server Code

我們打開 messages.pb.go ,裡面有一個 interface 如下,這是我們要實作 Server Code 的方法。

GreeterServer inteface
  • go
1
2
3
4
5
// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

所以這邊我們定義一個 MessageService ,實作 GreeterServer interface 的內容。 我的實作內容只是讀取 HellowRequest 的資訊,加一些訊息後回傳給 Client。

MessageService
  • go
1
2
3
4
5
6
type MessageService struct{
}

func (m MessageService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Server say hello to " + in.GetName()}, nil
}

建立 Server

接下來請參考下方的程式碼,我們要把 MessageService 註冊到 gRPC 的 Server ,並且將 gRPC Server 執行起來。

gRPC Server
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"google.golang.org/grpc"
"log"
"net"
"trygrpc/pb"
"trygrpc/service"
)

const (
port = ":50051"
)
func main() {
// Create gRPC Server
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
log.Println("gRPC server is running.")
pb.RegisterGreeterServer(s, &service.MessageService{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

這樣我們 Server Code 就定義好了!!!

實作 Client Code

一樣我們打開 messages.pb.go ,尋找下面那段程式碼,就會發現 Client 的程式碼我們不用自己實作,他已經幫我們產生好了!!! 我們只需要直接用。

Message Client
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
cc grpc.ClientConnInterface
}

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {
return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := c.cc.Invoke(ctx, "/Greeter/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}

而使用方式如下

Client
  • go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"context"
"google.golang.org/grpc"
"log"
"time"
"trygrpc/pb"
)

const (
address = "localhost:50051"
)

func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

// Contact the server and print out its response.
name := "Miles"

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
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())
}

這樣 Client Code 也完成了!!!

執行程式碼

接下來就先把 Server 先跑起來,然後再執行 Client。 確定跑起來後,我們就完成最簡單的 gRPC 的範例的!!

call-grpccall-grpc

最後我有把我的範例程式上傳到 Gihub 上,可以參考 MilesLin/gRPC-helloworld

官方提供的程式碼範例

今天範例是來自官方,可以從這邊找到 grpc-Build the example。 簡單的說我們只要安裝好 go get -u google.golang.org/grpc 的 package,就可以在 $GOPATH/src/google.golang.org/grpc/examples/helloworld 資料夾底下找到各種 grpc 的範例。 而這些範例也有放在 github 上 grpc-go/examples/features/

延伸閱讀

[grpc.io]
[What are protocol buffers]
[Protocol Buffers]
[Enhancing Application Communication with gRPC]
[Go Quick Start]
[gRPC Concepts]
[grpc-go/examples]