使用QNetworkAccessManager實現Qt的FTP下載操作,此外包含以下功能:(1)添加下載超時操作;(2)大文件分割下載。 附加C++實現命令行輸出進度條實現代碼。 ...
從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 }