文章目录

  • 简介
  • 安装及应用
  • 安装 gRPC-Gateway
  • 生成代码
  • gRPC 应用程序示例
  • 定义 gRPC 服务
  • 编写 gRPC 应用程序
  • 重新编写 gRPC 应用程序提供 HTTP API
  • 测试 gRPC-Gateway
  • 同一个端口提供 HTTP API 和 gRPC API



简介


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

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

grpc 客户端yml参数配置_rpc

在之前的 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&param=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 编译生成不同模块的源文件,分别在 serverclient 项目的 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 的调用命令中生成存根和反向代理服务,分别在 serverclient 项目的 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

上述命令应该会生成一个 *.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"}

  • 参考链接:gRPC 教程
  • 参考链接:gRPC 官网
  • 参考书籍:《gRPC与云原生应用开发:以Go和Java为例》([斯里兰卡] 卡山 • 因德拉西里 丹尼什 • 库鲁普 著)