【大話QT之十】實現FTP斷點續傳

應用需求:

        網盤開發工作逐步進入各部分的整合階段,當用戶在客戶端修改或新增加一個文件時,該文件要同步上傳到服務器端對應的用戶目錄下,因此針對數據傳輸(即:上傳、下載)這一塊現在既定了三種傳輸方式,即: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()發送原生命令返回後的槽函數:

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讀取的函數:

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. 現在實現的是單線程,尚未添加多線程斷點下載以及隊列的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章