gRPC-Gateway使用示例

grpc 相比REST HTTP服务没有默认的速度优势, 但是它提供了一些可选的能提高速度的方式:

  • 选择性消息压缩
  • 负载均衡
  • 等 grpc并不是万能的工具. 比如因为兼容性易维护的等需求, 我想提供一个传统的HTTP/JSON式的API.但是另写一份HTTP/JSON的API服务又是非常耗时的一件事. 社区提出的gRPC-Gateway项目解决了这个问题.

gRPC-Gateway是protoc编译器的一个插件, 它读取protobuf的服务定义并生成一个反向代理服务器, 反向代理会将RESTful HTTP API翻译为gRPC.reverse http to grpc gRPC-Gateway帮助一次性同时开发HTTP/JSON和gRPC API. 所以gRPC-Gateway的介绍如下:

  • protobuf编译器的插件
  • 从protobuf中生成代理代码
  • 翻译HTTP/JSON 调用为gRPC

使用示例

创建项目 安装工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
mkdir grpc_gateway && cd grpc_gateway 
go mod init grpcgateway

# 安装protoc的插件: 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

# 安装grpcurl工具
scoop install grpcurl

使用protocol buffers定义helloworld gRPC服务

编辑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
25
// proto/hellowrold/v1/hello_world.proto
syntax = "proto3";

package helloworld.v1;

option go_package="grpcgateway/helloworld";

import "google/api/annotations.proto";

// 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;
}

编写buf配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# buf.yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT
deps:
  - buf.build/googleapis/googleapis

编写buf.gen配置文件

1
2
3
4
5
6
7
8
9
# buf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: api
    opt: paths=source_relative
  - plugin: go-grpc
    out: api
    opt: paths=source_relative

编辑makefile文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
clean:
	rm -rf api/helloworld/v1

generate:
	buf generate .\proto\helloworld\v1\hello_world.proto

buf-update:
	buf mod update

run:
	go mod tidy && go run main.go

运行命令更新依赖, 生成proto代码

1
2
3
make buf-update

make generate

编写server端代码, 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// main.go
package main

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

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	helloworldpb "grpcgateway/api/proto/helloworld/v1"
)

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 runGRPCServer() {
	// 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{})

	// Enable reflection to allow clients query the server's services.
	reflection.Register(s)
	// Serve gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	log.Fatal(s.Serve(lis))
}

func main() {
	runGRPCServer()
}

启动服务, 使用grpcurl工具测试

1
2
3
4
5
6
grpcurl -plaintext -d '{"name":"Sunce"}' localhost:8080 helloworld.v1.Greeter/SayHello

# ouput: 
# {
#   "message": "Sunce world"
# }

配置gRPC-Gateway

修改proto文件如下, 表示该HTTP API允许POST方法请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// proto/hellowrold/v1/hello_world.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/helloworld"
      body: "*"
    };
  }
}

修改buf.gen配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# buf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: api
    opt: paths=source_relative
  - plugin: go-grpc
    out: api
    opt: paths=source_relative
    # 添加grpc-gateway插件配置
  - plugin: grpc-gateway
    out: api
    opt: 
      - paths=source_relative

重新生成代码

1
make generate

编辑main.go文件, 添加REST服务端代码

 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
68
69
70
71
72
73
74
75
76
77
78
79
80
// main.go
package main

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

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

	helloworldpb "grpcgateway/api/proto/helloworld/v1"
)

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 runGRPCServer(wg *sync.WaitGroup) {
	defer wg.Done()
	// 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{})

	// Enable reflection to allow clients query the server's services.
	reflection.Register(s)
	// Serve gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	log.Fatal(s.Serve(lis))
}

// 添加REST服务端
func runRESTServer(wg *sync.WaitGroup) {
	defer wg.Done()

	ctx := context.Background()
	mux := runtime.NewServeMux()

	conn, err := grpc.DialContext(ctx, "localhost:8080", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	if err := helloworldpb.RegisterGreeterHandler(ctx, mux, conn); err != nil {
		log.Fatalf("Failed to register handler: %v", err)
	}

	log.Println("Started a gRPC gateway server on 0.0.0.0:8081")
	log.Fatalln(http.ListenAndServe(":8081", mux))
}


func main() {
	var wg sync.WaitGroup
	wg.Add(2)

	go runGRPCServer(&wg)
	go runRESTServer(&wg)

	wg.Wait()
}

运行服务, 使用curl工具测试http接口

1
2
3
➜ curl -X POST http://localhost:8081/v1/helloworld -H "Content-Type: application/json" -d '{"name":"Sunce"}'
# ouput:
# {"message":"Sunce world"}