路由功能是web框架中一個很重要的功能,它將不一樣的請求轉發給不一樣的函數(handler)處理,很容易能想到,咱們能夠用一個字典保存它們之間的對應關係,字典的key存放path,value存放handler。當一個請求過來後,使用 routers.get(path, None) 就能夠找到對應的handler。node
利用字典實現路由能夠參考個人這篇文章:動手實現web框架 。git
使用字典有一個問題,不支持動態路由。若是路由像這樣呢?github
/hello/:name/profile
name前面是通配符: ,表示這是個動態的值。一個解決辦法是使用前綴樹trie。web
leetcode中有這個算法, 點這裏 查看。
前綴樹前綴樹,首先是一棵樹。不一樣的是樹中一個節點的全部子孫都有相同的前綴。前綴樹將單詞中的每一個字母依次插入樹中,插入前首先確認該單詞是否存在,不存在才建立新節點,若是一個單詞已經所有插入,則將末尾單詞設置爲標誌位。算法
type Node struct { isWord bool // 是不是單詞結尾 next map[string]*Node // 子節點 } type Trie struct { root *Node }
以單詞leetcode,leetd和code爲例,首先一次插入leetcode中的每一個單詞,而後插入leetd的時候,leet在樹中已經存在,跳過往下,如今要插入字母d,不存在,因此新建節點插入樹中,並將該節點的isWord置位true,代表到了單詞末尾。框架
最終插入結果爲:函數
func (this *Trie) Insert(word string) { cur := this.root for _, w := range []rune(word) { c := string(w) if cur.next[c] == nil { cur.next[c] = &Node{next: make(map[string]*Node)} } cur = cur.next[c] } cur.isWord = true }
那麼,當咱們要搜索單詞leetd的時候,從根節點開始查找,若是找到某條路徑是leetd,而且末尾的d是單詞標誌位,則表示搜索成功。this
func (this *Trie) Search(word string) bool { cur := this.root for _, w := range []rune(word) { c := string(w) if cur.next[c] == nil { return false } cur = cur.next[c] } return cur.isWord }
明白了前綴樹的原理,咱們來看看路由匹配是如何利用前綴樹來實現的。搜索引擎
go語言中gin框架的路由實現就是利用前綴樹,能夠看看它的源代碼是如何實現的。spa
考慮一下,路由或者說路徑的特色,是以 / 分隔的單詞組成的,那咱們將 / 的每一部分掛靠在前綴樹上就能夠了。以下圖所示:
還有一點須要考慮,咱們在用web框架定義路由的時候,常見的作法是根據不一樣的HTTP方法來定義。好比:
// 以go語言gin框架爲例 g := gin.New() g.GET("/hello", Hello) g.POST("/form", Form)
對於同一個路徑,可能有多個方法支持。因此咱們要以不一樣HTTP方法爲樹根建立前綴樹。當一個GET請求過來的時候,就從GET樹上搜索,POST請求就從POST樹上搜索。
除了爲不一樣的HTTP方法定義樹以外,還要給那些是通配符的節點增長一個標誌位。因此,咱們的路由前綴樹結構看起來像這樣:
type node struct { path string // 路由路徑 part string // 路由中由'/'分隔的部分 children map[string]*node // 子節點 isWild bool // 是不是通配符節點 } type router struct { root map[string]*node // 路由樹根節點 route map[string]HandlerFunc // 路由處理handler }
依照上面的前綴樹算法的實現,照葫蘆畫瓢,咱們能夠寫出插入路由和搜索路由的方法:
// addRoute 綁定路由到handler func (r *router) addRoute(method, path string, handler HandlerFunc) { parts := parsePath(path) if _, ok := r.root[method]; !ok { r.root[method] = &node{children: make(map[string]*node)} } root := r.root[method] key := method + "-" + path // 將parts插入到路由樹 for _, part := range parts { if root.children[part] == nil { root.children[part] = &node{ part: part, children: make(map[string]*node), isWild: part[0] == ':' || part[0] == '*'} } root = root.children[part] } root.path = path // 綁定路由和handler r.route[key] = handler } // getRoute 獲取路由樹節點以及路由變量 func (r *router) getRoute(method, path string) (node *node, params map[string]string) { params = map[string]string{} searchParts := parsePath(path) // get method trie var ok bool if node, ok = r.root[method]; !ok { return nil, nil } // 在該方法的路由樹上查找該路徑 for i, part := range searchParts { var temp string // 查找child是否等於part for _, child := range node.children { if child.part == part || child.isWild { // 添加參數 if child.part[0] == '*' { params[child.part[1:]] = strings.Join(searchParts[i:], "/") } if child.part[0] == ':' { params[child.part[1:]] = part } temp = child.part } } // 遇到通配符*,直接返回 if temp[0] == '*' { return node.children[temp], params } node = node.children[temp] } return }
上面的代碼是我本身實現的一個web框架 gaga 中路由前綴樹相關的代碼,有須要的能夠去看看源代碼。另外,歡迎star 呀。
其中的 addRoute 用來將路由插入到對應method的路由樹中,若是節點是通配符,將其設置爲 isWild , 同時綁定路由和handler方法。
getRoute 方法首先查找路由方法對應的路由前綴樹,而後在樹中查找是否存在該路徑。
前綴樹trie算法不光能夠用在路由的實現上,搜索引擎中自動補全的實現,拼寫檢查等等都是用trie實現的。trie樹查找的時間和空間複雜度都是線性的,效率很高,很適合路由這種場景使用。
路由的實現上,go語言中 httpRouter 這個庫除了使用前綴樹以外,還加入了優先級,有興趣的能夠看看它的源碼瞭解下。
個人blog:https://blog.shiniao.fun