using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Net;
using System.Web; namespace Deerchao.Utility
{
public class HttpClient
{
#region fields
private bool keepContext;
private string defaultLanguage = "zh-CN";
private Encoding defaultEncoding = Encoding.UTF8;
private string accept = "*/*";
private string userAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";
private HttpVerb verb = HttpVerb.GET;
private HttpClientContext context;
private readonly List<HttpUploadingFile> files = new List<HttpUploadingFile>();
private readonly Dictionary<string, string> postingData = new Dictionary<string, string>();
private string url;
private WebHeaderCollection responseHeaders;
private int startPoint;
private int endPoint;
#endregion #region events
public event EventHandler<StatusUpdateEventArgs> StatusUpdate; private void OnStatusUpdate(StatusUpdateEventArgs e)
{
EventHandler<StatusUpdateEventArgs> temp = StatusUpdate; if (temp != null)
temp(this, e);
}
#endregion #region properties
/// <summary>
/// 是否自动在不同的请求间保留Cookie, Referer
/// </summary>
public bool KeepContext
{
get { return keepContext; }
set { keepContext = value; }
} /// <summary>
/// 期望的回应的语言
/// </summary>
public string DefaultLanguage
{
get { return defaultLanguage; }
set { defaultLanguage = value; }
} /// <summary>
/// GetString()如果不能从HTTP头或Meta标签中获取编码信息,则使用此编码来获取字符串
/// </summary>
public Encoding DefaultEncoding
{
get { return defaultEncoding; }
set { defaultEncoding = value; }
} /// <summary>
/// 指示发出Get请求还是Post请求
/// </summary>
public HttpVerb Verb
{
get { return verb; }
set { verb = value; }
} /// <summary>
/// 要上传的文件.如果不为空则自动转为Post请求
/// </summary>
public List<HttpUploadingFile> Files
{
get { return files; }
} /// <summary>
/// 要发送的Form表单信息
/// </summary>
public Dictionary<string, string> PostingData
{
get { return postingData; }
} /// <summary>
/// 获取或设置请求资源的地址
/// </summary>
public string Url
{
get { return url; }
set { url = value; }
} /// <summary>
/// 用于在获取回应后,暂时记录回应的HTTP头
/// </summary>
public WebHeaderCollection ResponseHeaders
{
get { return responseHeaders; }
} /// <summary>
/// 获取或设置期望的资源类型
/// </summary>
public string Accept
{
get { return accept; }
set { accept = value; }
} /// <summary>
/// 获取或设置请求中的Http头User-Agent的值
/// </summary>
public string UserAgent
{
get { return userAgent; }
set { userAgent = value; }
} /// <summary>
/// 获取或设置Cookie及Referer
/// </summary>
public HttpClientContext Context
{
get { return context; }
set { context = value; }
} /// <summary>
/// 获取或设置获取内容的起始点,用于断点续传,多线程下载等
/// </summary>
public int StartPoint
{
get { return startPoint; }
set { startPoint = value; }
} /// <summary>
/// 获取或设置获取内容的结束点,用于断点续传,多下程下载等.
/// 如果为0,表示获取资源从StartPoint开始的剩余内容
/// </summary>
public int EndPoint
{
get { return endPoint; }
set { endPoint = value; }
} #endregion #region constructors
/// <summary>
/// 构造新的HttpClient实例
/// </summary>
public HttpClient()
: this(null)
{
} /// <summary>
/// 构造新的HttpClient实例
/// </summary>
/// <param name="url">要获取的资源的地址</param>
public HttpClient(string url)
: this(url, null)
{
} /// <summary>
/// 构造新的HttpClient实例
/// </summary>
/// <param name="url">要获取的资源的地址</param>
/// <param name="context">Cookie及Referer</param>
public HttpClient(string url, HttpClientContext context)
: this(url, context, false)
{
} /// <summary>
/// 构造新的HttpClient实例
/// </summary>
/// <param name="url">要获取的资源的地址</param>
/// <param name="context">Cookie及Referer</param>
/// <param name="keepContext">是否自动在不同的请求间保留Cookie, Referer</param>
public HttpClient(string url, HttpClientContext context, bool keepContext)
{
this.url = url;
this.context = context;
this.keepContext = keepContext;
if (this.context == null)
this.context = new HttpClientContext();
}
#endregion #region AttachFile
/// <summary>
/// 在请求中添加要上传的文件
/// </summary>
/// <param name="fileName">要上传的文件路径</param>
/// <param name="fieldName">文件字段的名称(相当于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
public void AttachFile(string fileName, string fieldName)
{
HttpUploadingFile file = new HttpUploadingFile(fileName, fieldName);
files.Add(file);
} /// <summary>
/// 在请求中添加要上传的文件
/// </summary>
/// <param name="data">要上传的文件内容</param>
/// <param name="fileName">文件名</param>
/// <param name="fieldName">文件字段的名称(相当于&lt;input type=file name=fieldName&gt;)里的fieldName)</param>
public void AttachFile(byte[] data, string fileName, string fieldName)
{
HttpUploadingFile file = new HttpUploadingFile(data, fileName, fieldName);
files.Add(file);
}
#endregion /// <summary>
/// 清空PostingData, Files, StartPoint, EndPoint, ResponseHeaders, 并把Verb设置为Get.
/// 在发出一个包含上述信息的请求后,必须调用此方法或手工设置相应属性以使下一次请求不会受到影响.
/// </summary>
public void Reset()
{
verb = HttpVerb.GET;
files.Clear();
postingData.Clear();
responseHeaders = null;
startPoint = ;
endPoint = ;
} private HttpWebRequest CreateRequest()
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.AllowAutoRedirect = false;
req.CookieContainer = new CookieContainer();
req.Headers.Add("Accept-Language", defaultLanguage);
req.Accept = accept;
req.UserAgent = userAgent;
req.KeepAlive = false; if (context.Cookies != null)
req.CookieContainer.Add(context.Cookies);
if (!string.IsNullOrEmpty(context.Referer))
req.Referer = context.Referer; if (verb == HttpVerb.HEAD)
{
req.Method = "HEAD";
return req;
} if (postingData.Count > || files.Count > )
verb = HttpVerb.POST; if (verb == HttpVerb.POST)
{
req.Method = "POST"; MemoryStream memoryStream = new MemoryStream();
StreamWriter writer = new StreamWriter(memoryStream); if (files.Count > )
{
string newLine = "\r\n";
string boundary = Guid.NewGuid().ToString().Replace("-", "");
req.ContentType = "multipart/form-data; boundary=" + boundary; foreach (string key in postingData.Keys)
{
writer.Write("--" + boundary + newLine);
writer.Write("Content-Disposition: form-data; name=\"{0}\"{1}{1}", key, newLine);
writer.Write(postingData[key] + newLine);
} foreach (HttpUploadingFile file in files)
{
writer.Write("--" + boundary + newLine);
writer.Write(
"Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"{2}",
file.FieldName,
file.FileName,
newLine
);
writer.Write("Content-Type: application/octet-stream" + newLine + newLine);
writer.Flush();
memoryStream.Write(file.Data, , file.Data.Length);
writer.Write(newLine);
writer.Write("--" + boundary + newLine);
}
}
else
{
req.ContentType = "application/x-www-form-urlencoded";
StringBuilder sb = new StringBuilder();
foreach (string key in postingData.Keys)
{
sb.AppendFormat("{0}={1}&", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(postingData[key]));
}
if (sb.Length > )
sb.Length--;
writer.Write(sb.ToString());
} writer.Flush(); using (Stream stream = req.GetRequestStream())
{
memoryStream.WriteTo(stream);
}
} if (startPoint != && endPoint != )
req.AddRange(startPoint, endPoint);
else if (startPoint != && endPoint == )
req.AddRange(startPoint); return req;
} /// <summary>
/// 发出一次新的请求,并返回获得的回应
/// 调用此方法永远不会触发StatusUpdate事件.
/// </summary>
/// <returns>相应的HttpWebResponse</returns>
public HttpWebResponse GetResponse()
{
HttpWebRequest req = CreateRequest();
HttpWebResponse res = (HttpWebResponse)req.GetResponse();
responseHeaders = res.Headers;
if (keepContext)
{
context.Cookies = res.Cookies;
context.Referer = url;
}
return res;
} /// <summary>
/// 发出一次新的请求,并返回回应内容的流
/// 调用此方法永远不会触发StatusUpdate事件.
/// </summary>
/// <returns>包含回应主体内容的流</returns>
public Stream GetStream()
{
return GetResponse().GetResponseStream();
} /// <summary>
/// 发出一次新的请求,并以字节数组形式返回回应的内容
/// 调用此方法会触发StatusUpdate事件
/// </summary>
/// <returns>包含回应主体内容的字节数组</returns>
public byte[] GetBytes()
{
HttpWebResponse res = GetResponse();
int length = (int)res.ContentLength; MemoryStream memoryStream = new MemoryStream();
byte[] buffer = new byte[0x100];
Stream rs = res.GetResponseStream();
for (int i = rs.Read(buffer, , buffer.Length); i > ; i = rs.Read(buffer, , buffer.Length))
{
memoryStream.Write(buffer, , i);
OnStatusUpdate(new StatusUpdateEventArgs((int)memoryStream.Length, length));
}
rs.Close(); return memoryStream.ToArray();
} /// <summary>
/// 发出一次新的请求,以Http头,或Html Meta标签,或DefaultEncoding指示的编码信息对回应主体解码
/// 调用此方法会触发StatusUpdate事件
/// </summary>
/// <returns>解码后的字符串</returns>
public string GetString()
{
byte[] data = GetBytes();
string encodingName = GetEncodingFromHeaders(); if (encodingName == null)
encodingName = GetEncodingFromBody(data); Encoding encoding;
if (encodingName == null)
encoding = defaultEncoding;
else
{
try
{
encoding = Encoding.GetEncoding(encodingName);
}
catch (ArgumentException)
{
encoding = defaultEncoding;
}
}
return encoding.GetString(data);
} /// <summary>
/// 发出一次新的请求,对回应的主体内容以指定的编码进行解码
/// 调用此方法会触发StatusUpdate事件
/// </summary>
/// <param name="encoding">指定的编码</param>
/// <returns>解码后的字符串</returns>
public string GetString(Encoding encoding)
{
byte[] data = GetBytes();
return encoding.GetString(data);
} private string GetEncodingFromHeaders()
{
string encoding = null;
string contentType = responseHeaders["Content-Type"];
if (contentType != null)
{
int i = contentType.IndexOf("charset=");
if (i != -)
{
encoding = contentType.Substring(i + );
}
}
return encoding;
} private string GetEncodingFromBody(byte[] data)
{
string encodingName = null;
string dataAsAscii = Encoding.ASCII.GetString(data);
if (dataAsAscii != null)
{
int i = dataAsAscii.IndexOf("charset=");
if (i != -)
{
int j = dataAsAscii.IndexOf("\"", i);
if (j != -)
{
int k = i + ;
encodingName = dataAsAscii.Substring(k, (j - k) + );
char[] chArray = new char[] { '>', '"' };
encodingName = encodingName.TrimEnd(chArray);
}
}
}
return encodingName;
} /// <summary>
/// 发出一次新的Head请求,获取资源的长度
/// 此请求会忽略PostingData, Files, StartPoint, EndPoint, Verb
/// </summary>
/// <returns>返回的资源长度</returns>
public int HeadContentLength()
{
Reset();
HttpVerb lastVerb = verb;
verb = HttpVerb.HEAD;
using (HttpWebResponse res = GetResponse())
{
verb = lastVerb;
return (int)res.ContentLength;
}
} /// <summary>
/// 发出一次新的请求,把回应的主体内容保存到文件
/// 调用此方法会触发StatusUpdate事件
/// 如果指定的文件存在,它会被覆盖
/// </summary>
/// <param name="fileName">要保存的文件路径</param>
public void SaveAsFile(string fileName)
{
SaveAsFile(fileName, FileExistsAction.Overwrite);
} /// <summary>
/// 发出一次新的请求,把回应的主体内容保存到文件
/// 调用此方法会触发StatusUpdate事件
/// </summary>
/// <param name="fileName">要保存的文件路径</param>
/// <param name="existsAction">指定的文件存在时的选项</param>
/// <returns>是否向目标文件写入了数据</returns>
public bool SaveAsFile(string fileName, FileExistsAction existsAction)
{
byte[] data = GetBytes();
switch (existsAction)
{
case FileExistsAction.Overwrite:
using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write)))
writer.Write(data);
return true; case FileExistsAction.Append:
using (BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.Append, FileAccess.Write)))
writer.Write(data);
return true; default:
if (!File.Exists(fileName))
{
using (
BinaryWriter writer =
new BinaryWriter(new FileStream(fileName, FileMode.Create, FileAccess.Write)))
writer.Write(data);
return true;
}
else
{
return false;
}
}
}
} public class HttpClientContext
{
private CookieCollection cookies;
private string referer; public CookieCollection Cookies
{
get { return cookies; }
set { cookies = value; }
} public string Referer
{
get { return referer; }
set { referer = value; }
}
} public enum HttpVerb
{
GET,
POST,
HEAD,
} public enum FileExistsAction
{
Overwrite,
Append,
Cancel,
} public class HttpUploadingFile
{
private string fileName;
private string fieldName;
private byte[] data; public string FileName
{
get { return fileName; }
set { fileName = value; }
} public string FieldName
{
get { return fieldName; }
set { fieldName = value; }
} public byte[] Data
{
get { return data; }
set { data = value; }
} public HttpUploadingFile(string fileName, string fieldName)
{
this.fileName = fileName;
this.fieldName = fieldName;
using (FileStream stream = new FileStream(fileName, FileMode.Open))
{
byte[] inBytes = new byte[stream.Length];
stream.Read(inBytes, , inBytes.Length);
data = inBytes;
}
} public HttpUploadingFile(byte[] data, string fileName, string fieldName)
{
this.data = data;
this.fileName = fileName;
this.fieldName = fieldName;
}
} public class StatusUpdateEventArgs : EventArgs
{
private readonly int bytesGot;
private readonly int bytesTotal; public StatusUpdateEventArgs(int got, int total)
{
bytesGot = got;
bytesTotal = total;
} /// <summary>
/// 已经下载的字节数
/// </summary>
public int BytesGot
{
get { return bytesGot; }
} /// <summary>
/// 资源的总字节数
/// </summary>
public int BytesTotal
{
get { return bytesTotal; }
}
}
}

.Net类库里提供了HttpWebRequest等类,方便我们编程与Web服务器进行交互. 但是实际使用中我们经常会遇到以下需求, 基础类里没有直接提供相应的功能 (WebClient类包含这些功能,只是用起来稍微麻烦一点--谢谢网友东吴居士的提醒):

  • 对HttpWebResponse获取的HTML进行文字编码转换,使之不会出现乱码;
  • 自动在Session间保持Cookie,Referer等相关信息;
  • 模拟HTML表单提交;
  • 向服务器上传文件;
  • 对二进制的资源,直接获取返回的字节数组(byte[]),或者保存为文件

为了解决这些问题,我开发了HttpClient类.下面是使用的方法:

  • 获取编码转换后的字符串

    HttpClient client=new HttpClient(url);
    string html=client.GetString();

    GetString()函数内部会查找Http Headers, 以及HTML的Meta标签,试图找出获取的内容的编码信息.如果都找不到,它会使用client.DefaultEncoding, 这个属性默认为utf-8, 也可以手动设置.

  • 自动保持Cookie, Referer

    HttpClient client=new HttpClient(url1, null, true);
    string html1=client.GetString();
    client.Url=url2;
    string html2=client.GetString();

    这里HttpClient的第三个参数,keepContext设置为真时,HttpClient会自动记录每次交互时服务器对Cookies进行的操
    作,同时会以前一次请求的Url为Referer.在这个例子里,获取html2时,会把url1作为Referer,
    同时会向服务器传递在获取html1时服务器设置的Cookies.
    当然,你也可以在构造HttpClient时直接提供第一次请求要发出的Cookies与Referer:

    HttpClient client=new HttpClient(url, new WebContext(cookies, referer), true);

    或者,在使用过程中随时修改这些信息:

    client.Context.Cookies=cookies;
    client.Context.referer=referer;

  • 模拟HTML表单提交

    HttpClient client=new HttpClient(url);
    client.PostingData.Add(fieldName1, filedValue1);
    client.PostingData.Add(fieldName2, fieldValue2);
    string html=client.GetString();

    上面的代码相当于提交了一个有两个input的表单. 在PostingData非空,或者附加了要上传的文件时(请看下面的上传和文件), HttpClient会自动把HttpVerb改成POST, 并将相应的信息附加到Request上.

  • 向服务器上传文件

    HttpClient client=new HttpClient(url);
    client.AttachFile(fileName, fieldName);
    client.AttachFile(byteArray, fileName, fieldName);
    string html=client.GetString();

    这里面的fieldName相当于<input type="file" name="fieldName"
    />里的fieldName. fileName当然就是你想要上传的文件路径了. 你也可以直接提供一个byte[] 作为文件内容,
    但即使如此,你也必须提供一个文件名,以满足HTTP规范的要求.

  • 不同的返回形式

    字符串: string html = client.GetString();
    流: Stream stream = client.GetStream();
    字节数组: byte[] data = client.GetBytes();
    保存到文件:  client.SaveAsFile(fileName);
    或者,你也可以直接操作HttpWebResponse: HttpWebResponse res = client.GetResponse();

    每调用一次上述任何一个方法,都会导致发出一个HTTP Request, 也就是说,你不能同时得到某个Response的两种返回形式.
    另外,调用后它们任意一个之后,你可以通过client.ResponseHeaders来获取服务器返回的HTTP头.

  • 下载资源的指定部分(用于断点续传,多线程下载)

    HttpClient client=new HttpClient(url);
    //发出HEAD请求,获取资源长度
    int length=client.HeadContentLength();

    //只获取后一半内容
    client.StartPoint=length/2;
    byte[] data=client.GetBytes();

    HeadContentLength()只会发出HTTP HEAD请求.根据HTTP协议, HEAD与GET的作用等同,
    但是,只返回HTTP头,而不返回资源主体内容.
    也就是说,用这个方法,你没法获取一个需要通过POST才能得到的资源的长度,如果你确实有这样的需求,建议你可以通过GetResponse(),然后
    从ResponseHeader里获取Content-Length.

计划中还有另外一些功能要加进来,比如断点续传, 多线程下载, 下载进度更新的事件机制等, 正在思考如何与现在的代码融合到一起,期待你的反馈.

注意:使用时应该添加对System.Web.dll的引用,并在使用此类的代码前添加"using System.Web;",不然会无法通过编译.

一个C#的与web服务器交互的HttpClient类的更多相关文章

  1. AngularJs与Java Web服务器交互

    AngularJs是Google工程师研发的产品,它的强大之处不是几句话就能描述的,只有真正使用过的人才能体会到,笔者准备在这篇文章中,以一个简单的登录校验的例子说明如何使用AngularJs和Web ...

  2. 一个简单的Java web服务器实现

    前言 一个简单的Java web服务器实现,比较简单,基于java.net.Socket和java.net.ServerSocket实现: 程序执行步骤 创建一个ServerSocket对象: 调用S ...

  3. Golang学习-第二篇 搭建一个简单的Go Web服务器

    序言 由于本人一直从事Web服务器端的程序开发,所以在学习Golang也想从Web这里开始学起,如果对Golang还不太清楚怎么搭建环境的朋友们可以参考我的上一篇文章 Golang的简单介绍及Wind ...

  4. nodeJS搭建一个简单的(代理)web服务器

    前端获取数据时经常遇见跨域问题,以前一直用nginx做反向代理.最近在用vuejs,发现webpack-dev-server的代理简单好用.于是仿照写了一个简单的web服务器,用于非webpack的项 ...

  5. 手写一个最迷你的Web服务器

    今天我们就仿照Tomcat服务器来手写一个最简单最迷你版的web服务器,仅供学习交流. 1. 在你windows系统盘的F盘下,创建一个文件夹webroot,用来存放前端代码.  2. 代码介绍: ( ...

  6. 浏览器与WEB服务器交互

    问题:打开浏览器,在地址栏输入url到页面展现,整个过程发生了什么? 图示: 步骤: 1 用户输入网址,包括协议和域名. 2 浏览器先查找自身缓存有没有记录,没有的话再找操作系统缓存. 3 当浏览器在 ...

  7. VPS -Digital Ocean -搭建一个最简单的web服务器

    简单的也是美的 在一个目录放自己的几个showcase网页方便和别人分享,最简单的方式是什么 创建文件夹,放入自己的网页文件 在目录下执行 $ nohup python -m SimpleHTTPSe ...

  8. 配置一个nginx+php-fpm的web服务器

    一.基本信息 系统(L):CentOS 6.9 #下载地址:http://mirrors.sohu.com 反代&负载均衡(N):NGINX 1.14.0 #下载地址:http://nginx ...

  9. 用C写一个web服务器(一) 基础功能

    .container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .conta ...

随机推荐

  1. css3之自定义字体

    使用@font-face自定义字体 我们在浏览国外的一些个人网站时,总是可以发现一些非常个性的字体,比如

  2. uC/OS-II测试(TEST)块

    /*************************************************************************************************** ...

  3. SQL Server 2012 学习笔记2

    1. 新建数据库 可以在对应目录下右键新建数据库,也可以用程序添加: 先打开程序编辑对话框"New Query" create database Library 2. 添加表格 可 ...

  4. WinForm------Reflector反编译工具下载

    地址: http://www.ddooo.com/softdown/70642.htm

  5. JQuery------.load()从服务器获取数据并加载到某个类的方法

    注意:需要在../Content/asf.txt路径下加入文件 html <button class="Btn">按钮</button> js 参数意义: ...

  6. MinGW: TOO MANY SECTIONS issue

    http://sourceforge.net/p/mingw-w64/mailman/mingw-w64-public/thread/509A38C6.6070807@doxos.eu/ MingGW ...

  7. Crontab的格式

    第1列分钟1-59第2列小时1-23(0表示子夜)第3列日1-31第4列月1-12第5列星期0-6(0表示星期天)第6列要运行的命令 下面是crontab的格式:分 时 日 月 星期 要运行的命令 这 ...

  8. mysql 插入/更新数据

    mysql 插入/更新数据 INSERT 语句 1.一次性列出全部字段的值,例如: INSERT INTO student VALUES('Chenqi','M', 29); INSERT INTO ...

  9. ssh-keygen不是内部或外部命令

    在**/Git/usr/bin目录下找到ssh-keygen.exe,将**/Git/usr/bin路径添加到环境变量中

  10. firefox的plugin-container.exe进程如何关闭?

    为什么要关闭container进程? 查看firefox所消耗的资源: ff本身: cpu一般是0-10%, 内存一般是400MB左右 plugin-container: cpu所占的比例很高, 可达 ...