FileZilla客戶端源碼解析


FileZilla客戶端源碼解析

  FTP是TCP/IP協議組的協議,有指令通路和數據通路兩條通道。一般來說,FTP標准命令TCP端口號是21,Port方式數據傳輸端口是20。

  FileZilla作為populate open source project,自然也有指令通路和數據通路。然而,FileZilla源碼極少有注釋,網上參考資料也寥寥無幾。FileZilla用的到類庫多且復雜(客戶端wxWeidgets、GnuTLS、sqlite3、GNU IDN Library - Libidn,服務端boost、zlib),模式也不易理解(proxy模式、改編CAsynSocket的CAsynSocketEx、layer等)。想要完全搞懂FileZilla的細節似乎是件很困難的事情。好在我只需了解里面核心代碼的工作原理,能封裝為己所用即可。

  FileZilla官方論壇回答提問時指出,engine工程重點是ControlSocket.h和transfersocket.h,顯然,一個對應ftp控制,另一個對應數據傳輸。interface工程重點是Mainfrm.h、state.h、commandqueue.h(出於效率考慮,很復雜)。engine工程其他重點類有 CServer, CServerPath, CDirectoryListing, COptionsBase,客戶端interface工程其他重點類有 COptions。此外,客戶端interface定義了資源文件dialog.xrc和menu.xrc(這兩個用wxWidgets資源編輯器打開。用文本打開也可以,內容是xml格式)。該論壇鏈接地址為https://forum.filezilla-project.org/viewtopic.php?f=3&t=8443。截圖如下

1  command指令相關

1.1  CCommand指令類,頭文件:commands.h,工程:engine,性質:抽象類、虛基類

  CCommand及其基類定義了ftp指令實體,是CCommandQueue操作的基本單元。例如:

1 m_pCommandQueue->ProcessCommand(new CConnectCommand(server));
2     m_pCommandQueue->ProcessCommand(new CListCommand(path, _T(""), LIST_FLAG_FALLBACK_CURRENT));
View Code

   表示向CCommandQueue隊列末尾插入連接指令CConnectCommand和獲取文件列表指令CListCommand。

  CCommand子類由宏DECLARE_COMMAND定義,DECLARE_COMMAND定義的子類是實現了CCommand的純虛函數GetId和Clone,實現如下:

1 // Small macro to simplify command class declaration
2 // Basically all this macro does, is to declare the class and add the required
3 // standard functions to it.
4 #define DECLARE_COMMAND(name, id) \
5     class name : public CCommand \
6     { \
7     public: \
8         virtual enum Command GetId() const { return id; } \
9         virtual CCommand* Clone() const { return new name(*this); }
View Code

  ccommand子類列表如下:

  類圖如下:

1.2  CCommandQueue客戶端命令隊列,頭文件:commandqueue.h,工程:engine,性質:客戶端命令緩存操作

  CCommandQueue用作為CCommand基類的緩存操作,內部維護了list列表 std::list<CCommand *> m_CommandList。基本操作如下:

1 void ProcessCommand(CCommand *pCommand);    //入隊操作
2 void ProcessNextCommand();    //向m_pEngine提交command請求
3 void Finish(COperationNotification *pNotification);    //CMainFrame的OnEngineEvent函數收到服務端nId_operation確認指令后,執行出隊操作,並調用ProcessNextCommand

   以點擊QuickConnect按鈕為例,簡單說一下上面3個函數的調用關系。QuickConnect將向隊列順序插入connectcommand和listcommand。connnect是list的基礎,沒有TCP連接自然不可能取回服務端列表信息,connect先入隊,list后入隊。

  入隊操作ProcessCommand沒什么好說的,源碼如下,如果隊列長度為1,取出首元素進行ProcessNextCommand處理。

1 void CCommandQueue::ProcessCommand(CCommand *pCommand)
2 {
3     m_CommandList.push_back(pCommand);
4     if (m_CommandList.size() == 1)
5     {
6         m_pState->NotifyHandlers(STATECHANGE_REMOTE_IDLE);
7         ProcessNextCommand();
8     }
9 }
View Code

   ProcessNextCommand,內部用while取出列表第一個元素,提交一個command請求,也就是connect command,實現代碼為

int res = m_pEngine->Command(*pCommand);

    m_pEngine內部調用Windows socket函數WSAEventSelect發送connect請求(socket.h/cpp內實現),由於是異步調用,無法立即得到響應,返回FZ_REPLY_WOULDBLOCK(等價於WSAEWOULDBLOCK,定義在commands.h),並退出while循環。當list請求入隊時,ProcessCommand監測到當前隊列有兩個元素,則直接返回。List command將待在connect command后,直至獲取到服務端對connect的確認后,再進行m_pEngine->Command操作。

  客戶端收到服務端確認后,CMainFrame::OnEngineEvent將收到通知NotificationId(notification.h/cpp)。如果通知為nId_operation類型,將調用Finish。在finish內部,將對nId_operation的返回值進行判斷,如果返回FZ_REPLY_OK(服務端確認連接),執行以下代碼:

 1 void CCommandQueue::Finish(COperationNotification *pNotification)
 2 {
 3     ...
 4     CCommand* pCommand = m_CommandList.front();
 5     ...
 6     else if (pCommand->GetId() == cmd_connect && pNotification->nReplyCode == FZ_REPLY_OK)
 7     {
 8         m_pState->SetSuccessfulConnect();
 9         m_CommandList.pop_front();
10     }
11     ...
12     ProcessNextCommand();
13 }
View Code

  取出隊列首元素並判斷類型,如果是connect command且通知回傳FZ_REPLY_OK,則表示connect成功,將首元素移出隊列,ProcessNextCommand取出首元素(此時應當是list command),提交m_pEngine->Command請求,進行下一個command循環。如果客戶端收到服務端對list請求的處理后,將list command出隊,隊列為空。 

 2  通知消息相關

 2.1  CNotification通知類,頭文件:notification.h,工程:engine,性質:抽象類、虛基類

   CNotification及其基類定義了服務端數據解析后的通知消息。這類消息將回傳給CMainFrame,CMainFrame的OnEngineEvent函數接收到消息后,根據消息id參數NotificationId判斷消息類型,並進行處理。NotificationId定義在notification.h,其子類和消息含義定義如下:

  類圖如下:

 2.2  Notification通知傳遞路徑

  FTP使用TCP通信,底層自然是socket。客戶端收到服務端數據后,將收到的字節流逐層轉化成CNotification和其他結構。為方便理解CNotification,避免陷入wsWidgets底部的事件通知(FileZilla底層socket使用wsWidgets的event handler來處理事件,我不懂wsWidgets,花了很長時間才梳理清楚關系),我從CMainFrame對CNotification的處理開始介紹。

2.2.1  CMainFrame的OnEngineEvent函數,頭文件:Mainfrm.h,工程:FileZilla,性質:CNotification對客戶端處理函數

  看一下OnEngineEvent函數的主要結構:

 1 void CMainFrame::OnEngineEvent(wxEvent &event)
 2 {
 3     CNotification *pNotification = pState->m_pEngine->GetNextNotification();
 4     while (pNotification)
 5     {
 6         switch (pNotification->GetID())
 7         {
 8         case nId_logmsg:
 9             m_pStatusView->AddToLog(reinterpret_cast<CLogmsgNotification *>(pNotification));
10             if (COptions::Get()->GetOptionVal(OPTION_MESSAGELOG_POSITION) == 2)
11                 m_pQueuePane->Highlight(3);
12             delete pNotification;
13             break;
14         case nId_operation:
15             pState->m_pCommandQueue->Finish(reinterpret_cast<COperationNotification*>(pNotification));
16             if (m_bQuit)
17             {
18                 Close();
19                 return;
20             }
21             break;
22         case nId_listing:
23         case nId_asyncrequest:
24         case nId_active:
25         case nId_transferstatus:
26         case nId_sftp_encryption:
27         case nId_local_dir_created:
28         default:
29             delete pNotification;
30             break;
31         }    
32         pNotification = pState->m_pEngine->GetNextNotification();
33     }
34 }    
View Code

    switch分支對通知類型作判斷,如果是nId_logmsg通知,則用AddToLog函數(CStatusView類,StatusView.h,FileZilla工程)取出通知的文本數據,並在客戶端界面上輸出。如果是nId_operation通知,則表明CCommandQueue的頭元素command已得到服務端確認,調用CCommandQueue的Finish函數將頭元素出隊,在Finish內部,投遞下一個cmmand請求(見1.2節介紹)。其余消息類型沒有驗證,這里就不寫了。

2.2.2  CFileZillaEnginePrivate類AddNotification函數,頭文件:engineprivate.h,工程:engine,性質:管理CNotification隊列,觸發CMainFrame的OnEngineEvent函數

   CFileZillaEnginePrivate類功能很多也很重要,這里只講與CNotification隊列相關的內容。CFileZillaEnginePrivate類內部維護了CNotification隊列m_NotificationList。底層服務通過調用CFileZillaEnginePrivate類的AddNotification函數向m_NotificationList內插入通知。AddNotification定義如下:

 1 void CFileZillaEnginePrivate::AddNotification(CNotification *pNotification)
 2 {
 3     m_lock.Enter();
 4     m_NotificationList.push_back(pNotification);
 5 
 6     if (m_maySendNotificationEvent && m_pEventHandler)
 7     {
 8         m_maySendNotificationEvent = false;
 9         m_lock.Leave();
10         wxFzEvent evt(wxID_ANY);
11         evt.SetEventObject(this);
12         wxPostEvent(m_pEventHandler, evt);
13     }
14     else
15         m_lock.Leave();
16 }
View Code

  在AddNotification內部,使用bool變量m_maySendNotificationEvent控制向CMainFrame發出wxPostEvent調用,該調用將觸發CMainFrame的OnEngineEvent函數。而在CMainFrame的OnEngineEvent中,將循環從通知隊列取出通知,直到隊列為空時,m_maySendNotificationEvent又變成了true。該控制方式和CCommandQueue類似。

2.2.3  CFtpControlSocket類OnSocketEvent函數,頭文件:ftpcontrolsocket.h,工程:engine,性質:control socket的實體類,CSocketEvent事件分類

  CFtpControlSocket是CRealControlSocket子類,后者又是CControlSocket的子類,CControlSocket和CRealControlSocket定義了一些接口,由CFtpControlSocket實現。CFtpControlSocke繼承CRealControlSocket的OnSocketEvent函數被底層socket調用,用於對底層socket event的分類處理。OnSocketEvent代碼如下:

 1 void CRealControlSocket::OnSocketEvent(CSocketEvent &event)
 2 {
 3     if (!m_pBackend)
 4         return;
 5 
 6     switch (event.GetType())
 7     {
 8     case CSocketEvent::hostaddress:
 9         {
10             const wxString& address = event.GetData();
11             LogMessage(Status, _("Connecting to %s..."), address.c_str());
12         }
13         break;
14     case CSocketEvent::connection_next:
15         if (event.GetError())
16             LogMessage(Status, _("Connection attempt failed with \"%s\", trying next address."), CSocket::GetErrorDescription(event.GetError()).c_str());
17         break;
18     case CSocketEvent::connection:
19         if (event.GetError())
20         {
21             LogMessage(Status, _("Connection attempt failed with \"%s\"."), CSocket::GetErrorDescription(event.GetError()).c_str());
22             OnClose(event.GetError());
23         }
24         else
25         {
26             if (m_pProxyBackend && !m_pProxyBackend->Detached())
27             {
28                 m_pProxyBackend->Detach();
29                 m_pBackend = new CSocketBackend(this, m_pSocket);
30             }
31             OnConnect();
32         }
33         break;
34     case CSocketEvent::read:
35         OnReceive();
36         break;
37     case CSocketEvent::write:
38         OnSend();
39         break;
40     case CSocketEvent::close:
41         OnClose(event.GetError());
42         break;
43     default:
44         LogMessage(Debug_Warning, _T("Unhandled socket event %d"), event.GetType());
45         break;
46     }
47 }
View Code

   例如,當收到事件類型為CSocketEvent::hostaddress時,LogMessage內部會調用AddNotification函數插入事件通知。收到CSocketEvent::read,會調用CFtpControlSocket的OnReceive接收消息,OnReceive內部解析服務端數據后會調用AddNotification。

2.2.4  CSocketEventDispatcher,頭文件:socket.h,工程:engine,性質:socket 事件分發

   CSocketEventDispatcher實現了底層socket事件的分發,CSocketEventDispatcher內部維護了一個CSocketEvent列表m_pending_events。CSocketEventDispatcher基於singleton模式實現,唯一的外部訪問接口是CSocketEventDispatcher::Get()函數,這意味着所有的socket event都由一個m_pending_events管理。CSocketEventDispatcher類定義如下:

 1 class CSocketEventDispatcher : protected wxEvtHandler
 2 {
 3 public:
 4     void SendEvent(CSocketEvent* evt);    //加入一個CSocketEvent
 5     void RemovePending(const CSocketEventHandler* pHandler); //移除一個與CSocketEventHandler關聯的SocketEvent
 6     void RemovePending(const CSocketEventSource* pSource);  //移除一個與CSocketEventHandler關聯的SocketEvent
 7     void UpdatePending(const CSocketEventHandler* pOldHandler, const CSocketEventSource* pOldSource, CSocketEventHandler* pNewHandler, CSocketEventSource* pNewSource);  //修改SocketEvent內容
 8     static CSocketEventDispatcher& Get();   //singleton訪問接口
 9 private:   
10     CSocketEventDispatcher();
11     ~CSocketEventDispatcher();
12     virtual bool ProcessEvent(wxEvent& event);  //m_pending_events不為空的話,取出首元素,處理,調用OnSocketEvent
13     std::list<CSocketEvent*> m_pending_events;    //socketEvent列表
14     wxCriticalSection m_sync;    //互斥訪問鎖
15     static CSocketEventDispatcher m_dispatcher;//全局唯一實例
16     bool m_inside_loop;
17 };
View Code

  底層socket會調用CSocketEventDispatcher::Get().SendEvent(evt)函數向dispatcher加入socket event,由於低層socket(socket.h/cpp)內部使用了wxWidgets線程和事件管理,這里就不介紹了。有興趣的可以在工程內搜索一下CSocketEventDispatcher::Get().SendEvent關鍵字,自行了解。

3  FileZilla客戶端FTP控制指令流程

  綜合所述,FileZilla客戶端與服務端控制指令通信的流程大致如下:

  1:CCommandQueue的ProcessCommand函數提交command請求到command隊列,如果command隊列長度為1,則調用ProcessNextCommand處理首條command。

  2:ProcessNextCommand利用CFileZillaEngine的Command函數對請求進行分類處理,並提交到底層socket。

  3:底層socket利用異步通信WSAEventSelect向服務端發出請求。

  4:未收到服務端確認前,CCommandQueue首元素不出隊,其余command請求暫停投遞。

  5:底層socket收到服務端數據,底層socket調用CSocketEventDispatcher::Get().SendEvent將socket event加入socketevent隊列

  6:SendEvent內部調用AddPendingEvent,觸發ProcessEvent對socket event隊列進行處理

  7:ProcessEvent判斷隊列是否為空,非空調用OnSocketEvent

  8:OnSocketEvent(由CSocketEventHandler的子類實現,如CFtpSocketControl)對socket event類型進行判斷,logmsg解析、send、recv等操作,然后調用CFileZillaEnginePrivate類的AddNotification函數向m_NotificationList通知隊列內插入操作結果Notification。

  9:AddNotification內部構造wxID_ANY消息,並post該消息到CMainFrame。

  10:CMainFrame的OnEngineEvent函數對nofication的ID進行判斷,例如,logmsg通知,則打印信息。nId_operation,則將CCommandQueue首元素出隊,取出下一個元素,投遞請求。

  附上幾個關鍵類的類關系圖:

4  FTP數據上傳數據

   回顧1.1節,有command子類CFileTransferCommand,可以猜想,數據上傳必然和此類有關。在QueueView.cpp的SendNextCommand內下斷點,進入

1 nt res = engineData.pEngine->Command(CFileTransferCommand(fileItem->GetLocalPath().GetPath() + fileItem->GetLocalFile(), fileItem->GetRemotePath(),    fileItem->GetRemoteFile(), fileItem->Download(), fileItem->m_transferSettings));

  給Engine提交filetransfile請求。進入FileZillaEngine后,調用FileTransfer函數,將請求提交給ControlSocket處理。

 

 

Engine工程預編譯設置:

1:threadex.cpp和socket.cpp文件設置Not Using Precompiled Headers

2:FileZillaEngine.cpp設置為

3:其余cpp設置為:

 

 

 

 

1


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM