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/