简体   繁体   English

使用Qt和QNetworkRequest恢复失败的HTTP下载

[英]Resuming a Failed HTTP Download With Qt & QNetworkRequest

I'm trying add auto-update capabilities to an application I'm developing. 我正在尝试为我正在开发的应用程序添加自动更新功能。 I've based this functionality off the Qt HTTP Example (and by based I mean I copied this example exactly and then went from there). 我已经将这个功能基于Qt HTTP示例 (基于我的意思是我完全复制了这个例子然后从那里开始)。 It's downloading a ZIP file and then extracting its contents to patch the application. 它正在下载ZIP文件,然后提取其内容以修补应用程序。

Occasionally, when downloading, the connection will fail, and the download stops. 有时,下载时,连接将失败,下载将停止。 To be a little more user friendly, I figured I'd add auto-restart capabilities to the downloader, where it will attempt to restart the download once if it fails. 为了更加用户友好,我想我会向下载器添加自动重启功能,如果失败则会尝试重启下载。

Here are the highlights of my code - the method names match the method names in the example: 以下是我的代码的亮点 - 方法名称与示例中的方法名称匹配:

void Autopatcher::httpReadyRead()
{
    //file is a QFile that is opened when the download starts
    if (file) {
        QByteArray qba = reply->readAll();
        //keep track of how many bytes have been written to the file
        bytesWritten += qba.size();
        file->write(qba);
    }
}

void Autopatcher::startRequest(QUrl url)
{
    //doResume is set in httpFinished() if an error occurred
    if (doResume) {
        QNetworkRequest req(url);
        //bytesWritten is incremented in httpReadyRead()
        QByteArray rangeHeaderValue = "bytes=" + QByteArray::number(bytesWritten) + "-";
        req.setRawHeader("Range",rangeHeaderValue);
        reply = qnam.get(req);
    } else {
        reply = qnam.get(QNetworkRequest(url));
    }
    //slot connections omitted for brevity
}

//connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(fileGetError(QNetworkReply::NetworkError)));
void Autopatcher::fileGetError(QNetworkReply::NetworkError error) {
    httpRequestAborted = true;
}

void Autopatcher::httpFinished() {
    //If an error occurred
    if (reply->error()) {
        //If we haven't retried yet
        if (!retried) {
            //Try to resume the download
            doResume=true;
            //downloadFile() is a method that handles some administrative tasks
            //like opening the file if doResume=false
            //and calling startRequest() with the appropriate URL
            QTimer::singleShot(5000,this,SLOT(downloadFile()));
        }
        //If we have retried already
        else {
            //Give up :(
            if (file) {
                file->close();
                file->remove();
                delete file;
                file = 0;
            }
        }
    //If no error, then we were successful!
    } else {
        if (file) {
            file->close();
            delete file;
            file = 0;
        }
        //Apply the patch
        doPatch();
    }
    reply->deleteLater();
    reply = 0;
}

Now, if the download completes normally with no interruptions, it works just fine. 现在,如果下载正常完成而没有中断,它可以正常工作。 The ZIP extracts perfectly. ZIP完美提取。 However, if the connection fails and the application restarts the download, it does finish downloading, and I can see all the contents of the ZIP file in 7-zip, but I cannot extract them (7-zip said something along the lines of "tried to move the pointer before the start of the file). 但是,如果连接失败并且应用程序重新启动下载,它完成下载,我可以看到7-zip中的ZIP文件的所有内容,但我无法提取它们(7-zip说了一些“试图在文件开始之前移动指针)。

I'm assuming that I've made a simple off-by-one error somewhere, like in the HTTP Range header. 我假设我在某个地方做了一个简单的逐个错误,就像在HTTP Range标题中一样。 I've seen an example of how to pause & resume downloads at this blog , but he writes the contents of the stream to file at pause , whereas I stream them into the file in httpReadyRead . 我已经看到了如何在这个博客上暂停和恢复下载的示例,但是他在暂停时将流的内容写入文件,而我将它们流式传输到httpReadyRead的文件中。 I don't know if that's causing a problem. 我不知道这是否会导致问题。

For testing, I've been using Sysinternals TCPView to sever the TCP connection during download. 为了测试,我一直在使用Sysinternals TCPView在下载过程中切断TCP连接。 I'm not sure how to debug this further, so let me know if more information would be useful! 我不确定如何进一步调试,所以如果有更多信息有用,请告诉我!

So today I investigated deeper. 所以今天我进行了深入研究 I was originally thinking that the file sizes of the uninterrupted and interrupted versions were the same +- a few bytes, but I was wrong. 我原本以为不间断和中断版本的文件大小是相同的+ - 几个字节,但我错了。 I downloaded two versions of the file, and the sizes were off by around 2 megabytes. 我下载了两个版本的文件,大小偏差大约2兆字节。

So, I compared them using VBinDiff (a nice utility if you're not afraid of a console interface) and here's what I found: 所以,我使用VBinDiff比较它们(如果你不害怕控制台界面,这是一个很好的实用工具),这就是我发现的:

  • The files stopped matching at address 0x0154 21F3 . 文件在地址0x0154 21F3处停止匹配。
  • That address in the bad file matched address 0x0178 1FD3 in the good file, and they continued to match until the end. 坏文件中的地址与好文件中的地址0x0178 1FD3匹配,并且它们一直持续匹配直到结束。
  • Therefore, the bad file was missing 2,358,752 bytes - which matches the 2MB approximation I was seeing in Explorer. 因此,坏文件缺少2,358,752字节 - 这与我在资源管理器中看到的2MB近似值相匹配。

This confirmed that when I tried to restart a download, I was skipping a significant portion of the remote file . 这证实了当我尝试重新启动下载时, 我正在跳过远程文件的重要部分 Unsure of what was going on, I decided to check the value of bytesWritten , which I was using to track how many bytes I had written into the file. 不确定发生了什么,我决定检查bytesWritten的值,我用它来跟踪我写入文件的字节数。 This value was what I was writing into the Range Request Header, so its value had to be incorrect. 这个值是我写入Range Request Header的值,所以它的值必须是不正确的。 (See the httpReadyRead() function in the question). (请参阅问题中的httpReadyRead()函数)。

So I added the code below, right before setting the Range Request Header: 所以我在设置Range Request Header之前添加了下面的代码:

file->flush();
bytesWritten = file->size();

Debugging the code, I was surprised to find that 调试代码,我很惊讶地发现

bytesWritten = 28,947,923
file->size() = 26,589,171

Confirming that the bytesWritten value was incorrect. 确认bytesWritten值不正确。 In fact, when I used the file size, instead of the bytesWritten value, the download was able to restart and finish successfully! 事实上,当我使用文件大小而不是bytesWritten值时,下载能够重新启动并成功完成!

I'm not going to go any deeper, as this works for me. 我不会再深入了,因为这对我有用。 In fact, this would allow restarting downloads between instances of the application, so in my opinion this is a superior method. 实际上,这将允许重新启动应用程序实例之间的下载,因此在我看来这是一种更好的方法。

tl;dr Don't keep track of the bytes written to the file. tl; dr不要跟踪写入文件的字节数。 Just check the file size when restarting a failed download. 只需在重新启动失败的下载时检查文件大小。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM