如何执行同步和异步 Web 下载






4.83/5 (29投票s)
2001 年 10 月 18 日
4分钟阅读

181468

1266
解释 WebRequest、WebResponse 和相关类的用法。
引言
在本文中,我们将了解如何从 Web 下载文件。使用 WebRequest
和 WebResponse
类可以毫不费力地完成此操作。这些类提供了允许我们以流的形式访问 Web 数据的各种方法。因此,我们可以使用各种可用的读/写类来处理流。有两种机制可用于下载文件。对于小文件,我们可以使用同步机制;对于大文件或从响应时间不可预测的服务器下载的文件,我们可以使用异步机制。我将在本文中演示这两种方法。
同步下载
void DownloadFile(String* url, String* fpath) { WebRequest* wrq = WebRequest::Create(url); HttpWebResponse* hwr = static_cast<HttpWebResponse*>(wrq->GetResponse()); Stream* strm = hwr->GetResponseStream(); FileStream* fs = new FileStream(fpath,FileMode::Create,FileAccess::Write); BinaryWriter* br = new BinaryWriter(fs); int b; while((b=strm->ReadByte()) != -1) { br->Write(Convert::ToByte(b)); } br->Close(); strm->Close(); }
我一口气使用了五个类。我想这正是 BCL
的意义所在,提供了丰富的类。 WebRequest
是一个抽象类,它允许用户以独立于协议的方式请求 Internet 数据。我们使用 static
方法 Create
来请求我们的文件。WebRequest
类有一个名为 GetResponse
的方法,该方法返回一个 WebResponse
对象。在我们的特定情况下,我们请求的是一个 HTTP 文件,因此我们将 WebResponse
对象转换为 HttpWebResponse
对象。使用这些类的一个大优点是它们都允许我们进行流访问。在我们的情况下,HttpWebResponse
类有一个 GetResponseStream
方法,该方法返回一个 Stream
对象,该对象封装了从 Web 请求的文件。如果您以前使用过流,那么剩下的就很简单了。如果没有,您可以在 CP 上阅读我关于文件和流的文章。我们只需从 HttpWebResponse
对象返回的流中读取数据,然后将数据写入文件。
异步下载
这比同步下载要复杂一些。但是,正如您可能期望的那样,当您下载多个大文件时,这是更有效的方法。我依稀记得微软有人说过,异步方法内部使用了 I/O 完成端口等高性能技术。
我们像上面一样创建 WebRequest
对象,但不是调用 GetResponse
,而是调用 BeginGetResponse
,它开始对 Internet 资源的异步请求。我们将一个响应回调函数指定为参数之一。然后,我们等待一个由回调函数设置的 ManualResetEvent
对象,以便我们的函数能够使用等待调用进行阻塞,直到整个响应被读取并存储。我们还将我们的 WebRequest
对象作为状态对象传递给回调函数。
void DownloadFileAsync(String* url, String* fpath) { WebRequest* wrq = WebRequest::Create(url); finished = new ManualResetEvent(false); m_writeEvent = new AutoResetEvent(true); buffer = new unsigned char __gc[512]; OutFile = new FileStream(fpath, FileMode::Create,FileAccess::Write); wrq->BeginGetResponse( new AsyncCallback(this,WebStuffDemo::ResponseCallback), wrq); finished->WaitOne(); OutFile->Close(); }
响应回调
void ResponseCallback(IAsyncResult* ar) { WebRequest* wrq = static_cast<WebRequest*>(ar->AsyncState); WebResponse* wrp = wrq->EndGetResponse(ar); Stream* strm = wrp->GetResponseStream(); strm->BeginRead(buffer,0,512, new AsyncCallback(this,WebStuffDemo::ReadCallBack),strm); }
EndGetResponse
方法使用 BeginGetResponse
方法启动的异步请求,并返回一个 WebResponse
对象,我们可以从中调用 GetResponseStream
来获取底层流对象。现在,我们开始对流进行下一个异步操作。我们使用 BeginRead
启动异步读取操作。如果您想知道为什么这样做,这里有一段来自 MSDN 的摘录。“在异步回调方法中使用同步调用可能会导致严重的性能损失。使用 WebRequest 及其派生类发出的 Internet 请求必须使用 Stream.BeginRead 来读取 WebResponse.GetResponseStream 方法返回的流”。
读取回调
void ReadCallBack(IAsyncResult* ar) { Stream* strm = static_cast<Stream*>(ar->AsyncState); int count = strm->EndRead(ar); if(count > 0) { __wchar_t Temp __gc[] = new __wchar_t __gc[512]; Decoder* d = Encoding::UTF8->GetDecoder(); d->GetChars(buffer,0,buffer->Length,Temp,0); String* s = new String(Temp,0,count); Console::WriteLine(s->Length); unsigned char wbuff __gc[] = new unsigned char __gc[512]; buffer->CopyTo(wbuff,0); OutFile->BeginWrite(wbuff,0,count, new AsyncCallback(this,WebStuffDemo::WriteCallBack),OutFile); strm->BeginRead(buffer,0,512, new AsyncCallback(this,WebStuffDemo::ReadCallBack),strm); } else { strm->Close(); finished->Set(); } }
我们调用流上的 EndRead
,并获取从流中读取的字节数。EndRead
是一个阻塞调用,并且必须对我们已经发起的每个 BeginRead
调用调用一次。如果读取的字节数大于零,则表示还有更多数据。否则,我们知道所有数据都已到达,然后我们关闭流,并设置我们的主函数正在等待的事件。正如我们必须使用异步方法来读取数据一样,我们必须使用 异步方法来将数据写入我们的文件,否则,异步回调函数中会出现阻塞调用,这是非常低效的。
因此,我们所做的是调用输出流对象上的 BeginRead
方法。我们将我们的写回调函数作为回调,并将输出流对象作为回调函数的 state 对象。完成此操作后,我们调用输入流对象上的 BeginRead
来启动另一个异步读取,因为还有更多数据需要检索。
写入回调
void WriteCallBack(IAsyncResult* ar) { m_writeEvent->WaitOne(); FileStream* out = static_cast<FileStream*>(ar->AsyncState); out->EndWrite(ar); m_writeEvent->Set(); }
我们调用输出流上的 EndWrite
,该方法结束由 BeginWrite
启动的异步写入操作。EndWrite
会阻塞直到所有数据都已写入。因此,我们不必担心确保所有数据都已写入。正如您所见,我使用了一个 AutoResetEvent
对象来确保两个写入操作不会并行进行,并且还确保写入操作按正确的顺序调用。如果调用了多个写入回调,它们都将在 WaitOne
调用处挂起,当它们被执行时,它们将按照调用 WaitOne
的顺序执行。