http 框架的路由實現原理

RryLee發表於2020-03-01

前不久在golang API閘道器中自己實現了一個api路由,最近又有機會使用了python,所以有機會對這些實現進行一個對比。其次,以前也用過很多 php 相關的框架,在這篇部落格中做一個總結對比。

Thinkphp

thinkphp 早期實現路由是通過請求 uri 和框架自定義的一套目錄+檔案類+類方法來實現的路由對映,除此以外,早期的php框架包括 ci,yii都有這種隱式路由的實現。(thinkphp 貌似也有自定義對映關係的實現,這裡不做討論)。

就拿 /api/user/get_user_info 這樣一個獲取使用者資訊的介面來說,在 TP 裡面相對的會存在以下這樣一個檔案,同時 Controller 裡面會有一個 getUserInfo 的方法。

Application/
├── Api
│   ├── Controller
│   │   ├── UserController.class.php

實現過程分為以下幾步:

  1. 解析到 url 為 /api/user/get_user_info
    1. 模組為 api,檔案為 UserController,方法為 getUserInfo
  2. 判斷 Api 資料夾是否存在
  3. 判斷 UserController.class.php 是否為檔案,然後 require 該檔案
  4. 判斷對應類方法是否存在,存在就呼叫。

可以看到,整個實現是通過框架對目錄結構,檔案命名等方式進行一個統一的約束,然後實現uri匹配的。整個過程存在著大量的系統呼叫,不過這也是指令碼語言在web場景下無法避免的事情。

通過這種方式,可能是實現一個介面最快的方式,因為你少了去定義路由的過程,但不得不得說在 restful 和 api 的語義化上可能就不能很方便的定義了。以現在的眼光來審視這些實現,可能會覺得太low,不屑一顧,不過當時解決了那麼多人在web開發的問題,國內無數的商業公司在使用,足以說明他是一個牛逼的框架(這裡我只是說點廢話,TP已經足夠好了,也不需要我的評判)。

Laravel

在過去很長很長的一段時間,我都在使用 laravel 進行開發,目前 laravel 已經成為了整個 php 生態圈最活躍,最優秀的框架之一了,在程式碼的架構上足夠模組化,第一次接觸你可能多會感嘆,“哇,原來php也能寫出這麼優秀的程式碼”。整個 laravel 的原始碼我曾經也通讀過一篇,很多日常開發中的思想和套路都是來自於此。

laravelapi 實現足以支援 restful 你所需要的全部功能,由於 php 每次處理請求都是一個 runtime,不可避免的也是需要像 tp 一樣做很多檔案載入的操作。laravel 的路由是可以進行單獨定義的,類似 flaskget post…,或者 spring 中的註解,這些註冊的介面會被新增進入一個集合。

這個集合第一層的 keyhttp method 作為入口,也就是區分不同的方法。拿 /api/user 這個介面為例子

routes: array:3 ["GET" => array:1 ["api/user" => Route {#114 ▶}
  ]
  "HEAD" => array:1 ["api/user" => Route {#114 ▶}
  ]
  "POST" => array:2 ["api/user/{id}/edit" => Route {#112 ▶}
  ]
]

其實看到這裡就比較清楚了,整個路由的匹配過程第一步就是獲取到對應的請求方法的 Route 陣列,然後對陣列依次進行匹配。不過 laravel 講匹配過程拆分為四個部分:

  1. 匹配 uri
  2. 匹配 method (laravel 有防止代理層修改 method,這裡其實用 method 做了一層相容,正常情況第一步就匹配過 method 了,不需要考慮這一部分)
  3. schema 匹配,http和https
  4. host 匹配,laravel 提供繫結域名的功能

相對來說,2,3,4都是很好理解的,這裡重點看看 uri 是怎麼匹配的,不過答案也只有一句話

每個route的uri匹配會編譯為一個正規表示式

考慮到這個效能消耗,laravel 也只會迴圈到被匹配的 route 才會進行編譯。

所以 laravel 做了一個路由快取的優化,你可以手動的進行編譯,laravel 會把你所有註冊的 uri 全部序列化到一個檔案中(這一步當然也包括正規表示式的編譯),之後每次請求都直接反序列化這個路由檔案,然後進行 uri 匹配。

php 其他框架

又參考了包括 FastRoute 等其他 php 中比較小,效能好的框架,基本上都是通過正規表示式進行匹配。

go httprouter

從演算法上來說,httprouter 是通過字首樹來進行匹配的,字首樹相比基礎的字典樹來說,在匹配很長的字串上有更好的效能,所以更適合 uri 的匹配場景。

對比了一下各種語言 http 框架的路由實現原理

簡單的來說每一個註冊的 url 都會通過 / 切分為 n 個樹節點(httprouter 會有一些區別,會存在根分裂),然後掛到相應 method 樹上去,所以業務中有幾種不同的 method 介面,就會產生對應的字首樹。在 httprouter 中,節點被分為 4 種型別:

  • static - 靜態節點,/user /api 這種
  • root - 根結點
  • param - 引數節點 /user/{id}id 就是一個引數節點
  • catchAll - 萬用字元

其實整個匹配的過程也比較簡單,通過對應的 method 拿到字首樹,然後開始進行一個廣度優先的匹配。

這裡值得學習的一點是,httprouter 對下級節點的查詢進行了優化,簡單來說就是把當前節點的下級節點的首字母維護在本身,匹配時先進行索引的查詢。

對比了一下各種語言 http 框架的路由實現原理

API proxy

這裡所指的 proxy 是最近開發的一個閘道器。

作為一個 api proxy,轉發所有的請求到目標服務,請求進入之後就需要進行 api 的匹配。這個匹配規則和 httprouter 差不多,所有的 api 都會構建在一棵字首樹上,和 httprouter 的按照 method 劃分不同,method 屬性會在每一個 node 上面。

Python

因為使用的是 tornado 框架,所以只看了這個框架的實現,再加上 tornado 作為一個高效能的非同步 HTTP 伺服器,所以期待能不能收穫到一些新的細節。最後大概瀏覽了一下,也是使用正規表示式做的匹配。

說在最後

不管動態語言,還是golang這種需要編譯的語言,在路由的實現上都大同小異,最大的區別可能就是對引數定義支援到不同的程度,某些框架可能你可以定義出及其複雜規則的 url。迴歸到業務,我呆過的兩家大公司,基本上都是 post 請求一把鎖,當你考慮到 restful 的定義是否合理可能和每個人的水平有關,restful 接入方也會有額外的成本,所以很多情況下大家還是會使用 post 請求來完成你業務中絕大部分的事情。


最後歡迎大家關注我的公眾號

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章