背景
需要在QT5中進行FTP文件下載,并需要支持整目錄下載,經過對比選擇,最后決定使用Qt4中的QFtp來完成我們的需求。因此決定學習源碼,看清結構,做到能真正解決所要面對的問題。
分解源碼
Qftp一共只有四個文件,主要文件是qftp.cpp,這個文件中,有太多的類,首先按類分解到各自文件中,這樣利用官方的示例代碼,跑起來后,可以方便的查看代碼。
類說明
- class QFtpCommand : 此類是對FTP命令的封裝,將命令與QIODevice設備關聯起來,并返回一個唯一的標識ID。
- class QFtpPI : 此類是對FTP協議的封裝,processReply是主要函數,應答服務端響應。
- class QFtpDTP : 此類是數據操作封裝,數據讀取、解析、存儲都是在此類中處理。
- class QFtpPrivate : 此類是QFtp的實際操作類,被組合到QFtp類中,是邏輯處理中心。
- class QFtp : 此類是外殼,用戶直接面對。
- class QUrlInfo : 此為信息類,存儲接收到的每一條文件數據信息。
運行流程
所有的客戶端命令被壓入到命令堆棧。一個命令運行有兩個入口:一是命令被壓入堆棧時,若堆棧中只有一條命令,即被運行;二是作響應服務端響應時,類型為idle或not waiting。
每個命運被構造時,都會返回唯一ID,這是很重要的一點,因為命令大多關聯著一本地IO設備,在清理IO時,要注意與命令對應,因為所有的操作都是異步的。
改造list響應
QFtp列當前目錄的原有邏輯是取一條數據就發(fā)送一條文件或目錄的消息,這樣在我們連續(xù)遍歷目錄時,無法分清楚是哪個目錄下的數據,無法進行正確的遞歸。這樣改造效率應該會好一些且完全控制整個目錄的顯示,比如,目錄顯示在上面。當然也可以將遞歸放到commandFinished消息響應中去。
QFtpDTP::socketReadyRead() - 修改讀取目錄列表時的信號發(fā)送方式
if (pi->currentCommand().startsWith(QLatin1String("LIST"))) {
QVector<QUrlInfo> infos; //增加vector來存儲整個目錄信息
while (socket->canReadLine()) {
QUrlInfo i;
QByteArray line = socket->readLine();
if (parseDir(line, QLatin1String(""), &i)) {
infos.push_back(i);
//emit listInfo(i); //原來在循環(huán)內,讀一條數據發(fā)送一個listInfo信號
}
else {
if (line.endsWith("No such file or directory\r\n"))
err = QString::fromLatin1(line);
}
}
emit listInfos(infos); //改為在循環(huán)外發(fā)送新增的listInfos信號
}
FtpWindow::addToList(const QVector<QUrlInfo>& urlInfos) - listInfos響應修改
for (int i = 0; i < urlInfos.size(); i++)
{
QTreeWidgetItem* item = new QTreeWidgetItem;
QUrlInfo urlInfo = urlInfos[i];
if (urlInfo.name().compare(".") != 0) {
item->setText(0, urlInfo.name().toLatin1());
item->setText(1, QString::number(urlInfo.size()));
item->setText(2, QString::number(urlInfo.isDir()));
item->setText(3, urlInfo.owner());
item->setText(4, urlInfo.group());
item->setText(5, urlInfo.lastModified().toString("MMM dd yyyy"));
QPixmap pixmap(urlInfo.isDir() ? ":/images/dir.png" : ":/images/file.png");
item->setIcon(0, pixmap);
isDirectory[urlInfo.name()] = urlInfo.isDir();
fileList->addTopLevelItem(item);
}
}
目錄下載
將FtpWindow::downloadFile() slot分解成兩個函數,增加downAllFile(QString rootDir)來完成目錄遞歸。
void FtpWindow::downloadFile()
{
files.clear(); //初始化本地設備
downDirs.clear(); //清空需要下載的目錄堆棧
downAllFile(currentPath); //下載具體操作,另一個入口在list的響應中
showProgressDialog(); //進度條顯示
}
下載的真實操作函數
void FtpWindow::downAllFile(QString rootDir) {
QString thisRoot(rootDir + "/"); //要下載的父目錄
QList<QTreeWidgetItem*> selectedItemList = fileList->selectedItems();
for (int i = 0; i < selectedItemList.size(); i++)
{
QString fileName = selectedItemList[i]->text(0);
if (isDirectory.value(fileName)) { //若是子目錄,組合完成的目錄,壓入待下載目錄堆棧
if(fileName != "..")
downDirs.push(thisRoot + fileName);
}
else {
downloadTotalBytes += selectedItemList[i]->text(1).toLongLong(); //統計需要下載的字節(jié)量
...
QFile* file = new QFile(dirTmp.append("/").append(fileName));
//文件下載請求,是異步操作
int id = ftp->get(QString::fromLatin1((selectedItemList[i]->text(0)).toStdString().c_str()), file);
files.insert(id, file); //本地IO設備與其命令綁定并存儲
}
}
if (downDirs.size() > 0) { //待下載目錄堆棧不空,處理一條
enterSubDir = true; //表示正在下載目錄
QString nextDir(downDirs.pop()); //取需要處理的下一個目錄
ftp->cd(nextDir); //切換到這個目錄
currentDownPath = nextDir;
ftp->list(); //列目錄,在其響應中將再遞歸調用本函數~~~~
}
}
list響應的遞歸處理部分
if (!enterSubDir) { //下載的文件中沒有目錄
...
}
else { //正處理于目錄下載中
fileList->selectAll(); //選中列表中所有
downAllFile(currentDownPath); //遞歸調用下載處理函數
}
項目地址
https://github.com/zhoutk/qtDemo
命令行編譯
git clone https://github.com/zhoutk/qtDemo
cd qtDemo/ftpClient & mkdir build & cd build
cmake ..
cmake --build .
編譯時注意:cmake默認為x86架構,需要與你安裝的Qt版本對應;編譯好了,運行前,請注意目錄結構是否正確。
小結
我選擇的這種目錄下載方式比較麻煩,沒有放到后臺再開一個進程去處理,試圖做整體考慮,且整個運行過程都是異步的,調試也比較難,其中進度條控件控制還有些坑,需要小心處理。過程艱難,收獲頗多。