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));

  表示向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); }

  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() == )
  5. {
  6. m_pState->NotifyHandlers(STATECHANGE_REMOTE_IDLE);
  7. ProcessNextCommand();
  8. }
  9. }

  ProcessNextCommand,内部用while取出列表第一个元素,提交一个command请求,也就是connect command,实现代码为:

  1. 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. }

  取出队列首元素并判断类型,如果是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) == )
  11. m_pQueuePane->Highlight();
  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. }

  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. }

  在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. }

  例如,当收到事件类型为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. };

  底层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

FileZilla客户端源码解析的更多相关文章

  1. vs2008编译FileZilla客户端源码

    vs2008编译FileZilla客户端源码 下载FileZilla客户端源码,下载地址https://download.filezilla-project.org/. FileZilla客户端解决方 ...

  2. Netty5客户端源码解析

    Netty5客户端源码解析 今天来分析下netty5的客户端源码,示例代码如下: import io.netty.bootstrap.Bootstrap; import io.netty.channe ...

  3. Feign 客户端源码解析

    Feign的使用非常简单,增加如下配置之后,便可以使用Feign进行调用.非常简单是不是.主要的工作由Feign框架完成.业务代码只提供了一个Interface, 然后由Feign动态生成代理类来实现 ...

  4. Spring Cloud系列(四):Eureka源码解析之客户端

    一.自动装配 1.根据自动装配原理(详见:Spring Boot系列(二):Spring Boot自动装配原理解析),找到spring-cloud-netflix-eureka-client.jar的 ...

  5. 第零章 dubbo源码解析目录

    第一章 第一个dubbo项目 第二章  dubbo内核之spi源码解析 2.1  jdk-spi的实现原理 2.2 dubbo-spi源码解析 第三章 dubbo内核之ioc源码解析 第四章 dubb ...

  6. Fabric1.4源码解析:客户端安装链码

          看了看客户端安装链码的部分,感觉还是比较简单的,所以在这里记录一下.       还是先给出安装链码所使用的命令好了,这里就使用官方的安装链码的一个例子: #-n 指定mycc是由用户定义 ...

  7. Netty源码解析—客户端启动

    Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...

  8. HDFS源码解析:教你用HDFS客户端写数据

    摘要:终于开始了这个很感兴趣但是一直觉得困难重重的源码解析工作,也算是一个好的开端. 本文分享自华为云社区<hdfs源码解析之客户端写数据>,作者: dayu_dls. 在我们客户端写数据 ...

  9. 转载-FileZilla Server源码分析(1)

    FileZilla Server源码分析(1) 分类: VC 2012-03-27 17:32 2363人阅读 评论(0) 收藏 举报 serversocketftp服务器usersockets工作 ...

随机推荐

  1. zoj1537- Playing with a Calculator

    http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=537 题意:给你一个k值,现在要你求一个最小的N 值,N每一个数位上的数值a均相 ...

  2. 软件设计师.NET认证考试测试卷(试题及答案)

    软件设计师.NET认证考试测试卷 注意事项:用蓝.黑色钢笔答题.保持卷面整洁. 得分 阅卷人 一.单项选择(40分,每小题1分) 1.以下标识符中不全是关键字的是(D  ) A.case for in ...

  3. FTP之虚拟用户

    基于虚拟用户访问ftp关闭防火墙,selinux 过程如下1.装包,配置.起服务配置过程如下: 需写入vsftpd.conf配置文件中的内容如下: anonymous_enable=NO ---- 匿 ...

  4. Maven 插件 maven-tomcat7-plugin - 常用命令及配置

    常用命令 tomcat7:deploy 说明:部署 WAR 到 Tomcat tomcat7:help 说明:查看插件帮助信息 tomcat7:run 说明:支行当前项目 配置 <project ...

  5. git shell 常用命令

    git branch 查看本地所有分支 git status 查看当前状态 git commit 提交 git branch -a 查看所有的分支 git branch -r 查看远程所有分支 git ...

  6. Nodejs --我自己的学习笔记

    对于Nodejs,相信客官并不陌生,网上却已众说纷纭,有人说是一个平台,有人说是服务器JavaScript,有人说一个框架… 之前亦有过研究,多怀可远观而不可亵玩也.高效率,I/O操作,异步编程,以及 ...

  7. CSS属性之absolute

    0.脱离标准文档流 绝对定位的元素会脱离标准文档流,拥有z-index属性,并且对于它的任何操作和改变都不会影响它的兄弟元素和父级元素,这里就不过多介绍. 不过值得注意的是,虽然绝对定位元素脱离的标准 ...

  8. Web window.print() 打印

    web打印 window.print() 我只给出比较有效的,方便的打印方法,有些WEB打印是调用ActiveX控件的,这样就需要用户去修改自己IE浏览器的Internet选项里的安全里的Active ...

  9. 怎样通过WireShark抓到的包分析出SIP流程图

    WireShark抓到了SIP包, 逐条分析, 看瞎...希望能够写个脚本, 自动生成流程图

  10. eclipse集成配置JDK和Tomcat

    在eclipse中集成JDK和tomcat服务器方法很简单,我们可以在服务器上运行想要的东西.比如我们学习javaweb时就要用到. 工具/原料   eclipse,JDK,tomcat 方法/步骤 ...