HttpClient介紹
HttpClient是.NET4.5引入的一個HTTP客戶端庫,其命名空間為System.Net.Http。.NET 4.5之前我們可能使用WebClient和HttpWebRequest來達到相同目的。但是有幾點值得關注:
- 可以使用單個HttpClient實例發任意數目的請求
- 一個HttpClient實例不會跟某個HTTP服務器或主機綁定,也就是說我們可以用一個實例同時給www.a.com和www.b.com發請求
- 可以繼承HttpClient達到定制目的
- HttpClient利用了最新的面向任務模式,使得處理異步請求非常容易
異步HTTP GET
下面是一個使用HttpClient進行HTTP GET請求數據的例子:
class HttpClientDemo { private const string Uri = "http://api.worldbank.org/countries?format=json"; static void Main(string[] args) { HttpClient httpClient = new HttpClient(); // 創建一個異步GET請求,當請求返回時繼續處理 httpClient.GetAsync(Uri).ContinueWith( (requestTask) => { HttpResponseMessage response = requestTask.Result; // 確認響應成功,否則拋出異常 response.EnsureSuccessStatusCode(); // 異步讀取響應為字符串 response.Content.ReadAsStringAsync().ContinueWith( (readTask) => Console.WriteLine(readTask.Result)); }); Console.WriteLine("Hit enter to exit..."); Console.ReadLine(); } }
代碼運行后將先輸出“Hit enter to exit...“,然后才輸出請求響應內容,因為采用的是GetAsync(string requestUri)異步方法,它返回的是Task<HttpResponseMessage>對象( 這里的 httpClient.GetAsync(Uri).ContinueWith(...)有點類似JavaScript中使用Promise對象進行異步編程的寫法,具體可以參考 JavaScript異步編程的四種方法 的第四節和 jQuery的deferred對象詳解)。
於是我們可以用.NET 4.5之后支持的async、await關鍵字來重寫上面的代碼,仍保持了異步性:
class HttpClientDemo { private const string Uri = "http://api.worldbank.org/countries?format=json"; static async void Run() { HttpClient httpClient = new HttpClient(); // 創建一個異步GET請求,當請求返回時繼續處理(Continue-With模式) HttpResponseMessage response = await httpClient.GetAsync(Uri); response.EnsureSuccessStatusCode(); string resultStr = await response.Content.ReadAsStringAsync(); Console.WriteLine(resultStr); } static void Main(string[] args) { Run(); Console.WriteLine("Hit enter to exit..."); Console.ReadLine(); } }
注意,如果以下面的方式獲取HttpResponseMessage會有什么后果呢?
HttpResponseMessage response = httpClient.GetAsync(Url).Result;
后果是訪問Result屬性會阻塞程序繼續運行,因此就失去了異步編程的威力。類似的:
string resultStr = response.Content.ReadAsStringAsync().Result;
也將導致程序運行被阻塞。
異步HTTP POST
public class OschinaLogin { // MD5或SHA1加密 public static string EncryptPassword(string pwdStr, string pwdFormat) { string EncryptPassword = null; if ("SHA1".Equals(pwdFormat.ToUpper())) { EncryptPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(pwdStr, "SHA1"); } else if ("MD5".Equals(pwdFormat.ToUpper())) { EncryptPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(pwdStr, "MD5"); } else { EncryptPassword = pwdStr; } return EncryptPassword; } /// <summary> /// OsChina登陸函數 /// </summary> /// <param name="email"></param> /// <param name="pwd"></param> public static void LoginOsChina(string email, string pwd) { HttpClient httpClient = new HttpClient(); // 設置請求頭信息 httpClient.DefaultRequestHeaders.Add("Host", "www.oschina.net"); httpClient.DefaultRequestHeaders.Add("Method", "Post"); httpClient.DefaultRequestHeaders.Add("KeepAlive", "false"); // HTTP KeepAlive設為false,防止HTTP連接保持 httpClient.DefaultRequestHeaders.Add("UserAgent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); // 構造POST參數 HttpContent postContent = new FormUrlEncodedContent(new Dictionary<string, string>() { {"email", email}, {"pwd", EncryptPassword(pwd, "SHA1")} }); httpClient .PostAsync("http://www.oschina.net/action/user/hash_login", postContent) .ContinueWith( (postTask) => { HttpResponseMessage response = postTask.Result; // 確認響應成功,否則拋出異常 response.EnsureSuccessStatusCode(); // 異步讀取響應為字符串 response.Content.ReadAsStringAsync().ContinueWith( (readTask) => Console.WriteLine("響應網頁內容:" + readTask.Result)); Console.WriteLine("響應是否成功:" + response.IsSuccessStatusCode); Console.WriteLine("響應頭信息如下:\n"); var headers = response.Headers; foreach (var header in headers) { Console.WriteLine("{0}: {1}", header.Key, string.Join("", header.Value.ToList())); } } ); } public static void Main(string[] args) { LoginOsChina("你的郵箱", "你的密碼"); Console.ReadLine(); } }
代碼很簡單,就不多說了,只要將上面的Main函數的郵箱、密碼信息替換成你自己的OsChina登錄信息即可。上面通httpClient.DefaultRequestHeaders屬性來設置請求頭信息,也可以通過postContent.Header屬性來設置。 上面並沒有演示如何設置Cookie,而有的POST請求可能需要攜帶Cookie,那么該怎么做呢?這時候就需要利用 HttpClientHandler(關於它的詳細信息見下一節)了,如下:
CookieContainer cookieContainer = new CookieContainer(); cookieContainer.Add(new Cookie("test", "0")); // 加入Cookie HttpClientHandler httpClientHandler = new HttpClientHandler() { CookieContainer = cookieContainer, AllowAutoRedirect = true, UseCookies = true }; HttpClient httpClient = new HttpClient(httpClientHandler);
然后像之前一樣使用httpClient。至於ASP.NET服務端如何訪問請求中的Cookie和設置Cookie,可以參考:ASP.NET HTTP Cookies 。
其他HTTP操作如PUT和DELETE,分別由HttpClient的PutAsync和DeleteAsync實現,用法基本同上面,就不贅述了。
異常處理
默認情況下如果HTTP請求失敗,不會拋出異常,但是我們可以通過返回的HttpResponseMessage對象的StatusCode屬性來檢測請求是否成功,比如下面:
HttpResponseMessage response = postTask.Result; if (response.StatusCode == HttpStatusCode.OK) { // ... }
或者通過HttpResponseMessage的IsSuccessStatusCode屬性來檢測:
HttpResponseMessage response = postTask.Result; if (response.IsSuccessStatusCode) { // ... }
再或者你更喜歡以異常的形式處理請求失敗情況,那么你可以用下面的代碼:
try { HttpResponseMessage response = postTask.Result; response.EnsureSuccessStatusCode(); } catch (HttpRequestException e) { // ... }
HttpResponseMessage對象的EnsureSuccessStatusCode()方法會在HTTP響應沒有返回成功狀態碼(2xx)時拋出異常,然后異常就可以被catch處理。
HttpMessageHandler Pipeline
在ASP.NET服務端,一般采用Delegating Handler模式來處理HTTP請求並返回HTTP響應:一般來說有多個消息處理器被串聯在一起形成消息處理器鏈 (HttpMessageHandler Pipeline),第一個消息處理器處理請求后將請求交給下一個消息處理器...最后在某個時刻有一個消息處理器處理請求后返回響應對象並再沿着消息處 理器鏈向上返回,如下圖所示:(本博客主要與HttpClient相關,所以如果想了解更多ASP.NET Web API服務端消息處理器方面的知識,請參考:HTTP Message Handlers )
HttpClient也使用消息處理器來處理請求,默認的消息處理器是HttpClientHandler(上面異步HTTP POST使用到了),我們也可以自己定制客戶端消息處理器,消息處理器鏈處理請求並返回響應的流程如下圖:
如果我們要自己定制一個客戶端消息處理器的話,可以繼承DelegatingHandler或者HttpClientHandler,並重寫下面這個方法:
Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken);
比如下面自定義了一個客戶端消息處理器來記錄HTTP錯誤碼:
class LoggingHandler : DelegatingHandler { StreamWriter _writer; public LoggingHandler(Stream stream) { _writer = new StreamWriter(stream); } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); if (!response.IsSuccessStatusCode) { _writer.WriteLine("{0}\t{1}\t{2}", request.RequestUri, (int)response.StatusCode, response.Headers.Date); } return response; } protected override void Dispose(bool disposing) { if (disposing) { _writer.Dispose(); } base.Dispose(disposing); } }
然后我們需要用下面的代碼將自定義消息處理器綁定到HttpClient對象上:
HttpClient client = HttpClientFactory.Create(new LoggingHandler(), new Handler2(), new Handler3());
上面的自定義消息處理器只攔截處理了HTTP響應,如果我們既想攔截處理HTTP請求,又想攔截處理HTTP響應,那么該怎么做呢?如下:
public class MyHandler : DelegatingHandler { private string _name; public MyHandler(string name) { _name = name; } // 攔截請求和響應 protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Console.WriteLine("Request path in handler {0}", _name); // 1 return base.SendAsync(request, cancellationToken).ContinueWith( requestTask => { Console.WriteLine("Response path in handler {0}", _name); return requestTask.Result; }, TaskContinuationOptions.OnlyOnRanToCompletion); } }
上面的1處攔截請求並處理。MSDN博客上還有一個例子利用客戶端消息處理器來實現OAuth驗證,具體可以移步:Extending HttpClient with OAuth to Access Twitter
WebRequestHandler
這里補充說明另外一個之前沒有涉及到的類 - WebRequestHandler。
WebRequestHandler繼承自HttpClientHandler,但是包含在System.Net.Http.WebRequest程序集中。它的一些屬性見下表:
屬性 | 說明 |
AllowPipelining | 獲取或設置是否允許請求被pipeline |
AuthenticationLevel | 獲取或設置認證級別 |
CachePolicy | 獲取或設置這個請求的緩存策略 |
ClientCertificates | 獲取或設置這個請求的安全證書集 |
ContinueTimeout | 當上傳數據時,客戶端必須先等待服務端返回100-continue,這個設置了返回100-continue的超時時間 |
MaxResponseHeadersLength | 獲取或設置響應頭的最大長度 |
ReadWriteTimeout | 獲取或設置寫請求或讀響應的超時時間 |
ServerCertificateValidationCallback | 獲取或設置SSL驗證完成后的回調函數 |
一個使用WebRequestHandler的簡單示例如下:
WebRequestHandler webRequestHandler = new WebRequestHandler(); webRequestHandler.UseDefaultCredentials = true; webRequestHandler.AllowPipelining = true; // Create an HttpClient using the WebRequestHandler(); HttpClient client = new HttpClient(webRequestHandler);
為HttpClient定制下載文件方法
HttpClient沒有直接下載文件到本地的方法。我們知道response.Content是HttpContent對象,表示HTTP響應消息的內 容,它已經支持ReadAsStringAsync、ReadAsStreamAsync和ReadAsByteArrayAsync等方法。響應消息內 容也支持異步讀取(為什么響應消息內容讀取也需要異步呢?原因在於響應消息內容非常大時異步讀取好處很明顯)。下面我給HttpContent類擴展一個 DownloadAsFileAsync方法,以支持直接異步下載文件到本地:
public static class HttpContentExtension { /// <summary> /// HttpContent異步讀取響應流並寫入本地文件方法擴展 /// </summary> /// <param name="content"></param> /// <param name="fileName">本地文件名</param> /// <param name="overwrite">是否允許覆蓋本地文件</param> /// <returns></returns> public static Task DownloadAsFileAsync(this HttpContent content, string fileName, bool overwrite) { string filePath = Path.GetFullPath(fileName); if (!overwrite && File.Exists(filePath)) { throw new InvalidOperationException(string.Format("文件 {0} 已經存在!", filePath)); } try { return content.ReadAsByteArrayAsync().ContinueWith( (readBytestTask) => { byte[] data = readBytestTask.Result; using (FileStream fs = new FileStream(filePath, FileMode.Create)) { fs.Write(data, 0, data.Length); //清空緩沖區 fs.Flush(); fs.Close(); } } ); } catch (Exception e) { Console.WriteLine("發生異常: {0}", e.Message); } return null; } } class HttpClientDemo { private const string Uri = "http://www.oschina.net/img/iphone.gif"; static void Main(string[] args) { HttpClient httpClient = new HttpClient(); // 創建一個異步GET請求,當請求返回時繼續處理 httpClient.GetAsync(Uri).ContinueWith( (requestTask) => { HttpResponseMessage response = requestTask.Result; // 確認響應成功,否則拋出異常 response.EnsureSuccessStatusCode(); // 異步讀取響應為字符串 response.Content.DownloadAsFileAsync(@"C:\TDDOWNLOAD\test.gif", true).ContinueWith( (readTask) => Console.WriteLine("文件下載完成!")); }); Console.WriteLine("輸入任意字符結束..."); Console.ReadLine(); }
上面我直接利用HttpContent的ReadAsBytesArrayAsync方法,我試過利用ReadAsStringAsync和ReadAsStreamAsync,但是都出現了亂碼現象,只有這種讀取到字節數組的方法不會出現亂碼。
SendAsync和HttpRequestMessage
前面講的GetAsync、PostAsync、PutAsync、DeleteAsync事實上都可以用一種方法實現:SendAsync結合 HttpRequestMessage。前面我們自定義HTTP消息處理器時重寫過SendAsync方法,它的第一個參數就是 HttpRequestMessage類型。一般性的示例如下:
class HttpClientDemo { private const string Uri = "http://www.oschina.net/"; static void Main(string[] args) { HttpClient httpClient = new HttpClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); httpClient.SendAsync(request).ContinueWith( (responseMessageTask) => { HttpResponseMessage response = responseMessageTask.Result; // 確認響應成功,否則拋出異常 response.EnsureSuccessStatusCode(); response.Content.ReadAsStringAsync().ContinueWith( (readTask) => Console.WriteLine(readTask.Result)); } ); Console.WriteLine("輸入任意字符退出..."); Console.ReadLine(); } }
參考資料:
http://blogs.msdn.com/b/henrikn/archive/2012/02/16/httpclient-is-here.aspx
http://blogs.msdn.com/b/henrikn/archive/2012/04/27/asp-net-web-api-updates-april-27.aspx
http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-a-net-client
http://www.asp.net/web-api/overview/working-with-http/http-message-handlers
http://www.asp.net/web-api/overview/working-with-http/http-cookies
http://blogs.msdn.com/b/henrikn/archive/2012/04/27/asp-net-web-api-updates-april-27.aspx