HttpClient 发送请求封装

📅 2026/7/21 16:57:42
HttpClient 发送请求封装
该类主要用于发送 HTTP 请求并支持一些高级功能例如代理设置和内容压缩。以下是对代码的详细分析主要功能HttpClient 配置使用HttpClientHandler配置 HTTP 客户端包括代理设置和请求头信息。支持为请求设置自定义头部。代理设置允许通过ProxySetting配置代理包括协议、地址、端口以及凭据。通过IWebProxy设置代理支持基本身份验证。SSL 证书验证支持 HTTPS 请求通过trustCertificate方法信任 SSL 证书确保连接的安全性。实现了自定义的服务器证书验证回调。HTTP 方法支持支持 GET、POST、PUT 和 DELETE 等 HTTP 方法。提供同步和异步两种方式的调用方法。内容压缩支持 GZip 和 Deflate 压缩类型使用CompressedContent类对请求体进行压缩。代码结构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 namehttpClientFactoryhttp客户端工厂管理连接池/param /// param nameskipSslValidation是否跳过HTTPS证书校验true仅测试内网自签证书生产必须false/param public HttpUtil(IHttpClientFactory httpClientFactory, bool skipSslValidation false) { _httpClientFactory httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _skipSslValidation skipSslValidation; } #region 对外快捷重载业务推荐使用简化调用 /// summaryGET快捷请求仅Query参数无Body/summary public async Taskstring GetAsync(string url, Dictionarystring, string? headers null, ProxySetting? proxy null, int? timeout null) { return await HttpClientCallAsync(url, null, headers, proxy, HttpMethodType.GET, false, timeout); } /// summaryPOST快捷请求支持请求体压缩/summary public async Taskstring PostAsync(string url, HttpContent body, Dictionarystring, string? headers null, ProxySetting? proxy null, bool enableReqCompress false, int? timeout null) { return await HttpClientCallAsync(url, body, headers, proxy, HttpMethodType.POST, enableReqCompress, timeout); } /// summaryPUT快捷请求/summary public async Taskstring PutAsync(string url, HttpContent body, Dictionarystring, string? headers null, ProxySetting? proxy null, bool enableReqCompress false, int? timeout null) { return await HttpClientCallAsync(url, body, headers, proxy, HttpMethodType.PUT, enableReqCompress, timeout); } /// summaryDELETE快捷请求仅Query参数无Body/summary public async Taskstring DeleteAsync(string url, Dictionarystring, 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 nameurl目标地址 http/https/param /// param nameparamlist请求BodyGET/DELETE禁止传入Query参数拼接在url中/param /// param namecustomHeaders单次请求自定义头/param /// param nameproxySetting代理配置null不启用/param /// param namehttpMethodType请求方式/param /// param nameenableCompression是否压缩上传请求体/param /// param nametimeoutSeconds自定义超时默认30s/param /// returns解压后的原始响应文本/returns public async Taskstring HttpClientCallAsync( string url, HttpContent? paramlist, Dictionarystring, 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 不允许携带BodyQuery参数拼接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 // 默认Acceptapplication/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 Taskstring ReadAutoDecompressStringAsync(Stream rawStream, IEnumerablestring 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 }