using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AIK.Service.Service { /// /// 基于 BouncyCastle 纯托管 TLS 实现的 HttpMessageHandler,专门用于 Win7。 /// 背景:Win7 系统 SChannel 默认未启用 TLS 1.2(且补丁/注册表方案常因精简系统、权限、未重启而不生效), /// 而 .NET Framework 的 HttpClient/HttpWebRequest 全部走 SChannel,代码层无法绕过。 /// BouncyCastle 的 TLS 客户端是纯托管实现(Org.BouncyCastle.Crypto.Tls),不依赖系统 SChannel, /// 因此在 Win7 上可直接协商 TLS 1.2 与 key.aikkey.cn 握手。 /// 本 Handler 内部流程:TCP 连接 → BouncyCastle TLS 1.2 握手(含 SNI)→ HTTP/1.1 请求/响应。 /// public class BouncyCastleTlsHandler : HttpMessageHandler { private const int DefaultTimeoutMilliseconds = 30000; private const int IdleTimeoutMilliseconds = 60000; // 流式下载:响应体每段读取的空闲超时 private const int MaxRedirects = 5; protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var currentRequest = request; int redirectCount = 0; while (true) { cancellationToken.ThrowIfCancellationRequested(); var response = await ExecuteSingleAsync(currentRequest, cancellationToken).ConfigureAwait(false); int status = (int)response.StatusCode; bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308; if (!isRedirect || redirectCount >= MaxRedirects) { return response; } var location = response.Headers.Location; response.Dispose(); if (location == null) { return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态 } redirectCount++; var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location); var method = currentRequest.Method; // 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法 if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head) { method = HttpMethod.Get; } var next = new HttpRequestMessage(method, nextUri); // 携带 token 等业务头 foreach (var header in currentRequest.Headers) { next.Headers.TryAddWithoutValidation(header.Key, header.Value); } if (method == HttpMethod.Get || method == HttpMethod.Head) { currentRequest.Content?.Dispose(); } else if (currentRequest.Content != null) { next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); foreach (var header in currentRequest.Content.Headers) { next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } currentRequest = next; } } /// /// 流式发送:仅读取响应头即返回,响应体通过 StreamContent 逐段读取(每段读取带 60s 空闲超时,无总时长限制), /// 专用于大文件下载,解决原实现"30s 总超时 + 全量读内存"导致的大文件必失败问题。 /// 调用方负责 Dispose 响应(会关闭底层 TLS 连接并发出 close_notify)。 /// public async Task SendStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var currentRequest = request; int redirectCount = 0; while (true) { cancellationToken.ThrowIfCancellationRequested(); var response = await ExecuteStreamingAsync(currentRequest, cancellationToken).ConfigureAwait(false); int status = (int)response.StatusCode; bool isRedirect = status == 301 || status == 302 || status == 303 || status == 307 || status == 308; if (!isRedirect || redirectCount >= MaxRedirects) { return response; } var location = response.Headers.Location; response.Dispose(); // 重定向不读取 body,直接关闭连接 if (location == null) { return new HttpResponseMessage(response.StatusCode); // 无 Location,直接返回原状态 } redirectCount++; var nextUri = location.IsAbsoluteUri ? location : new Uri(currentRequest.RequestUri, location); var method = currentRequest.Method; // 301/302/303 对非 GET/HEAD 请求按 HTTP 规范转成 GET;307/308 保持原方法 if ((status == 301 || status == 302 || status == 303) && method != HttpMethod.Get && method != HttpMethod.Head) { method = HttpMethod.Get; } var next = new HttpRequestMessage(method, nextUri); // 携带 token 等业务头 foreach (var header in currentRequest.Headers) { next.Headers.TryAddWithoutValidation(header.Key, header.Value); } if (method == HttpMethod.Get || method == HttpMethod.Head) { currentRequest.Content?.Dispose(); } else if (currentRequest.Content != null) { next.Content = new ByteArrayContent(await currentRequest.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); foreach (var header in currentRequest.Content.Headers) { next.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } currentRequest = next; } } /// 执行单次 HTTP 请求(不处理重定向)。 private async Task ExecuteSingleAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var uri = request.RequestUri; using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(DefaultTimeoutMilliseconds); var ct = timeoutCts.Token; // ---- 1. TCP 连接 ---- using var tcp = new TcpClient(); try { var connectTask = tcp.ConnectAsync(uri.Host, uri.Port); await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false); ct.ThrowIfCancellationRequested(); await connectTask.ConfigureAwait(false); } catch (Exception ex) when (!(ex is OperationCanceledException)) { throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex); } // ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ---- Stream tlsStream; try { tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false); } catch (Exception ex) when (!(ex is OperationCanceledException)) { throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex); } // BufferedStream.Dispose 会连带释放内层 TLS 流(close_notify),因此无需单独 using tlsStream using var buffered = new BufferedStream(tlsStream); try { // ---- 3. 发送 HTTP 请求 ---- byte[] bodyBytes = null; if (request.Content != null) { bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } var sb = new StringBuilder(); var pathQuery = uri.PathAndQuery; if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/"; sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n"); sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n"); sb.Append("Connection: close\r\n"); // 业务请求头(User-Agent 多值用空格连接,其余用逗号连接) foreach (var header in request.Headers) { bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase); sb.Append(header.Key).Append(": ") .Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n"); } // Content 头(Content-Type / Content-Length 等) if (request.Content != null) { foreach (var header in request.Content.Headers) { sb.Append(header.Key).Append(": ") .Append(string.Join(",", header.Value)).Append("\r\n"); } } sb.Append("\r\n"); var headBytes = Encoding.ASCII.GetBytes(sb.ToString()); await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false); if (bodyBytes != null && bodyBytes.Length > 0) { await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false); } await buffered.FlushAsync(ct).ConfigureAwait(false); // ---- 4. 读取响应头 ---- string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false); if (string.IsNullOrEmpty(statusLine)) { throw new HttpRequestException("服务器返回空响应(连接被关闭)"); } var statusParts = statusLine.Split(' '); if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal)) { throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}"); } int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture); string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty; var responseHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); string line; while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false))) { int colon = line.IndexOf(':'); if (colon <= 0) continue; var name = line.Substring(0, colon).Trim(); var value = line.Substring(colon + 1).Trim(); if (!responseHeaders.TryGetValue(name, out var list)) { list = new List(); responseHeaders[name] = list; } list.Add(value); } // ---- 5. 读取响应体 ---- byte[] body = await ReadBodyAsync(buffered, responseHeaders, ct).ConfigureAwait(false); // ---- 6. 解压(兼容原 HttpClientHandler.AutomaticDecompression 行为) ---- if (responseHeaders.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0) { string encoding = encodings[0].ToLowerInvariant(); if (encoding == "gzip" && body.Length > 0) { using var gz = new GZipStream(new MemoryStream(body), CompressionMode.Decompress); body = ReadAllBytes(gz); responseHeaders.Remove("Content-Encoding"); } else if (encoding == "deflate" && body.Length > 0) { using var def = new DeflateStream(new MemoryStream(body), CompressionMode.Decompress); body = ReadAllBytes(def); responseHeaders.Remove("Content-Encoding"); } } // ---- 7. 包装为 HttpResponseMessage ---- var msg = new HttpResponseMessage((HttpStatusCode)statusCode) { ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase, Content = new ByteArrayContent(body) }; foreach (var kv in responseHeaders) { if (IsContentHeader(kv.Key)) { msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } else { msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } } return msg; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (OperationCanceledException ex) { throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex); } catch (Exception ex) when (!(ex is HttpRequestException)) { throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex); } // 注:tcp/tlsStream/buffered 均为 using 声明,按反向顺序释放(buffered → tlsStream → tcp), // 确保 BouncyCastle 在 socket 关闭前发出 close_notify,避免 internal_error。 } /// 执行单次流式 HTTP 请求(不处理重定向)。响应体不预读,交给调用方通过 StreamContent 逐段读取。 private async Task ExecuteStreamingAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var uri = request.RequestUri; using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(DefaultTimeoutMilliseconds); // 仅覆盖:连接/握手/发送/读响应头 var ct = timeoutCts.Token; // ---- 1. TCP 连接 ---- var tcp = new TcpClient(); try { var connectTask = tcp.ConnectAsync(uri.Host, uri.Port); await Task.WhenAny(connectTask, Task.Delay(Timeout.Infinite, ct)).ConfigureAwait(false); ct.ThrowIfCancellationRequested(); await connectTask.ConfigureAwait(false); } catch (Exception ex) when (!(ex is OperationCanceledException)) { tcp.Dispose(); throw new HttpRequestException($"TCP 连接失败: {uri.Host}:{uri.Port} — {ex.Message}", ex); } // ---- 2. BouncyCastle TLS 1.2 握手(复用 BouncyCastleTlsHelper,含 SNI 与证书链校验) ---- Stream tlsStream; try { tlsStream = await Task.Run(() => BouncyCastleTlsHelper.ConnectTlsStream(tcp.GetStream(), uri.Host), ct).ConfigureAwait(false); } catch (Exception ex) when (!(ex is OperationCanceledException)) { tcp.Dispose(); throw new HttpRequestException($"TLS 1.2 握手失败(BouncyCastle): {uri.Host}:{uri.Port} — {ex.Message}", ex); } var buffered = new BufferedStream(tlsStream); try { // ---- 3. 发送 HTTP 请求 ---- byte[] bodyBytes = null; if (request.Content != null) { bodyBytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } var sb = new StringBuilder(); var pathQuery = uri.PathAndQuery; if (string.IsNullOrEmpty(pathQuery)) pathQuery = "/"; sb.Append(request.Method.Method).Append(' ').Append(pathQuery).Append(" HTTP/1.1\r\n"); sb.Append("Host: ").Append(uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}").Append("\r\n"); sb.Append("Connection: close\r\n"); // 业务请求头(User-Agent 多值用空格连接,其余用逗号连接) foreach (var header in request.Headers) { bool isUserAgent = string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase); sb.Append(header.Key).Append(": ") .Append(string.Join(isUserAgent ? " " : ",", header.Value)).Append("\r\n"); } // Content 头(Content-Type / Content-Length 等) if (request.Content != null) { foreach (var header in request.Content.Headers) { sb.Append(header.Key).Append(": ") .Append(string.Join(",", header.Value)).Append("\r\n"); } } sb.Append("\r\n"); var headBytes = Encoding.ASCII.GetBytes(sb.ToString()); await buffered.WriteAsync(headBytes, 0, headBytes.Length, ct).ConfigureAwait(false); if (bodyBytes != null && bodyBytes.Length > 0) { await buffered.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct).ConfigureAwait(false); } await buffered.FlushAsync(ct).ConfigureAwait(false); // ---- 4. 读取响应头 ---- string statusLine = await ReadLineAsync(buffered, ct).ConfigureAwait(false); if (string.IsNullOrEmpty(statusLine)) { throw new HttpRequestException("服务器返回空响应(连接被关闭)"); } var statusParts = statusLine.Split(' '); if (statusParts.Length < 2 || !statusParts[0].StartsWith("HTTP/", StringComparison.Ordinal)) { throw new HttpRequestException($"无法解析 HTTP 状态行: {statusLine}"); } int statusCode = int.Parse(statusParts[1], CultureInfo.InvariantCulture); string reasonPhrase = statusParts.Length > 2 ? string.Join(" ", statusParts, 2, statusParts.Length - 2) : string.Empty; var responseHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase); string line; while (!string.IsNullOrEmpty(line = await ReadLineAsync(buffered, ct).ConfigureAwait(false))) { int colon = line.IndexOf(':'); if (colon <= 0) continue; var name = line.Substring(0, colon).Trim(); var value = line.Substring(colon + 1).Trim(); if (!responseHeaders.TryGetValue(name, out var list)) { list = new List(); responseHeaders[name] = list; } list.Add(value); } // ---- 5. 构造流式响应体(chunked 解码 + gzip/deflate 解压 + 每段空闲超时) ---- var bodyStream = CreateBodyStream(buffered, responseHeaders); // ---- 6. 包装为 HttpResponseMessage ---- var msg = new HttpResponseMessage((HttpStatusCode)statusCode) { ReasonPhrase = string.IsNullOrEmpty(reasonPhrase) ? null : reasonPhrase, Content = new StreamContent(bodyStream) }; foreach (var kv in responseHeaders) { if (IsContentHeader(kv.Key)) { msg.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } else { msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } } // 连接所有权移交给 bodyStream,Dispose 响应时按序关闭(buffered → tlsStream → tcp) ((TlsBodyStream)bodyStream).AttachConnection(tcp, buffered); return msg; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { buffered.Dispose(); tcp.Dispose(); throw; } catch (OperationCanceledException ex) { buffered.Dispose(); tcp.Dispose(); throw new HttpRequestException($"请求超时({DefaultTimeoutMilliseconds}ms): {uri.Host}", ex); } catch (Exception ex) when (!(ex is HttpRequestException)) { buffered.Dispose(); tcp.Dispose(); throw new HttpRequestException($"HTTP 请求失败: {uri.Host} — {ex.Message}", ex); } } /// /// 按响应头构造流式读取链:Transfer-Encoding: chunked → ChunkedDecodingStream;Content-Encoding → gzip/deflate 解压流。 /// 连接(tcp/buffered)随后通过 TlsBodyStream.AttachConnection 移交。 /// private static TlsBodyStream CreateBodyStream(Stream buffered, Dictionary> headers) { Stream stream = buffered; if (headers.TryGetValue("Transfer-Encoding", out var tes)) { string te = string.Join(",", tes).ToLowerInvariant(); if (te.Contains("chunked")) { stream = new ChunkedDecodingStream(stream); } } if (headers.TryGetValue("Content-Encoding", out var encodings) && encodings.Count > 0) { string encoding = encodings[0].ToLowerInvariant(); if (encoding == "gzip") { stream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); } else if (encoding == "deflate") { stream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true); } // 解压后字节数不再等于 Content-Length,移除该头避免下游按 Content-Length 校验/判进度时误判 headers.Remove("Content-Encoding"); headers.Remove("Content-Length"); } return new TlsBodyStream(stream, headers); } /// 按 Transfer-Encoding / Content-Length / EOF 读取响应体。 private static async Task ReadBodyAsync(Stream stream, Dictionary> headers, CancellationToken ct) { if (headers.TryGetValue("Transfer-Encoding", out var tes)) { string te = string.Join(",", tes).ToLowerInvariant(); if (te.Contains("chunked")) { return await ReadChunkedAsync(stream, ct).ConfigureAwait(false); } } if (headers.TryGetValue("Content-Length", out var cl) && cl.Count > 0 && long.TryParse(cl[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out long length)) { return await ReadExactlyAsync(stream, length, ct).ConfigureAwait(false); } // 无长度信息(Connection: close 场景)→ 读到 EOF return await ReadAllAsync(stream, ct).ConfigureAwait(false); } private static async Task ReadChunkedAsync(Stream stream, CancellationToken ct) { using var ms = new MemoryStream(); while (true) { string sizeLine = await ReadLineAsync(stream, ct).ConfigureAwait(false); if (string.IsNullOrEmpty(sizeLine)) break; int semi = sizeLine.IndexOf(';'); if (semi >= 0) sizeLine = sizeLine.Substring(0, semi); int size = int.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture); if (size == 0) { // 读取 trailer 直到空行 while (!string.IsNullOrEmpty(await ReadLineAsync(stream, ct).ConfigureAwait(false))) { } break; } var chunk = await ReadExactlyAsync(stream, size, ct).ConfigureAwait(false); ms.Write(chunk, 0, chunk.Length); await ReadExactlyAsync(stream, 2, ct).ConfigureAwait(false); // \r\n } return ms.ToArray(); } private static async Task ReadExactlyAsync(Stream stream, long count, CancellationToken ct) { if (count > int.MaxValue) throw new HttpRequestException("响应体过大"); var buffer = new byte[(int)count]; int offset = 0; while (offset < buffer.Length) { int read = await stream.ReadAsync(buffer, offset, buffer.Length - offset, ct).ConfigureAwait(false); if (read == 0) throw new HttpRequestException($"响应体提前结束(期望 {count} 字节,实际 {offset})"); offset += read; } return buffer; } private static async Task ReadAllAsync(Stream stream, CancellationToken ct) { using var ms = new MemoryStream(); var buffer = new byte[81920]; while (true) { int read = await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false); if (read == 0) break; ms.Write(buffer, 0, read); } return ms.ToArray(); } /// 逐行读取(\r\n 结尾,不含结尾符)。 private static async Task ReadLineAsync(Stream stream, CancellationToken ct) { var sb = new StringBuilder(); var buffer = new byte[1]; int prev = -1; while (true) { int read = await stream.ReadAsync(buffer, 0, 1, ct).ConfigureAwait(false); if (read == 0) { if (sb.Length == 0) return null; break; // EOF,返回已收集内容 } int b = buffer[0]; if (prev == '\r' && b == '\n') { sb.Remove(sb.Length - 1, 1); // 去掉 \r break; } prev = b; sb.Append((char)b); } return sb.ToString(); } private static byte[] ReadAllBytes(Stream stream) { using var ms = new MemoryStream(); stream.CopyTo(ms); return ms.ToArray(); } private static readonly HashSet ContentHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) { "Content-Type", "Content-Length", "Content-Encoding", "Content-Language", "Content-Location", "Content-Range", "Content-Disposition", "Content-MD5", "Expires", "Last-Modified", "Allow", "Content-Transfer-Encoding", "Content-Security-Policy" }; private static bool IsContentHeader(string name) => ContentHeaders.Contains(name); /// /// 流式响应体读取流:每段 ReadAsync 带空闲超时(无总时长限制),Dispose 时按序关闭 /// buffered → tlsStream(close_notify)→ tcp,连接由 AttachConnection 在响应成功后移交。 /// private sealed class TlsBodyStream : Stream { private readonly Stream _inner; // 解码链后的流(可能为 ChunkedDecodingStream / GZipStream 包装) private readonly Dictionary> _headers; private TcpClient _tcp; private Stream _buffered; private bool _disposed; public TlsBodyStream(Stream inner, Dictionary> headers) { _inner = inner; _headers = headers; } public void AttachConnection(TcpClient tcp, Stream buffered) { _tcp = tcp; _buffered = buffered; } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { } public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { using var idleCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); idleCts.CancelAfter(IdleTimeoutMilliseconds); try { return await _inner.ReadAsync(buffer, offset, count, idleCts.Token).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new HttpRequestException($"读取响应数据超时(空闲 {IdleTimeoutMilliseconds}ms 无数据)"); } } protected override void Dispose(bool disposing) { if (_disposed) return; _disposed = true; if (disposing) { try { _inner.Dispose(); } catch { } // 先关闭解码链(GZipStream 等 leaveOpen 不关底层) try { _buffered?.Dispose(); } catch { } // buffered → tlsStream close_notify try { _tcp?.Dispose(); } catch { } } base.Dispose(disposing); } } /// HTTP chunked 分块解码流:隐藏分块帧,向调用方暴露连续的解码后数据。 private sealed class ChunkedDecodingStream : Stream { private readonly Stream _inner; private long _chunkRemaining; private bool _finished; public ChunkedDecodingStream(Stream inner) => _inner = inner; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { } public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_finished) return 0; while (_chunkRemaining == 0) { // 读取下一个 chunk 大小行(hex) string sizeLine = await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(sizeLine)) { // EOF 提前结束:正常终止必须读到 0 长度的终止块,这里说明连接被中途掐断 throw new HttpRequestException("chunked 响应体提前结束(未读到终止块)"); } int semi = sizeLine.IndexOf(';'); if (semi >= 0) sizeLine = sizeLine.Substring(0, semi); _chunkRemaining = long.Parse(sizeLine.Trim(), NumberStyles.HexNumber, CultureInfo.InvariantCulture); if (_chunkRemaining == 0) { // 终止块:读取 trailer 直到空行 while (!string.IsNullOrEmpty(await ReadChunkLineAsync(cancellationToken).ConfigureAwait(false))) { } _finished = true; return 0; } } int toRead = (int)Math.Min(count, _chunkRemaining); int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken).ConfigureAwait(false); if (read == 0) throw new HttpRequestException("chunked 响应体提前结束"); _chunkRemaining -= read; if (_chunkRemaining == 0) { // 吃掉 chunk 后的 \r\n var crlf = new byte[2]; int got = 0; while (got < 2) { int r = await _inner.ReadAsync(crlf, got, 2 - got, cancellationToken).ConfigureAwait(false); if (r == 0) break; got += r; } } return read; } /// 逐字节读一行(\r\n 结尾,不含结尾符),复用于 chunk 大小行与 trailer。 private async Task ReadChunkLineAsync(CancellationToken cancellationToken) { var sb = new StringBuilder(); var buffer = new byte[1]; int prev = -1; while (true) { int read = await _inner.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false); if (read == 0) { if (sb.Length == 0) return null; break; } int b = buffer[0]; if (prev == '\r' && b == '\n') { sb.Remove(sb.Length - 1, 1); // 去掉 \r break; } prev = b; sb.Append((char)b); } return sb.ToString(); } protected override void Dispose(bool disposing) { if (disposing) _inner.Dispose(); base.Dispose(disposing); } } } }