gin框架路由理論


gin框架路由理論

gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前綴的樹結構,它基本上是一個緊湊的Trie tree(或者只是Radix Tree)。具有公共前綴的節點也共享一個公共父節點。

一、Radix Tree

基數樹(Radix Tree)又稱為PAT位樹(Patricia Trie or crit bit tree),是一種更節省空間的前綴樹(Trie Tree)。對於基數樹的每個節點,如果該節點是唯一的子樹的話,就和父節點合並。下圖為一個基數樹示例:

img

Radix Tree可以被認為是一棵簡潔版的前綴樹。我們注冊路由的過程就是構造前綴樹的過程,具有公共前綴的節點也共享一個公共父節點。假設我們現在注冊有以下路由信息:

r := gin.Default()

r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)

那么我們會得到一個GET方法對應的路由樹,具體結構如下:

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

上面最右邊那一列每個*<數字>表示Handle處理函數的內存地址(一個指針)。從根節點遍歷到葉子節點我們就能得到完整的路由表。

例如:blog/:post其中:post只是實際文章名稱的占位符(參數)。與hash-maps不同,這種樹結構還允許我們使用像:post參數這種動態部分,因為我們實際上是根據路由模式進行匹配,而不僅僅是比較哈希值。

由於URL路徑具有層次結構,並且只使用有限的一組字符(字節值),所以很可能有許多常見的前綴。這使我們可以很容易地將路由簡化為更小的問題。此外,路由器為每種請求方法管理一棵單獨的樹。一方面,它比在每個節點中都保存一個method-> handle map更加節省空間,它還使我們甚至可以在開始在前綴樹中查找之前大大減少路由問題。

為了獲得更好的可伸縮性,每個樹級別上的子節點都按Priority(優先級)排序,其中優先級(最左列)就是在子節點(子節點、子子節點等等)中注冊的句柄的數量。這樣做有兩個好處:

  1. 首先優先匹配被大多數路由路徑包含的節點。這樣可以讓盡可能多的路由快速被定位。
  2. 類似於成本補償。最長的路徑可以被優先匹配,補償體現在最長的路徑需要花費更長的時間來定位,如果最長路徑的節點能被優先匹配(即每次拿子節點都命中),那么路由匹配所花的時間不一定比短路徑的路由長。下面展示了節點(每個-可以看做一個節點)匹配的路徑:從左到右,從上到下。
   ├------------
   ├---------
   ├-----
   ├----
   ├--
   ├--
   └-

二、Engine

type Engine struct {
	RouterGroup

	// Enables automatic redirection if the current route can't be matched but a
	// handler for the path with (without) the trailing slash exists.
	// For example if /foo/ is requested but a route only exists for /foo, the
	// client is redirected to /foo with http status code 301 for GET requests
	// and 307 for all other request methods.
	RedirectTrailingSlash bool

	// If enabled, the router tries to fix the current request path, if no
	// handle is registered for it.
	// First superfluous path elements like ../ or // are removed.
	// Afterwards the router does a case-insensitive lookup of the cleaned path.
	// If a handle can be found for this route, the router makes a redirection
	// to the corrected path with status code 301 for GET requests and 307 for
	// all other request methods.
	// For example /FOO and /..//Foo could be redirected to /foo.
	// RedirectTrailingSlash is independent of this option.
	RedirectFixedPath bool

	// If enabled, the router checks if another method is allowed for the
	// current route, if the current request can not be routed.
	// If this is the case, the request is answered with 'Method Not Allowed'
	// and HTTP status code 405.
	// If no other Method is allowed, the request is delegated to the NotFound
	// handler.
	HandleMethodNotAllowed bool

	// If enabled, client IP will be parsed from the request's headers that
	// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
	// fetched, it falls back to the IP obtained from
	// `(*gin.Context).Request.RemoteAddr`.
	ForwardedByClientIP bool

	// List of headers used to obtain the client IP when
	// `(*gin.Engine).ForwardedByClientIP` is `true` and
	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
	// network origins of `(*gin.Engine).TrustedProxies`.
	RemoteIPHeaders []string

	// List of network origins (IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
	// IPv6 CIDRs) from which to trust request's headers that contain
	// alternative client IP when `(*gin.Engine).ForwardedByClientIP` is
	// `true`.
	TrustedProxies []string

	// #726 #755 If enabled, it will trust some headers starting with
	// 'X-AppEngine...' for better integration with that PaaS.
	AppEngine bool

	// If enabled, the url.RawPath will be used to find parameters.
	UseRawPath bool

	// If true, the path value will be unescaped.
	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
	// as url.Path gonna be used, which is already unescaped.
	UnescapePathValues bool

	// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
	// See the PR #1817 and issue #1644
	RemoveExtraSlash bool

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	trustedCIDRs     []*net.IPNet
}

image-20211217083447948

image-20211217083627913

image-20211217083740409

三、路由樹節點

路由樹是由一個個節點構成的,gin框架路由樹的節點由node結構體表示,它有以下字段:

// tree.go

type node struct {
   // 節點路徑,比如上面的s,earch,和upport
	path      string
	// 和children字段對應, 保存的是分裂的分支的第一個字符
	// 例如search和support, 那么s節點的indices對應的"eu"
	// 代表有兩個分支, 分支的首字母分別是e和u
	indices   string
	// 兒子節點
	children  []*node
	// 處理函數鏈條(切片)
	handlers  HandlersChain
	// 優先級,子節點、子子節點等注冊的handler數量
	priority  uint32
	// 節點類型,包括static, root, param, catchAll
	// static: 靜態節點(默認),比如上面的s,earch等節點
	// root: 樹的根節點
	// catchAll: 有*匹配的節點
	// param: 參數節點
	nType     nodeType
	// 路徑上最大參數個數
	maxParams uint8
	// 節點是否是參數節點,比如上面的:post
	wildChild bool
	// 完整路徑
	fullPath  string
}

四、請求方法樹

在gin的路由中,每一個HTTP Method(GET、POST、PUT、DELETE…)都對應了一棵 radix tree,我們注冊路由的時候會調用下面的addRoute函數:

// routergroup.go
// 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)
}


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()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	// 兩個切片拼接
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	// 將開始的函數復制到mergedHandlers里面
	copy(mergedHandlers, group.Handlers)
	// 處理的函數
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers



// gin.go
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   	assert1(path[0] == '/', "path must begin with '/'")
	assert1(method != "", "HTTP method can not be empty")
	assert1(len(handlers) > 0, "there must be at least one handler")

	debugPrintRoute(method, path, handlers)
   // 獲取請求方法對應的樹
	root := engine.trees.get(method)
	if root == nil {
	
	   // 如果沒有就創建一個
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)
}

從上面的代碼中我們可以看到在注冊路由的時候都是先根據請求方法獲取對應的樹,也就是gin框架會為每一個請求方法創建一棵對應的樹。只不過需要注意到一個細節是gin框架中保存請求方法對應樹關系並不是使用的map而是使用的切片,engine.trees的類型是methodTrees,其定義如下:

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree  // slice

而獲取請求方法對應樹的get方法定義如下:

func (trees methodTrees) get(method string) *node {
	for _, tree := range trees {
		if tree.method == method {
			return tree.root
		}
	}
	return nil
}

為什么使用切片而不是map來存儲請求方法->樹的結構呢?我猜是出於節省內存的考慮吧,畢竟HTTP請求方法的數量是固定的,而且常用的就那幾種,所以即使使用切片存儲查詢起來效率也足夠了。順着這個思路,我們可以看一下gin框架中engine的初始化方法中,確實對tress字段做了一次內存申請:

func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		// ...
		// 初始化容量為9的切片(HTTP1.1請求方法共9種)
		trees:                  make(methodTrees, 0, 9),
		// ...
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

五、注冊路由

注冊路由的邏輯主要有addRoute函數和insertChild方法。

addRoute

// tree.go

// addRoute 將具有給定句柄的節點添加到路徑中。
// 不是並發安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
    // 構造路由樹
	fullPath := path
	n.priority++
	numParams := countParams(path)  // 數一下參數個數

	// Empty tree 空樹直接插入當前節點
	if len(n.path) == 0 && len(n.children) == 0 {
		// 構造基數樹
		n.insertChild(path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		// 更新當前節點的最大參數個數
		if numParams > n.maxParams {
			n.maxParams = numParams
		}

		// 找到最長的通用前綴
		// 這也意味着公共前綴不包含“:”"或“*” /
		// 因為現有鍵不能包含這些字符。
		i := longestCommonPrefix(path, n.path)

		// 分裂邊緣(此處分裂的是當前樹節點)
		// 例如一開始path是search,新加入support,s是他們通用的最長前綴部分
		// 那么會將s拿出來作為parent節點,增加earch和upport作為child節點
		if i < len(n.path) {
			child := node{
				path:      n.path[i:],  // 公共前綴后的部分作為子節點
				wildChild: n.wildChild,
				indices:   n.indices,
				children:  n.children,
				handlers:  n.handlers,
				priority:  n.priority - 1, //子節點優先級-1
				fullPath:  n.fullPath,
			}

			// Update maxParams (max of all children)
			for _, v := range child.children {
				if v.maxParams > child.maxParams {
					child.maxParams = v.maxParams
				}
			}

			n.children = []*node{&child}
			// []byte for proper unicode char conversion, see #65
			n.indices = string([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// 將新來的節點插入新的parent節點作為子節點
		if i < len(path) {
			path = path[i:]

			if n.wildChild {  // 如果是參數節點
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++

				// Update maxParams of the child node
				if numParams > n.maxParams {
					n.maxParams = numParams
				}
				numParams--

				// 檢查通配符是否匹配
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
					// 檢查更長的通配符, 例如 :name and :names
					if len(n.path) >= len(path) || path[len(n.path)] == '/' {
						continue walk
					}
				}

				pathSeg := path
				if n.nType != catchAll {
					pathSeg = strings.SplitN(path, "/", 2)[0]
				}
				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
				panic("'" + pathSeg +
					"' in new path '" + fullPath +
					"' conflicts with existing wildcard '" + n.path +
					"' in existing prefix '" + prefix +
					"'")
			}
			// 取path首字母,用來與indices做比較
			c := path[0]

			// 處理參數后加斜線情況
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 檢查路path下一個字節的子節點是否存在
			// 比如s的子節點現在是earch和upport,indices為eu
			// 如果新加一個路由為super,那么就是和upport有匹配的部分u,將繼續分列現在的upport節點
			for i, max := 0, len(n.indices); i < max; i++ {
				if c == n.indices[i] {
					parentFullPathIndex += len(n.path)
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}

			// 否則就插入
			if c != ':' && c != '*' {
				// []byte for proper unicode char conversion, see #65
				// 注意這里是直接拼接第一個字符到n.indices
				n.indices += string([]byte{c})
				child := &node{
					maxParams: numParams,
					fullPath:  fullPath,
				}
				// 追加子節點
				n.children = append(n.children, child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			}
			n.insertChild(numParams, path, fullPath, handlers)
			return
		}

		// 已經注冊過的節點
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		return
	}
}

其實上面的代碼很好理解,大家可以參照動畫嘗試將以下情形代入上面的代碼邏輯,體味整個路由樹構造的詳細過程:

  1. 第一次注冊路由,例如注冊search
  2. 繼續注冊一條沒有公共前綴的路由,例如blog
  3. 注冊一條與先前注冊的路由有公共前綴的路由,例如support

addroute

insertChild

// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
  // 找到所有的參數
	for numParams > 0 {
		// 查找前綴直到第一個通配符
		wildcard, i, valid := findWildcard(path)
		if i < 0 { // 沒有發現通配符
			break
		}

		// 通配符的名稱必須包含':' 和 '*'
		if !valid {
			panic("only one wildcard per path segment is allowed, has: '" +
				wildcard + "' in path '" + fullPath + "'")
		}

		// 檢查通配符是否有名稱
		if len(wildcard) < 2 {
			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
		}

		// 檢查這個節點是否有已經存在的子節點
		// 如果我們在這里插入通配符,這些子節點將無法訪問
		if len(n.children) > 0 {
			panic("wildcard segment '" + wildcard +
				"' conflicts with existing children in path '" + fullPath + "'")
		}

		if wildcard[0] == ':' { // param
			if i > 0 {
				// 在當前通配符之前插入前綴
				n.path = path[:i]
				path = path[i:]
			}

			n.wildChild = true
			child := &node{
				nType:     param,
				path:      wildcard,
				maxParams: numParams,
				fullPath:  fullPath,
			}
			n.children = []*node{child}
			n = child
			n.priority++
			numParams--

			// 如果路徑沒有以通配符結束
			// 那么將有另一個以'/'開始的非通配符子路徑。
			if len(wildcard) < len(path) {
				path = path[len(wildcard):]

				child := &node{
					maxParams: numParams,
					priority:  1,
					fullPath:  fullPath,
				}
				n.children = []*node{child}
				n = child  // 繼續下一輪循環
				continue
			}

			// 否則我們就完成了。將處理函數插入新葉子中
			n.handlers = handlers
			return
		}

		// catchAll
		if i+len(wildcard) != len(path) || numParams > 1 {
			panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
		}

		if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
			panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
		}

		// currently fixed width 1 for '/'
		i--
		if path[i] != '/' {
			panic("no / before catch-all in path '" + fullPath + "'")
		}

		n.path = path[:i]
		
		// 第一個節點:路徑為空的catchAll節點
		child := &node{
			wildChild: true,
			nType:     catchAll,
			maxParams: 1,
			fullPath:  fullPath,
		}
		// 更新父節點的maxParams
		if n.maxParams < 1 {
			n.maxParams = 1
		}
		n.children = []*node{child}
		n.indices = string('/')
		n = child
		n.priority++

		// 第二個節點:保存變量的節點
		child = &node{
			path:      path[i:],
			nType:     catchAll,
			maxParams: 1,
			handlers:  handlers,
			priority:  1,
			fullPath:  fullPath,
		}
		n.children = []*node{child}

		return
	}

	// 如果沒有找到通配符,只需插入路徑和句柄
	n.path = path
	n.handlers = handlers
	n.fullPath = fullPath
}

insertChild函數是根據path本身進行分割,將/分開的部分分別作為節點保存,形成一棵樹結構。參數匹配中的:*的區別是,前者是匹配一個字段而后者是匹配后面所有的路徑。

六、路由匹配

我們先來看gin框架處理請求的入口函數ServeHTTP

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  // 這里使用了對象池,從對象池中獲取一個對象,減少gc,內存申請的頻率,
	c := engine.pool.Get().(*Context) // 類型斷言,強制轉換Context指針
  // 這里有一個細節就是Get對象后做初始化
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我們要找的處理HTTP請求的函數

	engine.pool.Put(c)  // 處理完請求后將對象放回池子
}

函數很長,這里省略了部分代碼,只保留相關邏輯代碼:

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
	// ...

	// 根據請求方法找到對應的路由樹
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 在路由樹中根據path查找
		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
		}
	
	// ...
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

路由匹配是由節點的 getValue方法實現的。getValue根據給定的路徑(鍵)返回nodeValue值,保存注冊的處理函數和匹配到的路徑參數數據。

如果找不到任何處理函數,則會嘗試TSR(尾隨斜杠重定向)。

// tree.go

type nodeValue struct {
	handlers HandlersChain
	params   Params  // []Param
	tsr      bool
	fullPath string
}

// ..

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	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
		}

		if len(path) > len(prefix) && path[:len(prefix)] == prefix {
			path = path[len(prefix):]
			// 如果該節點沒有通配符(param或catchAll)子節點
			// 我們可以繼續查找下一個子節點
			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
					}
				}

				// 沒找到
				// 如果存在一個相同的URL但沒有末尾/的葉子節點
				// 我們可以建議重定向到那里
				value.tsr = path == "/" && n.handlers != nil
				return
			}

			// 根據節點類型處理通配符子節點
			n = n.children[0]
			switch n.nType {
			case param:
				// find param end (either '/' or path end)
				end := 0
				for end < len(path) && path[end] != '/' {
					end++
				}

				// 保存通配符的值
				if cap(value.params) < int(n.maxParams) {
					value.params = make(Params, 0, n.maxParams)
				}
				i := len(value.params)
				value.params = value.params[:i+1] // 在預先分配的容量內擴展slice
				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
				}

				// 繼續向下查詢
				if end < len(path) {
					if len(n.children) > 0 {
						path = path[end:]
						n = n.children[0]
						continue walk
					}

					// ... but we can't
					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 {
					// 沒有找到處理函數. 檢查此路徑末尾加/的路由是否存在注冊函數
					// 用於 TSR 推薦
					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] // 在預先分配的容量內擴展slice
				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
	}
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM