WEBKT

如何优雅地用 gRPC Gateway 将 gRPC 服务变身 RESTful API?(附配置避坑指南)

103 0 0 0

在微服务架构日益流行的今天,gRPC 以其高性能、强类型约束等优点,成为了服务间通信的热门选择。然而,并非所有客户端都能直接支持 gRPC,比如浏览器、移动应用,它们更习惯于使用 RESTful API。这时候,gRPC Gateway 就闪亮登场了,它能帮你无缝地将 gRPC 服务转换为 RESTful API,让你的服务能够被更广泛的客户端访问。

什么是 gRPC Gateway?

简单来说,gRPC Gateway 是一个反向代理服务器,它接收 RESTful API 请求,然后将这些请求转换为 gRPC 请求,发送到后端的 gRPC 服务。gRPC 服务处理完请求后,将响应返回给 Gateway,Gateway 再将 gRPC 响应转换为 RESTful API 响应,最终返回给客户端。这个过程对于客户端来说是透明的,它们只需要像调用普通的 RESTful API 一样即可。

为什么要使用 gRPC Gateway?

  • 兼容性: 解决 gRPC 服务与非 gRPC 客户端的兼容性问题,让你的服务能够被各种客户端访问。
  • 易用性: 简化客户端开发,开发者无需学习 gRPC 协议,只需使用熟悉的 HTTP 即可。
  • 服务暴露: 方便地将 gRPC 服务暴露给外部,无需修改 gRPC 服务代码。
  • API 管理: 可以利用 Gateway 进行 API 鉴权、限流、监控等操作,方便进行 API 管理。

gRPC Gateway 的工作原理

gRPC Gateway 的核心在于 protoc-gen-grpc-gateway 这个插件。它读取你的 .proto 文件,然后生成一个反向代理服务器的 Go 代码。这个生成的代码包含了将 RESTful API 请求转换为 gRPC 请求,以及将 gRPC 响应转换为 RESTful API 响应的逻辑。简单来说,就是根据你的 proto 文件生成一个翻译器。

实战演练:将 gRPC 服务转换为 RESTful API

接下来,我们通过一个简单的例子,演示如何使用 gRPC Gateway 将 gRPC 服务转换为 RESTful API。

1. 定义 gRPC 服务 (service.proto)

首先,我们需要定义一个 gRPC 服务。假设我们有一个 Greeter 服务,它有一个 SayHello 方法,接收一个 HelloRequest,返回一个 HelloReply

syntax = "proto3";

package example;

option go_package = "./example";

// 定义 Greeter 服务
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

2. 安装必要的工具

我们需要安装 protoc (Protocol Buffer 编译器)、protoc-gen-go (Go 语言的 Protocol Buffer 插件)、protoc-gen-grpc (Go 语言的 gRPC 插件) 和 protoc-gen-grpc-gateway (gRPC Gateway 插件)。

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

确保你的 $PATH 环境变量包含了这些工具的安装目录,通常是 $GOPATH/bin$HOME/go/bin

3. 定义 RESTful API 映射规则 (annotations)

我们需要在 .proto 文件中定义 RESTful API 的映射规则。这需要使用 google.api.http 注解。我们需要导入 google/api/annotations.proto 文件。

syntax = "proto3";

package example;

option go_package = "./example";

import "google/api/annotations.proto";

// 定义 Greeter 服务
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      get: "/v1/example/hello/{name}"
    };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

在这个例子中,我们使用 google.api.http 注解将 SayHello 方法映射到 GET /v1/example/hello/{name} 这个 RESTful API。{name} 表示从 HelloRequest 消息的 name 字段中获取值,作为 URL 的一部分。

重要提示: 你需要下载 google/api/annotations.proto 文件,并将其放在你的 .proto 文件所在的目录,或者配置 protoc--proto_path 参数,指向 google/api/annotations.proto 文件所在的目录。 这个文件可以从 googleapis 仓库下载,将其放在 google/api/ 目录下。目录结构类似:

. // 当前项目目录
├── example.proto
└── google
└── api
└── annotations.proto

4. 生成 gRPC 和 gRPC Gateway 代码

使用 protoc 命令生成 gRPC 和 gRPC Gateway 代码。

protoc -I. -I./google -I${GOPATH}/pkg/mod/github.com/googleapis/googleapis@v0.0.0-20230406121415-8aa341f4297a --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative example.proto

这个命令会生成以下文件:

  • example.pb.go: Go 语言的 Protocol Buffer 代码。
  • example_grpc.pb.go: Go 语言的 gRPC 代码。
  • example.pb.gw.go: Go 语言的 gRPC Gateway 代码。

注意点:

  • -I.-I./google 用于指定 .proto 文件的搜索路径,确保 protoc 能够找到 example.protogoogle/api/annotations.proto 文件。
  • --go_out=.--go-grpc_out=. 用于指定生成的 Go 语言代码的输出目录。
  • --grpc-gateway_out=. 用于指定生成的 gRPC Gateway 代码的输出目录。
  • --go_opt=paths=source_relative--go-grpc_opt=paths=source_relative--grpc-gateway_opt=paths=source_relative 用于指定生成的代码使用相对路径,方便管理。
  • ${GOPATH}/pkg/mod/github.com/googleapis/googleapis@v0.0.0-20230406121415-8aa341f4297a 这个路径可能因为 googleapis 的版本而有所不同,请根据实际情况修改。

5. 实现 gRPC 服务

我们需要实现 Greeter 服务的 SayHello 方法。

package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
example "./example"
)
// server is used to implement example.GreeterServer.
type server struct {
example.UnimplementedGreeterServer
}
// SayHello implements example.GreeterServer
func (s *server) SayHello(ctx context.Context, in *example.HelloRequest) (*example.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &example.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
// gRPC 服务
go func() {
port := 50051
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
example.RegisterGreeterServer(s, &server{})
log.Printf("gRPC server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}()
// gRPC Gateway
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Connect to the gRPC server endpoint
conn, err := grpc.DialContext(
ctx,
"0.0.0.0:50051",
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalln("Failed to dial server:", err)
}
defer conn.Close()
gwmux := runtime.NewServeMux()
// Register Greeter
err = example.RegisterGreeterHandler(ctx, gwmux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
openapiFile := "./example.swagger.json" // 替换为你的 OpenAPI 文件路径
dir := http.Dir(".")
// 创建文件服务器
fs := http.FileServer(dir)
// 创建 ServeMux
mux := http.NewServeMux()
mux.Handle("/", gwmux)
mux.Handle("/swagger/", http.StripPrefix("/swagger/", fs))
// 启动 HTTP 服务器
httpPort := 8080
log.Printf("HTTP server listening at :%d", httpPort)
srv := &http.Server{
Addr: fmt.Sprintf(":%d", httpPort),
Handler: mux,
}
err = srv.ListenAndServe()
if err != nil {
log.Fatalf("Failed to serve gRPC-Gateway: %v", err)
}
}

6. 启动 gRPC 服务和 gRPC Gateway

首先,编译并运行 gRPC 服务代码。

go run main.go

然后,你可以通过以下 URL 访问 RESTful API:

http://localhost:8080/v1/example/hello/world

你会得到以下响应:

{"message":"Hello world"}

7. 生成 OpenAPI (Swagger) 文档 (可选)

gRPC Gateway 还可以生成 OpenAPI (Swagger) 文档,方便 API 消费者了解 API 的使用方法。我们需要使用 protoc-gen-openapiv2 插件。

protoc -I. -I./google -I${GOPATH}/pkg/mod/github.com/googleapis/googleapis@v0.0.0-20230406121415-8aa341f4297a --openapiv2_out=. --openapiv2_opt=allow_merge=false,json_names_for_fields=false example.proto

这个命令会生成一个 example.swagger.json 文件,包含了 API 的详细描述。 可以在main.go中添加 swagger 的路由, 这样访问 /swagger/example.swagger.json 就可以看到 swagger 文档了。 你可以使用 Swagger UI 来展示这个文档。Swagger UI 是一个开源的工具,可以让你方便地浏览和测试 API。 只需要把 swagger 的静态资源文件放到项目目录下即可。

配置避坑指南

  • proto 文件路径: 确保 protoc 命令能够找到你的 .proto 文件和 google/api/annotations.proto 文件。可以使用 -I 参数指定搜索路径。
  • Go 版本: gRPC Gateway 需要 Go 1.16 或更高版本。
  • 依赖管理: 使用 go mod 管理依赖,确保所有依赖都已正确安装。
  • Context: 在调用 gRPC 服务时,需要传递 context.Context 对象。可以使用 context.Background() 创建一个空的 Context,也可以使用 context.WithTimeout() 创建一个带有超时的 Context。
  • 错误处理: 在 gRPC Gateway 中,需要正确处理 gRPC 服务的错误。可以使用 runtime.HTTPError() 函数将 gRPC 错误转换为 HTTP 错误。
  • 版本兼容性: 注意各个组件的版本兼容性,特别是 protoc, protoc-gen-go, protoc-gen-grpc, protoc-gen-grpc-gateway, google.golang.org/grpc, github.com/grpc-ecosystem/grpc-gateway 这些组件,版本不兼容会导致各种奇怪的问题。 建议使用最新版本。
  • import 包问题: 如果遇到 import google/api/annotations.proto: File not found. 错误, 请确保 google/api/annotations.proto 文件存在, 并且在 protoc 命令中使用 -I 参数指定了正确的搜索路径。
  • HTTP Method 选择:google.api.http 中, 可以指定不同的 HTTP Method, 例如 get, post, put, delete 等。 根据实际情况选择合适的 HTTP Method。 如果没有指定 HTTP Method, 默认使用 post 方法。如果你的API 只是获取数据, 建议使用 get 方法。

高级用法

  • 自定义 HTTP 状态码: 可以使用 google.api.http 注解的 response_body 字段,自定义 HTTP 状态码。
  • 自定义 HTTP Header: 可以使用 gRPC Metadata 来传递自定义 HTTP Header。
  • 中间件: 可以使用中间件来处理 HTTP 请求,例如鉴权、限流、日志等。
  • 多个 gRPC 服务: 可以使用 gRPC Gateway 来代理多个 gRPC 服务。

总结

gRPC Gateway 是一个强大的工具,可以帮助你将 gRPC 服务转换为 RESTful API,让你的服务能够被更广泛的客户端访问。 掌握 gRPC Gateway 的使用方法,可以让你在微服务架构中更加灵活地选择技术方案。 希望本文能够帮助你更好地理解和使用 gRPC Gateway。

码农小胖墩 gRPC GatewayRESTful API微服务

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9765