iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

昨天我們第一次成功讓整個流程完整跑通:

Browser

HTTP Request

Server

Routing

Handler

HTTP Response

Browser

而且 Browser 真的可以看到:

Hello!

或:

Products Page

甚至不存在的 Route 也可以收到:

404 Not Found

但做到這裡後,我很快發現一個問題。

每次要回傳 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;


byte[] responseBytes =
    Encoding.UTF8.GetBytes(response);


stream.Write(
    responseBytes,
    0,
    responseBytes.Length
);

如果找不到 Route,又要再做一次差不多的事情。

那如果未來有幾十個 Handler,難道每一個地方都要知道:

Status Line
Content-Type
Content-Length
Encoding
NetworkStream

嗎?

所以今天的問題變成:

Framework 為什麼需要自己的 HttpResponse 物件?

我原本以為以前使用 ASP.NET Core 時,我很習慣看到:

return Ok();

或是:

return "Hello";

感覺 Response 好像本來就是一個完整的東西。

但 Day14 自己手刻之後才知道,底層其實要處理:

HTTP/1.1 200 OK
Content-Type: ...
Content-Length: ...

Hello!

再轉成:

bytes

最後透過:

stream.Write(...)

真的送出去。

所以 Framework 所謂的 Response,其實不是憑空出現的。

它應該是把這些原本散落的資料與行為:

Status Code
Status Text
Content Type
Body
建立 HTTP 格式
轉換 bytes
寫入網路

整理到一個更好使用的抽象裡。


今天第一次在 Program.cs 之外,建立自己的 Framework 類別:

MiniWebFramework
├── Program.cs
└── HttpResponse.cs

HttpResponse.cs 一開始先寫:

public class HttpResponse
{
    public int StatusCode { get; set; }


    public string StatusText { get; set; } = "";


    public string ContentType { get; set; }
        = "text/plain; charset=utf-8";


    public string Body { get; set; } = "";
}

第一次看到這個 Class 時,其實它還沒有什麼神奇的功能,它只是把原本散落在 HTTP Response 字串中的資訊整理起來。

例如原本:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8

Hello!

現在可以對應成:

HttpResponse

├── StatusCode = 200

├── StatusText = "OK"

├── ContentType = "text/plain; charset=utf-8"

└── Body = "Hello!"

這裡第一次感覺到:

Class 可以不只是代表某個「實體」,也可以用來整理一組屬於同一件事情的資料。

Microsoft 的 C# 官方文件也說明,Class 可以包含 Properties 與 Methods,而 Property 可以提供讀取或設定資料的公開介面。


例如:

public int StatusCode { get; set; }

代表這個 HttpResponse 物件有一個:

StatusCode

可以讀取,也可以設定。

因此:

var response = new HttpResponse
{
    StatusCode = 200,
    StatusText = "OK",
    Body = "Hello!"
};

比起直接操作:

"HTTP/1.1 200 OK\r\n..."

更容易看出每個資料真正代表什麼。

這也是今天第一個很重要的改變:

原本

一大串 HTTP 字串

現在

有名稱、有意義的資料


Handler 不再只回傳 string

Day14 的 Route Table 是:

var routes =
    new Dictionary<string, Func<string>>
{
    ["/hello"] = () =>
    {
        return "Hello!";
    },


    ["/products"] = () =>
    {
        return "Products Page";
    }
};

也就是:

Path

Handler

string

但現在有了自己的:

HttpResponse

所以改成:

var routes =
    new Dictionary<string, Func<HttpResponse>>
{
    ["/hello"] = () =>
    {
        return new HttpResponse
        {
            StatusCode = 200,
            StatusText = "OK",
            Body = "Hello!"
        };
    },


    ["/products"] = () =>
    {
        return new HttpResponse
        {
            StatusCode = 200,
            StatusText = "OK",
            Body = "Products Page"
        };
    }
};

這次 Handler 回傳的不再只是:

"Hello!"

而是:

HttpResponse


Func 又代表什麼?

前幾天我們已經用過:

Func

今天變成:

Func

差別只是:

Func
→ 執行後回傳 string
Func
→ 執行後回傳 HttpResponse

Microsoft 官方 Func 文件也說明,它可以保存一個「沒有輸入參數、執行後回傳 TResult」的方法參考。

所以:

HttpResponse result = handler();

代表:

執行 Handler

取得 HttpResponse

這樣 Handler 就不只決定:

Body 是什麼

也可以一起決定:

Status Code 是什麼
Content Type 是什麼


如果找不到 Route,原本 Day14 是直接手動建立完整 HTTP 字串。

今天則改成:

result = new HttpResponse
{
    StatusCode = 404,
    StatusText = "Not Found",
    Body = "404 Not Found"
};

這裡我開始看到一個很重要的差別。

不管成功:

200 OK

或失敗:

404 Not Found

對 Program.cs 而言,最後拿到的都是:

HttpResponse

因此:

找到 Route

HttpResponse

和:

找不到 Route

HttpResponse

最後就可以走同一套流程。

https://ithelp.ithome.com.tw/upload/images/20260816/20183395rA9o5cK6I0.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395VRhMjCRsDD.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395jsvZlOfDpd.png

https://ithelp.ithome.com.tw/upload/images/20260816/201833957onx19zt1m.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395GNlPi5c8dT.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395h0suayWmZ4.png


原本 Day14 成功和失敗各自都有:

Encoding.UTF8.GetBytes(...)
stream.Write(...)

今天先整理成:

HttpResponse result;


if (routes.TryGetValue(
    path,
    out Func<HttpResponse>? handler))
{
    result = handler();
}
else
{
    result = new HttpResponse
    {
        StatusCode = 404,
        StatusText = "Not Found",
        Body = "404 Not Found"
    };
}

然後不管結果是哪一種,都統一做:

string response =
    $"HTTP/1.1 {result.StatusCode} {result.StatusText}\r\n" +
    $"Content-Type: {result.ContentType}\r\n" +
    $"Content-Length: {Encoding.UTF8.GetByteCount(result.Body)}\r\n" +
    "\r\n" +
    result.Body;


byte[] responseBytes =
    Encoding.UTF8.GetBytes(response);


stream.Write(
    responseBytes,
    0,
    responseBytes.Length
);

這次程式已經比昨天少了一些重複。

但是問題仍然存在: 為什麼 Program.cs 還要知道 HTTP Response 是怎麼組出來的?

資料整理好了,但責任還沒有真的拆乾淨

現在:

HttpResponse

已經負責保存:

StatusCode
StatusText
ContentType
Body

但真正建立 HTTP 格式的程式還在:

Program.cs

也就是:

Program.cs

├── Routing
├── 執行 Handler
├── 建立 HttpResponse
├── 組 HTTP 字串
├── Encoding
└── NetworkStream.Write()

這其實還是有點奇怪。

Program.cs 現在明明已經拿到一個:

HttpResponse

照理說,應該不用再知道:

HttpResponse 要怎麼把自己變成真正能送出去的 HTTP 資料,所以我們繼續做第二次重構。

這次把 HttpResponse.cs 改成:

using System.Net.Sockets;
using System.Text;


public class HttpResponse
{
    public int StatusCode { get; set; }


    public string StatusText { get; set; } = "";


    public string ContentType { get; set; }
        = "text/plain; charset=utf-8";


    public string Body { get; set; } = "";


    public void Send(NetworkStream stream)
    {
        string response =
            $"HTTP/1.1 {StatusCode} {StatusText}\r\n" +
            $"Content-Type: {ContentType}\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
        );
    }
}

最大的變化就是多了: public void Send(NetworkStream stream)

Send() 做了什麼?

它把昨天 Day14 學過的三件事情全部搬進來。

  1. 建立 HTTP Response
    string response = ...
  2. 轉成 bytes
    byte[] responseBytes =
    Encoding.UTF8.GetBytes(response);
  3. 寫回 Client
    stream.Write(
    responseBytes,
    0,
    responseBytes.Length
    );

Microsoft 官方文件指出,NetworkStream 可以用來透過 Stream Socket 傳送與接收資料,而 Write 會將指定 buffer 中的 bytes 送到網路。

所以今天並沒有改變 HTTP 傳輸的底層原理。

我們只是把:

怎麼送 Response

這件事情從:

Program.cs

移到了:

HttpResponse

裡面。

原本需要:

組 HTTP

Encoding

Write

現在只剩:

result.Send(stream);

也就是:

Program.cs

「這是我要回傳的 HttpResponse」

Send()

至於:

HTTP 要怎麼組?
Content-Length 怎麼算?
怎麼 Encoding?
怎麼 Write?

全部由:

HttpResponse

自己負責。

做到這一步,我開始理解 Framework 很重要的一個概念。

使用 Framework 的人可能只需要知道:

result.Send(stream);

但 Send() 裡面其實藏著:

Status Line
Headers
Body
Encoding
NetworkStream.Write

也就是:

使用者看到

Send()

Framework 裡面

很多真正的實作細節

ASP.NET Core 本身的 HttpResponse 也會把 Response 的 Status Code、Headers 與 Body 等資訊以物件成員提供,而開發者通常不需要自己拼完整的 HTTP Response 字串。

當然,我們今天自己做的:

HttpResponse

只是極度簡化的學習版本,不能直接等同 ASP.NET Core 的完整實作。

但「把 Response 的狀態、內容和相關操作集中起來」這個方向是相通的。


重構完,結果會不會壞掉?

/hello

Request:

GET /hello HTTP/1.1

Console 可以看到:

找到 Route,準備執行 Handler
Response 已送回 Browser!

Browser 仍然正常顯示:

Hello!
/products

Request:

GET /products HTTP/1.1

解析:

Path : /products

找到對應 Handler 後,Response 一樣成功送回 Browser。

Browser 仍然顯示:

Products Page
/abc

Request:

GET /abc HTTP/1.1

因為 Route Table 中不存在:

/abc

所以建立:

404 Not Found

Console 顯示:

找不到 Route,已回傳 404 Not Found

Browser 仍然可以收到正確結果。

所以今天重構之後:

外部結果沒有變,但內部結構變得更清楚。

https://ithelp.ithome.com.tw/upload/images/20260816/20183395JGM9PJheDY.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395EA9EAzEFHI.png

https://ithelp.ithome.com.tw/upload/images/20260816/20183395QuOxLKXnw4.png

https://ithelp.ithome.com.tw/upload/images/20260816/201833950wJzz3yhVw.png


一開始我可能會覺得:

昨天 Browser 已經可以顯示 Hello!,今天改完還是 Hello!,那到底改了什麼?

但這次真正改變的是:

程式的責任分配
重構前
Program.cs

├── 接收 Request
├── Parse
├── Routing
├── Handler
├── Response 資料
├── 組 HTTP 格式
├── Encoding
└── Write

幾乎所有事情都堆在同一個檔案。

重構後
Program.cs

├── 接收 Request
├── Parse
├── Routing
└── 選擇 HttpResponse

HttpResponse.cs

├── StatusCode
├── StatusText
├── ContentType
├── Body
└── Send()
├── 建立 HTTP 格式
├── Encoding
└── Write

所以今天的進步不是:功能增加

而是: 結構改善


今天做完後,我又回頭查 Microsoft 官方文件。

C# 的 Class 可以同時包含 Property 與 Method,因此很適合把同一個概念的「資料」和「行為」整理在一起。

ASP.NET Core 的 HttpResponse 也具有 Status Code、Headers、Body 等 Response 相關資訊;官方也提供向 Response Body 寫入內容的相關操作。

底層部分,NetworkStream 則負責實際的網路資料傳送與接收。

把它們放在一起看,可以更清楚理解今天做的分層:

高階
HttpResponse

描述「我要回什麼」

中間
HTTP 格式

描述「資料怎麼按照 HTTP 表達」

底層
NetworkStream

真的把 bytes 傳出去

Framework 需要自己的 Response 物件,是因為 HTTP Response 並不只有:

Body

它還包含:

Status
Headers
Body

以及最後如何真正將它送出去的相關處理。

如果所有地方都自己處理:

HTTP 格式
Encoding
NetworkStream

程式會很快充滿重複細節。

因此可以建立:

HttpResponse

將 Response 相關資料與操作集中管理。


目前 Mini Web Framework 已經慢慢不再只是單一的 Program.cs。

現在第一次出現:

MiniWebFramework

├── Program.cs

└── HttpResponse.cs

而整個流程變成:

Browser

TCP

HTTP Request

Parsing

Routing

Handler

HttpResponse

Send()

HTTP Response

Browser

這次最大的進步就是: Response

開始從「一段散落的程式碼」,變成真正的 Framework 元件。


Day14 的我第一次成功把:

Hello!

送到 Browser。

Day15 則讓我開始思考另一個問題:

功能可以跑,不代表程式的設計就已經完成。

一開始所有 HTTP Response 細節都放在 Program.cs 裡,功能確實能執行,但每增加一種 Response,就可能繼續堆更多重複的程式碼。

今天建立:

HttpResponse

之後,我第一次把:

Response 是什麼

和:

Response 怎麼被送出去

整理到同一個 Framework 元件裡。

最後 Program.cs 不再需要知道:

HTTP/1.1 要怎麼拼
Content-Length 怎麼算
怎麼轉 bytes
NetworkStream.Write 怎麼呼叫

只需要:

result.Send(stream);

這一行看起來很簡單,但背後其實就是 Framework 一直在做的事情:

把底層複雜度包起來,留下更容易使用的介面。

而做到這裡後,我也發現另一邊開始變得很刺眼。

Response 已經被包成:

HttpResponse

但 Request 到現在還是:

string request
string firstLine
string[] parts


string method
string path
string version

全部散落在 Program.cs 裡。


現在 Response 已經變成:

HttpResponse

但 Request 還是靠:

request.Split("\r\n")[0]

和:

firstLine.Split(' ')

自己手動解析。

那如果 Framework 想要提供:

Request.Method
Request.Path
Request.Version

是不是也應該把 Request 包成一個物件?

所以接下來最自然的問題就是:

既然有 HttpResponse,為什麼 Framework 也需要 HttpRequest?


上一篇
Server 到底要回傳什麼,Browser 才看得懂?
下一篇
為什麼 Framework 也需要自己的 Request 物件?
系列文
程式設計沒有告訴你的事:30 天破解每一個 Why17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言