
gRPC-Gateway 簡介
我們都知道 gRPC 並不是萬能的工具。 在某些情況下,我們仍然想提供傳統的 HTTP/JSON API。原因可能從保持向后兼容性到支持編程語言或 gRPC 無法很好地支持的客戶端。但是僅僅為了公開 HTTP/JSON API 而編寫另一個服務是一項非常耗時且乏味的任務。
那么,有什么方法可以只編寫一次代碼,卻可以同時在 gRPC 和 HTTP/JSON 中提供 API?
答案是 Yes。
gRPC-Gateway 是 Google protocol buffers compiler protoc 的插件。 它讀取 protobuf service 定義並生成反向代理服務器( reverse-proxy server) ,該服務器將 RESTful HTTP API 轉換為 gRPC。 該服務器是根據服務定義中的 google.api.http 批注(annotations)生成的。
這有助於你同時提供 gRPC 和 HTTP/JSON 格式的 API。
開始之前
在開始編碼之前,我們必須安裝一些工具。
在示例中,我們將使用 Go gRPC Server,因此請首先從 https://golang.org/dl/ 安裝 Go。
安裝 Go 之后,請使用 go get 下載以下軟件包:
$ go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
$ go get google.golang.org/protobuf/cmd/protoc-gen-go
$ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
這將安裝我們生成存根所需的協議生成器插件。確保將 $GOPATH/bin 添加到 $PATH 中,以便通過 go get 安裝的可執行文件在 $PATH 中可用。
我們將在本教程的新模塊中進行工作,因此,請立即在您選擇的文件夾中創建該模塊:
創建 go.mod 文件
使用 go mod init 命令啟動你的 module 以創建 go.mod 文件。
運行 go mod init 命令,給它代碼所在 module 的路徑。在這里,使用 github.com/myuser/myrepo 作為 module 路徑—在生產代碼中,這將是可以從其中下載 module 的 URL。
$ go mod init github.com/myuser/myrepo
go: creating new go.mod: module github.com/myuser/myrepo
go mod init 命令創建一個 go.mod 文件,該文件將您的代碼標識為可以從其他代碼中使用的 module。 您剛創建的文件僅包含模塊名稱和代碼支持的 Go 版本。 但是,當您添加依賴項(即其他模塊的軟件包)時,go.mod 文件將列出要使用的特定 module 版本。 這樣可以使構建具有可復制性,並使您可以直接控制要使用的 module 版本。
用 gRPC 創建一個簡單的 hello world
為了了解 gRPC-Gateway,我們首先要制作一個 hello world gRPC 服務。
使用 protocol buffers 定義 gRPC service
在創建 gRPC 服務之前,我們應該創建一個 proto 文件來定義我們需要的東西,這里我們在 proto/helloworld/ 目錄下創建了一個名為 hello_world.proto 的文件。
gRPC service 使用 Google Protocol Buffers 定義的。這里定義如下:
syntax = "proto3";
package helloworld;
// 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;
}
使用 buf 生成 stubs
Buf 是一個工具,它提供了各種 protobuf 實用程序,如 linting, breaking change detection 和 generation。請在 https://docs.buf.build/installation/ 上找到安裝說明。
它是通過 buf.yaml 文件配置的,應將其檢入你存儲庫的根目錄中。 如果存在,Buf 將自動讀取此文件。 也可以通過命令行標志 --config 提供配置,該標志接受 .json 或 .yaml 文件的路徑,或是直接 JSON 或 YAML 數據。
所有使用本地 .proto 文件作為輸入的 Buf 操作都依賴於有效的構建配置。這個配置告訴 Buf 在哪里搜索 .proto 文件,以及如何處理導入。與 protoc(所有 .proto 文件都是在命令行上手動指定的)不同,buf 的操作方式是遞歸地發現配置下的所有 .proto 文件並構建它們。
下面是一個有效配置的示例,假設您的 .proto 文件根位於相對於存儲庫根的 proto 文件夾中。
version: v1beta1
name: buf.build/myuser/myrepo
build:
roots:
- proto
要為 Go 生成 type 和 gRPC stubs,請在存儲庫的根目錄下創建文件 buf.gen.yaml:
version: v1beta1
plugins:
- name: go
out: proto
opt: paths=source_relative
- name: go-grpc
out: proto
opt: paths=source_relative
我們使用 go 和 go-grpc 插件生成 Go types 和 gRPC service 定義。我們正在輸出相對於 proto 文件夾的生成文件,並使用 path=source_relative 選項,這意味着生成的文件將與源 .proto 文件顯示在同一目錄中。
然后運行:
$ buf generate
這將為我們的 proto 文件層次結構中的每個 protobuf 軟件包生成一個 *.pb.go 和 *_grpc.pb.go 文件。
使用 protoc 生成 stubs
這是一個 protoc 命令可能會生成 Go stubs 的示例,假設您位於存儲庫的根目錄,並且您的 proto 文件位於一個名為 proto 的目錄中:
$ protoc -I ./proto \
--go_out ./proto --go_opt paths=source_relative \
--go-grpc_out ./proto --go-grpc_opt paths=source_relative \
./proto/helloworld/hello_world.proto
我們使用 go 和 go-grpc 插件生成 Go types 和 gRPC service 定義。我們正在輸出相對於 proto 文件夾的生成文件,並使用 path=source_relative 選項,這意味着生成的文件將與源 .proto 文件顯示在同一目錄中。
這將為 proto/helloworld/hello_world.proto 生成一個 *.pb.go 和 *_grpc.pb.go 文件。
創建 main.go
在創建 main.go 文件之前,我們假設用戶已經創建了一個名為 github.com/myuser/myrepo 的 go.mod。此處的 import 使用的是相對於存儲庫根目錄的 proto/helloworld 中生成的文件的路徑。
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)
type server struct{}
func NewServer() *server {
return &server{}
}
func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}
func main() {
// Create a listener on TCP port
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// Create a gRPC server object
s := grpc.NewServer()
// Attach the Greeter service to the server
helloworldpb.RegisterGreeterServer(s, &server{})
// Serve gRPC Server
log.Println("Serving gRPC on 0.0.0.0:8080")
log.Fatal(s.Serve(lis))
}
將 gRPC-Gateway 批注添加到現有的 proto 文件中
現在,我們已經可以使用 Go gRPC 服務器,我們需要添加 gRPC-Gateway 批注。
批注定義了 gRPC 服務如何映射到 JSON 請求和響應。 使用 protocol buffers 時,每個 RPC 必須使用 google.api.http 批注定義 HTTP 方法和路徑。
因此,我們需要將 google/api/http.proto 導入添加到 proto 文件中。我們還需要添加所需的 HTTP->gRPC 映射。在這種情況下,我們會將 POST /v1/example/echo 映射到我們的 SayHello RPC。
syntax = "proto3";
package helloworld;
import "google/api/annotations.proto";
// Here is the overall greeting service definition where we define all our endpoints
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/example/echo"
body: "*"
};
}
}
// The request message containing the user's name
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
生成 gRPC-Gateway stubs
現在我們已經將 gRPC-Gateway 批注添加到了 proto 文件中,我們需要使用 gRPC-Gateway 生成器來生成存根(stubs)。
使用 buf
我們需要將 gRPC-Gateway 生成器添加到生成配置中:
version: v1beta1
plugins:
- name: go
out: proto
opt: paths=source_relative
- name: go-grpc
out: proto
opt: paths=source_relative,require_unimplemented_servers=false
- name: grpc-gateway
out: proto
opt: paths=source_relative
我們還需要將 googleapis 依賴項添加到我們的 buf.yaml 文件中:
version: v1beta1
name: buf.build/myuser/myrepo
deps:
- buf.build/beta/googleapis
build:
roots:
- proto
然后,我們需要運行 buf beta mod update 以選擇要使用的依賴項版本。
就是這樣!現在,如果您運行:
$ buf generate
它應該產生一個 *.gw.pb.go 文件。
使用 protoc
在使用 protoc 生成 stubs 之前,我們需要將一些依賴項復制到我們的 proto 文件結構中。將一部分 googleapis 從官方存儲庫復制到您本地的原始文件結構中。之后看起來應該像這樣:
proto
├── google
│ └── api
│ ├── annotations.proto
│ └── http.proto
└── helloworld
└── hello_world.proto
現在我們需要將 gRPC-Gateway 生成器添加到 protoc 調用中:
$ protoc -I ./proto \
--go_out ./proto --go_opt paths=source_relative \
--go-grpc_out ./proto --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./proto --grpc-gateway_opt paths=source_relative \
./proto/helloworld/hello_world.proto
這將生成一個 *.gw.pb.go 文件。
我們還需要在 main.go 文件中添加 gRPC-Gateway 多路復用器(mux)並為其提供服務。
package main
import (
"context"
"log"
"net"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)
type server struct{
helloworldpb.UnimplementedGreeterServer
}
func NewServer() *server {
return &server{}
}
func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}
func main() {
// Create a listener on TCP port
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// Create a gRPC server object
s := grpc.NewServer()
// Attach the Greeter service to the server
helloworldpb.RegisterGreeterServer(s, &server{})
// Serve gRPC server
log.Println("Serving gRPC on 0.0.0.0:8080")
go func() {
log.Fatalln(s.Serve(lis))
}()
// Create a client connection to the gRPC server we just started
// This is where the gRPC-Gateway proxies the requests
conn, err := grpc.DialContext(
context.Background(),
"0.0.0.0:8080",
grpc.WithBlock(),
grpc.WithInsecure(),
)
if err != nil {
log.Fatalln("Failed to dial server:", err)
}
gwmux := runtime.NewServeMux()
// Register Greeter
err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
gwServer := &http.Server{
Addr: ":8090",
Handler: gwmux,
}
log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
log.Fatalln(gwServer.ListenAndServe())
}
測試 gRPC-Gateway
現在我們可以啟動服務器了:
$ go run main.go
然后,我們使用 cURL 發送 HTTP 請求:
$ curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'
{"message":"hello world"}
Refs
- https://github.com/iamrajiv/helloworld-grpc-gateway
- https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/
我是為少
微信:uuhells123
公眾號:黑客下午茶
加我微信(互相學習交流),關注公眾號(獲取更多學習資料~)
