寫給 go 開發者的 gRPC 教程 - gRPC Gateway
gRPC 使用 protobuf 來序列化數據,使用 protobuf 序列化的好處這裏就不再贅述了
但 protobuf 不是明文,不方便我們進行調試,如果能像 HTTP1.x 一樣進行訪問,就能減輕調試的負擔;在特殊場景下 client 側無法使用 HTTP2.0,因而也無法使用 gRPC 來進行調用,需要提供降級方案
gRPC-Gateway 是 protobuf 編譯器 protoc
的插件。它讀取 protobuf 文件中service
定義的內容,並生成反向代理服務器 (reverse-proxy server) ,該服務器可以將RESTful API
轉換爲 gRPC
,於是我們就可以像普通的 HTTP1.x 服務器一樣使用 JSON 請求 gRPC 服務
安裝
既然是protoc
的插件,那麼和其他插件的使用類似。先安裝 gRPC-Gateway 插件:protoc-gen-grpc-gateway
。當然protoc-gen-go
和protoc-gen-go-grpc
肯定是需要安裝的,它們兩個用於從 pb 文件生成數據結構和 grpc 服務
$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
生成 gRPC-Gateway 反向代理服務器
在不使用 gRPC-Gateway 時,我們定義的 pb 文件如下
syntax = "proto3";
package ecommerce;
import "google/protobuf/wrappers.proto";
option go_package = "/ecommerce";
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order);
rpc addOrder(Order) returns (google.protobuf.StringValue);
}
message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
google.protobuf.StringValue destination = 5;
}
目前有三種方式可以反向代理服務器
-
不做任何修改直接生成,
protoc-gen-grpc-gateway
會按照默認規則映射 Method 和參數等 HTTP 配置 -
給 protobuf 添加 annotations,可以自定義 Method 和 Path 等 HTTP 配置
-
使用外部配置,比較適用於不能修改源 protobuf 的情況下
下面演示第二種方式
給 protobuf 添加 annotations
gRPC-Gateway 反向代理服務器根據service
中google.api.http
的批註 (annotations
) 生成。
所以我們需要import "google/api/annotations.proto";
google.api.http
可以定義 HTTP 服務的 Method 和 Path 等
syntax = "proto3";
package ecommerce;
option go_package = "ecommerce/";
import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto";
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder"
};
}
}
message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
string destination = 5;
}
代碼生成
因爲使用了非內置的 pb 定義google/api/annotations.proto
,所以需要在生成代碼前需要添加 pb 依賴:從官方倉庫 [1] 複製對應的 pb 文件到本地。添加依賴後目錄結構如下
pb
├── google
│ └── api
│ ├── annotations.proto
│ └── http.proto
└── product.proto
之後執行 protoc 來生成代碼
protoc -I ./pb \
--go_out ./ecommerce --go_opt paths=source_relative \
--go-grpc_out ./ecommerce --go-grpc_opt paths=source_relative \
--grpc-gateway_out ./ecommerce --grpc-gateway_opt paths=source_relative \
./pb/product.proto
生成出來的代碼,對比非 gRPC-Gateway 的版本會多出了一個*.gw.pb.go
文件
使用 buf
使用 protoc 命令不僅需要複製依賴到本地,執行的命令行也比較長
之前介紹過 buf 工具,此時 buf 就能體現出作用了。buf 工具不僅可以簡化代碼生成的命令,還可以解決依賴的問題
🌲 首先在 pb 文件的目錄中初始化 buf
buf mod init
buf 命令會創建buf.yaml
文件,在此文件中添加依賴buf.build/googleapis/googleapis
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
## add
deps:
- buf.build/googleapis/googleapis
🌲 更新依賴
buf mod update
buf 命令從 Buf Schema Registry (BSR)[2] 中獲取依賴,把你所有的 deps
更新到最新版。並且會生成 buf.lock
來固定版本
pb
├── buf.lock
├── buf.yaml
└── product.proto
🌲 創建一個buf.gen.yaml
它是 buf 生成代碼的配置。上面的protoc
同等功能的buf.gen.yaml
可以寫成如下形式,相對 protoc 更加直觀
version: v1
plugins:
- plugin: go
out: ecommerce
opt:
- paths=source_relative
- plugin: go-grpc
out: ecommerce
opt:
- paths=source_relative
- name: grpc-gateway
out: ecommerce
opt:
- paths=source_relative
- generate_unbound_methods=true
🌲 生成代碼
buf generate pb
執行的效果和上文中 protoc 命令一樣
server 端的實現
只生成代碼還不夠,還得啓動 gRPC-Gateway 的反向代理服務器
啓動
有兩種方式來啓動 gRPC-Gateway
🌲 第一種先啓動 gRPC 服務,再以 gRPC 服務的連接爲基礎創建 grpc-gateway 服務
代碼中,我們啓動了 gRPC 的端口8009
,同時也啓用了普通 HTTP 端口8010
,gRPC-Gateway 通過 rpc 的方式訪問 gRPC,所以 gRPC 服務是必須要啓動的
package main
import (
"context"
"net"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
pb "github.com/liangwt/note/grpc/ecosystem/grpc-gateway/ecommerce"
"google.golang.org/grpc"
)
func main() {
grpcPort, gwPort := ":8009", ":8010"
go func() {
lis, err := net.Listen("tcp", grpcPort)
if err != nil {
panic(err)
}
s := grpc.NewServer()
pb.RegisterOrderManagementServer(s, &OrderManagementImpl{})
if err := s.Serve(lis); err != nil {
panic(err)
}
}()
// 建立一個到gRPC Port的連接
conn, err := grpc.DialContext(
context.Background(),
"127.0.0.1"+grpcPort,
grpc.WithBlock(),
grpc.WithInsecure(),
)
if err != nil {
panic(err)
}
gwmux := runtime.NewServeMux()
err = pb.RegisterOrderManagementHandler(context.Background(), gwmux, conn)
if err != nil {
panic(err)
}
http.ListenAndServe(gwPort, gwmux)
// 以下和http.ListenAndServe(gwPort, gwmux)等價
// gwServer := &http.Server{
// Addr: gwPort,
// Handler: gwmux,
// }
// if err := gwServer.ListenAndServe(); err != nil {
// panic(err)
// }
}
🌲 還有一種方式不依賴 grpc 服務,以本地函數調用的方式實現
第一種方式使用RegisterOrderManagementHandler
函數,這種方式使用RegisterOrderManagementHandlerServer
。可以看到我們僅啓用了8010
的 HTTP 端口,gRPC-Gateway 通過本地函數調用的方式訪問 gRPC
package main
import (
"context"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
pb "github.com/liangwt/note/grpc/ecosystem/grpc-gateway/ecommerce"
)
func main() {
gwmux := runtime.NewServeMux()
err := pb.RegisterOrderManagementHandlerServer(context.Background(), gwmux, &OrderManagementImpl{})
if err != nil {
panic(err)
}
http.ListenAndServe(":8010", gwmux)
}
測試訪問
無論哪種方式啓動 gRPC-Gateway,都可以通過 HTTP 進行訪問
$ curl -s -X GET \
'127.0.0.1:8010/v1/getOrder?value=101' \
--header 'Accept: */*' | jq
{
"id": "101",
"items": [
"Google",
"Baidu"
],
"description": "example",
"price": 0,
"destination": "example"
}
這裏有個細節需要注意,google.protobuf.StringValue
在映射到 HTTP 的時候默認參數名爲value
,所以訪問時請求參數寫成value=101
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder"
};
}
}
進階
GET 請求參數
對於 GET 請求參數也可以定義到 HTTP 的 PATH 中,pb 文件這樣寫
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder/{value}"
};
}
}
curl -X GET \
'127.0.0.1:8010/v1/getOrder/101' \
--header 'Accept: */*' \
我們還可以給參數設定個名字
service OrderManagement {
rpc getOrder(getOrderReq) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder"
};
}
}
message getOrderReq {
google.protobuf.StringValue id = 1;
}
curl -X GET \
'127.0.0.1:8010/v1/getOrder?id=101' \
--header 'Accept: */*' \
依舊可以把參數放到 path 中
service OrderManagement {
rpc getOrder(getOrderReq) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder/{id}"
};
}
}
message getOrderReq {
google.protobuf.StringValue id = 1;
}
curl -X GET \
'127.0.0.1:8010/v1/getOrder/101' \
--header 'Accept: */*' \
POST 請求
gRPC-Gatewway 當然可以生成 POST 請求,示例的 pb 文件如下
syntax = "proto3";
package ecommerce;
import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto";
option go_package = "/ecommerce";
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order) {
option (google.api.http) = {
get : "/v1/getOrder"
};
}
rpc addOrder1(Order) returns (google.protobuf.StringValue) {
option (google.api.http) = {
post : "/v1/addOrder1"
body : "*"
};
}
rpc addOrder2(OrderRequest) returns (google.protobuf.StringValue) {
option (google.api.http) = {
post : "/v1/addOrder2"
body : "order"
};
}
}
message OrderRequest {
Order order = 1;
}
message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
google.protobuf.StringValue destination = 5;
}
🌲 對於addOrder1
接口
通過 gRPC 請求時,入參需要一個Order
,body : "*"
表示在 HTTP 的請求中的 body 需要包含Order
的所有字段
$ curl -s -X POST \
'127.0.0.1:8010/v1/addOrder1' \
--header 'Accept: */*' \
--data '{"id": "102","items": ["Google","Baidu"],"description": "example","price": 0,"destination": "example"}'
🌲 對於addOrder2
接口
通過 gRPC 請求時,入參需要一個OrderRequest
,body : "order"
表示在 HTTP 的請求中的 body 需要包含OrderRequest
的order
字段
$ curl -s -X POST \
'127.0.0.1:8010/v1/addOrder2' \
--header 'Accept: */*' \
--data '{"id": "102","items": ["Google","Baidu"],"description": "example","price": 0,"destination": "example"}'
添加自定義路由
我們還可以在 pb 文件生成的路由基礎上添加自定義的路由
func main() {
gwmux := runtime.NewServeMux()
err := pb.RegisterOrderManagementHandlerServer(context.Background(), gwmux, &OrderManagementImpl{})
if err != nil {
panic(err)
}
err = gwmux.HandlePath("GET", "/hello/{name}", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
w.Write([]byte("hello " + pathParams["name"]))
})
http.ListenAndServe(":8010", gwmux)
}
自動生成 swagger
pb 除了可以生成 HTTP 的 gateway,還可以生成 swagger 文件,用於文檔生成,使用到的插件爲protoc-gen-openapiv2
🌲 安裝
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
🌲 生成 swagger 文件
和生成 grpc 和 grpc-gateway 一起執行,或者單獨執行
protoc -I ./pb --openapiv2_out ./doc --openapiv2_opt logtostderr=true \
./pb/product.proto
當然更推薦使用 buf 工具
version: v1
plugins:
- plugin: go
out: ecommerce
opt:
- paths=source_relative
- plugin: go-grpc
out: ecommerce
opt:
- paths=source_relative
- name: grpc-gateway
out: ecommerce
opt:
- paths=source_relative
- generate_unbound_methods=true
- name: openapiv2
out: doc
opt:
- logtostderr=true
於是便可以得到doc/product.swagger.json
文件
🌲 可視化展示
product.swagger.json
可以使用 swagger UI 來進行文檔的可視化展示
參考資料
[1]
官方倉庫: https://github.com/googleapis/googleapis
[2]
Buf Schema Registry (BSR): https://docs.buf.build/bsr/overview
[3]
grpc-gateway 官方文檔: https://grpc-ecosystem.github.io/grpc-gateway/
[4]
Transcoding HTTP/JSON to gRPC: https://cloud.google.com/endpoints/docs/grpc/transcoding#where_to_configure_transcoding
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/FfiNu7L5t65fJsRcdEdZIQ