资讯专栏INFORMATION COLUMN

翻译 使用正则的快速路由库

Lionad-Morotar / 703人阅读

摘要:的路由是使用了这个库作者写了一篇帖子介绍了它写这个库的原因原文链接使用正则的快速路由库前段时间我在路由库遇到了一些问题。它号称比现用的路由库快几个数量级的,因为为了达到这个这个目的,这个库是通过的扩展实现的。

首先先区分一下概念:
路由是指一个过程,就是利用定义好的一些规则,让不同的URI能够调用不同的处理器(一个匿名函数或者一个类中的方法)这样一个过程。

平常很多框架所说的定义一个路由就是注册一个这样的规则到系统中去。

slim的路由是使用了FastRoute这个库,作者写了一篇帖子,介绍了它写这个库的原因(原文链接):

使用正则的快速路由库

前段时间,我在Pux路由库遇到了一些问题。它号称比现用的路由库快几个数量级的,因为为了达到这个这个目的,这个库是通过PHP的C扩展实现的。
然而,粗略地看了Pux的源码之后,我强烈怀疑这个库优化了路由处理的错误部分,而我不借助于C扩展却可以很轻松地到更好的性能。当我看了Pux的基准测试之后,发现只测试了几个非常简单实际的单路由例子,我就更加肯定了我的怀疑。
为了进一步调查这个问题,我写了一个小型路由库:FastRoute,这个库实现了接下来描述的分发处理。为了给出预先观点,我贴出了和Pux库对比的小型基准测试结果:

1 placeholder  | Pux (no ext) | Pux (ext) | FastRoute
-----------------------------------------------------
First route    | 0.17 s       | 0.13 s    | 0.14 s
Last route     | 2.51 s       | 1.20 s    | 0.49 s
Unknown route  | 2.34 s       | 1.10 s    | 0.34 s

9 placeholders | Pux (no ext) | Pux (ext) | FastRoute
-----------------------------------------------------
First route    | 0.22 s       | 0.19 s    | 0.20 s
Last route     | 2.65 s       | 1.78 s    | 0.59 s
Unknown route  | 2.50 s       | 1.49 s    | 0.40 s

这个基准测试使用了一百个路由,然后找出其中最快的路由(最佳例子),最慢的路由(最差的例子)和一个总共的未知平均路由。测试通过设置一个变量分为两部分,一部分使用了1个占位符,另一部分使用了9个占位符。整个测试很明显进行了循环了几百次。

关于路由的问题

为了确保我们在说同一个事物,让我们定义一下"路由"是什么。在大多数实际的形式中,它是指跟以下形式类似的利用一套路由定义:

$r->addRoute("GET", "/user/{name}/{id:d+}", "handler0");
$r->addRoute("GET", "/user/{id:d+}", "handler1");
$r->addRoute("GET", "/user/{name}", "handler2");

然后调度处理一个基于它们的URI的过程:

$d->dispatch("GET", "/user/nikic/42");
// => provides "handler0" and ["name" => "nikic", "id" => "42"]

把这个过程提升到一个更抽象的层次,我们将会为路由定义提供HTTP方法和任何特定的格式。在本文中,我会考虑的唯一一样事情是路由的调度阶段——路由如何被解析或调度器生成的数据不会被覆盖。
那么,路由处理的最耗时的部分是哪里呢?在一个混乱不堪的,过度被设计的系统中,它可能是实例化数十个对象和调用数百个方法的开销。Pux库在减少这方面的开销做的很好。然而,在一个比较原始层次的系统中,依次经过一系列数十个或者数百个路由表达式,之后再通过提供的URI和它们进行匹配这个过程是最耗时的部分。让这个过程更快就是本文的主题。
合并的正则表达式:
优化这类问题的最基本方法是避免一个个的去匹配那些正则表达式,相反地,把它们结合在一起,变成一个大的正则表达式,这样的话,你只需要进行一次匹配就可以了。就拿最后一个例子的路由作说明,合并的正则表达式是这样的:

Individual regexes:

    ~^/user/([^/]+)/(d+)$~
    ~^/user/(d+)$~
    ~^/user/([^/]+)$~

Combined regex:

    ~^(?:
        /user/([^/]+)/(d+)
      | /user/(d+)
      | /user/([^/]+)
    )$~x

这个转化很简单:基本上你只要将那些正则表达式一个个地用OR 连接在一起就可以了。当匹配这个合并的正则表达式,如何找出具体哪个路由规则被匹配了呢?为了找出来,让我们来看一看preg_match这个函数对一个样本的输出:

preg_match($regex, "/user/nikic", $matches);
=> [
    "/user/nikic",   # full match
    "", "",          # groups from first route (empty)
    "",              # groups from second route (empty)
    "nikic",         # groups from third route (used!)
]

那么,在$matches数组中找到第一个非空入口就是诀窍了(当然,没算上第一个完全匹配)。


这里我贴上代码,方便大家测试:

$regex = "~^(?:/user/([^/]+)/(d+)|/user/(d+)|/user/([^/]+))$~x";
preg_match($regex, "/user/nikic", $matches);

var_dump($matches);

(?:regexp) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 "或" 字符 (|) 来组合一个模式的各个部分是很有用。例如, "industr(?:y|ies) 就是一个比 "industry|industries" 更简略的表达式。介绍


为了使用这个结果,你将需要一个额外的数据结构来映射$matches的索引到匹配的路由规则(或者,一些关联那个路由规则的信息)

[
    1 => ["handler0", ["name", "id"]],
    3 => ["handler1", ["id"]],
    4 => ["handler2", ["name"]],
]

这里是一个实现整个处理流程的例子:

public function dispatch($uri) {
    if (!preg_match($this->regex, $uri, $matches)) {
        return [self::NOT_FOUND];
    }

    // find first non-empty match (skipping full match)
    for ($i = 1; "" === $matches[$i]; ++$i);

    list($handler, $varNames) = $this->routeData[$i];

    $vars = [];
    foreach ($varNames as $varName) {
        $vars[$varName] = $matches[$i++];
    }
    return [self::FOUND, $handler, $vars];
}

在找到第一个非空的索引,关联的数据就可以被查找到了。通过遍历$matches数组并配对值和变量名,占位符的变量就可以被填充了。
那么这个方法执行起来效率如何呢?这里给出了和Pux的比较结果(使用C扩展):

1 placeholder  | Pux (ext) | GPB-NC
-----------------------------------
First route    | 0.13 s    | 0.20 s
Last route     | 1.20 s    | 0.70 s
Unknown route  | 1.10 s    | 0.16 s

9 placeholders | Pux (ext) | GPB-NC
-----------------------------------
First route    | 0.19 s    | 0.41 s
Last route     | 1.78 s    | 4.09 s
Unknown route  | 1.49 s    | 0.30 s

GPB-NC表示“Group position based, non-chunked”调度。如你所见的那样,在单个占位符的测试例子这个方法提供了不错的性能。当然它不能打败在最快路由上正确匹配的C扩展实现,但是在最糟糕匹配上,它表现地快了一点,如果一个都没匹配的话,更快了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/23212.html

相关文章

  • 前端资源分享-只为更好前端

    摘要:一团队组织网站说明腾讯团队腾讯前端团队,代表作品,致力于前端技术的研究腾讯社交用户体验设计,简称,腾讯设计团队网站腾讯用户研究与体验设计部百度前端研发部出品淘宝前端团队用技术为体验提供无限可能凹凸实验室京东用户体验设计部出品奇舞团奇虎旗下前 一、团队组织 网站 说明 腾讯 AlloyTeam 团队 腾讯Web前端团队,代表作品WebQQ,致力于前端技术的研究 ISUX 腾...

    zxhaaa 评论0 收藏0
  • 前端资源分享-只为更好前端

    摘要:一团队组织网站说明腾讯团队腾讯前端团队,代表作品,致力于前端技术的研究腾讯社交用户体验设计,简称,腾讯设计团队网站腾讯用户研究与体验设计部百度前端研发部出品淘宝前端团队用技术为体验提供无限可能凹凸实验室京东用户体验设计部出品奇舞团奇虎旗下前 一、团队组织 网站 说明 腾讯 AlloyTeam 团队 腾讯Web前端团队,代表作品WebQQ,致力于前端技术的研究 ISUX 腾...

    JouyPub 评论0 收藏0
  • 前端资源分享-只为更好前端

    摘要:一团队组织网站说明腾讯团队腾讯前端团队,代表作品,致力于前端技术的研究腾讯社交用户体验设计,简称,腾讯设计团队网站腾讯用户研究与体验设计部百度前端研发部出品淘宝前端团队用技术为体验提供无限可能凹凸实验室京东用户体验设计部出品奇舞团奇虎旗下前 一、团队组织 网站 说明 腾讯 AlloyTeam 团队 腾讯Web前端团队,代表作品WebQQ,致力于前端技术的研究 ISUX 腾...

    vslam 评论0 收藏0
  • [nginx文档翻译系列]新手指南

    摘要:主进程的目的是为了读取和评估配置并保持工作进程。默认情况下,这个配置文件名为。如果一个块指令在大括号中有其他的指令,则称之为上下文如和。放在配置文件最外面的指令的称之为主文,指令在主文中在中,在中。注意指令已经被放置在环境中。 原文链接:http://nginx.org/en/docs/begi...转自我的github有些地方觉得翻译的不是很合理,所以在括号中写出了原句。如果有地方翻...

    jk_v1 评论0 收藏0
  • [nginx文档翻译系列]新手指南

    摘要:主进程的目的是为了读取和评估配置并保持工作进程。默认情况下,这个配置文件名为。如果一个块指令在大括号中有其他的指令,则称之为上下文如和。放在配置文件最外面的指令的称之为主文,指令在主文中在中,在中。注意指令已经被放置在环境中。 原文链接:http://nginx.org/en/docs/begi...转自我的github有些地方觉得翻译的不是很合理,所以在括号中写出了原句。如果有地方翻...

    phoenixsky 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<