跳至主要內容

GRPC教程 4 - GRPC-Gateway教程与Transcoding

离心原创大约 4 分钟tutorialgolanggrpc

GRPC教程 4 - GRPC-Gateway教程与Transcoding

相关信息

GRPC-Gateway是protoc的一个插件,类似protoc-gen-go和protoc-gen-go-grpc插件,前者是生成.pb.go后者是生成.grpc.pb.go文件。

那么这两个插件是帮助proto文件生成go语言的插件,那么GRPC-Gateway呢,它是一个可以根据proto文件的定义生成一个反向代理器的,服务器可以将 RESTful JSON API 转换为 GRPC。

这里你会有三个疑惑?什么是反向代理器?为什么要根据proto文件生成反向代理器?为什么要将RESTful JSON API转换成GRPC?

其实这一个图就可以解释

基本示例

我们继续使用我们一开始的hello.proto文件来做示例

├── client
│   ├── go.mod
│   ├── main.go
│   └── pb
│       ├── hello_grpc.pb.go
│       ├── hello.pb.go
│       └── hello.proto
├── go.work
└── server
    ├── go.mod
    ├── go.sum
    ├── main.go
    └── pb
        ├── hello_grpc.pb.go
        ├── hello.pb.go
        └── hello.proto

这次我们发现我们每次调用server进程下的服务,每次都需要重新定义一个客户端再去调用,那能不能说,就是像gin这种框架一样,使用简单的JSON body请求就可以请求GRPC呢,这就是grpc gateway的作用了,我们来演示一下怎么来用grpc-gateway。

首先添加 gRPC-Gateway 注释,这些注释定义了我们GRPC服务如何映射为JSON的请求和响应。使用protobuf的时候,GRPC service必须用google.api.HTTP来定义。

这里我用POST /v1/hello 来 映射到 Hello 的GRPC服务。

修改后的service Greeter是这样

import "google/api/annotations.proto";

service Greetering {
  rpc Hello (HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      post: "/api/v1/hello"
      body: "*"
    };
  };
}

在这里 google/api/annotations.proto 却是在go/include/下,也就是我们下载proto时,解压下来的文件夹。和include 同一目录。所以我们需要去跟github上去下载。

https://github.com/googleapis/googleapis/tree/master/google/apiopen in new window

随后需要重新加上grpc-gateway插件器创建对应的pb.gw.go文件。

在这里你需要下载grpc-gateway

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
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 \
  pb/hello.proto 

现在我们开始编写main.go,我们加入gateway的方式就是新起一个goroutine 去开启gateway代理

go func() {

		conn, err := grpc.DialContext(
			context.Background(),
			"0.0.0.0:7890",
			grpc.WithBlock(),
			grpc.WithTransportCredentials(insecure.NewCredentials()),
		)
		if err != nil {
			log.Fatalln("Failed to dial server:", err)
		}

		gwmux := runtime.NewServeMux()
		err = pb.RegisterGreeteringHandler(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())
	}()

接着我们用curl脚本命令,或者电脑上可以下载下postman去测试一下。

curl -X POST http://127.0.0.1:8090/api/v1/hello '{"name": "Lixin"}'

Transcoding

下面接着来讲一下更多的匹配规则,在上个例子中,我们并没有讲解更多的路径匹配,比如/api/:name或者传递query参数,我们来讲解一下这样的方式

首先我们先修改一下对应之前的pb/hello.proto

message HelloReq {
  string name = 1;
  int64 age = 2;
  string msg = 3;
}

message HelloResp {
  string msg = 1;
}

service Greetering {
  rpc Hello (HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/api/v1/hello/{name}"
    };
  };
}

来方便我们进行查看如何去使用http transcoding 规则查询。

我们再次进行编码 protoc ..., 运行服务

接着启动服务,用curl去测试一下

curl --location 'http://127.0.0.1:8090/api/v1/hello/lixin'

{"msg":"0 / lixin /  Hello"}

可以看到获取到了对应的name字段,那如果我们传入query参数呢?

curl --location 'http://127.0.0.1:8090/api/v1/hello/lixin?age=12'

{"msg":"12 / lixin /  Hello"}

curl --location 'http://127.0.0.1:8090/api/v1/hello/lixin?age=12&msg=message'
{"msg":"12 / lixin / message Hello"} 

可以看到,我们这样就可以给http 路由传递 query参数去访问对应的grpc服务。

那还有没有别的用法?,我们继续修改proto文件 get: "/api/v1/hello/{name=names/*}", 接着我们传递

curl --location 'http://127.0.0.1:8090/api/v1/hello/names/lixin?age=12&msg=message'

{"msg": "12 / names/lixin / message Hello"}

可以看到我们修改了对应的:name为{names/*},这样我们就让names/lixin顺利传递给了name参数。

接下来,我们看看body的用法,我们一般不会用get方法去传递body,所以下面我们接着再来修改一下proto文件,让对应的http 方法称为post,然后加上body参数。

service Greetering {
  rpc Hello (HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      post: "/api/v1/hello/{name}"
      body: "msg"
    };
  };
}

我们再来观察一下这个option里面的东西,post路径为/api/v1/hello/{name}, 这意味着,我们可以给path传递name,然后给body传递msg参数,那剩下的age呢?我们可以用query路径传递进去,就是这样

message HelloReq {
  string name = 1;   // path 
  int64 age = 2; // query  
  string msg = 3; // body
}
curl --location 'http://127.0.0.1:8090/api/v1/hello/lixin?age=12' \
--header 'Content-Type: text/plain' \
--data '"eeeesae"'

{ "msg": "12 / lixin / eeeesae Hello"}

可以看到我们传递的data,就是对应到msg参数里面的。

下面我们再修改一下proto文件, 将body修改为*

service Greetering {
  rpc Hello (HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      post: "/api/v1/hello/{name}"
      body: "*"
    };
  };
}

重新生成gw.go protoc ...

curl --location 'http://127.0.0.1:8090/api/v1/hello/lixin?age=12' \
--header 'Content-Type: application/json' \
--data '{
    "name": "Lixinsss",
    "age": 21,
    "msg": "This is a message."
}'

可以看到,虽然body是*, 但是body里面只有传递age和msg有效,当body是*时,query参数就没有效果了。
{"msg": "21 / lixin / This is a message. Hello"}

总结

我们本篇文章,主要是介绍了GRPC-Gateway,下载gatwway插件,学习使用gateway,并且学习了传递HTTP的多种方式