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

從零手寫一個 HTTP Web Server:C# Socket 實戰完整指南

     編輯:關於C語言

想理解 HTTP 協議本質?沒有比親手寫一個 Web Server 更好的方式。本文將帶你用 C# TcpListener 與 Socket 從零構建一個可運行的靜態檔案伺服器,涵蓋連線接受、請求解析、回應封裝、檔案傳輸與錯誤處理全流程。程式碼精簡、邏輯清晰,適合網路程式設計入門與教學示範。

核心架構概覽

  • 監聽執行緒:TcpListener 綁定 Port 8080,AcceptSocket() 同步阻塞等待連線
  • 請求處理:讀取原始位元組 → ASCII 解碼 → 解析 GET 請求行與路徑
  • 靜態檔案服務:映射虛擬路徑至實體目錄 → FileStream 讀取 → 組裝 HTTP 回應
  • 回應發送:狀態行 + 標頭 + 空行 + 實體內容,支援 Content-Length 與 Content-Type

完整原始碼(單檔案可直接編譯執行)

using 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(); // 保持主執行緒 } } }

關鍵技術點深度解析

1. 同步 Socket vs. 非同步/IOCP

範例採用同步阻塞模式(AcceptSocket/Receive/Send),配合 ThreadPool 處理併發,適合教學與低併發場景。生產環境建議:

  • SocketAsyncEventArgs 或 await SocketTaskExtensions 實現高併發非同步 I/O
  • 或直接使用 HttpListener / ASP.NET Core Kestrel

2. HTTP 請求解析的最小集

  • 請求行:METHOD PATH VERSION
  • 標頭:以 \r\n\r\n 結束(本範例簡化忽略標頭解析)
  • 實體:GET 請求通常無 Body

3. 路徑映射與安全

  • Path.Combine + Path.GetFullPath 防止 ../../etc/passwd 遍歷攻擊
  • 預設文件:目錄請求自動補 index.html

4. HTTP 回應格式規範

HTTP/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 必須精確,否則瀏覽器會掛起等待。

5. MIME 類型對應表

內建 GetMimeType 覆蓋常見靜態資源,未知類型回傳 application/octet-stream 觸發下載。

如何測試

  1. 建立目錄 E:\MyWebServerRoot\,放入 index.html、style.css、圖片等靜態檔案
  2. 編譯執行:csc Program.cs && Program.exe
  3. 瀏覽器訪問 https://localhost:8080/
  4. Console 將輸出每個請求的詳細日誌

擴展方向建議

方向關鍵技術
HTTPS 支援SslStream + PFX 憑證
動態路由/Controller路由表 + 反射/委派調度
靜態檔案快取MemoryCache + ETag/Last-Modified
壓縮傳輸GZipStream + Accept-Encoding
WebSocket 升級解析 Upgrade 標頭、握手回應

結語:不到 200 行核心代碼,便能跑通 HTTP/1.1 靜態服務全流程。這不僅是 Socket 程式設計的練兵場,更能讓你對 Web 基礎設施有「拆解級」的理解。下一步,嘗試加上非同步 I/O 與路由系統,你就擁有了屬於自己的微型 Web 框架雛形。

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