gRPC 的网关

作者: adm 分类: go 发布时间: 2023-12-18

gRPC 网关插件( gRPC-Gateway )能够让 protocol buffers 编译器读取 gRPC 服务定义,并生成反向代理服务器端,该服务器是根据服务定义中的 google.api.http 注释生成的,能够将 RESTful JSON API 翻译为 gRPC ,为了同时支持从 gRPC 和 HTTP 客户端应用程序调用 gRPC 服务。

例如,以 gRPC 方式和 RESTful 方式调用 gRPC 服务,有一个 ProductInfo 服务契约,使用该契约构建了名为 ProductInfo 服务的 gRPC 服务。

在之前的 gRPC 应用程序中,会构建一个 gRPC 客户端来与该 gRPC 服务进行交互,但在这里没有构建 gRPC 客户端,而是构建了一个反向代理服务。该服务为 gRPC 服务中的每个远程方法暴露了 RESTful API 并且接收了来自 REST 客户端的 HTTP 请求,之后,它会将请求翻译成 gRPC 消息,并调用后端服务的远程方法,来自后端服务器的响应消息会再次转换成 HTTP 响应,并发送给客户端。

安装及应用
安装 gRPC-Gateway
使用 gRPC-Gateway 工具需要安装 protoc-gen-grpc-gateway 的插件来生成对应的 grpc-gateway 代码,输入如下的命令:

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2

生成代码
以 helloworld.proto 文件为基础,编写插件对 protoc 进行扩展,编译出不同语言不同模块的源文件,整体流程如下所示:

定义 proto 文件;

由 protoc 将 proto 文件编译成 protobuf 格式的数据;

将编译后的数据传递到各个插件,生成对应语言、对应模块的源代码。

根据服务定义生成反向代理服务将 gRPC-Gateway 生成器添加到 protoc 的调用命令中,执行以下的命令生成一个 *.gw.pb.go 文件:

protoc --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 *.proto

Go Plugins 用于生成 .pb.go 文件,gRPC Plugins 用于生成 _grpc.pb.go
gRPC-Gateway 则是 pb.gw.go 文件,以上命令会同时生成 Go、gRPC 、gRPC-Gateway 需要的 3 个文件。

要为服务定义生成反向代理服务,首先需要更新服务定义,从而将 gRPC 方法映射为 HTTP 资源,以已经创建好的同一个 ProductInfo 服务为例,为其添加映射条目,更新后的 Protocol Buffers 定义,该文件的具体内容如下:

syntax = "proto3";
// 导入 proto 文件(google/api/annotations.proto)以添加对协议定义的注解支持
import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto"; 

package ecommerce;

service ProductInfo {
		// 为 addProduct 方法添加 gRPC/HTTP 映射并声明 URL 路径模板(/v1/product)、HTTP 方法(post)以及消息体的样子
		// 在这里,消息体映射使用了“*”,表示没有在路径模板绑定的所有字段都应该映射到请求体中
		rpc addProduct(Product) returns (google.protobuf.StringValue) {
				option (google.api.http) = { 
				post: "/v1/product"
				body: "*"
				};
		}
		// 为 getProduct 方法添加 gRPC/HTTP 映射,这里是一个 GET 方法,URL 路径模板是 / v1/product/{value},传入的 ProductID 作为路径参数
		rpc getProduct(google.protobuf.StringValue) returns (Product) {
				option (google.api.http) = { 
						get:"/v1/product/{value}"
				};
		}
}

message Product {
		string id = 1;
		string name = 2;
		string description = 3;
		float price = 4;
}

在将 gRPC 方法映射为 HTTP 资源时,需要使用一定的规则,下面列出几个重要的规则:

每个映射都需要指定一个 URL 路径模板和一个 HTTP 方法;

路径模板可以包含一个或多个 gRPC 请求消息中的字段,但这些字段应该是 nonrepeated 的原始类型字段;

如果没有 HTTP 请求体,那么出现在请求消息中但没有出现在路径模板中的字段,将自动成为 HTTP 查询参数;

映射为 URL 查询参数的字段应该是原始类型、repeated 原始类型或 nonrepeated 消息类型;

对于查询参数的 repeated 字段,参数可以在 URL 中重复,形式为 …?param=A¶m=B;

对于查询参数中的消息类型,消息的每个字段都会映射为单独的参数,比如 …?foo.a=A&foo.b=B&foo.c=C。

在编写完服务定义后,需要使用 Protocol Buffers 编译器对其进行编译并生成存根以及反向代理服务的源代码,执行以下命令生成一个反向代理服务(product_info.pb.gw.go):

protoc -I=proto --go_out=proto --go_opt=paths=source_relative --go-grpc_out=proto --go-grpc_opt=paths=source_relative --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative product_info.proto

为 HTTP 服务器创建监听器端点并运行刚刚创建的反向代理服务,编写 gRPC 应用程序,该程序的具体代码如下所示:

package main

import (
		"context"
		"log"
		"net/http"
		"github.com/grpc-ecosystem/grpc-gateway/runtime"
		"google.golang.org/grpc"
		gw "github.com/grpc-up-and-running/samples/ch08/grpc-gateway/go/gw" // 导入生成的反向代理代码所在的包
)

var (
		// 声明 gRPC 服务器端点 URL,确保后端 gRPC 服务器在所述的端点上正常运行
		grpcServerEndpoint = "localhost:50051" 
)

func main() {
		ctx := context.Background()
		ctx, cancel := context.WithCancel(ctx)
		defer cancel()
		mux := runtime.NewServeMux()
		opts := []grpc.DialOption{grpc.WithInsecure()}
		// 使用代理 handler 注册 gRPC 服务器端点,在运行时,请求多路转换器(multiplexer)将 HTTP 请求匹配为模式并调用对应的 handler
		err := gw.RegisterProductInfoHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) 
		if err != nil {
				log.Fatalf("Fail to register gRPC gateway service endpoint: %v", err)
		}
		if err := http.ListenAndServe(":8081", mux); err != nil { 
				log.Fatalf("Could not setup HTTP endpoint: %v", err)
		}
}

构建完 HTTP 反向代理服务器后,就可以通过同时运行 gRPC 服务器和 HTTP 服务器来进行测试,在本例中,gRPC 服务器监听端口 50051,而 HTTP 服务器监听端口 8081。

通过 curl 发送几个 HTTP 请求并观察它的行为:

(1)添加新商品到 ProductInfo 服务,执行以下命令:

curl -X POST http://localhost:8081/v1/product -d '{"name": "Apple", "description": "iphone7", "price": 699}' "38e13578-d91e-11e9"

(2)添加新商品到 ProductInfo 服务,执行以下命令:

curl http://localhost:8081/v1/product/38e13578-d91e-11e9 {"id":"38e13578-d91e-11e9","name":"Apple","description":"iphone7","price":699}

(3)添加反向代理服务后,gRPC 网关还支持生成反向代理服务的 swagger 定义,这可以通过执行以下命令实现:

protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --swagger_out=logtostderr=true:. product_info.proto

使用 gRPC 网关为 gRPC 服务实现了 HTTP 反向代理服务,通过这种方式,可以将 gRPC 服务器端暴露给 HTTP 客户端应用程序使用。

gRPC 应用程序示例
定义 gRPC 服务
(1)在任意目录下,创建 server 文件夹并初始化(go mod init server)作为项工程文件,在该项目目录下创建一个 proto 文件夹存放 helloword.proto 文件,具体的目录结构如下所示:

Gateway
├── client
│   └── proto
│       └── helloworld.proto
└── server
    └── proto
        └── helloworld.proto

(2)编写 helloworld.proto 文件,添加如下内容:

syntax = "proto3";

package helloworld;

option go_package="../proto";

// 定义一个 Greeter 服务
service Greeter {
  	// 打招呼方法
  	rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义请求的 message
message HelloRequest {
  	string name = 1;
}

// 定义响应的 message
message HelloReply {
  	string message = 1;
}

(3)使用 protoc 编译生成不同模块的源文件,分别在 server 和 client 项目的 proto 目录下执行以下命令生成 gRPC 源码程序:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto

成功生成后项目的目录结构如下所示:

Gateway
├── client
│   └── proto
│       ├── helloworld_grpc.pb.go
│       ├── helloworld.pb.go
│       └── helloworld.proto
└── server
    └── proto
        ├── helloworld_grpc.pb.go
        ├── helloworld.pb.go
        └── helloworld.proto

编写 gRPC 应用程序
(1)编写 gRPC 服务端应用程序,该程序的具体代码如下:

package main

import (
        "context"
        "log"
        "net"
        "google.golang.org/grpc"
        helloworldpb "server/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
}

(2)编写 gRPC 客户端应用程序,该程序的具体代码如下:

package main

import (
        "log"
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        pb "client/proto"
)

const (
        address     = "localhost:8080"
        defaultName = "hello"
)

func main() {
        conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
        if err != nil {
                panic(err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)
        r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: defaultName})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.Message)
}

(3)运行 服务端和客户端程序,输出如下的结果:

2023/03/01 16:25:20 Greeting: hello world

重新编写 gRPC 应用程序提供 HTTP API
(1)将 gRPC-Gateway 注释添加到现有的 helloworld 文件,这些注释定义了 gRPC 服务如何映射到 JSON 请求和响应。使用 Protocol Buffers 时,每个 RPC 服务必须使用 google.api.HTTP 注释来定义 HTTP 方法和路径。

在本例中,将 POST /v1/greeter/sayhello 映射到 SayHello RPC 中,修改后的 helloworld.proto 文件具体的内容如下:

syntax = "proto3";
package helloworld;
option go_package="../proto";

// 导入 google/api/annotations.proto
import "google/api/annotations.proto";

// 定义一个 Greeter 服务
service Greeter {
  		// 打招呼方法
  		rpc SayHello (HelloRequest) returns (HelloReply) {
    			// 这里添加 google.api.http 注释
    			option (google.api.http) = {
      					post: "/v1/greeter/sayhello"
     	 				body: "*"
    			};
  		}
}

// 定义请求的 message
message HelloRequest {
  		string name = 1;
}

// 定义响应的 message
message HelloReply {
  		string message = 1;
}

每个方法都必须添加 google.api.http 注解后 gRPC-Gateway 才能生成对应 http 方法,其中 post 为 HTTP Method ,即 POST 方法,/v1/greeter/sayhello 则是请求路径。

更多的语法参考 官方资料 。

(2)使用 gRPC-Gateway 生成器来生成存根,还需要添加所需的 HTTP-> gRPC 映射,将 googleapis 的一个子集从 官方库 复制到本地项目文件结构中,拷贝后的目录结构如下所示:

Gateway
├── client
│   └── proto
│       ├── google
│       │   └── api
│       │       ├── annotations.proto
│       │       └── http.proto
│       └── helloworld.proto
└── server
    └── proto
        ├── google
        │   └── api
        │       ├── annotations.proto
        │       └── http.proto
        └── helloworld.proto

(3)将 gRPC-Gateway 生成器添加到 protoc 的调用命令中生成存根和反向代理服务,分别在 server 和 client 项目的 proto 目录下执行如下的命令:

protoc –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 *.proto
1
上述命令应该会生成一个 *.gw.pb.go 文件,用于启动 HTTP 服务,其中 –proto_path=. 用于指定 import 文件路径(默认为 {$pwd} ),即前面引入的 google/api/annotations.proto文件的位置,此时项目的目录结构如下:

Gateway
├── client
│   └── proto
│       ├── google
│       │   └── api
│       │       ├── annotations.proto
│       │       └── http.proto
│       ├── helloworld_grpc.pb.go
│       ├── helloworld.pb.go
│       ├── helloworld.pb.gw.go
│       └── helloworld.proto
└── server
    └── proto
        ├── google
        │   └── api
        │       ├── annotations.proto
        │       └── http.proto
        ├── helloworld_grpc.pb.go
        ├── helloworld.pb.go
        ├── helloworld.pb.gw.go
        └── helloworld.proto

(4)向 gRPC 服务端程序中添加和启动 gRPC-Gateway mux ,修改后程序的具体的代码如下:

package main

import (
        "context"
        "log"
        "net"
        "net/http"
        helloworldpb "server/proto"
        "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // v2 版本
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

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)
        }

        // 创建一个 gRPC server 对象
        s := grpc.NewServer()
        // 注册 Greeter service 到 server
        helloworldpb.RegisterGreeterServer(s, &server{})
        // 8080 端口启动 gRPC Server
        log.Println("Serving gRPC on 0.0.0.0:8080")
        go func() {
                log.Fatalln(s.Serve(lis))
        }()

        // 创建一个连接到刚刚启动的 gRPC 服务器的客户端连接
        // gRPC-Gateway 就是通过它来代理请求(将 HTTP 请求转为 RPC 请求)
        conn, err := grpc.DialContext(
                context.Background(),
                "0.0.0.0:8080",
                grpc.WithBlock(),
                grpc.WithTransportCredentials(insecure.NewCredentials()),
        )
        if err != nil {
                log.Fatalln("Failed to dial server:", err)
        }

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

        gwServer := &http.Server{
                Addr:    ":8090",
                Handler: gwmux,
        }
        // 8090 端口提供 gRPC-Gateway 服务
        log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
        log.Fatalln(gwServer.ListenAndServe())
}

注意:导入的 ”github.com/grpc-ecosystem/grpc-gateway/v2/runtime” 是 v2版本,需要使用单独的 goroutine 启动 gRPC 服务。

测试 gRPC-Gateway
(1)启动 gRPC 服务端应用程序,使用 curl 工具发送 HTTP POST 请求,执行以下命令:

curl -X POST -k http://localhost:8090/v1/greeter/sayhello -d '{"name": " hello"}'

输出的结果如下:

{"message":" hello world"}

将 POST 请求方式修改为 GET 请求方式,只需要修改 helloworld.proto` 文件中的注释并重新编译即可,如以下的代码形式:

...
    	option (google.api.http) = {
      	get: "/v1/greeter/sayhello",
      			body: "*"
    	};
...

(2)启动 gRPC 服务端应用程序,使用 curl 工具发送 HTTP GET 请求,执行以下命令:

curl -k http://localhost:8080/v1/greeter/sayhello?name=world

输出的结果如下:

{"message":" hello world"}

上面的 gRPC 应用程序在 8080 端口提供了 gRPC API ,在 8090 端口提供了HTTP API ,下面进行编写 gRPC 应用程序实现由同一个端口同时提供 gRPC API 和 HTTP API 两种服务,由请求方来决定具体使用哪个协议。

同一个端口提供 HTTP API 和 gRPC API
(1)重新编写 gRPC 服务端程序,该程序的具体的代码如下:

package main

import (
        "context"
        "log"
        "net"
        "net/http"
        "strings"
        helloworldpb "server/proto"
        "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // v2 版本
        "golang.org/x/net/http2"
        "golang.org/x/net/http2/h2c" // 没有启用 TLS 加密通信,所以这里使用 h2c 包实现对 HTTP/2 的支持,h2c 协议是 HTTP/2 的非 TLS 版本
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
)

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)
        }

        // 创建一个 gRPC server 对象
        s := grpc.NewServer()
        // 注册 Greeter service 到 server
        helloworldpb.RegisterGreeterServer(s, &server{})

        // gRPC-Gateway mux
        gwmux := runtime.NewServeMux()
        dops := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
        err = helloworldpb.RegisterGreeterHandlerFromEndpoint(context.Background(), gwmux, "127.0.0.1:8080", dops)
        if err != nil {
                log.Fatalln("Failed to register gwmux:", err)
        }

        mux := http.NewServeMux()
        mux.Handle("/", gwmux)

        // 定义 HTTP server 配置
        gwServer := &http.Server{
                Addr:    "127.0.0.1:8080",
                Handler: grpcHandlerFunc(s, mux), // 请求的统一入口
        }
        log.Println("Serving on http://127.0.0.1:8080")
        log.Fatalln(gwServer.Serve(lis)) // 启动 HTTP 服务
}

// grpcHandlerFunc 将 gRPC 请求和HTTP 请求分别调用不同的handler 处理
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
        return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
                        grpcServer.ServeHTTP(w, r)
                } else {
                        otherHandler.ServeHTTP(w, r)
                }
        }), &http2.Server{})
}

(2)运行该 gRPC 服务端程序,该程序在 8080 端口启动,测试 gRPC API ,运行客户端程序后输出如下的响应结果:

2023/03/01 16:56:03 Greeting: hello world

(3)运行该 gRPC 服务端程序,该程序在 8080 端口启动,使用 curl 工具测试 HTTP API ,执行以下命令:

curl -X POST -k http://127.0.0.1:8080/v1/greeter/sayhello -d '{"name": " hello"}'

输出如下的响应结果:

{"message":" hello world"}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!