Go gRPC进阶-gRPC转换HTTP(十) - 烟花易冷人憔悴 - 博客园
Excerpt 前言 我们通常把 用作内部通信,而使用 进行外部通信。为了避免写两套应用,我们使用 “grpc gateway” 把 转成 。服务接收到 请求后, 把它转成 进行处理,然后以 形式返回数据。本篇代码以上篇为基础,最终转成的 支持 验证、数据验证,并添加 文档。 gRPC转成HTT
我们通常把RPC
用作内部通信,而使用Restful Api
进行外部通信。为了避免写两套应用,我们使用grpc-gateway 把gRPC
转成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> &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> && strings.Contains(r.Header.Get(<span>"Content-Type"</span>), <span>"application/grpc"</span>) { grpcServer.ServeHTTP(w, r) } <span>else</span> { otherHandler.ServeHTTP(w, r) } }), &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 = &pair <span>return</span> &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
服务已经同时支持RPC
和HTTP
请求了,而且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(&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-gateway
GitHub上的这个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 <token>"</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-gateway
让gRPC
同时支持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