/* SimpleHttpClient class — 支援 HTTP/HTTPS、GET/POST、自動轉址、gzip/deflate 解壓 支援轉址與壓縮 自動處理 301 / 302 轉址 自動解壓縮 gzip / deflate 壓縮的內容 保持使用 Socket + (SslStream),不依賴 HttpClient 或 WebRequest usage1: 建立實例與 GET 請求: var client = new SimpleHttpClient(); string html = client.Get("https://example.com"); Console.WriteLine(html); usage2: POST 請求(form): string response = client.Post("https://httpbin.org/post", "username=test&password=1234"); Console.WriteLine(response); usage3: POST 請求(JSON): client.DefaultHeaders["Accept"] = "application/json"; string json = "{\"name\":\"ChatGPT\",\"job\":\"assistant\"}"; string response = client.Post("https://httpbin.org/post", json, "application/json"); Console.WriteLine(response); 功能 支援 HTTP / HTTPS ✅ GET / POST ✅ 自訂 headers ✅ 表單 / JSON body ✅ 301 / 302 跳轉 ✅ 自動 gzip / deflate ✅ 自動解壓縮 純 Socket + SslStream ✅ 接下來可擴充的方向(可選): 支援 cookie 管理 支援 chunked 讀取(Transfer-Encoding: chunked) 支援 keep-alive 串流持續連線 */ using System; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Sockets; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Collections.Generic; public class SimpleHttpClient { private const int MaxRedirects = 5; public string UserAgent { get; set; } = "SimpleHttpClient/1.0"; public Dictionary DefaultHeaders { get; set; } = new(); public string Get(string url) => SendRequest(url, "GET", null); public string Post(string url, string body, string contentType = "application/x-www-form-urlencoded") => SendRequest(url, "POST", body, contentType); private string SendRequest(string url, string method, string body = null, string contentType = null, int redirectCount = 0) { if (redirectCount > MaxRedirects) throw new Exception("Too many redirects."); Uri uri = new Uri(url); string host = uri.Host; string path = string.IsNullOrEmpty(uri.PathAndQuery) ? "/" : uri.PathAndQuery; bool useHttps = uri.Scheme == "https"; int port = uri.Port; IPAddress ipAddress = Dns.GetHostEntry(host).AddressList[0]; IPEndPoint remoteEP = new IPEndPoint(ipAddress, port); using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { socket.Connect(remoteEP); Stream netStream = useHttps ? new SslStream(new NetworkStream(socket), false, (_, __, ___, ____) => true) : new NetworkStream(socket); if (useHttps) ((SslStream)netStream).AuthenticateAsClient(host); var requestBuilder = new StringBuilder(); requestBuilder.AppendLine($"{method} {path} HTTP/1.1"); requestBuilder.AppendLine($"Host: {host}"); requestBuilder.AppendLine("Connection: Close"); requestBuilder.AppendLine($"User-Agent: {UserAgent}"); requestBuilder.AppendLine("Accept-Encoding: gzip, deflate"); foreach (var header in DefaultHeaders) requestBuilder.AppendLine($"{header.Key}: {header.Value}"); if (!string.IsNullOrEmpty(body)) { byte[] bodyBytes = Encoding.UTF8.GetBytes(body); requestBuilder.AppendLine($"Content-Length: {bodyBytes.Length}"); requestBuilder.AppendLine($"Content-Type: {contentType ?? "application/x-www-form-urlencoded"}"); requestBuilder.AppendLine(); requestBuilder.Append(body); } else { requestBuilder.AppendLine(); } byte[] requestBytes = Encoding.UTF8.GetBytes(requestBuilder.ToString()); netStream.Write(requestBytes, 0, requestBytes.Length); using MemoryStream responseStream = new MemoryStream(); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = netStream.Read(buffer, 0, buffer.Length)) > 0) responseStream.Write(buffer, 0, bytesRead); string fullResponse = Encoding.UTF8.GetString(responseStream.ToArray()); int headerEndIndex = fullResponse.IndexOf("\r\n\r\n", StringComparison.Ordinal); if (headerEndIndex < 0) return fullResponse; string headerPart = fullResponse[..headerEndIndex]; byte[] bodyBytes = responseStream.ToArray()[headerEndIndex + 4..]; // Redirect string[] headers = headerPart.Split("\r\n"); string statusLine = headers[0]; if (statusLine.Contains(" 301 ") || statusLine.Contains(" 302 ")) { foreach (var line in headers) { if (line.StartsWith("Location:", StringComparison.OrdinalIgnoreCase)) { string newUrl = line[9..].Trim(); return SendRequest(newUrl, method, body, contentType, redirectCount + 1); } } } // Decompress if needed string encoding = ""; foreach (var line in headers) { if (line.StartsWith("Content-Encoding:", StringComparison.OrdinalIgnoreCase)) { encoding = line[18..].Trim().ToLower(); break; } } Stream decompressed = new MemoryStream(bodyBytes); if (encoding == "gzip") decompressed = new GZipStream(decompressed, CompressionMode.Decompress); else if (encoding == "deflate") decompressed = new DeflateStream(decompressed, CompressionMode.Decompress); using StreamReader reader = new StreamReader(decompressed, Encoding.UTF8); return reader.ReadToEnd(); } } }