客户端在线更新-QT
实现思路
最近在做客户端的时候,需要将客户端在线更新,所以整理一下在线更新的思路,希望对你有帮助。
首先在线更新是利用了文件解压之后会自动替换的原理,因此具体需要做的就是从服务器上下载到在线更新的压缩包,之后在对下载到的压缩包进行解压缩替换现有程序内的文件即可。
需要注意的是更新软件需要独立运行,且注意不能占用需要解压的部分,也就是说在更新时候需要将软件本体退出,如下图所示
同样的,您可以将上图部分做成双向更新,就可以使用软件本体更新更新客户端,使用更新客户端在更新软件本体,从而达到不在下载全部软件的目的,但是在客户端和服务器端做好区分。
实现方式
1.解压部分
解压我选择了使用Quazip,Quazip是Qt平台下面的一个压缩解压缩库,Quazip编译需要依赖zlib库。
Quazip编译和简单的使用
从官网下载Quazip源码压缩包,官网地址:http://quazip.sourceforge.net/在官网可以看到它支持的平台以及下载入口。
首先进入Quazip官网已经将对应的源码指向了github:https://github.com/stachenov/quazip 你也可以选择到此处下载源码
Quazip的编译并不复杂,我这里用windows为例子对Quazip进行编译讲解。
- 首先将下载的源码解压之后在源码目录下建立一个bulid文件夹
- 打开CMake工具选择需要编译的文件夹和输出文件夹,选好之后点击configure,之后选择对应的平台和vs版本
-
之后点击open,打开vs编译出对应的debug和release版本就可以啦
-
编译完成之后测试对应的代码
#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
注意:
项目有个进入主程序的功能,因此加了版本的判断,只有需要更新的时候才会显示更新。
-
首先我们需要创建一个软件界面,项目widgets下文件是跟界面有关的内容
-
core内放了一些必要的线程类
-
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");
}
}