Gin 源碼學習(四)丨Gin 對請求的處理流程

在上一篇文章 Gin 源碼學習(三)丨路由是如何構建和匹配的? 中,講解了 Gin 的路由是如何實現的,那麼,當路由成功匹配後,或者匹配失敗後,在 Gin 內部會對其如何處理呢?json

在這一篇文章中,將講解 Gin 對一個 HTTP 請求的具體處理流程是怎樣的。緩存

下面,將對一個請求進入 Gin 的處理範圍後的內容,進行一步步展開,講解 Gin 對請求的處理流程。安全

Go 版本:1.14cookie

Gin 版本:v1.5.0閉包

目錄

  • 請求的處理流程
  • 小結

請求的處理流程

在上一篇文章中,咱們講到 Gin 其實實現了 Go 自帶函數庫 net/http 庫中的 Handler 接口,而且從實現的源代碼中能夠發現,當一個 HTTP 請求到達 Gin 處理的範圍時,首先是在 Gin 的 Engine 類型中的 ServeHTTP(w http.ResponseWriter, req *http.Request) 方法中對 Gin 保存上下文信息的 gin.Context 進行屬性設置和重置操做,而後纔是使用 engine.handleHTTPRequest(c *Context) 方法來對 HTTP 請求進行處理的,下面,咱們一步一步來對相關源代碼進行分析:併發

// ServeHTTP conforms to the http.Handler interface.
// 符合 http.Handler 接口的約定
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// 從對象池中獲取已存在的上下文對象
	c := engine.pool.Get().(*Context)
	// 重置該上下文對象的 ResponseWriter 屬性
	c.writermem.reset(w)
	// 設置該上下文對象的 Request 屬性
	c.Request = req
	// 重置上下文中的其餘屬性信息
	c.reset()

	// 對請求進行處理
	engine.handleHTTPRequest(c)

	// 將該上下文對象從新放回對象池中
	engine.pool.Put(c)
}

// 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.
// 上下文是 Gin 最重要的部分.
// 它容許咱們在中間件之間傳遞變量, 管理流程, 例如驗證請求的 JSON 並呈現 JSON 響應.
type Context struct {
	// 對 net/http 庫中的 ResponseWriter 進行了封裝
	writermem responseWriter
	// 請求對象
	Request   *http.Request
	// 非 net/http 庫中的 ResponseWriter
	// 而是 Gin 用來構建 HTTP 響應的一個接口
	Writer    ResponseWriter

	// 存放請求中的 URI 參數
	Params   Params
	// 存放該請求的處理函數切片, 包括中間件加最終處理函數
	handlers HandlersChain
	// 用於標記當前執行的處理函數
	index    int8
	// 請求的完整路徑
	fullPath string

	// Gin 引擎對象
	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

	// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
	// 用於緩存請求的 URL 參數
	queryCache url.Values

	// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	// 用於緩存請求體中的參數
	formCache url.Values
}
複製代碼

上面源代碼中,展現了 engine.ServeHTTP(w http.ResponseWriter, req *http.Request) 方法的執行過程以及 gin.Context 類型的內部結構,須要注意的是,gin.Context 實現了 Go 的 Context 接口,可是並無對其作併發安全處理,所以,應該避免多個 goroutine 同時訪問同一個 Context,若是存在這種狀況,需使用 gin.Context.Copy() 方法,對 gin.Context 進行復制使用。ide

而且,Gin 使用對象池來存放上下文信息,這是一個很是巧妙的設計思想,由於在 Gin 中,會將請求的許多處理信息存放於 gin.Context 中,而 Go 是一門帶有 GC(垃圾回收)的語言,假如在訪問量較大的場景下,若是不使用對象池來緩衝 gin.Context 對象的話,那麼爲每個請求建立一個 gin.Context 對象,而且在完成請求的處理後,將該 gin.Context 對象交給 GC 去處理,這無疑對 GC 增添了許多壓力。因爲 gin.Context 只是用於保存當前請求的處理信息,用於上下文之間的參數傳遞,屬於徹底能夠複用的對象,所以,使用對象池對其進行存放能夠在必定程度上減小 GC 壓力。函數

下面先來看一下在 engine.ServeHTTP(w http.ResponseWriter, req *http.Request) 方法中對 gin.Context 初始化時設置了什麼樣的初始值:post

const (
	// 表示未寫入
	noWritten     = -1
	// 200 狀態碼
	defaultStatus = http.StatusOK
)

type responseWriter struct {
	// net/http 庫中的 ResponseWriter
	http.ResponseWriter
	// 響應內容大小
	size   int
	// 響應狀態碼
	status int
}

func (w *responseWriter) reset(writer http.ResponseWriter) {
	w.ResponseWriter = writer
	w.size = noWritten
	w.status = defaultStatus
}

func (c *Context) reset() {
	c.Writer = &c.writermem
	c.Params = c.Params[0:0]
	c.handlers = nil
	c.index = -1
	c.fullPath = ""
	c.Keys = nil
	c.Errors = c.Errors[0:0]
	c.Accepted = nil
	c.queryCache = nil
	c.formCache = nil
}
複製代碼

這裏須要留意的是 gin.Context.index 的初始值,Gin 經過該值來調用處理函數和判斷當前上下文是否終止。學習

下面,咱們來看 Gin 對請求的處理流程,先來看一下 engine.handleHTTPRequest(c *Context) 方法,該方法在前面的幾篇 Gin 源碼學習的文章中出現過屢次,因此這裏也是一樣,只保留其與當前文章主題相關的源代碼:

func (engine *Engine) handleHTTPRequest(c *Context) {
	// 省略...

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			// 開始對請求執行中間件和處理函數
			c.Next()
			// 設置響應頭信息
			c.writermem.WriteHeaderNow()
			return
		}
		// 省略...
		break
	}

	// 省略...
	c.handlers = engine.allNoRoute
	// 處理 404 錯誤
	serveError(c, http.StatusNotFound, default404Body)
}
複製代碼

在上一篇文章中講到過 Gin 請求路由的匹配是在 root.getValue(path string, po Params, unescape bool) 方法中實現的,而且,當返回的 value 對象中的 handlers 屬性不爲 nil 時,則表示該請求存在處理函數,而後將 value 對象中的處理函數切片集、請求參數以及請求的完整路徑信息存放至該請求的上下文對象中,接着調用 context.Next() 方法,下面來看一下該方法的源代碼:

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// Next 只能在中間件內部使用
// 它在調用處理程序內的鏈中執行掛起的處理程序
// 相似於遞歸調用或函數裝飾器
func (c *Context) Next() {
    // index 初始值爲 -1
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}
複製代碼

context.Next() 方法的邏輯比較簡單,其實就是遍歷存放於 Gin 上下文中的中間件/處理函數切片,並調用,在上一篇文章中,咱們也講過,context.handlers 切片中,在有多個 HandlerFunc 的時候,除了最後一個爲該路由的處理函數以外,其他的都爲中間件。

這裏須要注意的點是,該 Next() 方法,在使用的時候,只能在中間件內部使用,也就是說,在平常開發中,該方法只能在本身編寫的中間件中出現,而不能出如今其它地方。

下面,咱們以 gin.Default() 建立 gin.Engine 時添加的兩個默認中間件 LoggerRecovery,並結合一個模擬身份驗證的中間件 Auth 爲例,來對 Gin 中間件的工做流程進行詳細講解,先來看一下 gin.Default() 方法添加的默認中間件 Logger 的相關源代碼:

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
// Logger 是一箇中間件, 該中間件會將日誌寫入 gin.DefaultWriter.
// 默認的 gin.DefaultWriter 爲 os.Stdout, 即標準輸出流, 控制檯
func Logger() HandlerFunc {
	return LoggerWithConfig(LoggerConfig{})
}

// LoggerConfig defines the config for Logger middleware.
// Logger 中間件的相關配置
type LoggerConfig struct {
	// Optional. Default value is gin.defaultLogFormatter
	// 用於輸出內容的格式化, 默認爲 gin.defaultLogFormatter
	Formatter LogFormatter

	// Output is a writer where logs are written.
	// Optional. Default value is gin.DefaultWriter.
	// 日誌輸出對象
	Output io.Writer

	// SkipPaths is a url path array which logs are not written.
	// Optional.
	// 忽略日誌輸出的 URL 切片
	SkipPaths []string
}

// LoggerWithConfig instance a Logger middleware with config.
// 使用 LoggerConfig 配置的 Logger 中間件
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
	formatter := conf.Formatter
	if formatter == nil {
		formatter = defaultLogFormatter
	}

	out := conf.Output
	if out == nil {
		out = DefaultWriter
	}

	notlogged := conf.SkipPaths

	// 是否輸出至終端
	isTerm := true

	if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
		(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
		isTerm = false
	}

	// 標記忽略日誌的 path
	var skip map[string]struct{}

	if length := len(notlogged); length > 0 {
		skip = make(map[string]struct{}, length)

		for _, path := range notlogged {
			skip[path] = struct{}{}
		}
	}

	return func(c *Context) {
		// Start timer
		// 記錄開始時間
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		// 繼續執行下一個中間件
		c.Next()

		// Log only when path is not being skipped
		// 若是 path 在 skip 中, 則忽略日誌記錄
		if _, ok := skip[path]; !ok {
			param := LogFormatterParams{
				Request: c.Request,
				isTerm:  isTerm,
				Keys:    c.Keys,
			}

			// Stop timer
			// 記錄結束時間
			param.TimeStamp = time.Now()
			// 計算耗時
			param.Latency = param.TimeStamp.Sub(start)

			// 客戶端 IP
			param.ClientIP = c.ClientIP()
			// 請求方法
			param.Method = c.Request.Method
			// 請求狀態碼
			param.StatusCode = c.Writer.Status()
			// 錯誤信息
			param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
			// 響應體大小
			param.BodySize = c.Writer.Size()

			if raw != "" {
				path = path + "?" + raw
			}

			param.Path = path

			// 日誌打印
			fmt.Fprint(out, formatter(param))
		}
	}
}
複製代碼

其實中間件也就是裝飾器或閉包,實質上就是一種返回類型爲 HandlerFunc 的函數,通俗地講,就是一種返回函數的函數,目的就是爲了在外層函數中,對內層函數進行裝飾或處理,而後再將被裝飾或處理後的內層函數返回。

因爲 HandlerFunc 函數只能接受一個 gin.Context 參數,所以,在上面源代碼中的 LoggerWithConfig(conf LoggerConfig) 函數中,使用 LoggerConfig 配置,對 HandlerFunc 進行裝飾,並返回。

一樣地,在返回的 HandlerFunc 匿名函數中,首先是記錄進入該中間件時的一些信息,包括時間,而後再調用 context.Next() 方法,掛起當前的處理程序,遞歸去調用後續的中間件,當後續全部中間件和處理函數執行完畢時,再回到此處,若是要記錄該 path 的日誌,則再獲取一次當前的時間,與開始記錄的時間進行計算,便可得出本次請求處理的耗時,再保存其它信息,包括請求 IP 和響應的相關信息等,最後將該請求的日誌進行打印處理,這就是使用 gin.Default() 實例一個 gin.Engine 默認添加的 Logger() 中間件的處理流程。

下面,咱們來看一下,另外一個默認中間件 Recovery() 的相關源代碼:

// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// Recovery 中間件用於捕獲處理流程中出現 panic 的錯誤
// 若是鏈接未斷開, 則返回 500 錯誤響應
func Recovery() HandlerFunc {
	// DefaultErrorWriter = os.Stderr
	return RecoveryWithWriter(DefaultErrorWriter)
}

// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
// 使用傳遞的 out 對 Recovery 中間件進行裝飾
func RecoveryWithWriter(out io.Writer) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				// 用於標記鏈接是否斷開
				var brokenPipe bool
				// 從錯誤信息中判斷鏈接是否斷開
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				// 省略, 日誌打印相關...

				// If the connection is dead, we can't write a status to it.
				if brokenPipe {	// 若是鏈接已斷開, 則已經沒法爲其寫入狀態碼
					// 添加錯誤信息至上下文中, 用於日誌輸出
					c.Error(err.(error)) // nolint: errcheck
					// 終止該上下文
					c.Abort()
				} else {	// 鏈接未斷開
					// 終止該上下文並寫入 500 錯誤狀態碼
					c.AbortWithStatus(http.StatusInternalServerError)
				}
			}
		}()
		// 繼續執行下一個中間件
		c.Next()
	}
}
複製代碼

LoggerWithConfig(conf LoggerConfig) 函數同樣,RecoveryWithWriter(out io.Writer) 函數僅爲了對最終返回的中間件 HandlerFunc 函數進行裝飾,在該中間件中,可分爲兩個邏輯塊,一個是 defer,一個是 Next()Next()Logger() 中間件中的 Next() 做用相似,這裏在 defer 中使用 recover() 來捕獲在後續中間件中 panic 的錯誤信息,並對該錯誤信息進行處理。

在該中間件中,首先是判斷當前鏈接是否已中斷,而後是進行相關的日誌處理,最後,若是鏈接已中斷,則直接設置錯誤信息,並終止該上下文,不然,終止該上下文並返回 500 錯誤響應。

下面,咱們來看一下 context.Abort() 方法和 context.AbortWithStatus(code int) 方法的相關源代碼:

// 63
const abortIndex int8 = math.MaxInt8 / 2

// 終止上下文
func (c *Context) Abort() {
	c.index = abortIndex
}

// 判斷上下文是否終止
func (c *Context) IsAborted() bool {
	return c.index >= abortIndex
}

// 終止上下文並將 code 寫入響應頭中
func (c *Context) AbortWithStatus(code int) {
	c.Status(code)
	c.Writer.WriteHeaderNow()
	c.Abort()
}
複製代碼

context.Abort() 方法將當前上下文的 index 值設置爲 63,用於標誌上下文的終止。

context.AbortWithStatus(code int) 也是終止當前的上下文,只不過額外的使用了 code 參數,對響應的頭信息進行了設置。

最後,咱們再來看一個模擬身份校驗的中間件 Auth,其實現的相關源代碼以下:

type RequestData struct{
	Action string `json:"action"`
	UserID int `json:"user_id"`
}

func main() {
	router := gin.Default()
	// 註冊中間件
	router.Use(Auth())

	router.POST("/action", func(c *gin.Context) {
		var RequestData RequestData
		if err := c.BindJSON(&RequestData); err == nil {
			c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "success"})
		}
	})

	router.Run(":8000")
}

func Auth() gin.HandlerFunc {
	// TODO: 可模仿 Logger() 或 Recovery() 中間件, 結合該函數的調用參數, 在此處作一些配置操做
	return func(c *gin.Context) {
		// TODO: 可模仿 Logger() 中間件, 在此處對請求的 path 進行忽略處理
		if auth(c.Request) {
			c.Next()
		} else {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"})
		}
	}
}

func auth(req *http.Request) bool {
	// TODO: 對 http.Request 中的信息進行校驗, 如 cookie, token...
	return req.Header.Get("Auth") == "colelie"
}
複製代碼

首先是 Auth() 函數,該函數用於裝飾並返回 gin.HandlerFunc 函數,在該函數內,返回了一個 gin.HandlerFunc 匿名函數,在該匿名函數中,經過調用 auth(req *http.Request) 函數對請求信息進行校驗,這裏只是一個簡單地對請求頭中的 Auth 進行驗證。

因此,在該案例中,當咱們訪問 /action 接口時,首先會進入 Logger() 中間件,而後進入 Recovery() 中間件,再進入 Auth() 中間件,當前面的中間件都沒有發生對上下文的終止操做時,纔會進入咱們聲明的 router.POST("/action", func) 處理函數。

當咱們向 /action 接口發起一個普通的 POST 請求時,會收到以下響應:

這是因爲在 Auth() 中間件中身份校驗沒經過,咱們爲該請求的頭部信息中添加一個 Key 爲 Auth,Value 爲 colelie 的字段,會收到以下響應:

能夠發現,一樣的,出現了錯誤響應,而此次的錯誤響應碼爲 400,這是爲何呢?

Gin 源碼學習(二)丨請求體中的參數是如何解析的? 中,咱們講過,在使用 MustBind 一類的綁定函數時,若是在參數解析過程當中出現錯誤,會調用 c.AbortWithError(http.StatusBadRequest, err) 方法,終止當前的上下文並返回 400 響應錯誤碼,在上面的聲明的對 /action 的處理函數中,使用了 context.BindJSON(obj interface{}) 方法對請求參數進行綁定操做,下面,咱們在爲請求添加可以綁定成功的請求體,會收到以下響應:

此次,獲得了正確的響應內容。

小結

在這篇文章中,咱們圍繞 gin.Context 的內部結構、Gin 中間件和處理函數的工做流程,講解了 Gin 對請求的處理流程。

首先,在 gin.Engine 中,使用對象池 sync.Pool 來存放 gin.Context 這樣作的目的是爲 Go GC 減小壓力。

而後,在 Gin 內部,當路由匹配成功後,將調用 context.Next() 方法,開始進入 Gin 中間件和處理函數的執行操做,而且,須要注意的是,在平常開發中,該方法,只能在中間件中被調用。

最後,以使用 gin.Default() 方法建立 gin.Engine 時攜帶的兩個默認中間件 Logger()Recovery(),和咱們本身編寫的一個模擬身份校驗的中間件 Auth(),結合註冊的 path 爲 /action 的路由,對 Gin 中間件和處理函數的工做流程進行了講解。

至此,Gin 源碼學習的第四篇也就到此結束了,感謝你們對本文的閱讀~~

歡迎掃描如下二維碼關注筆者的我的訂閱號,獲取最新文章推送:

相關文章
相關標籤/搜索