昨天,我第一次建立了自己的 HttpResponse。
原本所有 Response 相關的資訊都散落在 Program.cs:
StatusCode
StatusText
ContentType
Body
HTTP 格式
Encoding
NetworkStream.Write()
最後則整理成:
HttpResponse
├── StatusCode
├── StatusText
├── ContentType
├── Body
└── Send()
做到這裡後,再回頭看 Request,突然覺得很奇怪。
因為目前 Request 還是這樣處理:
先把收到的 bytes 轉成字串,再找出第一行,接著用空白切開,最後分別取得 Method、Path 和 Version。
也就是說:
Response 已經是一個有結構的物件,但 Request 還是一大堆散落的字串處理。
那既然 Framework 可以把 Response 包成 HttpResponse,Request 為什麼不能也變成 HttpRequest?
所以今天的問題就是:
Framework 為什麼需要自己的 Request 物件?
💭 我原本以為……
以前使用 ASP.NET Core 時,很習慣直接寫:
Request.Method
或:
Request.Path
感覺 Method、Path、Headers、Query、Body,好像本來就已經存在於 Request 裡。
但前幾天自己從 Socket 開始做後,才知道 Browser 真正送來的一開始其實只是網路上的資料。
例如:
GET /products HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: …
Accept: …
我們之前甚至還必須自己:
收到 bytes
↓
轉成 string
↓
找到 Request Line
↓
Split
↓
取得 Method / Path / Version
所以現在才發現:
Request.Path 並不是 Browser 直接傳來的一個 C# Property。
它是 Framework 先理解原始 HTTP Request 之後,才整理出來的結果。
回頭看現在的 Program.cs
目前 Program.cs 大概同時負責:
Program.cs
├── 等待 TCP Connection
├── 讀取 bytes
├── bytes → string
├── 找 Request Line
├── Split Request Line
├── 取得 Method
├── 取得 Path
├── 取得 Version
├── Routing
└── 回傳 HttpResponse
昨天我們才剛把 Response 的責任拆出去。
現在 Request 這邊卻還全部擠在 Program.cs。
尤其是目前的程式只有我們自己知道:
parts[0] → Method
parts[1] → Path
parts[2] → Version
如果未來其他地方也想取得 Path,難道每個地方都要重新記得 parts[1] 嗎?
顯然不太合理。
🧠 如果有 HttpRequest,會變成什麼?
我想要的是,把目前的:
GET /products HTTP/1.1
經過解析後變成:
HttpRequest
├── Method = GET
├── Path = /products
└── Version = HTTP/1.1
這樣其他程式就不必知道 Request Line 怎麼 Split。
只需要:
request.Method
request.Path
request.Version
例如 Routing 現在是拿 path 去查 Route Table。
未來就可以變成:
request.Path
也就是:
原始 HTTP
↓
HttpRequest
↓
Routing
而不是:
原始 HTTP
↓
一堆 string / array
↓
Routing
HttpRequest 第一版應該包含什麼?
我們目前其實還不用一次把 Request 做得很完整。
先從 Day12 已經理解的三個東西開始:
HttpRequest
├── Method
├── Path
└── Version
例如 Browser 傳:
GET /hello HTTP/1.1
最後得到:
Method = GET
Path = /hello
Version = HTTP/1.1
這和我們 Day12 做過的事情一模一樣。
差別只是:
以前解析結果散落在三個變數,現在開始把它們視為「同一個 Request 的資料」。
那 Parsing 應該放在哪裡?
這又產生另一個問題。
如果建立 HttpRequest,但 Program.cs 還是自己 Split,那其實只完成一半。
比較理想的方向應該是:
raw HTTP Request
↓
HttpRequest.Parse(…)
↓
HttpRequest
例如未來可能會變成:
HttpRequest request = HttpRequest.Parse(rawRequest);
這樣 Program.cs 就不需要知道:
第一行怎麼取得
空白怎麼拆
哪個 index 是 Path
這些 Parsing 細節。
只要知道:
把原始 Request 交給 HttpRequest,最後拿回整理好的 Request 物件。
為什麼 Parse() 可以是 static?
未來實作時,我預計會寫成類似:
public static HttpRequest Parse(string rawRequest)
這裡的 static 代表這個方法是屬於 HttpRequest 這個 Type,而不是某一個已經存在的 HttpRequest Instance。
所以可以直接:
HttpRequest.Parse(rawRequest)
而不是:
new HttpRequest().Parse(…)
放在今天的情境就很好理解:
現在還沒有 HttpRequest 物件
↓
手上只有 rawRequest
↓
呼叫 HttpRequest.Parse(…)
↓
建立 HttpRequest
所以 Parse() 很適合先作為「從原始資料建立 Request」的入口。
📚 官方怎麼說?
完成目前的設計思考後,我去看了 ASP.NET Core 的 HttpRequest。
Microsoft 官方文件中,HttpRequest 代表一個 HTTP Request,並提供 Request 相關資訊,其中包含 Method、Path、Headers、Host 等成員。
這和我們目前想做的方向非常接近。
我們現在預計先做:
Method
Path
Version
真正 ASP.NET Core 的 Request 當然遠遠不只這些。
它還有例如:
Headers
Host
Query
Scheme
Body
Cookies
ContentType
ContentLength
……
所以今天自己設計的 HttpRequest,不是要複製 ASP.NET Core。
而是從最小版本理解:
Framework 為什麼需要先把原始 HTTP 資料,整理成 Request 抽象。
為什麼不直接永遠使用原始字串?
理論上當然可以。
例如每次想知道 Path,就重新從 rawRequest 裡找第一行,再 Split,最後取得第二個位置。
確實也能跑。
但問題是,其他程式會開始依賴 HTTP 原始格式。
例如:
Router
↓
知道 HTTP 格式
Handler
↓
知道 HTTP 格式
其他功能
↓
也知道 HTTP 格式
這樣 HTTP Parsing 的細節就會散落到整個 Framework。
如果有 HttpRequest,則可以變成:
Raw HTTP
↓
HttpRequest Parsing
↓
HttpRequest
├── Method
├── Path
└── Version
↓
其他元件使用
也就是:
只有負責 Parsing 的地方需要理解原始 HTTP 格式。
其他地方只需要理解 HttpRequest。
🧠 今天的認知更新
以前的我:
Browser 傳 Request
↓
Framework 得到 Request
現在這句話可以拆得更細:
Browser
↓
TCP
↓
bytes
↓
原始 HTTP Request
↓
Parsing
↓
HttpRequest
↓
Routing / Handler
也就是說:
Request 物件其實是 Framework 建立出來的一層抽象。
Browser 並沒有把 new HttpRequest() 傳過來。
Browser 傳的是符合 HTTP 規則的資料。
Framework 收到後,再把它轉成開發者比較方便使用的物件。
Request 與 Response 開始對稱了
做到 Day15 和 Day16,我發現整個架構開始慢慢變得比較整齊。
之前:
Request → 一大堆 string
Response → 一大堆 string
Day15:
Request → 一大堆 string
Response → HttpResponse
Day16 想完成:
Request → HttpRequest
Response → HttpResponse
也就是:
Browser
↓
Raw Request
↓
HttpRequest
↓
Framework
↓
HttpResponse
↓
Raw Response
↓
Browser
這個結構突然比之前清楚很多。