Go语言动手写Web框架——前缀树路由——参考极客兔兔

Go语言动手写Web框架——前缀树路由


参考链接: 极客兔兔主页.

什么是前缀树

在进行web框架设计时,我们可以选用一个表用于记载路由地址与对应的Handler,但是这存在着一个问题,当遇到动态路由时(例如,在URL的请求信息中可提取出固定的信息,根据不同的信息取值返回不同的结果),使用哈希表进行路由存储就不尽如人意,因此,为实现动态路由,可以选取在动态路由所在位置的part部分加入":“或者”*"来代表这条路由可以根据用户输入的值提取结果,对于part将在后续进行讲解,下图为前缀树的示意图。

Alt

前缀树的结点

任何树形的数据结构都由结点构成,前缀树也不例外,在一棵树中需要有根节点,也需要有子节点,对于非叶子节点的子节点也可成为其余节点的父节点,对于前缀树的节点,所必须拥有的操作为插入与查找,下边先看前缀树节点的结构体设计。

type node struct {
	pattern  string  //待匹配路由 例如/p/:lang
	part     string  //路由的一部分 例如 :lang
	children []*node //子节点,例如 /doc /tutorial /intro
	isWild   bool    //是否使用精准匹配
}

从上述代码段可看出前缀树节点共有四个字段,依次为pattern,part,children以及isWild,pattern为匹配的模式,只有叶子节点才进行存储,part是pattern的一个组成成分,例如一个路径(path,/go/func/doc),其模式(pattern)为/:language/func/go,/:language代表着language可以被替换为其他语言,是动态的,根据用户传入的不同语言进行匹配,例如,go也可以被替换为java、python等。对于part,其为pattern拆分的结果,在我们注册路由时对于传入的pattern进行拆分,其代码如下所示:

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
}

其输入值为pattern,由注册路由时传入,返回为一个字符串型的切片,首先通过Split函数对于pattern按照"/"进行区分,获得一个切片vs,之后对于parts切片进行初始化,循环将part传入parts,当part的首字母为*时,代表一通配符,当前路径可为任意,当part首字母为:时,代表精准匹配,不进行区分。接下来讲述如何对pattern进行插入。

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.handlers[key] = handler
	r.roots[method].insert(pattern, parts, 0)
}

在进行路径插入的函数中,需要传递三个参数,依次为method,pattern和handler,method代表请求方式,有GET、POST、PUT、DELETE等,对于不同的请求方式,均生成以请求方式为根节点的树,并以键值对的方式进行存储,首先对router的字段进行介绍。

type router struct {
	roots    map[string]*node
	handlers map[string]HandlerFunc
}

rooter的字段分为两个,roots用于存储不同请求方式对应的前缀树,handlers用于存储不同pattern对应的HandlerFunction。在addRoute函数中通过pattern获取不同的part,然后根据roots这个map获取method(请求方式)对应的前缀树根节点,如果没有获取到此method对应的前缀树根节点,则新建键值对存储于map之中,将key与对应的handlerfunc存储于handlers中,后根据method对应的前缀树父节点对pattern进行插入,node节点对应的插入方式如下述代码所示:

func (n *node) insert(pattern string, parts []string, height int) {
	if len(parts) == height {
		n.pattern = pattern
		return
	}
	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)
}

insert函数需要三个元素,pattern,string切片parts,以及高度height,height代表前缀树的深度,同时前缀树的插入也是一个递归的过程,在height为0时,插入parts中的第一个part,举个例子(/go/func/doc),做了parsePattern后得到的part为[go,func,doc],首先对go进行插入,对go进行插入后插入func,再插入doc,在插入doc时,每插入一个part就对height进行加一,由于其为parts的最后一个部分,则height会达到parts的size大小,对最后一个part节点的pattern赋值后返回,在代码中出现了matchChild函数,用于寻找一个node节点中与part所匹配的节点,如果没有找到则新建一个child(node)节点,将其插入其父节点的children中去,matchChild代码如下。

func (n *node) matchChild(part string) *node {
	for _, child := range n.children {
		if child.part == part || child.isWild {
			return child
		}
	}
	return nil
}

对于上述代码可能存在一个疑问,child.isWild是否会和child.part==part重合,答案是不可能的,因为在同一级":“可以代替所有的part,如果在有”:"的同一级part再添加任意其他路由是没有必要的(本人认为此解释有问题,不符合上述流程图),在节点的插入讲解完毕后,开始讲述节点的查找与对应HandlerFunc方法的提取。首先看在router层面都调用了哪些接口,router层进行调用的方式如下。

func (r *router) getRoute(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
	}
	n := root.search(searchParts, 0)
	if n != nil {
		parts := parsePattern(n.pattern)
		for index, part := range parts {
			if part[0] == ':' {
				params[part[1:]] = strings.Join(searchParts[index], "/")
				break
			}
		}
		return n, params
	}
	return nil, nil
}

getRoute传入的参数为method和path,其中method指代请求方式,这里path需要和pattern做一个区分,path是用户传入的路径,而pattern是由程序员自定义的处理路由,对于传入的path,首先将其分解为一个个part,后根据method去访问method对应的前缀树,获取根节点。如果获取不到根节点,则表示后端不支持此method方法。接下来看search方法。

func (n *node) search(parts []string, height int) *node {
	if len(parts) == height || strings.HasPrefix(n.part, "*") {
		if n.pattern == "" {
			return nil
		}
		return n
	}

	part := parts[height]
	children := n.matchChildren(part)
	for _, child := range children {
		result := child.search(parts, height+1)
		if result != nil {
			return result
		}
	}

	return nil
}

search方法需传入parts和height,返回值为node指针,假如请求路径为go/func/doc,searchParts三个值分别为go,func,doc,search方法仍为一递归调用方法,假设注册的路由为:language/func/doc,:language/func/apply,传入parts后,height初始为0,part取到go,此时的根为GET方法下的root,其children只有一个,即:language,n.matchChildren返回结果只有:language,之后对children进行遍历,此时height为1,获取func,找:language节点下的子节点,找到了func,开始对doc进行查找,n.matchchildren的范围结果仍只有一个,即doc,匹配成功后逐层返回,返回结果中的pattern符合path。最后生成一个键值对,键值均为string,键对应":"后的参数名称,例如language,值为language处匹配的值,此处为go,在键值对添加完成后,返回该path对应的pattern的节点以及params,用于提取url中的参数。接下来看context(上下文)的变化。

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode int
}

func (c *Context) Param(key string) string {
	value, _ := c.Params[key]
	return value
}

在Context中新增加了一个字段,名为Params,其用于存储URL中的关键信息,下面的函数Param通过传入key值即可获取对应的value,例如前述的go/func/doc,有一个键,即language,通过传入language可以获取对应的value,go。

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
		c.Params = params
		key := c.Method + "-" + n.pattern
		r.handlers[key](c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

上述代码为router处理请求的全部过程,首先通过r.getRoute获取对应的pattern节点与params,将params写入context中的params中去,获得key,通过router中的字段handlers获取到对应的处理函数handler并对context加以处理,以上即为前缀树方法下的router的全部过程,有关上层的封装后续再进行讲解。
本文仅用于个人学习,可能存在大量笔误口误以及不合理的描述方式,如有问题欢迎批评指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值