• Go gRPC进阶-gRPC转换HTTP(十)


    前言

    我们通常把RPC用作内部通信,而使用Restful Api进行外部通信。为了避免写两套应用,我们使用grpc-gatewaygRPC转成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
    ", 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服务已经同时支持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插件,第一篇有介绍,只要保存,就会自动编译,很方便,无需记忆指令。完整配置如下:

        // 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-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-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

  • 相关阅读:
    网易云信流媒体服务端架构设计与实现
    从零开始搭建创业公司后台技术栈
    协程(coroutine)简介
    微服务的简介和技术栈
    分布式系统中最容易被忽视的六大“暗流”
    分布式架构的演进
    全网最详尽的负载均衡原理图解
    图解 | 搞定分布式,程序员进阶之路
    Enterprise Library 3.0体验(4):Validation Application Block与ASP.NET的集成
    Enterprise Library 3.0 发布
  • 原文地址:https://www.cnblogs.com/FireworksEasyCool/p/12782137.html
Copyright © 2020-2023  润新知