首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 從零手寫極簡 HTTP Server:Socket、多線程與請求解析實戰

     編輯:關於C語言

前言:為什麼要手寫 HTTP Server?

雖然生產環境直接用 ASP.NET Core / Kestrel / Nginx,但**手寫一個極簡 HTTP Server** 能深入理解:

  • TCP Socket 連線建立與資料收發
  • HTTP 協議請求行、標頭、主體解析
  • 多線程併發處理模型
  • 靜態檔案服務與錯誤回應

本文基於經典教學範例,重構為結構清晰、註解完整的現代 C# 版本(.NET 6+ 風格)。

架構設計

  1. HttpServer:監聽 Socket,`Accept` 客戶端連線,為每個連線啟動執行緒。
  2. HttpProcessor:處理單一連線的請求解析、路由、靜態檔案讀取、回應寫入。

核心程式碼實作

1. HttpProcessor — 請求處理核心

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 { } } }

2. HttpServer — 監聽與分派

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}");
            }
        }
    }
}

3. 程式進入點

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();
    }
}

執行與測試

  1. 專案根目錄建立 `wwwroot` 資料夾,放入 `index.html`、CSS、JS、圖片等靜態檔案。
  2. 執行程式,瀏覽器開啟 https://127.0.0.1:8080/ 即可看到首頁。

關鍵技術點解析

技術點說明
Socket vs TcpListenerTcpListener 封裝 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 簡化處理。

擴展方向(練習建議)

  • 支援 **POST / PUT** 請求主體讀取(依 `Content-Length`)。
  • 實作 **路由系統**:依 URL 對應不同處理器(Controller/Action)。
  • 加入 **靜態檔案快取**(`ETag`, `Last-Modified`, `Cache-Control`)。
  • 支援 **HTTPS**(`SslStream` 包裝 `NetworkStream`)。
  • 改用 **`async/await` + `Pipe` / `MemoryPool`** 實現高效能非同步 IO。

結語

這個極簡 HTTP Server 雖不適合生產,卻是理解網路程式設計、HTTP 協議、多線程併發的**最佳教具**。建議動手跑起來、加斷點、改壞它、再修好——這過程比閱讀任何框架原始碼都更能建立扎實的底層直覺。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved