FileZilla客户端实现分析 FileZilla Client Analysis By David.Zhu 2005/8/2
Content List:
- Source File Classification
- Main Framework of FileZilla Client
- Network Implementation Analysis
- Get Nuggets from Misc
- Explore FileZilla Client GUI
- Knowledge Repository
- Summarize
- Reference
1.1 Source File Classification
Kernel |
CommandQueue.cpp FileZilla.cpp FileZillaApi.cpp FtpListResult.cpp MainThread.cpp SiteManager.cpp |
Definition & Configuration |
SFtpCommandIDs.h FzApiStructures.h structures.h |
Network Substrate |
AsyncSocketEx.cpp AsyncGssSocketLayer.cpp AsyncProxySocketLayer.cpp AsyncSocketExLayer.cpp AsyncSslSocketLayer.cpp ControlSocket.cpp IdentServerControl.cpp IdentServerDataSocket.cpp SftpControlSocket.cpp FtpControlSocket.cpp TransferSocket.cpp |
Misc |
AsyncRequestQueue.cpp FileChangeNotify.cpp MFC64bitFix.cpp PathFunctions.cpp version.cpp ApiLog.cpp ServerPath.cpp SFtpIpc.cpp SpeedLimit.cpp DirectoryCache.cpp Crypt.cpp |
GUI Related |
All others .They are too many and I don't wanna list them here. |
1.2 Main Framework of FileZilla Client
FileZilla客户端的GUI核心就是CMainFrame,CMainFrame在创建的时候(CMainFrame::OnCreate)会创建一个CFileZillaApi对象(m_pFileZillaApi)和一个CCommandQueue对象(m_pCommandQueue),FileZilla客户端为其命令通道专门创建了一线程CMainThread, 该线程采用 Command模式来实现的:GUI部分通过PostThreadMessage发送FZAPI_THREADMSG_COMMAND消息给CMainThread来完成发送FTP命令,CMainThread将所有的FTP命令封装成一个 t_command结构(defined in FileZillaApi.h),通过FZAPI_THREADMSG_COMMAND线程消息来接收GUI发来的FTP命令。 CFileZillaApi则在GUI和 CMainFrame中充当了一个Proxy的作用,而CCommandQueue则充当着命令缓冲和序列化的作用。在CMainThread中保存了主窗口的HWND和消息ID,在必要的时候CMainThread就可以通过窗口的消息机制来通知主窗口线程。
我们知道在FTP的时候都是走的双通道:一个命令通道;一个数据通道。在FileZilla中命令通道对应的就是CFtpControlSocket,数据通道对应的就是CTransferSocket.在CMainThread初始化的时候(CMainThread::InitInstance),就创建了CFtpControlSocket,在GUI发命令要求连接FTP服务器的时候,其调用 Connect去连接远程FTP服务器。数据传输通道的建立则是在通过命令通道发送了LIST命令,FTP服务器返回之后,才创建了CTransferSocket对象,并将其作为一个Server,调用Listen等待FTP服务器的连接。数据通道主要是接收LIST命令返回的服务器文件树和DOWNLOAD命令传来的下载文件的数据(见CTransferSocket::OnReceive)。
FileZilla客户端提供了 KeepAlive机制,在CMainThread中设置了一个Timer,如果配置了OPTION_KEEPALIVE选项,系统就会在空闲时向服务器随机的发送 PWD,REST 0 ,TYPE A ,TYPE I 命令,以避免FTP服务器因为客户端连上来后长时间没有动作而自动关闭与客户端的连接。
1.3 Network Implementation Analysis
FileZilla的网络部分的结构图如下:
其中CAsyncSocketEx是对MFC的CAsyncSocket的改进,据说比CAysncSocket性能上要高一点。在CAsyncSocketEx中添加了对CAsyncSocketLayer的支持,各个SocketLayer 链接成一个双向的链表(见CAsyncSocketExLayer::m_pNextLayer和CAsyncSocketExLayer::m_pPrevLayer),在CAsyncSocketEx中记录了这个链表的头尾指针。
对FileZilla来说网络就是其核心,而FillaZilla的网络部分主要就是CAsyncSocketEx和CAsyncSocketExLayer了,下面我们来分析一下CAsyncSocketEx和CAsyncSocketExLayer的工作机制。
首先讲讲CAsyncSocketEx,其实在MFC里面有一个对基于HWND的异步事件的Winsock管理模型的封装类CAsyncSocket,但作者Tim Kosse这里又重新了一个CAsyncSocketEx类提供了跟MFC的CAsyncSocket相同的接口方法和功能,在eMule开源项目中我发现其网络部分也是用的这个FileZilla中的CAsyncSocketEx。那么CAsyncSocketEx比CAsyncSocket好,好在哪里呢?Tim Kosse在解释为什么CAsyncSocketEx比MFC的CAsyncSocket快时说:" 每个使用CAsyncSocket的线程都会创建一个window,CAsyncSocket就是使用这个窗口的句柄来调用WSAAsyncSelect的,直到这里CAsyncSocketEx和CAsyncSocket都是一样的,但是对一个线程中的所有Socket,CAsyncSocket仅仅使用一个消息WM_SOCKET_NOTIFY,其wParam是套接字的句柄,而window通过一个内部建立好的map,根据这个套接字的句柄来获取CAsyncSocket的Instance.而CAsyncSocketEx就不同了,它使用了从WM_USER到0xBFFF的消息来为每个Socket提供一个通知消息,这样window在接受到一个消息后,将 index of message - WM_USER作为内部数组的Index来获取CAsyncSocketEx的Instance.所以CAsyncSocketEx就要比CAsyncSocket快一些,特别是在管理大量的Socket的时候,性能的提升还是比较明显的。".另外,CAsyncSocketEx还提供了a flexible layer system和套接字的状态机实现。
接下来我们来分析一下CAsyncSocketEx的Layer系统是如何工作的.
从上图我们可以看到,CAsyncSocketEx将Receive,Send,Close,Connect,Create,Listen,Accept,ShutDown,GetPeerName,GetSockName这些调用按照左边的顺序一层一层的往下抛,一直到最后一个Layer才真正调用相应的API函数。同时,Helper窗口函数在收到通过WSAASyncSelect机制发过来的事件时,首先将其发给最下面一层处理,然后按照上图右边的顺序,从下往上通过OnReceive,OnClose,OnAccept,OnConnect,OnSend 一层一层的调用。
同时,CAysncSocketLayer还使用WM_USER消息将其内部错误反馈给CAsyncSocketEx(通过TriggerEvent,PostMessage到HelperWindow);在CAysncSocketLayer和CAsyncSocketEx之间还有一个Callback机制,CAsyncSocketExLayer::DoLayerCallback把消息封装成t_callbackMsg,调用CAsyncSocketEx::AddCallbackNotification添加到CAsyncSocketEx的内部队列m_pendingCallbacks中等待处理。
我感觉对CAsyncSocketLayer的机制还不是很清晰,下面再进一步分析一个Layer的实现吧.在FileZilla中已经实现了三个Layer了:ProxyLayer,sFtp Layer和SSL Layer.下面找个最简单的分析一下啦,就看Proxy Layer吧:) CAsyncProxySocketLayer主要是提供了对SOCKSv4,SOCKSv5和HTTP1.1 代理的支持。通过其源代码的分析,其也主要是在Connect和GetHostByName这两个部分做了一些处理,使得在连接应用服务器之前先连接上代理服务器,并对客户端进行认证,在代理服务器上建立circuits .其他的就没什么了。在 CAsyncSocketLayer的实现中,可以看到其主要还是利用SendNext,ReceiveNext,XXXXNext方法来处理数据,而不是在末尾调用CAsyncSocketLayer基类的同样方法。在发现错误的时候通过调用TriggerEvent和DoLayerCallback来向上层报告相关的网络错误。
1.4 Get Nuggets from Misc
多语言支持
FileZilla客户端的多语言支持采取的是资源DLL的形式,每种Language都被编译成一个独立的DLL,这个DLL中包含了FileZilla的所有Dialog,Menu,String Table,Toolbar,Version,Accelarator资源。本来不想去研究具体实现的,但我开了个Win32 DLL工程把rc文件加进去才发现我生成的文件大小是108K,而FileZilla发布的FzResChs.dll才76K ,而且我里面就是一个dummy DllMain函数,为什么会有这么大的文件大小差距呢?纯资源DLL难道不是这样做的么?我用eXeScope发现FzResChs.dll里面竟然没有.text和.rdata等段,呵呵:),原来它没有代码的(这也正是为什么叫做纯资源DLL吧?),于是在Project Setting的Link Table中加入/NOENTRY /nodefaultlib,从工程中删除stdafx.h stdafx.cpp *.cpp,重新编译。Okey,这下我们的DLL也76K啦^_^.
资源DLL是编译好了,下一步就是如何使用这个DLL啦。其实蛮简单的,在GUI主线程初始化的时候(CWinApp::InitInstance),加载这个资源DLL,然后调用AfxSetResourceHandle(hResourceDll),将本线程的资源句柄设置为这个资源DLL就可以了。对于非MFC程序,就不能用AfxSetResourceHandle,可以用 FindResource,LoadResource,LockResource来获取DLL模块中的资源。但这个感觉比较麻烦哦。不知到Windows有没有提供跟AfxSetResourceHandle功能一样的API可以使用呢?我想应该是没有这样的函数了:(
带消息处理的线程类封装
以前比较喜欢MFC的CWinApp,因为它就是一个GUI线程类,支持线程消息。在FileZilla中也有类似这样的线程,可是它不是从CWinApp派生的,而是自己写的一个。来看一下它是如何实现线程消息的吧!
首先定义一个static的线程函数ThreadProc,并将其传给CreateThread.ThreadProc函数里通过GetMessage/TranslateMessage/DispatchMessage的一个消息Pump来获取线程的消息。在获取到消息后再调用OnThreadMessage做消息的分发处理。发消息直接通过PostThreadMessage API调用来完成啦。到现在,我想我们都必须再次看看GetMessage的MSDN文档了,MSDN中指出以hWnd=NULL为参数调用GetMessage,会获取到线程消息队列中所有窗口和通过PostThreadMessage API调用发送过来的消息。而每个Win32线程都有自己的消息队列的。
Option配置选项和属性页的封装
在FileZilla项目中有众多的Option和属性页,下面我们就来分析一下FileZilla是如何组织这些属性页的.下图是FileZilla的选项页:
FileZilla把所要的属性页放在Option.h中,然后将Option.h加到stdafx.h中使其全局化。其做了一个属性页的GUI框架,所有的属性页从CSAPrefsSubDlg派生,而主框架从 CSAPrefsDialog派生。这个很自然(我写的UpdaterManager也是这样做的哦^_^),FileZilla将所有的配置不再写到系统注册表中(现在都提倡绿色软件啦),而是保存到一个XML格式配置文件FileZilla.xml中,这个文件就放在安装目录下。
现在看看SetOption和GetOption是如何实现的,FileZilla把所有的Option的值分为int型和string型两类,并用t_Option结构记录每个Option在XML配置文件中的节点名和Option的值类型(0为字符串;1为整型),然后用一个static t_Option options[OPTIONS_NUM]的数组来做nOptionID到Option对应的t_Option结构的映射。SetOption和GetOption通过这个nOptionID来指定相应的Option.并且每次都需要调用 Init函数装载配置文件FileZilla.xml来实现Option的Set/Get操作。为了缓解每次文件存取的延迟,COption机制还使用了Cache的思想(见static t_OptionsCache m_OptionsCache[OPTIONS_NUM]),在系统启动是读取配置,以后每次对配置的更改都会写到配置文件中并且Cache起来。这样下次Get的时候就可以直接取了不用在去加载配置文件了。
Base64 Encoding
Base64编码主要是将任意长度的sequences of octets编码成not be humanly readable。根据RFC2045介绍,编码后的数据将比编码前最多大33%左右。Base64 Encoding将3个字节的数据(3*8=24bits)编码成4个Base64 Encoding 字母表中的字母(4*6=24bits),Base64 Alphabet就是字母(大小写一共52个),数字(10个)和+ /一共64个字符组成,所以叫做Base64编码。每个Base64 Alphabet中的字母用一个6bits来表示。Base64编码将编码后的字符按行来组织,每行不得多于76个字符。另外对于编码最后少于3个字节的,采取补零和用"="来标示。具体可以参考RFC2045.
UTF-8
最早只有Ascii和EBCDIC编码,后来为了支持软件的国际化,使用了UTF-8 Encoding。为支持多国的字符编码,ISO/IEC 10646定义了两套编码方案:UCS-4 和 UCS-2。UCS-4使用四个字节来标示一个字符;UCS-2使用两个字节来标示一个字符。Unicode标准的Version 2只和ISO/IEC 10646的UCS-2编码方案兼容。UTF-8是UCS Transformation Format 8的缩写(Abbreviation),它只是一个传输格式,并不是字符集哦。UTF-8的基本思想是用1到6个字节来表示一个UCS字符,根据UCS字符的编码设置这1-6个字节的Most significant bits为相应的1/0串。根据UTF-8编码后的Sequence的第一个字节的最前面的1的数量就可以知道是用多少个字节来表示这个UCS字符的。比如对于UTF-8编码110xxxxx 10xxxxxx ,我们就知道其使用2个字节来表示这个UCS字符。
1.5 Explore FileZilla Client GUI
上图给出了FillZilla客户端GUI界面的框架:最上面是一个Rebar(m_pWndReBar),它由一个Toolbar(m_pWndToolBar)和一个DialogBar(m_pWndDlgBar)组成。最下面的是一个Status Bar (m_wndStatusBar)。中间是一个Splitter窗口(m_wndVertSplitter),这个SplitterWnd是三行一列的,第一行是一个CStatusView,第三行是一个CQueueView;第二行又是一个一行两列的 SplitterWnd,左边是LocalFile View,右边是RemoteFile View;并且这两个File View都是一个两行一列的SplitterWnd. 复杂吧? ^_^
1.7 Knowledge Repository
获取一个Socket的接收缓冲的Pending Data的数据量大小:
ioctlsocket(hSocket,FIONREAD,&dwBytes);
WSAAsyncGetHostByName:
这是gethostbyname的异步版本。在获取完成后会以指定的消息发送到指定的窗口。
WSACancelAsyncRequest:
取消未完成的异步Winsock操作,这些操作主要就是WSAAsyncGetXByY 型的异步调用。
FindFirstChangeNotification:
使用FindFirstChangeNotification/FindNextChangeNotification/FindCloseChangeNotification,可以实现对一个目录及其子目录下的所有文件和文件夹的监控,如果其文件名,文件大小,文件的最后写时间等属性改变了我们可以立刻得到通知。
1.8 Summarize
Okey,总结一下FileZilla客户端,我认为其结构还是组织得比较合理的:GUI和网络部分的接口使用Proxy模式,采用独立的FTP命令处理线程和命令模式来发送FTP命令;另外,采用资源DLL的方式来支持多语言;采用独立的XML配置文件和static函数来支持对系统的众多配置;同时,使用了总多的Cache和Queue技术来支持顺序操作和提高访问效率。技术上来说,其比较核心的就是Tim Kosse的那套支持代理和SSL的CAsyncSocketEx和CAsyncSocketLayer机制了。但我感觉好像整个系统里面就好像只有两个线程:一个GUI主线程和一个CMainFrame。感觉是不是太少了,将数据通道放在单独的线程中处理可能对提高系统的效率有一定的作用。同时,socket的数据缓冲管理,在FileZilla中没有做特殊的处理,没有使用缓冲池和Lookaside List等类似的技术。当然,FileZilla只是一个客户端,我们不能对其要求太高^_^。
1.9 Reference
SOCKS Proxy(RFC 1928)
HTTP1.1 Proxy(RFC2616)
FileZilla Home Page
Base64 Content-Transfer-Encoding(RFC2045)
Internationalization of FTP(RFC2640)