使用原始 Socket 建立 HTTP/1.0 伺服器,瀏覽器輸入 https://ip:port 即可看到伺服器當前時間,無需撰寫客戶端程式。適合理解 HTTP 協議本質、嵌入式裝置管理介面、輕量健康檢查端點。
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
HTTP/1.0 200 OK (版本、狀態碼、原因片語)Content-Type (MIME + charset)、Content-Length (內文位元組數)\r\n\r\n
// 使用 AcceptAsync + ReceiveAsync/SendAsync (SocketAsyncEventArgs) 或
// 更簡單:TcpListener + NetworkStream (參考前文 TCP 時間伺服器)
// 真實生產環境建議用 Kestrel (ASP.NET Core) 或成熟庫
ThreadPool.QueueUserWorkItem:每請求一執行緒池工作項,簡單但高併發下執行緒耗盡SemaphoreSlim 限制併發數、或完整非同步 I/O0.0.0.0 (IPAddress.Any):監聽所有網卡介面 (預設)127.0.0.1 (IPAddress.Loopback):僅本機存取 (安全)
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();
}
dotnet run 或 VS F5https://localhost:8080curl -v https://localhost:8080
curl https://localhost:8080/json
curl https://localhost:8080/health
SemaphoreSlim 或 Token Bucket 限制每 IP QPSSslStream + 有效憑證從零實作 HTTP 伺服器能深入理解「請求-回應」模型、TCP 流與應用層協議的邊界、標頭與內文分隔、編碼與 Content-Length 的關係。雖然生產環境絕不建議手寫 (請用 Kestrel/NGINX/IIS),但這個練習是網路程式設計進階的必經之路,也是面試常考核心。