「连载三」Swagger了解一下

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

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

介绍

Swagger

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

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

同时grpc-gateway也支持Swagger

[image]

OpenAPI规范

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

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

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

注:OpenAPI规范的介绍引用自原文

使用

生成Swagger的说明文件

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

若不存在则需要执行:

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

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

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

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

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

下载Swagger UI文件

Swagger提供可视化的API管理平台,就是Swagger UI

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

Swagger UI转换为Go源代码

在这里我们使用的转换工具是go-bindata

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

安装

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

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

转换

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

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

检查

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

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

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

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

安装

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

编写

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

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

serveSwaggerFile

引用包stringspath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
}

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

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

serveSwaggerUI

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

1
2
3
4
5
6
7
8
9
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))
}

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

结合

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

首先我们在server.go新增包全局变量SwaggerDir,修改cmd/server.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
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)
}

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

最终server.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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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))
}

测试

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

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

小结

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

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

参考

示例代码


免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 [email protected]

推荐阅读:

文章标题:「连载三」Swagger了解一下

本文作者:知识铺

发布时间:2018-03-04, 12:00:00

最后更新:2020-04-19, 17:52:49

原始链接:https://blog.zshipu.com/2018/03/04/golang/go/grpc-gateway/2018-03-04-swagger/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏