【前缀树路由】

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
}

链接

七天用Go从零实现系列

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值