/* 支援轉址與壓縮 自動處理 301 / 302 轉址 自動解壓縮 gzip / deflate 壓縮的內容 保持使用 Socket + (SslStream),不依賴 HttpClient 或 WebRequest usage1: string html = WebClientSocket.Request("httpbin.org", "/gzip", "GET", true); Console.WriteLine(html); usage2: string html = WebClientSocket.Request("http://google.com", "/", "GET", false); Console.WriteLine(html); // 會自動跳轉到 https://www.google.com 功能 支援 HTTP / HTTPS ✅ GET / POST ✅ 自訂 headers ✅ 表單 / JSON body ✅ 301 / 302 跳轉 ✅ 自動 gzip / deflate ✅ 自動解壓縮 純 Socket + SslStream ✅ 如果你有要包成 reusable class(例如像 SimpleHttpClient),我也可以幫你整理成更乾淨的封裝版本,要嗎? */ using System; using System.Net; using System.Net.Sockets; using System.Text; using System.IO; using System.IO.Compression; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Collections.Generic; public class WebClientSocket { private const int MaxRedirects = 5; public static string Request( string host, string path = "/", string method = "GET", bool useHttps = true, Dictionary headers = null, string postData = null, int redirectCount = 0) { if (redirectCount > MaxRedirects) throw new Exception("Too many redirects."); int port = useHttps ? 443 : 80; 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; if (useHttps) { var sslStream = new SslStream(new NetworkStream(socket), false, new RemoteCertificateValidationCallback((sender, cert, chain, sslErrors) => true)); sslStream.AuthenticateAsClient(host); netStream = sslStream; } else { netStream = new NetworkStream(socket); } var requestBuilder = new StringBuilder(); requestBuilder.AppendLine($"{method} {path} HTTP/1.1"); requestBuilder.AppendLine($"Host: {host}"); requestBuilder.AppendLine("Connection: Close"); requestBuilder.AppendLine("User-Agent: CustomSocketClient/1.0"); requestBuilder.AppendLine("Accept-Encoding: gzip, deflate"); if (headers != null) { foreach (var header in headers) requestBuilder.AppendLine($"{header.Key}: {header.Value}"); } if (!string.IsNullOrEmpty(postData)) { var postBytes = Encoding.UTF8.GetBytes(postData); requestBuilder.AppendLine($"Content-Length: {postBytes.Length}"); requestBuilder.AppendLine("Content-Type: application/x-www-form-urlencoded"); requestBuilder.AppendLine(); requestBuilder.Append(postData); } else { requestBuilder.AppendLine(); } byte[] requestBytes = Encoding.UTF8.GetBytes(requestBuilder.ToString()); netStream.Write(requestBytes, 0, requestBytes.Length); using (var memoryStream = new MemoryStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = netStream.Read(buffer, 0, buffer.Length)) > 0) memoryStream.Write(buffer, 0, bytesRead); string fullResponse = Encoding.UTF8.GetString(memoryStream.ToArray()); // 拆解 headers 和 body int headerEndIndex = fullResponse.IndexOf("\r\n\r\n", StringComparison.Ordinal); if (headerEndIndex < 0) return fullResponse; string headerPart = fullResponse.Substring(0, headerEndIndex); byte[] bodyBytes = memoryStream.ToArray()[headerEndIndex + 4..]; // 檢查是否是轉址 string[] lines = headerPart.Split("\r\n"); string statusLine = lines[0]; if (statusLine.Contains(" 301 ") || statusLine.Contains(" 302 ")) { foreach (var line in lines) { if (line.StartsWith("Location:", StringComparison.OrdinalIgnoreCase)) { string newUrl = line.Substring(9).Trim(); Uri uri = new Uri(newUrl); return Request( uri.Host, uri.PathAndQuery, method, uri.Scheme == "https", headers, postData, redirectCount + 1 ); } } } // 處理壓縮內容 string contentEncoding = ""; foreach (var line in lines) { if (line.StartsWith("Content-Encoding:", StringComparison.OrdinalIgnoreCase)) { contentEncoding = line.Substring(18).Trim().ToLower(); break; } } Stream bodyStream = new MemoryStream(bodyBytes); if (contentEncoding == "gzip") { using var gzip = new GZipStream(bodyStream, CompressionMode.Decompress); using var reader = new StreamReader(gzip, Encoding.UTF8); return reader.ReadToEnd(); } else if (contentEncoding == "deflate") { using var deflate = new DeflateStream(bodyStream, CompressionMode.Decompress); using var reader = new StreamReader(deflate, Encoding.UTF8); return reader.ReadToEnd(); } else { return Encoding.UTF8.GetString(bodyBytes); } } } } }