Node.js 服務性能翻倍的祕密(二)

image

前言

前一篇文章介紹了 fastify 經過 schema 來序列化 JSON,爲 Node.js 服務提高性能的方法。今天的文章會介紹 fastify 使用的路由庫,翻閱其源碼(lib/route.js)能夠發現,fastify 的路由庫並非內置的,而是使用了一個叫作 find-my-way 的路由庫。node

route.js

這個路由庫的簡介也頗有意思,號稱「超級無敵快」的 HTTP 路由。git

README

看上去 fastify 像是依賴了第三方的路由庫,其實這兩個庫的做者是同一批人。github

author

如何使用

find-my-way 經過 on 方法綁定路由,而且提供了 HTTP 全部方法的簡寫。web

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)
}

路由的添加和查找都基於樹結構來實現的,下面咱們來看看具體的實現。koa

Radix Tree

find-my-way 採用了名爲 Radix Tree(基數樹) 的算法,也被稱爲 Prefix Tree(前綴樹)。Go 語言裏經常使用的 web 框架echogin都使用了Radix Tree做爲路由查找的算法。函數

在計算機科學中,基數樹,或稱壓縮前綴樹,是一種更節省空間的Trie( 前綴樹)。對於基數樹的每一個節點,若是該節點是肯定的子樹的話,就和父節點合併。

Radix Tree

find-my-way 中每一個 HTTP 方法(GETPOSTPUT ...)都會對應一棵前綴樹。性能

// 方法有所簡化...
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 來解析路徑的),效率上仍是高不少的。

image

相關文章
相關標籤/搜索