gRPC Transcoding 是一种定义了grpc与http rest api转化规则,这种规则适用于 Google APIs, Cloud Endpoints, gRPC Gateway等,目前已被广泛使用。
英文文档见:
https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule
代码可见:
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
本篇算是对以上文档的翻译与主要观点提炼,再加上个人测试结果。
由于我写的是proto3,所以protoc选用了3.11,这个只要选用最新的,差异不大
这两个我踩坑了。。.主要之前安装过protoc-gen-go,它是生成pb.go的工具,后来再安装的protoc-gen-grpc-gateway时不清楚之前protoc-gen-go的版本,所以直接安装最新了,代码跑起来就会报各种方法未实现。研究了一下protoc-gen-grpc-gateway是生成pb.gw.go的工具,由于protoc-gen-go有版本v1和版本v2,且两个差异较大,相应的protoc-gen-grpc-gateway也应该使用对应的版本。
怎么区分protoc-gen-go新旧版本呢?
了解到v1版的protoc-gen-go,在执行protoc-gen-go --version命令后会一直挂起,而v2版本执行完此命令后会立刻返回版本号。因为我的protoc-gen-go是v1版本,在下载github.com/grpc-ecosystem/grpc-gateway后,把tag切到v1后,重新go build 生成protoc-gen-grpc-gateway ,移到本机GOPATH下
最后还遇到个问题仅供参考:undefined: runtime.ServerTransportStream,github.com/grpc-ecosystem/grpc-gateway错误,查询官网把
grpc-gateway版本改成 v1.14.8就可以解决了。
最后推荐https://github.com/jergoo/go-grpc-tutorial这个git项目,适合初学grpc的,我是直接在它项目上改改验证此次结论的。
//request
message msgtype{
string msgname = 1;
}
message HelloURLPathRequest{
string name = 1;
repeated string msg = 2;
msgtype msgfield = 3;
}
//response
message HelloHTTPResponse {
string message = 1;
}
//grpc func
func (h helloHTTPService) SayHelloURLPath(ctx context.Context, in *pb.HelloURLPathRequest) (*pb.HelloHTTPResponse, error) {
resp := new(pb.HelloHTTPResponse)
resp.Message = "SayHelloURLPath: Hello " + in.Name + ". "
for i, msg := range in.Msg {
resp.Message += "msg" + fmt.Sprintf("%d: %s. ", i, msg)
}
if in.Msgfield != nil {
resp.Message += "msgfield.msgname: " + in.Msgfield.Msgname
}
return resp, nil
}
官网说明:url path 可以引用gRPC请求消息中的一个或多个字段。但是这些字段必须是原生基础类型的非数组字段,不支持消息类型的字段,也不支持数组字段。
测试结果:
test1:
rpc SayHelloURLPath(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v1/{name}"
};
}
请求: curl -X POST http://localhost:8080/v1/test?msg=a\&msg=b\&msgfield.msgname=c
输出: {"message":"SayHelloURLPath: Hello test. msg0: a. msg1: b. msgfield.msgname: c"}
test2:
rpc SayHelloURLPath2(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v2/{name=messages/*}"
};
}
请求: curl -X POST http://localhost:8080/v2/messages/test?msg=a\&msg=b\&msgfield.msgname=c
输出: {"message":"SayHelloURLPath2: Hello messages/test . msg0: a. msg1: b. msgfield.msgname: c"}
请求url只能是v2/messages/*
test3:
rpc SayHelloURLPath3(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v3/{msg}"
};
}
请求: curl -X POST http://localhost:8080/v3/a?msg=b\&msgfield.msgname=c
输出: {"message":"SayHelloURLPath3: Hello . msg0: a. msgfield.msgname: c"}
官网说不能是数组类型,发现数组类型是url path,编译也没报错,但是其实并没有接受到数组的多个值,只接受到一个值,所以还是不建议这么用
test4:
rpc SayHelloURLPath4(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v4/{msgfield}"
};
}
把消息类型做为url path,直接报错了msgfield is a protobuf message type. Protobuf message types cannot be used as path parameters, use a scalar value type (such as string) instead
官网说明:如果没有HTTP请求体,请求消息中没有被url path绑定的任何字段都会自动成为HTTP查询参数。
转化规则:
映射成URL查询参数的字段必须是原生基础类型、原生基础数组类型或非数组消息类型。
原生基础数组类型转化格式:...?param=A¶m=B
非数组消息类型转化格式:...?foo.a=A&foo.b=B&foo.c=C
这个规则3.2的测试已经可以覆盖大部分了
在3.2 test1的基础上给请求加上 repeated msgtype msgfields = 4,如下:
message HelloURLPathRequest{
string name = 1;
repeated string msg = 2;
msgtype msgfield = 3;
repeated msgtype msgfields = 4;
}
编译没有报错,但是请求时会报{"error":"unexpected repeated field in msgfields.msgname","code":3,"message":"unexpected repeated field in msgfields.msgname"};从而验证必须是非数组消息类型。
官网说明:
1. body里指明了映射的字段,则请求body就是指明的字段,其余未被url path绑定的变为url params
2. body里未指明映射字段,用*来定义。这时没有被url path绑定的每个字段都应该映射到请求主体。注意这时,因为没有绑定到路径的所有字段都在BODY中。这使得该选项在定义REST api时在实践中很少使用。*的常见用法是在自定义方法中,这些方法根本不使用URL来传输数据。
test5:
rpc SayHelloBody(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v5/{name}"
body:"msgfield"
};
}
请求: curl -X POST http://localhost:8080/v5/test?msg=a\&msg=b -d '{"msgname":"c"}'
输出: {"message":"SayHelloBody: Hello test . msg0: a. msg1: b. msgfield.msgname: c"}
test6:
rpc SayHelloBody2(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v6/{name}"
body:"msgfields" //"msg"
};
}
报错:--grpc-gateway_out: repeated field not allowed in field path: msgfields in msgfields//测试"msg"字段也是一样
说明body字段是不支持数组类型的字段
test7:
rpc SayHelloBody3(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v7/{name}"
body:"*"
};
}
请求: curl -X POST http://localhost:8080/v7/test -d '{"msgfield":{"msgname":"d"}, "msg":["b", "c"]}'
输出: {"message":"SayHelloBody3: Hello test . msg0: b. msg1: c. msgfield.msgname: d"}
官网说明:
通过使用additional_bindings选项,可以为一个RPC定义多个HTTP方法。
test8:
rpc SayHelloadditional(HelloURLPathRequest) returns (HelloHTTPResponse) {
option (google.api.http) = {
post: "/v8/{name}"
additional_bindings {
post: "/v9/{name}"
}
};
}
请求1: curl -X POST http://localhost:8080/v8/test?msg=a\&msg=b\&msgfield.msgname=c
输出1: {"message":"SayHelloadditional: Hello test . msg0: a. msg1: b. msgfield.msgname: c"}
请求2: curl -X POST http://localhost:8080/v9/test?msg=a\&msg=b\&msgfield.msgname=c
输出2: {"message":"SayHelloadditional: Hello test . msg0: a. msg1: b. msgfield.msgname: c"}
总结:
1.叶子请求字段(请求消息中的递归扩展嵌套消息)分为三类:
path模板引用的字段。它们通过URL路径传递。
被HttpRule.body引用的字段。它们通过HTTP请求体传递。
所有其他字段都通过URL查询参数传递,参数名称是请求消息中的字段path。数组字段可以表示为相同名称下的多个查询参数。
2. 如果HttpRule。正文为“*”,没有URL查询参数,所有字段通过URL路径和HTTP请求正文传递。
3. 如果HttpRule。省略主体,没有HTTP请求主体,所有字段通过URL路径和URL查询参数传递。
补充:
路径模板语法:
Template = "/" Segments [ Verb ] ;
Segments = Segment { "/" Segment } ;
Segment = "*" | "**" | LITERAL | Variable;
Variable = "{" FieldPath [ "=" Segments ] "}" ;
FieldPath = IDENT { "." IDENT } ;
Verb = ":" LITERAL ;
语法*匹配单个URL路径Segment。语法**匹配0个或多个URL路径Segment,这些Segment必须是URL路径的最后一部分,但Verb除外。
Variable匹配模板指定的URL路径的一部分。Variable不能包含其他Variable。如果一个Variable匹配单个路径Segment,它的模板可以省略,例如{var}等价于{var=*}。
语法LITERAL匹配URL路径中的文字文本。如果LITERAL包含任何reserved字符,这些字符应该在匹配之前进行百分比编码。
如果一个Variable只包含一个路径Segment,例如“{var}”或“{var=*}”,当这样的Variable在客户端展开为URL路径时,除[-_.~0-9a-zA-Z]以外的所有字符需要进行百分比编码。服务器端执行反向解码。这些变量在发现文档中显示为{var}。
如果一个Variable包含多个路径Segment,如“{var=foo/*}”或“{var=**}”,当在客户端将该变量展开为URL路径时除[-_.~0-9a-zA-Z]以外的所有字符需要进行百分比编码。服务器端执行反向解码,除非“%2F”和“%2F”保持不变。这些变量在发现文档中显示为{+var}。