0%

Go gRPC进阶-gRPC转换HTTP(十) - 烟花易冷人憔悴 - 博客园

Excerpt

前言 我们通常把 用作内部通信,而使用 进行外部通信。为了避免写两套应用,我们使用 “grpc gateway” 把 转成 。服务接收到 请求后, 把它转成 进行处理,然后以 形式返回数据。本篇代码以上篇为基础,最终转成的 支持 验证、数据验证,并添加 文档。 gRPC转成HTT


前言#

我们通常把RPC用作内部通信,而使用Restful Api进行外部通信。为了避免写两套应用,我们使用grpc-gatewaygRPC转成HTTP。服务接收到HTTP请求后,grpc-gateway把它转成gRPC进行处理,然后以JSON形式返回数据。本篇代码以上篇为基础,最终转成的Restful Api支持bearer token验证、数据验证,并添加swagger文档。

gRPC转成HTTP#

编写和编译proto

1.编写simple.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
26
27
28
29
30
<p>Copy</p><code id="code-lang-protobuf">syntax = <span>"proto3"</span>;

<span>package</span> proto;

<span>import</span> <span>"github.com/mwitkow/go-proto-validators/validator.proto"</span>;
<span>import</span> <span>"go-grpc-example/10-grpc-gateway/proto/google/api/annotations.proto"</span>;

<span>message </span><span>InnerMessage</span> {
<span>// some_integer can only be in range (1, 100).</span>
<span>int32</span> some_integer = <span>1</span> [(validator.field) = {int_gt: <span>0</span>, int_lt: <span>100</span>}];
<span>// some_float can only be in range (0;1).</span>
<span>double</span> some_float = <span>2</span> [(validator.field) = {float_gte: <span>0</span>, float_lte: <span>1</span>}];
}

<span>message </span><span>OuterMessage</span> {
<span>// important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).</span>
<span>string</span> important_string = <span>1</span> [(validator.field) = {regex: <span>"^[a-z]{2,5}$"</span>}];
<span>// proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.</span>
InnerMessage inner = <span>2</span> [(validator.field) = {msg_exists : <span>true</span>}];
}

<span>service </span><span>Simple</span>{
<span><span>rpc</span> Route (InnerMessage) <span>returns</span> (OuterMessage)</span>{
<span>option</span> (google.api.http) ={
post:<span>"/v1/example/route"</span>
body:<span>"*"</span>
};
}
}
</code>

可以看到,proto变化不大,只是添加了API的路由路径

1
2
3
4
5
<p>Copy</p><code id="code-lang-protobuf">      <span>option</span> (google.api.http) ={
post:<span>"/v1/example/route"</span>
body:<span>"*"</span>
};
</code>

2.编译simple.proto

simple.proto文件引用了google/api/annotations.proto来源),先要把它编译了。我这里是把google/文件夹直接复制到项目中的proto/目录中进行编译。发现annotations.proto引用了google/api/http.proto,那把它也编译了。

进入annotations.proto所在目录,编译:

1
2
3
<p>Copy</p><code id="code-lang-powershell">protoc <span>--go_out</span>=plugins=grpc:./ ./http.proto
protoc <span>--go_out</span>=plugins=grpc:./ ./annotations.proto
</code>

进入simple.proto所在目录,编译:

1
2
3
4
5
<p>Copy</p><code id="code-lang-powershell"><span>#生成simple.validator.pb.go和simple.pb.go</span>
protoc <span>--govalidators_out</span>=. <span>--go_out</span>=plugins=grpc:./ ./simple.proto
<span>#生成simple.pb.gw.go</span>
protoc <span>--grpc-gateway_out</span>=logtostderr=true:./ ./simple.proto
</code>

以上完成proto编译,接着修改服务端代码。

服务端代码修改

1.server/文件夹下新建gateway/目录,然后在里面新建gateway.go文件

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
<p>Copy</p><code id="code-lang-go"><span>package</span> gateway

<span>import</span> (
<span>"context"</span>
<span>"crypto/tls"</span>
<span>"io/ioutil"</span>
<span>"log"</span>
<span>"net/http"</span>
<span>"strings"</span>

pb <span>"go-grpc-example/10-grpc-gateway/proto"</span>
<span>"go-grpc-example/10-grpc-gateway/server/swagger"</span>

<span>"github.com/grpc-ecosystem/grpc-gateway/runtime"</span>
<span>"golang.org/x/net/http2"</span>
<span>"golang.org/x/net/http2/h2c"</span>
<span>"google.golang.org/grpc"</span>
<span>"google.golang.org/grpc/credentials"</span>
<span>"google.golang.org/grpc/grpclog"</span>
)

<span>// ProvideHTTP 把gRPC服务转成HTTP服务,让gRPC同时支持HTTP</span>
<span><span>func</span> <span>ProvideHTTP</span><span>(endpoint <span>string</span>, grpcServer *grpc.Server)</span></span> *http.Server {
ctx := context.Background()
<span>//获取证书</span>
creds, err := credentials.NewClientTLSFromFile(<span>"../tls/server.pem"</span>, <span>"go-grpc-example"</span>)
<span>if</span> err != <span>nil</span> {
log.Fatalf(<span>"Failed to create TLS credentials %v"</span>, err)
}
<span>//添加证书</span>
dopts := []grpc.DialOption{grpc.WithTransportCredentials(creds)}
<span>//新建gwmux,它是grpc-gateway的请求复用器。它将http请求与模式匹配,并调用相应的处理程序。</span>
gwmux := runtime.NewServeMux()
<span>//将服务的http处理程序注册到gwmux。处理程序通过endpoint转发请求到grpc端点</span>
err = pb.RegisterSimpleHandlerFromEndpoint(ctx, gwmux, endpoint, dopts)
<span>if</span> err != <span>nil</span> {
log.Fatalf(<span>"Register Endpoint err: %v"</span>, err)
}
<span>//新建mux,它是http的请求复用器</span>
mux := http.NewServeMux()
<span>//注册gwmux</span>
mux.Handle(<span>"/"</span>, gwmux)
log.Println(endpoint + <span>" HTTP.Listing whth TLS and token..."</span>)
<span>return</span> &amp;http.Server{
Addr: endpoint,
Handler: grpcHandlerFunc(grpcServer, mux),
TLSConfig: getTLSConfig(),
}
}

<span>// grpcHandlerFunc 根据不同的请求重定向到指定的Handler处理</span>
<span><span>func</span> <span>grpcHandlerFunc</span><span>(grpcServer *grpc.Server, otherHandler http.Handler)</span></span> http.Handler {
<span>return</span> h2c.NewHandler(http.HandlerFunc(<span><span>func</span><span>(w http.ResponseWriter, r *http.Request)</span></span> {
<span>if</span> r.ProtoMajor == <span>2</span> &amp;&amp; strings.Contains(r.Header.Get(<span>"Content-Type"</span>), <span>"application/grpc"</span>) {
grpcServer.ServeHTTP(w, r)
} <span>else</span> {
otherHandler.ServeHTTP(w, r)
}
}), &amp;http2.Server{})
}

<span>// getTLSConfig获取TLS配置</span>
<span><span>func</span> <span>getTLSConfig</span><span>()</span></span> *tls.Config {
cert, _ := ioutil.ReadFile(<span>"../tls/server.pem"</span>)
key, _ := ioutil.ReadFile(<span>"../tls/server.key"</span>)
<span>var</span> demoKeyPair *tls.Certificate
pair, err := tls.X509KeyPair(cert, key)
<span>if</span> err != <span>nil</span> {
grpclog.Fatalf(<span>"TLS KeyPair err: %v\n"</span>, err)
}
demoKeyPair = &amp;pair
<span>return</span> &amp;tls.Config{
Certificates: []tls.Certificate{*demoKeyPair},
NextProtos: []<span>string</span>{http2.NextProtoTLS}, <span>// HTTP2 TLS支持</span>
}
}

</code>

它主要作用是把不用的请求重定向到指定的服务处理,从而实现把HTTP请求转到gRPC服务。

2.gRPC支持HTTP

1
2
3
4
5
6
<p>Copy</p><code id="code-lang-go">    <span>//使用gateway把grpcServer转成httpServer</span>
httpServer := gateway.ProvideHTTP(Address, grpcServer)
<span>if</span> err = httpServer.Serve(tls.NewListener(listener, httpServer.TLSConfig)); err != <span>nil</span> {
log.Fatal(<span>"ListenAndServe: "</span>, err)
}
</code>

使用postman测试

在动图中可以看到,我们的gRPC服务已经同时支持RPCHTTP请求了,而且API接口支持bearer token验证和数据验证。为了方便对接,我们把API接口生成swagger文档。

生成swagger文档#

生成swagger文档-simple.swagger.json

1.安装protoc-gen-swagger

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

2.编译生成simple.swagger.json

到simple.proto文件目录下,编译:
protoc --swagger_out=logtostderr=true:./ ./simple.proto

再次提一下,本人在VSCode中使用VSCode-proto3插件,第一篇有介绍,只要保存,就会自动编译,很方便,无需记忆指令。完整配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<p>Copy</p><code id="code-lang-swift">    <span>// vscode-proto3插件配置</span>
<span>"protoc"</span>: {
<span>// protoc.exe所在目录</span>
<span>"path"</span>: <span>"C:<span>\\</span>Go<span>\\</span>bin<span>\\</span>protoc.exe"</span>,
<span>// 保存时自动编译</span>
<span>"compile_on_save"</span>: <span>true</span>,
<span>"options"</span>: [
<span>"--go_out=plugins=grpc:."</span>,<span>//在当前目录编译输出.pb.go文件</span>
<span>"--govalidators_out=."</span>,<span>//在当前目录编译输出.validator.pb文件</span>
<span>"--grpc-gateway_out=logtostderr=true:."</span>,<span>//在当前目录编译输出.pb.gw.go文件</span>
<span>"--swagger_out=logtostderr=true:."</span><span>//在当前目录编译输出.swagger.json文件</span>
]
}
</code>

编译生成后把需要的文件留下,不需要的删掉。

把swagger-ui转成Go代码,备用

1.下载swagger-ui

下载地址,把dist目录下的所有文件拷贝我们项目的server/swagger/swagger-ui/目录下。

2.把Swagger UI转换为Go代码

安装go-bindata
go get -u github.com/jteeuwen/go-bindata/...

回到server/所在目录,运行指令把Swagger UI转成Go代码。
go-bindata --nocompress -pkg swagger -o swagger/datafile.go swagger/swagger-ui/...

  • 这步有坑,必须要回到main函数所在的目录运行指令,因为生成的Go代码中的_bindata 映射了swagger-ui的路径,程序是根据这些路径来找页面的。如果没有在main函数所在的目录运行指令,则生成的路径不对,会报404,无法找到页面。本项目server/端的main函数在server.go中,所以在server/所在目录下运行指令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p>Copy</p><code id="code-lang-go"><span>var</span> _bindata = <span>map</span>[<span>string</span>]<span><span>func</span><span>()</span></span> (*asset, <span>error</span>){
<span>"swagger/swagger-ui/favicon-16x16.png"</span>: swaggerSwaggerUiFavicon16x16Png,
<span>"swagger/swagger-ui/favicon-32x32.png"</span>: swaggerSwaggerUiFavicon32x32Png,
<span>"swagger/swagger-ui/index.html"</span>: swaggerSwaggerUiIndexHtml,
<span>"swagger/swagger-ui/oauth2-redirect.html"</span>: swaggerSwaggerUiOauth2RedirectHtml,
<span>"swagger/swagger-ui/swagger-ui-bundle.js"</span>: swaggerSwaggerUiSwaggerUiBundleJs,
<span>"swagger/swagger-ui/swagger-ui-bundle.js.map"</span>: swaggerSwaggerUiSwaggerUiBundleJsMap,
<span>"swagger/swagger-ui/swagger-ui-standalone-preset.js"</span>: swaggerSwaggerUiSwaggerUiStandalonePresetJs,
<span>"swagger/swagger-ui/swagger-ui-standalone-preset.js.map"</span>: swaggerSwaggerUiSwaggerUiStandalonePresetJsMap,
<span>"swagger/swagger-ui/swagger-ui.css"</span>: swaggerSwaggerUiSwaggerUiCss,
<span>"swagger/swagger-ui/swagger-ui.css.map"</span>: swaggerSwaggerUiSwaggerUiCssMap,
<span>"swagger/swagger-ui/swagger-ui.js"</span>: swaggerSwaggerUiSwaggerUiJs,
<span>"swagger/swagger-ui/swagger-ui.js.map"</span>: swaggerSwaggerUiSwaggerUiJsMap,
}
</code>

对外提供swagger-ui

1.在swagger/目录下新建swagger.go文件

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
<p>Copy</p><code id="code-lang-go"><span>package</span> swagger

<span>import</span> (
<span>"log"</span>
<span>"net/http"</span>
<span>"path"</span>
<span>"strings"</span>

assetfs <span>"github.com/elazarl/go-bindata-assetfs"</span>
)

<span>//ServeSwaggerFile 把proto文件夹中的swagger.json文件暴露出去</span>
<span><span>func</span> <span>ServeSwaggerFile</span><span>(w http.ResponseWriter, r *http.Request)</span></span> {
<span>if</span> !strings.HasSuffix(r.URL.Path, <span>"swagger.json"</span>) {
log.Printf(<span>"Not Found: %s"</span>, r.URL.Path)
http.NotFound(w, r)
<span>return</span>
}

p := strings.TrimPrefix(r.URL.Path, <span>"/swagger/"</span>)
<span>// "../proto/"为.swagger.json所在目录</span>
p = path.Join(<span>"../proto/"</span>, p)

log.Printf(<span>"Serving swagger-file: %s"</span>, p)

http.ServeFile(w, r, p)
}

<span>//ServeSwaggerUI 对外提供swagger-ui</span>
<span><span>func</span> <span>ServeSwaggerUI</span><span>(mux *http.ServeMux)</span></span> {
fileServer := http.FileServer(&amp;assetfs.AssetFS{
Asset: Asset,
AssetDir: AssetDir,
Prefix: <span>"swagger/swagger-ui"</span>, <span>//swagger-ui文件夹所在目录</span>
})
prefix := <span>"/swagger-ui/"</span>
mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
}
</code>

2.注册swagger

gateway.go中添加如下代码

1
2
3
4
<p>Copy</p><code id="code-lang-go"><span>//注册swagger</span>
mux.HandleFunc(<span>"/swagger/"</span>, swagger.ServeSwaggerFile)
swagger.ServeSwaggerUI(mux)
</code>

到这里我们已经完成了swagger文档的添加工作了,由于谷歌浏览器不能使用自己制作的TLS证书,所以我们用火狐浏览器进行测试。

用火狐浏览器打开:https://127.0.0.1:8000/swagger-ui/

在最上面地址栏输入:https://127.0.0.1:8000/swagger/simple.swagger.json

然后就可以看到swagger生成的API文档了。

还有个问题,我们使用了bearer token进行接口验证的,怎么把bearer token也添加到swagger中呢?
最后我在grpc-gatewayGitHub上的这个Issues找到解决办法。

在swagger中配置bearer token

1.修改simple.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
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
<p>Copy</p><code id="code-lang-protobuf">syntax = <span>"proto3"</span>;

<span>package</span> proto;

<span>import</span> <span>"github.com/mwitkow/go-proto-validators/validator.proto"</span>;
<span>import</span> <span>"go-grpc-example/10-grpc-gateway/proto/google/api/annotations.proto"</span>;
<span>import</span> <span>"go-grpc-example/10-grpc-gateway/proto/google/options/annotations.proto"</span>;

<span>message </span><span>InnerMessage</span> {
<span>// some_integer can only be in range (1, 100).</span>
<span>int32</span> some_integer = <span>1</span> [(validator.field) = {int_gt: <span>0</span>, int_lt: <span>100</span>}];
<span>// some_float can only be in range (0;1).</span>
<span>double</span> some_float = <span>2</span> [(validator.field) = {float_gte: <span>0</span>, float_lte: <span>1</span>}];
}

<span>message </span><span>OuterMessage</span> {
<span>// important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).</span>
<span>string</span> important_string = <span>1</span> [(validator.field) = {regex: <span>"^[a-z]{2,5}$"</span>}];
<span>// proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.</span>
InnerMessage inner = <span>2</span> [(validator.field) = {msg_exists : <span>true</span>}];
}

<span>option</span> (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
security_definitions: {
security: {
key: <span>"bearer"</span>
value: {
type: TYPE_API_KEY
in: IN_HEADER
name: <span>"Authorization"</span>
description: <span>"Authentication token, prefixed by Bearer: Bearer &lt;token&gt;"</span>
}
}
}

security: {
security_requirement: {
key: <span>"bearer"</span>
}
}

info: {
title: <span>"grpc gateway sample"</span>;
version: <span>"1.0"</span>;
license: {
name: <span>"MIT"</span>;
};
}

schemes: HTTPS
};

<span>service </span><span>Simple</span>{
<span><span>rpc</span> Route (InnerMessage) <span>returns</span> (OuterMessage)</span>{
<span>option</span> (google.api.http) ={
post:<span>"/v1/example/route"</span>
body:<span>"*"</span>
};
<span>// //禁用bearer token</span>
<span>// option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {</span>
<span>// security: { } // Disable security key</span>
<span>// };</span>
}
}
</code>

2.重新编译生成simple.swagger.json

大功告成!

验证测试

1.添加bearer token

2.调用接口,正确返回数据

3.传递不合规则的数据,返回违反数据验证逻辑错误

总结#

本篇介绍了如何使用grpc-gatewaygRPC同时支持HTTP,最终转成的Restful Api支持bearer token验证、数据验证。同时生成swagger文档,方便API接口对接。

教程源码地址:https://github.com/Bingjian-Zhu/go-grpc-example

参考文档:
https://eddycjy.com/tags/grpc-gateway/
https://segmentfault.com/a/1190000008106582