Golang 語言 gRPC 服務(wù)怎么同時支持 gRPC 和 HTTP 客戶端調(diào)用?
01介紹
關(guān)于 gRPC 的文章,我們之前寫過幾篇,如果讀者朋友還對 gRPC 不了解,我建議您可以翻閱一下公眾號的歷史文章。
當(dāng)我們需要提供 gRPC 服務(wù)的 RESTful API 時,可以先創(chuàng)建一個 gRPC 客戶端服務(wù),在 gRPC 客戶端服務(wù)編寫 RESTful API,接收到 HTTP 請求時,通過 gRPC 客戶端服務(wù)調(diào)用 gRPC 服務(wù)端服務(wù)的方法。
相信讀者朋友們也意識到,僅僅為了提供 RESTful API 而編寫一個 gRPC 客戶端服務(wù),顯然有些小題大做。
在不借助 gRPC 客戶端服務(wù)的前提下,gRPC 服務(wù)端服務(wù)怎么同時支持 gRPC 和 HTTP 客戶端調(diào)用?今天我們介紹一個 protoc 插件 gRPC-Gateway。
02gRPC-Gateway
gRPC-Gateway 是 protoc 的一個插件。它讀取 gRPC 服務(wù)定義并生成一個反向代理服務(wù)器,該服務(wù)器將 RESTful JSON API 轉(zhuǎn)換為 gRPC。此服務(wù)器是根據(jù) gRPC 定義中的自定義選項生成的。
gRPC-Gateway 可幫助您同時以 gRPC 和 RESTful 風(fēng)格提供 API。
在我們開始編碼之前,需要一些先決條件。
首先,我們需要先搭建一個 Go 環(huán)境。
使用 go get 工具下載一些依賴包。
使用 go mod init 工具創(chuàng)建一個 go.mod 文件。
依賴包列表:
$ 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
03gRPC-Gateway 實戰(zhàn)
在完成以上先決條件后,我們創(chuàng)建一個 gRPC 服務(wù)端服務(wù),本文我們創(chuàng)建一個 ToDoList gRPC 服務(wù)。在創(chuàng)建 gRPC 服務(wù)之前,我們使用 protocol buffers 創(chuàng)建一個 proto 文件。
創(chuàng)建 proto 文件
...
service ToDoList {
rpc CreateToDoList (ToDoListDetail) returns (CreateToDoListResult) {}
rpc ReadToDoList (ToDoListPage) returns (ReadToDoListByPage) {}
}
...
生成 gRPC 服務(wù)端存根
使用 protoc 命令工具生成存根
protoc -I proto \
--go_out ./pb/todoPb --go_opt paths=source_relative \
--go-grpc_out ./pb/todoPb --go-grpc_opt paths=source_relative \
proto/toDoList.proto
執(zhí)行上面 protoc 命令工具,生成一個 *.pb.go 文件和一個 *_grpc.pb.go 文件。
編寫剩余 Go 代碼
創(chuàng)建 main.go
func main() {
InitEngine()
lis, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
server := grpc.NewServer()
pb.RegisterToDoListServer(server, new(service.ToDoList))
log.Printf("server listening at %v\n", lis.Addr())
if err := server.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
添加 gRPC-Gateway 選項
gRPC-Gateway 使用 google.api.http 選項定義 gRPC 服務(wù)如何映射到 JSON 請求和響應(yīng),使用 protoc 時,每個 RPC 必須使用 google.api.http 選項定義 HTTP 方法和路徑。
因此,我們需要將 google/api/http.proto 導(dǎo)入添加到 proto 文件中。我們還需要添加我們想要的 HTTP -> gRPC 映射。
syntax = "proto3";
import "google/api/annotations.proto";
service ToDoList {
rpc CreateToDoList (ToDoListDetail) returns (CreateToDoListResult) {
option (google.api.http) = {
post: "/v1/todolist/add"
body: "*"
};
}
rpc ReadToDoList (ToDoListPage) returns (ReadToDoListByPage) {
option (google.api.http) = {
get: "/v1/todolist/select"
};
}
}
...
關(guān)于 HTTP 和 gRPC 映射的更多內(nèi)容,可以參閱 Google API 文檔。
生成 gRPC-Gateway 存根
現(xiàn)在,我們已將 gRPC-Gateway 選項添加到 proto 文件中,我們需要使用 gRPC-Gateway 生成器來生成存根。
在使用 protoc 生成存根之前,我們需要將一些依賴項復(fù)制到 proto 文件目錄中。將 googleapis 的子集從官方存儲庫下載并復(fù)制到本地 proto 文件目錄中。如下所示:
.
├── dao
│ ├── mysql.go
│ └── toDoList.go
├── grpc-gateway
│ └── main.go
├── main.go
├── pb
│ └── todoPb
│ ├── toDoList.pb.go
│ ├── toDoList.pb.gw.go
│ └── toDoList_grpc.pb.go
├── proto
│ │ └── api
│ │ ├── annotations.proto
│ │ └── http.proto
│ └── toDoList.proto
└── service
└── toDoList.go
使用 protoc 生成存根
protoc -I proto \
--go_out ./pb/todoPb --go_opt paths=source_relative \
--go-grpc_out ./pb/todoPb --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./pb/todoPb --grpc-gateway_opt paths=source_relative \
proto/toDoList.proto
protoc-go-inject-tag -XXX_skip=xorm -input=./pb/todoPb/toDoList.pb.go
執(zhí)行以上 protoc 命令工具,生成一個 *.gw.pb.go 文件。
創(chuàng)建 grpc-gateway 目錄,并創(chuàng)建 main.go 文件,創(chuàng)建 gRPC-Gateway 多路復(fù)用器。
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := pb.RegisterToDoListHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts)
if err != nil {
log.Fatalf("Fail to register gRPC gateway service endpoint: %v", err)
}
if err = http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("Could not setup HTTP endpoint: %v", err)
}
}
啟動服務(wù)
grpc 服務(wù)
go run main.go
gRPC-Gateway
go run grpc-gateway/main.go
cURL 測試
curl http://127.0.0.1:8080/v1/todolist/select?page=1&count=2
響應(yīng)結(jié)果:
{
"todolist": [
{
"id": "1",
"content": "編程寫代碼",
"datetime": "1632541505",
"created": "1632541505",
"updated": "1632541505"
},
{
"id": "2",
"content": "編程寫代碼",
"datetime": "1632543373",
"created": "1632543373",
"updated": "1632543373"
}
]
}
04總結(jié)
本文我們介紹 gRPC-Gateway 如何實現(xiàn)同時支持 gRPC 和 RESTful 風(fēng)格的 API。
當(dāng) HTTP 請求到達(dá) gRPC-Gateway 時,它會將 JSON 數(shù)據(jù)解析為 protobuf 消息。然后,它使用解析的 protobuf 消息發(fā)出正常的 Go gRPC 客戶端請求。
Go gRPC 客戶端將 protobuf 結(jié)構(gòu)編碼為 protobuf 二進(jìn)制格式,并將其發(fā)送到 gRPC 服務(wù)器。gRPC 服務(wù)器處理請求并以 protobuf 二進(jìn)制格式返回響應(yīng)。
Go gRPC 客戶端將其解析為 protobuf 消息,并將其返回到 gRPC-Gateway,后者將 protobuf 消息編碼為 JSON 并將其返回到原始客戶端。
圖片來自 gRPC-Gateway 官方文檔