多圖詳解萬星 Restful 框架原理與實現

rest 框架概覽

我們先通過 go-zero 自帶的命令行工具 goctl 來生成一個 api service,其 main 函數如下:

func main() {
 flag.Parse()

 var c config.Config
 conf.MustLoad(*configFile, &c)

 ctx := svc.NewServiceContext(c)
 server := rest.MustNewServer(c.RestConf)
 defer server.Stop()

 handler.RegisterHandlers(server, ctx)

 fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
 server.Start()
}
  1. 解析配置文件

  2. 將配置文件傳入,初始化 serviceContext

  3. 初始化 rest server

  4. context 注入 server 中:

  5. 註冊路由

  6. context 中的啓動的 endpoint 同時注入到 router 當中

  7. 啓動 server

接下來我們來一步步講解其設計原理!Let's Go!

web 框架

從日常開發經驗來說,一個好的 web 框架大致需要滿足以下特性:

  1. 路由匹配 / 多路由支持

  2. 支持自定義中間件

  3. 框架和業務開發完全解耦,方便開發者快速開發

  4. 參數校驗 / 匹配

  5. 監控 / 日誌 / 指標等服務自查功能

  6. 服務自保護 (熔斷 / 限流)

go-zero rest 設計

https://github.com/zeromicro/go-zero/tree/master/rest

概覽

  1. 藉助 context (不同於 gin 的 context),將資源初始化好 → 保存在 serviveCtx 中,在 handler 中共享(至於資源池化,交給資源自己處理,serviveCtx 只是入口和共享點)

  2. 獨立 router 聲明文件,同時加入 router group 的概念,方便開發者整理代碼結構

  3. 內置若干中間件:監控 / 熔斷 / 鑑權等

  4. 利用 goctl codegen + option 設計模式,方便開發者自己控制部分中間件的接入

上圖描述了 rest 處理請求的模式和大部分處理路徑。

  1. 框架內置的中間件已經幫開發者解決了大部分服務自處理的邏輯

  2. 同時 go-zero 在 business logic 處也給予開發者開箱即用的組件 (dq、fx 等)

  3. 從開發模式上幫助開發者只需要關注自己的 business logic 以及所需資源準備

下面我們來細說一下整個 rest 是如何啓動的?

啓動流程

上圖描述了整體 server 啓動經過的模塊和大致流程。準備按照如下流程分析 rest 實現:

  1. 基於 http.server 封裝以及改造:把 engine(web 框架核心) 和 option 隔離開

  2. 多路由匹配採取 radix-tree 構造

  3. 中間件採用洋蔥模型 → []Middleware

  4. http parse 解析以及匹配校驗 → httpx.Parse()

  5. 在請求過程會收集指標 (createMetrics()) 以及監控埋點 (prometheus)

server engine 封裝

點開大圖觀看

engine 貫穿整個 server 生命週期中:

  1. router 會攜帶開發者定義的 path/handler,會在最後的 router.handle() 執行

  2. 註冊的自定義中間件 + 框架中間件,在 router handler logic 前執行

在這裏:go-zero 處理的粒度在 route 上,封裝和處理都在 route 一層層執行

路由匹配

那麼當 request 到來,首先是如何到路由這一層的?

首先在開發最原始的 http server ,都有這麼一段代碼:

type helloHandler struct{}

func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!"))
}

func main() {
    http.Handle("/"&helloHandler{})
    http.ListenAndServe(":12345", nil)
}

http.ListenAndServe()  內部會執行到:server.ListenAndServe()

我們看看在 rest 裏面是怎麼運用的:

而傳入的 handler 其實就是:router.NewRouter() 生成的 router。這個 router 承載了整個 server 的處理函數集合。

同時 http.Server 結構在初始化時,是把 handler 注入到裏面的:

type Server struct {
 ...
 Handler Handler
}

func start(..., handler http.Handler, run func(srv *http.Server) error) (err error) {
 server := &http.Server{
  Addr:    fmt.Sprintf("%s:%d", host, port),
  Handler: handler,
 }
 ...
 return run(server)
}

在 http.Server 接收 req 後,最終執行的也是:handler.ServeHTTP(rw, req)

所以內置的 router 也需要實現 ServeHTTP 。至於 router 自己是怎麼實現 ServeHTTP : 無外乎就是尋找匹配路由,然後執行路由對應的 handle logic。

解析參數

解析參數是 http 框架需要提供的基本能力。在 goctl code gen 生成的代碼中,handler 層已經集成了 req argument parse 函數:

// generate by goctl
func QueryAllTaskHandler(ctx *svc.ServiceContext) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  // custom request in .api file
  var req types.QueryAllTaskRequest
  // parse http request
  if err := httpx.Parse(r, &req); err != nil {
   httpx.Error(w, err)
   return
  }

  l := logic.NewEventLogic(r.Context(), ctx)
  resp, err := l.QueryAllTask(req)
  baseresponse.FormatResponseWithRequest(resp, err, w, r)
 }
}

進入到 httpx.Parse() ,主要解析以下幾塊:

https://github.com/zeromicro/go-zero/blob/master/rest/httpx/requests.go#L32:6

  1. 解析 path

  2. 解析 form 表單

  3. 解析 http header

  4. 解析 json

Parse() 中的 參數校驗 的功能見:

https://go-zero.dev/cn/api-grammar.html 中的 tag修飾符

Tips

學習源碼推薦 fork 出來邊看邊寫註釋和心得,可以加深理解,以後用到這塊功能的時候也可以回頭翻閱。

項目地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支持我們!

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/v_-2cJnZ67so2yUhVgQgGQ