news 2026/7/21 16:57:37

HttpClient 发送请求封装

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/21 16:56:20

Reducer 是什么?多个节点如何安全更新状态

上一篇文章里&#xff0c;我们讨论了 Edge 和 Conditional Edge。 一个重要结论是&#xff1a; Edge 让流程可以分支&#xff0c;也可以回到前面的节点。 但一旦流程变复杂&#xff0c;就会遇到另一个问题&#xff1a; 多个节点如何安全更新同一份 State&#xff1f; 这就是 Re…

作者头像 李华
网站建设 2026/7/21 16:55:18

【亲测免费】 picacomic-downloader:快速下载哔咔漫画的利器

picacomic-downloader&#xff1a;快速下载哔咔漫画的利器 在数字化阅读的时代&#xff0c;漫画爱好者总希望找到一种高效的方式来收集自己喜欢的漫画资源。今天&#xff0c;我要向大家推荐一个开源项目——picacomic-downloader&#xff0c;一款能够帮助用户快速下载哔咔漫画的…

作者头像 李华
网站建设 2026/7/21 16:54:00

通义千问CLI终极指南:从命令行到智能代理的技术深度解析

通义千问CLI终极指南&#xff1a;从命令行到智能代理的技术深度解析 【免费下载链接】Qwen The official repo of Qwen (通义千问) chat & pretrained large language model proposed by Alibaba Cloud. 项目地址: https://gitcode.com/GitHub_Trending/qw/Qwen 在当…

作者头像 李华