本篇为【写给go开发者的gRPC教程】系列第十篇
第一篇:protobuf基础
第二篇:通信模式
第三篇:拦截器
第四篇:错误处理
第五篇:metadata
第六篇:超时控制
第七篇:安全
第八篇:用户认证
第九篇:服务发现与负载均衡
第十篇: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服务
shell
$ 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文件如下
protobuf
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等
protobuf
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依赖:从官方仓库复制对应的pb文件到本地。添加依赖后目录结构如下
shell
pb
├── google
│ └── api
│ ├── annotations.proto
│ └── http.proto
└── product.proto
之后执行protoc来生成代码
shell
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
shell
buf mod init
buf命令会创建buf.yaml
文件,在此文件中添加依赖buf.build/googleapis/googleapis
yaml
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
## add
deps:
- buf.build/googleapis/googleapis
🌲 更新依赖
shell
buf mod update
buf命令从Buf Schema Registry (BSR)中获取依赖,把你所有的 deps
更新到最新版。并且会生成 buf.lock
来固定版本
shell
pb
├── buf.lock
├── buf.yaml
└── product.proto
🌲 创建一个buf.gen.yaml
它是buf生成代码的配置。上面的protoc
同等功能的buf.gen.yaml
可以写成如下形式,相对protoc更加直观
yaml
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
🌲 生成代码
shell
buf generate pb
执行的效果和上文中protoc命令一样
server端的实现
只生成代码还不够,还得启动gRPC-Gateway的反向代理服务器
启动
有两种方式来启动gRPC-Gateway
🌲 第一种先启动gRPC服务,再以gRPC服务的连接为基础创建grpc-gateway服务
代码中,我们启动了gRPC的端口8009
,同时也启用了普通HTTP端口8010
,gRPC-Gateway通过rpc的方式访问gRPC,所以gRPC服务是必须要启动的
go
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
go
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进行访问
shell
$ 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
protobuf
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder"
};
}
}
进阶
GET请求参数
对于GET请求参数也可以定义到HTTP的PATH中,pb文件这样写
protobuf
service OrderManagement {
rpc getOrder(google.protobuf.StringValue) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder/{value}"
};
}
}
shell
curl -X GET \
'127.0.0.1:8010/v1/getOrder/101' \
--header 'Accept: */*' \
我们还可以给参数设定个名字
protobuf
service OrderManagement {
rpc getOrder(getOrderReq) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder"
};
}
}
message getOrderReq {
google.protobuf.StringValue id = 1;
}
shell
curl -X GET \
'127.0.0.1:8010/v1/getOrder?id=101' \
--header 'Accept: */*' \
依旧可以把参数放到path中
protobuf
service OrderManagement {
rpc getOrder(getOrderReq) returns (Order){
option(google.api.http) = {
get: "/v1/getOrder/{id}"
};
}
}
message getOrderReq {
google.protobuf.StringValue id = 1;
}
shell
curl -X GET \
'127.0.0.1:8010/v1/getOrder/101' \
--header 'Accept: */*' \
POST请求
gRPC-Gatewway当然可以生成POST请求,示例的pb文件如下
protobuf
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
的所有字段
shell
$ 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
字段
shell
$ 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文件生成的路由基础上添加自定义的路由
go
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
🌲 安装
shell
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
🌲 生成swagger文件
和生成grpc和grpc-gateway一起执行,或者单独执行
shell
protoc -I ./pb --openapiv2_out ./doc --openapiv2_opt logtostderr=true \
./pb/product.proto
当然更推荐使用buf工具
yaml
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来进行文档的可视化展示

✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的后端知识 ✨