~$ 存档

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

路由树单纯从代码层面来说,应该是后端最难模块之一,还有一个是ORM,写的都很复杂。
夜里没事写了一篇,内容比较长,这篇内容非常关键,参考的是beego的路由模块。

----------
补充几句:最能体现计算机编程艺术的地方就是算法。看完之余不禁赞叹,实在太精彩!只有在算法这个地方,计算机真正的和艺术搭上了关系!
-----------

确保先看这篇文档,https://beego.vip/docs/mvc/controller/router.md

(除了beego之外,还有gin等都用了类似的路由原理)

能运用之后,再看源码如果觉得不能理解,就可以参考本篇(未写完,后面会持续完善

路由树

路由树是Tree结点连接组成的一棵树。每个Tree结点包含四个项,分别为

  • prefix //前缀
  • fixrouters //[ ]固定(静态)路由
  • wildcard //通配符(树)
  • leaves //[ ] 叶子结点

其中fixrouters和wildcard的项又是Tree,这样就递归往复。

叶子结点是一个leafinfo的结构。

下面从调试结果来看将会一目了然

匹配样式

变化1:通配在末端

后端路由:

/user/get
/user/get/?:id:int
/user/get/?:id:string

调试结果

这个图清晰说明了思路,对于get结点来说,它的通配符wildcard有两个孩子。

叶子结点有三个孩子,原因是:

  1. /user/get本身就已经没有后续了,因此有一个叶子
  2. /user/get/?xx,?表示有或没有,由于它具有"没有"这个属性,因此也归为叶子。而在本段代码中,有两个?的路由,所以都划进来了

变化2:通配在中间

这种形式应该说不太合规范,只从原理上看个结果

/user/:id/get

变化一是常规,也是最正常的形式,如果是/user/:id/get这种样式怎么办?也就是不确定的匹配在中间掺杂,这里不考虑实际,而是从纯理论上来看,是有这种可能

打印结果如下:

符合推测结果,get被挂到user的通配符树下,这也说明,通配树和静态树两者可以互相嵌套对挂。

/user/?:id/get

由于?:id具有"有"或"没有"的两层含义,因此产生两种变化。路由算法将第一种变化直接归为静态路由,完全正确。

/user/?:id:int/get

打印结果如下:

与变化三相比,唯一多的就是一个正则匹配项,原因是这里限定了:int类型,

匹配符 *.*

匹配符*.*一般都放在尾端,如果非要放在中间,比如:/download/*.*/api,那就当没说...

理论上是可以,但是没有实际意义

*.*被解释为通配符

路由定义:/download/*.*

打印结果

情形一

请求路由:/download/file/js/api.xml

  1. 通配符被组合成["file","js","api.xml"]
  2. 最后一个值"api.xml"被取出并做分割得到扩展名.xml
  1. 路径再组通配符值组合得到:"file/js/api"

总结来说,从第一个通配符起始到末尾整个被匹配

其中,最后一项当做文件名

源码如下

 

匹配符 *

打印结果如下:

说明:

*号:从结果来看,*被解释为【通配符】,并在wildcards里面固定添加了一个":splat"。

下面记下一个匹配过程,比如有下面请求:

后端路由定义:/user/get/*

前端请求路由:/user/get/20/id/query

(这个路由只是为了举例说明)

 

匹配到20的时候,由于get之下已经没有静态路由,于是转到通配符树继续匹配。

转到之后发现此通配符树既没有静态,也没有子通配符树,因此,最终来到最后一步,用leaves来匹配。

在这一步中,先将所有的通配内容组装在一块,也就是说20/id/query,被一一拆分又被组装到wildcardValues中。

 

源码如下:

由于*的匹配能力太强大,这里就被接盘了。

正则自定义 cms_:id([0-9]+)_:page([0-9]+).html

打印结果

匹配策略

有很多东西用语言描述似乎很困难,真是只可意会,不可言转,尤其对于递归

首先看一张概念图(如果看图理解了,语言就是多余的:)-)

这个概念图主要表明几点:

  1. 路由首先尽可能努力的往下匹配,匹配路径依次为:静态路由,通配符,叶子结点。
    具体说就是,先匹配静态路由,一直到失败或静态路由为0。接着在当前失败节点下再尝试通配符树匹配,再不行,做最后一次努力,用叶子结点尝试匹配。
  2. 如果步骤1失败,那么递归回滚到倒数第二个结点,再看它的通配符和叶子节点行不行。如果再失败,回滚到倒数第三个结点(往顶点方向前进),以此类推。

    举个例子来说,路由定义为:
    /user/get/aa/bb/cc/
    /user/get/*
    /user/*

    请求的路由为:/user/get/aa/dd,先尽可能努力往下匹配,一直到aa/bb时,由于静态路由不等,因此开始用当前失败结点aa的通配符树匹配,失败,再用叶子结点匹配,继续失败,此时当前结点已经尝试匹配完全了,递归代码开始往回滚,....一直到get这个结点时,发现它有一个强大的*通配符,因此这个点接盘,完成了路由匹配。(假如这个点还不成功,一直回滚到/user,如果到了原点还是没有匹配的,那么路由匹配彻底失败)

    总结就是:A->B->C->D->E...,先努力往后匹配,如果不成功就倒回来D->C->B->A...,在每个结点内部再按着通配符、叶子结点的顺序挨个的匹配。非常类似于DOM的冒泡机制
  1. 通过步骤1,2可以看出,当前路由匹配失败并不一定会失败,可能在前部的某个节点中接盘。从侧面也说明,路由的定义要谨慎。因为定义太多,说不定哪个地方就会出现漏洞。

匹配优先级

理解了原理,自然就会想到这个问题

比如,后端打算的路由是这样的:

/user/get/:id:int

意思是匹配:/user/get/整数

结果,某个同事在不知情的情况下又在前面加了一个路由:

/user/get/*

这样整个路由的排列顺序为:

/user/get/*

/user/get/:id:int

现在前端无论发送什么样的/user/get/xx都被/user/get/*匹配了,显然这不是想要的结果

因此,路由在定义时务必很小心。

路由示例一览表

(后续添加)

 
posted on 2022-01-19 02:59  LuoTian  阅读(362)  评论(0编辑  收藏  举报