背景
在平时工作中我偶尔会写一些脚本监控HTTP接口的健康状况,基本上都是发送HTTP GET或HTTP POST请求,然后检测响应内容。但一直用的是WebClient和HttpWebRequest,虽然用它们也能进行异步请求(可参考我分享的代码:C#异步GET的3个例子),但总感觉那样用起来不太自然。(关于测试大量页面链接,Stackoverflow的开发人员有一篇相关文章:Testing 3 Million Hyperlinks, Lessons Learned)
网上搜索后发现.NET 4.5引入的HttpClient库能够完成异步HTTP请求。于是结合多方资料总结下HttpClient的特性和使用。本文属于阅读笔记性质博客!
准备工作
在VS2012(VS2010应该也可以)中新建一个控制台应用,然后添加对System.Net和System.Net.Http程序集的引用。
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请求数据的例子:
- using System;
- using System.Net.Http;
-
- namespace HttpClientProject
- {
- class HttpClientDemo
- {
- private const string Uri = "http://api.worldbank.org/countries?format=json";
-
- static void Main(string[] args)
- {
- HttpClient httpClient = new HttpClient();
-
-
- 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关键字来重写上面的代码,仍保持了异步性:
- using System;
- using System.Net.Http;
-
- namespace HttpClientProject
- {
- class HttpClientDemo
- {
- private const string Uri = "http://api.worldbank.org/countries?format=json";
-
- static async void Run()
- {
- HttpClient httpClient = new HttpClient();
-
-
- 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
我以前写过一个用HttpWebRequest自动登录开源中国的代码(见 C#自动登录开源中国 ),在此基础上我用HttpClient的PostAsync方法改写如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Net;
- using System.Net.Http;
- using System.Web.Security;
-
- namespace AsycLoginOsChina
- {
- public class OschinaLogin
- {
-
- 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;
- }
-
-
-
-
-
-
- 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");
- 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");
-
-
- 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"));
- 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);
- 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;
-
-
- HttpClient client = new HttpClient(webRequestHandler);
为HttpClient定制下载文件方法
HttpClient没有直接下载文件到本地的方法。我们知道response.Content是HttpContent对象,表示HTTP响应消息的内容,它已经支持ReadAsStringAsync、ReadAsStreamAsync和ReadAsByteArrayAsync等方法。响应消息内容也支持异步读取(为什么响应消息内容读取也需要异步呢?原因在于响应消息内容非常大时异步读取好处很明显)。下面我给HttpContent类扩展一个DownloadAsFileAsync方法,以支持直接异步下载文件到本地:
- using System;
- using System.IO;
- using System.Net.Http;
- using System.Threading.Tasks;
-
- namespace HttpDownloadFile
- {
-
- public static class HttpContentExtension
- {
-
-
-
-
-
-
-
- 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();
-
-
- 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类型。一般性的示例如下:
- using System;
- using System.Net.Http;
-
- namespace HttpSendAsync
- {
- 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();
- }
- }
- }