5.3 Swagger瞭解一下
在上一節,我們完成了一個服務端同時支援Rpc和RESTful Api後,你以為自己大功告成了,結果突然發現要寫Api文件和前端同事對接= = 。。。
你尋思有沒有什麼元件能夠自動化生成Api文件來解決這個問題,就在這時你發現了Swagger,一起了解一下吧!
介紹
Swagger
Swagger是全球最大的OpenAPI規範(OAS)API開發工具框架,支援從設計和文件到測試和部署的整個API生命週期的開發
Swagger是目前最受歡迎的RESTful Api文件生成工具之一,主要的原因如下
- 跨平臺、跨語言的支援
- 強大的社群
- 生態圈 Swagger Tools(Swagger Editor、Swagger Codegen、Swagger UI ...)
- 強大的控制檯
同時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可執行檔案
若不存在則需要執行:
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
等待執行完畢後,可在$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
成功後執行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程式中。並且在將檔案資料轉換為原始位元組片之前,可以選擇壓縮檔案資料
安裝
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下,執行命令
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 UI的Go程式碼,結合net/http對外提供服務
安裝
go get github.com/elazarl/go-bindata-assetfs/...
編寫
透過分析,我們得知生成的檔案提供了一個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)
}
在函式中,我們利用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))
}
在函式中,我們使用了go-bindata-assetfs來排程先前生成的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)
}
修改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))
}
測試
訪問路徑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文件,完成與介面對接