使用QNetworkAccessManager实现Qt的FTP下载服务
正文之前,给大家介绍一个服务器管理工具:
FTP客户端-IIS7服务器管理工具
作为FTP客户端,它支持批量管理ftp站点。定时上传和定时下载,定时备份,且操作简洁。同时iis7服务器管理工具还是vnc客户端。并且支持批量管理管理windows及linux服务器、vps。让服务器真正实现了一站式管理,真的是非常方便。
下载地址:http://fwqglgj.iis7.net/cp/ftp/?zmyc-jz
使用截图如下:
——————————————————————————————————————————————————
正文:
从Qt5开始,官方推荐使用QNetworkAccessManager进行Ftp和http的上传和下载操作;Qt4中使用的QtFtp模块即作为独立模块,需要自己从github上进行下载编译后使用(官方地址:https://github.com/qt/qtftp)。
官方的QtFtp最后一次更新为2014年,根据搜索的资料,其尚存在若干bug。不过有人对此代码在Github上进行维护和更新,如果需要使用的话,可以搜索一下。
QNetworkAccessManager的相关API比较丰富,但是相应也比较低级。如果需要对Ftp进行较为复杂的操作,在缺少资料的基础上就会很麻烦,需要较好的功底。
因为个人对Ftp的操作仅限于下载或者上传,因此使用`QNetworkAccessManager`即可满足要求。此处仅对下载进行示范,上传基本一致。
1 #ifndef FTPGETWINDOW_H 2 #define FTPGETWINDOW_H 3 4 #include <QWidget> 5 #include <QUrl> 6 #include <QDir> 7 #include <QNetworkReply> 8 9 class QFile; 10 class QLabel; 11 class QLineEdit; 12 class QTextEdit; 13 class QPushButton; 14 class QProgressBar; 15 class QGridLayout; 16 17 class QTimer; 18 class QNetworkAccessManager; 19 20 class FtpgetWindow : public QWidget 21 { 22 Q_OBJECT 23 24 public: 25 FtpgetWindow(QWidget *parent = 0); 26 ~FtpgetWindow(); 27 28 private slots: 29 void timeOut(); 30 void updateSelectSaveDir(); 31 void updateTaskRunningState(); 32 void slotReadyRead(); 33 void readReplyError(QNetworkReply::NetworkError error); 34 void downloadFinishReply(QNetworkReply* reply); 35 void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); 36 37 private: 38 bool checkUrl(); 39 bool checkSaveDir(); 40 bool createDownloadFile(); 41 void startDownloadFile(); 42 43 private: 44 qint64 fileDownloadSize; 45 qint64 lastDownloadSize; 46 QUrl url; 47 QDir saveDir; 48 QFile *file; 49 QTimer *timer; 50 QNetworkReply *downloadReply; 51 QNetworkAccessManager *downloadManager; 52 53 QLabel *urlLabel; 54 QLabel *dirLoactionLabel; 55 QLabel *downlaodInfoLabel; 56 QLabel *runningTipLabel; 57 QLineEdit *urlTextEdit; 58 QLineEdit *dirTextEdit; 59 QTextEdit *downloadInfoTextEdit; 60 QPushButton *runningTaskButton; 61 QPushButton *dirLocationButton; 62 QProgressBar *progressBar; 63 QGridLayout *mainLayout; 64 }; 65 66 #endif // FTPGETWINDOW_H
头文件无需赘述。
1 #include "ftpgetwindow.h" 2 3 #include <QLabel> 4 #include <QLineEdit> 5 #include <QTextEdit> 6 #include <QPushButton> 7 #include <QProgressBar> 8 #include <QGridLayout> 9 #include <QFileDialog> 10 11 #include <QUrl> 12 #include <QDir> 13 #include <QFile> 14 #include <QTimer> 15 #include <QFileInfo> 16 #include <QMetaEnum> 17 #include <QNetworkAccessManager> 18 19 FtpgetWindow::FtpgetWindow(QWidget *parent) 20 : QWidget(parent), 21 fileDownloadSize(0), 22 lastDownloadSize(0), 23 file(Q_NULLPTR) 24 { 25 downloadManager = new QNetworkAccessManager(this); 26 connect(downloadManager, SIGNAL(finished(QNetworkReply*)),SLOT(downloadFinishReply(QNetworkReply*))); 27 28 //初始化超时检查定时器,30秒查询一次 29 timer = new QTimer; 30 connect(timer, SIGNAL(timeout()), SLOT(timeOut())); 31 32 urlLabel = new QLabel; 33 urlLabel->setText(tr("Url:")); 34 35 urlTextEdit = new QLineEdit; 36 urlLabel->setBuddy(urlTextEdit); 37 38 runningTaskButton = new QPushButton; 39 runningTaskButton->setText("Run"); 40 connect(runningTaskButton, SIGNAL(clicked(bool)), SLOT(updateTaskRunningState())); 41 42 dirLoactionLabel = new QLabel; 43 dirLoactionLabel->setText(tr("Save Dir:")); 44 45 dirTextEdit = new QLineEdit; 46 dirTextEdit->setReadOnly(true); 47 dirLoactionLabel->setBuddy(dirTextEdit); 48 49 dirLocationButton = new QPushButton; 50 dirLocationButton->setText("Select Save Dir"); 51 connect(dirLocationButton, SIGNAL(clicked(bool)), SLOT(updateSelectSaveDir())); 52 53 runningTipLabel = new QLabel; 54 runningTipLabel->setText(tr("Runing task:")); 55 56 progressBar = new QProgressBar; 57 runningTipLabel->setBuddy(progressBar); 58 59 downlaodInfoLabel = new QLabel; 60 downlaodInfoLabel->setText(tr("Download Info:")); 61 62 downloadInfoTextEdit = new QTextEdit; 63 downloadInfoTextEdit->setReadOnly(true); 64 downlaodInfoLabel->setBuddy(downloadInfoTextEdit); 65 66 mainLayout = new QGridLayout; 67 mainLayout->setColumnStretch(0, 1); 68 mainLayout->setColumnStretch(1, 3); 69 mainLayout->setColumnStretch(2, 1); 70 mainLayout->setMargin(15); 71 mainLayout->setColumnMinimumWidth(2, 15); 72 73 mainLayout->addWidget(urlLabel, 0, 0); 74 mainLayout->addWidget(urlTextEdit, 0, 1); 75 mainLayout->addWidget(runningTaskButton, 0, 2); 76 mainLayout->addWidget(dirLoactionLabel, 1, 0); 77 mainLayout->addWidget(dirTextEdit, 1, 1); 78 mainLayout->addWidget(dirLocationButton, 1, 2); 79 mainLayout->addWidget(runningTipLabel, 2, 0, 1, 1); 80 mainLayout->addWidget(progressBar, 2, 1, 1, 1); 81 mainLayout->addWidget(downlaodInfoLabel, 3, 0, 1, 1); 82 mainLayout->addWidget(downloadInfoTextEdit, 4, 0, 3, 3); 83 setLayout(mainLayout); 84 85 setFixedWidth(800); 86 setWindowTitle(tr("FpGet Window")); 87 } 88 89 FtpgetWindow::~FtpgetWindow() 90 { 91 if(file != Q_NULLPTR) 92 { 93 file->deleteLater(); 94 file = Q_NULLPTR; 95 } 96 //downloadManager的父对象是窗体,会自动进行析构 97 } 98 99 /** 100 * @brief 进行下载超时判断,错误则发送超时信号 101 */ 102 void FtpgetWindow::timeOut() 103 { 104 if(lastDownloadSize != fileDownloadSize) 105 lastDownloadSize = fileDownloadSize; 106 else 107 emit downloadReply->error(QNetworkReply::TimeoutError); //下载超时,发送超时错误信号 108 } 109 110 /** 111 * @brief 检查Url地址合法性 112 * @return 113 */ 114 bool FtpgetWindow::checkUrl() 115 { 116 url = QUrl(urlTextEdit->text()); 117 if(!url.isValid()) 118 { 119 downloadInfoTextEdit->append("Error: Invalid URL"); 120 return false; 121 } 122 123 if(url.scheme() != "ftp") 124 { 125 downloadInfoTextEdit->append("Error: URL must start with 'ftp:'"); 126 return false; 127 } 128 129 if (url.path().isEmpty()) { 130 downloadInfoTextEdit->append("Error: URL has no path"); 131 return false; 132 } 133 return true; 134 } 135 136 /** 137 * @brief 检查文件下载地址 138 * @return 139 */ 140 bool FtpgetWindow::checkSaveDir() 141 { 142 QString dir = dirTextEdit->text(); 143 if(dir.isEmpty()) 144 dir = QDir::currentPath() + "/Download/"; 145 saveDir = QDir(dir); 146 147 if(!saveDir.exists()) 148 { 149 auto ok = saveDir.mkdir(dir); 150 if(!ok) return false; 151 } 152 return true; 153 } 154 155 bool FtpgetWindow::createDownloadFile() 156 { 157 auto localFileName = QFileInfo(url.path()).fileName(); 158 if (localFileName.isEmpty()) 159 localFileName = "ftpget.out"; 160 161 file = new QFile; 162 file->setFileName(saveDir.absoluteFilePath(localFileName)); 163 if(!file->open(QIODevice::WriteOnly)) 164 { 165 auto info = "Error: Cannot write file " + file->fileName() 166 + ": " + file->errorString(); 167 downloadInfoTextEdit->append(info); 168 return false; 169 } 170 return true; 171 } 172 173 /** 174 * @brief 开始下载文件操作 175 */ 176 void FtpgetWindow::startDownloadFile() 177 { 178 if(!createDownloadFile()) return; 179 180 if(timer->isActive()) 181 timer->stop(); 182 fileDownloadSize = lastDownloadSize = 0; //重新设置定时器以及相关变量 183 184 downloadInfoTextEdit->append("Download file: " + url.fileName()); 185 186 downloadReply = downloadManager->get(QNetworkRequest(url)); 187 188 //分块获取文件信息,并写入文件中 189 connect(downloadReply, SIGNAL(readyRead()), SLOT(slotReadyRead())); 190 191 //获取下载进度信息 192 connect(downloadReply, SIGNAL(downloadProgress(qint64,qint64)), 193 SLOT(downloadProgress(qint64,qint64))); 194 195 //下载过程出错,进行报错处理(超时处理也是丢出超时信号,交由此槽函数进行处理) 196 connect(downloadReply, SIGNAL(error(QNetworkReply::NetworkError)), 197 SLOT(readReplyError(QNetworkReply::NetworkError))); 198 199 timer->start(30 * 1000); //启动超时检查定时器,每30秒查询下载情况 200 } 201 202 void FtpgetWindow::updateSelectSaveDir() 203 { 204 dirTextEdit->setText(""); 205 QString dir = QFileDialog::getExistingDirectory(this, tr("Open Directory"), 206 "C://", 207 QFileDialog::ShowDirsOnly 208 | QFileDialog::DontResolveSymlinks); 209 if(!dir.isEmpty()) 210 dirTextEdit->setText(dir); 211 } 212 213 void FtpgetWindow::updateTaskRunningState() 214 { 215 if(!checkUrl() || !checkSaveDir()) 216 return; 217 218 downloadInfoTextEdit->clear(); //清空信息栏 219 220 runningTaskButton->setEnabled(false); 221 dirLocationButton->setEnabled(false); 222 startDownloadFile(); 223 } 224 225 /** 226 * @brief 文件下载完成的清尾操作 227 * @param reply 228 */ 229 void FtpgetWindow::downloadFinishReply(QNetworkReply *reply) 230 { 231 file->waitForBytesWritten(5 * 1000); //等待文件写入结束 232 if(0 == file->size()) 233 //此处下载失败,不再进行重新下载操作 234 downloadInfoTextEdit->append("Nothing be download."); 235 else 236 downloadInfoTextEdit->append("Download file success."); 237 238 if(timer->isActive()) 239 timer->stop(); //停止超时计时器 240 241 file->deleteLater(); 242 file = Q_NULLPTR; 243 244 reply->deleteLater(); 245 reply = Q_NULLPTR; 246 247 runningTaskButton->setEnabled(true); 248 dirLocationButton->setEnabled(true); 249 } 250 251 void FtpgetWindow::slotReadyRead() 252 { 253 file->write(downloadReply->readAll()); 254 fileDownloadSize = file->size(); //更新下载字节数 255 } 256 257 /** 258 * @brief 下载异常,重新进行下载 259 * @param error 260 */ 261 void FtpgetWindow::readReplyError(QNetworkReply::NetworkError error) 262 { 263 auto metaEnum = QMetaEnum::fromType<QNetworkReply::NetworkError>(); 264 //PS:字符串转换为枚举值 265 //Qt::Alignment alignment = (Qt::Alignment)metaEnum.keyToValue("Qt::AlignLeft"); 266 //alignment = (Qt::Alignment)metaEnum.keysToValue("Qt::AlignLeft | Qt::AlignVCenter"); 267 //枚举值转换为字符串 268 auto errStr = metaEnum.valueToKey(error); 269 downloadInfoTextEdit->append("Download file occur error: " + QString(errStr)); 270 271 file->deleteLater(); 272 file = Q_NULLPTR; 273 274 downloadReply->deleteLater(); 275 downloadReply = Q_NULLPTR; 276 277 startDownloadFile(); //重新尝试下载文件 278 } 279 280 void FtpgetWindow::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) 281 { 282 if(0 != bytesTotal) 283 { 284 progressBar->setMaximum(bytesTotal); 285 progressBar->setValue(bytesReceived); 286 } 287 }
(1)超时操作:
在下载过程中,经常出现假死操作,因为不清楚如何进行续传操作,现有做法是取消当前下载任务并重新开始。
在启动下载操所时,启动定时器,每隔30秒记录当前下载数值和上一次记录的下载数值比较,如果相同,则可以认为在30秒内无操作,发送超时信号,断开连接重新开始下载任务。
(2)大文件下载:
现有仅测试了上百M的文件,可以在下载结束的时候,一次读取所有字节并写入文件,但是这样的压力比较大。
因此,当QNetworkReply发送信号告知有分段数据可供读取的时候,即读取并写入文件中。
(3)大文件上传:
调用put函数时,主要有两种方式,将文件信息读取出保存至QByteArray中,或者上传文件的操作指针。使用后者即可实现大型文件的上传操作。
(4)下载进度信息:
下载过程中,QNetworkReply会发送下载进度信息,用户可以根据此刷新QProgressBar控件,或者在命令行刷新进度条。
以下代码为在命令行实现进度条刷新操作,关键在于每次输出进度信息的时候,不要添加换行符,并且在输出信息头部添加"\r"即可。
1 /** 2 * @brief 实现命令行下进度条,提示下载进度 3 * @param bytesReceived 4 * @param bytesTotal 5 */ 6 void FtpGet::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) 7 { 8 int barLength = 50; 9 int percent = int(qreal(bytesReceived) / qreal(bytesTotal) * barLength); 10 QString out = "\rPercent: " + QString(percent, '#') + QString(barLength - percent, ' '); 11 out += " " + QString::number(bytesReceived) + " / " + QString::number(bytesTotal); 12 std::cout << qPrintable(out) << std::flush; 13 }