原文高清傳送門:ichuan.me/4-test
簡評
如果事先沒有了解過 swift,可以先到 swift.org 管窺一二,server-side project 作為 swift 的應用領域之一,也是其未來重點發力的地方。
本文主要探討的是 swift 服務端框架 Perfect 和國慶前夕 swift-server 小組釋出的 http 服務端基礎框架 api,此兩者的架構分析和異同點比較。
首先兩者的定位是不一樣的,Perfect
是一套成熟的服務端框架,其自給自足的周邊已經相當完善了,開發者可基於它快速搭建一套 web 服務,可以直接類比 Node.js 服務端框架 koa / express,它的目標使用者是 web 服務的開發者。swift-server/http
的定位則是實現一組基礎的 HTTP 服務 API,上層 web 應用框架可基於它來搭建,可以說它的目標使用者是 web 應用框架,比如 PerfectX :)
以上兩張圖分別代表了 Perfect
和 swift-server/http
的層次或者呼叫架構,當然這是以我個人的視角(經驗)來分析的,或者會有理解出問題的地方,還請各位看官雅正。我先聊一下對這兩者的認識吧。
Perfect
Perfect 似乎天生就帶有中式基因,API 文件標配中英版本,這對於國內的開發者而言,多少拉近了些距離。而且 PerfectlySoft
這個組織有著不錯的運營和推廣,團隊華人成員RockfordWei,就是其中一名核心開發。
Perfect 目前發展的挺快,官網一直在更新其周邊。
其基礎庫和衍生的擴充套件庫(Perfect 全家桶)發展迅猛,(靜態)路由、模板、過濾器、安全、認證、資料庫、websocket、IPV6/http2/https、TensorFlow 支援等等。基礎庫實現都是跨平臺的,歷史原因也保留了自己的多執行緒、佇列實現(目前 swift-corelibs-libdispatch 已經實現了跨平臺)。
在上圖中,其基礎架構和呼叫流程是比較簡單的:
- server 完成初始化,載入路由;
- 可選的設定 request/response 過濾器;
- server.start 中完成 socket bind/listen/accept;
- 每個收到的 c_socket,啟用全域性佇列中的執行緒來 handlerConnection 處理;
- (僅討論 HTTP1.1) handlerConnection 分別呼叫 readRequest/runRequest,此兩者是核心;
- readRequest 中從 socket 讀取資料,成功則進入下一步,失敗則會啟用 NetEvent 開始 I/O 多路複用 epoll/kevent,直到資料可讀取,再次讀取成功則進入下一步;
- readRequest 從 socket 資料讀取成功,會觸發
http_parser
設定好的各種回撥,這些回撥會把 request 的完整資訊 parse 出來暫存下來。至此,讀取階段結束; - runRequest 主要處理 request 過濾器(如果有)和路由;
- 在路由中最後會呼叫
response.complete
,觸發 completeCallback,隨即呼叫flush
寫出資料到 socket,分別為pushHeader
和pushBody
,這兩者又會呼叫 response 的過濾(如果有),最後真正寫出。與讀取同樣,寫出若失敗則會給到 NetEvent epoll/kevent,直到可寫再次寫出。
以上即為 Perfect 的請求處理流程,結構上是比較清晰的(相對於 swift-server/http)。
這裡面有不少細節處理的比較精細,比如 NetEvent 僅在需要時才建立等。
吹毛求疵的說,第 4 步有點莫名的接連兩次啟用了非同步執行緒來處理 c_socket,私以為沒有必要。第二個就是過濾器的設定有點疑問,.high/.medium/.low 既然是通過級別來劃組,高階別如果處理了,同級別的跳過,低階別還能處理的設定就有點奇怪了。
總的來說,Perfect 使用上比較方便,特別是在 macOS 環境上,配合 IDE 除錯也很方便。一般的服務,我都會盡力嘗試用它來替代 Nodejs。
swift-server/http
應該是國慶假期的時候,偶然發現了這個庫。聽說是 server-group 官方釋出,於是見獵心喜,馬上就對著原始碼對比看了看。在評價之前,對著上圖,我們先走一遍流程:
- server.init
- server.start,socket bind/listen,基本上這個時候主執行緒的邏輯就到這裡了,接著 Runloop.run 到天荒地老了;
- 非同步執行緒跑一段 loop:accept,round-robin 選擇該 socket 讀寫使用的 queue,構造 parser/listener。
parser 是一個稍顯重量的類,主要負責資料(request)的分析、解構,資料分析的同時驅動後續流程(handler 的處理),listener 則主要負責 socket 的基本管理,比如 close/read/write;
- 接著在 acceptQueue 中執行 listener.process 邏輯。這裡 acceptQueue 的最大併發量表示了最多能同時處理多少客戶端的連結請求;
- process 通過
DispatchSourceRead
設定 event_callback,在可讀事件回撥中執行parser.readStream
。這裡可以對比 Perfect 流程的第 6 步; - readStream 做的事情和 Perfect 中
readRequest
基本一致,但是它除了解構組裝 request 資料之外,同時負責驅動 handler 呼叫。這是這個框架最特立獨行的地方,至少從這裡開始就和 Perfect 走向截然不同的道路了; - 在 readStream 中
bodyReceived
/messageCompletion
階段,會呼叫 handler 邏輯,一般我們會呼叫response.writeHeader
/response.writeBody
,這裡會直接寫出到套接字。流程結束。
整個流程相比 Perfect 顯得不那麼有結構性,從第 5 步開始筆鋒鬥轉,readStream 操作直接驅動後續所有流程,這中間少了很多可以操作的空間。
這裡有幾個值得注意的點:
- 顯然,
swift-server/http
能力非常單一,流程銜接緊張,上層框架所有能介入的地方,只有一處,即一開始的 handler,這個 handler 必然要包含所有的處理,包括過濾器,路由等等。似乎它只處理了 request 資料讀取和分析解構; - 相較 Perfect 的 I/O 事件機制 epoll/kevent,
swift-server/http
使用 DispatchSource 來獲取可讀可寫事件,寫法上更顯純 swift 風格一些,雖然底層上可能也是使用 epoll/kevent 機制。但是有點奇怪的地方在於,它在資料讀取階段使用了 DispatchSourceRead,但是寫出階段卻是直寫,未通過 DispatchSourceWrite,這應該是一個明顯的 bug,因為 socket 在 send 時會因為視窗等原因,傳送是有可能失敗的(需等待緩衝區),這裡會直接 close 掉導致資料寫出失敗。
總評
swift-server/http
作為一個剛剛發起的專案,後續還會有很多更新。考慮到其背景特殊,未來基於它而實現的 web 應用框架或許也不會少。
私以為,類似 Perfect 這樣已經相當成熟的 web 框架應該不大會轉投到 swift-server/http
陣營,畢竟是和 Perfect-Net/Perfect-HTTP/Perfect-HTTPServer 等核心模組直接衝突的,而且當下看來,Perfect 在基礎功能和擴充套件功能的組合結構性上會更合理一點。
當然了,我也可能被打臉,萬一呢。哈哈。
題外話
swift-server/which_is_the_fastest 這個專案中測試的得分可能稍顯牽強,因為 swift-server/http 本身的功能就比較少,沒有路由、過濾等消耗,得分相比其他 web 應用框架自然會較高。
PS: 我怎麼感覺自己是 swift-server/http 黑,逃)