UWP (通用 Windows 平台) 应用开发者在构建通过 HTTP 与 Web 服务或服务器断点交互的应用时,有多种 API 可以选择。要在一个托管 UWP 应用中实现 HTTP 客户端角色,最常用也是推荐的两种 API 即 System.Net.Http.HttpClient 和Windows.Web.Http.HttpClient。 相对于 WebClient
以及 HttpWebRequest
等老旧过时的 API,应当优先选择上述两种 API(尽管出于向后兼容的考虑, HttpWebRequest
的一个小子集在 UWP 中仍然可用)。
很多开发者对于 UWP 中的上述两个 API 有着功能异同、如何选择等疑问。本文旨在解答这些疑问并阐明两个 API 各自的用途。
概述
System.Net.Http.HttpClient
这一 API 在 .NET 4.5 中被首次引入,同时也有一个变体以 NuGet 包的形式为 .NET 4.0 以及 Windows Phone 8 Silverlight 应用提供支持。该 API 的目的在于提供一种比老旧的 HttpWebRequest
API 更为简单明了的抽象层,以及更为弹性灵活的 HTTP 客户端角色的实现方法。例如,开发者可以通过其提供的链式自定义 handler,拦截每个请求或响应并实现自定义逻辑。直到 Windows 8.1 为止,该 API 的底层都是由纯 .NET 实现的。在 Windows 10 中,该 API 的 UWP 实现已经改为基于 Windows.Web.Http
和 WinINet HTTP 协议栈实现了。
另一方面,Windows.Web.Http.HttpClient
API 则在 Windows 8.1 被引入,并同时可用于 Windows Phone 8.1。创建这一 API 的最大动因在于整合各种 Windows 应用开发语言可用的 HTTP API,使一种 API 能够提供这些语言各自 API 的全部特性。其中大部分基础 API 的设计均来源于 System.Net.Http
,而其实现则是基于WinINet HTTP 协议栈。
在 Windows 商店应用中使用上述两种 API时,操作系统版本以及编程语言的支持情况如下:
API | 操作系统版本 | 支持语言 |
System.Net.Http.HttpClient | Windows, Windows Phone 8 以上 | 仅限 .NET 语言 |
Windows.Web.Http.HttpClient | Windows, Windows Phone 8.1 以上 | 所有 Windows 商店应用语言 |
如何选择?
两种 API 在 UWP 中均可用,因而 HTTP 开发者面临的最大问题就是该在应用中选择二者中的哪一种。选择结果要依一些具体因素而定。
-
你是否需要整合原生 UI 以收集用户凭据、控制 HTTP 患侧读写行为或传递指定 SSL 客户端证书用于验证?
如果是,则使用Windows.Web.Http.HttpClient
。截至撰写本文时,相对于System.Net.Http
API,Windows.Web.Http.HttpClient
API 提供了更多对 HTTP 设置的掌控能力。未来System.Net.Http
API 可能也会得到加强以提供这些特性。 -
你是否要编写跨平台 .NET 代码(通用于 UWP/ASP.NET 5/iOS 以及 Android 平台)?
如果是,则使用System.Net.Http
API。使用该 API 编写的代码可以在 ASP.NET 5 以及 .NET Framework 桌面应用程序等其它平台上复用。感谢 Xamarin,如今该 API 也支持在 iOS 和 Android 平台上使用,所以你的代码也可以在这些平台上复用。
对象模型
现在我们已经了解了创建这两个相似 API 的原因以及如何选择的基本原则,接下来深入了解一下它们各自的对象模型。
System.Net.Http
该 API 对象模型的顶级抽象层是 HttpClient 对象。HttpClient
对象表示 HTTP 协议描绘的客户端-服务端模型中的客户端实体。客户端可以向服务端发送多个请求(由HttpRequestMessage 表示)并接收相应的响应(由 HttpResponseMessage 表示)。每个 HTTP 请求或响应的 entity body 和 content header 由基类 HttpContent及其派生类 StreamContent
、MultipartContent
和 StringContent
等表示。这些类型分别代表了不同类型的 HTTP entity body。这些类型均提供了一组 ReadAs*
方法将一个请求或响应的 entity body 读出为字符串、字节数粗或流。
每个 HttpClient
对象底层均有一个 handler 对象表示所有客户端 HTTP 相关设置。你可以从概念上把 handler 理解为客户端底层的 HTTP 栈。它负责把客户端的 HTTP 请求发送至服务器并传回相应的响应。
System.Net.Http
API 中默认使用的 handler 类是 HttpClientHandler。当你创建一个 HttpClient
对象的新实例时——例如,调用 new HttpClient()
——一个 HttpClientHandler
对象都会自动创建,并携带默认的 HTTP 栈设置。如果你想要修改缓存行为、自动压缩、凭据或代理等设置,你可以自己创建 HttpClientHandler
的实例,修改其相应属性再传递给 HttpClient
的构造函数:
1 HttpClientHandler myHandler = new HttpClientHandler(); 2 myHandler.AllowAutoRedirect = false; 3 HttpClient myClient = new HttpClient(myHandler);
链式 Handler
System.Net.Http.HttpClient
API 设计中的一个关键优势就是可以在一个 HttpClient
对象底层插入自定义 handler,并创建一条 handler 对象链。假设你要构建一个需要从 Web 服务查询数据的应用。你编写了自定义逻辑来处理从服务器返回的 HTTP 4xx(客户端错误) 和 5xx(服务端错误) 错误,并发起更换端点或添加用户凭证等重试动作。而你也想要将这部分与 HTTP 相关的工作与其它处理 Web 服务返回数据的业务逻辑分开。
要实现上述需求,可以从 DelegatingHandler 派生一个新的 handler 类(例如 CustomHandler1),再创建一个派生类的实例传递给 HttpClient
的构造函数。DelegatingHandler
类的 InnerHandler
属性用于指定链中的下一个 handler ——举例,你可以借此把另一个自定义 handler (例如 CustomHandler2)添加到链中。而在最后一个 handler 里,你可以把 InnerHandler
设置为一个 HttpClientHandler
实例,该实例会将请求传递给系统的 HTTP 栈。过程如下图所示:
完成上述需求的示例代码:
1 public class CustomHandler1 : DelegatingHandler 2 { 3 // 此处放置构造和其它代码。 4 protected async override Task<HttpResponseMessage> SendAsync( 5 HttpRequestMessage request, CancellationToken cancellationToken) 6 { 7 // 在此处理 HttpRequestMessage 对象。 8 Debug.WriteLine("Processing request in Custom Handler 1"); 9 10 // 一旦前步骤完成,调用 DelegatingHandler.SendAsync 继续将其传递至 inner handler 11 HttpResponseMessage response = await base.SendAsync(request, cancellationToken); 12 13 // 在此处理返回的 HttpResponseMessage 对象。 14 Debug.WriteLine("Processing response in Custom Handler 1"); 15 16 return response; 17 } 18 } 19 20 public class CustomHandler2 : DelegatingHandler 21 { 22 // 内容与 CustomHandler1 类似 23 } 24 public class Foo 25 { 26 public void CreateHttpClientWithChain() 27 { 28 HttpClientHandler systemHandler = new HttpClientHandler(); 29 CustomHandler1 myHandler1 = new CustomHandler1(); 30 CustomHandler2 myHandler2 = new CustomHandler2(); 31 32 // 将两个 Handler 链接在一起。 33 myHandler1.InnerHandler = myHandler2; 34 myHandler2.InnerHandler = systemHandler; 35 36 // 使用链中的顶级 Handler 创建客户端对象。 37 HttpClient myClient = new HttpClient(myHandler1); 38 } 39 }
注意:
- 如果你打算向一个远程服务器端点发送请求,通常链中的最后一个 handler 都是
HttpClientHandler
,该 handler 负责实际通过系统的 HTTP 栈发送请求并接收响应。 若非如此,你可以使用一个自定义 handler 模拟发送请求以及接收模拟响应的过程。 - 在将请求传递到下一个 handler 之前,或在前一个返回响应之前添加处理逻辑可能会带来性能损失。在这类场景中最好避免开销昂贵的同步操作。
有关链式 Handler 的更多信息,可以参阅 Henrik Nielsen 撰写的文章(注意该文中谈及的 API 是指 ASP.NET Web API 版本的,与我们在本文中谈论的 .NET Framework 版略有区别,不过有关链式 handler 的概念是通用的。)
Windows.Web.Http
Windows.Web.Http
API 的对象模式与上文中描述的 System.Net.Http
非常类似,它也有客户端实体、handler(该命名空间内叫做 "filter",即过滤器)以及在客户端与系统默认 filter 之间插入自定义逻辑等概念。
本 API 中大部分类型与 System.Net.Http
的对象模型类似:
HTTP 客户端角色表示 | System.Net.Http 类型 | 对应 Windows.Web.Http 类型 |
客户端实体 | HttpClient | HttpClient |
HTTP 请求 | HttpRequestMessage | HttpRequestMessage |
HTTP 响应 | HttpResponseMessage | HttpResponseMessage |
HTTP 或响应的 entity body | HttpContent | IHttpContent |
HTTP 内容的字符串、流等表示 | StringContent, StreamContent and ByteArrayContent | HttpStringContent, HttpStreamContent and HttpBufferContent respectively |
HTTP 栈/设置 | HttpClientHandler | HttpBaseProtocolFilter |
用于创建自定义 handlers/filters 的基类/接口 | DelegatingHandler | IHttpFilter |
上文中关于 System.Net.Http
API 链式 handler 的讨论亦可用于 Windows.Web.Http
API。你可以创建一组链式自定义 filter,传递给 HttpClient 对象的构造函数。
实现常见 HTTP 场景
现在我们来看看一些代码片段,分别使用两种 HttpClient API 实现常见 HTTP 场景。更多细节和指导可以查阅Windows.Web.Http.HttpClient 和System.Net.Http.HttpClient 的 MSDN 文档。
修改头
System.Net.Http:
修改 HttpClient 实例发出的所有请求的头:
1 var myClient = new HttpClient(); 2 myClient.DefaultRequestHeaders.Add("X-HeaderKey", "HeaderValue"); 3 myClient.DefaultRequestHeaders.Referrer = new Uri("http://www.contoso.com");
只修改特定请求的头:
1 HttpRequestMessage myrequest = new HttpRequestMessage(); 2 myrequest.Headers.Add("X-HeaderKey", "HeaderValue"); 3 myrequest.Headers.Referrer = new Uri("http://www.contoso.com");
Windows.Web.Http:
上述代码同样适用于 Windows.Web.Http
API。
注意:
- 部分头项目是集合,修改需要通过 Add 和 Remove 方法实现。
- HttpClient.DefaultRequestHeaders 属性表示在应用层次上,默认头集合是否会添加到请求中。由于请求是由系统的 HTTP 栈处理的,所以请求实际发送出去前,一些附加头可能会添加到请求中。
超时设置
System.Net.Http:
在 System.Net.Http
API 中,有两种方式设置超时。要为客户端发出的所有请求设置超时,使用:
1 myClient.Timeout = TimeSpan.FromSeconds(30);
要为单个请求设置超时,则使用 CancellationToken:
1 var cts = new CancellationTokenSource(); 2 cts.CancelAfter(TimeSpan.FromSeconds(30)); 3 4 var httpClient = new HttpClient(); 5 var resourceUri = new Uri("http://www.contoso.com"); 6 7 try 8 { 9 HttpResponseMessage response = await httpClient.GetAsync(resourceUri, cts.Token); 10 } 11 catch (TaskCanceledException ex) 12 { 13 // 因超时取消请求的逻辑 14 } 15 catch (HttpRequestException ex) 16 { 17 // 处理其它可能异常的逻辑 18 }
Windows.Web.Http:
Windows.Web.Http.HttpClient
类型中没有超时属性可用,因此你必须向上文一样使用 CancellationToken 实现超时处理。
使用身份验证凭据
System.Net.Http:
为了保护用户的凭证信息,HTTP 栈默认不会向发出的请求添加任何验证凭据。要使用指定用户的凭据,可以使用如下方法:
1 var myClientHandler = new HttpClientHandler(); 2 myClientHandler.Credentials = new NetworkCredential(myUsername, myPassword);
Windows.Web.Http:
对于 Windows.Web.Http
API,默认情况下如果发出的请求访问了要求用户验证的资源,系统会弹出一个 UI 对话框。要关闭 UI 对话框,可以把 HttpBaseProtocolFilter
的 AllowUI
属性设置为 false。要使用指定用户的凭据,可以使用如下方法:
1 var myFilter = new HttpBaseProtocolFilter(); 2 myFilter.ServerCredential = new PasswordCredential(“fooBar”, myUsername, myPassword);
注意:
- 在上述示例中,
myUsername
和myPassword
两个字符串变量可以来自用户通过 UI 输入或者应用自身的配置。 - 在 UWP 应用中,HttpClientHandler.Credentials 只能设置为 null、DefaultCredentials 或一个 NetworkCredential 类型的对象实例。
使用客户端证书
System.Net.Http:
为保护用户的凭据信息,该 API 默认不会向服务器发送任何客户端证书。要使用客户端证书用于验证,使用如下方法:
1 var myClientHandler = new HttpClientHandler(); 2 myClientHandler.ClientCertificateOptions = ClientCertificateOption.Automatic;
Windows.Web.Http:
使用该 API 进行客户端证书验证有两种选择——默认方式是弹出一个 UI 让用户选择证书;另一种选择是在代码中自行指定一个客户端证书:
1 var myFilter = new HttpBaseProtocolFilter(); 2 myFilter.ClientCertificate = myCertificate;
注意:
- 无论使用哪种 API ,要使用客户端证书,你必须先根据这些步骤将证书添加到应用的证书储存区。拥有企业权限的应用也能够使用用户的“我的”储存区中已经存在的证书。
- HttpClientHandler.ClientCertificateOptions 属性允许两种值:
Automatic
和Manual
。设置为 Automatic 则会从应用的证书储存区自动选择最佳匹配的证书用于验证。设置为 Manual 则会保证在服务器请求前,任何客户端证书都不会被发送。
代理设置
默认情况下,两种 API 的代理设置都会自动根据 Internet Explorer/ Microsoft Edge 的设置自动为所有 HTTP 调用进行配置。这使得应用在用户通过代理连接网络时也能正常工作。两种 API 都没有提供任何方法为应用指定一个自定义代理。然而你可以通过在 System.Net.Http 中设置 HttpClientHandler.UseProxy
为 false 或在 Windows.Web.Http 中设置 HttpBaseProtocolFilter.UseProxy
为 false 来禁止使用默认代理配置。
Cookie 处理
默认情况下,两种 API 都会保存服务器发来的 cookie 数据,并自动附加到随后产生的请求对相同应用容器 URI 的请求中。对特定 URI 的 cookie 也能读取或添加自定义 cookie 数据。最后,两种 API 都提供了选项来关闭向服务器发送 cookie:对于 System.Net.Http
,将 HttpClientHandler.UseCookies 设置为 false;对于 Windows.Web.Http
,设置 HttpBaseProtocolFilter.CookieUsageBehavior 为HttpCookieUsageBehavior.NoCookies。
System.Net.Http:
为客户端生成的所有请求添加一个 cookie:
1 // 手动添加cookie。 2 myClientHandler.CookieContainer.Add(resourceUri, myCookie);
为单个请求添加 cookie:
1 HttpRequestMessage myRequest = new HttpRequestMessage(); 2 myRequest.Headers.Add("Cookie", "user=foo; key=bar");
查看给定 URI 的所有 cookie:
1 var cookieCollection = myClientHandler.CookieContainer.GetCookies(resourceUri);
Windows.Web.Http:
为客户端生成的所有请求添加一个 cookie:
1 // 手动添加cookie。 2 filter.CookieManager.SetCookie(myCookie);
上文中为单个请求添加 cookie 的示例同样适用于 Windows.Web.Http
API。
管理 cookie:
1 // 获取给定 URI 的所有 cookies。 2 var cookieCollection = filter.CookieManager.GetCookies(resourceUri); 3 4 // 删除一个 cookie。 5 filter.CookieManager.DeleteCookie(myCookie);
注意:
- 在
Windows.Web.Http
API 中,对于应用容器内的Windows.Web.Syndication
、Windows.Web.AtomPub
以及XHR
等几个使用 WinINet 栈实现的网络 API之间,cookie 管理器内的 cookie 数据是共享的。因此,之前一个 Syndication API 调用通过服务器响应获得 cookie 可能会被添加到相同应用容器内之后发送给同一服务器的 HttpClient 请求里。
每服务器最大连接数
默认情况下,操作系统底层的 HTTP 栈对每个服务器最多使用六个连接。System.Net.Http
的 HttpClient API没有提供任何方法控制最大连接数。对于 Windows.Web.Http
API,可以使用如下方法设置:
1 var myFilter = new HttpBaseProtocolFilter(); 2 myFilter.MaxConnectionsPerServer = 15;
其它
Windows 10 中的 UWP 在两种 API 中都添加了对 HTTP/2 的支持。该支持默认开启,因此开发者无需做任何代码修改即可享受更低延迟的提升。两种 API (System.Net.Http
和 Windows.Web.Http
)也都允许手动关闭这一特性并强制 HTTP 版本为 1.1 或 1.0。
本文编译自微软 Building Apps for Windows 博客,原文地址:Demystifying HttpClient APIs in the Universal Windows Platform。本文原文由 Windows 网络 API 组的 Program Manager Sidharth Nabar 撰写。
摘要: 本文为个人博客备份文章,原文地址:http://validvoid.net/demystifying-httpclient-apis-in-the-uwp/