雖然生產環境直接用 ASP.NET Core / Kestrel / Nginx,但**手寫一個極簡 HTTP Server** 能深入理解:
本文基於經典教學範例,重構為結構清晰、註解完整的現代 C# 版本(.NET 6+ 風格)。
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
public class HttpProcessor
{
private readonly Socket _clientSocket;
private readonly NetworkStream _stream;
private readonly StreamReader _reader;
private readonly StreamWriter _writer;
private string _method, _url, _protocol;
private readonly Dictionary _headers = new();
public HttpProcessor(Socket clientSocket)
{
_clientSocket = clientSocket;
_stream = new NetworkStream(clientSocket, FileAccess.ReadWrite, ownsSocket: true);
_reader = new StreamReader(_stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
_writer = new StreamWriter(_stream, Encoding.UTF8, leaveOpen: true) { NewLine = "\r\n", AutoFlush = true };
}
public void Process()
{
try
{
ParseRequestLine();
ParseHeaders();
ServeRequest();
}
catch (Exception ex)
{
Console.Error.WriteLine($"處理請求異常: {ex.Message}");
SendResponse(500, "Internal Server Error", "text/plain", "Server Error");
}
finally
{
Cleanup();
}
}
private void ParseRequestLine()
{
string line = _reader.ReadLine() ?? throw new InvalidOperationException("空請求");
string[] parts = line.Split(' ', 3);
if (parts.Length != 3) throw new InvalidOperationException("請求行格式錯誤");
(_method, _url, _protocol) = (parts[0], parts[1], parts[2]);
}
private void ParseHeaders()
{
string line;
while (!string.IsNullOrEmpty(line = _reader.ReadLine()))
{
int colon = line.IndexOf(':');
if (colon > 0)
{
string name = line.Substring(0, colon).Trim();
string value = line.Substring(colon + 1).Trim();
_headers[name] = value;
}
}
}
private void ServeRequest()
{
if (_method != "GET")
{
SendResponse(405, "Method Not Allowed", "text/plain", "Only GET supported");
return;
}
// 安全:移除查詢字串、禁止目錄遍歷
string path = _url.Split('?')[0];
string filePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "wwwroot", path.TrimStart('/')));
string root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "wwwroot"));
if (!filePath.StartsWith(root, StringComparison.OrdinalIgnoreCase) || !File.Exists(filePath))
{
SendResponse(404, "Not Found", "text/html", $"404 Not Found
{_url}
");
return;
}
// 簡易 MIME 對應
string mime = GetMimeType(Path.GetExtension(filePath));
byte[] fileBytes = File.ReadAllBytes(filePath);
SendResponse(200, "OK", mime, fileBytes);
}
private void SendResponse(int statusCode, string reason, string contentType, byte[] body)
{
_writer.WriteLine($"HTTP/1.1 {statusCode} {reason}");
_writer.WriteLine($"Content-Type: {contentType}");
_writer.WriteLine($"Content-Length: {body.Length}");
_writer.WriteLine("Connection: close");
_writer.WriteLine();
_writer.Flush();
_stream.Write(body, 0, body.Length);
}
private void SendResponse(int statusCode, string reason, string contentType, string body)
{
byte[] bytes = Encoding.UTF8.GetBytes(body);
SendResponse(statusCode, reason, contentType, bytes);
}
private static string GetMimeType(string ext) => ext.ToLowerInvariant() switch
{
".html" or ".htm" => "text/html",
".css" => "text/css",
".js" => "application/javascript",
".json" => "application/json",
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".svg" => "image/svg+xml",
".ico" => "image/x-icon",
_ => "application/octet-stream"
};
private void Cleanup()
{
try { _writer?.Dispose(); } catch { }
try { _reader?.Dispose(); } catch { }
try { _stream?.Dispose(); } catch { }
try { _clientSocket?.Close(); } catch { }
}
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
public class HttpServer
{
private readonly int _port;
private readonly TcpListener _listener;
private volatile bool _running;
public HttpServer(int port = 8080)
{
_port = port;
_listener = new TcpListener(IPAddress.Loopback, port); // 僅本機測試;實際部署用 IPAddress.Any
}
public void Start()
{
_listener.Start();
_running = true;
Console.WriteLine($"HTTP Server 啟動於 https://127.0.0.1:{_port}/");
Thread acceptThread = new Thread(AcceptLoop) { IsBackground = true };
acceptThread.Start();
}
public void Stop()
{
_running = false;
_listener.Stop();
}
private void AcceptLoop()
{
while (_running)
{
try
{
Socket clientSocket = _listener.AcceptSocket();
var processor = new HttpProcessor(clientSocket);
Thread t = new Thread(processor.Process) { IsBackground = true };
t.Start();
}
catch (SocketException) when (!_running) { /* 正常停止 */ }
catch (Exception ex)
{
Console.Error.WriteLine($"Accept 錯誤: {ex.Message}");
}
}
}
}
class Program
{
static void Main(string[] args)
{
int port = args.Length > 0 && int.TryParse(args[0], out int p) ? p : 8080;
var server = new HttpServer(port);
server.Start();
Console.WriteLine("按 Enter 鍵停止伺服器...");
Console.ReadLine();
server.Stop();
}
}
https://127.0.0.1:8080/ 即可看到首頁。| 技術點 | 說明 |
|---|---|
| Socket vs TcpListener | TcpListener 封裝 Socket,簡化監聽邏輯;底層仍是 `Socket.Accept`。 |
| 每請求一線程 | 簡單易懂,但高併發下線程開銷大。生產級用 **IOCP / async-await / Kestrel** 的事件驅動模型。 |
| 請求解析 | 僅解析請求行與標頭,不支援主體、Chunked、Keep-Alive、管線化等 HTTP/1.1 完整特性。 |
| 路徑安全 | 使用 Path.GetFullPath + `StartsWith(root)` 防禦目錄遍歷攻擊(`../../etc/passwd`)。 |
| 編碼處理 | 標頭用 ISO-8859-1 或 UTF-8;本範例統一 UTF-8 簡化處理。 |
這個極簡 HTTP Server 雖不適合生產,卻是理解網路程式設計、HTTP 協議、多線程併發的**最佳教具**。建議動手跑起來、加斷點、改壞它、再修好——這過程比閱讀任何框架原始碼都更能建立扎實的底層直覺。