18

web框架中路由实现原理解析

 4 years ago
source link: https://studygolang.com/articles/26324
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

路由功能是web框架中一个很重要的功能,它将不同的请求转发给不同的函数(handler)处理,很容易能想到,我们可以用一个字典保存它们之间的对应关系,字典的key存放path,value存放handler。当一个请求过来后,使用 routers.get(path, None) 就可以找到对应的handler。

利用字典实现路由可以参考我的这篇文章: 动手实现web框架

使用字典有一个问题,不支持动态路由。如果路由像这样呢?

/hello/:name/profile

name前面是通配符 : ,表示这是个动态的值。一个解决办法是使用前缀树trie。

前缀树

leetcode中有这个算法, 点这里 查看。

前缀树前缀树,首先是一棵树。不同的是树中一个节点的所有子孙都有相同的前缀。前缀树将单词中的每个字母依次插入树中,插入前首先确认该单词是否存在,不存在才创建新节点,如果一个单词已经全部插入,则将末尾单词设置为标志位。

type Node struct {
    isWord bool // 是否是单词结尾
    next   map[string]*Node // 子节点
}

type Trie struct {
    root *Node
}

以单词leetcode,leetd和code为例,首先一次插入leetcode中的每个单词,然后插入leetd的时候,leet在树中已经存在,跳过往下,现在要插入字母d,不存在,所以新建节点插入树中,并将该节点的isWord置位true,表明到了单词末尾。

最终插入结果为:

qY7Ffe7.jpg!web

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是单词标志位,则表示搜索成功。

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框架的路由实现就是利用前缀树,可以看看它的源代码是如何实现的。

考虑一下,路由或者说路径的特点,是以 / 分隔的单词组成的,那我们将 / 的每一部分挂靠在前缀树上就可以了。如下图所示:

还有一点需要考虑,我们在用web框架定义路由的时候,常见的做法是根据不同的HTTP方法来定义。比如:

// 以go语言gin框架为例
g := gin.New()
g.GET("/hello", Hello)
g.POST("/form", Form)

对于同一个路径,可能有多个方法支持。所以我们要以不同HTTP方法为树根创建前缀树。当一个GET请求过来的时候,就从GET树上搜索,POST请求就从POST树上搜索。

qU7JVjm.jpg!web

除了为不同的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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK