65.9K
CodeProject 正在变化。 阅读更多。
Home

基于套接字的 HTTP 客户端,具有带宽限制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (12投票s)

2010 年 8 月 10 日

CPOL

4分钟阅读

viewsIcon

57114

downloadIcon

3264

一个简单的基于套接字的 HTTP 客户端实现,能够限制上传/下载速度

引言

通过 HTTP 进行通信是一项非常常见的任务。大多数 .NET 程序员都熟悉 WebRequestWebResponseWebClient 等类,它们非常有用,但有时您需要它们不提供的功能。在这些情况下,我们别无选择,只能基于 TCP 套接字编写自己的 HTTP 客户端实现。本文是一个示例,展示了如何做到这一点。读者应该了解 HTTP 协议的基础知识。

背景

我编写自己的 HTTP 客户端库的首要原因是需要将大型文件上传到服务器并显示当前的上传速度。这似乎并不难,但我发现标准类在发送之前会将所有输出数据加载到内存中,尽管 WebRequest 类为 POST 数据提供了输出流。HTTP 协议并不复杂,所以为什么不直接打开一个端口为 80 的套接字并开始通信呢?

我不想重写我的整个应用程序,而是想先进行一些实验,以确保我的想法是可持续的。我需要创建一个 abstract 接口,使我能够随时返回到标准解决方案。以下是我的类:

  • HttpConnection - 表示一个到 Web 服务器的 abstract 连接,一次只能处理一个请求
  • HttpSocketConnection - 基于 TCP 套接字的新客户端实现
  • HttpWebRequestConnection - 使用标准 WebRequestWebResponse 类的实现
  • HttpMessage - HTTP 请求或响应的容器,包括标头和数据流

这些类是您运行带有双向数据上传/下载的简单 HTTP 请求所需的一切。我还准备了一些代码,可以控制网络通信使用的带宽。这可以通过简单地减慢读取流来实现——首先是 POST 数据,然后是服务器响应,所以我创建了一个流代理类来减慢代理所持有的给定流的读/写操作。

  • WatchedStream - 作为给定流的代理,在每次读/写操作之前调用事件
  • WatchedStreamEventArgs - 流传输事件的事件参数
  • HttpAdvancedStream - 使用基类的事件来插入一些等待时间
  • BandWidthManager - 负责测量时间和计算传输的数据以保持所需速度

Class diagram 1

如果您想使用该库发送 POST 请求,您可以使用某个框架来创建 POST 正文。当然,我创建了一个,它可以准备两种常见格式的请求正文:简单的 URL 编码和多部分格式。

  • HttpPostBodyBuilder - POST 正文构建器的 abstract
  • HttpPostBodyBuilder.Multipart - 以标准多部分格式准备请求正文,允许您上传文件
  • HttpPostBodyBuilder.UrlEncoded - 以标准 URL 格式准备请求正文
  • MergingStream - 此流从多个给定流中读取数据,这允许在不需要将所有必需数据加载到内存的情况下准备整个请求正文
  • HttpUtility - 从 System.Web.dll 中提取的 URL 编码逻辑。这允许您避免引用该库,并将应用程序保持在 .NET Framework Client Profile 下。

Class diagram 2

Using the Code

我准备并附上了一个客户端/服务器示例解决方案供您测试和更好地理解该库。有一个 ASP.NET 网站允许您上传图像,然后进行一些图形操作,并将结果发送回浏览器。要调用该服务,您可以使用标准浏览器或模拟浏览器行为的 WinForms 客户端。

Screenshot

这部分代码至关重要

this.NotifyState("Converting file...", 0);

// choose connection
HttpConnection http;
switch (httpMethod)
{
	case 0:
		http = new HttpSocketConnection();
		break;
	case 1:
		http = new HttpWebRequestConnection();
		break;
	default:
		throw new NotSupportedException();
}

// prepare request
var url = "https://: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;

// send request
advStream.BeforeRead +=
	(s, e) => this.NotifyState("Uploading...", e.Position, bodyStream.Length);
var response = http.Send(url, req);

// get response
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);

正如您所见,代码片段并不算短,但它完成了大量工作

  • 选择上传数据的方法(WebRequests/套接字)
  • 准备带有要上传文件的请求正文(文件在发送过程中持续加载)
  • 准备限速流(您也可以选择不使用它们,或在传输过程中更改速度)
  • 使用 BeforeRead 事件通知用户进度
  • 展示如何设置请求标头

限制

请不要将此库视为 HttpWebRequest 和其他类的完整替代品。这只是一个简单的解决方案,由于其限制,不能用于任何场景

  • 不支持 HTTPS
  • 不支持代理
  • 无缓存
  • 不保持套接字打开
  • 只有少数 HTTP 状态被正确处理(100、206、302 和当然 200)
  • 只有支持查找的流才能用作上传源

历史

  • 1.0 - 初始版本
© . All rights reserved.