不使用 HttpListener 或 ApplicationHost 等高階封裝,直接操作 Socket API,實作能收發 HTTP 封包的網路服務。不求完整符合 RFC 2616,重在學習協議解析、非同步 I/O、執行緒模型等核心機制。
\r\n(0x0D 0x0A)分隔。\r\n 分隔。Content-Length 標頭決定。: 分隔。網路封包可能分多次接收(甚至單一中文字元拆兩包),若每次收到就 Encoding.GetString() 再 IndexOf/Split,效能極差且易出錯。策略:
List<byte> 緩衝區,持續累積收到的位元組。\r\n\r\n 定位標頭結束位置。Content-Length。Stream(MemoryStream 或檔案流),由上層應用決定如何處理(JSON、表單、檔案上傳等)。void OnReceive(IAsyncResult ar) {
Socket sock = (Socket)ar.AsyncState;
int received = sock.EndReceive(ar);
if (received > 0) {
buffer.AddRange(receivedBytes.Take(received));
// 關鍵:先啟動下一次接收,再解析本次資料
sock.BeginReceive(...);
ParseBuffer(); // 解析完整封包並分派給應用層
}
}
為何先 BeginReceive? 防止解析過程拋出例外或執行緒阻塞導致後續封包無法接收。雖有觀點建議「收完一個完整封包再 BeginReceive」,但實測現代 IOCP 架構下,先重新投遞接收反而能最大化併發吞吐量。
解析邏輯直接在 IOCP 完成埠執行緒上執行,不引入額外佇列與解析執行緒。原則:一個執行緒只操作自己的私有資料,跨執行緒資料傳遞改用 Post 訊息給擁有者執行緒處理,避免鎖競爭。
Socket.IOControl(FIONREAD) 或 socket.Available 可一次讀取緩衝區所有資料,但微軟不建議頻繁呼叫(內部需鎖定計算),實測固定 8KB~64KB 緩衝區循環讀取效能最穩定。Split:手寫 ParseHeaders(byte[] buf, int offset, int count) 回傳 Dictionary<string,string>,零配置、零分配。Transfer-Encoding: chunked 時,本文解析器改為狀態機逐塊讀取。下篇將實作 HttpRequest/HttpResponse 物件模型、路由對應、靜態檔案服務與動態處理管線,逐步完成功能可用的迷你 ASP.NET Core 風格伺服器。