理解并使用.NET 4.5中的HttpClient
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请求数据的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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关键字来重写上面的代码,仍保持了异步性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
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(关于它的详细信息见下一节)了,如下:
1
2
3
4
5
6
7
8
9
|
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属性来检测请求是否成功,比如下面:
1
2
3
4
5
|
HttpResponseMessage response = postTask.Result; if (response.StatusCode == HttpStatusCode.OK) { // ... } |
或者通过HttpResponseMessage的IsSuccessStatusCode属性来检测:
1
2
3
4
5
|
HttpResponseMessage response = postTask.Result; if (response.IsSuccessStatusCode) { // ... } |
再或者你更喜欢以异常的形式处理请求失败情况,那么你可以用下面的代码:
1
2
3
4
5
6
7
8
9
|
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,并重写下面这个方法:
1
2
|
Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken); |
比如下面自定义了一个客户端消息处理器来记录HTTP错误码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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对象上:
1
|
HttpClient client = HttpClientFactory.Create( new LoggingHandler(), new Handler2(), new Handler3()); |
上面的自定义消息处理器只拦截处理了HTTP响应,如果我们既想拦截处理HTTP请求,又想拦截处理HTTP响应,那么该怎么做呢?如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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的简单示例如下:
1
2
3
4
5
6
|
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方法,以支持直接异步下载文件到本地:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
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类型。一般性的示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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(); } } |