深入淺出 Gin 生命週期

liuqing_hu發表於2020-12-11

本文首發於 深入淺出 Gin 生命週期 轉載請註明出處。

Gin 是一個用 Go (Golang) 編寫的 web 框架,由於出色的效能優勢而被廣泛使用,這裡我們就來分析下 Gin 的請求生命週期

1 Gin 目錄結構

先來了解下其目錄結構:

.
├── binding 依據 HTTP 請求 Content-Type 解析響應資料格式
│   ├── binding.go
│   ├── binding_nomsgpack.go
│   ├── default_validator.go
│   ├── form.go
│   ├── form_mapping.go
│   ├── header.go
│   ├── json.go
│   ├── msgpack.go
│   ├── multipart_form_mapping.go
│   ├── protobuf.go
│   ├── query.go
│   ├── uri.go
│   ├── xml.go
│   ├── yaml.go
├── ginS
│   └── gins.go
├── internal
│   ├── bytesconv
│   │   ├── bytesconv.go
│   └── json
│       ├── json.go
│       └── jsoniter.go
├── render 依據解析的 HTTP 請求 Content-Type 響應格式生成響應
│   ├── data.go
│   ├── html.go
│   ├── json.go
│   ├── msgpack.go
│   ├── protobuf.go
│   ├── reader.go
│   ├── redirect.go
│   ├── render.go
│   ├── text.go
│   ├── xml.go
│   └── yaml.go
├── auth.go
├── *context.go
├── context_appengine.go
├── debug.go
├── deprecated.go
├── errors.go
├── fs.go
├── *gin.go
├── logger.go
├── mode.go 設定 Gin 執行環境模式
├── path.go Path 處理
├── recovery.go 處理 Panic 的 Recovery 中介軟體
├── *response_writer.go ResponseWriter
├── *routergroup.go 路由組設定
├── tree.go 路由演算法
├── utils.go helper 函式
└── version.go

其中比較重要的模組為: context.go,gin.go,routergroup.go,以及 tree.go;分別處理 HTTP 請求及響應上下文,gin 引擎初始化,路由註冊及路由查詢演算法實現。

binding 目錄內提供基於 HTTP 請求訊息頭 Context-Type 的 MIME 資訊自動解析功能,相對應的 Render 目錄下提供具體資料格式渲染的實現方法。

2 Gin 請求生命週期

本文著重介紹 Gin 實現一個 Web 伺服器,從請求到達到生成響應整個生命週期內的核心功能點,這將有助於我們理解 Gin 的執行原理和以後的開發工作的展開。

2.1 簡單瞭解下 Gin 服務執行流程

先從官網的第一個 demo example.go 出發:

package main

import "github.com/gin-gonic/gin"

func main() {
    // 建立 Gin Engine 例項
    r := gin.Default()

    // 設定請求 URI /ping 的路由及響應處理函式
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    // 啟動 Web 服務,監聽埠,等待 HTTP 請求到並生成響應
    r.Run() // 監聽並在 0.0.0.0:8080 上啟動服務
}

通過執行 go run example.go 命令來執行程式碼,它會啟動一個阻塞程式監聽並等待 HTTP 請求:

# 執行 example.go 並且在瀏覽器中訪問 0.0.0.0:8080/ping
$ go run example.go

從程式碼中我們可以看出通過 Gin 實現一個最簡單的 Web 伺服器,只需 3 個步驟:

1)建立 Gin 例項
2)註冊路由及處理函式
3)啟動 Web 服務

2.2 Gin 生命週期

2.2.1 建立 Gin 例項

Gin 例項建立通過 gin.Default() 方法完成,其定義在 gin.go#L159 檔案裡:

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

Default() 方法實現如下功能:
1)建立 Gin 框架物件 Engine
2)配置 Gin 預設的中介軟體,Logger() 和 Recovery(),其實現分別位於 logger.go 和 recovery.go 檔案內
3)返回 Gin 框架物件

其中 New() 方法會例項化 Gin 的 Engine 物件,gin.go#L129

// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash:  true
// - RedirectFixedPath:      false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP:    true
// - UseRawPath:             false
// - UnescapePathValues:     true
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        RemoveExtraSlash:       false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJSONPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

例項化比較核心的功能是:
1)初始化 Engine 物件, 關鍵步驟是初始化路由組 RouterGroup。
2)初始化 pool, 這是核心步驟. pool 用來儲存 context 上下文物件. 用來優化處理 http 請求時的效能。

後面會重點分析 engine.pool 的實現細節。

Engine 是 Gin 框架的核心引擎 gin.go#L56,資料結構如下:

type Engine struct {
    RouterGroup // 關鍵:路由組

    // 設定開關
    RedirectTrailingSlash bool
    RedirectFixedPath bool
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool

    AppEngine bool
    UseRawPath bool

    UnescapePathValues bool
    MaxMultipartMemory int64
    RemoveExtraSlash bool

    // 界定符
    delims           render.Delims
    secureJSONPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain

    pool             sync.Pool // 關鍵:context 處理

    trees            methodTrees
    maxParams        uint16
}

Engine 結構體內部除一些功能性開關設定外,核心的就是 RouterRroup,pool 和 trees。Gin 的所有元件都是由 Engine 驅動。

2.2.2 路由註冊

完成 Gin 的例項化之後,我們可以通過 r.GET(“/ping”, func(c *gin.Context) {}) 定義 HTTP 路由及處理 handler 函式。

以 gin.GET 為例,展開原始碼如下:

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

gin.GET 定義 HTTP GET 請求的路由及處理方法,並返回 IRoutes 物件例項。

2.2.2.1 RouterGroup 結構體
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

RouterGroup routergroup.go#L41 用於配置路由,其中:

  • Handlers 陣列定義了 Gin 中介軟體呼叫的 handler 方法
  • engine 為 gin.go 例項化時設定的 Engine 例項物件
2.2.2.2 handle 新增路由

gin.GET 方法內部通過呼叫 group.handle() routergroup.go#L72 方法新增路由:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)

    group.engine.addRoute(httpMethod, absolutePath, handlers)// 新增路由
    return group.returnObj()
}

路由就和 Engine 繫結好關係了。

2.2.2.3 IRoute 介面型別
// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

IRoute 是個介面型別,定義了 router 所需的 handle 介面,RouterGroup 實現了這個介面。

2.2.2.4 小結

推而廣之,Gin 還支援如下等路由註冊方法:

  • r.POST
  • r.DELETE
  • r.PATCH
  • r.PUT
  • r.OPTIONS
  • r.HEAD
  • 以及 r.Any

它們都定義在 routergroup.go 檔案內。

2.2.3 接收請求並響應

Gin 例項化和路由設定後工作完成後,我們進入 Gin 生命週期執行的核心功能分析,Gin 究竟是如何啟動 Web 服務,監聽 HTTP 請求並執行 HTTP 請求處理函式生成響應的。這些工作統統從 gin.Run() 出發 gin.go#L305

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

gin.Run() 是 net/http 標準庫 http.ListenAndServe(addr, router) 的簡寫,功能是將路由連線到 http.Server 啟動並監聽 HTTP 請求。

由此,我們不得不放下手頭的工作,率先了解下 net/http 標準庫的執行邏輯。

2.2.3.1 net/http 標準庫

net/http 標準庫的 ListenAndServe(addr string, handler Handler) 方法定義在 net/http/server.go#L3162 檔案裡。

  • 引數簽名的第一個引數是監聽的服務地址和埠;
  • 第二個引數接收一個 Handler 物件它是一個介面型別需要實現 ServeHTTP(ResponseWriter, *Request) 方法。
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

ListenAndServe(addr string, handler Handler) 內部則呼叫的是 Server 物件的 ListenAndServe() 方法由交由它啟動監聽和服務功能:

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)// 監聽
    if err != nil {
        return err
    }
    return srv.Serve(ln)// 啟動服務等待連線
}

然後,執行 srv.Serve(ln)Server.Serve(l net.Listener) server.go#L2951,在 net.Listen(“tcp”, addr) 等待連線,建立新的 goroutine 來處理請求和生成響應的業務邏輯:

func (srv *Server) Serve(l net.Listener) error {
    ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            ...
            return e
        }
        if cc := srv.ConnContext; cc != nil {
            ctx = cc(ctx, rw)
            if ctx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx) // 啟動 Web 服務
    }
}

最後,進入到 go c.serve(ctx) 啟動 Web 服務,讀取 HTTP 請求資料,生成響應 server.go#L1817

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    ...

    // HTTP/1.x from here on.
    for {
        w, err := c.readRequest(ctx)// 讀取 HTTP 去請求

        ...

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        // But we're not going to implement HTTP pipelining because it
        // was never deployed in the wild and the answer is HTTP/2.
        serverHandler{c.server}.ServeHTTP(w, w.req)
        ...
    }
}

最終,呼叫 r.Run() 方法傳入的 Engine 來執行 serverHandler{c.server}.ServeHTTP(w, w.req) 處理接收到的 HTTP 請求和生成響應,這裡將響應處理的控制權交回給 Gin Engine。

小結

Go 標準庫 net/http 提供了豐富的 Web 程式設計介面支援,感興趣的朋友可以深入研究下 net/http 標準庫原始碼,瞭解其實現細節。

2.2.3.2 Engine.ServeHTTP 處理 HTTP 請求

Engine.ServeHTTP 是 Gin 框架核心中的核心,我們來看下它是如何處理請求和響應的:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // 從臨時物件池 pool 獲取 context 上下文物件
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c) // 處理 HTTP 請求

    engine.pool.Put(c) // 使用完 context 物件, 歸還給 pool 
}

ServeHTTP會先獲取 Gin Context 上下文資訊,接著將 Context 注入到 engine.handleHTTPRequest(c) 方法內來處理 HTTP 請求:

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path

    ...

    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {

        ...

        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, unescape)

        ...

        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next() // 具體執行響應處理
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && rPath != "/" {
            ...
        }
        break
    }

    ...
}

handleHTTPRequest 完成 路由回撥 方法的查詢,執行 Gin.Context.Next() 呼叫處理響應。

2.2.3.3 Gin.Context.Next() 在內部中介軟體執行 handler 方法

Gin.Context.Next() 僅有數行程式碼:

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

功能是在 Gin 內部中介軟體中執行 handler 呼叫,即 r.GET() 中傳入的

func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

方法生成 HTTP 響應。

到這裡我們完成了 Gin 的請求和響應的完整流程的原始碼走讀,但是我們有必要對 Gin.Context 有多一些的瞭解。

2.2.3.4 Gin.Context 上下文處理

Gin 的 Context 實現了對 request 和 response 的封裝是 Gin 的核心實現之一,其資料結構如下:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter

    Request   *http.Request    // HTTP 請求
    Writer    ResponseWriter   // HTTP 響應

    Params   Params
    handlers HandlersChain  // 關鍵: 陣列: 內包含方法集合
    index    int8

    engine *Engine  // 關鍵: 引擎

    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}

    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs

    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
}

其包含了 Gin 請求及響應的上下文資訊和 Engine 指標資料

  • Request *http.Request : HTTP 請求
  • Writer ResponseWriter : HTTP 響應
  • handlers HandlersChain : 是 type HandlerFunc func(*Context) 方法集即路由設定的回撥函式
  • engine *Engine : gin框架物件

Gin 官方文件 幾乎所有的示例都是在講解 Context 的使用方法,可用說研究 Context 原始碼對用好 Gin 框架會起到只管重要的作用。

感興趣的朋友可以自行閱讀 Context.go

3 參考資料

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章