• gRPC-Gateway插件的安装和使用
  • 发布于 2个月前
  • 223 热度
    0 评论
  • 果酱
  • 20 粉丝 44 篇博客
  •   
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文件如下
# 堆代码 duidaima.com
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;
}
目前有三种方式可以反向代理服务器:
1.不做任何修改直接生成,protoc-gen-grpc-gateway会按照默认规则映射Method和参数等HTTP配置
2.给protobuf添加annotations,可以自定义Method和Path等HTTP配置

3.使用外部配置,比较适用于不能修改源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工具不仅可以简化代码生成的命令,还可以解决依赖的问题。

1.首先在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
2. 更新依赖
buf mod update
buf命令从Buf Schema Registry (BSR)[2]中获取依赖,把你所有的 deps 更新到最新版。并且会生成 buf.lock 来固定版本
pb
├── buf.lock
├── buf.yaml
└── product.proto
3.创建一个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
4.生成代码
buf generate pb
执行的效果和上文中protoc命令一样

server端的实现
只生成代码还不够,还得启动gRPC-Gateway的反向代理服务器

启动
有两种方式来启动gRPC-Gateway:

1.第一种先启动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)
 // }
}
2.还有一种方式不依赖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;
}
1.对于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"}'
2.对于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

1.安装
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
2. 生成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文件

3.可视化展示
product.swagger.json可以使用swagger UI来进行文档的可视化展示

用户评论