热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

c#中httpclient设置超时的最佳实践

HttpClient作为官方推荐的http客户端,相比之前的WebClient和WebRequest好用了很多,但默认无法为每个请求单独设置超时,只能给HttpClient设置默认



HttpClient
作为官方推荐的 http
客户端,相比之前的 WebClient
WebRequest
好用了很多,但默认无法为每个请求单独设置超时,只能给 HttpClient
设置默认超时,使用起来不太方便。



声明:本文主要是翻译自 THOMAS LEVESQUE’S .NET BLOG
的文章: Better timeout handling with HttpClient



由于工作原因,需要用c#,就语法层而言,c#确实比java优秀,一些库接口封装也更方便简洁。特别是 HttpClient
,结合了 task异步模型
,使用起来非常顺手。


本人水平有限,如有问题,还望各位多多海涵,不吝赐教


问题



如果你经常用 HttpClient
去调用 Restfull
接口或传送文件,你可能会对 HttpClient
这个类处理 Request(请求)
超时的方式感到恼火,因为存在这两个问题:




  • timeout(超时)
    只能在 HttpClient
    class
    级别处理。也就是说,一旦设置好了,所有 httpClient
    下的请求都会应用同样的超时设置,这显然不灵活,如果能够为每个 request
    请求分别指定一个超时时间,将非常方便。


  • 当请求超时时,抛出的异常很不好辨认。你认为请求超时时, httpclient
    会抛出 TimeoutException
    ?,不,其实它会抛出一个 TaskCanceledException
    ,而单看这个异常,你一时还无法分辨是取消导致的还是真正超时导致的。



幸运的是,得益于 HttpClient
的灵活设计,可以非常容易的弥补此缺陷。


因此,我们将针对这两个问题做出解决方案。让我们回顾一下我们想要的:




  • 可以为每个 request
    请求单独设置超时时间


  • 当超时发生时, catch
    的异常是 TimeoutException
    而不是 TaskCanceledException


为每个request设置超时值



怎样将超时时间值和 Request
请求关联起来呢? HttpRequestMessage
这个类有个 Properties
的属性,它是一个 字典(Dictionary)
类型的属性,我们可以放入我们任何自定义需要的内容到这个属性中。我们将使用这个属性存储 请求(request)
的超时时间,为了便于实现此功能,我们给 HttpRequestMessage
创建一个扩展方法:




public static class HttpRequestExtensions
{
private static string TimeoutPropertyKey = "RequestTimeout";
public static void SetTimeout(
this HttpRequestMessage request,
TimeSpan? timeout)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
request.Properties[TimeoutPropertyKey] = timeout;
}
public static TimeSpan? GetTimeout(this HttpRequestMessage request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Properties.TryGetValue(
TimeoutPropertyKey,
out var value)
&& value is TimeSpan timeout)
return timeout;
return null;
}
}




这是一段很普通的代码, timout
参数是可null的 TimeSpan
值,我们现在可以给请求设置超时值,但是目前还没有实际使用到这段代码。


Http Handler



HttpClient
使用 管道体系( pipeline architecture)
结构:每个请求都通过一系列类型为 HttpMessageHandler
Handler
处理,并且以相反顺序逐级返回响应。有了这种机制,我们可以非常方便的加入我们自己的 Handler
来具体处理超时问题。如果您想了解更多, 本文
将对此进行更详细的说明。



我们的自己的超时 Handler
将继承 DelegatingHandler
DelegatingHandler
是一种设计为链式调用其他 Handler
的类(简单提一下: DelegatingHandler
内部有个 InnerHandler
成员变量,我们可以在调用 innerHandler.SendAsync()
前后对 request
CancellationToken
response
做相应处理)。要实现我们的 Handler
,我们重写 SendAsync
方法。最小的实现如下所示:




class TimeoutHandler : DelegatingHandler
{
protected async override Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken);
}
}




上述代码并没有任何用处,因为只是将实际处理丢给了 base.SendAsync
,目前还没有对 TimeoutHandler
进行任何加工处理,我们将逐步对其加强扩充,以达到我们的目的。



Request
加上超时处理



首先,让我们给 TimeoutHandler
添加一个 TimeSpan
类型的 DefaultTimeout
属性,这个默认超时时间是给没有特意设置超时时间的请求使用的:




public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);




就像 HttpClient.Timeout
一样,我们也设置默认超时时间为100秒。



为了实现我们的超时处理,我们需要从 request
中获取超时时间(如果 request
中没有设置,则应用 DefaultTimeout
的值)。接着,我们创建一个在指定时间(超时时间)后将会被取消的 CancellationToken
,并把这个 CancellationToken
传入到链的下一个 Handler
。这样之后,如果指定超时时间内没有获取到 response
响应,我们刚刚创建的 CancellationToken
就会被 取消(cancel)



我们创建一个 CancellationTokenSource
,这个类可以创建和控制 CancellationToken
。它将根据超时时间来创建:




private CancellationTokenSource GetCancellationTokenSource(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var timeout = request.GetTimeout() ?? DefaultTimeout;
if (timeout == Timeout.InfiniteTimeSpan)
{
// No need to create a CTS if there's no timeout
//不需要创建CTS,因为不处理超时(下面会讲到)
return null;
}
else
{
var cts = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
return cts;
}
}



这里主要关注两个点:




  • 如果 request
    超时值为 Timeout.InfiniteTimeSpan
    ,程序并不会创建 CancellationTokenSource
    ,它将不会被取消,因此节省了无用的分配。也就是说在这种情况下,我们将不会处理超时。


  • 以上相反,我们创建了一个在指定 timeout
    后被自动取消的 CancellationTokenSource
    (因为调用了 CancelAfter
    )。请注意,
    这个CTS连接了传入参数的 cancellationToken
    ,这个 cancellationToken
    其实来自 SendAsync
    方法的实参

    。这样做之后,当真正的超时发生,或者参数的 cancellationToken
    自身被取消, CTS
    都会被取消。如果想要获取跟多 CancellationToken
    的内容,请 访问这篇文章



最后,我们修改下 SendAsync
方法,应用刚刚创建的 CancellationTokenSource




protected async override Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
using (var cts = GetCancellationTokenSource(request, cancellationToken))
{
return await base.SendAsync(
request,
cts?.Token ?? cancellationToken);
}
}




我们创建了 CTS
后,把 CTS
token
传入到 base.SendAsync
中,注意,我们使用 cts?.Token
是因为 GetCancellationTokenSource
返回的 cts
可能为 null
,如果 cts
null
,则直接使用参数自己的 cancellationToken
,我们就不做任何超时处理。



通过这一步,我们有了自己的超时 Handler
,可以为每个请求指定不同的超时时间。但是,当超时发生时,我们仍然只能捕获到 TaskCanceledException
异常,这个问题很容易修复它。


抛出正确的异常



我们需要捕获 TaskCanceledException
(或者它的基类 OperationCanceledException
),然后检测 cancellationToken
参数是否是被取消的:




  • 如果是,说明这个 cancel
    是调用者自身导致的,对此直接将异常上抛不处理


  • 如果不是,这意味着是因为我们的超时导致的 cancel
    ,因此,我们将抛出一个 TimeoutException



这是最终的 SendAsync
方法:




protected async override Task SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
using (var cts = GetCancellationTokenSource(request, cancellationToken))
{
try
{
return await base.SendAsync(
request,
cts?.Token ?? cancellationToken);
}
catch(OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException();
}
}
}




我们使用了一个 exception filter
,通过这种方式,我们只 cactch
我们符合我们情况需要的异常,然后做相应处理。



至此,我们的超时 Handler
已经完成了,接下来看看怎么使用它



使用 Handler



当创建一个 HttpClient
时,可以指定一个自己的 Handler
作为 管道(pipeline)
的第一个 Handler
。如果没有指定,默认使用的是 HttpClientHandler
,这个 handler
直接发送请求到网络上。为了使用我们自己的 TimeoutHandler
,我们需要先创建它,然后将 timeoutHandler
指定为 httpClient
handler
。在 timeoutHandler
中,指定 InnerHandler
为我们自己创建的 HttpClientHandler
,这样实际的网络请求就委托到了 HttpClientHandler
中。




var handler = new TimeoutHandler
{
InnerHandler = new HttpClientHandler()
};
using (var client = new HttpClient(handler))
{
client.Timeout = Timeout.InfiniteTimeSpan;
...
}




通过将 httpclient
timeout
设置为 InfiniteTimeSpan
来禁用默认的超时设置,如果不这样做,默认超时会干扰我们自己的超时


现在,我们尝试发送一个设定了5秒超时的请求到需要很久才能响应的服务器




var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var respOnse= await client.SendAsync(request);




如果服务器在5秒内响应数据,我们将会捕获到一个 TimeoutException
,而不是 TaskCanceledException
,因此事情似乎按预期进行。



为了检测 cancellation
是否正确运行,我们传入一个在2秒(比超时实际小)后会被取消的 CancellationToken
:




var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var respOnse= await client.SendAsync(request, cts.Token);




这时,我们可以捕获到 TaskCanceledException
,这正是我们期望的。


总结



通过实现我们自己的 Http Handler
,我们可以用一个智能的 timout handler
来解决开始我们提出的问题。



这篇文章的所有代码 在这




推荐阅读
  • Hadoop MapReduce 实战案例:手机流量使用统计分析
    本文通过一个具体的Hadoop MapReduce案例,详细介绍了如何利用MapReduce框架来统计和分析手机用户的流量使用情况,包括上行和下行流量的计算以及总流量的汇总。 ... [详细]
  • Java中提取字符串的最后一部分
    本文介绍了如何使用Java中的substring()和split()方法来提取字符串的最后一部分,特别是在处理包含特殊字符的路径时的方法与技巧。 ... [详细]
  • 问题描述现在,不管开发一个多大的系统(至少我现在的部门是这样的),都会带一个日志功能;在实际开发过程中 ... [详细]
  • 本文介绍了如何通过安装和配置php_uploadprogress扩展来实现文件上传时的进度条显示功能。通过一个简单的示例,详细解释了从安装扩展到编写具体代码的全过程。 ... [详细]
  • 使用Java计算两个日期之间的月份数
    本文详细介绍了利用Java编程语言计算两个指定日期之间月份数的方法。文章通过实例代码讲解了如何使用Joda-Time库来简化日期处理过程,旨在为开发者提供一个高效且易于理解的解决方案。 ... [详细]
  • 本文详细探讨了在Windows 98环境下安装Apache 1.3.9、JServ、GNUJSP 1.0、JDK 1.2.2及JSDK 2.0后遇到的中文显示问题,并提供了多种有效的解决方案。 ... [详细]
  • Java连接MySQL数据库的方法及测试示例
    本文详细介绍了如何安装MySQL数据库,并通过Java编程语言实现与MySQL数据库的连接,包括环境搭建、数据库创建以及简单的查询操作。 ... [详细]
  • 本文探讨了如何利用 Android 的 Movie 类来展示 GIF 动画,并详细介绍了调整 GIF 尺寸以适应不同布局的方法。同时,提供了相关的代码示例和注意事项。 ... [详细]
  • 本文探讨了如何使用Scrapy框架构建高效的数据采集系统,以及如何通过异步处理技术提升数据存储的效率。同时,文章还介绍了针对不同网站采用的不同采集策略。 ... [详细]
  • Gradle 是 Android Studio 中默认的构建工具,了解其基本配置对于开发效率的提升至关重要。本文将详细介绍如何在 Gradle 中定义和使用共享变量,以确保项目的一致性和可维护性。 ... [详细]
  • 最近遇到了一个关于单链表的编程问题,这是来自福富公司的笔试题目。以往我通常使用C语言来解决这类问题,但这次决定尝试用Java来实现。该题目要求实现一个单链表,并完成特定的方法。 ... [详细]
  • 设计一个算法,用于计算给定字符串中出现的不同ASCII字符数量。该任务将重点考察字符串处理、集合操作以及基础的输入输出技术。 ... [详细]
  • Java多线程售票案例分析
    本文通过一个售票系统的实例,深入探讨了Java中的多线程技术及其在资源共享和并发控制中的应用。售票过程涉及查询、收款、找零和出票等多个步骤,其中对总票数的管理尤为关键。 ... [详细]
  • 本文详细介绍如何在SSM(Spring + Spring MVC + MyBatis)框架中实现分页功能。包括分页的基本概念、数据准备、前端分页栏的设计与实现、后端分页逻辑的编写以及最终的测试步骤。 ... [详细]
  • 本文详细介绍了在Luat OS中如何实现C与Lua的混合编程,包括在C环境中运行Lua脚本、封装可被Lua调用的C语言库,以及C与Lua之间的数据交互方法。 ... [详细]
author-avatar
寒时凝结公寓_264
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有