背景
在平时工作中我偶尔会写一些脚本监控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来达到相同目的。但是有几点值得关注:
异步HTTP GET
下面是一个使用HttpClient进行HTTP GET请求数据的例子:
01 |
using System; |
02 |
using System.Net.Http; |
03 |
04 |
namespace HttpClientProject |
05 |
{ |
06 |
class HttpClientDemo |
07 |
{ |
08 |
private const string Uri = "http://api.worldbank.org/countries?format=json" ; |
09 |
10 |
static void Main( string [] args) |
11 |
{ |
12 |
HttpClient httpClient = new HttpClient(); |
13 |
14 |
// 创建一个异步GET请求,当请求返回时继续处理 |
15 |
httpClient.GetAsync(Uri).ContinueWith( |
16 |
(requestTask) => |
17 |
{ |
18 |
HttpResponseMessage response = requestTask.Result; |
19 |
20 |
// 确认响应成功,否则抛出异常 |
21 |
response.EnsureSuccessStatusCode(); |
22 |
23 |
// 异步读取响应为字符串 |
24 |
response.Content.ReadAsStringAsync().ContinueWith( |
25 |
(readTask) => Console.WriteLine(readTask.Result)); |
26 |
}); |
27 |
28 |
Console.WriteLine( "Hit enter to exit..." ); |
29 |
Console.ReadLine(); |
30 |
} |
31 |
} |
32 |
} |
代码运行后将先输出“Hit enter to exit...“,然后才输出请求响应内容,因为采用的是GetAsync(string requestUri)异步方法,它返回的是Task<HttpResponseMessage>对象( 这里的httpClient.GetAsync(Uri).ContinueWith(...)有点类似JavaScript中使用Promise对象进行异步编程的写法,具体可以参考 JavaScript异步编程的四种方法 的第四节和 jQuery的deferred对象详解)。于是我们可以用.NET 4.5之后支持的async、await关键字来重写上面的代码,仍保持了异步性:
01 |
using System; |
02 |
using System.Net.Http; |
03 |
04 |
namespace HttpClientProject |
05 |
{ |
06 |
class HttpClientDemo |
07 |
{ |
08 |
private const string Uri = "http://api.worldbank.org/countries?format=json" ; |
09 |
10 |
static async void Run() |
11 |
{ |
12 |
HttpClient httpClient = new HttpClient(); |
13 |
14 |
// 创建一个异步GET请求,当请求返回时继续处理(Continue-With模式) |
15 |
HttpResponseMessage response = await httpClient.GetAsync(Uri); |
16 |
response.EnsureSuccessStatusCode(); |
17 |
string resultStr = await response.Content.ReadAsStringAsync(); |
18 |
19 |
Console.WriteLine(resultStr); |
20 |
} |
21 |
22 |
static void Main( string [] args) |
23 |
{ |
24 |
Run(); |
25 |
26 |
Console.WriteLine( "Hit enter to exit..." ); |
27 |
Console.ReadLine(); |
28 |
} |
29 |
} |
30 |
} |
注意,如果以下面的方式获取HttpResponseMessage会有什么后果呢?
1 |
HttpResponseMessage response = httpClient.GetAsync(Url).Result; |
1 |
string resultStr = response.Content.ReadAsStringAsync().Result; |
异步HTTP POST
我以前写过一个用HttpWebRequest自动登录开源中国的代码(见 C#自动登录开源中国 ),在此基础上我用HttpClient的PostAsync方法改写如下:
01 |
using System; |
02 |
using System.Collections.Generic; |
03 |
using System.Linq; |
04 |
using System.Net; |
05 |
using System.Net.Http; |
06 |
using System.Web.Security; |
07 |
08 |
namespace AsycLoginOsChina |
09 |
{ |
10 |
public class OschinaLogin |
11 |
{ |
12 |
// MD5或SHA1加密 |
13 |
public static string EncryptPassword( string pwdStr, string pwdFormat) |
14 |
{ |
15 |
string EncryptPassword = null ; |
16 |
if ( "SHA1" .Equals(pwdFormat.ToUpper())) |
17 |
{ |
18 |
EncryptPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(pwdStr, "SHA1" ); |
19 |
} |
20 |
else if ( "MD5" .Equals(pwdFormat.ToUpper())) |
21 |
{ |
22 |
EncryptPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(pwdStr, "MD5" ); |
23 |
} |
24 |
else |
25 |
{ |
26 |
EncryptPassword = pwdStr; |
27 |
} |
28 |
return EncryptPassword; |
29 |
} |
30 |
31 |
/// <summary> |
32 |
/// OsChina登陆函数 |
33 |
/// </summary> |
34 |
/// <param name="email"></param> |
35 |
/// <param name="pwd"></param> |
36 |
public static void LoginOsChina( string email, string pwd) |
37 |
{ |
38 |
HttpClient httpClient = new HttpClient(); |
39 |
40 |
// 设置请求头信息 |
41 |
httpClient.DefaultRequestHeaders.Add( "Host" , "www.oschina.net" ); |
42 |
httpClient.DefaultRequestHeaders.Add( "Method" , "Post" ); |
43 |
httpClient.DefaultRequestHeaders.Add( "KeepAlive" , "false" ); // HTTP KeepAlive设为false,防止HTTP连接保持 |
44 |
httpClient.DefaultRequestHeaders.Add( "UserAgent" , |
45 |
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11" ); |
46 |
47 |
// 构造POST参数 |
48 |
HttpContent postContent = new FormUrlEncodedContent( new Dictionary< string , string >() |
49 |
{ |
50 |
{ "email" , email}, |
51 |
{ "pwd" , EncryptPassword(pwd, "SHA1" )} |
52 |
}); |
53 |
54 |
httpClient |
55 |
.PostAsync( "http://www.oschina.net/action/user/hash_login" , postContent) |
56 |
.ContinueWith( |
57 |
(postTask) => |
58 |
{ |
59 |
HttpResponseMessage response = postTask.Result; |
60 |
61 |
// 确认响应成功,否则抛出异常 |
62 |
response.EnsureSuccessStatusCode(); |
63 |
64 |
// 异步读取响应为字符串 |
65 |
response.Content.ReadAsStringAsync().ContinueWith( |
66 |
(readTask) => Console.WriteLine( "响应网页内容:" + readTask.Result)); |
67 |
Console.WriteLine( "响应是否成功:" + response.IsSuccessStatusCode); |
68 |
69 |
Console.WriteLine( "响应头信息如下:\n" ); |
70 |
var headers = response.Headers; |
71 |
foreach (var header in headers) |
72 |
{ |
73 |
Console.WriteLine( "{0}: {1}" , header.Key, string .Join( "" , header.Value.ToList())); |
74 |
} |
75 |
} |
76 |
); |
77 |
} |
78 |
79 |
public static void Main( string [] args) |
80 |
{ |
81 |
LoginOsChina( "你的邮箱" , "你的密码" ); |
82 |
83 |
Console.ReadLine(); |
84 |
} |
85 |
} |
86 |
} |
代码很简单,就不多说了,只要将上面的Main函数的邮箱、密码信息替换成你自己的OsChina登录信息即可。上面通过httpClient.DefaultRequestHeaders属性来设置请求头信息,也可以通过postContent.Header属性来设置。上面并没有演示如何设置Cookie,而有的POST请求可能需要携带Cookie,那么该怎么做呢?这时候就需要利用HttpClientHandler(关于它的详细信息见下一节)了,如下:
1 |
CookieContainer cookieContainer = new CookieContainer(); |
2 |
cookieContainer.Add( new Cookie( "test" , "0" )); // 加入Cookie |
3 |
HttpClientHandler httpClientHandler = new HttpClientHandler() |
4 |
{ |
5 |
CookieContainer = cookieContainer, |
6 |
AllowAutoRedirect = true , |
7 |
UseCookies = true |
8 |
}; |
9 |
HttpClient httpClient = new HttpClient(httpClientHandler); |
然后像之前一样使用httpClient。至于ASP.NET服务端如何访问请求中的Cookie和设置Cookie,可以参考:ASP.NET HTTP Cookies 。
其他HTTP操作如PUT和DELETE,分别由HttpClient的PutAsync和DeleteAsync实现,用法基本同上面,就不赘述了。
出错处理
默认情况下如果HTTP请求失败,不会抛出异常,但是我们可以通过返回的HttpResponseMessage对象的StatusCode属性来检测请求是否成功,比如下面:
1 |
HttpResponseMessage response = postTask.Result; |
2 |
if (response.StatusCode == HttpStatusCode.OK) |
3 |
{ |
4 |
// ... |
5 |
} |
1 |
HttpResponseMessage response = postTask.Result; |
2 |
if (response.IsSuccessStatusCode) |
3 |
{ |
4 |
// ... |
5 |
} |
1 |
try |
2 |
{ |
3 |
HttpResponseMessage response = postTask.Result; |
4 |
response.EnsureSuccessStatusCode(); |
5 |
} |
6 |
catch (HttpRequestException e) |
7 |
{ |
8 |
// ... |
9 |
} |
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 |
Task<HttpResponseMessage> SendAsync( |
2 |
HttpRequestMessage request, CancellationToken cancellationToken); |
01 |
class LoggingHandler : DelegatingHandler |
02 |
{ |
03 |
StreamWriter _writer; |
04 |
05 |
public LoggingHandler(Stream stream) |
06 |
{ |
07 |
_writer = new StreamWriter(stream); |
08 |
} |
09 |
10 |
protected override async Task<HttpResponseMessage> SendAsync( |
11 |
HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) |
12 |
{ |
13 |
var response = await base .SendAsync(request, cancellationToken); |
14 |
15 |
if (!response.IsSuccessStatusCode) |
16 |
{ |
17 |
_writer.WriteLine( "{0}\t{1}\t{2}" , request.RequestUri, |
18 |
( int )response.StatusCode, response.Headers.Date); |
19 |
} |
20 |
return response; |
21 |
} |