gRPC Gateway 研究

gRPC

gRPC 是由 Google 开源的高性能 RPC 框架,它的客户端和服务端可以在各种环境中运行和相互通信。同时它还可插拔地支持 Load Balance、Tracing、Health Check、Authentication 等功能。多语言的支持也是它最强大的能力之一,开发者可以用 gRPC 支持的任何语言来编写。因此,可以轻松地用 Java 创建一个 gRPC Server ,同时拥有 Go、Python 或 Ruby 编写的 Client,反之亦然。

Protocol Buffer

gRPC 默认使用 protocol buffer 作为结构化数据序列化的方法,当然也可以选择其他数据格式例如 json。它的工作原理如下图所示:

使用 protocol buffer 的好处是:

  • 数据存储紧凑
  • 数据解析快速
  • 多语言支持,包括:C++、C#、Java、Kotlin、Objective-C、PHP、Python、Ruby、Dart、Go 等
  • 通过自动生成代码进行功能优化

gRPC 核心架构

gRPC 基于描述服务的理念,通过定义可调用的方法及其参数和返回类型来描述 RPC 服务。默认情况下,gRPC 使用 Protocol Buffers,简称 Protobuf 作为接口定义语言(IDL)来描述服务接口和负载消息的结构。以 hello.proto 定义的 hello 服务为例,它的 gRPC 定义如下:

// file: hello.proto
// The hello service definition.
service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

gRPC 支持四种类型的服务调用(service method):

  1. Unary RPC(一元 RPC),简单类型的 RPC,其中客户端发送单个请求并获得单个响应。一旦客户端调用了一个存根方法,服务器就会收到通知,表明 RPC 已经用客户端的元数据、方法名和适用的指定截止时间被调用。然后,服务器可以立即发送回自己的初始元数据(在任何响应之前必须发送),或者等待客户端的请求消息。哪个先发生,取决于具体的应用。一旦服务器得到客户端的请求消息,它就会进行必要的工作来创建和填充一个响应。然后响应(如果成功)连同状态详情(状态码和可选的状态消息)和可选的尾部元数据一起返回给客户端。如果响应状态是 OK,那么客户端就会得到响应,这就完成了客户端端的调用。
  2. Server Streaming RPC(服务器流式 RPC),服务器流式 RPC 类似于一元 RPC,不同之处在于服务器返回一个消息流作为对客户端请求的响应。在发送完所有消息后,服务器的状态详情(状态码和可选的状态消息)和可选的尾部元数据会发送给客户端。这完成了服务器端的处理。客户端在收到所有服务器消息后完成。
  3. Client Streaming RPC(客户端流式 RPC),客户端流式 RPC 类似于一元 RPC,不同之处在于客户端向服务器发送一系列消息而不是单个消息。服务器用单个消息响应(连同其状态详情和可选的尾部元数据),通常但不一定在收到客户端所有消息之后。
  4. Bidirectional Streaming RPC(双向流式 RPC)在双向流式 RPC 中,调用由客户端调用方法和服务器接收客户端元数据、方法名和截止时间发起。服务器可以选择发送回自己的初始元数据或等待客户端开始流式传输消息。客户端和服务器端的流处理是应用程序特定的。由于两个流是独立的,客户端和服务器可以以任何顺序读写消息。例如,服务器可以等到收到客户端所有消息后再写自己的消息,或者服务器和客户端可以类似“玩乒乓球”一样:服务器收到一个请求,然后发送回一个响应,然后客户端根据响应发送另一个请求,依此类推。

gRPC Gateway

虽然 gRPC 可以在许多编程语言中生成 API Client 和 Server(stub),且具有速度快、易于使用、带宽高效等优点。然而,业务场景可能仍然需要同时提供传统的 RESTful API 的能力支持。原因可能有很多,从维护向后兼容性,到支持 gRPC 不太支持的语言或客户端,或者仅仅是为了保持 RESTful 架构的美观性和相关工具的使用等等。

于是对于为 gRPC 添加 HTTP 语义支持就成为 gRPC 需要支持的重要功能之一,幸运的是 Google 已经早早地识别出该需求场景 —— gRPC Gateway 项目就是在已有的 gRPC 服务中进行少量配置从而实现附加 HTTP 语义的功能。

gRPC Gateway 原理

gRPC Gateway 是 Protocol Buffer 编译器 protoc 的一个插件,它读取 protobuf 服务定义,并生成一个反向代理服务器,该服务器将 RESTful HTTP API 翻译成 gRPC。这个服务器是根据 protobuf 服务定义中的 google.api.http 注解生成的。

gRPC Gateway 使用

下面就研究一下如果在 gRPC 项目中如何增加 RESTful API 的支持(以 Go 语言项目为例)。

gRPC 与 gRPC Gateway 依赖

安装 protocol buffer 编译器 protoc 用到的 go 语言插件、go gRPC 插件、 gRPC Gateway 插件:

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
go install github.com/bufbuild/buf/cmd/buf@v1.28.1

简单的 gRPC Go 项目

  1. 初始化 Go repo
>> go mod init grpcgateway
  1. 定义 gRPC protobuf
// proto/hello_world.proto
syntax = "proto3";

package helloworld;

// The greeting service definition
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;
}
  1. 生成 protobuf stubs buf 工具配置文件:
// buf.yaml
version: v1
breaking:
  use:
    - FILE

buf 工具 generation 配置文件:

version: v1
plugins:
  - plugin: go
    out: .
    opt: paths=source_relative
  - plugin: go-grpc
    out: .
    opt: paths=source_relative

运行命令生成 protobuf stubs:

buf generate

gRPC 服务 Go 实现

通过 Go 语言实现基于 proto/hello_world.proto 定义的 gRPC 服务:

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"

	helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)

type server struct{
	helloworldpb.UnimplementedGreeterServer
}

func NewServer() *server {
	return &server{}
}

func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
	return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}

func main() {
	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// Create a gRPC server object
	s := grpc.NewServer()
	// Attach the Greeter service to the server
	helloworldpb.RegisterGreeterServer(s, &server{})
	// Serve gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	log.Fatal(s.Serve(lis))
}

通过如下命令就可以运行 gRPC 服务了:

go mod tidy
go run main.go

添加 gRPC Gateway 支持

上文已经初始化好了一个简单的支持 gRPC 的 Go 项目,现在对这个项目添加 RESTful API 语义的支持。

在 protobuf 定义中增加 gRPC Gateway 注释

新的 protobuf 文件如下所示:

syntax = "proto3";

package grpcgateway;

option go_package="grpcgateway/proto";

import "google/api/annotations.proto";

// The greeting service definition
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/example/echo"
      body: "*"
    };
  }
}

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

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

生成 gRPC Gateway stubs

buf 工具 generation 配置文件 buf.gen.yaml 中增加 gRPC Gateway 插件配置:

version: v1
plugins:
  - plugin: go
    out: .
    opt: paths=source_relative
  - plugin: go-grpc
    out: .
    opt: paths=source_relative
  - plugin: grpc-gateway
    out: .
    opt:
      - paths=source_relative
      - generate_unbound_methods=true

buf 工具配置文件 buf.yaml 中添加 googleapis 编译依赖:

version: v1
breaking:
  use:
    - FILE
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc-ecosystem/grpc-gateway

运行命令就可以完成 gRPC Gateway stubs 的生成:

buf mod update && buf generate

gRPC Gateway 服务 Go 实现

需要同时启动 gRPC server (goroutine 运行)以及反向代理的 HTTP Server:

package main

import (
	"context"
	"log"
	"net"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	helloworldpb "grpcgateway/proto"
)

type server struct {
	helloworldpb.UnimplementedGreeterServer
}

func NewServer() *server {
	return &server{}
}

func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) {
	return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil
}

func main() {
	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":5300")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// Create a gRPC server object
	s := grpc.NewServer()
	// Attach the Greeter service to the server
	helloworldpb.RegisterGreeterServer(s, &server{})
	// Serve gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:5300")
	//log.Fatal(s.Serve(lis))
	go func() {
		log.Fatalln(s.Serve(lis))
	}()

	// Create a client connection to the gRPC server we just started
	// This is where the gRPC-Gateway proxies the requests
	conn, err := grpc.DialContext(
		context.Background(),
		"0.0.0.0:5300",
		grpc.WithBlock(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalln("Failed to dial server:", err)
	}

	gwmux := runtime.NewServeMux()
	// Register Greeter
	err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn)
	if err != nil {
		log.Fatalln("Failed to register gateway:", err)
	}

	gwServer := &http.Server{
		Addr:    ":5301",
		Handler: gwmux,
	}

	log.Println("Serving gRPC-Gateway on http://0.0.0.0:5301")
	log.Fatalln(gwServer.ListenAndServe())
}

gRPC Gateway 服务运行测试

上述工作完成之后就可以测试 gRPC Gateway 运行效果了,如下动画所示:

References

  1. Introduction to gRPC | gRPC
  2. gRPC-Gateway | gRPC-Gateway Documentation Website
  3. grpc-ecosystem/grpc-gateway: gRPC to JSON proxy generator following the gRPC HTTP spec