您现在的位置是:亿华云 > 人工智能

Node.js 服务性能翻倍的秘密(二)

亿华云2025-10-09 13:43:25【人工智能】0人已围观

简介前言前一篇文章介绍了 fastify 通过 schema 来序列化 JSON,为 Node.js 服务提升性能的方法。今天的文章会介绍 fastify 使用的路由库,翻阅其源码(lib/route.j

前言

前一篇文章介绍了 fastify 通过 schema 来序列化 JSON,服务翻倍为 Node.js 服务提升性能的服务翻倍方法。今天的服务翻倍文章会介绍 fastify 使用的路由库,翻阅其源码(lib/route.js)可以发现,服务翻倍fastify 的服务翻倍路由库并不是内置的,而是服务翻倍使用了一个叫做 find-my-way 的路由库。

route.js

这个路由库的服务翻倍简介也很有意思,号称“超级无敌快”的服务翻倍 HTTP 路由。

README

看上去 fastify 像是服务翻倍依赖了第三方的路由库,其实这两个库的服务翻倍作者是同一批人。

author

如何使用find-my-way 通过 on 服务翻倍方法绑定路由,并且提供了 HTTP 所有方法的服务翻倍简写。

const router = require(./index)() router.on(GET,服务翻倍 /a, (req, res, params) => {    res.end({ "message": "GET /a"}) }) router.get(/a/b, (req, res, params) => {    res.end({ "message": "GET /a/b"}) })) 

其实内部就是通过遍历所有的 HTTP 方法名,然后在原型上扩展的服务翻倍。

Router.prototype.on = function on (method,服务翻倍 path, opts, handler) {    if (typeof opts === function) {      // 如果 opts 为函数,表示此时的服务器租用 opts 为 handler     handler = opts     opts = { }   }   // ... } for (var i in http.METHODS) {    const m = http.METHODS[i]   const methodName = m.toLowerCase()   // 扩展方法简写   Router.prototype[methodName] = function (path, handler) {      return this.on(m, path, handler)   } } 

绑定的路由可以通过 lookup 调用,只要将原生的 req 和 res 传入 lookup 即可。

const http = require(http) const server = http.createServer((req, res) => {    // 只要将原生的 req 和 res 传入 lookup 即可   router.lookup(req, res) }) server.listen(3000) 

find-my-way 会通过 req.method/req.url 找到对应的 handler,然后进行调用。

Router.prototype.lookup = function lookup (req, res) {    var handle = this.find(req.method, sanitizeUrl(req.url))   if (handle === null) {      return this._defaultRoute(req, res, ctx)   }   // 调用 hendler   return handle.handler(req, res, handle.params) } 

路由的添加和查找都基于树结构来实现的,下面我们来看看具体的实现。

Radix Tree

find-my-way 采用了名为 Radix Tree(基数树) 的算法,也被称为 Prefix Tree(前缀树)。Go 语言里常用的 web 框架echo和gin都使用了Radix Tree作为路由查找的算法。

在计算机科学中,基数树,或称压缩前缀树,是一种更节省空间的Trie(前缀树)。对于基数树的每个节点,如果该节点是确定的子树的话,就和父节点合并。

Radix Tree

在 find-my-way 中每个 HTTP 方法(GET、服务器托管POST、PUT ...)都会对应一棵前缀树。

// 方法有所简化... function Router (opts) {    opts = opts || { }   this.trees = { }   this.routes = [] } Router.prototype.on = function on (method, path, opts, handler) {    if (typeof opts === function) {      // 如果 opts 为函数,表示此时的 opts 为 handler     handler = opts     opts = { }   }   this._on(method, path, opts, handler) } Router.prototype._on = function on (method, path, opts, handler) {    this.routes.push({      method, path, opts, handler,   })   // 调用 _insert 方法   this._insert(method, path, handler) } Router.prototype._insert = function _insert (method, path, handler) {    // 取出方法对应的 tree   var currentNode = this.trees[method]   if (typeof currentNode === undefined) {      // 首次插入构造一个新的 Tree     currentNode = new Node({  method })     this.trees[method] = currentNode   }   while(true) {      // 为 currentNode 插入新的节点...   } } 

每个方法对应的树在第一次获取不存在的时候,都会先创建一个根节点,根节点使用默认字符(/)。

trees

每个节点的数据结构如下:

// 只保留了一些重要参数,其他的暂时忽略 function Node(options) {    options = options || { }   this.prefix = options.prefix || / // 去除公共前缀之后的字符,默认为 /   this.label = this.prefix[0]         // 用于存放其第一个字符   this.method = options.method        // 请求的方法   this.handler = options.handler      // 请求的回调   this.children = options.children || { } // 存放后续的子节点 } 

当我们插入了几个路由节点后,树结构的具体构造如下:

router.on(GET, /a, (req, res, params) => {    res.end({ "message":"hello world"}) }) router.on(GET, /aa, (req, res, params) => {    res.end({ "message":"hello world"}) }) router.on(GET, /ab, (req, res, params) => {    res.end({ "message":"hello world"}) }) 

  

GET Tree

Node {    label: a,   prefix: a,   method: GET,   children: {      a: Node {        label: a,       prefix: a,       method: GET,       children: { },       handler: [Function]     },     b: Node {        label: b,       prefix: b,       method: GET,       children: { },       handler: [Function]     }   },   handler: [Function] } 

如果我们绑定一个名为 /axxx 的路由,为了节约内存,不会生成三个 label 为x 的节点,只会生成一个节点,其 label 为 x,prefix 为 xxx。

router.on(GET, /a, (req, res, params) => {    res.end({ "message":"hello world"}) }) router.on(GET, /axxx, (req, res, params) => {    res.end({ "message":"hello world"}) }) 

 

GET Tree

Node {    label: a,   prefix: a,   method: GET,   children: {      a: Node {        label: x,       prefix: xxx,       method: GET,       children: { },       handler: [Function]     }   },   handler: [Function] } 

插入路由节点

通过之前的代码可以看到, on 方法最后会调用内部的云服务器 _insert 方法插入新的节点,下面看看其具体的实现方式:

Router.prototype._insert = function _insert (method, path, handler) {    // 取出方法对应的 tree   var currentNode = this.trees[method]   if (typeof currentNode === undefined) {      // 首次插入构造一个新的 Tree     currentNode = new Node({  method })     this.trees[method] = currentNode   }   var len = 0   var node = null   var prefix =    var prefixLen = 0   while(true) {      prefix = currentNode.prefix     prefixLen = prefix.length     len = prefixLen     path = path.slice(len)     // 查找是否存在公共前缀     node = currentNode.findByLabel(path)     if (node) {        // 公共前缀存在,复用       currentNode = node       continue     }     // 公共前缀不存在,创建一个     node = new Node({  method: method, prefix: path })     currentNode.addChild(node)   } } 

插入节点会调用 Node 原型上的 addChild 方法。

Node.prototype.getLabel = function () {    return this.prefix[0] } Node.prototype.addChild = function (node) {    var label = node.getLabel() // 取出第一个字符做为 label   this.children[label] = node   return this } 

本质是遍历路径的每个字符,然后判断当前节点的子节点是否已经存在一个节点,如果存在就继续向下遍历,如果不存在,则新建一个节点,插入到当前节点。

tree

查找路由节点

find-my-way 对外提供了 lookup 方法,用于查找路由对应的方法并执行,内部是通过 find 方法查找的。

Router.prototype.find = function find (method, path, version) {    var currentNode = this.trees[method]   if (!currentNode) return null   while (true) {      var pathLen = path.length     var prefix = currentNode.prefix     var prefixLen = prefix.length     var len = prefixLen     var previousPath = path     // 找到了路由     if (pathLen === 0 || path === prefix) {        var handle = currentNode.handler       if (handle !== null && handle !== undefined) {          return {            handler: handle.handler         }       }     }     // 继续向下查找     path = path.slice(len)     currentNode = currentNode.findChild(path)   } } Node.prototype.findChild = function (path) {    var child = this.children[path[0]]   if (child !== undefined || child.handler !== null)) {      if (path.slice(0, child.prefix.length) === child.prefix) {        return child     }   }   return null } 

查找节点也是通过遍历树的方式完成的,找到节点之后还需要放到 handle 是否存在,存在的话需要执行回调。

总结

本文主要介绍了 fastify 的路由库通过 Radix Tree 进行提速的思路,相比于其他的路由库通过正则匹配(例如 koa-router 就是通过 path-to-regexp 来解析路径的),效率上还是高很多的。

很赞哦!(349)