Go语言动手写Web框架 - Gee第三天 前缀树路由Router
1. 目标
- 使用Trie树实现动态路由(dynamic route)解析
- 支持两种模式:name和*filepath
对于静态路由来说,我们只需要考虑使用map即可。但对于动态的路由来说,光使用map似乎已经无法满足我们的需求了。
1.1. 什么是前缀树(Trie)
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
如何将前缀树运用到路由匹配中?
1.2 前缀树在动态路由匹配中的运用
一般上来说,当我们的访问路径为:
- /:lang/doc
- /:lang/tutorial
- /:lang/intro
- /about
- /p/blog
- /p/related
前缀树如下:
HTTP的请求路径是由/进行分离的,所以我们可以用每一段作为一个节点。通过树结构查询的方式来得到是否存在匹配的路径,当然我们可能会面临一些特殊情况: - 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc。
- 通配*。例如 /static/*filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。
2. 代码设计解读
2.1 Trie树实现
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string // 路由中的一部分,例如 :lang
children []*node // 子节点,例如 [doc, tutorial, intro]
isWild bool // 是否精确匹配,part 含有 : 或 * 时为true
}
// part代表需要匹配的下一层的part,对比当前节点的children,查看是否有匹配的节点,并返回第一个匹配的
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
// part代表需要匹配的下一层的part,对比当前节点的children,返回所有匹配的节点
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
// 插入一个新的路由
func (n *node) insert(pattern string,parts []string,height int){
// 已经匹配完成,在这个节点插入完整的路由即可
if len(parts)==height{
n.pattern = pattern
return
}
// 获得这一层需要匹配的part
part := parts[height]
// 查找匹配的第一个子节点
child := n.matchChild(part)
// 若没有子节点,则新建一个节点后,插入到当前这个节点的列表中
if child == nil {
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children,child)
}
// 递归到下一层进行插入
child.insert(pattern,parts,height+1)
}
// 查询是否有匹配的路由
func (n *node) search(parts []string,height int) *node{
if len(parts) == height || strings.HasPrefix(n.part,"*"){
//如果达到层高或者遇到*通配符时,即查看节点的pattern是否为空,若为空则表示没有对应的路由。
if n.pattern == "" {
return nil
}
return n
}
// 获取需要匹配的part
part := parts[height]
// 查找所有匹配part的children节点
children := n.matchChildren(part)
// 通过递归的方式查询是否有答案
for _, child := range children{
result := child.search(parts, height+1)
if result != nil{
return result
}
}
}
2.2 router的实现
type router struct{
roots map[string]*node
handlers map[string]HandlerFunc
}
func newRouter() *router{
return &router{
roots: make(map[string]*node),
handlers: make(map[string]HandlerFunc)
}
}
// Only one * is allowed
func parsePattern(pattern string) []string{
vs := strings.Split(pattern,"/")
parts := make([]string,0)
for _,item := range vs{
if item != ""{
parts = append(parts, item)
if item[0] == '*' {
break
}
}
}
return parts
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)
key := method + "-" + pattern
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}
// 获取路由
func (r *router) getRouter(method string,path string)(*node,map[string]string){
searchParts := parsePattern(path)
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
return nil, nil
}
// node为匹配的节点
n := root.search(searchParts, 0)
if n!=nil{
//获取输入url匹配的URL的pattern
parts := parsePattern(n.pattern)
for index,part := range parts{
//如果是:,则加入到params中
if part[0] == ':'{
params[part[1:]]=searchParts[index]
}
//如果是*, 则将剩下的各part组合成一个string加入到params中
if part[0] == '*' && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}
return n,params
}
return nil,nil
}