65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (29投票s)

2001 年 10 月 18 日

4分钟阅读

viewsIcon

181468

downloadIcon

1266

解释 WebRequest、WebResponse 和相关类的用法。

引言

在本文中,我们将了解如何从 Web 下载文件。使用 WebRequestWebResponse 类可以毫不费力地完成此操作。这些类提供了允许我们以流的形式访问 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 的顺序执行。

© . All rights reserved.