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

C# 實作極簡 HTTP 伺服器:瀏覽器直接取得伺服器時間

     編輯:關於C語言

專案目標

使用原始 Socket 建立 HTTP/1.0 伺服器,瀏覽器輸入 https://ip:port 即可看到伺服器當前時間,無需撰寫客戶端程式。適合理解 HTTP 協議本質、嵌入式裝置管理介面、輕量健康檢查端點。

完整原始碼 (Console Application)


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace MiniHttpServer
{
    class HttpProcessor
    {
        private readonly Socket _clientSocket;
        private readonly BufferedStream _stream;
        private readonly StreamReader _reader;
        private readonly StreamWriter _writer;

        public HttpProcessor(Socket clientSocket)
        {
            _clientSocket = clientSocket;
            var netStream = new NetworkStream(clientSocket, FileAccess.ReadWrite, ownsSocket: false);
            _stream = new BufferedStream(netStream);
            _reader = new StreamReader(_stream, Encoding.ASCII);
            _writer = new StreamWriter(_stream, Encoding.ASCII) { AutoFlush = true, NewLine = "\r\n" };
        }

        public void Process()
        {
            try
            {
                // 1. 讀取請求行 (只處理第一行:GET / HTTP/1.1)
                string requestLine = _reader.ReadLine();
                if (string.IsNullOrEmpty(requestLine)) return;

                Console.WriteLine($"Request: {requestLine}");

                // 2. 讀取並丟棄標頭 (直到空行)
                while (!string.IsNullOrEmpty(_reader.ReadLine())) { }

                // 3. 回應 HTTP 200 OK + 時間內容
                WriteSuccessResponse();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Process error: {ex.Message}");
                try { WriteErrorResponse(); } catch { }
            }
            finally
            {
                // 關閉連線 (HTTP/1.0 無 Keep-Alive)
                try { _clientSocket.Shutdown(SocketShutdown.Both); } catch { }
                _clientSocket.Close();
            }
        }

        private void WriteSuccessResponse()
        {
            string time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            string body = $"

Server Time

{time}

"; byte[] bodyBytes = Encoding.UTF8.GetBytes(body); _writer.WriteLine("HTTP/1.0 200 OK"); _writer.WriteLine("Server: MiniHttpServer/1.0"); _writer.WriteLine($"Date: {DateTime.UtcNow:R}"); // RFC 1123 _writer.WriteLine("Content-Type: text/html; charset=utf-8"); _writer.WriteLine($"Content-Length: {bodyBytes.Length}"); _writer.WriteLine("Connection: close"); _writer.WriteLine(); // 空行分隔標頭與內文 _writer.Flush(); _stream.Write(bodyBytes, 0, bodyBytes.Length); _stream.Flush(); } private void WriteErrorResponse() { string body = "

404 Not Found

"; byte[] bodyBytes = Encoding.UTF8.GetBytes(body); _writer.WriteLine("HTTP/1.0 404 Not Found"); _writer.WriteLine("Content-Type: text/html; charset=utf-8"); _writer.WriteLine($"Content-Length: {bodyBytes.Length}"); _writer.WriteLine("Connection: close"); _writer.WriteLine(); _writer.Flush(); _stream.Write(bodyBytes, 0, bodyBytes.Length); } } class HttpServer { private readonly int _port; private readonly IPAddress _bindAddress; private Socket _listener; private volatile bool _running; public HttpServer(int port = 8080, string bindIp = "0.0.0.0") { _port = port; _bindAddress = IPAddress.Parse(bindIp); } public void Start() { _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(new IPEndPoint(_bindAddress, _port)); _listener.Listen(100); // backlog _running = true; Console.WriteLine($"HTTP Server started on https://{_bindAddress}:{_port}"); Console.WriteLine("Press Ctrl+C to stop..."); while (_running) { try { Socket client = _listener.Accept(); // 封鎖等待連線 ThreadPool.QueueUserWorkItem(_ => new HttpProcessor(client).Process()); } catch (SocketException ex) when (!_running) { break; // 正常停止 } catch (Exception ex) { Console.WriteLine($"Accept error: {ex.Message}"); } } } public void Stop() { _running = false; _listener?.Close(); } } class Program { static void Main(string[] args) { int port = args.Length > 0 ? int.Parse(args[0]) : 8080; var server = new HttpServer(port); Console.CancelKeyPress += (s, e) => { e.Cancel = true; server.Stop(); }; server.Start(); } } }

核心流程圖解

[瀏覽器] ── TCP 連線 (三次交手) ──→ [Socket.Accept()]
    │                                      │
    │  GET / HTTP/1.1\r\n                  │
    │  Host: localhost:8080\r\n            │  StreamReader.ReadLine()
    │  User-Agent: ...\r\n                 │  (讀取請求行)
    │  \r\n                                │  (讀取並丟棄標頭)
    │                                     ▼
    │  HTTP/1.0 200 OK\r\n               StreamWriter.WriteLine()
    │  Content-Type: text/html\r\n       (寫入狀態行、標頭)
    │  Content-Length: 123\r\n           │
    │  \r\n                               │  (空行)
    │  ...時間...            │  Stream.Write(bodyBytes)
    │                                     │
    ◄── TCP 關閉 (四次揮手) ─────────────┘  Socket.Shutdown/Close

關鍵技術細節

1. HTTP 協議最小實作要素

  • 狀態行:HTTP/1.0 200 OK (版本、狀態碼、原因片語)
  • 必要標頭:Content-Type (MIME + charset)、Content-Length (內文位元組數)
  • 空行分隔:標頭結束必須有 \r\n\r\n
  • 內文:HTML/JSON/純文字等
  • Connection: close:HTTP/1.0 預設短連線,回應後主動關閉

2. Socket 非同步改良 (.NET 4.5+)


// 使用 AcceptAsync + ReceiveAsync/SendAsync (SocketAsyncEventArgs) 或
// 更簡單:TcpListener + NetworkStream (參考前文 TCP 時間伺服器)
// 真實生產環境建議用 Kestrel (ASP.NET Core) 或成熟庫

3. 多執行緒模型

  • ThreadPool.QueueUserWorkItem:每請求一執行緒池工作項,簡單但高併發下執行緒耗盡
  • 改良:SemaphoreSlim 限制併發數、或完整非同步 I/O

4. 繫結 IP 位址選擇

  • 0.0.0.0 (IPAddress.Any):監聽所有網卡介面 (預設)
  • 127.0.0.1 (IPAddress.Loopback):僅本機存取 (安全)
  • 特定 LAN IP:僅接受內網請求

功能擴充範例

支援路由:不同路徑回應不同內容


private void Process()
{
    string requestLine = _reader.ReadLine(); // "GET /time HTTP/1.1"
    if (string.IsNullOrEmpty(requestLine)) return;

    string[] parts = requestLine.Split(' ');
    if (parts.Length < 2) return;
    string path = parts[1]; // "/time"

    // 讀取標頭
    while (!string.IsNullOrEmpty(_reader.ReadLine())) { }

    // 簡單路由
    switch (path)
    {
        case "/":
        case "/time":
            WriteHtml($"

Server Time

{DateTime.Now:yyyy-MM-dd HH:mm:ss}

"); break; case "/json": WriteJson(new { time = DateTime.UtcNow.ToString("o"), server = "MiniHttpServer" }); break; case "/health": WritePlain("OK"); break; default: WriteErrorResponse(); break; } } private void WriteJson(object obj) { string json = System.Text.Json.JsonSerializer.Serialize(obj); byte[] bytes = Encoding.UTF8.GetBytes(json); WriteHeaders("application/json", bytes.Length); _stream.Write(bytes, 0, bytes.Length); } private void WriteHtml(string html) { string full = $"{html}"; byte[] bytes = Encoding.UTF8.GetBytes(full); WriteHeaders("text/html; charset=utf-8", bytes.Length); _stream.Write(bytes, 0, bytes.Length); } private void WriteHeaders(string contentType, int contentLength) { _writer.WriteLine("HTTP/1.0 200 OK"); _writer.WriteLine($"Content-Type: {contentType}"); _writer.WriteLine($"Content-Length: {contentLength}"); _writer.WriteLine("Connection: close"); _writer.WriteLine(); _writer.Flush(); }

測試與驗證

  1. 編譯執行:dotnet run 或 VS F5
  2. 瀏覽器開啟:https://localhost:8080
  3. 命令列測試:
    curl -v https://localhost:8080
    curl https://localhost:8080/json
    curl https://localhost:8080/health
  4. 網路抓包 (Wireshark) 觀察 HTTP 封包結構

安全性考量 (若對外暴露)

  • 輸入驗證:嚴格解析請求行,防止緩衝區溢位、路徑遍歷
  • 速率限制:SemaphoreSlim 或 Token Bucket 限制每 IP QPS
  • 標頭注入防護:使用者輸入不直接拼接至回應標頭
  • HTTPS:生產環境必須用 SslStream + 有效憑證
  • 請求大小限制:避免大 POST 佔用記憶體

結語

從零實作 HTTP 伺服器能深入理解「請求-回應」模型、TCP 流與應用層協議的邊界、標頭與內文分隔、編碼與 Content-Length 的關係。雖然生產環境絕不建議手寫 (請用 Kestrel/NGINX/IIS),但這個練習是網路程式設計進階的必經之路,也是面試常考核心。

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