对于.NET程序员来说,像WebRequest、WebResponse或WebClient这样的类非常有用,但有时它们并不提供需要的所有功能。在这种情况下,可能别无选择,只能基于TCP套接字编写自己的HTTP客户端实现。本文就是一个如何实现的例子。读者应该了解HTTP协议的基础知识。
编写自己的HTTP客户端库的第一个原因是需要将大文件上传到服务器,并显示当前的上传速度。这看起来并不困难,但发现了标准类在发送数据之前会将所有输出数据加载到内存中,尽管WebRequest类为POST数据提供了输出流。HTTP协议并不复杂,为什么不直接在端口80上打开一个套接字并开始通信呢?
不想重写整个应用程序,只是想先做一些实验,以确保想法是可行的。需要创建一个抽象接口,这样可以随时返回到标准解决方案。这些是类:
HttpConnection - 表示对Web服务器的抽象连接,一次只能处理一个请求。
HttpSocketConnection - 基于TCP套接字的新客户端实现。
HttpWebRequestConnection - 使用标准WebRequest和WebResponse类的实现。
HttpMessage - 包含HTTP请求或响应的容器,包括头和数据流。
这些类是运行简单HTTP请求以及双向数据上传/下载所需的全部。还准备了一些代码,可以控制网络通信使用的带宽。这可以通过减慢读流来实现 - 首先是POST数据,其次是服务器响应,所以创建了一个流代理类,用于减慢给定流的读写操作。
作为给定流的代理,在每次读写操作之前调用事件。
流传输事件的事件参数。
使用基类的事件插入一些等待时间。
负责测量时间并计算传输的数据以保持所需的速度。
用于创建POST请求体的抽象类。
以标准多部分格式准备请求体,允许上传文件。
以标准URL格式准备请求体。
这个流从多个给定的流中读取数据,允许准备整个请求体而无需将所有必要的数据加载到内存中。
从System.Web.dll中提取的URL编码逻辑。这允许避免引用这个库,并将应用程序保持在.NETFramework Client Profile下。
为准备了一个客户端/服务器示例解决方案,供测试和更好地理解库。有一个ASP.NET网站允许上传图像,然后它执行一些图形操作,并将结果发送回浏览器。要调用服务,可以使用标准浏览器或WinForms客户端模拟浏览器行为。
// 选择连接
HttpConnection http;
switch(httpMethod)
{
case 0:
http = new HttpSocketConnection();
break;
case 1:
http = new HttpWebRequestConnection();
break;
default:
throw new NotSupportedException();
}
// 准备请求
var url = "http://localhost:12345/Page.aspx";
var postBody = new HttpPostBodyBuilder.Multipart();
var fileStream = new FileStream(this.openFileDialog.FileName, FileMode.Open, FileAccess.Read);
var advStream = new BandwidthControlledStream(fileStream);
lock(this.bandWidthSync)
{
this.uploadSpeed = advStream.ReadSpeed;
this.uploadSpeed.BytesPerSecond = 1024 * (int)this.upSpeedBox.Value;
}
postBody.AddData("imageFile", advStream, Path.GetFileName(this.openFileDialog.FileName), GetMimeType(this.openFileDialog.FileName));
var bodyStream = postBody.PrepareData();
bodyStream.Position = 0;
var req = new HttpMessage.Request(bodyStream, "POST");
req.ContentLength = bodyStream.Length;
req.ContentType = postBody.GetContentType();
req.Headers["Referer"] = url;
// 发送请求
advStream.BeforeRead += (s, e) => this.NotifyState("Uploading...", e.Position, bodyStream.Length);
var response = http.Send(url, req);
// 获取响应
var readStream = new BandwidthControlledStream(response.Stream);
lock(this.bandWidthSync)
{
this.downloadSpeed = readStream.ReadSpeed;
this.downloadSpeed.BytesPerSecond = 1024*(int)this.downSpeedBox.Value;
}
readStream.BeforeRead += (s, e) => this.NotifyState("Downloading...", e.Position, response.ContentLength);
this.convertedFile = ReadAll(readStream, (int) response.ContentLength);
this.NotifyState("Done", 100, true);
如所见,代码片段并不短,但它做了很多工作: