前言
**所谓动态路由,**即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。
动态路由有很多种实现方式,支持的规则、性能等有很大的差异。例如开源的路由实现httplib支持在路由规则中嵌入正则表达式,例如/p/[0-9A-Za-z]+,即路径中的参数仅匹配数字和字母;
前缀树作为一种高效的数据结构,可以快速进行前缀匹配,逐级匹配字符,从而实现动态路由的灵活性和高性能。相比之下,使用正则表达式进行动态路由匹配可能会引入更复杂的模式匹配和性能开销,不如前缀树简洁高效。
对于动态路由中的参数匹配,使用前缀树可以更灵活地处理不同类型的参数。前缀树可以支持在节点中存储参数信息,
因此,前缀树在实现动态路由时具有必要性,并能够满足灵活的路由匹配需求。
一、为什么可以使用前缀树来实现动态路由
**实现动态路由最常用的数据结构,被称为前缀树(Trie树)。**看到名字你大概也能知道前缀树长啥样了:每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配。
使用前缀树实现动态路由的优点如下:
前缀匹配:前缀树的一个重要特性是可以进行前缀匹配。在路由查找中,可以使用前缀树找到最长匹配的前缀,从而确定应该采取哪个路由。这对于路由的选择和路由表的压缩都是非常有用的。
高效的查找:前缀树可以在O(m)的时间复杂度内查找到匹配给定前缀的节点,其中m是前缀的长度。这使得路由查找非常快速,无论路由表的大小如何。
动态更新:前缀树支持动态更新,可以方便地插入、删除或修改节点。这意味着在路由表发生变化时,可以快速地更新前缀树,而不需要重新构建整个路由表。
相比于使用正则表达式实现动态路由,使用前缀树(Trie树)的方式具有以下几个优势:
**简化的路由规则:**使用前缀树,路由规则可以更加简化和直观。每个节点代表一个字符或字符序列,可以直接映射到相应的路由路径。相比之下,使用正则表达式实现动态路由可能需要编写更复杂的模式匹配规则,难以直观地表示路由结构。
**更好的可读性和可维护性:**前缀树的结构更易于理解和维护。路由规则以树的形式展示,可以清晰地表示路由层次和结构。这使得代码更易于阅读、理解和调试,也方便进行路由的添加、删除和修改操作。
**灵活的路由匹配:**前缀树支持多种灵活的路由匹配方式,如通配符匹配、参数化路由等。这使得在路由规则中引入一些灵活性变得更加容易,而在正则表达式中可能需要更复杂的模式匹配。
**参数提取:**上述方法通过将路由模式中的冒号后的字符串作为参数名,将路径中对应位置的实际值提取出来。而正则表达式路由可以使用具有捕获组的正则表达式来直接提取参数值,不需要手动解析路径。
性能:基于前缀树的路由匹配在大规模路由规则和高并发请求的情况下性能较好,因为它使用了数据结构来加速匹配过程。而正则表达式匹配相对较慢,特别是在复杂的正则表达式模式和大量请求的情况下。
二、动态路由规则
实现动态路由最常用的数据结构,被称为前缀树(Trie树)。看到名字你大概也能知道前缀树长啥样了:每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配,比如我们定义了如下路由规则:
/: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,这种模式常用于静态服务器,能够递归地匹配子路径。
三、前缀树的构建
类实现思路:
通过构建一个路由树来实现路由匹配。路由树是一个多叉树,每个节点代表一个路径的一部分。根节点表示空路径,每个子节点表示一个路径的部分。通过在树中插入模式,可以构建一个完整的路由树。
在插入模式时,根据路径的部分将模式插入到合适的节点中。如果节点不存在,则创建一个新节点,并将其添加到父节点的子节点列表中。如果路径的部分是通配符(以":“或”*"开头),则将iswild标志设置为true。
在搜索时,根据路径的部分逐级向下搜索匹配的节点。如果节点的部分路径与给定部分路径匹配,或者节点是通配符节点,则继续向下搜索。如果找到与路径完全匹配的节点,则返回该节点。如果没有找到匹配的节点,则返回nullptr。
class Node {
private:
string pattern;
string part;
vector<Node*> children;
bool iswild;
public:
Node(string part, bool iswild = false) {
this->part = part;
this->iswild = iswild;
}
string getPattern() const {
return pattern;
}
void setPattern(const string& pattern) {
this->pattern = pattern;
}
string getPart() const {
return part;
}
bool isWild() const {
return iswild;
}
成员变量:
**pattern:**存储该节点对应的路由模式,仅叶子节点才会存储具体的路由模式。
**part:**存储该节点的字符或字符序列,代表路径中的一部分。
**children:**存储该节点的子节点列表。
**iswild:*标识该节点是否是通配符节点(如参数化路由中的:或通配符路由中的)。
Node* matchChild(const string& part) const {
for (Node* child : children) {
if (child->getPart() == part || child->isWild()) {
return child;
}
}
return nullptr;
}
这个函数用于在当前节点的子节点列表中匹配指定的字符或字符序列 part
。它遍历当前节点的子节点列表,对于每个子节点,检查其存储的字符或字符序列是否与 part
相等,或者该子节点是否为通配符节点(使用 isWild()
方法判断)。如果找到匹配的子节点,则返回该子节点指针;如果没有找到匹配的子节点,则返回 nullptr
。
vector<Node*> matchChildren(const string& part) const {
vector<Node*> nodes;
for (Node* child : children) {
if (child->getPart() == part || child->isWild()) {
nodes.push_back(child);
}
}
return nodes;
}
这个函数用于在当前节点的子节点列表中匹配指定的字符或字符序列 part
,并返回匹配到的所有子节点。它遍历当前节点的子节点列表,对于每个子节点,检查其存储的字符或字符序列是否与 part
相等,或者该子节点是否为通配符节点。如果找到匹配的子节点,则将该子节点添加到 nodes
向量中。最后,返回包含所有匹配子节点的向量。
void insert(const string& pattern, const vector<string>& parts, int height) {
if (parts.