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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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来解决开始我们提出的问题。
这篇文章的所有代码在这
以上就是c# HttpClient设置超时的步骤的详细内容,更多关于c# HttpClient设置超时的资料请关注自由互联其它相关文章!