写给go开发者的gRPC教程-gRPC Gateway

本篇为【写给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-goprotoc-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反向代理服务器根据servicegoogle.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请求时,入参需要一个Orderbody : "*"表示在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请求时,入参需要一个OrderRequestbody : "order"表示在HTTP的请求中的body需要包含OrderRequestorder字段

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来进行文档的可视化展示


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

参考

相关推荐
苹果酱056716 分钟前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱36 分钟前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
bug菌¹1 小时前
滚雪球学SpringCloud[4.1讲]: Spring Cloud Gateway详解
java·spring cloud·微服务
计算机学姐3 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
小筱在线3 小时前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务
JustinNeil3 小时前
简化Java对象转换:高效实现大对象的Entity、VO、DTO互转与代码优化
后端
青灯文案13 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
cooldream20094 小时前
828华为云征文 | 在华为云X实例上部署微服务架构的文物大数据管理平台的实践
微服务·架构·华为云·文物大数据平台
微尘84 小时前
C语言存储类型 auto,register,static,extern
服务器·c语言·开发语言·c++·后端
计算机学姐4 小时前
基于PHP的电脑线上销售系统
开发语言·vscode·后端·mysql·编辑器·php·phpstorm