QT 斷點續傳
應用需求:
網盤開發工作逐步進入各部分的整合階段,當用戶在客戶端修改或新增加一個檔案時,該檔案要同步上傳到伺服器端對應的使用者目錄下,因此針對資料傳輸(即:上傳、下載)這一塊現在既定了三種傳輸方式,即:Ftp傳輸、HTTP傳輸以及基於UDT的傳輸。且這三種資料傳輸方式是可配的,可以通過不同的介面呼叫。相比這三種方式,基於UDT的大量檔案傳輸是比較值得研究與創新的地方,它在底層是基於UDP,在上層實現了可靠性的控制;同時它充分考慮到了基於在公網環境下基於Tcp進行傳輸時擁塞控制演算法的缺點,實現了自己的擁塞控制演算法,在實際測試中其效能也是明顯高於基於Tcp的傳輸。關於UDT實現檔案傳輸只進行了技術調研,還沒有真正實現,這一部分內容將在後續文章中提及。這三天的時間只實現了基於FTP的支援斷點續傳的檔案上傳、下載。
實現原理:
離我們最近的斷點續傳的應用例子是:迅雷。當使用迅雷下載一個大檔案時,它實現了下面的功能:1> 電腦突然斷電或程式突然退出後,當我們重新啟動迅雷時它還會從程式退出時已經下載的檔案點繼續向後下載,而不是檔案又從頭開始下載。2> 可以設定採用多個執行緒同時下載,每個執行緒只下載檔案中的某一部分,例如:使用三個執行緒下載一個9000個位元組的檔案,則第一個執行緒下載第1—3000個位元組,第二個執行緒下載第3001—6000個位元組,第三個執行緒下載第6001—9000個位元組。這三個執行緒是同時下載一個檔案,只是下載不同的部分,它會把下載的檔案片段暫存在某個位置,當三個執行緒全部下載完成時再拼成一個完整的檔案。這裡不用多說,其優點顯而易見。
其實,斷點續傳實現的原理很簡單,就是無論是上傳還是下載時都可以實時記錄下已經上傳了或下載了多少位元組,如果中間因為某種原因傳輸斷開,下載啟動時只需要再重新從已經下載的位置繼續下載或上傳就可以了。
利用Qftp實現斷點續傳:
QT中有一個實現Ftp的類:Qftp,它提供了基本的ftp的使用方式,連線ftp伺服器:connectToHost;登入:login;上傳:put;下載:get,使用這些方法可以實現與ftp伺服器互動實現檔案上傳、下載。但是使用它原生提供的put與get方法,無法實現斷點續傳。因此,為了實現斷點續傳我們需要重新實現檔案傳輸,並在其中新增斷點續傳的控制。其實Ftp檔案傳輸的本質也是使用Tcp來實現底層的檔案傳輸。大體思路就是:利用Qftp的connectToHost登入ftp伺服器,使用login登入ftp伺服器,使用rawCommand傳送ftp原生態的命令,使用QTcpSocket實現檔案資料的傳輸。
首先,使用QTcpSocket實現檔案資料的傳輸,需要設定ftp伺服器為“PASV”被動接收方式,即ftp伺服器被動地接收來自客戶端的連線請求。
Ftp伺服器所有可以傳送的原生命令有:http://www.nsftools.com/tips/RawFTP.htm。
實現斷點上傳的命令傳送流程:
1、rawCommand("TYPE I");設定傳輸資料的型別:二進位制資料或ASCII
2、rawCommand("PASV");設定伺服器為被動接收方式。傳送PASV命令後,伺服器會返回自己開啟的資料傳輸的埠,等待客戶端連線進行資料傳輸。返回的資料格式為:“227 Entering Passive Mode (192, 168, 2, 18, 118, 32)”,然後從返回的資訊裡面或去相關的資訊,ftp伺服器的IP地址:192.168.2.18;ftp伺服器開啟的用於資料傳輸的埠:118*256 + 32 = 30240;獲得該資訊後就需要建立TcpSocket的通訊鏈路,連線ftp伺服器。
3、rawCommand("APPE remote-file-path");設定伺服器端remote-file-path為追加的方式。如果此時改檔案不存在,則伺服器端會建立一個。
4、完成上述流程後,就可以開啟本地檔案進行讀取,並通過tcpsocket鏈路傳送出去(write)。
實現斷點下載的命令傳送流程:
1、rawCommand("TYPE I");設定傳輸資料的型別:二進位制資料或ASCII
2、rawCommand("PASV");設定伺服器為被動接收方式。傳送PASV命令後,伺服器會返回自己開啟的資料傳輸的埠,等待客戶端連線進行資料傳輸。返回的資料格式為:“227 Entering Passive Mode (192, 168, 2, 18, 118, 32)”,然後從返回的資訊裡面或去相關的資訊,ftp伺服器的IP地址:192.168.2.18;ftp伺服器開啟的用於資料傳輸的埠:118*256 + 32 = 30240;獲得該資訊後就需要建立TcpSocket的通訊鏈路,連線ftp伺服器。
3、rawCommand("REST size");該命令設定ftp伺服器從本地檔案的哪個地方開始進行資料傳輸。
4、rawCommand(“RETR remote-file-path”);開始從遠端主機傳輸檔案。
檔案上傳時在設定APPE返回之後,就可以開啟本地檔案進行上傳;檔案下載時,收到PASV的返回資訊建立tcpsocket的連線後,需要建立readyRead()的訊號槽,在該槽函式中實現資料的讀取。
關鍵程式碼:
1. 處理rawCommand()傳送原生命令返回後的槽函式:
[cpp] view plaincopy
- void LHTFileTransfer::ProcRawCommandReply(int nReplyCode, QString strDetail)
- {
- //! TYPE
- if (200 == nReplyCode)
- {
- m_ftpHandle->rawCommand("PASV");
- if (currentItem.task_type.compare("Upload") == 0)
- {
- op = QString("Put");
- }
- else if (currentItem.task_type.compare("DownLoad") == 0)
- {
- op = QString("Get");
- }
- }
- //! PASV
- else if(227 == nReplyCode)
- {
- const QString backResult = strDetail;
- if (NULL != m_sendDataSocket)
- {
- m_sendDataSocket->close();
- delete m_sendDataSocket;
- }
- m_sendDataSocket = new QTcpSocket();
- connect(m_sendDataSocket, SIGNAL(readyRead()), this, SLOT(ProcReadyRead()), Qt::UniqueConnection);
- connect(m_sendDataSocket, SIGNAL(readChannelFinished()), this, SLOT(ProcReadChannelFinished()), Qt::UniqueConnection);
- connect(m_sendDataSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(ProcBytesWritten(qint64)), Qt::UniqueConnection);
- QStringList lstr = backResult.split("(").last().split(")").first().split(",");
- int nAddress = lstr.at(0).toInt()<<24 |
- lstr.at(1).toInt()<<16 |
- lstr.at(2).toInt()<<8 |
- lstr.at(3).toInt();
- QHostAddress hostAddress(nAddress);
- int nPort = lstr.at(lstr.length() - 2).toInt() * 256 + lstr.last().toInt();
- m_sendDataSocket->connectToHost(hostAddress, nPort);
- //! APPE , 需要接遠端檔案的絕對路徑
- QString appeShell;
- if (op.compare("Put") == 0)
- {
- appeShell = QString("APPE %1").arg(currentItem.file_remote_path);
- }
- else if (op.compare("Get") == 0)
- {
- //! 這裡的REST後面的大小應該為本地儲存的問價的大小
- appeShell = QString("REST 0");
- }
- m_ftpHandle->rawCommand(appeShell);
- }
- //! 傳送資料
- else if (150 == nReplyCode)
- {
- if (op.compare("Put") == 0)
- {
- m_fileHandle = new QFile(currentItemFilePath);
- if (!m_fileHandle->open(QIODevice::ReadOnly))
- {
- qDebug() << "file open error ...";
- return ;
- }
- const qint64 fileSize = m_fileHandle->size();
- m_fileHandle->seek(currentItem.uploaded_size);
- while(!m_fileHandle->atEnd())
- {
- const qint64 nBlockSize = 16 * 1024 ;
- char buf[16 * 1024];
- qint64 nowPos = m_fileHandle->pos();
- qint64 readLen = m_fileHandle->read(buf, nBlockSize);
- if (readLen !=0 && readLen != -1)
- {
- m_sendDataSocket->write(buf, readLen);
- m_sendDataSocket->flush();
- emit DataTransferProgress(nowPos, fileSize);
- }
- }
- m_sendDataSocket->flush();
- m_sendDataSocket->close();
- m_sendDataSocket = NULL ;
- emit DataTransferProgress(m_fileHandle->pos(), m_fileHandle->size());
- m_procTask.remove(currentItemFilePath);
- m_fileHandle->close();
- //emit StartNextTask();
- }
- else if(op.compare("Get") == 0)
- {
- m_fileHandle = new QFile(currentItem.file_remote_path);
- if (!m_fileHandle->open(QIODevice::WriteOnly))
- {
- qDebug() << "file open error ...";
- return ;
- }
- }
- }
- else if(350 == nReplyCode)
- {
- QString shell = QString("RETR %1").arg(currentItemFilePath);
- m_ftpHandle->rawCommand(shell);
- }
- }
2. 斷點下載時實現buffer讀取的函式:
[cpp] view plaincopy
- void LHTFileTransfer::ProcReadyRead()
- {
- qDebug() << "[DownLoad] ProcReadyReady ....";
- QByteArray buffer = m_sendDataSocket->readAll();
- m_fileHandle->write(buffer);
- m_fileHandle->flush();
- emit DataTransferProgress(m_fileHandle->size(), 0);
- }
面臨的問題以及後續需要優化的地方:
1. 字元編碼問題,即當需要上傳的檔名是中文名稱時,需要對其進行轉碼。
2. 現在實現的是單執行緒,尚未新增多執行緒斷點下載以及佇列的實現。