前言:實在太久沒更新了,回來做一個簡短教學。如果對德州撲克算牌有興趣的也可以繼續看下去。

閱讀須知

  1. 需要對 Golang 有一定的基礎知識 (本文使用 go 1.19 作為教學版本)
  2. 了解以下的名詞 gRPCProtocol BuffersHTTP/2
  3. 這篇文章不講名詞原理,著重在實作的解釋上。
  4. 本文的程式碼在這裡 → grpc-demo

目標

  1. 實作一個 server.go 讓他與其他 client 透過 gRPC 的方式進行資料交換
  2. 這邊的 client 本文會暫時用 BloomRPC 這個類似 Postman 的工具代替
  3. 其中的資料交換我會用德州撲克的算牌作為內容,如果想知道德州撲克的算牌獲勝率可以參考我另外一個專案 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 則是代表這個參數我們可以傳輸任意次 (包含零次),我們會視為他是一個 Arraystring 則是定義這個參數的型別,最後的 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 可以透過指令調整。想多了解可以自己查詢 goflag 包。

  • 輸入第二段程式碼
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 也沒有被佔用的話,應該會出現如下圖結果。
伺服器已經啟動且監聽了 port 50051
  • 接下來打開測試工具 BloomRPC,左上角匯入 proto 檔案,接著輸入對應格式的資料,發送之後理論上就會收到伺服器的回應。
最大的牌是兩對沒錯

結語

最基本的教學就到這邊,後續有空的話會再補上 client 的實作,我們目前是用 BloomRPC 取代。

後續如果有更深入的說明會另開文章或是補充在下面。
如果有錯誤或問題歡迎糾正或提出。

參考資料

https://grpc.io/docs/languages/go
https://github.com/grpc/grpc-go
https://developers.google.com/protocol-buffers