昨天已經成功完成:
Browser
↓
HTTP Request
↓
解析 Path
↓
Routing
↓
找到 Handler
↓
執行 Handler
例如:
/hello
↓
Hello Handler
以及:
/products
↓
Products Handler
但當時 Handler 做的事情只有:
Console.WriteLine("Hello Handler 被執行了!");
結果只會出現在 Server 自己的 Console。
Browser 發出 Request 後,真正等待的其實是 Server 的回答。
所以今天的問題是: Handler 執行完之後,Server 到底要回傳什麼,Browser 才知道該怎麼處理?
以前寫 Web 程式時,我可能只會寫:
return "Hello";
或:
return Ok(data);
Browser 就會自己收到結果。
所以以前很容易產生一種感覺:
return 之後,Framework 好像就會自動把東西送到 Browser。
但現在沒有 ASP.NET Core 幫忙。
如果只是:
string body = "Hello!";
這只是 Server 程式裡的一個字串,Browser 根本不會自動拿到它。
因此中間一定還缺一件事情: Server 必須真的把資料寫回網路連線。
昨天 Route Table 使用:
Dictionary<string, Action>
例如:
["/hello"] = () =>
{
Console.WriteLine("Hello Handler 被執行了!");
}
Action 可以執行一段程式,但不會回傳結果。
今天改成:
Dictionary<string, Func<string>>
例如:
var routes = new Dictionary<string, Func<string>>
{
["/hello"] = () =>
{
return "Hello!";
},
["/products"] = () =>
{
return "Products Page";
}
};
這裡第一次使用:
Func<string>
目前可以先把它理解成:
一段執行之後,會產生 string 結果的程式。
所以:
string body = handler();
執行 /hello Handler 後會得到:
body = "Hello!"
而 /products 則會得到:
body = "Products Page"
但是到這裡,Browser 還是收不到。
因為:
Handler
↓
產生 string
和:
Server
↓
把資料送回 Browser
仍然是兩件不同的事情。
接著,我自己建立了一段 Response:
string response =
"HTTP/1.1 200 OK\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
$"Content-Length: {Encoding.UTF8.GetByteCount(body)}\r\n" +
"\r\n" +
body;
第一次看到這段時,會覺得它只是一大串文字。
但其實它有非常明確的結構:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 6
Hello!
可以拆成:
HTTP Response
│
├── Status Line
│
├── Headers
│
├── 空白行
│
└── Body
1.Status Line
第一行:
HTTP/1.1 200 OK
就是這次 Response 的 Status Line。
可以拆成:
HTTP/1.1 200 OK
│ │ │
│ │ └─ Reason Phrase
│ │
│ └─ Status Code
│
└─ HTTP Version
今天最重要的是:
200 OK
它代表這次 Request 成功被處理。
所以當 /hello 找得到 Handler 時,我們就回:
HTTP/1.1 200 OK
接著是:
Content-Type: text/plain; charset=utf-8
Content-Length: 6
這些就是 Response Headers。
Content-Type
Content-Type: text/plain; charset=utf-8
是在告訴 Client:
我接下來傳給你的內容是純文字,而且使用 UTF-8 字元編碼。
所以 Browser 收到:
Hello!
時,知道目前可以把它當作文字處理。
Content-Length
Content-Length: 6
表示 Body 的長度。
我們使用:
Encoding.UTF8.GetByteCount(body)
取得的是 UTF-8 編碼後的 byte 數量。
這一點不能單純使用:
body.Length
取代。
因為 HTTP 傳輸的是 bytes,而某些字元在 UTF-8 中不一定只佔一個 byte。
例如英文:
Hello!
剛好是 6 個字元,也剛好是 6 bytes。
但如果未來 Body 出現中文,就不能假設: 字元數 = byte 數
所以用:
Encoding.UTF8.GetByteCount(body)
會更符合我們實際送出去的資料長度。
3.空白行
Headers 結束後,我們加入:
"\r\n"
形成一個空白行。
也就是:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 6
← 空白行
Hello!
這個空白行非常重要。
它是在告訴 HTTP 接收端:
Headers 到這裡結束,後面開始是 Body。
4.Body
最後才是真正想讓 Browser 看到的內容:
Hello!
也就是:
body
因此完整概念可以整理成:
HTTP/1.1 200 OK ← Status Line
Content-Type: text/plain... ← Header
Content-Length: 6 ← Header
← 空白行
Hello! ← Body
把 Response 寫回 NetworkStream
組出 Response 之後,我們還需要真正送出去。
先將字串轉成 bytes:
byte[] responseBytes =
Encoding.UTF8.GetBytes(response);
再使用:
stream.Write(
responseBytes,
0,
responseBytes.Length
);
把這些 bytes 寫進 NetworkStream。
Microsoft 的 NetworkStream 文件說明,它提供透過 stream socket 傳送及接收資料的功能;在 .NET 的 TCP 範例中,也會透過 NetworkStream 將資料寫回已連線的 Client。
這裡讓我重新理解 Day10 的 NetworkStream。
之前:
Browser
↓
NetworkStream
↓
Server Read()
是把 Browser 的 Request 讀進來。
今天則反方向:
Server
↓
NetworkStream
↓
Write()
↓
Browser
同一條 TCP Connection 可以用來交換資料。
第一次在 Browser 看見自己的 Response
接著在 Browser 輸入:
結果頁面真的出現:
Hello!
再輸入:
http://127.0.0.1:5000/products
結果變成:
Products Page
這和前幾天最大的不同是:
以前:
Server Console
↓
Hello Handler 被執行了!
今天:
Browser
↓
Hello!
也就是整個 Request / Response 循環第一次真正跑通。


做到這裡,前幾天學過的東西第一次形成完整循環。
Browser
│
│ HTTP Request
│ GET /hello HTTP/1.1
▼
Server
│
├─ 接收 TCP Connection
│
├─ Read Request
│
├─ Parse Request
│
├─ Routing
│
├─ 找到 Hello Handler
│
├─ handler()
│
├─ body = "Hello!"
│
├─ 組成 HTTP Response
│
│ HTTP/1.1 200 OK
│ Content-Type: ...
│
│ Hello!
│
▼
NetworkStream.Write()
│
▼
Browser
│
└─ 顯示 Hello!
這是目前整個 Mini Web Framework 最重要的一次串接。
目前:/hello 和 /products
都有 Route。
但如果輸入:
/abc
Route Table 裡根本找不到。
之前我們只是:
Console.WriteLine("找不到對應的 Route");
但這只告訴 Server 自己。Browser 仍然需要收到一個 Response。
所以今天又做了第二種 HTTP 狀態。
當找不到 Route 時:
else
{
string body = "404 Not Found";
string response =
"HTTP/1.1 404 Not Found\r\n" +
"Content-Type: text/plain; charset=utf-8\r\n" +
$"Content-Length: {Encoding.UTF8.GetByteCount(body)}\r\n" +
"\r\n" +
body;
byte[] responseBytes =
Encoding.UTF8.GetBytes(response);
stream.Write(
responseBytes,
0,
responseBytes.Length
);
}
這次最大的差別就是:
HTTP/1.1 200 OK
變成:
HTTP/1.1 404 Not Found
然後在 Browser 開啟:
Browser 成功顯示:
404 Not Found

200 和 404 到底差在哪?
今天第一次真正自己控制兩個 HTTP Status Code。
找得到 Route
HTTP/1.1 200 OK
代表 Request 已成功處理。
找不到 Route
HTTP/1.1 404 Not Found
代表要求的資源不存在。
Microsoft .NET 的 HttpStatusCode 官方文件中,也將 NotFound 對應為 404,表示要求的資源不存在於 Server。
ASP.NET Core 官方錯誤處理文件同樣將 404 - Not Found 列為 HTTP error status code;Routing 無法找到對應結果時,也可能產生 404 Response。
所以今天自己手寫的:
HTTP/1.1 404 Not Found
和真正 Framework 使用的 HTTP 語意是一致的。
昨天到 ASP.NET Core Routing 負責把進來的 HTTP Request 與可執行 Endpoint 配對並分派。
而今天則補上了 Routing 後面的另一半:
找到 Endpoint / Handler
↓
執行
↓
產生 Response
另外,Microsoft 的 ASP.NET Core 錯誤處理文件也指出,HTTP 400–599 的錯誤狀態可以作為 Response Status Code;例如 404 Not Found。
而底層網路傳輸部分,NetworkStream 則提供透過 Stream Socket 傳送與接收資料的能力。
把這三件事情接起來:
Routing
↓
Handler
↓
Response
↓
NetworkStream
↓
Client
就和今天實驗出來的流程一致。
以前的我:
return "Hello"
↓
Browser 自己就會收到
現在的我:
Handler
↓
產生 Body
↓
建立 HTTP Response
↓
Status Line
↓
Headers
↓
空白行
↓
Body
↓
轉成 bytes
↓
NetworkStream.Write()
↓
Browser
也就是說:
Handler 產生結果,和 Server 把結果傳回 Client,是兩個不同層次的工作。
Framework 幫我們把這些步驟包裝起來,所以平常使用 ASP.NET Core 時才會覺得只要:
return "Hello";
事情就全部完成了。
今天最大的發現是:
Browser 和 Server 之間交換的,不是「C# 物件」,而是按照 HTTP 規則組成的資料。
以前看到:
return Ok();
或:
return "Hello";
會覺得這就是 Response。
但現在知道 Framework 還必須把這些高階寫法轉換成某種真正可以在網路上傳輸的 HTTP Response。
最後送出去的仍然是 bytes。
C# Handler Result
↓
HTTP Response
↓
bytes
↓
TCP
↓
Browser
這和 Day2~Day7 一直在看的概念竟然又有點類似:我們平常寫的高階抽象,最後都要被轉成更底層真正能處理的形式。
目前 Mini Web Framework 已經走到:
Day10
TCP Server
↓
接受連線
Day11
↓
讀到 HTTP Request
Day12
↓
解析 Request
Day13
↓
Routing
↓
Handler
Day14
↓
產生 HTTP Response
↓
送回 Browser
現在整個基本 Web 流程已經第一次形成完整閉環:
┌──────── Browser ────────┐
│ │
│ HTTP Request │
│ ↓ │
└────────── Server ───────┘
↓
Parsing
↓
Routing
↓
Handler
↓
HTTP Response
↓
┌──────── Browser ────────┐
│ │
│ 顯示 Response │
│ │
└─────────────────────────┘
這對 Mini Web Framework 是很重要的一步。
因為現在它已經不只是: 「可以接收到 Browser 傳來的東西。」
而是開始真正具有:
Request → Processing → Response
這個 Web Framework 最基本的生命週期。
目前 Handler 只想做:
return "Hello!";
但 Server 還得另外處理:
Status Code
Content-Type
Content-Length
HTTP 格式
Encoding
NetworkStream.Write
所以新的問題變成:
Framework 能不能把「建立 HTTP Response」這件事情也包起來?
也就是讓 Handler 不必每次都知道這麼多 HTTP 細節。
這會是下一步非常自然的方向:
為什麼 Framework 需要自己的 Response 物件?