昨天研究 Query String 時,我們看到 Browser 可以把一些資料直接放在網址裡。
例如:
/search?q=book
可以理解成:
Path = /search
Query = q → book
所以如果只是很簡單的搜尋條件、頁碼或篩選資訊,放在網址後面似乎就可以了。
但這時我突然想到一個問題。
平常填寫登入表單時,可能會輸入:
username
password
建立資料時,也可能會傳:
name
email
age
如果今天傳的是一大段 JSON:
{
"name": "Andy",
"age": 20
}
這些資料總不可能全部塞在網址後面吧?
那它們到底放在哪裡?
所以今天想理解的問題就是:
POST 傳來的資料到底藏在哪裡?
Request Body 又是什麼?
我原本以為……
以前寫 Web API 時,我很習慣看到類似:
POST /users
然後 Controller 或 Handler 就可以直接取得使用者傳來的資料。
尤其使用 ASP.NET Core 時,可能只需要定義一個 Model,就可以很自然地取得 JSON 裡的內容。
所以以前很容易覺得:
Client 傳 JSON
↓
Server 得到 C# Object
中間好像沒有什麼特別的事情。
但現在自己從 TCP 和 HTTP 開始拆之後,就知道事情不可能這麼簡單。
就像 Request.Path 不是 Browser 直接傳來的一個 C# Property 一樣,
Request Body 也不是 Browser 直接傳來一個 C# Object。
Browser 真正傳過來的仍然只是符合 HTTP 格式的資料。
Framework 必須自己知道:
Body 從哪裡開始?
Body 有多長?
Body 是什麼格式?
最後要怎麼把它轉成程式真正能使用的資料?
Request Body 到底在哪裡?
前面 Day11 第一次看到 HTTP Request 時,我們已經知道 Request 大致上可以理解成:
Request Line
↓
Headers
↓
空白行
↓
Body(不一定有)
例如一個很簡化的 POST Request 可能長得像:
POST /users HTTP/1.1
Host: 127.0.0.1:5000
Content-Type: application/json
Content-Length: ...
{"name":"Andy"}
如果把它拆開:
POST /users HTTP/1.1
↓
Request Line
Host: ...
Content-Type: ...
Content-Length: ...
↓
Headers
空白行
↓
代表 Headers 結束
{"name":"Andy"}
↓
Body
所以 Body 並不是另外偷偷傳一份資料。
它本來就是 HTTP Request 的其中一部分。
只是它位在 Headers 後面的空白行之後。
GET 和 POST 的差別就是有沒有 Body 嗎?
這裡也很容易產生誤解。
看到 GET 常常只有:
GET /hello HTTP/1.1
而 POST 常常帶著 Body,就可能會直覺認為:
GET = 沒有 Body
POST = 有 Body
但這樣理解太簡化了。
比較好的方式是:
HTTP Method 描述這次 Request 想做什麼,而 Body 則是 Request 可以攜帶的一部分資料。
在實際 Web 開發中,POST、PUT、PATCH 等 Method 很常使用 Body 傳送資料,所以我們會特別容易把 POST 和 Body 聯想到一起。
今天先把重點放在最常見的情況:
POST
↓
Request Body
↓
傳送要建立或處理的資料
例如:
POST /users
↓
Body
↓
{"name":"Andy"}
為什麼不全部放在 Query String?
假設我要建立一個使用者。
如果全部放網址:
/users?name=Andy&age=20&email=...
資料少的時候可能看起來還可以。
但如果資料越來越多:
姓名
年齡
Email
地址
設定
其他欄位
網址就會變得很長。
如果資料本身還有比較複雜的結構,例如:
{
"name": "Andy",
"address": {
"city": "Taipei",
"district": "Xinzhuang"
}
}
用 Query String 表達就會越來越麻煩。
所以 HTTP Request 提供 Body,讓 Client 可以在 Request 裡另外攜帶內容。
可以先簡單理解成:
Path
→ 我要對哪個資源做事情
Query
→ 這次 Request 額外帶的一些條件
Body
→ 這次 Request 真正要傳送的一份內容
當然,真正 HTTP 的使用情境比這更彈性,但目前用這個方式理解最清楚。
Content-Type 為什麼突然變重要?
假設 Body 是:
{"name":"Andy"}
Server 收到後只看到一串 bytes。
那它怎麼知道這是一段 JSON?
靠的就是 Header 裡的:
Content-Type
例如:
Content-Type: application/json
就是在告訴 Server:
後面的內容要按照 JSON 的形式理解。
如果是一般文字,可能會看到:
Content-Type: text/plain
如果是 HTML:
Content-Type: text/html
所以 Day17 學過的 Headers 到今天開始真的變得很重要。
以前看到:
Content-Type
可能只是覺得:
這是一個 Header。
現在才知道它可能直接影響 Framework:
要怎麼理解 Body。
整個概念可以整理成:
Request Body
↓
先看 Content-Type
↓
決定這份資料是什麼格式
↓
再進一步解析
例如:
Content-Type: application/json
↓
Body 當成 JSON
Content-Type: text/plain
↓
Body 當成純文字
Content-Length 又是什麼?
另外一個很重要的 Header 是:
Content-Length
它表示 Message Body 的長度是多少 bytes。
這件事對我們現在自己做 TCP Server 特別重要。
因為目前程式一直是:
建立一個 buffer
↓
stream.Read(...)
↓
拿到一些 bytes
很容易會產生一個錯覺:
呼叫一次 Read,就等於拿到一整份 HTTP Request。
但其實 TCP 本質上是 byte stream。
一次 Read 到多少資料,並不保證剛好等於完整 HTTP Request 的大小。
這件事情在 Request 很短時可能暫時感覺不到。
例如:
GET /hello HTTP/1.1
資料量很小,第一次 Read 很可能就拿到了我們想看的東西。
但是如果 POST Body 很大:
POST /users HTTP/1.1
...
Content-Length: 5000
後面還有 5000 bytes 的 Body。
那一次 Read 不一定就把整個 Body 收完。
所以 Server 不能單純想:
Read 一次
↓
Request 完成
而是必須知道:
Headers 到哪裡結束?
Body 應該有多長?
目前已經收了多少?
還要不要繼續 Read?
這也讓我第一次開始看見:
真正做一個 HTTP Parser,其實比單純 Split 字串複雜很多。
為什麼 Day17 的 Header 解析很重要?
前幾天可能會覺得:
Headers 那麼多,我真的需要全部理解嗎?
但到了 Request Body 後,就開始看到 Headers 和 Body 其實有直接關係。
例如:
Content-Type
↓
告訴 Framework Body 是什麼格式
Content-Length
↓
告訴 Framework Body 有多少 bytes
所以整個 Request Parsing 其實不是每一部分互不相干。
而是:
Request Line
Headers
Body
會互相影響後續處理方式。
Framework 要怎麼找到 Body?
目前先不進入完整實作,但概念上可以想成:
拿到 Raw HTTP Request
↓
找到 Headers 結束的位置
↓
也就是空白行
↓
空白行後面的資料
↓
Body
HTTP/1.1 常見的 Headers 和 Body 之間會用一個空白行分隔。
我們之前一直看到的:
\r\n\r\n
其實就很重要。
可以想成:
Request Line + Headers
↓
\r\n\r\n
↓
Body
因此未來 HttpRequest.Parse() 除了解析:
Method
Path
Version
Headers
Query
還會需要再往下一步:
Body
最後可能變成:
HttpRequest
├── Method
├── Path
├── Version
├── Headers
├── Query
└── Body
HttpRequest 又變得更完整了
Day16 時,我們想像的 HttpRequest 還很簡單:
Method
Path
Version
Day17:
Method
Path
Version
Headers
Day18:
Method
Path
Version
Headers
Query
到了 Day19:
Method
Path
Version
Headers
Query
Body
這幾天一路拆下來後,我才開始理解:
一個看起來很普通的 Request 物件,背後其實已經替開發者整理了很多 HTTP 細節。
平常只看到:
request.Body
但在它出現以前,Framework 可能已經處理了:
TCP bytes
↓
HTTP 格式
↓
Headers
↓
Body 邊界
↓
Body 長度
↓
最後才建立 request.Body
官方怎麼說?
HTTP Message 可以包含 Header Fields 和 Message Content,而 Content-Length 可以用來表示內容長度。
在 ASP.NET Core 中,HttpRequest 也提供 Body、ContentType、ContentLength 等 Request 相關資訊。
這和我們這幾天逐步建立 HttpRequest 的方向很接近。
只是我們現在做的是最小化的學習版本。
真正的 Framework 還必須處理更多細節,例如不同的 Request Framing、Streaming、大型 Body、安全限制等問題。
所以今天的目的不是直接做出完整 HTTP Server。
而是理解:
為什麼平常一個看似簡單的 request.Body,背後其實需要很多底層工作。
以前的我:
POST
↓
傳資料
↓
Server 得到資料
現在的我:
Client
↓
POST Request
↓
TCP bytes
↓
Request Line
↓
Headers
↓
空白行
↓
Body
↓
Framework Parsing
↓
request.Body
↓
再依照 Content-Type 進一步理解資料
也就是說:
Body 不是突然出現在 Framework 裡的一個變數。
它原本就是 HTTP Request 中的一段資料。
Framework 的工作是找到它、讀完整它,再整理成程式容易使用的形式。
今天最大的發現是:
TCP 的 Read 和 HTTP 的 Request 並不是同一個邊界。
以前很容易把:
stream.Read()
理解成:
讀取一個 Request。
但現在開始知道:
stream.Read()
比較接近:
這次從 TCP Stream 讀到了「一些 bytes」。
至於這些 bytes:
是不是完整 Request?
Headers 有沒有讀完?
Body 有沒有讀完?
是 HTTP 層還需要自己判斷的事情。
所以:
TCP
↓
負責傳 bytes
HTTP
↓
定義這些 bytes 應該怎麼組成 Request
Framework
↓
負責把這些資料解析成我們能使用的物件
這三個層次又更清楚了一點。
今天的答案:
Client 傳送 POST Request 時,要傳送的資料可以放在 HTTP Request Body。
與 Mini Web Framework 的關聯
目前我們想像中的 HttpRequest 已經逐漸變成:
HttpRequest
├── Method
├── Path
├── Version
├── Headers
├── Query
└── Body
而整個流程也開始變成:
Browser
↓
TCP bytes
↓
HTTP Request
↓
HttpRequest.Parse()
↓
Method
Path
Headers
Query
Body
↓
Routing
↓
Handler
↓
HttpResponse
↓
Browser
這也代表 Mini Web Framework 開始從:
「可以收到一些 HTTP 文字」
往:
「真的理解一個 HTTP Request」
前進。
今天暫時不實作
今天和前幾天一樣,先把 Request Body 的概念想清楚。
之後補實作時,不會只是單純在 rawRequest 上做一次 Split,就假裝完成 Body Parsing。
因為現在已經知道:
TCP Read 不保證一次取得整個 Request。
所以實作時要更小心處理:
Headers 是否完整
Body 應該有多長
目前已經讀到多少 bytes
這會比前面的 Method、Path、Query Parsing 更接近真正的 HTTP Server 問題。
結尾
今天原本只是想知道:
POST 的資料到底放在哪裡?
結果一路追下去,卻又回到了 TCP。
Request Body 本身其實不難理解。
它就是 HTTP Request 中可以攜帶內容的部分。
真正困難的是:
Server 到底怎麼知道它讀完整了?
做到這裡我才更清楚理解,HTTP Framework 替我們處理的不只是「把 JSON 轉成 Object」。
在那之前,它還必須先把網路收到的 bytes,正確辨認成一個完整的 HTTP Request。
然後才有:
Headers
Body
Content-Type
Content-Length
這些我們平常很習慣使用的資訊。
也就是:
TCP 給 Framework bytes
HTTP 告訴 Framework這些 bytes 的格式
Framework 再把它們整理成 HttpRequest
最後,開發者才可以很簡單地取得 request.Body。
現在 Request 已經慢慢完整了。
我們知道:
Method
Path
Headers
Query
Body
但還有一個問題。
假設 Body 裡收到的是:
{"name":"Andy","age":20}
目前它對 Framework 來說可能仍然只是一串文字或 bytes。
那為什麼平常 ASP.NET Core 可以直接讓我們拿到一個 C# Model?
例如:
User user
裡面直接就有:
Name = "Andy"
Age = 20
中間到底發生了什麼?