当前位置: 首页 > news >正文

HttpClient 发送请求封装

该类主要用于发送 HTTP 请求,并支持一些高级功能,例如代理设置和内容压缩。以下是对代码的详细分析:

主要功能

  1. HttpClient 配置

    • 使用HttpClientHandler配置 HTTP 客户端,包括代理设置和请求头信息。
    • 支持为请求设置自定义头部。
  2. 代理设置

    • 允许通过ProxySetting配置代理,包括协议、地址、端口以及凭据。
    • 通过IWebProxy设置代理,支持基本身份验证。
  3. SSL 证书验证

    • 支持 HTTPS 请求,通过trustCertificate方法信任 SSL 证书,确保连接的安全性。
    • 实现了自定义的服务器证书验证回调。
  4. HTTP 方法支持

    • 支持 GET、POST、PUT 和 DELETE 等 HTTP 方法。
    • 提供同步和异步两种方式的调用方法。
  5. 内容压缩

    • 支持 GZip 和 Deflate 压缩类型,使用CompressedContent类对请求体进行压缩。

代码结构

  • HttpUtil 类

    • 包含主要的 HTTP 调用逻辑。
    • HttpClientSettings方法用于配置和返回HttpClient实例。
    • HttpclientCallHttpclientCallAsync方法用于执行实际的 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 }
http://www.cnnetsun.cn/news/3565139.html

相关文章:

  • 内存泄漏系列专题分析之五:使用malloc_debug定位C/C++ native heap内存泄露
  • Reducer 是什么?多个节点如何安全更新状态
  • OpenZFS内核模块编译与调试:深入理解文件系统架构的终极指南 [特殊字符]
  • 【亲测免费】 picacomic-downloader:快速下载哔咔漫画的利器
  • Apache Airflow 3.0完整指南:5分钟构建企业级数据工作流自动化系统
  • 通义千问CLI终极指南:从命令行到智能代理的技术深度解析
  • 打造个性化轮播:使用LESS自定义jQuery.Flipster主题的完整教程
  • 5分钟快速入门DeepXDE:科学机器学习与物理信息学习的终极指南
  • 想听全球电台?这款轻量神器收录10万+频道,躺着也能录节目!
  • 农耕劳动是优质刚需,补齐生产劳动核心短板
  • 告别动漫下载卡顿:3步配置专业Tracker加速方案
  • 2026网盘不限速实测:直链下载助手pandownload安装指南
  • 在k8s环境部署Apache Seatunnel2.3.13
  • 深入解析SSI接口:从SPI基础到TI M3实战配置与调试
  • 终极相机参数水印工具:5分钟学会为照片批量添加专业水印
  • rafx编辑器插件开发:自定义资产导入工具终极指南 [特殊字符]
  • 零售旺季呼叫中心从200到2000坐席平滑扩容:3阶段实施方案
  • Zotero-Dark-Theme未来展望:即将支持的新功能与改进方向
  • 基于协同过滤推荐算法的云裳非物质文化商城平台
  • mac远程畅玩pc端游的方法 mac怎么远程玩pc游戏
  • Buzz语音转录工具:3步实现完全离线的音频转文字,保护隐私同时提升工作效率
  • AI流程图生成实战指南(提示词结构×视觉逻辑×工具链三重校准)
  • AI视频互动率优化的“临界点法则”(基于127万条真实视频行为数据建模)
  • NUXTOR企业级应用开发:构建可维护的桌面应用架构设计终极指南
  • 如何使用Chronotrains快速规划5小时欧洲火车旅行路线
  • Blender 3D打印完整指南:从模型修复到完美打印的终极教程
  • libsm64:如何将经典《超级马里奥64》游戏引擎嵌入现代游戏开发 [特殊字符]
  • GridPlayer:如何免费实现10个视频同步播放?多视频网格播放器完全指南
  • 从Apple到Google:Awesome Design Principles中的顶级设计系统深度解析
  • Windows系统优化终极指南:5分钟轻松清理150+预装应用