热门标签 | 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
来解决开始我们提出的问题。



这篇文章的所有代码 在这




推荐阅读
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • 本文探讨了为何采用RESTful架构及其优势,特别是在现代Web应用开发中的重要性。通过前后端分离和统一接口设计,RESTful API能够提高开发效率,支持多种客户端,并简化维护。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 使用 Azure Service Principal 和 Microsoft Graph API 获取 AAD 用户列表
    本文介绍了一段通用代码示例,该代码不仅能够操作 Azure Active Directory (AAD),还可以通过 Azure Service Principal 的授权访问和管理 Azure 订阅资源。Azure 的架构可以分为两个层级:AAD 和 Subscription。 ... [详细]
  • 深入了解 Windows 窗体中的 SplitContainer 控件
    SplitContainer 控件是 Windows 窗体中的一种复合控件,由两个可调整大小的面板和一个可移动的拆分条组成。本文将详细介绍其功能、属性以及如何通过编程方式创建复杂的用户界面。 ... [详细]
  • 开发笔记:2020 BJDCTF Re encode
    开发笔记:2020 BJDCTF Re encode ... [详细]
  • 本文介绍如何在华为CE交换机上配置M-LAG(多链路聚合组),以实现CE1和CE2设备作为VLAN 10网关的高可用性。通过详细的配置步骤,确保网络冗余和稳定性。 ... [详细]
  • Python处理Word文档的高效技巧
    本文详细介绍了如何使用Python处理Word文档,涵盖从基础操作到高级功能的各种技巧。我们将探讨如何生成文档、定义样式、提取表格数据以及处理超链接和图片等内容。 ... [详细]
  • 在尝试使用C# Windows Forms客户端通过SignalR连接到ASP.NET服务器时,遇到了内部服务器错误(500)。本文将详细探讨问题的原因及解决方案。 ... [详细]
  • 本文档介绍了如何在Visual Studio 2010环境下,利用C#语言连接SQL Server 2008数据库,并实现基本的数据操作,如增删改查等功能。通过构建一个面向对象的数据库工具类,简化了数据库操作流程。 ... [详细]
  • 本文探讨了Web API 2中特性的路由机制,特别是如何利用它来构建RESTful风格的URI。文章不仅介绍了基本的特性路由使用方法,还详细说明了如何通过特性路由进行API版本控制、HTTP方法的指定、路由前缀的应用以及路由约束的设置。 ... [详细]
  • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
    技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
  • 深入解析 Vue 中的 Axios 请求库
    本文深入探讨了 Vue 中的 Axios 请求库,详细解析了其核心功能与使用方法。Axios 是一个基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js 环境。文章首先介绍了 Axios 的基本概念,随后通过具体示例展示了如何在 Vue 项目中集成和使用 Axios 进行数据请求。无论你是初学者还是有经验的开发者,本文都能为你解决 Vue.js 相关问题提供有价值的参考。 ... [详细]
  • 在Java领域,谈到网络编程,可能大家脑海里第一反应就是MINA,NETTY,GRIZZLY等优秀的开源框架。没错,不过在深入探究这些框架之前,我们需要先从最original的技 ... [详细]
  • Httpclient.setHttpRequestRetryHandler(requestRetryHandler);***设置重连机制和异常自动恢复处理*privatestaticHt ... [详细]
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社区 版权所有