Golang之Gin框架源碼解讀——第三章

Gin是使用Go語言編寫的高性能的web服務框架,根據官方的測試,性能是httprouter的40倍左右。要使用好這套框架呢,首先我們就得對這個框架的基本結構有所瞭解,所以我將從以下幾個方面來對Gin的源碼進行解讀。

  • 第一章:Gin是如何儲存和映射URL路徑到相應的處理函數的
  • 第二章:Gin中間件的設計思想及其實現
  • 第三章:Gin是如何解析客戶端發送請求中的參數的
  • 第四章:Gin是如何將各類格式(JSON/XML/YAML等)數據解析

Gin Github官方地址

Gin是如何解析客戶端發送請求中的參數的

事實上,Gin也是基於http包封裝來實現的網絡通信,底層仍舊使用的是http.ListenAndServe來創建的監聽端口和服務,只不過將接收到的數據解析爲GinContext上下文後,最終再傳遞到type HandlerFunc func(*Context)處理函數中去的。

再瞭解一個大致的數據處理過程之後,我們就從Gin的監聽入口開始逐漸摸索。

建立監聽服務

if err := router.Run();err != nil {
		log.Println("something error");
}

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
}

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

通過上面這個過程可以瞭解到Ginhttp通信框架建立聯繫是通過engine *Engine實現的,同時ListenAndServe要求傳入的是一個Handler類型的對象,而該對象定義如下:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

這咋一看,瞬間就明白了許多,ResponseWriter, *Request這兩個參數一目瞭然——請求與響應流http包就是底層處理過後將這兩個數據通過該接口傳遞到Gin框架內部的,所以我們找到該接口的實現。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    //從連接池中取出一個上下文對象
    c := engine.pool.Get().(*Context)
    //將上下文對象中的響應流設置爲傳入的參數
    c.writermem.reset(w)
    //將上下文對象中請求數據結構設置爲傳入參數
    c.Request = req
    //初始化上下文對象
	c.reset()
    //正式處理請求
	engine.handleHTTPRequest(c)
    //使用完畢後放回連接池
	engine.pool.Put(c)
}

服務處理

在正式開始瞭解這個處理過程之前,我們先來了解一下Context這個貫穿整個Gin框架的上下文對象,在C/S通信過程中所有的數據都保存在這個對象中了。

type Context struct {
    //響應輸出流(私有,供框架內部數據寫出)
    writermem responseWriter
    //客戶端發送的所有信息都保存在這個對象裏面
    Request   *http.Request
    //響應輸出流(公有,供給處理函數寫出)
    // 在初始化後,由writermem克隆而來的
	Writer    ResponseWriter

    //保存解析得到的參數,路徑中的REST參數
    Params   Params
    //該請求對應的處理函數鏈,從樹節點中獲取
    handlers HandlersChain
    //記錄已經被處理的函數個數
    index    int8
    //當前請求的完整路徑
	fullPath string
    //Gin的核心引擎
	engine *Engine
    //併發讀寫鎖
	KeysMutex *sync.RWMutex

	//用於保存當前會話的鍵值對,用於不同處理函數中傳遞
	Keys map[string]interface{}

	//處理函數鏈輸出的錯誤信息
	Errors errorMsgs

	//客戶端希望接受的數據類型,如:json、xml、html
	Accepted []string

    //存儲URL中的查詢參數,如:/test?name=jhon&age=11
    // 這樣的參數儲存在這個對象裏
	queryCache url.Values

	//這個用於存儲POST/PATCH等提交的body中的參數
	formCache url.Values

    //用來限制第三方 Cookie,一個int值,有Strict、Lax、None
    // Strict:只有當前網頁的 URL 與請求目標一致,纔會帶上 Cookie
    // Lax規則稍稍放寬,大多數情況也是不發送第三方 Cookie,
    // 但是導航到目標網址的 Get 請求除外
    // 設置了Strict或Lax以後,基本就杜絕了 CSRF 攻擊
	sameSite http.SameSite
}

在瞭解完Context後,我們來進入正式的數據解析過程:

func (engine *Engine) handleHTTPRequest(c *Context) {
    //獲取客戶端的http請求方法
    httpMethod := c.Request.Method
    //獲取請求的URL地址,這裏的URL是進過處理的
    rPath := c.Request.URL.Path
    //是否不啓動字符轉義
    unescape := false
    //判斷是否啓用原URL,未轉義字符
	if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
		rPath = c.Request.URL.RawPath
		unescape = engine.UnescapePathValues
	}

    //判斷是否需要移除多餘的分隔符"/"
	if engine.RemoveExtraSlash {
		rPath = cleanPath(rPath)
	}

	
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
        }
        //首先獲取到指定HTTP方法的搜索樹的根節點
		root := t[i].root
		//從根節點開始搜索匹配該路徑的節點
        value := root.getValue(rPath, c.Params, unescape)
        //將節點中的存儲的信息,拷貝到Context上下文中
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
            c.fullPath = value.fullPath
            //這裏就是在遍歷執行處理函數鏈
            // func (c *Context) Next() {
            //     c.index++
            //     for c.index < int8(len(c.handlers)) {
            //         c.handlers[c.index](c)
            //         c.index++
            //     }
            // }
            c.Next()
            //寫出響應狀態碼
			c.writermem.WriteHeaderNow()
			return
        }
        //如果沒有找到對應的匹配節點,則考慮是否是以下的特殊情況
		if httpMethod != "CONNECT" && rPath != "/" {
            //如果啓動自動重定向,刪除最後的"/"並重定向
			if value.tsr && engine.RedirectTrailingSlash {
				redirectTrailingSlash(c)
				return
            }
            //啓動路徑修復後,當/../foo找不到匹配路由時,
            // 會自動刪除..部分路由,然後重新匹配直到找到匹配路由,並重定向
			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
				return
			}
		}
		break
	}
    //是HTTP方法不匹配,而路徑匹配則返回405
	if engine.HandleMethodNotAllowed {
		for _, tree := range engine.trees {
			if tree.method == httpMethod {
				continue
			}
			if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
				c.handlers = engine.allNoMethod
				serveError(c, http.StatusMethodNotAllowed, default405Body)
				return
			}
		}
    }
    //如果都找不到路由則返回404
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

上述代碼就是整個請求的處理過程,而節點查找和參數解析都在getValue函數之中,我們來看一下他是如何匹配路徑和參數解析的:

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
    //先保存原有的REST參數列表
	value.params = po
walk: //這個標號使用中遞歸的,這裏使用的是循環式的遞歸
	for {
        // 當前節點的路徑
        prefix := n.path
        //如果該路徑與當前節點路徑剛好匹配
		if path == prefix {
            //如果處理函數是一樣的
            // 則說明已經搜索過了更新路徑後跳出。
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
            }
            
            //這種情況直接推薦重定向
			if path == "/" && n.wildChild && n.nType != root {
                //這個表示重定向後可以找到滿足條件的節點
				value.tsr = true
				return
			}

			//如果以上條件都未匹配,則根據索引去搜索子節點
			indices := n.indices
			for i, max := 0, len(indices); i < max; i++ {
				if indices[i] == '/' {
					n = n.children[i]
					value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
						(n.nType == catchAll && n.children[0].handlers != nil)
					return
				}
			}

			return
        }
        
        //這裏這種情況說明的是path的前綴剛好和該節點吻合
        //所以進入子節點搜索
		if len(path) > len(prefix) && path[:len(prefix)] == prefix {
            path = path[len(prefix):]
            //如果該節點沒有通配符子節點,則根據索引查找子節點
			if !n.wildChild {
				c := path[0]
				indices := n.indices
				for i, max := 0, len(indices); i < max; i++ {
					if c == indices[i] {
						n = n.children[i]
						continue walk
					}
				}

				//如果沒找到匹配的子節點,則建議重定向搜索
				value.tsr = path == "/" && n.handlers != nil
				return
			}

            //下面是子節點是統配符節點的情況
            // 需要根據傳入的URL對路徑中的參數進行解析
            // 因爲如果n.wildChild爲true的話,那麼n就只能有一個子節點
			n = n.children[0]
			switch n.nType {
            //子節點爲參數節點
			case param:
				//尋找參數的字符長度
				end := 0
				for end < len(path) && path[end] != '/' {
					end++
				}

				//根據maxParams來預分配更大的參數列表(僅僅是容量)
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
                i := len(value.params)
                //拓展參數列表長度
                value.params = value.params[:i+1]
                //獲取參數名從1開始是因爲一般都是*:開頭的
                value.params[i].Key = n.path[1:]
                // 獲取參數值
                val := path[:end]
                //如果需要轉義則調用轉義函數
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
						value.params[i].Value = val // fallback, in case of error
					}
				} else {
					value.params[i].Value = val
				}

				//如果path還沒解析完
				if end < len(path) {
                    // 進入其子節點
					if len(n.children) > 0 {
						path = path[end:]
						n = n.children[0]
						continue walk
					}

					// 若僅僅是多了個"/",則推薦重定向
					value.tsr = len(path) == end+1
					return
				}

				if value.handlers = n.handlers; value.handlers != nil {
					value.fullPath = n.fullPath
					return
				}
				if len(n.children) == 1 {
					//如果子節點有匹配"/"的,則推薦重定向
					n = n.children[0]
					value.tsr = n.path == "/" && n.handlers != nil
				}
				return
            //這個類型表明所有的參數都已經匹配完了
			case catchAll:
                //下面的過程和上面差不多
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // expand slice within preallocated capacity
				value.params[i].Key = n.path[2:]
				if unescape {
					var err error
					if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
						value.params[i].Value = path // fallback, in case of error
					}
				} else {
					value.params[i].Value = path
				}
                //獲取節點中保存的處理函數鏈
                value.handlers = n.handlers
                //獲取該節點下的完整路徑
				value.fullPath = n.fullPath
				return

			default:
				panic("invalid node type")
			}
		}

        // 說明該節點是個,則只有推薦重定向了
		value.tsr = (path == "/") ||
			(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
				path == prefix[:len(prefix)-1] && n.handlers != nil)
		return
	}
}

其實從上面都能看出,這個過程就是從搜索樹的根節點依次向下搜索,每次搜索完畢後,都會更新當前路徑path,例如:Path:/test/add、當前節點路徑爲/test,那麼進入子節點後Path就會變爲/add,按這種模式一直匹配,直到path爲空或者爲/,如果是/通常都是將value.tsr設置爲true然後返回,這樣就會使得服務器返回一個對路徑優化過(/test/優化爲/test)的重定向命令,然後再重新路由。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章