客户端在线更新-QT

  • Post author:
  • Post category:其他




客户端在线更新-QT



实现思路

​ 最近在做客户端的时候,需要将客户端在线更新,所以整理一下在线更新的思路,希望对你有帮助。

​ 首先在线更新是利用了文件解压之后会自动替换的原理,因此具体需要做的就是从服务器上下载到在线更新的压缩包,之后在对下载到的压缩包进行解压缩替换现有程序内的文件即可。

​ 需要注意的是更新软件需要独立运行,且注意不能占用需要解压的部分,也就是说在更新时候需要将软件本体退出,如下图所示

在这里插入图片描述

​ 同样的,您可以将上图部分做成双向更新,就可以使用软件本体更新更新客户端,使用更新客户端在更新软件本体,从而达到不在下载全部软件的目的,但是在客户端和服务器端做好区分。



实现方式



1.解压部分

​ 解压我选择了使用Quazip,Quazip是Qt平台下面的一个压缩解压缩库,Quazip编译需要依赖zlib库。



Quazip编译和简单的使用

​ 从官网下载Quazip源码压缩包,官网地址:http://quazip.sourceforge.net/在官网可以看到它支持的平台以及下载入口。

​ 首先进入Quazip官网已经将对应的源码指向了github:https://github.com/stachenov/quazip 你也可以选择到此处下载源码

Quazip的编译并不复杂,我这里用windows为例子对Quazip进行编译讲解。

  1. 首先将下载的源码解压之后在源码目录下建立一个bulid文件夹

在这里插入图片描述

  1. 打开CMake工具选择需要编译的文件夹和输出文件夹,选好之后点击configure,之后选择对应的平台和vs版本

在这里插入图片描述

  1. 之后点击open,打开vs编译出对应的debug和release版本就可以啦

    在这里插入图片描述

  2. 编译完成之后测试对应的代码

#include <iostream>
#include <JlCompress.h>
// 压缩解压可以按照代码分开测试
int main()
{
    // JlCompress::compressFile("test.zip","test.txt"); //压缩
    JlCompress::extractFile("test.zip","111.txt");  //解压
    return 0;
}



2.下载部分

​ 服务器方面使用的是http接口,因此QT这边选择使用QT自带的QNetworkAccessManager来做下载(当然也可以使用其他的C库去实现http接口,比如libevent(后续会单独拿出来做讲解,这边就先只介绍QNetworkAccessManager))。

NetworkAccess API是围绕一个QNetworkAccessManager对象构造的,该对象包含它发送的请求的公共配置和设置。它包含代理和缓存配置,以及与此类问题相关的信号,以及可用于监视网络运行进度的应答信号。

一旦创建了QNetworkAccessManager对象,应用程序就可以使用它通过网络发送请求。QNetworkAccessManager提供了一组标准函数,它们接受一个请求和可选数据,每个函数返回一个QNetworkReply对象。返回的对象用于获取响应相应请求而返回的任何数据。

QNetworkAccessManager有一个异步API。 当上面的replyFinished槽被调用时,它接受的参数是QNetworkReply对象,该对象包含下载的数据和元数据(头等)。


注意:

请求完成后,用户有责任在适当的时候删除QNetworkReply对象。不要直接在finished()连接的槽内删除它。你可以使用deleteLater()函数。


注意:

QNetworkAccessManager将它接收到的请求队列化。并行执行的请求数量取决于协议。目前,对于桌面平台的HTTP协议,一个主机/端口组合并行执行6个请求。



代码展示:

download_manager.h

#ifndef UPDATE_DOWNLOAD_MANAGER_H
#define UPDATE_DOWNLOAD_MANAGER_H

#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>

class DownloadManager : public QObject
{
    Q_OBJECT
public:
    explicit DownloadManager(QObject *parent = 0);
    ~DownloadManager() override;
    // 下载文件
    void downloadFile(const QString &url,const QString &fileName);
    // 设置是否支持断点续传
    void setDownInto(const bool &isSupportBreakPoint);
    // 停止下载
    void stopDownload();
    // 关闭下载
    void closeDownload();
    // 获取下载url
    QString getDownloadUrl();
    // 重置参数;
    void reset();

private:
    // 停止下载工作;
    void stopWork();
    // 删除文件;
    void removeFile(const QString &fileName);

signals:
    void signalDownloadProcess(qint64, qint64);
    void signalReplyFinished(int);
    void signalDownloadError();

private slots:
    void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
    void onReadyRead();
    void onFinished();
    void onError(QNetworkReply::NetworkError code);

private:
    QNetworkAccessManager *network_manager_{nullptr};
    QNetworkReply         *reply_{nullptr};
    QUrl                   url_{};
    QString                file_name_{};
    bool                   is_support_break_point_{false};

    qint64                 bytes_received_;
    qint64                 bytes_total_;
    qint64                 bytes_current_received_;

    bool                   is_stop_{true};
};

#endif //UPDATE_DOWNLOAD_MANAGER_H

download_manager.cpp

#include <QFile>
#include <QFileInfo>
#include <iostream>
#include "download_manager.h"

#define DOWNLOAD_FILE_SUFFIX "_tmp"

DownloadManager::DownloadManager(QObject *parent) : QObject(parent)
{
    network_manager_ = new QNetworkAccessManager(this);
    for(const auto &str : network_manager_->supportedSchemes())
        std::cout << str.toStdString() << std::endl;
}

DownloadManager::~DownloadManager()
{

}

void DownloadManager::downloadFile(const QString &url, const QString &fileName)
{
// 防止多次点击开始下载按钮,进行多次下载,只有在停止标志变量为true时才进行下载;
    if (is_stop_)
    {
        is_stop_ = false;
        url_ = QUrl(url);
        // 从url 中获取文件名,但不是都有效;
        // 将当前文件名设置为临时文件名,下载完成时修改回来;
        file_name_ = fileName + DOWNLOAD_FILE_SUFFIX;

        // 如果当前下载的字节数为0那么说明未下载过或者重新下载
        // 则需要检测本地是否存在之前下载的临时文件,如果有则删除
        if (bytes_current_received_ <= 0)
        {
            removeFile(file_name_);
        }

        QNetworkRequest request;
        request.setUrl(url_);

        // 如果支持断点续传,则设置请求头信息;
        if (is_support_break_point_)
        {
            QString strRange = QString("bytes=%1-").arg(bytes_current_received_);
            request.setRawHeader("Range", strRange.toLatin1());
        }

        reply_ = network_manager_->get(request);

        connect(reply_, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)));
        connect(reply_, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
        connect(reply_, SIGNAL(finished()), this, SLOT(onFinished()));
        connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
    }
}

void DownloadManager::setDownInto(const bool &isSupportBreakPoint)
{
    is_support_break_point_ = isSupportBreakPoint;
}

void DownloadManager::stopDownload()
{
    // 这里is_stop_变量为了保护多次点击暂停下载按钮,bytes_current_received_ 被不停累加;
    if (!is_stop_)
    {
        //记录当前已经下载字节数;
        bytes_current_received_ += bytes_received_;
        stopWork();
    }
}

void DownloadManager::closeDownload()
{
    stopWork();
    reset();
    removeFile(file_name_);
}

QString DownloadManager::getDownloadUrl()
{
    return url_.toString();
}

void DownloadManager::reset()
{
    bytes_current_received_ = 0;
    bytes_received_ = 0;
    bytes_total_ = 0;
}

void DownloadManager::stopWork()
{
    is_stop_ = true;
    if (reply_ != nullptr)
    {
        disconnect(reply_, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)));
        disconnect(reply_, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
        disconnect(reply_, SIGNAL(finished()), this, SLOT(onFinished()));
        disconnect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
        reply_->abort();
        reply_->deleteLater();
        reply_ = nullptr;
    }
}

void DownloadManager::removeFile(const QString &fileName)
{
    // 删除已下载的临时文件;
    QFileInfo fileInfo(fileName);
    if (fileInfo.exists())
    {
        QFile::remove(fileName);
    }
}

void DownloadManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    if (!is_stop_)
    {
        bytes_received_ = bytesReceived;
        bytes_total_ = bytesTotal;
        // 更新下载进度;(加上 bytes_current_received_ 是为了断点续传时之前下载的字节)
        emit signalDownloadProcess(bytes_received_ + bytes_current_received_, bytes_total_ + bytes_current_received_);
    }
}

void DownloadManager::onReadyRead()
{
    if (!is_stop_)
    {
        QFile file(file_name_);
        if (file.open(QIODevice::WriteOnly | QIODevice::Append))
        {
            file.write(reply_->readAll());
        }
        file.close();
    }
}

void DownloadManager::onFinished()
{
    is_stop_ = true;
    QVariant statusCode = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute);

    if (reply_->error() == QNetworkReply::NoError)
    {
        // 重命名临时文件;
        QFileInfo fileInfo(file_name_);
        if (fileInfo.exists())
        {
            int index = file_name_.lastIndexOf(DOWNLOAD_FILE_SUFFIX);
            QString realName = file_name_.left(index);
            QFile::rename(file_name_, realName);
        }
    }
    else
    {
        // 有错误输出错误;
        QString strError = reply_->errorString();
        std::cout << "__________" + strError.toStdString() << std::endl;
    }
    emit signalReplyFinished(statusCode.toInt());
}

void DownloadManager::onError(QNetworkReply::NetworkError code)
{
    QString strError = reply_->errorString();
    std::cout << "__________" + strError.toStdString() << std::endl;
    closeDownload();
    emit signalDownloadError();
}

main.cpp

#include <QApplication>
#include "download_manager.h"

int main(int argc, char *argv[])
{
    QApplication app(argc,argv);
    DownloadManager dd;
    dd.downloadFile("https://dl.hdslb.com/mobile/fixed/bili_win/bili_win-install.exe?v=1.6.1","aa.exe");
    return app.exec();
}



项目示例

​ 好了,有了上面的基础部分,接下来我们就可以完成我们的整体部分的代码

​ 参考项目地址: https://gitee.com/turbolove/qt-update.git



注意:

项目有个进入主程序的功能,因此加了版本的判断,只有需要更新的时候才会显示更新。

  1. 首先我们需要创建一个软件界面,项目widgets下文件是跟界面有关的内容

  2. core内放了一些必要的线程类

  3. http内放了专门做文件下载等http接口实现的类



关键代码:

版本判断,我这边比较简单,就是一个版本号字符串的解析和比较

bool MainWindow::checkNeedUpdate(const QString &version)
{
    if(version == m_version)
    {
        return false;
    }
    if(m_version.split(".").size() != version.split(".").size() || m_version.split(".").size() < 3)
    {
        return false;
    }
    auto strList1 = m_version.split(".");
    auto strList2 = version.split(".");
    for (int i = 0; i < strList1.size(); ++i)
    {
        int mVer = strList1[i].toInt();
        int newVer = strList2[i].toInt();
        if (mVer == newVer)
        {
            continue;
        }
        return newVer > mVer;
    }
    return false;
}

下载速度转换

// 转换单位;
QString MainWindow::transformUnit(qint64 bytes , bool isSpeed)
{
    QString strUnit = " B";
    if (bytes <= 0)
    {
        bytes = 0;
    }
    else if (bytes < UNIT_KB)
    {
    }
    else if (bytes < UNIT_MB)
    {
        bytes /= UNIT_KB;
        strUnit = " KB";
    }
    else if (bytes < UNIT_GB)
    {
        bytes /= UNIT_MB;
        strUnit = " MB";
    }
    else if (bytes > UNIT_GB)
    {
        bytes /= UNIT_GB;
        strUnit = " GB";
    }

    if (isSpeed)
    {
        strUnit += "/S";
    }
    return QString("%1%2").arg(bytes).arg(strUnit);
}

版本判断

void MainWindow::updateVersionCheck(const QJsonArray &array)
{
    m_list.clear();
    m_versionMap.clear();
    ui.listWidget->clear();
    for(const auto & ver : array)
    {
        QString version = ver.toObject()["name"].toString();
        QString filename = ver.toObject()["file_path"].toString();
        QString describe = ver.toObject()["describe"].toString();
        if(checkNeedUpdate(version))
        {
            m_list << version;
            m_versionMap[version] = QPair<QString,QString>(filename,describe);
        }
    }

    std::reverse(m_list.begin(),m_list.end());
	// 版本号获取顺序逆序插入列表
    for(int i = 0 ; i < m_list.size() ;i++)
    {
        ui.listWidget->addItem(m_list[i]);
    }

    if(m_list.size()>0)
    {
        ui.plainTextEdit->setPlainText(m_versionMap[m_list[0]].second);
        ui.btn_enter->setText("Enter(Update)");
    }
    else
    {
         ui.btn_enter->setText("Enter");
    }
}



版权声明:本文为turbolove原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。