iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

昨天已經成功完成:

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

  1. Headers

接著是:

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 輸入:

http://127.0.0.1:5000/hello

結果頁面真的出現:

Hello!

再輸入:

http://127.0.0.1:5000/products

結果變成:

Products Page

這和前幾天最大的不同是:

以前:

Server Console

Hello Handler 被執行了!

今天:

Browser

Hello!

也就是整個 Request / Response 循環第一次真正跑通。

https://ithelp.ithome.com.tw/upload/images/20260815/20183395I9Whp3Ab2Y.png

https://ithelp.ithome.com.tw/upload/images/20260815/20183395RJlo8cQeOX.png

做到這裡,前幾天學過的東西第一次形成完整循環。

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 開啟:

http://127.0.0.1:5000/abc

Browser 成功顯示:

404 Not Found

https://ithelp.ithome.com.tw/upload/images/20260815/20183395jtkjCtdeg0.png


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 物件?


上一篇
Framework 到底怎麼根據 Path 找到正確的程式?
下一篇
為什麼 Framework 需要自己的 Response 物件?
系列文
程式設計沒有告訴你的事:30 天破解每一個 Why16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言