目录
HTTP 状态码
服务端各不相同
客户端差异巨大
- webClient
- HttpWebRequest
- RestSharp
- HttpClient
- HttpClientFactory
随着纯单体项目的逐渐减少, 远程服务调用失败变得十分常见. 由于 HTTP 协议的开放性, 远程服务调用异常的复杂度在增长.
HTTP 状态码
HTTP 状态码是描述响应的重要信息, 参考 List of HTTP status codes.
1XX 未被定义在 HTTP/1.0 协议中;
2XX 表示请求已成功被服务器接收, 理解, 并接受;
3XX 表示需要客户端采取进一步的操作才能完成请求;
4XX 表示客户端看起来可能发生了错误, 妨碍了服务器的处理;
5XX 表示服务器在处理请求的过程中有错误或者异常状态发生;
3XX 响应不在本文讨论之列
服务端各不相同
HTTP 状态码目前集中于 1XX 到 5XX 区间, 这形成以下事实:
REST 风格接口往往使用 200,400,500 描述响应, 部分版本的 ASPNET Core 中将暴露的路由所在方式定义为 void 可以观察到 204 状态码(使用 IActionResult 则可以进行更精确的控制).
在实践中, 各厂商的策略也千差万别:
友盟和又拍云接口的 HTTP 状态码集中在 200 和 400 上, 配合四位业务意义上的状态码表达响应, 参考
友盟 - U-Push API 集成文档 - 附录 I 接口调用错误码
又拍云 - 图片处理 - 状态码表
微信支付相关接口文档未声明 HTTP 状态码的意义, 相反它定义了以 "SUCCESS/FAIL" 描述的状态码, 以及具体接口的错误码, 参考 微信支付开发文档 - 统一下单 文未部分.
微信笃信自己的服务器不会挂, 所有非 200 响应均可认为服务出了问题, 但这做法并不另类
客户端差异巨大
大多数部分客户端认为 4XX 和 5XX 为异常响应, 但各语言集成的 HTTP 客户端或者第三方以及各版本存在部分差异. 以 .net 中的 WebClient,HttpWebRequest 来说, 遇到 4XX 和 5XX 直接抛出异常, 这使得即便接收到 HTTP 响应, 获取响应状态码及正文却需要在 catch 语句中进行, 使用起来极为丑陋.
WebClient
WebClient API 看起来简单, 但建议避免使用 WebClient , 理由如下:
WebClient 会捕获请求的线程上下文, 有造成死锁的可能;
WebClient 基于 HttpWebRequest, 不但历史包袱严重, 混杂了 EAP 模式 (Event-based Asynchronous Pattern) 与 Task 模式, 而且缺失基本的超时设置;
HttpWebRequest
HttpWebRequest 必须在异常捕获逻辑中处理服务器的非 2xx 响应, 同步版本支持超时设置, 请求示例:
- var url = "http://localhost:4908/api/test/2";
- //url = "http://www.google.com";
- var client = HttpWebRequest.CreateHttp(url);
- client.Method = HttpMethod.Get.Method;
- client.Timeout = 3000;
- try {
- var resp = client.GetResponse(); // 超时生效
- //var resp = await client.GetResponseAsync() as HttpWebResponse; // 超时不生效
- using (var stream = resp.GetResponseStream())
- using (var reader = new StreamReader(stream)) {
- var respText = await reader.ReadToEndAsync();
- Console.WriteLine(respText);
- }
- }
- catch (WebException ex) {
- // 开始处理失败请求
- var resp = ex.Response as HttpWebResponse;
- if (resp != null) {
- Console.WriteLine("request failed: {0}, statusCode: {1}", resp.StatusDescription, resp.StatusCode);
- using (var stream = ex.Response.GetResponseStream())
- using (var reader = new StreamReader(stream)) {
- var respText = await reader.ReadToEndAsync();
- Console.WriteLine(respText);
- }
- }
- // 服务器无法响应, 比如 DNS 查找失败
- else {
- throw ex.InnerException ?? ex;
- }
- }
HttpWebRequest 的缺陷
HttpWebRequest 存在着设计和实现缺陷, 都与超时相关. 在开始之前必须指出:.net core 不同版本存在差异,.net framework 不同版本存在差异,.net framework 与 .net core 存在差异
首先是 DNS 查找成本不计入超时时长, 在 .net framework 上能够复现, 在 .net core 版本上可能得到了修正.
调用结果显示, 设置了 1 秒的超时时间,.net framework 版本耗时 2.261 秒, 差异不容忽略,.net core 版本耗时 1.137 秒, 满足预期.
接着是异步版本不支持超时, 即设置了超时时长的 await HttpWebRequest.GetResponseAsync() 无法按预期工作, 参考
- Timeout behaviour in HttpWebRequest.GetResponse() vs GetResponseAsync()
- HttpWebRequest.Timeout Property
明明是设计与实现问题, 官方却解释到 "The Timeout property has no effect on asynchronous requests made with the BeginGetResponse or BeginGetRequestStream method" 云云.
为什么这么说? 因为 .net core 版本修复了这个问题, 请继续阅读.
http://localhost:13340/API/trial/11 是一个 webapi 接口, 内部使用 Thread.Sleep(10000)挂起 10 秒, 问题在 .net framework 上能够复现, 在 .net core 版本按预期工作.
这意味着我们必须做更多的工作. 超时模式本可以解决这个问题, 需要先借助 TaskFactory.FromAsync() 将 APM 模式 (Asynchronous Programming Model) 转换成 TPL 模式, 即基于 Task 的异步模式
- async Task Main() {
- var url = "http://localhost:13340/api/trial/11";
- var client = HttpWebRequest.CreateHttp(url);
- // 避免干扰, 没有对 HttpWebRequest.Timeout 赋值
- var timeout = TimeSpan.FromSeconds(5);
- var start = DateTime.UtcNow;
- Console.WriteLine(Environment.Version);
- Console.WriteLine("Start {0}", DateTime.Now);
- try {
- //await client.GetResponseAsync();
- var resp = await Task.Factory.FromAsync(client.BeginGetResponse, client.EndGetResponse, null)
- .SetTimeout(timeout);
- }
- catch (OperationCanceledException) {
- Console.WriteLine("Request timeout");
- }
- catch (WebException ex) {
- Console.WriteLine(ex.InnerException ?? ex);
- }
- finally {
- Console.WriteLine("Finish {0}", DateTime.UtcNow.Subtract(start));
- }
- }
- public static class TaskExension {
- [System.Diagnostics.DebuggerStepThrough]
- public static async Task<T> SetTimeout<T>(this Task<T> task, TimeSpan timeout) {
- using (var cts = new CancellationTokenSource(timeout)) {
- var tsc = new TaskCompletionSource<T>();
- using (cts.Token.Register(state => tsc.TrySetCanceled(), tsc)) {
- if (task != await Task.WhenAny(task, tsc.Task)) {
- throw new OperationCanceledException(cts.Token);
- }
- }
- return await task;
- }
- }
- }
.net core 版本同样工作完好, 在此忽略, 至此 HttpWebRequest 的坑点已经数的差不多了.
RestSharp
GitHub 上的接近 7000 星项目 https://github.com/restsharp/RestSharp 使用 HttpWebRequest 完成实现, 关键代码见 , 它支持以下模式:
基于同步: IRestClient.Get/Post -> Execute() -> RestClient.DoExecuteAsXXXX() -> Http.AsXXXX() -> Http.XXXXInternal(), ConfigureWebRequest() 返回 HttpWebRequest
基于回调: IRestClient.GetAsync/PostAsync() -> RestClient.ExecuteAsync() -> DoAsXXXXAsync() 返回 HttpWebRequest
基于 Task:IRestClient.GetAsync/PostAsync() -> RestClient.ExecuteXXXXTaskAsync() -> ExecuteTaskAsync() -> ExecuteAsync() 进入基于回调的实现
项目 HttpWebRequest 完成实现, 异步请求的版在回调版本基础上借助 TaskCompletionSource 完成实现, 绕开了 await HttpWebRequest.GetResponseAsync() 的超时缺陷. 但 HttpWebRequest 固有的 DNS 问题无法避免, 故项目在 Note about error handling 中特别备注到:
- Note about error handling
- If there is a network transport error (network is down, failed DNS lookup, etc),
- HttpClient
HttpClient 的出现使得情况些许改观, 不考虑超时, 使用 4 行代码即可读取返回非 2XX 状态码的响应正文:
- var client = new HttpClient();
- var url = "http://localhost:4908/api/test/2";
- var resp = await client.GetAsync(url);
- // 遇到 4XX,5XX 也不会抛出异常
- var respText = await resp.Content.ReadAsStringAsync();
- Console.WriteLine(respText);
可以使用 HttpResponseMessage.EnsureSuccessStatusCode() 进行成功请求断言
添加异常处理与超时机制, 代码在 20 行左右, 是 HttpWebRequest 规模的 1/3 左右.
- var url = "http://localhost:4908/api/test/1";
- //url = "http://www.google.com";
- var client = new HttpClient();
- client.Timeout = TimeSpan.FromSeconds(5);
- HttpResponseMessage resp = null;
- try {
- resp = await client.GetAsync(url);
- }
- catch (TaskCanceledException) {
- // 开始处理请求超时
- Console.WriteLine("Request timeout");
- throw new TimeoutException();
- }
- catch (HttpRequestException ex) {
- // 服务器无法响应, 比如未开机, DNS
- if (ex.InnerException is WebException ex2) {
- throw ex2.InnerException ?? ex2;
- }
- throw ex;
- }
- // 已获取到响应
- if (resp.IsSuccessStatusCode) {
- // 安全地读取 resp.Content, 进行反序列化等,
- // 也可以直接使用 EnsureSuccessStatusCode() 断言
- }
- else {
- // 开始处理失败请求
- Console.WriteLine("Request failed: {0}, statusCode: {1}", resp.ReasonPhrase, resp.StatusCode);
- // 直接读取不会抛出异常
- var respText = await resp.Content.ReadAsStringAsync();
- Console.WriteLine(respText);
- }
可见基于 HttpClient 易于使用, 然而 HttpClient 有自己的问题, 虽然偏离主题, 但不得不拿出篇幅来陈述.
HttpClient 的缺陷
搜索 "HttpClient dispose" 可见一二:
YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE
简单地说, HttpClient 和 DbConnection 一样都从 IDispose 继承, 然而其工作方式大不一样: 后者将连接释放回连接池, 前者却需要 4 分钟关闭 TCP 连接, 这导致高负载的站点可能用尽资源.
然而网上解决办法都建议静态或单例化 HttpClient 实例, 如博客园站长 dudu 的 C# 中 HttpClient 使用注意: 预热与长连接, 9102 年了, 汇总下 HttpClient 问题, 封印一个 , 这些做法会引入了其他问题:
但事实证明, 有一个更严重的问题: HttpClient 不遵循 DNS 变化, 它会 (通过 HttpClientHandler) 独占连接, 直到套接字关闭. 没有时间限制!
.NET HttpClient 的缺陷和文档错误让开发人员倍感沮丧
Bugs and Documentation Errors in .NET's HttpClient Frustrate Developers https://www.infoq.com/news/2016/09/HttpClient/
在实际开发中 DNS 变化可能不是很大问题, 虽然 HttpClient 是线程安全的, 但是唯一的 HttpClient 不能满足差异化的 Http 请求, 比如有时候需要自定义头部, 有时候需要使用证书发起请求, 静态或单例化的 HttpClient 不能很好地满足需要.
HttpClientFactory
为了克服以上问题, 微软在 .Net core 2.1 版本引入了 HttpClientFactory, 基础使用方法简单, 请自行阅读不再详细陈述.
- Use HttpClientFactory to implement resilient HTTP requests
- 3 ways to use HTTPClientFactory in ASP.NET Core 2.1
IHttpClientFactory 内部引用了 Policy, 建议非常谨慎地使用重试策略, 讨论不在本篇展开.
来源: https://www.cnblogs.com/leoninew/p/remote-response-and-error-1.html