怪奇物语

怪奇物语

首页 新随笔 联系 管理

C# HTTP请求重试处理器:实现与使用指南

在网络请求场景中,临时网络波动、服务器短暂不可用等问题时常发生。为提高HTTP请求的稳定性,重试机制是一种常见且有效的解决方案。本文将详细解析一个C#编写的HttpRetryHandler类,包括其核心功能、代码注解及使用方法,方便开发者集成到项目中。

一、类核心功能概述

HttpRetryHandler是一个通用的HTTP请求重试处理器,主要解决以下问题:

  1. 网络不稳定重试:当HTTP请求因临时错误失败时,自动重试(支持自定义重试次数)。
  2. 指数退避延迟:重试间隔采用“指数退避”策略(避免短时间内频繁请求给服务器造成压力)。
  3. 代理支持:可配置HTTP代理(适用于需要通过代理访问外部服务的场景)。
  4. 超时控制:自定义HTTP请求超时时间(防止请求长时间阻塞)。
  5. 异常封装:达到最大重试次数仍失败时,封装原始异常并抛出(便于问题定位)。

二、完整代码与详细注解

以下是HttpRetryHandler的完整代码,关键逻辑已通过注释说明:

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

/// <summary>
/// HTTP请求重试处理器
/// 功能:封装HTTP请求的自动重试、代理配置、超时控制、指数退避延迟
/// 适用场景:网络不稳定的HTTP请求(如API调用、文件下载等)
/// </summary>
public class HttpRetryHandler : IDisposable // 补充:实现IDisposable释放HttpClient资源(原代码未加,建议添加)
{
    // 私有字段:HTTP客户端(核心工具,用于发送请求)
    private readonly HttpClient _httpClient;
    // 私有字段:最大重试次数(请求失败后最多重试几次)
    private readonly int _maxRetryCount;
    // 私有字段:初始重试延迟(第一次重试前的等待时间,单位:秒)
    private readonly int _initialRetryDelaySeconds;
    // 私有字段:标记是否已释放资源(用于IDisposable)
    private bool _disposed = false;

    /// <summary>
    /// 构造函数:初始化HTTP重试处理器
    /// </summary>
    /// <param name="proxyIp">代理IP地址(含端口),如"http://127.0.0.1:8888";无需代理则传null或空字符串</param>
    /// <param name="timeoutSeconds">HTTP请求超时时间(单位:秒),默认100秒</param>
    /// <param name="maxRetryCount">最大重试次数,默认3次(即:1次原始请求+3次重试,共4次)</param>
    /// <param name="initialRetryDelaySeconds">初始重试延迟(单位:秒),默认15秒</param>
    public HttpRetryHandler(
        string? proxyIp, 
        int timeoutSeconds = 100, 
        int maxRetryCount = 3, 
        int initialRetryDelaySeconds = 15)
    {
        // 校验参数合法性(补充:原代码未加参数校验,建议添加以避免无效配置)
        if (timeoutSeconds <= 0)
            throw new ArgumentOutOfRangeException(nameof(timeoutSeconds), "超时时间必须大于0秒");
        if (maxRetryCount < 0)
            throw new ArgumentOutOfRangeException(nameof(maxRetryCount), "最大重试次数不能为负数");
        if (initialRetryDelaySeconds < 0)
            throw new ArgumentOutOfRangeException(nameof(initialRetryDelaySeconds), "初始重试延迟不能为负数");

        // 根据是否配置代理,创建对应的HttpClient
        _httpClient = string.IsNullOrEmpty(proxyIp) 
            ? CreateHttpClient(null, timeoutSeconds) 
            : CreateHttpClient(proxyIp, timeoutSeconds);

        // 初始化重试配置
        _maxRetryCount = maxRetryCount;
        _initialRetryDelaySeconds = initialRetryDelaySeconds;
    }

    /// <summary>
    /// 私有方法:创建HttpClient实例(封装HttpClient的创建逻辑,解耦构造函数)
    /// </summary>
    /// <param name="proxyIp">代理地址</param>
    /// <param name="timeoutSeconds">超时时间(秒)</param>
    /// <returns>配置完成的HttpClient</returns>
    private HttpClient CreateHttpClient(string? proxyIp, int timeoutSeconds)
    {
        // 1. 创建HttpClient处理器(负责底层HTTP协议细节,如代理、证书验证等)
        var handler = new HttpClientHandler();

        // 2. 配置代理(若传入代理地址)
        if (!string.IsNullOrEmpty(proxyIp))
        {
            // 验证代理地址格式(补充:原代码未验证,避免无效URI)
            if (!Uri.TryCreate(proxyIp, UriKind.Absolute, out var proxyUri))
                throw new ArgumentException($"代理地址格式无效:{proxyIp}", nameof(proxyIp));
            
            // 设置代理:BypassProxyOnLocal=true表示"本地请求不走代理"
            handler.Proxy = new WebProxy(proxyUri) { BypassProxyOnLocal = true };
            // 补充:若代理需要认证,可在此处添加 credentials(如handler.Proxy.Credentials = new NetworkCredential("用户名", "密码");)
        }

        // 3. 补充:默认情况下HttpClientHandler会验证SSL证书,若需忽略证书错误(仅测试环境用),可解除以下注释
        // handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;

        // 4. 创建HttpClient并配置超时时间
        var client = new HttpClient(handler);
        client.Timeout = TimeSpan.FromSeconds(timeoutSeconds); // 超时后直接抛出TaskCanceledException

        return client;
    }

    /// <summary>
    /// 核心方法:执行带重试机制的HTTP请求
    /// </summary>
    /// <typeparam name="T">请求返回值类型(如string、HttpResponseMessage、自定义模型等)</typeparam>
    /// <param name="operation">HTTP请求委托(传入HttpClient,返回异步任务)</param>
    /// <returns>请求成功后的返回值(T类型)</returns>
    /// <exception cref="Exception">达到最大重试次数仍失败时,抛出封装后的异常</exception>
    public async Task<T> ExecuteWithRetryAsync<T>(Func<HttpClient, Task<T>> operation)
    {
        // 校验委托参数(避免空委托导致的空引用异常)
        if (operation == null)
            throw new ArgumentNullException(nameof(operation), "HTTP请求委托不能为空");

        int retryCount = 0; // 已重试次数(初始为0,代表未重试)

        // 循环重试:直到请求成功或达到最大重试次数
        while (true)
        {
            try
            {
                // 执行HTTP请求委托(传入内部维护的HttpClient)
                return await operation(_httpClient);
            }
            catch (Exception ex)
            {
                // 1. 重试次数+1
                retryCount++;
                Console.WriteLine($"请求失败(第 {retryCount} 次尝试):{ex.Message}");

                // 2. 检查是否达到最大重试次数:是则抛出异常,终止循环
                if (retryCount >= _maxRetryCount)
                {
                    // 封装异常:包含重试次数信息,便于定位问题(InnerException为原始异常)
                    throw new Exception($"已达到最大重试次数({_maxRetryCount} 次),请求最终失败", ex);
                }

                // 3. 计算重试延迟(指数退避算法):delay = 初始延迟 * 2^(重试次数-1)
                // 例:初始延迟15秒,第1次重试延迟15*2^0=15秒,第2次重试延迟15*2^1=30秒,第3次60秒...
                int delaySeconds = (int)(_initialRetryDelaySeconds * Math.Pow(2, retryCount - 1));
                Console.WriteLine($"将在 {delaySeconds} 秒后进行第 {retryCount + 1} 次重试...");

                // 4. 等待延迟时间(异步等待,不阻塞线程)
                await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
            }
        }
    }

    /// <summary>
    /// 释放资源(实现IDisposable,避免HttpClient资源泄漏)
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // 告诉GC无需执行析构函数
    }

    /// <summary>
    /// 实际释放逻辑(分离托管资源和非托管资源释放)
    /// </summary>
    /// <param name="disposing">是否为手动调用Dispose(true=手动,false=GC调用)</param>
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        // 释放托管资源(HttpClient实现了IDisposable)
        if (disposing)
        {
            _httpClient?.Dispose();
        }

        // 若有非托管资源,可在此处释放(本类无,故省略)

        _disposed = true;
    }

    /// <summary>
    /// 析构函数(防止忘记手动调用Dispose时,GC仍能释放资源)
    /// </summary>
    ~HttpRetryHandler()
    {
        Dispose(false);
    }
}

三、关键技术点解析

1. 指数退避延迟(Exponential Backoff)

重试间隔采用“指数退避”策略,核心公式为:
delaySeconds = initialRetryDelaySeconds * 2^(retryCount - 1)

示例(初始延迟15秒,最大重试3次):

  • 第1次失败后:等待15秒(15*2⁰),进行第2次尝试;
  • 第2次失败后:等待30秒(15*2¹),进行第3次尝试;
  • 第3次失败后:等待60秒(15*2²),进行第4次尝试;
  • 第4次失败:达到最大重试次数,抛出异常。

优势:避免短时间内频繁重试给服务器造成“雪上加霜”的压力,同时兼顾重试效率。

2. HttpClient的正确使用

  • 资源释放:原代码未实现IDisposable,可能导致HttpClient资源泄漏(如TCP连接未释放)。补充IDisposable接口后,可通过using语句自动释放资源。
  • 超时控制:通过client.Timeout设置全局超时(超时后抛出TaskCanceledException,会被重试逻辑捕获并重试)。
  • 代理配置:通过WebProxy类实现代理支持,若代理需要认证,可补充NetworkCredential(见代码注释)。

3. 异常处理

  • 异常封装:达到最大重试次数时,抛出新异常并将原始异常作为InnerException,保留完整错误堆栈(便于定位问题)。
  • 参数校验:补充了参数合法性校验(如超时时间不能为负、委托不能为null),避免无效配置导致的隐性bug。

四、使用方法示例

HttpRetryHandler通过委托(Func) 接收HTTP请求逻辑,支持任意返回值类型(如stringHttpResponseMessage、自定义模型),灵活性极高。以下是3个常见场景的使用示例:

示例1:发送GET请求,获取字符串响应

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main(string[] args)
    {
        // 1. 初始化重试处理器(无代理,超时30秒,最大重试2次,初始延迟10秒)
        using (var retryHandler = new HttpRetryHandler(
            proxyIp: null,
            timeoutSeconds: 30,
            maxRetryCount: 2,
            initialRetryDelaySeconds: 10))
        {
            try
            {
                // 2. 定义HTTP请求逻辑(委托:传入HttpClient,返回Task<string>)
                var getRequest = async (client) =>
                {
                    string url = "https://jsonplaceholder.typicode.com/todos/1"; // 测试API
                    HttpResponseMessage response = await client.GetAsync(url);
                    
                    // 补充:若HTTP状态码为错误(如404、500),需手动抛出异常触发重试
                    response.EnsureSuccessStatusCode(); // 状态码非2xx时抛出HttpRequestException
                    
                    return await response.Content.ReadAsStringAsync(); // 返回响应字符串
                };

                // 3. 执行带重试的请求
                string result = await retryHandler.ExecuteWithRetryAsync(getRequest);
                Console.WriteLine("请求成功!响应内容:");
                Console.WriteLine(result);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"请求最终失败:{ex.Message}");
                Console.WriteLine($"原始错误:{ex.InnerException?.Message}");
            }
        }
    }
}

示例2:发送POST请求,提交JSON数据

using System;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    // 自定义模型(与API请求体结构对应)
    public class Todo
    {
        public int UserId { get; set; }
        public string Title { get; set; } = string.Empty;
        public bool Completed { get; set; }
    }

    static async Task Main(string[] args)
    {
        // 1. 初始化重试处理器(使用代理,如"http://127.0.0.1:8888")
        using (var retryHandler = new HttpRetryHandler(
            proxyIp: "http://127.0.0.1:8888",
            timeoutSeconds: 60,
            maxRetryCount: 3))
        {
            try
            {
                // 2. 构造POST请求数据
                var todo = new Todo { UserId = 1, Title = "测试POST请求", Completed = false };
                string json = JsonSerializer.Serialize(todo);
                var content = new StringContent(json, Encoding.UTF8, MediaTypeHeaderValue.Parse("application/json"));

                // 3. 定义POST请求逻辑(返回值为Todo类型,即API响应的反序列化结果)
                var postRequest = async (client) =>
                {
                    string url = "https://jsonplaceholder.typicode.com/todos";
                    HttpResponseMessage response = await client.PostAsync(url, content);
                    response.EnsureSuccessStatusCode(); // 触发错误状态码的重试
                    
                    // 反序列化响应为自定义模型
                    return await response.Content.ReadFromJsonAsync<Todo>();
                };

                // 4. 执行请求
                Todo responseTodo = await retryHandler.ExecuteWithRetryAsync(postRequest);
                Console.WriteLine($"POST成功!新增Todo的ID:{responseTodo?.Id}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"POST失败:{ex.Message}");
            }
        }
    }
}

示例3:下载文件(返回值为void)

若请求无需返回值(如文件下载),可将返回值类型设为Task(即T=Task):

static async Task DownloadFileWithRetry()
{
    using (var retryHandler = new HttpRetryHandler(null, timeoutSeconds: 120))
    {
        try
        {
            // 定义文件下载逻辑(返回Task,即T=Task)
            var downloadRequest = async (client) =>
            {
                string fileUrl = "https://example.com/large-file.zip";
                string savePath = "D:/download/large-file.zip";

                using (var response = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
                {
                    response.EnsureSuccessStatusCode();
                    using (var fileStream = System.IO.File.Create(savePath))
                    {
                        // 流式下载(避免内存占用过高)
                        await response.Content.CopyToAsync(fileStream);
                    }
                }
                return Task.CompletedTask; // 无返回值,返回空Task
            };

            // 执行下载(注意:T=Task,故返回值为Task,需await)
            await retryHandler.ExecuteWithRetryAsync(downloadRequest);
            Console.WriteLine("文件下载成功!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"下载失败:{ex.Message}");
        }
    }
}

五、注意事项与优化建议

1. 哪些异常需要重试?

默认逻辑会重试所有异常,但实际场景中并非所有异常都需要重试(如400 Bad Request(参数错误)、401 Unauthorized(认证失败))。建议在委托中增加异常过滤,仅重试临时错误

var getRequest = async (client) =>
{
    try
    {
        var response = await client.GetAsync(url);
        // 仅重试5xx(服务器错误)和429(请求过于频繁)
        if (response.StatusCode >= HttpStatusCode.InternalServerError || response.StatusCode == HttpStatusCode.TooManyRequests)
        {
            throw new HttpRequestException($"服务器错误:{response.StatusCode},触发重试");
        }
        response.EnsureSuccessStatusCode(); // 其他错误(如400)直接抛出,不重试
        return await response.Content.ReadAsStringAsync();
    }
    catch (HttpRequestException ex)
    {
        // 重试网络错误(如连接超时、DNS解析失败)
        if (ex.Message.Contains("timeout") || ex.Message.Contains("connect"))
        {
            throw; // 触发重试
        }
        // 其他HTTP异常(如400)不重试,直接抛出
        throw new Exception("非重试类错误", ex);
    }
};

2. 避免HttpClient重复创建

HttpClient设计为可复用(频繁创建会导致TCP连接泄漏),HttpRetryHandler内部维护一个HttpClient实例,无需外部重复创建。

3. 测试环境忽略SSL证书(谨慎使用)

若测试环境的SSL证书无效(如自签证书),可解除CreateHttpClientServerCertificateCustomValidationCallback的注释,忽略证书错误。生产环境严禁使用

六、总结

HttpRetryHandler通过封装重试逻辑、代理配置、超时控制,显著提高了HTTP请求的稳定性。其核心优势在于:

  • 通用性:支持任意HTTP请求类型(GET/POST/PUT等)和返回值类型;
  • 灵活性:可自定义重试次数、延迟、超时、代理;
  • 安全性:补充了资源释放和参数校验,避免隐性bug。

开发者可根据实际需求(如增加重试日志、动态调整延迟策略)进一步扩展此类,使其更贴合项目场景。

posted on 2025-09-03 08:00  超级无敌美少男战士  阅读(117)  评论(0)    收藏  举报