扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 宝典简介

  • gin系列目录

  • 杂谈

  • 爬虫系列目录

  • gRPC系列目录

    • gRPC gRPC Gateway实践一 介绍与环境安装
    • gRPC gRPC Gateway实践二 有些复杂的Hello World
    • gRPC gRPC Gateway实践三 Swagger了解一下

扫码订阅《 》或入驻星球,即可阅读文章!

gRPC gRPC Gateway实践三 Swagger了解一下


煎鱼

在上一节 (opens new window),我们完成了一个服务端同时支持Rpc和RESTful Api后,你以为自己大功告成了,结果突然发现要写Api文档和前端同事对接= = 。。。

你寻思有没有什么组件能够自动化生成Api文档来解决这个问题,就在这时你发现了Swagger,一起了解一下吧!

# 介绍

# Swagger

Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,支持从设计和文档到测试和部署的整个API生命周期的开发

Swagger是目前最受欢迎的RESTful Api文档生成工具之一,主要的原因如下

  • 跨平台、跨语言的支持
  • 强大的社区
  • 生态圈 Swagger Tools(Swagger Editor (opens new window)、Swagger Codegen (opens new window)、Swagger UI (opens new window) …)
  • 强大的控制台

同时grpc-gateway也支持Swagger

[image]

# OpenAPI规范

OpenAPI规范是Linux基金会的一个项目,试图通过定义一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程。OpenAPI规范帮助我们描述一个API的基本信息,比如:

  • 有关该API的一般性描述
  • 可用路径(/资源)
  • 在每个路径上的可用操作(获取/提交…)
  • 每个操作的输入/输出格式

目前V2.0版本的OpenAPI规范 (opens new window)(也就是SwaggerV2.0规范)已经发布并开源在github上。该文档写的非常好,结构清晰,方便随时查阅。

注:OpenAPI规范的介绍引用自原文 (opens new window)

# 使用

# 生成Swagger的说明文件

第一,我们需要检查$GOBIN下是否包含protoc-gen-swagger可执行文件

若不存在则需要执行:

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

等待执行完毕后,可在$GOPATH/bin下发现该执行文件,将其移动到$GOBIN下即可

第二,回到$GOPATH/src/grpc-hello-world/proto下,执行命令

protoc -I/usr/local/include -I. -I$GOPATH/src/grpc-hello-world/proto/google/api --swagger_out=logtostderr=true:. ./hello.proto
1

成功后执行ls即可看到hello.swagger.json文件

# 下载Swagger UI文件

Swagger提供可视化的API管理平台,就是Swagger UI (opens new window)

我们将其源码下载下来,并将其dist目录下的所有文件拷贝到我们项目中的$GOPATH/src/grpc-hello-world/third_party/swagger-ui去

# 将Swagger UI转换为Go源代码

在这里我们使用的转换工具是go-bindata (opens new window)

它支持将任何文件转换为可管理的Go源代码。用于将二进制数据嵌入到Go程序中。并且在将文件数据转换为原始字节片之前,可以选择压缩文件数据

# 安装

go get -u github.com/jteeuwen/go-bindata/...
1

完成后,将$GOPATH/bin下的go-bindata移动到$GOBIN下

# 转换

在项目下新建pkg/ui/data/swagger目录,回到$GOPATH/src/grpc-hello-world/third_party/swagger-ui下,执行命令

go-bindata --nocompress -pkg swagger -o pkg/ui/data/swagger/datafile.go third_party/swagger-ui/...
1

# 检查

回到pkg/ui/data/swagger目录,检查是否存在datafile.go文件

# Swagger UI文件服务器(对外提供服务)

在这一步,我们需要使用与其配套的go-bindata-assetfs (opens new window)

它能够使用go-bindata所生成Swagger UI的Go代码,结合net/http对外提供服务

# 安装

go get github.com/elazarl/go-bindata-assetfs/...
1

# 编写

通过分析,我们得知生成的文件提供了一个assetFS函数,该函数返回一个封装了嵌入文件的http.Filesystem,可以用其来提供一个HTTP服务

那么我们来编写Swagger UI的代码吧,主要是两个部分,一个是swagger.json,另外一个是swagger-ui的响应

# serveSwaggerFile

引用包strings、path

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/")
    p = path.Join("proto", p)
    log.Printf("Serving swagger-file: %s", p)
    http.ServeFile(w, r, p)
}
1
2
3
4
5
6
7
8
9
10
11

在函数中,我们利用r.URL.Path进行路径后缀判断

主要做了对swagger.json的文件访问支持(提供https://127.0.0.1:50052/swagger/hello.swagger.json的访问)

# serveSwaggerUI

引用包github.com/elazarl/go-bindata-assetfs、grpc-hello-world/pkg/ui/data/swagger

func serveSwaggerUI(mux *http.ServeMux) {
    fileServer := http.FileServer(&assetfs.AssetFS{
        Asset:    swagger.Asset,
        AssetDir: swagger.AssetDir,
        Prefix:   "third_party/swagger-ui",
    })
    prefix := "/swagger-ui/"
    mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
}
1
2
3
4
5
6
7
8
9

在函数中,我们使用了go-bindata-assetfs (opens new window)来调度先前生成的datafile.go,结合net/http来对外提供swagger-ui的服务

# 结合

在完成功能后,我们发现path.Join("proto", p)是写死参数的,这样显然不对,我们应该将其导出成外部参数,那么我们来最终改造一番

首先我们在server.go新增包全局变量SwaggerDir,修改cmd/server.go文件:

package cmd
import (
    "log"
    "github.com/spf13/cobra"
    "grpc-hello-world/server"
)
var serverCmd = &cobra.Command{
    Use:   "server",
    Short: "Run the gRPC hello-world server",
    Run: func(cmd *cobra.Command, args []string) {
        defer func() {
            if err := recover(); err != nil {
                log.Println("Recover error : %v", err)
            }
        }()
        server.Run()
    },
}
func init() {
    serverCmd.Flags().StringVarP(&server.ServerPort, "port", "p", "50052", "server port")
    serverCmd.Flags().StringVarP(&server.CertPemPath, "cert-pem", "", "./conf/certs/server.pem", "cert-pem path")
    serverCmd.Flags().StringVarP(&server.CertKeyPath, "cert-key", "", "./conf/certs/server.key", "cert-key path")
    serverCmd.Flags().StringVarP(&server.CertServerName, "cert-server-name", "", "grpc server name", "server's hostname")
    serverCmd.Flags().StringVarP(&server.SwaggerDir, "swagger-dir", "", "proto", "path to the directory which contains swagger definitions")
    rootCmd.AddCommand(serverCmd)
}
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

修改path.Join("proto", p)为path.Join(SwaggerDir, p),这样的话我们swagger.json的文件路径就可以根据外部情况去修改它

最终server.go文件内容:

package server
import (
    "crypto/tls"
    "net"
    "net/http"
    "log"
    "strings"
    "path"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "github.com/elazarl/go-bindata-assetfs"
    pb "grpc-hello-world/proto"
    "grpc-hello-world/pkg/util"
    "grpc-hello-world/pkg/ui/data/swagger"
)
var (
    ServerPort string
    CertServerName string
    CertPemPath string
    CertKeyPath string
    SwaggerDir string
    EndPoint string
    tlsConfig *tls.Config
)
func Run() (err error) {
    EndPoint = ":" + ServerPort
    tlsConfig = util.GetTLSConfig(CertPemPath, CertKeyPath)
    conn, err := net.Listen("tcp", EndPoint)
    if err != nil {
        log.Printf("TCP Listen err:%v\n", err)
    }
    srv := newServer(conn)
    log.Printf("gRPC and https listen on: %s\n", ServerPort)
    if err = srv.Serve(util.NewTLSListener(conn, tlsConfig)); err != nil {
        log.Printf("ListenAndServe: %v\n", err)
    }
    return err
}
func newServer(conn net.Listener) (*http.Server) {
    grpcServer := newGrpc()
    gwmux, err := newGateway()
    if err != nil {
        panic(err)
    }
    mux := http.NewServeMux()
    mux.Handle("/", gwmux)
    mux.HandleFunc("/swagger/", serveSwaggerFile)
    serveSwaggerUI(mux)
    return &http.Server{
        Addr:      EndPoint,
        Handler:   util.GrpcHandlerFunc(grpcServer, mux),
        TLSConfig: tlsConfig,
    }
}
func newGrpc() *grpc.Server {
    creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)
    if err != nil {
        panic(err)
    }
    opts := []grpc.ServerOption{
        grpc.Creds(creds),
    }
    server := grpc.NewServer(opts...)
    pb.RegisterHelloWorldServer(server, NewHelloService())
    return server
}
func newGateway() (http.Handler, error) {
    ctx := context.Background()
    dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertServerName)
    if err != nil {
        return nil, err
    }
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    gwmux := runtime.NewServeMux()
    if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
        return nil, err
    }
    return gwmux, nil
}
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/")
    p = path.Join(SwaggerDir, p)
    log.Printf("Serving swagger-file: %s", p)
    http.ServeFile(w, r, p)
}
func serveSwaggerUI(mux *http.ServeMux) {
    fileServer := http.FileServer(&assetfs.AssetFS{
        Asset:    swagger.Asset,
        AssetDir: swagger.AssetDir,
        Prefix:   "third_party/swagger-ui",
    })
    prefix := "/swagger-ui/"
    mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
}
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

# 测试

访问路径https://127.0.0.1:50052/swagger/hello.swagger.json,查看输出内容是否为hello.swagger.json的内容,例如: [image]

访问路径https://127.0.0.1:50052/swagger-ui/,查看内容 [image]

# 小结

至此我们这一章节就完毕了,Swagger和其生态圈十分的丰富,有兴趣研究的小伙伴可以到其官网 (opens new window)认真研究

而目前完成的程度也满足了日常工作的需求了,可较自动化的生成RESTful Api文档,完成与接口对接

# 参考

# 示例代码

  • grpc-hello-world (opens new window)
  • 介绍
  • Swagger
  • OpenAPI规范
  • 使用
  • 生成Swagger的说明文件
  • 下载Swagger UI文件
  • 将Swagger UI转换为Go源代码
  • Swagger UI文件服务器(对外提供服务)
  • 测试
  • 小结
  • 参考
  • 示例代码