C# HTTP请求重试处理器:实现与使用指南
在网络请求场景中,临时网络波动、服务器短暂不可用等问题时常发生。为提高HTTP请求的稳定性,重试机制是一种常见且有效的解决方案。本文将详细解析一个C#编写的HttpRetryHandler类,包括其核心功能、代码注解及使用方法,方便开发者集成到项目中。
一、类核心功能概述
HttpRetryHandler是一个通用的HTTP请求重试处理器,主要解决以下问题:
- 网络不稳定重试:当HTTP请求因临时错误失败时,自动重试(支持自定义重试次数)。
- 指数退避延迟:重试间隔采用“指数退避”策略(避免短时间内频繁请求给服务器造成压力)。
- 代理支持:可配置HTTP代理(适用于需要通过代理访问外部服务的场景)。
- 超时控制:自定义HTTP请求超时时间(防止请求长时间阻塞)。
- 异常封装:达到最大重试次数仍失败时,封装原始异常并抛出(便于问题定位)。
二、完整代码与详细注解
以下是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请求逻辑,支持任意返回值类型(如string、HttpResponseMessage、自定义模型),灵活性极高。以下是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证书无效(如自签证书),可解除CreateHttpClient中ServerCertificateCustomValidationCallback的注释,忽略证书错误。生产环境严禁使用!
六、总结
HttpRetryHandler通过封装重试逻辑、代理配置、超时控制,显著提高了HTTP请求的稳定性。其核心优势在于:
- 通用性:支持任意HTTP请求类型(GET/POST/PUT等)和返回值类型;
- 灵活性:可自定义重试次数、延迟、超时、代理;
- 安全性:补充了资源释放和参数校验,避免隐性bug。
开发者可根据实际需求(如增加重试日志、动态调整延迟策略)进一步扩展此类,使其更贴合项目场景。
浙公网安备 33010602011771号