API 文件就是你的伺服器,REST 的另一個選擇:gRPC

在前幾個星期的文章中我們提到了「比起 JSON 更方便、更快速、更簡短的 Protobuf 格式」,但這還沒完。Google 為此推出了一個新的程式呼叫協議 gRPC,簡單說就是能夠從你的 Protobuf 檔案生出一個伺服器出來,這個好處就是不需要像傳統 RESTful API 伺服器ㄧ樣撰寫一堆路由。

當我們撰寫傳統 RESTful API 伺服器時,我們會花費許多的時間在路徑命名上。除此之外,兩台 REST 伺服器如果要進行溝通,就必須知道對方的 RESTful API 路由⋯⋯等,步驟十分麻煩,這些在 gRPC 中都有所改善。

結構就是伺服器

還記得 .proto 檔案可以用來定義資料結構嗎?現在我們在裡面新增了一個 service 用來定義一個服務,而其中的 rpc Plus(.. 定義著這個服務有什麼樣的功能。當你用上這些方法之後,你的 Protobuf 就升級成 gRPC 了。

// Calculator 定義了一個計算用的服務。
service Calculator {  
    // Plus 會接收 CalcRequest 資料作加總,最終會回傳 CalcReply。
    rpc Plus (CalcRequest) returns (CalcReply) {}
}

// CalcRequest 包含了兩個數字,將會傳送至計算服務並對兩個數字進行計算。
message CalcRequest {  
    int32 number_a = 1;
    int32 number_b = 2;
}

// CalcReply 是計算結果,將會回傳給客戶端。
message CalcReply {  
    int32 result = 1;
}

你的伺服器具有哪些功能、接收哪些資料現在都已經寫得十分明確了,是時候透過這個結構建立伺服器了。


1. 安裝 gRPC 所需工具

在安裝之前請記得先安裝主要的 Protobuf 生成工具,接著透過下列指令安裝 gRPC 套件。

$ go get google.golang.org/grpc

然後是安裝擴充插件,這樣 Protobuf 才能夠將其中 gRPC 的用法轉化成程式語言。

$ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

接著宣告環境變數,這樣才能直接在終端機使用 Go 的程式。

$ export PATH=$PATH:$GOPATH/bin

2. 建立資料夾

先依照下列結構建立三個資料夾。

.
├── client   // gRPC 客戶端
├── pb       // 擺放 Protobuf 文件
└── server   // gRPC 伺服器

在這裡稍微解釋一下這三個資料夾的分別用途:

  • client:這裡會放置 gRPC 客戶端,客戶端就是用來和伺服器溝通的程式。如果你是撰寫網路服務,那麼就有點像是瀏覽器對伺服器的感覺。

(要注意的是瀏覽器現在還不能直接跟 gRPC 伺服器溝通,所以你需要安插一個 Gateway 將請求轉到 gRPC 客戶端)

  • pb:這個資料夾會擺放我們定義的 Protobuf 文件,而這裡也會放置轉化後的 Protobuf 程式。
  • server:這裡則是擺置 gRPC 伺服器,就像是平常所撰寫的 RESTful API 伺服器一般。

理解了之後我們就可以到下一步啦。

3. 轉化 Proto 文件

將下列的內容存檔成 calc.proto,並擺至於 /pb 資料夾內。

// 撰寫格式是 Proto v3。
syntax = "proto3";  
// 生成的程式在 Golang 中將會屬於 `pb` 套件。
package pb;

// Calculator 定義了一個計算用的服務。
service Calculator {  
    // Plus 會接收 CalcRequest 資料作加總,最終會回傳 CalcReply。
    rpc Plus (CalcRequest) returns (CalcReply) {}
}

// CalcRequest 包含了兩個數字,將會傳送至計算服務並對兩個數字進行計算。
message CalcRequest {  
    int32 number_a = 1;
    int32 number_b = 2;
}

// CalcReply 是計算結果,將會回傳給客戶端。
message CalcReply {  
    int32 result = 1;
}

現在我們要將這個 Protobuf 文件轉化成 Golang 程式,這樣我們才能夠在 Golang 中使用,先確定終端機進入了 /pb 資料夾,然後執行下列指令。

$ protoc --go_out=plugins=grpc:. *.proto

現在你的目錄結構看起來會像這樣。

.
├── client
├── pb
│   ├── calc.pb.go
│   └── calc.proto
└── server

接著我們要透過這個生成的 .pb.go 檔案來建造出我們的 gRPC 伺服器。

4. 建立 gRPC 伺服器

我們將會建立一個像下列圖片所示的 gRPC 伺服器,這個伺服器將會有一個 Plus() 函式,會接收 CalcRequest 然後做處理,最終回傳 CalcReply

先在你的 /server 目錄下建立一個新的 main.go,然後引用這些套件。

package main

import (  
    "log"
    "net"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

    "./../pb"
)

接著我們就要開始實作剛才 Protobuf 內所定義的功能啦!

// server 建構體會實作 Calculator 的 gRPC 伺服器。
type server struct{}

// Plus 會將傳入的數字加總。
func (s *server) Plus(ctx context.Context, in *pb.CalcRequest) (*pb.CalcReply, error) {

    // 計算傳入的數字。
    result := in.NumberA + in.NumberB

    // 包裝成 Protobuf 建構體並回傳。
    return &pb.CalcReply{Result: result}, nil
}

現在讓我們在 main() 函式中監聽 :50051 埠口,然後在這個埠口部署我們的 Calculator 伺服器。

func main() {  
    // 監聽指定埠口,這樣服務才能在該埠口執行。
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("無法監聽該埠口:%v", err)
    }

    // 建立新 gRPC 伺服器並註冊 Calculator 服務。
    s := grpc.NewServer()
    pb.RegisterCalculatorServer(s, &server{})

    // 在 gRPC 伺服器上註冊反射服務。
    reflection.Register(s)

    // 開始在指定埠口中服務。
    if err := s.Serve(lis); err != nil {
        log.Fatalf("無法提供服務:%v", err)
    }
}

接下來這樣就完成了你的第一個 gRPC 伺服器,不過先別急著啟動⋯⋯。這看起來可能很茫然,因為比起傳統的 RESTful API 伺服器來說,你並不需要在 gRPC 裡面定義路由。但是別擔心!等一下寫到客戶端的時候你可能就會理解了。

5. 建立 gRPC 客戶端

當你有了伺服器之後,你就需要有能夠和伺服器互動的程式,而這就是客戶端。同樣地,我們可以透過剛才生成的 Protobuf 程式輕易地直接呼叫伺服器的程式,而不用像以前使用 RESTful API 伺服器時還要自己翻閱 API 文件。

現在到 /client 目錄下建立一個 main.go 檔案,並引用下列套件。

package main

import (  
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    "./../pb"
)

然後在 main() 函式中初始化與遠端 gRPC 伺服器的連線,然後直接使用遠端的 Plus() 函式,像這樣。

func main() {  
    // 連線到遠端 gRPC 伺服器。
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("連線失敗:%v", err)
    }
    defer conn.Close()

    // 建立新的 Calculator 客戶端,所以等一下就能夠使用 Calculator 的所有方法。
    c := pb.NewCalculatorClient(conn)

    // 傳送新請求到遠端 gRPC 伺服器 Calculator 中,並呼叫 Plus 函式,讓兩個數字相加。
    r, err := c.Plus(context.Background(), &pb.CalcRequest{NumberA: 32, NumberB: 32})
    if err != nil {
        log.Fatalf("無法執行 Plus 函式:%v", err)
    }
    log.Printf("回傳結果:%d", r.Result)
}

接下來就是時候看看我們的 gRPC 成果了。

6. 測試 gRPC 伺服器與客戶端

現在進入最上層的目錄,透過下列指令啟動伺服器和客戶端。

# 啟動 gRPC 伺服器。
go run ./server/main.go  
# 開啟客戶端與伺服器溝通。
go run ./client/main.go  

接著就能看見下列結果,這個結果是客戶端傳遞 3232 到 gRPC 伺服器作為加總計算所回傳的結果。

2017/03/12 06:37:53 回傳結果:64  

神奇之處

通常來說我們要呼叫一個 RESTful API 伺服器,不外乎就是要在程式裡面透過 HTTP 請求去傳送,這個時候有點麻煩,例如說你有個路徑為 /user/:id 的網址,這個時候你還要先把使用者編號從資料裡面獨立出來,放到網址中,像這樣 fmt.Sprintf("/user/%d", u.ID),久了之後又不易維護。

透過 gRPC,我們可以省去 HTTP 請求還有那煩躁的網址參數問題,仔細看看客戶端的這一段程式。

// 傳送新請求到遠端 gRPC 伺服器 Calculator 中,並呼叫 Plus 函式,讓兩個數字相加。
r, err := c.Plus(context.Background(), &pb.CalcRequest{NumberA: 32, NumberB: 32})  

沒錯,你可以直接在本地程式直接呼叫遠端程式,用法十分貼切。因為我們有 Protobuf 協助我們在客戶端和伺服端都共享同一份協定、結構。