Gin
是使用Go語言編寫的高性能的web
服務框架,根據官方的測試,性能是httprouter
的40倍左右。要使用好這套框架呢,首先我們就得對這個框架的基本結構有所瞭解,所以我將從以下幾個方面來對Gin
的源碼進行解讀。
- 第一章:
Gin
是如何儲存和映射URL
路徑到相應的處理函數的 - 第二章:
Gin
中間件的設計思想及其實現 - 第三章:
Gin
是如何解析客戶端發送請求中的參數的 - 第四章:
Gin
是如何將各類格式(JSON/XML/YAML
等)數據解析
Gin是如何解析客戶端發送請求中的參數的
事實上,Gin
也是基於http
包封裝來實現的網絡通信,底層仍舊使用的是http.ListenAndServe
來創建的監聽端口和服務,只不過將接收到的數據解析爲Gin
的Context
上下文後,最終再傳遞到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()
}
通過上面這個過程可以瞭解到Gin
和http
通信框架建立聯繫是通過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
)的重定向命令,然後再重新路由。