使用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 }
posted @ 2017-11-14 02:58  从此寂静无声  阅读(13325)  评论(0编辑  收藏  举报