想理解 HTTP 協議本質?沒有比親手寫一個 Web Server 更好的方式。本文將帶你用 C# TcpListener 與 Socket 從零構建一個可運行的靜態檔案伺服器,涵蓋連線接受、請求解析、回應封裝、檔案傳輸與錯誤處理全流程。程式碼精簡、邏輯清晰,適合網路程式設計入門與教學示範。
TcpListener 綁定 Port 8080,AcceptSocket() 同步阻塞等待連線GET 請求行與路徑FileStream 讀取 → 組裝 HTTP 回應Content-Length 與 Content-Typeusing System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace MiniWebServer
{
public class HttpServer
{
private readonly TcpListener _listener;
private readonly string _webRoot;
public HttpServer(int port = 8080, string webRoot = @"E:\MyWebServerRoot\")
{
_listener = new TcpListener(IPAddress.Any, port);
_webRoot = webRoot;
}
public void Start()
{
_listener.Start();
Console.WriteLine($"Web Server running on https://localhost:{((IPEndPoint)_listener.LocalEndpoint).Port} ... Press Ctrl+C to stop.");
var listenThread = new Thread(ListenLoop) { IsBackground = true };
listenThread.Start();
}
private void ListenLoop()
{
while (true)
{
Socket clientSocket = _listener.AcceptSocket();
if (clientSocket.Connected)
ThreadPool.QueueUserWorkItem(HandleClient, clientSocket);
}
}
private void HandleClient(object state)
{
var socket = (Socket)state;
try
{
var buffer = new byte[1024];
int received = socket.Receive(buffer);
string request = Encoding.ASCII.GetString(buffer, 0, received);
Console.WriteLine($"Client connected: {socket.RemoteEndPoint}");
Console.WriteLine($"Request:\n{request}");
// 僅處理 GET
if (!request.StartsWith("GET "))
{
SendError(socket, 400, "Bad Request", "Only GET method supported");
return;
}
// 解析請求行:GET /path HTTP/1.1
var lines = request.Split('\n');
var requestLine = lines[0].Trim();
var parts = requestLine.Split(' ');
if (parts.Length != 3)
{
SendError(socket, 400, "Bad Request", "Invalid request line");
return;
}
string rawPath = parts[1];
string httpVersion = parts[2];
// 正規化路徑
string path = rawPath.Split('?')[0].Replace('/', '\\').TrimStart('\\');
if (string.IsNullOrEmpty(path) || path.EndsWith("\\"))
path += "index.html";
string physicalPath = Path.Combine(_webRoot, path);
// 安全檢查:防止目錄遍歷
string fullRoot = Path.GetFullPath(_webRoot);
string fullRequest = Path.GetFullPath(physicalPath);
if (!fullRequest.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase))
{
SendError(socket, 403, "Forbidden", "Path traversal attempt denied");
return;
}
if (!File.Exists(fullRequest))
{
SendError(socket, 404, "Not Found", $"File not found: {path}");
return;
}
// 讀取檔案並發送
byte[] fileBytes = File.ReadAllBytes(fullRequest);
string mime = GetMimeType(Path.GetExtension(fullRequest));
SendResponse(socket, httpVersion, "200 OK", mime, fileBytes);
}
catch (Exception ex)
{
Console.WriteLine($"Error handling client: {ex}");
SendError(socket, 500, "Internal Server Error", ex.Message);
}
finally
{
socket.Close();
}
}
private void SendResponse(Socket socket, string httpVersion, string status, string mime, byte[] body)
{
var header = new StringBuilder();
header.Append($"{httpVersion} {status}\r\n");
header.Append("Server: MiniWebServer/1.0\r\n");
header.Append($"Content-Type: {mime}\r\n");
header.Append("Accept-Ranges: bytes\r\n");
header.Append($"Content-Length: {body.Length}\r\n");
header.Append("\r\n");
byte[] headerBytes = Encoding.ASCII.GetBytes(header.ToString());
socket.Send(headerBytes);
socket.Send(body);
Console.WriteLine($"Sent {body.Length} bytes [{status}]");
}
private void SendError(Socket socket, int code, string status, string message)
{
string html = $"{code} {status}
{message}
";
byte[] body = Encoding.UTF8.GetBytes(html);
SendResponse(socket, "HTTP/1.1", $"{code} {status}", "text/html; charset=utf-8", body);
}
private static string GetMimeType(string ext)
{
return ext.ToLowerInvariant() switch
{
".html" or ".htm" => "text/html; charset=utf-8",
".css" => "text/css; charset=utf-8",
".js" => "application/javascript; charset=utf-8",
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".svg" => "image/svg+xml",
".ico" => "image/x-icon",
".json" => "application/json; charset=utf-8",
".txt" => "text/plain; charset=utf-8",
_ => "application/octet-stream"
};
}
}
class Program
{
static void Main()
{
var server = new HttpServer(8080, @"E:\MyWebServerRoot\");
server.Start();
Console.ReadLine(); // 保持主執行緒
}
}
}
範例採用同步阻塞模式(AcceptSocket/Receive/Send),配合 ThreadPool 處理併發,適合教學與低併發場景。生產環境建議:
SocketAsyncEventArgs 或 await SocketTaskExtensions 實現高併發非同步 I/OHttpListener / ASP.NET Core KestrelMETHOD PATH VERSION\r\n\r\n 結束(本範例簡化忽略標頭解析)Path.Combine + Path.GetFullPath 防止 ../../etc/passwd 遍歷攻擊index.htmlHTTP/1.1 200 OK\r\n
Server: MiniWebServer/1.0\r\n
Content-Type: text/html; charset=utf-8\r\n
Content-Length: 1234\r\n
Accept-Ranges: bytes\r\n
\r\n
[檔案二進位內容]
關鍵點:標頭結束必須雙 \r\n,Content-Length 必須精確,否則瀏覽器會掛起等待。
內建 GetMimeType 覆蓋常見靜態資源,未知類型回傳 application/octet-stream 觸發下載。
E:\MyWebServerRoot\,放入 index.html、style.css、圖片等靜態檔案csc Program.cs && Program.exehttps://localhost:8080/| 方向 | 關鍵技術 |
|---|---|
| HTTPS 支援 | SslStream + PFX 憑證 |
| 動態路由/Controller | 路由表 + 反射/委派調度 |
| 靜態檔案快取 | MemoryCache + ETag/Last-Modified |
| 壓縮傳輸 | GZipStream + Accept-Encoding |
| WebSocket 升級 | 解析 Upgrade 標頭、握手回應 |
結語:不到 200 行核心代碼,便能跑通 HTTP/1.1 靜態服務全流程。這不僅是 Socket 程式設計的練兵場,更能讓你對 Web 基礎設施有「拆解級」的理解。下一步,嘗試加上非同步 I/O 與路由系統,你就擁有了屬於自己的微型 Web 框架雛形。