昨天我們第一次成功讓整個流程完整跑通:
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
最後就可以走同一套流程。






原本 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 學過的三件事情全部搬進來。
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 仍然可以收到正確結果。
所以今天重構之後:
外部結果沒有變,但內部結構變得更清楚。




一開始我可能會覺得:
昨天 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?