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

时间:2013-12-22 06:16:11

标签: c# httpwebrequest progress

我正在尝试使用HttpWebRequest中的C#上传和下载(在同一请求中)服务器,因为数据大小相当大(考虑到网络速度)我想展示用户完成工作的距离和剩余的工作量(不是以秒为单位,而是以百分比表示)。

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

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

3 个答案:

答案 0 :(得分:33)

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

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

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

  1. 对于方法Foo(),有一个BeginFoo()可能需要参数,并且必须返回IAsyncResult实现。此方法执行任务Foo()执行,但在不阻止当前线程的情况下完成工作。
  2. 如果BeginFoo()采用AsyncCallback参数,它希望您提供一个它将在完成后调用的委托。 (这是了解它的几种方法之一,但恰好是HttpWebRequest使用的技术。)
  3. EndFoo()IAsyncResult返回的BeginFoo()作为参数,并返回相同的内容Foo()返回。
  4. 接下来,您应该注意一个陷阱。控件有一个名为" thread affinity"的属性,这意味着如果您正在处理创建控件的线程,它只对与控件交互有效。这很重要,因为不能保证从该线程调用您提供给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参数,但我没有使用它。它使用我们之前获得的EndGetResponse()调用IAsyncResult,现在我们可以继续进行,就像我们没有使用异步调用一样。 但是因为这是一个异步回调,它可能正在工作线程上执行,所以不要在这里直接更新任何控件。

    无论如何,它从响应中获取内容长度,如果您想计算下载进度,则需要该内容长度。有时提供此信息的标题不存在,您得到-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<int>(UpdateProgressBar), percentage);
        } else {
            progressBar1.Value = percentage;
        }
    }
    

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

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

    需要考虑的事项:

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

答案 1 :(得分:0)

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

答案 2 :(得分:0)

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

唯一需要修复的是另一个局部变量作为累加器来跟踪总进度,例如totalBytesReceived + = bytesReceived;并使用while(totalBytesReceived&lt; contentLength)代替。我尝试使用currentIndex用于此目的,但它导致了问题(下载停顿),因此使用了一个新的自变量。

这是我最终使用的:

    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;

}

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