该类主要用于发送 HTTP 请求,并支持一些高级功能,例如代理设置和内容压缩。以下是对代码的详细分析:
主要功能
HttpClient 配置:
- 使用
HttpClientHandler配置 HTTP 客户端,包括代理设置和请求头信息。 - 支持为请求设置自定义头部。
- 使用
代理设置:
- 允许通过
ProxySetting配置代理,包括协议、地址、端口以及凭据。 - 通过
IWebProxy设置代理,支持基本身份验证。
- 允许通过
SSL 证书验证:
- 支持 HTTPS 请求,通过
trustCertificate方法信任 SSL 证书,确保连接的安全性。 - 实现了自定义的服务器证书验证回调。
- 支持 HTTPS 请求,通过
HTTP 方法支持:
- 支持 GET、POST、PUT 和 DELETE 等 HTTP 方法。
- 提供同步和异步两种方式的调用方法。
内容压缩:
- 支持 GZip 和 Deflate 压缩类型,使用
CompressedContent类对请求体进行压缩。
- 支持 GZip 和 Deflate 压缩类型,使用
代码结构
HttpUtil 类:
- 包含主要的 HTTP 调用逻辑。
HttpClientSettings方法用于配置和返回HttpClient实例。HttpclientCall和HttpclientCallAsync方法用于执行实际的 HTTP 请求。
CompressedContent 类:
- 继承自
HttpContent,用于将内容压缩后发送。 - 实现了压缩的序列化逻辑。
- 继承自
ProxySetting 和 ProxyCredentials 类:
- 分别用于存储代理设置和凭据的结构体。
CompressionType 枚举:
- 定义了支持的压缩类型。
using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; namespace HttpUtil { /// <summary> /// 通用HTTP/HTTPS请求工具类 /// 修复原版本池化HttpClient头污染、资源泄漏、证书硬放行、异常丢失堆栈等全部问题 /// 核心优化:基于HttpRequestMessage隔离单次请求头,不再修改池客户端DefaultRequestHeaders /// </summary> public class HttpUtil { private const string ClientName = "SensesMill"; private const int DefaultTimeoutSeconds = 30; private readonly IHttpClientFactory _httpClientFactory; private readonly bool _skipSslValidation; /// <summary> /// 构造注入 /// </summary> /// <param name="httpClientFactory">http客户端工厂,管理连接池</param> /// <param name="skipSslValidation">是否跳过HTTPS证书校验:true仅测试内网自签证书,生产必须false</param> public HttpUtil(IHttpClientFactory httpClientFactory, bool skipSslValidation = false) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _skipSslValidation = skipSslValidation; } #region 对外快捷重载(业务推荐使用,简化调用) /// <summary>GET快捷请求(仅Query参数,无Body)</summary> public async Task<string> GetAsync(string url, Dictionary<string, string>? headers = null, ProxySetting? proxy = null, int? timeout = null) { return await HttpClientCallAsync(url, null, headers, proxy, HttpMethodType.GET, false, timeout); } /// <summary>POST快捷请求,支持请求体压缩</summary> public async Task<string> PostAsync(string url, HttpContent body, Dictionary<string, string>? headers = null, ProxySetting? proxy = null, bool enableReqCompress = false, int? timeout = null) { return await HttpClientCallAsync(url, body, headers, proxy, HttpMethodType.POST, enableReqCompress, timeout); } /// <summary>PUT快捷请求</summary> public async Task<string> PutAsync(string url, HttpContent body, Dictionary<string, string>? headers = null, ProxySetting? proxy = null, bool enableReqCompress = false, int? timeout = null) { return await HttpClientCallAsync(url, body, headers, proxy, HttpMethodType.PUT, enableReqCompress, timeout); } /// <summary>DELETE快捷请求(仅Query参数,无Body)</summary> public async Task<string> DeleteAsync(string url, Dictionary<string, string>? headers = null, ProxySetting? proxy = null, int? timeout = null) { return await HttpClientCallAsync(url, null, headers, proxy, HttpMethodType.DELETE, false, timeout); } #endregion /// <summary> /// 底层通用请求入口 /// 关键优化:全部头信息绑定 HttpRequestMessage,不污染池化HttpClient全局默认头,并发安全 /// </summary> /// <param name="url">目标地址 http/https</param> /// <param name="paramlist">请求Body:GET/DELETE禁止传入,Query参数拼接在url中</param> /// <param name="customHeaders">单次请求自定义头</param> /// <param name="proxySetting">代理配置,null=不启用</param> /// <param name="httpMethodType">请求方式</param> /// <param name="enableCompression">是否压缩上传请求体</param> /// <param name="timeoutSeconds">自定义超时,默认30s</param> /// <returns>解压后的原始响应文本</returns> public async Task<string> HttpClientCallAsync( string url, HttpContent? paramlist, Dictionary<string, string>? customHeaders, ProxySetting? proxySetting, HttpMethodType httpMethodType, bool enableCompression = false, int? timeoutSeconds = null) { // 全局参数强校验 if (string.IsNullOrWhiteSpace(url)) throw new ArgumentException("请求地址url不能为空", nameof(url)); if (timeoutSeconds <= 0) throw new ArgumentException("超时时间必须大于0", nameof(timeoutSeconds)); // HTTP规范拦截:GET/DELETE 不允许携带Body,Query参数拼接URL即可 if ((httpMethodType is HttpMethodType.GET or HttpMethodType.DELETE) && paramlist != null) { throw new NotSupportedException($"[{httpMethodType}] 禁止携带请求Body,查询参数请拼接至Url地址"); } HttpClient? httpClient = null; HttpContent? requestBody = null; // 使用using自动释放消息、响应,淘汰finally手动Dispose await using var requestMsg = new HttpRequestMessage(); try { // 1. 创建客户端(代理/池化分支) httpClient = CreateHttpClient(proxySetting, timeoutSeconds); // 2. 初始化请求消息 requestMsg.RequestUri = new Uri(url); requestMsg.Method = httpMethodType switch { HttpMethodType.GET => HttpMethod.Get, HttpMethodType.POST => HttpMethod.Post, HttpMethodType.PUT => HttpMethod.Put, HttpMethodType.DELETE => HttpMethod.Delete, _ => throw new NotSupportedException($"不支持请求方法:{httpMethodType}") }; // 3. 构建请求体(压缩包装) if (enableCompression && paramlist != null) { requestBody = new CompressedContent(paramlist, CompressionType.GZip); } else { requestBody = paramlist; } requestMsg.Content = requestBody; // 4. 绑定单次请求头(仅作用当前消息,不会污染池客户端全局头,解决并发覆盖bug) // 默认Accept=application/json,用户自定义Accept会覆盖默认值 bool userSetAccept = customHeaders != null && customHeaders.ContainsKey("Accept"); if (!userSetAccept) { requestMsg.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); } // 追加自定义头,自动去重 if (customHeaders != null) { foreach (var kv in customHeaders) { var val = kv.Value?.Trim(); if (string.IsNullOrEmpty(val)) continue; requestMsg.Headers.Remove(kv.Key); requestMsg.Headers.TryAddWithoutValidation(kv.Key, val); } } // 5. 发送请求 await using var response = await httpClient.SendAsync(requestMsg); // 6. 非2xx状态码,携带完整响应体抛出,保留原始异常堆栈 if (!response.IsSuccessStatusCode) { var rawResp = await response.Content.ReadAsStreamAsync(); var respText = await ReadAutoDecompressStringAsync(rawResp, response.Content.Headers.ContentEncoding); throw new HttpRequestException( $"Http请求失败,Code={(int)response.StatusCode}, Url={url}, Resp={respText}", null, response.StatusCode); } // 7. 自动解压响应内容并返回字符串 await using var respStream = await response.Content.ReadAsStreamAsync(); return await ReadAutoDecompressStringAsync(respStream, response.Content.Headers.ContentEncoding); } catch (Exception ex) { // 不吞原始异常,InnerException完整保留,日志可打印原始异常类型与堆栈 throw new HttpRequestException($"请求接口异常 Url:{url}", ex); } finally { // 代理分支手动释放自建HttpClient;工厂池化客户端由框架管理,无需释放 if (proxySetting?.UseProxy == true) { httpClient?.Dispose(); } requestBody?.Dispose(); } } /// <summary> /// 创建HttpClient,区分代理/池化分支 /// 修复点:证书校验开关由构造注入,不硬编码true;代理端口数字校验 /// </summary> private HttpClient CreateHttpClient(ProxySetting? proxySetting, int? timeoutSeconds) { var timeout = TimeSpan.FromSeconds(timeoutSeconds ?? DefaultTimeoutSeconds); HttpClient client; // 启用代理分支 if (proxySetting?.UseProxy == true) { // 代理字段完整校验 if (string.IsNullOrWhiteSpace(proxySetting.Protocol) || string.IsNullOrWhiteSpace(proxySetting.Address) || string.IsNullOrWhiteSpace(proxySetting.Port)) { throw new ArgumentException("代理Protocol/Address/Port不能为空", nameof(proxySetting)); } // 端口强数字校验,防止拼接非法URL if (!int.TryParse(proxySetting.Port, out int proxyPort) || proxyPort is <= 0 or > 65535) { throw new ArgumentException($"代理端口格式非法:{proxySetting.Port},必须为1~65535数字"); } var proxyUrl = $"{proxySetting.Protocol}://{proxySetting.Address}:{proxyPort}"; var handler = new HttpClientHandler { Proxy = new WebProxy(proxyUrl, false), UseProxy = true }; // 证书校验开关(可配置,生产默认关闭) if (_skipSslValidation) { handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; } // 代理账号认证 if (proxySetting.ProxyCredentials != null && !string.IsNullOrEmpty(proxySetting.ProxyCredentials.UserName) && !string.IsNullOrEmpty(proxySetting.ProxyCredentials.Password)) { handler.Proxy.Credentials = new NetworkCredential( proxySetting.ProxyCredentials.UserName, proxySetting.ProxyCredentials.Password); } client = new HttpClient(handler); } // 无代理:使用池化客户端,不再修改DefaultRequestHeaders,彻底解决头污染 else { client = _httpClientFactory.CreateClient(ClientName); } client.Timeout = timeout; return client; } /// <summary> /// 自动解压响应流:支持gzip/deflate,解决服务端返回压缩内容乱码问题 /// </summary> private async Task<string> ReadAutoDecompressStringAsync(Stream rawStream, IEnumerable<string> encodings) { Stream targetStream = rawStream; foreach (var enc in encodings) { if (enc.Equals("gzip", StringComparison.OrdinalIgnoreCase)) { targetStream = new GZipStream(targetStream, CompressionMode.Decompress, leaveOpen: true); } else if (enc.Equals("deflate", StringComparison.OrdinalIgnoreCase)) { targetStream = new DeflateStream(targetStream, CompressionMode.Decompress, leaveOpen: true); } } await using var reader = new StreamReader(targetStream, Encoding.UTF8); return await reader.ReadToEndAsync(); } } /// <summary> /// 请求体压缩包装器,修复头部重复添加、完善异步释放 /// </summary> public class CompressedContent : HttpContent { private readonly HttpContent _originalContent; private readonly CompressionType _compressionMethod; public CompressedContent(HttpContent content, CompressionType compressionMethod) { _originalContent = content ?? throw new ArgumentNullException(nameof(content)); if (compressionMethod is not CompressionType.GZip and not CompressionType.Deflate) throw new ArgumentException("仅支持GZip/Deflate压缩", nameof(compressionMethod)); _compressionMethod = compressionMethod; // 复制原始Header,先清理同键避免重复 foreach (var header in _originalContent.Headers) { Headers.Remove(header.Key); Headers.TryAddWithoutValidation(header.Key, header.Value); } Headers.ContentEncoding.Add(_compressionMethod.ToString().ToLowerInvariant()); } protected override bool TryComputeLength(out long length) { length = -1; return false; } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { switch (_compressionMethod) { case CompressionType.GZip: await using (var zip = new GZipStream(stream, CompressionMode.Compress, leaveOpen: true)) await _originalContent.CopyToAsync(zip); break; case CompressionType.Deflate: await using (var deflate = new DeflateStream(stream, CompressionMode.Compress, leaveOpen: true)) await _originalContent.CopyToAsync(deflate); break; } } // 同步释放原始内容 protected override void Dispose(bool disposing) { if (disposing) _originalContent.Dispose(); base.Dispose(disposing); } // 兼容异步释放 protected override ValueTask DisposeAsyncCore() { return _originalContent.DisposeAsync(); } } #region 代理模型 public class ProxySetting { public bool UseProxy { get; set; } public string Protocol { get; set; } = string.Empty; public string Address { get; set; } = string.Empty; /// <summary>端口字符串,内部会自动强转数字校验</summary> public string Port { get; set; } = string.Empty; public ProxyCredentials? ProxyCredentials { get; set; } } public class ProxyCredentials { public string UserName { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } #endregion #region 枚举 public enum CompressionType { GZip, Deflate } public enum HttpMethodType { GET, POST, PUT, DELETE } #endregion }