使用HttpWebRequest类在上传和下载时显示百分比的进度
我正在尝试使用C#
HttpWebRequest
上传和下载(在同一请求中)到服务器,因为数据的大小相当大(考虑网络速度)我想向用户显示工作完成的距离以及如何留下了很多(不是几秒钟,而是百分比)。
我已经阅读了几个尝试实现这一点的例子,但没有一个显示任何进度条。 他们都只是使用async
而不是在上传/下载时阻止UI。 而且他们主要关注上传/下载,没有人尝试将它们都包含在同一个请求中。
由于我使用.Net 4作为我的目标框架,我自己无法实现async
方法。 如果你要建议任何异步,请使用Begin...
方法而不是await
关键字! 谢谢。
你需要知道一些事情才能成功。
第0步:保持.NET 4.0的文档方便:
- HttpWebRequest的
- HttpWebResponse
如果查看HttpWebRequest
文档,您会看到GetResponse()
有两个类似命名的方法: BeginGetResponse()
和EndGetResponse()
。 这些方法使用最古老的.NET异步模式,称为“IAsyncResult模式”。 有关此模式的数千页文本,如果您需要详细信息,可以阅读教程。 这是速成课程:
- 对于方法
Foo()
,有一个BeginFoo()
可以接受参数,并且必须返回IAsyncResult实现。 此方法执行Foo()
执行的任务,但在不阻塞当前线程的情况下完成工作。 - 如果
BeginFoo()
接受AsyncCallback参数,它希望您提供一个它将在完成时调用的委托。 (这是知道它已经完成的几种方法之一,但恰好是HttpWebRequest
使用的技术。) -
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;