在上一篇文章中,咱們聊了聊在Golang中怎麼實現一個Http服務器。可是在最後咱們能夠發現,當然DefaultServeMux
能夠作路由分發的功能,可是他的功能一樣是不完善的。html
由DefaultServeMux
作路由分發,是不能實現RESTful
風格的API的,咱們沒有辦法定義請求所需的方法,也沒有辦法在API
路徑中加入query
參數。其次,咱們也但願可讓路由查找的效率更高。node
因此在這篇文章中,咱們將分析httprouter
這個包,從源碼的層面研究他是如何實現咱們上面提到的那些功能。而且,對於這個包中最重要的前綴樹,本文將以圖文結合的方式來解釋。git
咱們一樣以怎麼使用做爲開始,自頂向下的去研究httprouter
。咱們先來看看官方文檔中的小例子:github
package main import ( "fmt" "net/http" "log" "github.com/julienschmidt/httprouter" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprint(w, "Welcome!\n") } func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) } func main() { router := httprouter.New() router.GET("/", Index) router.GET("/hello/:name", Hello) log.Fatal(http.ListenAndServe(":8080", router)) }
其實咱們能夠發現,這裏的作法和使用Golang自帶的net/http
包的作法是差很少的。都是先註冊相應的URI和函數,換一句話來講就是將路由和處理器相匹配。算法
在註冊的時候,使用router.XXX
方法,來註冊相對應的方法,好比GET
,POST
等等。服務器
註冊完以後,使用http.ListenAndServe
開始監聽。微信
至於爲何,咱們會在後面的章節詳細介紹,如今只須要先了解作法便可。app
咱們先來看看第一行代碼,咱們定義並聲明瞭一個Router
。下面來看看這個Router
的結構,這裏把與本文無關的其餘屬性省略:函數
type Router struct { //這是前綴樹,記錄了相應的路由 trees map[string]*node //記錄了參數的最大數目 maxParams uint16 }
在建立了這個Router
的結構後,咱們就使用router.XXX
方法來註冊路由了。繼續看看路由是怎麼註冊的:ui
func (r *Router) GET(path string, handle Handle) { r.Handle(http.MethodGet, path, handle) } func (r *Router) POST(path string, handle Handle) { r.Handle(http.MethodPost, path, handle) } ...
在這裏還有一長串的方法,他們都是同樣的,調用了
r.Handle(http.MethodPost, path, handle)
這個方法。咱們再來看看:
func (r *Router) Handle(method, path string, handle Handle) { ... if r.trees == nil { r.trees = make(map[string]*node) } root := r.trees[method] if root == nil { root = new(node) r.trees[method] = root r.globalAllowed = r.allowed("*", "") } root.addRoute(path, handle) ... }
在這個方法裏,一樣省略了不少細節。咱們只關注一下與本文有關的。咱們能夠看到,在這個方法中,若是tree
尚未初始化,則先初始化這顆前綴樹。
而後咱們注意到,這顆樹是一個map
結構。也就是說,一個方法,對應了一顆樹。而後,對應這棵樹,調用addRoute
方法,把URI
和對應的Handle
保存進去。
又稱單詞查找樹,Trie樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不只限於字符串),因此常常被搜索引擎系統用於文本詞頻統計。它的優勢是:利用字符串的公共前綴來減小查詢時間,最大限度地減小無謂的字符串比較,查詢效率比哈希樹高。
簡單的來說,就是要查找什麼,只要跟着這棵樹的某一條路徑找,就能夠找獲得。
好比在搜索引擎中,你輸入了一個蔡:
他會有這些聯想,也能夠理解爲是一個前綴樹。
再舉個例子:
在這顆GET
方法的前綴樹中,包含了如下的路由:
說到這裏你應該能夠理解了,在構建這棵樹的過程當中,任何兩個節點,只要有了相同的前綴,相同的部分就會被合併成一個節點。
上面說的addRoute
方法,就是這顆前綴樹的插入方法。假設如今數爲空,在這裏我打算以圖解的方式來講明這棵樹的構建。
假設咱們須要插入的三個路由分別爲:
(1)插入/hello/world
由於此時樹爲空,因此能夠直接插入:
(2)插入/hello/china
此時,發現/hello/world
和/hello/china
有相同的前綴/hello/
。
那麼要先將原來的/hello/world
結點,拆分出來,而後將要插入的結點/hello/china
,截去相同部分,做爲/hello/world
的子節點。
(3)插入/hello/chinese
此時,咱們須要插入/hello/chinese
,可是發現,/hello/chinese
和結點/hello/
有公共的前綴/hello/
,因此咱們去查看/hello/
這個結點的子節點。
注意,在結點中有一個屬性,叫indices
。它記錄了這個結點的子節點的首字母,便於咱們查找。好比這個/hello/
結點,他的indices
值爲wc
。而咱們要插入的結點是/hello/chinese
,除去公共前綴後,chinese
的第一個字母也是c
,因此咱們進入china
這個結點。
這時,有沒有發現,狀況回到了咱們一開始插入/hello/china
時候的局面。那個時候公共前綴是/hello/
,如今的公共前綴是chin
。
因此,咱們一樣把chin
截出來,做爲一個結點,將a
做爲這個結點的子節點。而且,一樣把ese
也做爲子節點。
到這裏,構建就已經結束了。咱們來總結一下算法。
具體帶註釋的代碼將在本文最末尾給出,若是想要了解的更深能夠自行查看。在這裏先理解這個過程:
(1)若是樹爲空,則直接插入
(2)不然,查找當前的結點是否與要插入的URI
有公共前綴
(3)若是沒有公共前綴,則直接插入
(4)若是有公共前綴,則判斷是否須要分裂當前的結點
(5)若是須要分裂,則將公共部分做爲父節點,其他的做爲子節點
(6)若是不須要分裂,則尋找有無前綴相同的子節點
(7)若是有前綴相同的,則跳到(4)
(8)若是沒有前綴相同的,直接插入
(9)在最後的結點,放入這條路由對應的Handle
可是到了這裏,有同窗要問了:怎麼這裏的路由,不帶參數的呀?
其實只要你理解了上面的過程,帶參數也是同樣的。邏輯是這樣的:在每次插入以前,會掃描當前要插入的結點的path是否帶有參數(即掃描有沒有/
或者*
)。若是帶有參數的話,將當前結點的wildChild
屬性設置爲true
,而後將參數部分,設置爲一個新的子節點。
在講完了路由的註冊,咱們來聊聊路由的監聽。
在上一篇文章的內容中,咱們有提到這個:
type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) }
當時咱們提到,若是咱們不傳入任何的Handle
方法,Golang將使用默認的DefaultServeMux
方法來處理請求。而如今咱們傳入了router
,因此將會使用router
來處理請求。
所以,router
也是實現了ServeHTTP
方法的。咱們來看看(一樣省略了一些步驟):
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { ... path := req.URL.Path if root := r.trees[req.Method]; root != nil { if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { if ps != nil { handle(w, req, *ps) r.putParams(ps) } else { handle(w, req, nil) } return } } ... // Handle 404 if r.NotFound != nil { r.NotFound.ServeHTTP(w, req) } else { http.NotFound(w, req) } }
在這裏,咱們選擇請求方法所對應的前綴樹,調用了getValue
方法。
簡單解釋一下這個方法:在這個方法中會不斷的去匹配當前路徑與結點中的path
,直到找到最後找到這個路由對應的Handle
方法。
注意,在這期間,若是路由是RESTful風格的,在路由中含有參數,將會被保存在Param
中,這裏的Param
結構以下:
type Param struct { Key string Value string }
若是未找到相對應的路由,則調用後面的404方法。
到了這一步,其實和之前的內容幾乎同樣了。
在獲取了該路由對應的Handle
以後,調用這個函數。
惟一和以前使用net/http
包中的Handler
不同的是,這裏的Handle
,封裝了從API中獲取的參數。
type Handle func(http.ResponseWriter, *http.Request, Params)
謝謝你能看到這裏~
至此,httprouter介紹完畢,最關鍵的也就是前綴樹的構建了。在上面我用圖文結合的方式,模擬了一次前綴樹的構建過程,但願可讓你理解前綴樹是怎麼回事。固然,若是還有疑問,也能夠留言或者在微信中與我交流~
固然,若是你不知足於此,能夠看看後面的附錄,有前綴樹的全代碼註釋。
固然了,做者也是剛入門。因此,可能會有不少的疏漏。若是在閱讀的過程當中,有哪些解釋不到位,或者理解出現了誤差,也請你留言指正。
再次感謝~
PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~
type node struct { path string //當前結點的URI indices string //子結點的首字母 wildChild bool //子節點是否爲參數結點 nType nodeType //結點類型 priority uint32 //權重 children []*node //子節點 handle Handle //處理器 }
func (n *node) addRoute(path string, handle Handle) { fullPath := path n.priority++ // 若是這是個空樹,那麼直接插入 if len(n.path) == 0 && len(n.indices) == 0 { //這個方法實際上是在n這個結點插入path,可是會處理參數 //詳細實如今後文會給出 n.insertChild(path, fullPath, handle) n.nType = root return } //設置一個flag walk: for { // 找到當前結點path和要插入的path中最長的前綴 // i爲第一位不相同的下標 i := longestCommonPrefix(path, n.path) // 此時相同的部分比這個結點記錄的path短 // 也就是說須要把當前的結點分裂開 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} // 把這個結點的首字母加入indices中 // 目的是查找更快 n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handle = nil n.wildChild = false } // 此時相同的部分只佔了新URI的一部分 // 因此把path後面不相同的部分要設置成一個新的結點 if i < len(path) { path = path[i:] // 此時若是n的子節點是帶參數的 if n.wildChild { n = n.children[0] n.priority++ // 判斷是否會不合法 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 != catchAll { 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 + "'") } } // 把截取的path的第一位記錄下來 idxc := path[0] // 若是此時n的子節點是帶參數的 if n.nType == param && idxc == '/' && len(n.children) == 1 { n = n.children[0] n.priority++ continue walk } // 這一步是檢查拆分出的path,是否應該被合併入子節點中 // 具體例子可看上文中的圖解 // 若是是這樣的話,把這個子節點設置爲n,而後開始一輪新的循環 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中 n.indices += string([]byte{idxc}) child := &node{} n.children = append(n.children, child) n.incrementChildPrio(len(n.indices) - 1) // 新建一個結點 n = child } // 對這個結點進行插入操做 n.insertChild(path, fullPath, handle) return } // 直接插入到當前的結點 if n.handle != nil { panic("a handle is already registered for path '" + fullPath + "'") } n.handle = handle return } }
func (n *node) insertChild(path, fullPath string, handle Handle) { for { // 這個方法是用來找這個path是否含有參數的 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] == ':' { if i > 0 { // 把當前的path設置爲參數以前的那部分 n.path = path[:i] // 準備把參數後面的部分做爲一個新的結點 path = path[i:] } //而後把參數部分做爲新的結點 n.wildChild = true child := &node{ nType: param, path: wildcard, } n.children = []*node{child} n = child n.priority++ // 這裏的意思是,path在參數後面尚未結束 if len(wildcard) < len(path) { // 把參數後面那部分再分出一個結點,continue繼續處理 path = path[len(wildcard):] child := &node{ priority: 1, } n.children = []*node{child} n = child continue } // 把處理器設置進去 n.handle = handle return } else { // 另一種狀況 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 + "'") } // 判斷在這以前有沒有一個/ i-- if path[i] != '/' { panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[:i] // 設置一個catchAll類型的子節點 child := &node{ wildChild: true, nType: catchAll, } n.children = []*node{child} n.indices = string('/') n = child n.priority++ // 把後面的參數部分設置爲新節點 child = &node{ path: path[i:], nType: catchAll, handle: handle, priority: 1, } n.children = []*node{child} return } } // 對應最開頭的部分,若是這個path裏面沒有參數,直接設置 n.path = path n.handle = handle }
最關鍵的幾個方法到這裏就所有結束啦,先給看到這裏的你鼓個掌!
這一部分理解會比較難,可能須要多看幾遍。
若是仍是有難以理解的地方,歡迎留言交流,或者直接來公衆號找我~