前言
我们通常把 RPC 用作内部通信, 而使用 Restful API 进行外部通信. 为了避免写两套应用, 我们使用 https://github.com/grpc-ecosystem/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
下载地址 https://github.com/swagger-api/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-gatewayGitHub 上的这个 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
来源: https://www.cnblogs.com/FireworksEasyCool/p/12782137.html