使用HttpWebRequest类在上传和下载时显示百分比的进度

我正在尝试使用C# HttpWebRequest上传和下载(在同一请求中)到服务器,因为数据的大小相当大(考虑网络速度)我想向用户显示工作完成的距离以及如何留下了很多(不是几秒钟,而是百分比)。

我已经阅读了几个尝试实现这一点的例子,但没有一个显示任何进度条。 他们都只是使用async而不是在上传/下载时阻止UI。 而且他们主要关注上传/下载,没有人尝试将它们都包含在同一个请求中。

由于我使用.Net 4作为我的目标框架,我自己无法实现async方法。 如果你要建议任何异步,请使用Begin...方法而不是await关键字! 谢谢。

你需要知道一些事情才能成功。

第0步:保持.NET 4.0的文档方便:

  • HttpWebRequest的
  • HttpWebResponse

如果查看HttpWebRequest文档,您会看到GetResponse()有两个类似命名的方法: BeginGetResponse()EndGetResponse() 。 这些方法使用最古老的.NET异步模式,称为“IAsyncResult模式”。 有关此模式的数千页文本,如果您需要详细信息,可以阅读教程。 这是速成课程:

  1. 对于方法Foo() ,有一个BeginFoo()可以接受参数,并且必须返回IAsyncResult实现。 此方法执行Foo()执行的任务,但在不阻塞当前线程的情况下完成工作。
  2. 如果BeginFoo()接受AsyncCallback参数,它希望您提供一个它将在完成时调用的委托。 (这是知道它已经完成的几种方法之一,但恰好是HttpWebRequest使用的技术。)
  3. EndFoo()EndFoo()返回的IAsyncResult作为参数,并返回Foo()返回的相同内容。

接下来,您应该注意一个陷阱。 控件具有一个名为“线程关联”的属性,这意味着如果您正在处理创建控件的线程,它只对与控件交互有效。 这很重要,因为不能保证从该线程调用您给BeginFoo()的回调方法。 这实际上很容易处理:

  • 每个控件都有一个InvokeRequired属性,这是从错误的线程调用的唯一安全属性。 如果它返回true ,则表示您处于不安全的线程中。
  • 每个控件都有一个Invoke()方法,它接受一个委托参数,并在创建控件的线程上调用该委托。

完成所有这些后,让我们开始查看我在一个简单的WinForms应用程序中编写的一些代码,以报告下载Google主页的进度。 这应该类似于解决您的问题的代码。 它不是最好的代码,但它演示了这些概念。 表单有一个名为progressBar1的进度条,我从按钮单击调用了GetWebContent()

 HttpWebRequest _request; IAsyncResult _responseAsyncResult; private void GetWebContent() { _request = WebRequest.Create("http://www.google.com") as HttpWebRequest; _responseAsyncResult = _request.BeginGetResponse(ResponseCallback, null); } 

此代码启动GetResponse()的异步版本。 我们需要将请求和IAsyncResult存储在字段中,因为ResponseCallback()需要调用EndGetResponse()GetWebContent()所有内容都在UI线程上,因此如果您想更新某些控件,可以在此处安全地执行此操作。 接下来, ResponseCallback()

 private void ResponseCallback(object state) { var response = _request.EndGetResponse(_responseAsyncResult) as HttpWebResponse; long contentLength = response.ContentLength; if (contentLength == -1) { // You'll have to figure this one out. } Stream responseStream = response.GetResponseStream(); GetContentWithProgressReporting(responseStream, contentLength); response.Close(); } 

它被迫通过AsyncCallback委托签名获取一个object参数,但我没有使用它。 它使用我们之前获得的IAsyncResult调用EndGetResponse() ,现在我们可以继续进行,好像我们没有使用异步调用一样。 由于这是一个异步回调,它可能正在工作线程上执行,所以不要在这里直接更新任何控件。

无论如何,它从响应中获取内容长度,如果您想要计算下载进度,则需要这样做。 有时提供此信息的标头不存在,您得到-1。 这意味着您可以自己进行进度计算,并且您必须找到一些其他方法来了解您正在下载的文件的总大小。 在我的情况下,将变量设置为某个值就足够了,因为我不关心数据本身。

之后,它获取表示响应的流,并将其传递给执行下载和进度报告的辅助方法:

 private byte[] GetContentWithProgressReporting(Stream responseStream, long contentLength) { UpdateProgressBar(0); // Allocate space for the content var data = new byte[contentLength]; int currentIndex = 0; int bytesReceived = 0; var buffer = new byte[256]; do { bytesReceived = responseStream.Read(buffer, 0, 256); Array.Copy(buffer, 0, data, currentIndex, bytesReceived); currentIndex += bytesReceived; // Report percentage double percentage = (double)currentIndex / contentLength; UpdateProgressBar((int)(percentage * 100)); } while (currentIndex < contentLength); UpdateProgressBar(100); return data; } 

此方法仍然可能位于工作线程上(因为它是由回调调用的),因此从此处更新控件仍然不安全。

代码对于下载文件的示例是常见的。 它分配一些内存来存储文件。 它分配一个缓冲区以从流中获取文件块。 在循环中,它抓取一个块,将块放入更大的数组中,并计算进度百分比。 当它下载尽可能多的字节时,它就会退出。 这有点麻烦,但是如果你要使用让你一次下载文件的技巧,你将无法在中间报告下载进度。

(有一件事我觉得有必要指出:如果你的块太小,你将很快更新进度条,这仍然会导致表单锁定。我通常会让Stopwatch运行并尝试不更新进度每秒超过两次,但还有其他方法来限制更新。)

无论如何,唯一剩下的就是实际更新进度条的代码,并且它有自己的方法:

 private void UpdateProgressBar(int percentage) { // If on a worker thread, marshal the call to the UI thread if (progressBar1.InvokeRequired) { progressBar1.Invoke(new Action(UpdateProgressBar), percentage); } else { progressBar1.Value = percentage; } } 

如果InvokeRequired返回true,则它通过Invoke()调用自身。 如果碰巧在正确的线程上,它会更新进度条。 如果你碰巧使用WPF,有类似的方法来编组调用,但我相信它们是通过Dispatcher对象发生的。

我知道这很多。 这就是为什么新的async内容如此之大的部分原因。 IAsyncResult模式function强大,但不会从您身上抽象出许多细节。 但即使在较新的模式中,您也必须跟踪何时更新进度条是安全的。

需要考虑的事项:

  • 这是示例代码。 为简洁起见,我没有包含任何exception处理。
  • 如果GetResponse()抛出exception,则在调用EndGetResponse()时会抛出exception。
  • 如果回调中发生exception并且您没有处理它,它将终止其线程但不终止您的应用程序。 对于新手异步程序员来说,这可能是奇怪和神秘的。
  • 注意我所说的内容长度! 您可能认为可以通过检查其Length属性来询问Stream ,但我发现这是不可靠的。 如果不确定, Stream可以自由地抛出NotSupportedException或者返回一个类似于-1的值,如果你事先没有告诉你需要多少数据,那么你通常会使用网络流,然后你只能继续问:“还有更多吗?”
  • 我没有说上传,因为:
    • 我对上传不熟悉。
    • 您可以遵循类似的模式:您可能会使用HttpWebRequest.BeginGetRequestStream()因此您可以异步写入要上载的文件。 有关更新控件的所有警告均适用。

只要在调用GetRequestStream之前设置HttpWebRequest.ContentLength或HttpWebRequest.SendChunked,您发送的数据将在每次调用Stream时发送到服务器。[Begin] Write。 如果您以小块的forms编写文件建议,您可以了解您的距离。 这是一个下载解决方案。

bytesReceived变量从responseStream.Read(buffer,0,256)获取它的值; 返回接收到的每个单个数据包的大小,而不是总数,因此(do … while)几乎总是返回true,导致无限循环,除非下载的内容太小以至于数据包与TCP填充结合最终大于或等于contentLength。

唯一需要修复的是使用另一个局部变量作为累加器来跟踪总进度,例如totalBytesReceived + = bytesReceived; 并使用while(totalBytesReceived

这是我最终使用的:

  private byte[] GetContentWithProgressReporting(Stream responseStream, long contentLength) { UpdateProgressBar(0); // Allocate space for the content var data = new byte[contentLength]; int currentIndex = 0; int bytesReceived = 0; int totalBytesReceived = 0; var buffer = new byte[256]; do { bytesReceived = responseStream.Read(buffer, 0, 256); Array.Copy(buffer, 0, data, currentIndex, bytesReceived); currentIndex += bytesReceived; totalBytesReceived += bytesReceived; // Report percentage double percentage = (double)currentIndex / contentLength; UpdateProgressBar((int)(percentage * 100)); } while (totalBytesReceived < contentLength); //DEBUGGING: MessageBox.Show("GetContentWithProgressReporting Method - Out of loop"); UpdateProgressBar(100); return data; 

}

除此之外,代码工作得非常好!