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

理解并使用.NET 4.5中的HttpClient的更多相关文章

  1. 理解并使用.NET 4.5中的HttpClient(转)

    原文地址:http://www.cnblogs.com/wywnet/p/httpclient.html HttpClient介绍HttpClient是.NET4.5引入的一个HTTP客户端库,其命名 ...

  2. 理解性能的奥秘——应用程序中慢,SSMS中快(6)——SQL Server如何编译动态SQL

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(5)--案例:如何应对参数嗅探 我们抛开参数嗅探的话题,回到了本系列的最 ...

  3. 理解性能的奥秘——应用程序中慢,SSMS中快(5)——案例:如何应对参数嗅探

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(4)--收集解决参数嗅探问题的信息 首先我们需要明白,参数嗅探本身不是问 ...

  4. 理解性能的奥秘——应用程序中慢,SSMS中快(4)——收集解决参数嗅探问题的信息

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(3)--不总是参数嗅探的错 前面已经提到过关于存储过程在SSMS中运行很 ...

  5. 理解性能的奥秘——应用程序中慢,SSMS中快(3)——不总是参数嗅探的错

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(2)--SQL Server如何编译存储过程 在我们开始深入研究如何处理 ...

  6. 理解性能的奥秘——应用程序中慢,SSMS中快(2)——SQL Server如何编译存储过程

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 接上文:理解性能的奥秘--应用程序中慢,SSMS中快(1)--简介 本文介绍SQL Server如何编译存储过程并使用计划缓存 ...

  7. 理解性能的奥秘——应用程序中慢,SSMS中快(1)——简介

    本文属于<理解性能的奥秘--应用程序中慢,SSMS中快>系列 在工作中发现有不少类似的现象,有幸看到国外大牛写的一篇文章,由于已经完善得不能再添油加醋,所以决定直接翻译,原文出处:http ...

  8. map和flatmap的区别+理解、学习与使用 Java 中的 Optional

    转自:map和flatmap的区别 对于stream,   两者的输入都是stream的每一个元素,map的输出对应一个元素,必然是一个元素(null也是要返回),flatmap是0或者多个元素(为n ...

  9. 理解ORM的前提:数据库中的范式和约束

    理解ORM的前提:数据库中的范式和约束 一.数据库中的范式: 范式, 英文名称是 Normal Form,它是英国人 E.F.Codd(关系数据库的老祖宗)在上个世纪70年代提出关系数据库模型后总结出 ...

随机推荐

  1. SAFS Init Files

    There're many deployment files for configuration. We need to learn how SAFS read these depolyment fi ...

  2. JQ中mouseover和mouseenter的区别

    我最近也在学习JQuery,所以最近对JQ中的一些小问题进行总结,方便学习. 在对于刚开始学习JQ的初学者来说,mouseover事件和mouseenter事件总是傻傻分不清楚,毕竟刚开始学习的时候, ...

  3. Swift - 计算文本高度

    Swift - 计算文本高度 效果 源码 // // String+StringHeight.swift // StringHeight // // Created by YouXianMing on ...

  4. 努力学习 HTML5 (2)—— 元素的增和删

    HTML5 放松了某些规则,HTML5 的制定者想让这门语言更紧密地反映浏览器的现实. 放松的规则 不要求包含 <html>.<head> 和 <body> 元素. ...

  5. 在github上写博客

    在github上混了几个月,收获颇多.作为一个开源的坚定信仰者,深深觉得每一个码农都应该参与到开源社区中,github提供了一个平台,让你为开源项目提交代码变得异常简单和直接.以前由于工作异常繁忙和繁 ...

  6. td 自动换行

    Two solutions for cell width:1. Omit words: <td style="width:60px;"><div style=&q ...

  7. 解决方案: scp/ssh 的登陆提示很慢 (Linux)

    看着用 windows 的 scp 命令很快很是羡慕. 这个问题让我实实郁闷了好几天. 在 Linux 下不管是用 ssh 还是用 scp, 连接速度都很慢 (登陆提示框的弹出时间). 确切地讲, 每 ...

  8. 如何在maven项目的pom.xml文件中添加jar包

    在使用maven进行项目开发时,我们需要在pom.xml文件中添加自己所需要的jar包.这就要求我们获取jar包的groupId和artifactId. 我们可以在一些maven仓库上搜索我们所需要的 ...

  9. Grunt 新手指南

    导言 作为一个正在准备从java 后端转大前端,一直都有想着,在js 的世界里面有没有类似于maven或者gradle 的东西..然后,就找到了grunt 这玩意 Grunt是用来干什么的 诸如ant ...

  10. OsmocomBB 编译安装

    工具: sudo apt-get install libtool shtool autoconf git-core pkg-config make gcc gnuarm: ## 32 bit wget ...