gRPC 网关插件能够让 protocol buffers 编译器读取 gRPC 服务定义,并生成反向代理服务器端,该服务器端能够将 RESTful JSON API 翻译为gRPC。这是专门为 Go 编写的,为了同时支持从 gRPC 和 HTTP 客户端应用程序调用 gRPC 服务。图 8-1 展示了如何以 gRPC 方式和 RESTful方式调用 gRPC 服务。
如图 8-1 所示,有一个ProductInfo
服务契约,使用该契约构建了名为 ProductInfo
服务的 gRPC 服务。在此之前,我们曾构建了 gRPC客户端来与该 gRPC 服务进行交互,但在这里,我们没有构建 gRPC 客户端,而是构建了一个反向代理服务。该服务为 gRPC 服务中的每个远程方法暴露了 RESTful API,并且接收了来自 REST 客户端的 HTTP 请求,之后,它会将请求翻译成 gRPC 消息,并调用后端服务的远程方法。来自后端服务器的响应消息会再次转换成 HTTP 响应,并发送给客户端。
要为服务定义生成反向代理服务,首先需要更新服务定义,从而将gRPC 方法映射为 HTTP 资源。我们以已经创建好的同一个ProductInfo 服务为例,为其添加映射条目。代码清单 8-1 展示了更新后的 protocol buffers
定义。
代码清单 8-1 更新 ProductInfo
服务的 protocol buffers
定义
syntax = "proto3";
import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto"; ➊
package ecommerce;
service ProductInfo {
rpc addProduct(Product) returns (google.protobuf.StringValue) {
option (google.api.http) = { ➋
post: "/v1/product"
body: "*"
};
}
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;
}
- ❶ 导入 proto 文件(
google/api/annotations.proto
)以添加对协议定义的注解支持。 - ❷ 为 addProduct 方法添加
gRPC/HTTP
映射。声明 URL 路径模板(/v1/product
)、HTTP 方法(post)以及消息体的样子。在这里,消息体映射使用了“*
”,表示没有在路径模板绑定的所有字段都应该映射到请求体中。 - ❸ 为 getProduct 方法添加
gRPC/HTTP
映射。这里是一个 GET 方法,URL 路径模板是/v1/product/{value}
,传入的 ProductID 作为路径参数。
在将 gRPC 方法映射为 HTTP 资源时,还有另一些需要知道的规则。下面列出几个重要的规则。你可以参考 Google API 文档了解 HTTP 和
gRPC 映射的更多详细信息。
每个映射都需要指定一个 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 编译器对其进行编译,并生成反向代理服务的源代码。接下来讨论一下如何通过 Go 语言生成代码并实现服务器端。
在编译服务定义之前,需要获取几个依赖包。通过以下命令下载这些包:
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
go get -u github.com/golang/protobuf/protoc-gen-go
在下载完这些包之后,执行以下命令编译服务定义(product_info.proto)并生成存根:
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc:. \
product_info.proto
执行完命令后,会在相同的位置生成一个存根(product_info.pb.go
)。除了生成的存根之外,我们还需要创建一个反向代理服务,以支持 RESTful 客户端的调用。这个反向代理服务可以通过 Go 编译器支持的网关插件来生成。
gRPC 网关只支持 Go 语言,这意味着我们无法为其他语言编译和生成 gRPC 网关的反向代理服务。
可以通过执行以下命令根据服务定义生成反向代理服务:
protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
product_info.proto
执行完命令后,它会在相同的位置生成一个反向代理服务(product_info.pb.gw.go)。
接下来为 HTTP 服务器创建监听器端点,并运行刚刚创建的反向代理服务。代码清单 8-2 展示了如何创建新的服务器实例并注册服务,以监听传入的 HTTP 请求。
代码清单 8-2 以 Go 语言编写的 HTTP 反向代理
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 (
grpcServerEndpoint = "localhost:50051" ➋
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
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)
}
}
- ❶ 导入生成的反向代理代码所在的包。
- ❷ 声明 gRPC 服务器端点 URL,确保后端 gRPC 服务器在所述的端点上正常运行,这里使用与第 2 章相同的 gRPC 服务。
- ❸ 使用代理 handler 注册 gRPC 服务器端点。在运行时,请求多路转换器(multiplexer)将 HTTP 请求匹配为模式,并调用对应的handler。
- ❹ 开始在端口 8081 上监听 HTTP 请求。
构建完 HTTP 反向代理服务器后,就可以通过同时运行 gRPC 服务器和HTTP 服务器来测试它了。在本例中,gRPC 服务器监听端口 50051
,而HTTP 服务器监听端口 8081
。
我们通过 curl 发送几个 HTTP 请求并观察它的行为。
01. 添加新商品到 ProductInfo 服务:
$ curl -X POST http://localhost:8081/v1/product -d '{"name": "Apple", "description": "iphone7", "price": 699}'
"38e13578-d91e-11e9"
02. 使用 ProductID 获取已有的商品:
$ curl http://localhost:8081/v1/product/38e13578-d91e-11e9{"id":"38e13578-d91e-11e9","name":"Apple","description":"iphone7","price":699}
03. 添加反向代理服务后,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
04. 执行命令后,它会在相同的位置为反向代理服务生成 swagger 定
义(product_info.swagger.json)。对于 ProductInfo 服务来说,生成的 swagger 定义如下所示:
{
"swagger": "2.0",
"info": {
"title": "product_info.proto",
"version": "version not set"
},
"schemes": [
"http",
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/product": {
"post": {
"operationId": "addProduct",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "string"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/ecommerceProduct"
}
}
],
"tags": [
"ProductInfo"
]
}
},
"/v1/product/{value}": {
"get": {
"operationId": "getProduct",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/ecommerceProduct"
}
}
},
"parameters": [
{
"name": "value",
"description": "The string value.",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ProductInfo"
]
}
}
},
"definitions": {
"ecommerceProduct": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"price": {
"type": "number",
"format": "float"
}
}
}
}
}
到现在为止,我们使用 gRPC 网关为 gRPC 服务实现了 HTTP 反向代理服务。通过这种方式,可以将 gRPC 服务器端暴露给 HTTP 客户端应用程序使用。通过 gRPC 网关的仓库,可以了解更多关于网关实现的信息。
如前所述,gRPC 网关只支持 Go,同样的概念也被称为HTTP/JSON 转码。8.2 节将讨论 HTTP/JSON 转码的更多内容。