树袋熊|实现,前缀树算法实现路由匹配原理解析:Go

路由功能是web框架中一个很重要的功能 , 它将不同的请求转发给不同的函数(handler)处理 , 很容易能想到 , 我们可以用一个字典保存它们之间的对应关系 , 字典的key存放path , value存放handler 。 当一个请求过来后 , 使用routers.get(path,None)就可以找到对应的handler 。
利用字典实现路由可以参考我的这篇文章:动手实现web框架[1] 。
使用字典有一个问题 , 不支持动态路由 。 如果路由像这样呢?
/hello/:name/profilename前面是通配符: , 表示这是个动态的值 。 一个解决办法是使用前缀树trie 。
前缀树leetcode中有这个算法 , 点这里[2]查看 。
前缀树前缀树 , 首先是一棵树 。 不同的是树中一个节点的所有子孙都有相同的前缀 。 前缀树将单词中的每个字母依次插入树中 , 插入前首先确认该单词是否存在 , 不存在才创建新节点 , 如果一个单词已经全部插入 , 则将末尾单词设置为标志位 。
typeNodestruct{isWordbool//是否是单词结尾nextmap[string]*Node//子节点}typeTriestruct{root*Node}以单词leetcode , leetd和code为例 , 首先一次插入leetcode中的每个单词 , 然后插入leetd的时候 , leet在树中已经存在 , 跳过往下 , 现在要插入字母d , 不存在 , 所以新建节点插入树中 , 并将该节点的isWord置位true , 表明到了单词末尾 。
最终插入结果为:
func(this*Trie)Insert(wordstring){cur:=this.rootfor_,w:=range[]rune(word){c:=string(w)ifcur.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(wordstring)bool{cur:=this.rootfor_,w:=range[]rune(word){c:=string(w)ifcur.next[c]==nil{returnfalse}cur=cur.next[c]}returncur.isWord}明白了前缀树的原理 , 我们来看看路由匹配是如何利用前缀树来实现的 。
路由前缀树go语言中gin框架的路由实现就是利用前缀树 , 可以看看它的源代码是如何实现的 。
考虑一下 , 路由或者说路径的特点 , 是以/分隔的单词组成的 , 那我们将/的每一部分挂靠在前缀树上就可以了 。 如下图所示:
还有一点需要考虑 , 我们在用web框架定义路由的时候 , 常见的做法是根据不同的HTTP方法来定义 。 比如:
//以go语言gin框架为例g:=gin.New()g.GET("/hello",Hello)g.POST("/form",Form)对于同一个路径 , 可能有多个方法支持 。 所以我们要以不同HTTP方法为树根创建前缀树 。 当一个GET请求过来的时候 , 就从GET树上搜索 , POST请求就从POST树上搜索 。
除了为不同的HTTP方法定义树之外 , 还要给那些是通配符的节点增加一个标志位 。 所以 , 我们的路由前缀树结构看起来像这样:
typenodestruct{pathstring//路由路径partstring//路由中由'/'分隔的部分childrenmap[string]*node//子节点isWildbool//是否是通配符节点}typerouterstruct{rootmap[string]*node//路由树根节点routemap[string]HandlerFunc//路由处理handler}依照上面的前缀树算法的实现 , 照葫芦画瓢 , 我们可以写出插入路由和搜索路由的方法:
//addRoute绑定路由到handlerfunc(r*router)addRoute(method,pathstring,handlerHandlerFunc){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:=rangeparts{ifroot.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//绑定路由和handlerr.route[key]=handler}//getRoute获取路由树节点以及路由变量func(r*router)getRoute(method,pathstring)(node*node,paramsmap[string]string){params=map[string]string{}searchParts:=parsePath(path)//getmethodtrievarokboolifnode,ok=r.root[method];!ok{returnnil,nil}//在该方法的路由树上查找该路径fori,part:=rangesearchParts{vartempstring//查找child是否等于partfor_,child:=rangenode.children{ifchild.part==part||child.isWild{//添加参数ifchild.part[0]=='*'{params[child.part[1:]]=strings.Join(searchParts[i:],"/")}ifchild.part[0]==':'{params[child.part[1:]]=part}temp=child.part}}//遇到通配符* , 直接返回iftemp[0]=='*'{returnnode.children[temp],params}node=node.children[temp]}return}上面的代码是我自己实现的一个web框架**gaga**[3]中路由前缀树相关的代码 , 有需要的可以去看看源代码 。 另外 , 欢迎star呀 。


推荐阅读