C# 优雅实现 HttpClient 封装(可直接复用的工具类)

C# 优雅实现 HttpClient 封装(可直接复用的工具类)

在 C# 开发中,HttpClient 是发送 HTTP 请求的核心组件,但直接 new 实例的方式存在性能差、连接泄漏等问题。本文将分享「静态单例 + 通用方法封装」的最优实践,同时补充 .NET Core/.NET 5+ 推荐的 IHttpClientFactory 实现方案,提供可直接复制复用的工具类代码,适配不同 .NET 版本场景。

📌 本文核心价值:

  • 解决 HttpClient 频繁创建导致的性能问题

  • 提供 GET/POST(JSON/表单) 通用封装方法

  • 适配 .NET Framework 和 .NET Core 多场景

  • 包含完整异常处理、请求配置最佳实践

一、核心前提:为什么不建议每次 new HttpClient?

很多开发者习惯在每次请求时 new HttpClient,这种写法存在明显缺陷:

  1. 创建成本高:每次 new 会初始化新的 HttpMessageHandler(含连接池、SSL 上下文等重量级组件)

  2. 连接池无法复用:导致 TCP 连接频繁创建/销毁,增加网络开销,甚至引发端口耗尽

  3. 资源泄漏风险:未正确 Dispose 会导致 socket 连接长期占用(.NET Framework 中更明显)

  4. DNS 缓存问题:单例 HttpClient 可能缓存 DNS,导致域名解析变更后无法生效(.NET Core 前版本)

✅ 核心原则:HttpClient 应复用而非频繁创建,基于单例或工厂模式管理实例生命周期。

二、基础方案:静态单例 + 通用方法封装(.NET Framework 适用)

适合 .NET Framework 项目,通过静态工具类封装 HttpClient 单例,提供 GET/POST 通用方法,统一处理请求头、超时、异常等逻辑。

2.1 完整工具类代码

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

/// 
public static class HttpClientHelper
{
    // 全局单例 HttpClient(核心:复用连接池)
    private static readonly HttpClient _httpClient;

    /// 
    static HttpClientHelper()
    {
        // 1. 配置连接池、超时等核心参数
        var handler = new HttpClientHandler
        {
            // 限制每个服务器的最大并发连接数(根据目标服务能力调整,建议 50-200)
            MaxConnectionsPerServer = 100,
            // 允许自动重定向(根据业务需求调整)
            AllowAutoRedirect = true,
            // 【测试环境专用】忽略 SSL 证书错误(生产环境务必删除!)
            // ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
        };

        // 2. 初始化 HttpClient 实例
        _httpClient = new HttpClient(handler)
        {
            // 全局默认超时(30秒,避免请求无限挂起)
            Timeout = TimeSpan.FromSeconds(30)
        };

        // 3. 设置默认请求头(减少重复代码)
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
        _httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("CSharp-HttpClient-Tool/1.0");
    }

    #region 通用 GET 请求(返回字符串结果)
    /// 
    /// <param name="url">请求地址(必填)</param>
    /// <param name="headers">自定义请求头(可选,如 Authorization)</param>
    /// <returns>响应内容字符串</returns>
    /// <exception cref="ArgumentNullException">url 为空时抛出</exception>
    /// <exception cref="HttpRequestException">HTTP 请求失败时抛出(非 2xx 状态码)</exception>
    public static async Task<string> GetAsync(string url, HeaderDictionary headers = null)
    {
        // 入参校验
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "请求地址不能为空");

        // 构建请求消息
        using var request = new HttpRequestMessage(HttpMethod.Get, url);

        // 添加自定义请求头
        if (headers != null)
        {
            foreach (var header in headers)
            {
                // 尝试添加到请求头,失败则尝试添加到内容头
                if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value) &&
                    !request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value) ?? false)
                {
                    throw new InvalidOperationException($"无法添加请求头:{header.Key}");
                }
            }
        }

        // 发送请求(复用单例 HttpClient)
        using var response = await _httpClient.SendAsync(request);
        // 确保响应成功(非 2xx 状态码会抛 HttpRequestException)
        response.EnsureSuccessStatusCode();

        // 读取响应内容
        return await response.Content.ReadAsStringAsync();
    }
    #endregion

    #region 通用 POST 请求(JSON 入参)
    /// 
    /// <param name="url">请求地址(必填)</param>
    /// <param name="jsonBody">JSON 字符串(必填)</param>
    /// <param name="headers">自定义请求头(可选)</param>
    /// <returns>响应内容字符串</returns>
    /// <exception cref="ArgumentNullException">url 或 jsonBody 为空时抛出</exception>
    public static async Task<string> PostJsonAsync(string url, string jsonBody, HeaderDictionary headers = null)
    {
        // 入参校验
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "请求地址不能为空");
        if (string.IsNullOrEmpty(jsonBody))
            throw new ArgumentNullException(nameof(jsonBody), "JSON 入参不能为空");

        // 构建 JSON 内容(指定 UTF-8 编码和 application/json 格式)
        using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

        // 构建请求消息
        using var request = new HttpRequestMessage(HttpMethod.Post, url)
        {
            Content = content
        };

        // 添加自定义请求头
        if (headers != null)
        {
            foreach (var header in headers)
            {
                if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value) &&
                    !request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
                {
                    throw new InvalidOperationException($"无法添加请求头:{header.Key}");
                }
            }
        }

        // 发送请求并处理响应
        using var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
    #endregion

    #region 扩展:POST 表单请求(键值对入参)
    ///     /// <param name="url">请求地址(必填)</param>
    /// <param name="formData">表单数据(键值对)</param>
    /// <returns>响应内容字符串</returns>
    public static async Task<string> PostFormAsync(string url, FormUrlEncodedContent formData)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "请求地址不能为空");
        if (formData == null)
            throw new ArgumentNullException(nameof(formData), "表单数据不能为空");

        // 直接调用 PostAsync(简化表单请求逻辑)
        using var response = await _httpClient.PostAsync(url, formData);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
    #endregion
}

/// 
public class HeaderDictionary : Dictionary<string, string>
{
    public HeaderDictionary() : base(StringComparer.OrdinalIgnoreCase) { }
}

2.2 使用示例(复制即运行)

csharp 复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // 示例1:调用 GET 方法(带 Authorization 头)
        await TestGetAsync();

        // 示例2:调用 POST JSON 方法
        await TestPostJsonAsync();

        // 示例3:调用 POST 表单方法
        await TestPostFormAsync();
    }

    /// 
    private static async Task TestGetAsync()
    {
        try
        {
            // 自定义请求头(如 Token 认证)
            var headers = new HeaderDictionary
            {
                { "Authorization", "Bearer your_token_here" }
            };

            // 调用工具类方法
            string result = await HttpClientHelper.GetAsync(
                "https://api.example.com/data", headers);

            Console.WriteLine("GET 响应结果:" + result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("GET 请求失败:" + ex.Message);
        }
    }

    /// 
    private static async Task TestPostJsonAsync()
    {
        try
        {
            // 构造 JSON 入参
            string jsonBody = "{\"name\":\"测试用户\",\"age\":25}";

            // 调用工具类方法
            string result = await HttpClientHelper.PostJsonAsync(
                "https://api.example.com/submit", jsonBody);

            Console.WriteLine("POST JSON 响应结果:" + result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("POST JSON 请求失败:" + ex.Message);
        }
    }

    /// 
    private static async Task TestPostFormAsync()
    {
        try
        {
            // 构造表单数据(用户名密码登录)
            var formData = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("username", "admin"),
                new KeyValuePair<string, string>("password", "123456")
            });

            // 调用工具类方法
            string result = await HttpClientHelper.PostFormAsync(
                "https://api.example.com/login", formData);

            Console.WriteLine("POST 表单响应结果:" + result);
        }
        catch (Exception ex)
        {
            Console.WriteLine("POST 表单请求失败:" + ex.Message);
        }
    }
}

三、进阶方案:IHttpClientFactory 实现(.NET Core/.NET 5+ 推荐)

对于 .NET Core/.NET 5+ 项目,官方推荐使用IHttpClientFactory 管理 HttpClient 生命周期,解决了传统单例的 DNS 缓存问题,支持灵活配置和生命周期管理。

3.1 步骤1:注册服务(Program.cs)

csharp 复制代码
// .NET 6+ 极简模式(Program.cs)
var builder = WebApplication.CreateBuilder(args);

// 方式1:注册命名客户端(推荐,按业务分组配置)
builder.Services.AddHttpClient("DefaultClient", client =>
{
    // 基础地址(后续请求可省略域名)
    client.BaseAddress = new Uri("https://api.example.com/");
    // 全局超时配置
    client.Timeout = TimeSpan.FromSeconds(30);
    // 默认请求头
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
})
// 配置连接池和 Handler 生命周期
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    MaxConnectionsPerServer = 100, // 连接池最大并发数
    AllowAutoRedirect = true
})
// 每 5 分钟刷新 Handler(解决 DNS 缓存问题)
.SetHandlerLifetime(TimeSpan.FromMinutes(5));

// 注册自定义服务(后续注入使用)
builder.Services.AddScoped<HttpClientService>();

var app = builder.Build();

// 其他中间件配置...

app.Run();

3.2 步骤2:封装服务类

csharp 复制代码
using System;
using System.Net.Http;
using System.Threading.Tasks;

/// 
public class HttpClientService
{
    private readonly IHttpClientFactory _httpClientFactory;

    // 构造函数注入 IHttpClientFactory
    public HttpClientService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    /// 
    public async Task<string> GetAsync(string url, HeaderDictionary headers = null)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "请求地址不能为空");

        // 从工厂获取命名客户端("DefaultClient" 对应注册时的名称)
        var client = _httpClientFactory.CreateClient("DefaultClient");

        using var request = new HttpRequestMessage(HttpMethod.Get, url);

        // 添加自定义请求头
        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }
        }

        // 发送请求并处理响应
        using var response = await client.SendAsync(request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

    /// 
    public async Task<string> PostJsonAsync(string url, string jsonBody, HeaderDictionary headers = null)
    {
        if (string.IsNullOrEmpty(url))
            throw new ArgumentNullException(nameof(url), "请求地址不能为空");
        if (string.IsNullOrEmpty(jsonBody))
            throw new ArgumentNullException(nameof(jsonBody), "JSON 入参不能为空");

        var client = _httpClientFactory.CreateClient("DefaultClient");
        using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
        using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };

        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }
        }

        using var response = await client.SendAsync(request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

// 复用之前定义的 HeaderDictionary 辅助类
public class HeaderDictionary : Dictionary<string, string>
{
    public HeaderDictionary() : base(StringComparer.OrdinalIgnoreCase) { }
}

3.3 使用示例(Web 项目控制器)

csharp 复制代码
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

/// 
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
    private readonly HttpClientService _httpClientService;

    // 构造函数注入
    public TestController(HttpClientService httpClientService)
    {
        _httpClientService = httpClientService;
    }

    [HttpGet("data")]
    public async Task<IActionResult> GetData()
    {
        try
        {
            // 调用服务方法(此处 url 可省略基础地址,因为注册时配置了 BaseAddress)
            var result = await _httpClientService.GetAsync("data");
            return Ok(result);
        }
        catch (Exception ex)
        {
            return BadRequest($"请求失败:{ex.Message}");
        }
    }

    [HttpPost("submit")]
    public async Task<IActionResult> SubmitData([FromBody] UserInfo userInfo)
    {
        try
        {
            // 序列化对象为 JSON(需引用 Newtonsoft.Json 或使用 System.Text.Json)
            string jsonBody = System.Text.Json.JsonSerializer.Serialize(userInfo);
            var result = await _httpClientService.PostJsonAsync("submit", jsonBody);
            return Ok(result);
        }
        catch (Exception ex)
        {
            return BadRequest($"请求失败:{ex.Message}");
        }
    }
}

// 测试实体类
public class UserInfo
{
    public string Name { get; set; }
    public int Age { get; set; }
}

四、关键注意事项(避坑指南)

  1. 禁止每次请求 new HttpClient:无论哪种方案,核心都是复用实例,避免重复创建 HttpMessageHandler

  2. 不要手动 Dispose 复用的 HttpClient:Dispose 会销毁连接池,导致后续请求失败(using 语句仅用于 HttpRequestMessage/HttpResponseMessage)

  3. 必须处理非 2xx 状态码EnsureSuccessStatusCode() 会在非 2xx 时抛 HttpRequestException,需通过 try-catch 捕获并处理业务逻辑

  4. 合理配置超时:默认超时 100 秒,建议显式设置为 30 秒内(根据业务场景调整),避免请求长期挂起

  5. 限制连接池并发数MaxConnectionsPerServer 建议设置为 50-200,避免压垮目标服务器

  6. DNS 缓存问题处理:.NET Core 前版本使用静态单例时,若目标域名解析变更,需手动刷新 HttpMessageHandler;.NET Core/.NET 5+ 直接使用 IHttpClientFactory 即可

  7. 生产环境禁用 SSL 证书忽略 :测试环境可临时开启 ServerCertificateCustomValidationCallback,生产环境务必删除,避免安全风险

五、方案选型建议

项目类型 推荐方案 优势
.NET Framework(4.x) 静态单例 + 通用方法封装 简单易用,无需依赖注入,适配旧项目
.NET Core/.NET 5+(Web/控制台) IHttpClientFactory + 注入式服务 自动管理生命周期,解决 DNS 缓存问题,支持灵活配置
简单控制台/工具类项目 静态单例 HttpClient 代码简洁,无额外依赖

六、总结

本文提供的两种 HttpClient 封装方案,核心都是「复用实例、统一配置、简化调用」:

  • 基础方案适配 .NET Framework 旧项目,复制代码即可直接使用;

  • 进阶方案适配 .NET Core 新项目,符合官方最佳实践,扩展性更强。

通过封装,不仅能提升请求性能(连接池复用),还能统一处理异常、请求头、超时等逻辑,大幅降低重复编码成本。如果需要扩展其他请求类型(如 PUT/DELETE),可参考文中方法直接补充即可。

相关推荐
林太白2 小时前
Rust01-认识安装
开发语言·后端·rust
龙山云仓2 小时前
No095:沈括&AI:智能的科学研究与系统思维
开发语言·人工智能·python·机器学习·重构
IoT智慧学堂2 小时前
C语言循环结构综合应用篇(详细案例讲解)
c语言·开发语言
AuroraWanderll2 小时前
类和对象(三)-默认成员函数详解与运算符重载
c语言·开发语言·数据结构·c++·算法
青云交2 小时前
Java 大视界 -- Java+Spark 构建企业级用户画像平台:从数据采集到标签输出全流程(437)
java·开发语言·spark·hbase 优化·企业级用户画像·标签计算·高并发查询
航Hang*2 小时前
第3章:复习篇——第1节:创建和管理数据库
开发语言·数据库·笔记·sql·sqlserver
云栖梦泽2 小时前
鸿蒙原子化服务开发实战:构建免安装的轻量应用
开发语言·鸿蒙系统
YY&DS2 小时前
《Qt 手写 HTTP 登录服务实战》
开发语言·qt·http
阿华hhh2 小时前
数据结构(树)
linux·c语言·开发语言·数据结构