昨天開始正式把巨大 Program.cs 拆開。
原本很多事情都混在一起:
TCP Server
Request Parsing
Routing
Handler
Response
現在則開始把它們分成不同責任。
例如:
MiniWebServer
↓
負責 Server
HttpRequest
↓
負責 Request
Router
↓
負責找 Handler
HttpResponse
↓
負責 Response
這樣看起來比較乾淨了。
但拆開之後又出現一個新的問題。
這些 Class 現在只是分開放著。
如果它們沒有真的接起來,也只是:
很多獨立零件。
所以今天真正想解決的是:
Framework 收到一個 Request 之後,到底要怎麼把 Router、Handler、Response 串成一條真正可以執行的流程?
以前會覺得:
把程式拆成幾個 Class,就算是重構完成。
但現在開始發現:
拆開只是第一步。
Framework 更重要的是:
這些元件之間怎麼合作。
例如:
MiniWebServer 收到 Request。
那接下來是誰負責找 Route?
Router 找到 Handler 後,又是誰執行?
Handler 回傳結果後,怎麼變成 HttpResponse?
HttpResponse 又怎麼回到原本那條 NetworkStream?
所以真正的 Framework 不是:
有很多 Class。
而是:
這些 Class 之間形成一條清楚的 Request Processing Flow。
今天要接起來的核心流程
今天先不要一次加入所有進階功能。
像:
Reflection
Parameter Binding
Middleware
JSON Model Binding
都可以再慢慢補。
Day27 最重要的是先把最核心的流程跑通:
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
↓
Browser
只要這條主幹可以順利運作,後面的功能就有地方可以插進去。
第一步:Server 不應該自己知道所有 Route
假設 MiniWebServer 收到:
GET /hello HTTP/1.1
Server 可以透過 HttpRequest 取得:
Method = GET
Path = /hello
但 Server 不應該自己寫:
如果是 /hello 就執行這個。
如果是 /products 就執行那個。
因為這樣 Routing 又回到 Server 裡面了。
比較合理的是:
MiniWebServer
↓
把 request.Path 交給 Router
↓
Router 找 Handler
也就是:
Server 負責收到 Request。
Router 負責回答:
這個 Request 應該交給誰。
這樣兩者的責任才真的分開。
Router 裡面到底存什麼?
之前我們已經用過:
Dictionary
來建立最簡單的 Route Table。
例如概念上:
/hello
↓
Hello Handler
/products
↓
Products Handler
所以 Router 可以理解成:
Route Table
+
Route Matching
Application 啟動時:
註冊 Route
Request 進來時:
查 Route
也就是:
Register
↓
之後 Match
這兩件事都是 Router 很自然的責任。
Route Registration 和 Route Matching 不一樣
這裡開始有一個很重要的區分。
Framework 啟動時,我們會做:
註冊 Route。
例如:
/hello → Hello Handler
這時還沒有任何 Browser Request。
只是先告訴 Framework:
未來如果有人要求 /hello,要交給這個 Handler。
而真正 Request 進來後:
GET /hello HTTP/1.1
才開始:
Route Matching
也就是:
Path = /hello
↓
Route Table
↓
找到 Hello Handler
所以:
Route Registration
→ 建立規則
Route Matching
→ 使用規則
Framework 啟動階段和 Request 處理階段,其實是不一樣的。
找到 Handler 後怎麼辦?
假設 Router 找到:
Hello Handler
接下來就是:
執行 Handler。
目前最簡單的 Handler 可以先維持:
沒有參數
執行後回傳 HttpResponse
所以可以想成:
Router
↓
找到 Func
↓
handler()
↓
HttpResponse
這裡就重新接回 Day13 學過的 Delegate。
Handler 不需要自己知道:
Browser 是誰
TCP 怎麼連
NetworkStream 在哪裡
它只要做:
自己的處理邏輯
最後產生結果。
例如:
Hello Handler
↓
Body = Hello!
Products Handler
↓
Body = Products Page
這樣 Handler 才會比較像 Application Logic。
Handler 為什麼不直接 Send?
假設 Handler 自己:
建立 HttpResponse
然後直接:
Send(stream)
看起來好像也可以。
但這樣 Handler 就必須知道:
NetworkStream
也就是又開始碰底層 Server 細節。
更乾淨的設計是:
Handler
↓
只回傳 HttpResponse
Framework
↓
負責真正 Send
這樣 Handler 的責任就是:
決定「要回什麼」。
而 Framework 的責任是:
決定「怎麼送出去」。
這就是 Day15 HttpResponse 出現後,很重要的一個分工。
所以完整流程可以是:
Handler
↓
return HttpResponse
↓
Framework
↓
response.Send(stream)
Handler 和 Browser 之間還隔著 Framework。
找不到 Route 怎麼辦?
如果 Router 收到:
/abc
但 Route Table 裡沒有:
/abc
那 Router 就找不到 Handler。
這時也不能讓 Server 什麼都不做。
Framework 還是要產生:
404 Not Found
所以可以想成:
Router.Match(request)
↓
找到?
├── 有
│ ↓
│ Handler
│ ↓
│ HttpResponse
│
└── 沒有
↓
404 HttpResponse
最後不管哪一種,都統一:
HttpResponse
↓
Send
↓
Browser
這就是一個很漂亮的統一出口。
成功和失敗最後都是 Response
以前:
Route 找得到
↓
走一套邏輯
Route 找不到
↓
另外一套邏輯
但現在開始可以統一成:
Request
↓
最後一定產生 HttpResponse
可能是:
200 OK
也可能是:
404 Not Found
甚至未來可能:
400 Bad Request
500 Internal Server Error
對 Server 而言,最後只需要:
拿到 Response
↓
Send
這樣整條流程會乾淨很多。
MiniWebServer 的角色開始變清楚
做到這裡後,可以重新思考 MiniWebServer。
它不需要理解每個 Handler 在做什麼。
也不需要知道:
Hello 是什麼。
Products 是什麼。
它真正負責的是:
啟動 Listener
等待 Client
讀取 Request
建立 HttpRequest
交給 Framework 處理
取得 HttpResponse
送回 Client
可以整理成:
TCP
↓
HttpRequest
↓
Application Pipeline
↓
HttpResponse
↓
TCP
所以 MiniWebServer 比較像:
HTTP 世界和 Framework 世界之間的入口。
Program.cs 又可以再變簡單一點
如果 Router 和 Server 都拆出去了,Program.cs 理論上只需要:
建立 Server
建立 Router
註冊 Route
啟動
也就是從:
我要告訴程式「每個 Request 怎麼底層處理」
慢慢變成:
我要告訴 Framework「我的 Application 有哪些功能」
例如概念上:
註冊 /hello
註冊 /products
Run
這已經開始接近 Framework 使用體驗。
使用 Framework 和實作 Framework 的差別開始出現
Framework 內部可能知道:
TcpListener
NetworkStream
HTTP Parsing
Dictionary
HttpResponse.Send
但使用 Framework 的 Program.cs 不應該全部知道。
它只需要:
Route
Handler
Run
這時候整個專案開始明顯分成兩個世界。
Framework Internals:
Server
Request
Router
Response
Application:
/hello
/products
其他 Handler
這個分界非常重要。
因為如果未來把 Framework 做成 Library,
其他專案理論上也可以使用同一套 Server、Router、Request、Response。
只需要換掉:
Application Routes
這才開始真正有「Framework 可重用性」的感覺。
Request Processing Flow 到底是什麼?
前面一直講 Request Pipeline。
今天先不加入完整 Middleware,但其實 Router、Handler、Response 本身就已經形成最基本的 Processing Flow。
例如:
Browser
↓
MiniWebServer
↓
HttpRequest.Parse
↓
Router.Match
↓
Handler
↓
HttpResponse
↓
Send
↓
Browser
這就是最小 Request Processing Flow。
後面如果加入 Middleware,只是在這條主線外再包更多步驟。
例如:
Browser
↓
MiniWebServer
↓
HttpRequest
↓
Logging Middleware
↓
Router
↓
Handler
↓
HttpResponse
↓
Logging Middleware
↓
Browser
所以今天先把核心流程接好,比立刻做完整 Middleware 更重要。
Framework 的核心其實開始很小
做到這裡,我突然發現:
一個最小 Web Framework 的核心,其實可以先縮成幾個問題。
第一:
Request 是什麼?
第二:
Request 要交給誰?
第三:
誰來處理?
第四:
最後要回什麼?
也就是:
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
其他功能其實都是在這條主線上慢慢增加。
例如:
Headers
Query
Body
Parameter Binding
Middleware
Exception Handling
都沒有改變最核心的:
Request → Handler → Response
官方怎麼說?
真正的 ASP.NET Core 中,Routing 會將 HTTP Request 與 Endpoint 進行配對。
Endpoint 則代表應用程式中可以被執行的 Request Processing Code。
Request 經過 Pipeline 後,Endpoint 執行,再產生 HTTP Response。
雖然真正 ASP.NET Core 的架構比我們的 Mini Web Framework 複雜非常多,但核心方向仍然可以理解為:
Request
↓
Routing
↓
Endpoint
↓
Response
所以我們現在先把:
Router
Handler
HttpResponse
接起來,正是在建立最小版本的這條核心路徑。
以前的我:
Server 收到網址
↓
if / else
↓
執行某段程式
現在的我:
MiniWebServer
↓
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
↓
MiniWebServer
↓
Browser
也就是說:
Server 不負責所有事情。
它只是讓 Request 進入 Framework,再讓 Response 回到 Client。
真正 Application 的邏輯則存在 Handler 裡。
這讓 Framework、Server、Application 三者的角色開始真正分開。
今天最大的發現是:
拆 Class 並不是結束。
真正重要的是建立「元件之間的資料流」。
例如:
HttpRequest 的輸出
↓
成為 Router 的輸入
Router 的輸出
↓
是 Handler
Handler 的輸出
↓
是 HttpResponse
HttpResponse
↓
再交回 Server
每一個元件不需要知道所有事情。
只需要知道:
我要接收什麼。
我要產生什麼。
這種介面之間的合作,才讓很多小元件變成真正完整的 Framework。
Framework 的核心流程可以先建立成:
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
MiniWebServer 只負責接收 Client、建立 Request,以及最後送出 Response。
這樣 Application Logic 和 HTTP / TCP 底層細節就開始真正分開。
🛠 與 Mini Web Framework 的關聯
Day26 做的是:
把零件拆開。
Day27 則是:
把零件重新接起來。
目前最終成品的核心已經可以想像成:
Program.cs
↓
Register Routes
↓
Run
Framework 內部:
MiniWebServer
↓
HttpRequest
↓
Router
↓
Handler
↓
HttpResponse
當 Browser 打:
/hello
Framework:
收到 Request
↓
解析 /hello
↓
Router 找到 Hello Handler
↓
執行
↓
得到 Hello Response
↓
Send
↓
Browser 顯示 Hello
這就是最基本但完整的 Web Framework Core Flow。
今天的實作目標
Day27 真正實作時,可以集中做幾件事。
第一:
完成 Router 類別。
第二:
讓 Router 可以 Register Route。
第三:
讓 Router 可以根據 Path 找到 Handler。
第四:
讓 MiniWebServer 收到 HttpRequest 後呼叫 Router。
第五:
Handler 回傳 HttpResponse。
第六:
找不到 Route 時建立 404 Response。
第七:
重新測試:
/hello
/products
/abc
確認拆開後整個流程還是正常。
完成之後,應該可以得到:
/hello
↓
Hello!
/products
↓
Products Page
/abc
↓
404 Not Found
外部功能可能跟 Day14 看起來差不多。
但內部架構已經完全不一樣。
這就是最後幾天最重要的事情:
不是一直增加新功能。
而是把前面理解過的東西真正組成 Framework。
結尾
Day26,我們開始把巨大 Program.cs 拆開。
但今天才真正發現:
拆開只是為了讓元件可以用更清楚的方式重新合作。
HttpRequest 告訴 Framework:
這次 Request 是什麼。
Router 告訴 Framework:
誰應該處理它。
Handler 負責:
真正的 Application Logic。
HttpResponse 則描述:
最後要回什麼。
MiniWebServer 把最前面的 TCP 和最後的 TCP 接起來。
整個流程:
Browser
↓
Request
↓
Framework
↓
Handler
↓
Framework
↓
Response
↓
Browser
終於開始真正成形。
從最開始只有:
Console.WriteLine("Hello World");
到現在已經可以畫出一條自己的 Request Processing Flow。
最後三天的工作,就是繼續讓這條流程:
更完整
更乾淨
更像一個真正可以使用的 Mini Web Framework。
目前最核心的:
Request
↓
Router
↓
Handler
↓
Response
已經接起來了。
但前面我們還學過一個非常重要的 Framework 功能:
Middleware。
現在如果想讓每個 Request 都自動經過 Logging,
又不想把 Logging 寫進每一個 Handler,
到底要怎麼把 Middleware 真正插進我們自己的 Framework Pipeline?