2022 用 Golang 實作簡易 gRPC Server
前言:實在太久沒更新了,回來做一個簡短教學。如果對德州撲克算牌有興趣的也可以繼續看下去。
閱讀須知
- 需要對 Golang 有一定的基礎知識 (本文使用 go 1.19 作為教學版本)
- 了解以下的名詞 gRPC、Protocol Buffers、HTTP/2。
- 這篇文章不講名詞原理,著重在實作的解釋上。
- 本文的程式碼在這裡 → grpc-demo
目標
- 實作一個 server.go 讓他與其他 client 透過 gRPC 的方式進行資料交換
- 這邊的 client 本文會暫時用 BloomRPC 這個類似 Postman 的工具代替
- 其中的資料交換我會用德州撲克的算牌作為內容,如果想知道德州撲克的算牌獲勝率可以參考我另外一個專案 Poker Hand Evaluator
第一階段: Protocol Buffers
- 開啟一個全新的 Golang project (以下假設你的專案名稱也跟我一樣,叫做 grpc-demo),預期目前裡面現在只會有一個 go.mod。
- 新增一個資料夾叫做 proto,裡面預計放我們的 .proto 檔案
- 進入這個資料夾新增一個檔案名稱為 poker.proto 的檔案,並貼上以下程式碼。
syntax = "proto3";
package poker;
option go_package = "proto/grpc-demo";
message GetNutsRequest {
repeated string hand = 1; // 手牌
repeated string river = 2; // 公共牌
}
message GetNutsResponse {
string card = 1; // 最強手牌
}
service Poker {
rpc GetNuts(GetNutsRequest) returns (GetNutsResponse);
}
以下開始說明上述的內容
syntax = "proto3"
指的是 proto 語法的版本
package poker;
是防止名稱衝突一個類似 namespace 的概念,官方說明
option go_package = "proto/grpc-demo";
定義你生成 .pb 檔案時的 package
message GetNutsRequest {
repeated string hand = 1; // 手牌
repeated string river = 2; // 公共牌
}
message GetNutsRequest 定義了我傳入的參數,GetNutsRequest 這個名字是可以自由命名的,第一個是 hand (手牌),第二個參數是 river (公共牌),repeated 則是代表這個參數我們可以傳輸任意次 (包含零次),我們會視為他是一個 Array,string 則是定義這個參數的型別,最後的 1 跟 2,則只要定義為不同的數字即可。
message GetNutsResponse {
string card = 1; // 最強手牌
}
message GetNutsResponse 定義了我要回傳的參數,GetNutsResponse 這個名字是可以自由命名的,回傳的 card 參數是 string 型別的。
service Poker {
rpc GetNuts(GetNutsRequest) returns (GetNutsResponse);
}
這定義了我 Poker 這個 Service 提供了一個 GetNuts 的方法,所以我 server 必須要實作,GetNutsRequest 就是要求 client 傳進來的參數,而 server 回傳 GetNutsResponse。
說明:這個 GetNuts 會收集使用者的手牌及公共牌,算出一個最強的牌型,如兩對、三條、同花順…等等。
- 接著我們打開終端機,安裝 protoc 的工具,輸入以下指令安裝 ,這段滿多朋友可能會因為蘋果電腦是 Intel 晶片或是 Apple 晶片的不同,而出現一些問題,尤其是 Apple 晶片,如果有錯誤的人歡迎截圖在下面問我。
brew install protobuf
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- 接著一樣是終端機,我們切到專案內 proto 的資料夾底下後,輸入
/opt/homebrew/bin/protoc --go_out=. --go_opt=paths=source_relative \--go-grpc_out=. --go-grpc_opt=paths=source_relative \poker.proto
如此一來我們 proto 資料夾底下就會出現一個 .pb.go 的檔案以及一個 _grpc.pb.go 的檔案。
- .pb.go 包含用於 protobuf 消息的序列化/反序列化的程式
- _grpc.pb.go 包含 gRPC 服務器和客戶端的程式
如此一來我們 Protocol Buffers 的相關準備都做好了,接下來就是準備 server.go 的實作!
第二階段: Server 實作
- 打開終端機在專案底下輸入
go get -u google.golang.org/grpc
才可以使用 gRPC 的套件。 - 在專案根目錄底下新增一個 server.go 檔案
- 輸入第一段程式碼
var (
port = flag.Int("port", 50051, "The server port")
)
這段沒什麼,只是讓我們的 port 可以透過指令調整。想多了解可以自己查詢 go 的 flag 包。
- 輸入第二段程式碼
type server struct {
pb.UnimplementedPokerServer
}
這段是要求我們要實作當初定義的方法。下面會用到。
- 輸入第三段程式碼
func (s *server) GetNuts(ctx context.Context, req *pb.GetNutsRequest) (*pb.GetNutsResponse, error) {
res, err := poker.PokerEvaluator(req.Hand, req.River)
return &pb.GetNutsResponse{
Card: res,
}, err
}
這段就是我們需要實作的 GetNuts,想要真的實作的人可 clone 我的專案( →grpc-demo ),然後複製我對應的資料夾跟程式碼 (就是根目錄底下的 poker 資料夾)。
如果沒興趣的可以直接回傳一些固定字串即可,但接收內容跟回傳內容還是要符合當初定義的 GetNutsRequest 以及 GetNutsResponse。
- 輸入主程式
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterPokerServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
flag.Parse()
接收外部傳進來的參數
接下來監聽 port 50051 ,然後起一個 gRPC Server,接著註冊我們的服務進去,最後把服務建立起來。
另外其中的 reflection 其實我們的服務是不需要的,詳細需要了解可以查看這裡的官方說明,這也是我目前正在研究的課題。
第三階段: 測試
當完成所有事情之後我們就可以開始測試了
- 在專案根目錄下輸入
go run server.go
如果你的程式碼都沒有出錯, port 也沒有被佔用的話,應該會出現如下圖結果。
- 接下來打開測試工具 BloomRPC,左上角匯入 proto 檔案,接著輸入對應格式的資料,發送之後理論上就會收到伺服器的回應。
結語
最基本的教學就到這邊,後續有空的話會再補上 client 的實作,我們目前是用 BloomRPC 取代。
後續如果有更深入的說明會另開文章或是補充在下面。
如果有錯誤或問題歡迎糾正或提出。