Go gRPC进阶-gRPC转换HTTP(十)
source link: https://studygolang.com/articles/28233
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
前言
我们通常把 RPC
用作内部通信,而使用 Restful Api
进行外部通信。为了避免写两套应用,我们使用 grpc-gateway 把 gRPC
转成 HTTP
。服务接收到 HTTP
请求后, grpc-gateway
把它转成 gRPC
进行处理,然后以 JSON
形式返回数据。本篇代码以上篇为基础,最终转成的 Restful Api
支持 bearer token
验证、数据验证,并添加 swagger
文档。
gRPC转成HTTP
编写和编译proto
1.编写simple.proto
syntax = "proto3"; package proto; import "github.com/mwitkow/go-proto-validators/validator.proto"; import "go-grpc-example/10-grpc-gateway/proto/google/api/annotations.proto"; message InnerMessage { // some_integer can only be in range (1, 100). int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}]; // some_float can only be in range (0;1). double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}]; } message OuterMessage { // important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax). string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}]; // proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage. InnerMessage inner = 2 [(validator.field) = {msg_exists : true}]; } service Simple{ rpc Route (InnerMessage) returns (OuterMessage){ option (google.api.http) ={ post:"/v1/example/route" body:"*" }; } }
可以看到, proto
变化不大,只是添加了API的路由路径
option (google.api.http) ={ post:"/v1/example/route" body:"*" };
2.编译 simple.proto
simple.proto
文件引用了 google/api/annotations.proto
( 来源 ),先要把它编译了。我这里是把 google/
文件夹直接复制到项目中的 proto/
目录中进行编译。发现 annotations.proto
引用了 google/api/http.proto
,那把它也编译了。
进入 annotations.proto
所在目录,编译:
protoc --go_out=plugins=grpc:./ ./http.proto protoc --go_out=plugins=grpc:./ ./annotations.proto
进入 simple.proto
所在目录,编译:
#生成simple.validator.pb.go和simple.pb.go protoc --govalidators_out=. --go_out=plugins=grpc:./ ./simple.proto #生成simple.pb.gw.go protoc --grpc-gateway_out=logtostderr=true:./ ./simple.proto
以上完成 proto
编译,接着修改服务端代码。
服务端代码修改
1. server/
文件夹下新建 gateway/
目录,然后在里面新建 gateway.go
文件
package gateway import ( "context" "crypto/tls" "io/ioutil" "log" "net/http" "strings" pb "go-grpc-example/10-grpc-gateway/proto" "go-grpc-example/10-grpc-gateway/server/swagger" "github.com/grpc-ecosystem/grpc-gateway/runtime" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" ) // ProvideHTTP 把gRPC服务转成HTTP服务,让gRPC同时支持HTTP func ProvideHTTP(endpoint string, grpcServer *grpc.Server) *http.Server { ctx := context.Background() //获取证书 creds, err := credentials.NewClientTLSFromFile("../tls/server.pem", "go-grpc-example") if err != nil { log.Fatalf("Failed to create TLS credentials %v", err) } //添加证书 dopts := []grpc.DialOption{grpc.WithTransportCredentials(creds)} //新建gwmux,它是grpc-gateway的请求复用器。它将http请求与模式匹配,并调用相应的处理程序。 gwmux := runtime.NewServeMux() //将服务的http处理程序注册到gwmux。处理程序通过endpoint转发请求到grpc端点 err = pb.RegisterSimpleHandlerFromEndpoint(ctx, gwmux, endpoint, dopts) if err != nil { log.Fatalf("Register Endpoint err: %v", err) } //新建mux,它是http的请求复用器 mux := http.NewServeMux() //注册gwmux mux.Handle("/", gwmux) log.Println(endpoint + " HTTP.Listing whth TLS and token...") return &http.Server{ Addr: endpoint, Handler: grpcHandlerFunc(grpcServer, mux), TLSConfig: getTLSConfig(), } } // grpcHandlerFunc 根据不同的请求重定向到指定的Handler处理 func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { grpcServer.ServeHTTP(w, r) } else { otherHandler.ServeHTTP(w, r) } }), &http2.Server{}) } // getTLSConfig获取TLS配置 func getTLSConfig() *tls.Config { cert, _ := ioutil.ReadFile("../tls/server.pem") key, _ := ioutil.ReadFile("../tls/server.key") var demoKeyPair *tls.Certificate pair, err := tls.X509KeyPair(cert, key) if err != nil { grpclog.Fatalf("TLS KeyPair err: %v\n", err) } demoKeyPair = &pair return &tls.Config{ Certificates: []tls.Certificate{*demoKeyPair}, NextProtos: []string{http2.NextProtoTLS}, // HTTP2 TLS支持 } }
它主要作用是把不用的请求重定向到指定的服务处理,从而实现把 HTTP
请求转到 gRPC
服务。
2.gRPC支持HTTP
//使用gateway把grpcServer转成httpServer httpServer := gateway.ProvideHTTP(Address, grpcServer) if err = httpServer.Serve(tls.NewListener(listener, httpServer.TLSConfig)); err != nil { log.Fatal("ListenAndServe: ", err) }
使用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
插件, 第一篇 有介绍,只要保存,就会自动编译,很方便,无需记忆指令。完整配置如下:
// vscode-proto3插件配置 "protoc": { // protoc.exe所在目录 "path": "C:\\Go\\bin\\protoc.exe", // 保存时自动编译 "compile_on_save": true, "options": [ "--go_out=plugins=grpc:.",//在当前目录编译输出.pb.go文件 "--govalidators_out=.",//在当前目录编译输出.validator.pb文件 "--grpc-gateway_out=logtostderr=true:.",//在当前目录编译输出.pb.gw.go文件 "--swagger_out=logtostderr=true:."//在当前目录编译输出.swagger.json文件 ] }
编译生成后把需要的文件留下,不需要的删掉。
把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/
所在目录下运行指令。
var _bindata = map[string]func() (*asset, error){ "swagger/swagger-ui/favicon-16x16.png": swaggerSwaggerUiFavicon16x16Png, "swagger/swagger-ui/favicon-32x32.png": swaggerSwaggerUiFavicon32x32Png, "swagger/swagger-ui/index.html": swaggerSwaggerUiIndexHtml, "swagger/swagger-ui/oauth2-redirect.html": swaggerSwaggerUiOauth2RedirectHtml, "swagger/swagger-ui/swagger-ui-bundle.js": swaggerSwaggerUiSwaggerUiBundleJs, "swagger/swagger-ui/swagger-ui-bundle.js.map": swaggerSwaggerUiSwaggerUiBundleJsMap, "swagger/swagger-ui/swagger-ui-standalone-preset.js": swaggerSwaggerUiSwaggerUiStandalonePresetJs, "swagger/swagger-ui/swagger-ui-standalone-preset.js.map": swaggerSwaggerUiSwaggerUiStandalonePresetJsMap, "swagger/swagger-ui/swagger-ui.css": swaggerSwaggerUiSwaggerUiCss, "swagger/swagger-ui/swagger-ui.css.map": swaggerSwaggerUiSwaggerUiCssMap, "swagger/swagger-ui/swagger-ui.js": swaggerSwaggerUiSwaggerUiJs, "swagger/swagger-ui/swagger-ui.js.map": swaggerSwaggerUiSwaggerUiJsMap, }
对外提供swagger-ui
1.在 swagger/
目录下新建 swagger.go
文件
package swagger import ( "log" "net/http" "path" "strings" assetfs "github.com/elazarl/go-bindata-assetfs" ) //ServeSwaggerFile 把proto文件夹中的swagger.json文件暴露出去 func ServeSwaggerFile(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "swagger.json") { log.Printf("Not Found: %s", r.URL.Path) http.NotFound(w, r) return } p := strings.TrimPrefix(r.URL.Path, "/swagger/") // "../proto/"为.swagger.json所在目录 p = path.Join("../proto/", p) log.Printf("Serving swagger-file: %s", p) http.ServeFile(w, r, p) } //ServeSwaggerUI 对外提供swagger-ui func ServeSwaggerUI(mux *http.ServeMux) { fileServer := http.FileServer(&assetfs.AssetFS{ Asset: Asset, AssetDir: AssetDir, Prefix: "swagger/swagger-ui", //swagger-ui文件夹所在目录 }) prefix := "/swagger-ui/" mux.Handle(prefix, http.StripPrefix(prefix, fileServer)) }
2.注册 swagger
在 gateway.go
中添加如下代码
//注册swagger mux.HandleFunc("/swagger/", swagger.ServeSwaggerFile) swagger.ServeSwaggerUI(mux)
到这里我们已经完成了 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
文件
syntax = "proto3"; package proto; import "github.com/mwitkow/go-proto-validators/validator.proto"; import "go-grpc-example/10-grpc-gateway/proto/google/api/annotations.proto"; import "go-grpc-example/10-grpc-gateway/proto/google/options/annotations.proto"; message InnerMessage { // some_integer can only be in range (1, 100). int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}]; // some_float can only be in range (0;1). double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}]; } message OuterMessage { // important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax). string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}]; // proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage. InnerMessage inner = 2 [(validator.field) = {msg_exists : true}]; } option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { security_definitions: { security: { key: "bearer" value: { type: TYPE_API_KEY in: IN_HEADER name: "Authorization" description: "Authentication token, prefixed by Bearer: Bearer <token>" } } } security: { security_requirement: { key: "bearer" } } info: { title: "grpc gateway sample"; version: "1.0"; license: { name: "MIT"; }; } schemes: HTTPS }; service Simple{ rpc Route (InnerMessage) returns (OuterMessage){ option (google.api.http) ={ post:"/v1/example/route" body:"*" }; // //禁用bearer token // option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = { // security: { } // Disable security key // }; } }
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK