本文主要记录了如何使用 gRPC-Gateway 同时对外提供 RESTful API 和 gRPC 接口。

原文作者: 意琦行

原文链接: gRPC(Go)教程(七)—利用Gateway同时提供HTTP和RPC服务 | 指月小筑|意琦行的个人博客

1. 概述

gRPC-Gateway 是Google protocol buffers compiler(protoc)的一个插件。读取 protobuf 定义然后生成反向代理服务器,将 RESTful HTTP API 转换为 gRPC。

换句话说就是将 gRPC 转为 RESTful HTTP API。

源自 coreos 的一篇博客,转载到了 gRPC 官方博客 gRPC with REST and Open APIs

etcd v3 改用 gRPC 后为了兼容原来的 API,同时要提供 HTTP/JSON 方式的API,为了满足这个需求,要么开发两套 API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步。

架构如下:

当 HTTP 请求到达 gRPC-Gateway 时,它将 JSON 数据解析为 Protobuf 消息。然后,它使用解析的 Protobuf 消息发出正常的 Go gRPC 客户端请求。Go gRPC 客户端将 Protobuf 结构编码为 Protobuf 二进制格式,然后将其发送到 gRPC 服务器。gRPC 服务器处理请求并以 Protobuf 二进制格式返回响应。Go gRPC 客户端将其解析为 Protobuf 消息,并将其返回到 gRPC-Gateway,后者将 Protobuf 消息编码为 JSON 并将其返回给原始客户端。

2. 环境准备

环境主要分为 3 部分:

  1. Protobuf 相关
    • Go
    • Protocol buffer compile(protoc)
    • Go Plugins
  2. gRPC相关
    • gRPC Lib
    • gRPC Plugins
  3. gRPC-Gateway 相关
    • gRPC-Gateway

1. Protobuf

具体见 Protobuf 章节

2. gRPC

具体见 gRPC章节

3. gRPC-Gateway

gRPC-Gateway 只是一个插件,只需要安装一下就可以了。

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

protoc-gen-openapiv2 可以用于生成swagger.json 文件

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

4. 整体流程

大致就是以 .proto 文件为基础,编写插件对 protoc 进行扩展,编译出不同语言不同模块的源文件。

  1. 首先定义 .proto 文件;
  2. 然后由 protoc 将 .proto 文件编译成 protobuf 格式的数据;
  3. 将 2 中编译后的数据传递到各个插件,生成对应语言、对应模块的源代码。
    • Go Plugins 用于生成 .pb.go 文件
    • gRPC Plugins 用于生成 _grpc.pb.go
    • gRPC-Gateway 则是 .pb.gw.go

其中步骤2和3是一起的,只需要在 protoc 编译时传递不同参数即可。

比如以下命令会同时生成 Go、gRPC 、gRPC-Gateway 需要的 3 个文件。

1
protoc --go_out . --go-grpc_out . --grpc-gateway_out . hello_world.proto

3. 例子

1. 创建.proto文件

创建一个 gateway.proto 文件,内容如下

具体目录为:proto/gateway/gateway.proto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";

option go_package = "grpc_study/features/proto/gateway";

package gateway;

import "google/api/annotations.proto";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/greeter/sayhello",
      body: "*"
    };
  }
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

第一步引入annotations.proto

1
import "google/api/annotations.proto";

引入annotations.proto文件,因为添加的注解依赖该文件。

该文件需要手动从 grpc-gateway/third_party/googleapis 目录复制到自己的项目中。

该文件需要手动从 grpc-gateway/third_party/googleapis 目录复制到自己的项目中。

该文件需要手动从 grpc-gateway/third_party/googleapis 目录复制到自己的项目中。

下载链接如下:

1
https://github.com/grpc-ecosystem/grpc-gateway/tree/master/third_party/googleapis/google/api

复制后的目录结构如下:

1
2
3
4
5
6
7
proto
├── google
│   └── api
│       ├── annotations.proto
│       └── http.proto
└── gateway
    └── gateway.proto

第二步增加 http 相关注解

1
2
3
4
5
6
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/greeter/sayhello"
      body: "*"
    };
  }

每个方法添加 google.api.http 注解后 gRPC-Gateway 才能生成对应 http 方法。

其中post为 HTTP Method,即 POST方法,/v1/greeter/sayhello 则是请求路径。

更多语法看这里:

1
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto

2. 编译

1
2
3
4
5
grpc_study/features/proto$ protoc --proto_path . \
--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 \
./gateway/gateway.proto

如果给–go_opt=paths=source_relative标记 protoc,则将输出文件放置在与输入文件相同的相对目录中。例如,该文件protos/foo.proto 生成名为的文件protos/foo.pb.go。

其中--proto_path=.用于指定 import 文件路径(默认为{$pwd}),即前面引入的google/api/annotations.proto文件的位置。--proto_path别名-I

本次生成了如下四个文件, 其中包含一个gateway.pb.gw.go 文件,用于启动 HTTP 服务。

1
2
3
4
-rw-r--r-- 1 不可 197121 7187 Apr 21 15:26 gateway.pb.go
-rw-r--r-- 1 不可 197121 6494 Apr 21 15:26 gateway.pb.gw.go
-rw-r--r-- 1 不可 197121  693 Apr 21 15:31 gateway.proto
-rw-r--r-- 1 不可 197121 3493 Apr 21 15:26 gateway_grpc.pb.go

3. 启动server

将会在原有 gRPC 基础上在启动一个 HTTP 服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
    "context"
    "flag"
    "fmt"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    pb "golang_study/grpc_study/features/proto/gateway"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "log"
    "net"
    "net/http"
)

var port = flag.Int("port", 50051, "the port to serve on")
var restful = flag.Int("restful", 8080, "the port to restful serve on")

type server struct {
    pb.UnimplementedGreeterServer
}

func (*server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "hello " + in.Name}, nil
}

func main() {
    flag.Parse()
    // Create a gRPC server object
    s := grpc.NewServer()
    // Attach the Greeter service to the server
    pb.RegisterGreeterServer(s, &server{})
    // Serve gRPC Server
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    log.Println("Serving gRPC on 0.0.0.0" + fmt.Sprintf(":%d", *port))
    // gRPC 服务
    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatalf("failed to serve: %v", err)
        }
    }()
    // 2. 启动 HTTP 服务
    // Create a client connection to the gRPC server we just started
    // This is where the gRPC-Gateway proxies the requests
    conn, err := grpc.Dial(
        fmt.Sprintf("localhost:%d", port),
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalln("Failed to dial server:", err)
    }
    gwmux := runtime.NewServeMux()
    // Register Greeter
    err = pb.RegisterGreeterHandler(context.Background(), gwmux, conn)
    if err != nil {
        log.Fatalln("Failed to register gateway:", err)
    }
    gwServer := &http.Server{
        Addr:    fmt.Sprintf(":%d", *restful),
        Handler: gwmux,
    }
    log.Println("Serving gRPC-Gateway on http://0.0.0.0" + fmt.Sprintf(":%d", *restful))
    log.Fatalln(gwServer.ListenAndServe())
}

Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "i-go/grpc/gateway/proto/helloworld"
)

const (
    address     = "localhost:50051"
    defaultName = "world"
)

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

4. 运行

运行并测试效果。

1
2
3
grpc_study/features/gateway/server$ go run main.go 
2022/04/21 16:01:31 Serving gRPC on 0.0.0.0:50051
2022/04/21 16:01:31 Serving gRPC-Gateway on http://0.0.0.0:8080

gRPC 请求

1
2
grpc_study/features/gateway/client$ go run main.go 
2022/04/21 16:01:52 Greeting: hello world

HTTP 请求

1
2
$ curl -X POST "http://127.0.0.1:8080/v1/greeter/sayhello" -d '{"name":"world"}'
{"message":"hello world"}

查询参数以及body参数映射

  1. body里指明了映射的字段,则请求body就是指明的字段,其余未被url path绑定的变为url params

  2. body里未指明映射字段,用*来定义。这时没有被url path绑定的每个字段都应该映射到请求主体。注意这时,因为没有绑定到路径的所有字段都在BODY中。

  3. 如果没有HTTP请求体,请求消息中没有被url path绑定的任何字段都会自动成为HTTP查询参数。

参考: googleapis/http.proto

5. 源码分析

首先建立 gRPC 连接,然后New 一个 ServeMux 接着调用了pb.RegisterGreeterHandler() 方法,最后就启动了一个 HTTP 服务。

很明显重点就是pb.RegisterGreeterHandler()这个方法,该方法就是 gRPC-Gateway 生成的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func RegisterGreeterHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
    return RegisterGreeterHandlerClient(ctx, mux, NewGreeterClient(conn))
}
func RegisterGreeterHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GreeterClient) error {

    mux.Handle("POST", pattern_Greeter_SayHello_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
        ctx, cancel := context.WithCancel(req.Context())
        defer cancel()
        inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
        rctx, err := runtime.AnnotateContext(ctx, mux, req, "/helloworld.Greeter/SayHello")
        if err != nil {
            runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
            return
        }
        resp, md, err := request_Greeter_SayHello_0(rctx, inboundMarshaler, client, req, pathParams)
        ctx = runtime.NewServerMetadataContext(ctx, md)
        if err != nil {
            runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
            return
        }

        forward_Greeter_SayHello_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)

    })

    return nil
}

4. 参考

https://grpc-ecosystem.github.io/grpc-gateway/

https://developers.google.com/protocol-buffers

https://github.com/googleapis/googleapis/blob/869d32e2f0af2748ab530646053b23a2b80d9ca5/google/api/http.proto