我正在尝试使用HttpWebRequest
中的C#
上传和下载(在同一请求中)服务器,因为数据大小相当大(考虑到网络速度)我想展示用户完成工作的距离和剩余的工作量(不是以秒为单位,而是以百分比表示)。
我已经阅读了几个尝试实现此功能的示例,但没有一个显示任何进度条。他们都只是使用async
在上传/下载时不阻止用户界面。而且他们主要关注上传/下载,没有人尝试将它们都包含在同一个请求中。
由于我使用.Net 4作为我的目标框架,我自己无法实现async
方法。如果您要建议异步,请使用Begin...
方法,而不是await
关键字!感谢。
答案 0 :(得分:33)
你需要知道一些事情才能成功。
步骤0:保持.NET 4.0的文档方便:
如果查看HttpWebRequest
文档,您会发现GetResponse()
有两个同名的方法:BeginGetResponse()
和EndGetResponse()
。这些方法使用最古老的.NET异步模式,称为" IAsyncResult模式"。有关此模式的数千页文本,如果您需要详细信息,可以阅读教程。这是速成课程:
Foo()
,有一个BeginFoo()
可能需要参数,并且必须返回IAsyncResult实现。此方法执行任务Foo()
执行,但在不阻止当前线程的情况下完成工作。BeginFoo()
采用AsyncCallback参数,它希望您提供一个它将在完成后调用的委托。 (这是了解它的几种方法之一,但恰好是HttpWebRequest
使用的技术。)EndFoo()
将IAsyncResult
返回的BeginFoo()
作为参数,并返回相同的内容Foo()
返回。接下来,您应该注意一个陷阱。控件有一个名为" 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;
}
除此之外,代码工作得非常好!