分享一波gin的路由算法

[TOC]node

gin的路由算法分享

gin是什麼呢?

咱們在github上看看官方簡介git

Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.

Gin 是用 Go 開發的一個微框架,Web框架,相似 Martinier 的 API,接口簡潔,性能極高,也由於 httprouter的性能提升了 40 倍。github

若是你須要良好的表現和工做效率,你會喜歡Ginweb

img

gin有啥特性呢?

tag 說明
異常處理 服務始終可用,不會宕機
Gin 能夠捕獲 panic,並恢復。並且有極爲便利的機制處理HTTP請求過程當中發生的錯誤。
路由分組 能夠將須要受權和不須要受權的API分組,不一樣版本的API分組。
並且分組可嵌套,且性能不受影響。
例如v1/xxx/xxx v2/xxx/xxx
渲染內置 原生支持JSON,XML和HTML的渲染。
JSON Gin能夠解析並驗證請求的JSON。
這個特性對Restful API的開發尤爲有用。
中間件 HTTP請求,可先通過一系列中間件處理
就向日志Logger,Authorization等。
中間件機制也極大地提升了框架的可擴展性。

gin大體都包含了哪些知識點?

gin的實戰演練咱們以前也有分享過,咱們再來回顧一下,gin大體都包含了哪些知識點算法

  • :路由*路由
  • query查詢參數
  • 接收數組和 Map
  • Form 表單
  • 單文件和多文件上傳
  • 分組路由,以及路由嵌套
  • 路由中間件
  • 各類數據格式的支持,json、struct、xml、yaml、protobuf
  • HTML模板渲染
  • url重定向
  • 異步協程等等

要是朋友們對gin還有點興趣的話,能夠點進來看看,這裏有具體的知識點對應的案例gin實戰演練json

路由是什麼?

咱們再來了解一下路由是什麼數組

路由器是一種鏈接多個網絡或網段的網絡設備,它能將不一樣網絡或網段之間的數據信息進行「翻譯」,以使它們可以相互「讀」懂對方的數據,從而構成一個更大的網絡。

路由器有兩大典型功能網絡

  • 即數據通道功能

包括轉發決定、背板轉發以及輸出鏈路調度等,通常由特定的硬件來完成數據結構

  • 控制功能

通常用軟件來實現,包括與相鄰路由器之間的信息交換、系統配置、系統管理等app

gin裏面的路由

路由是web框架的核心功能

寫過路由的朋友最開始是否是這樣看待路由的:

  • 根據路由裏的 / 把路由切分紅多個字符串數組
  • 而後按照相同的前子數組把路由構形成樹的結構

當須要尋址的時候,先把請求的 url 按照 / 切分,而後遍歷樹進行尋址,這樣子有點像是深度優先算法的遞歸遍歷,從根節點開始,不停的向根的地方進行延伸,知道不能再深刻爲止,算是獲得了一條路徑

舉個栗子

定義了兩個路由 /v1/hi/v1/hello

那麼這就會構造出擁有三個節點的路由樹,根節點是 v1,兩個子節點分別是 hi hello

上述是一種實現路由樹的方式,這種是比較直觀,容易理解的。對 url 進行切分、比較,但是時間複雜度是 O(2n),那麼咱們有沒有更好的辦法優化時間複雜度呢?大名鼎鼎的GIn框架有辦法,日後看

算法是什麼?

再來提一提算法是啥。

算法是解決某個問題的計算方法、步驟,不只僅是有了計算機纔有算法這個名詞/概念的,

例如咱們小學學習的九九乘法表

中學學習的各類解決問題的計算方法,例如物理公式等等

如今各類吃播大秀廚藝,作法的流程和方法也是算法的一種

  • 面臨的問題是bug , 解決的方法不盡相同,步驟也截然不同
  • 面臨豬蹄,烹飪方法各有特點
  • 面臨咱們生活中的難題,也許每一個人都會碰到一樣的問題,但是每一個人解決問題的方式方法差別也很是大,有的人處理事情很是漂亮,有的人拖拖拉拉,總留尾巴

大學裏面學過算法這本書,算法是計算機的靈魂,面臨問題,好的算法可以輕易應對且健壯性好

面臨人生難題,好的解決方式,也一樣可以讓咱們走的更遠,更確切有點來講,應該是好的思惟模型。

算法有以下五大特徵

每一個事物都會有本身的特色,不然如何才能讓人記憶深入呢

  • 有限性 , 算法得有明確限步以後會結束
  • 確切性,每個步驟都是明確的,涉及的參數也是確切的
  • 輸入,算法有零個或者多個輸入
  • 輸出,算法有零個或者多個輸出
  • 可行性,算法的每個步驟都是能夠分解出來執行的,且均可以在有限時間內完成

gin的路由算法

那咱們開始進入進入正題,gin的路由算法,千呼萬喚始出來

gin的是路由算法相似於一棵前綴樹

只需遍歷一遍字符串便可,時間複雜度爲O(n)。比上面提到的方式,在時間複雜度上來講真是大大滴優化呀

不過,僅僅是對於一次 http 請求來講,是看不出啥效果的

誒,敲黑板了,什麼叫作前綴樹呢?

Trie樹,又叫 字典樹前綴樹(Prefix Tree),是一種多叉樹結構

畫個圖,大概就能明白前綴樹是個啥玩意了

這棵樹還和二叉樹不太同樣,它的鍵不是直接保存在節點中,而是由節點在樹中的位置決定

一個節點的全部子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串

例如上圖,咱們一個一個的來尋址一下,會有這樣的字符串

  • MAC
  • TAG
  • TAB
  • HEX

前綴樹有以下幾個特色:

  • 前綴樹除根節點不包含字符,其餘節點都包含字符
  • 每一個節點的子節點包含的字符串不相同
  • 從根節點到某一個節點,路徑上通過的字符鏈接起來,爲該節點對應的字符串
  • 每一個節點的子節點一般有一個標誌位,用來標識單詞的結束

有沒有以爲這個和路由的樹一毛同樣?

gin的路由樹算法相似於一棵前綴樹. 不過並非只有一顆樹, 而是每種方法(POST, GET ,PATCH...)都有本身的一顆樹

例如,路由的地址是

  • /hi
  • /hello
  • /:name/:id

那麼gin對應的樹會是這個樣子的

GO中 路由對應的節點數據結構是這個樣子的

type node struct {
    path      string
    indices   string
    children  []*node
    handlers  HandlersChain
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
}

具體添加路由的方法,實現方法是這樣的

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)
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers)
}

仔細看,gin的實現不像一個真正的樹

由於他的children []*node全部的孩子都會放在這個數組裏面,具體實現是,他會利用indices, priority變相的去實現一棵樹

咱們來看看不一樣註冊路由的方式有啥不一樣?每一種註冊方式,最終都會反應到gin的路由樹上面

普通註冊路由

普通註冊路由的方式是 router.xxx,能夠是以下方式

  • GET
  • POST
  • PATCH
  • PUT
  • ...
router.POST("/hi", func(context *gin.Context) {
    context.String(http.StatusOK, "hi xiaomotong")
})

也能夠以組Group的方式註冊,以分組的方式註冊路由,便於版本的維護

v1 := router.Group("v1")
{
    v1.POST("hello", func(context *gin.Context) {
        context.String(http.StatusOK, "v1 hello world")
    })
}

在調用POST, GET, PATCH等路由HTTP相關函數時, 會調用handle函數

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath) // calculateAbsolutePath
    handlers = group.combineHandlers(handlers) //  combineHandlers
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

calculateAbsolutePathcombineHandlers 還會再次出現

調用組的話,看看是咋實現的

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}

一樣也會調用 calculateAbsolutePathcombineHandlers 這倆函數,咱們來看看 這倆函數是幹啥的,看到函數名字,也許大概也能猜出個因此然了吧,來看看源碼

img

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)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
    return joinPaths(group.basePath, relativePath)
}

func joinPaths(absolutePath, relativePath string) string {
    if relativePath == "" {
        return absolutePath
    }

    finalPath := path.Join(absolutePath, relativePath)
    appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
    if appendSlash {
        return finalPath + "/"
    }
    return finalPath
}

joinPaths函數在這裏至關重要,主要是作拼接的做用

從上面來看,能夠看出以下2點:

  • 調用中間件, 是將某個路由的handler處理函數和中間件的處理函數都放在了Handlers的數組中
  • 調用Group, 是將路由的path上面拼上Group的值. 也就是/hi/:id, 會變成v1/hi/:id

使用中間件的方式註冊路由

咱們也可使用中間件的方式來註冊路由,例如在訪問咱們的路由以前,咱們須要加一個認證的中間件放在這裏,必需要認證經過了以後,才能夠訪問路由

router.Use(Login())
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

不論是普通的註冊,仍是經過中間件的方式註冊,裏面都有一個關鍵的handler

handler方法 調用 calculateAbsolutePathcombineHandlers 將路由拼接好以後,調用addRoute方法,將路由預處理的結果註冊到gin Engine的trees上,來在看讀讀handler的實現

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

那麼,服務端寫好路由以後,咱們經過具體的路由去作http請求的時候,服務端是如何經過路由找到具體的處理函數的呢?

咱們仔細追蹤源碼, 咱們能夠看到以下的實現

...
// 一棵前綴樹
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
    // 這裏經過 path 來找到相應的  handlers 處理函數
    handlers, params, tsr := root.getValue(path, c.Params, unescape) 
    if handlers != nil {
        c.handlers = handlers
        c.Params = params
        // 在此處調用具體的 處理函數
        c.Next()
        c.writermem.WriteHeaderNow()
        return
    }
    if httpMethod != "CONNECT" && path != "/" {
        if tsr && engine.RedirectTrailingSlash {
            redirectTrailingSlash(c)
            return
        }
        if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
            return
        }
    }
    break
}
...
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

當客戶端請求服務端的接口時, 服務端此處 handlers, params, tsr := root.getValue(path, c.Params, unescape) , 經過 path 來找到相應的 handlers 處理函數,

handlersparams 複製給到服務中,經過 c.Next()來執行具體的處理函數,此時就能夠達到,客戶端請求響應的路由地址,服務端能過對響應路由作出對應的處理操做了

總結

  • 簡單回顧了一下gin的特性
  • 介紹了gin裏面的路由
  • 分享了gin的路由算法,以及具體的源碼實現流程

img

好了,本次就到這裏,下一次 分享最經常使用的限流算法以及如何在http中間件中加入流控

技術是開放的,咱們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

相關文章
相關標籤/搜索