httprouter解讀(continuing)

httprouter解讀

核心思想

  • 與defaultServeMux的實現區別在於什麼?採起特殊的數據結構做路由。
  • defaultServeMux的實現採用什麼樣的數據結構?
  • httprouter的實現採用什麼樣的數據結構?
  • router的架構圖是怎樣的,與server.go有何異同
  • 一些http相關的細節
  • 其餘細節

細讀源碼

  • Router的 paramsPool?sync.Pool須要看看 done
  • 修復重定向是代碼大頭,得細細解讀重定向相關的協議內容 這部分先跳過吧,細節太多了
  • http包的server.go裏面req的context裏面包含了params怎麼取消的。done

「server.serve => conn.serve ==>defer cancleCtx/ w.cancelCtx() 」node

所以, 最要緊的就是看tree.go裏面定義的數據結構。曹大的圖能夠幫助理解。python

tree

  • addRoute方法爲切入口
  • 初次閱讀有幾個不明白的地方,還須要回顧router.go
  • 具體的路由規則要爛熟,由此解讀node的幾個類型
  • priority/wildchild/indices的意思和做用
  • test文件的測試有沒有沒覆蓋到的狀況?

node 的結構

type node struct {
    path      string
    indices   string 
    wildChild bool
    nType     nodeType
    priority  uint32
    children  []*node
    handle    Handle
}

疑難字段名

-pathgolang

一截兒path,絕非全路徑,這點能夠肯定。具體怎麼表述?
  • indices
// incrementChildPrio
if newPos != pos {
  n.indices = n.indices[:newPos] +
   n.indices[pos:pos+1] +
   n.indices[newPos:pos] + n.indices[pos+1:] 
 }
可見每一個node爲chidren指定惟一的一字節字符,和排好序的chidren slice一一對應。問題是怎麼指定的呢?
  • wildchild
是什麼?幹嗎的?
  • priority
幹嗎的?

「加」的兩個重頭方法 addRoute 和 insertChild

解讀

  1. 事實上,在router.go中,只用到了addRoute爲咱們的router添加路由以及對應的handle, insertChild爲addRoute服務。
  2. 進一步的事實是,調用addRoute的老是根節點root.
  • 咱們根據以上事實,參照代碼,在腦海中模擬運行。找到點感受再更細緻的分析(其實還有一個不錯的方法是參照測試文件看看,找找感受)。
// router.go Handle
if root == nil {
    root = new(node)
    r.trees[method] = root
    // 其餘邏輯
    ]}
// tree.go addRoute
fullpath := path
n.priority ++

//Empty tree
if len(n.path) == 0 && len(n.indices) == 0 {
    n.insertChild(path, fullpath, handle)
    n.nType == root // 常量: root
    return
}
  • insertChild 的邏輯是怎麼樣?
  • 沒有「:」「*」都好說,直接:
//insertChild
n.path = path
n.handle = handle
  • 有呢?有點複雜。將狀況簡單化,若是以前是空樹的話(如今要有第一個節點了),咱們想一想,有「:」「*」有什麼說頭?
  • 問題一: 遇到無效的wildcard怎麼處理?
  • 問題二:可能要設置wildcard衝突,怎麼設置呢?
  • 問題一已經被槍斃了
if !valid {
    panic("only one wildcard per path segment is allowed, has: '" + wildcard + "' in path '" + fullPath + "'")
}
  • 另外還贈送了兩種槍斃狀況,「光桿司令」 「鳩佔鵲巢(初次插入時不會出現,可是很好理解,即衝突,後會說起此事)」
  • 好了,開始好循環了嗎?由於findWildCard只會找到離根部最近的那個。
  • param,這個簡單點,先從這兒開始吧。先貼源碼。
if wildcard[0] == ':' {   // param
    if i > 0 {
        // Insert prefix before the current wildcard
        n.path = path[:i]
        path = path[i:]
    }

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

    // If the path doesn't end with the wildcard, then there will be another non-wildcard subpath starting with '/'
    if len(wildcard) < len(path) {
        path = path[len(wildcard):]
        child := &node{
            priority: 1,
        }
        n.children = []*node{child}
        n = child
        continue
    }

    // Otherwise we're done. Insert the handle in the new leaf
    n.handle = handle
    return
}
  • 其實很簡單。當前節點n不是沒children嘛(有chidren的以前已經被槍斃), 我param給你當吧。別高興的太早,你得把我爹媽照顧好,因而有了:
if i > 0 {
        n.path = path[:i]
        path = path[i:]
    }
  • 須要注意的是這個path不是亂傳的。path和fullpath和樹和當前節點是有一致性的,在addRoute裏會體現。咱們能夠猜測一下,n.path 和參數path的關係, n.path應該是參數path脫去通配內容以後的前綴,不然以上的語句就會平白無故抹掉n.path的某種信息。設想path以":"開頭的狀況,也能夠想通。這一點待驗證。
  • 咱們當前關注的空樹插節點不須要考慮這麼深刻,若「:」前有任何內容,直接給n便可,咱們集中精力處理以":"開頭的一段。path在這個過程當中不斷的「脫」。不管如何,如今的path以":"開頭了。
n.wildchild = true
  • 出現了,wildchild意思是,n的孩子是wildCard,並非「野孩子」。固然wildchild也必須是獨子,不然,會引發衝突,果真有點野。
child := &node{
        nType: param,
        path: wildcard,
    }
    n.children = []*node{child}
    n = child
    n.priority++
  • 注意,上面的wildcard是不包含"/"的、以「:」開頭的一段內容。如願以償的,這個野孩子當了n的孩子,不只如此,還篡權了,成了新一代的n,接手如下的朝政工做。剛建立死後priority爲0,如今爲1。若是wildcard後面沒有內容了,即沒有「/」了,完事兒了,到頭了:
n.handle = handle
    return
  • 後面還有內容的話,必須還沒完事兒,作好重走循環的準備
if len(wildcard) < len(path) {
        path = path[len(wildcard):]
        child := &node{
            priority: 1,
        }
        n.children = []*node{child}
        n = child
        continue
    }
  • path又開始「脫」了,path就像python(非彼python)同樣一直蛻皮。這下path必定是以"/"開頭的,這是從fildWildCard函數的實現中得出的結論。如今的n的類型是以前的param,形如「:xxx」的不含"/", 它建立了一個空孩子,而且這個空孩子當政成了n,重走循環。看咱們以前推測的,n.path必須是參數path的前綴, 空串固然是path的前綴。
  • 接下來是catchall狀況,感受應該和param雷同,可是又有點不同。根據router.go開頭的註釋和測試用例,猜測一下:catchall必定是葉子節點,不以"/"結尾。
if i+len(wildcard) < len(path) {
    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 + "'")
            }
  • 也就是若是n.path非空,必須以"/"結尾。什麼意思?什麼node不以"/"結尾?好比param類型的, 好比catchall類型的,還好比想通配一個,「/file/prefix_*filepath」 通通不合法了。
  • 可是path自己會不會是「/somesubpath/*cathall」類型的呢?這樣接在param類型的node後面爲何是非法的?猜測:param類型的node不會直接調用insertChild方法!(等待驗證)
  • 這時候又來了下面一句:
i--
    if path[i] != '/' {
        panic("no / before catch-all in path '" + fullPath + "'")}
  • 意思和上一條雷同。只不過非法狀況來自
  • i--? 若是i == 0 呢?因而可知,path不會以「*」開頭(等待驗證)
小結一下: 調用插入方法的node的path會是參數path的前綴,是空串最保險了。參數path不會以「*」開頭, 以「:」開頭是可能的。node不會是param類型(catch也不會)。
n.path = path[:i]

// First node: catchAll node with empty path
child := &node{
    wildChild: true,
    nType:     catchAll,
    }
    n.children = []*node{child}
    n.indices = string('/')
    n = child
    n.priority++

// Second node: node holding the variable
child = &node{
    path:     path[i:],
    nType:    catchAll,
    handle:   handle,
    priority: 1,
    }
    n.children = []*node{child}

    return
  • path脫了後(還記得咱們的前綴論嗎),進行了一波騷操做,一連插入兩個catchall類型的節點。不明白爲何,彷佛是做了一個保護,最後一個節點纔是真正的通配符。而且這裏首次出現了indices.更懵圈了。
  • 畢竟insertChild只是一個輔助方法,使用的狀況不少不少限制。

回到addRoute吧。數據結構

// Find the longest common prefix. 
//This also implies that the common prefix contains no ':' or '*' 
//since the existing key can't contain those chars.
i := longestCommonPrefix(path, n.path)
再回顧一遍insertChild方法: 設想向空樹插入:「/:name」 , ":name", "/*catchall", 根結點不會帶「:」「*」,要麼空串, 要麼"/"。注意插入「*catchall」非法(index out of range!!!)。能夠理解他的註釋。
if i < len(n.path) {
    child := node{
    path:      n.path[i:],
    wildChild: n.wildChild,
    nType:     static,
    indices:   n.indices,
    children:  n.children,
    handle:    n.handle,
    priority:  n.priority - 1,
    }

    n.children = []*node{&child}
    // []byte for proper unicode char conversion, see #65
    n.indices = string([]byte{n.path[i]})
    n.path = path[:i]
    n.handle = nil
    n.wildChild = false
}
  • 好的。i是path和n.path的最長公共前綴。最長公共前綴是要被剝離放在祖輩的。由於樹生長的方向是向下,而越向下,生成的路徑就會越長。這裏至關於舊路徑和新路徑分叉了,天然要把前綴放在祖輩位置。
  • 以上代碼還有幾個值得注意的細節。爲何接管n.path後半段的child的priority要-1?暫時還不清楚。

-n.indices的含義。從上能夠瞥見一點端倪。記得咱們說過,n.indices 和 n.children一一對應?這裏n.indices就是這個child的開頭字母,正如其名,起到的是索引的做用。架構

  • n.wildchild是false,由於咱們說了,最長前綴不含「:」"*". 有個特殊狀況:path和n.path同樣都是,:param 別的特殊狀況也有,所以這個註釋意思不明
  • 還須要注意,n做爲原n.path的子串,是沒有設置handle的,你能夠以後爲這條路徑設置。就直接走到該方法的最後:
if n.handle != nil {
    panic("a handle is already registered for path '" + fullPath + "'")
    }
    n.handle = handle
  • 咱們繼續往下走。先總覽一下全貌。
if i < len(path) {
    path = path[i:]

    // 節點n的子節點是通配,殺傷力比較大,先掌權,後清洗
    if n.wildchild {
        // 1. 掌權
        // 2.清洗,生存者能夠進入下一輪。
        // 思考:
        // 1. 什麼樣的能逃過清洗?
        // 2. 進入下一輪的初始狀態是怎樣的: 虛擬根結點
    }

    // 如今面臨的狀況:n的孩子節點沒有param或catchall
    //可是n自己多是
    idxc := path[0]

    // 1. 一種簡單的狀況的處理 n是param,只有一個孩子
    // 那必然是「/」開頭的或空串,直接重走循環

    // 2. n的某個孩子和path有公共前綴,提高priority並重走循環

    // 3.其餘狀況插入新的子節點,使樹增加
    // 如今的情形是:  n.path 和 path 絕對並無公共前綴了
    // 3.1 等待拼接的全新path不以":" "*" 打頭,須要作一點工做 
    // 插入空節點,做用不明, 保護做用?

    // 結合insertChild的狀況,有個狀況能夠排除: 
    // 若是 idxc是「:」 "*" 打頭的,n必然沒孩子
    // 若是n是param, path是普通字母打頭的呢?按理來講,這是個不合法現象。這在上面的判斷(wildcard衝突判斷中)已經槍斃。
    // 建立孩子(空串),插入此節點。
    n.insertChild(path, fullpPath, handle)
    }

}
  • 須要注意的是,若是節點是param類型,path必然是「:」和參數名,毫不含有「/」,也側面說明兩個param類型的節點不多是父子。
  • 若是是staitc類型,又沒有「/」都是合法的,而且能夠在任意位置(中間合法嗎?),從上往下拼就能夠了。
  • continue walk 的狀況:
  • 公共前綴不爲空或n.path爲空串
  • 若n爲param(模擬根結點沒法作到的), path必定是「:param」 或 「:param/」 或 「/」打頭三種狀況之一。
  • param子節點如有必定是獨生子!!
  • param沒孩子的話,要加一個空串「」

源碼參考:app

if i < len(path) {
    path = path[i:]

    if n.wildchild {
        n = n.children[0]
        n.prority ++

    // Check if the wildcard mathes
    if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
    n.nType != catchAll &&
    (len(n.path) >= len(path) || path[len(n.path)] == "/") {
        continue walk
    } else {
        pathSeg := path
        if n.nType != cathAll {
            pathSeg = strings.SplitN(pathSeg, "/", 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 +
        "'")
        }
    }

    idxc := path[0]

    if n.nType == param && idxc == "/" && len(n.children) == 1 {
        n = n.children[0]
        n.prority++
        continue walk
    }

    for i, c := range []byte(n.indices) {
        if c == idxc {
            i = n.incrementChildPrio(i)
            n = n.children[i]
            continue walk
        }
    }

    if idxc != ':' && idxc != '*' {
        n.indices += string([]byte{idxc})
        child := &node{}
        n.children = append(n.children, child)
        n.incrementChildPrio(len(n.indices) - 1)
        n = child
    }
    n.insertChild(path, fullpPath, handle)
}

「拿」的重要方法:getValue

做用猜測:根據樹,匹配手頭的path,把全部參數取出來,放在從sync.Pool裏拿出臨時對象[]Params, 不斷把解析的param加入。 catchall怎麼處理?拭目以待!函數

相關文章
相關標籤/搜索