FileZilla Server源码分析--大漠落日5节

之所以有本系列的分析,是因为两点:
  1. FileZilla 是目前非常火爆的开源ftp项目,整个项目采用C++代码编写,代码紧凑可读性高,值得学习(缺陷是注释太少)。
  2. 网络上已有的对该源码的分析基于的版本是0.9.18,分析比较粗略,无论是框架还是细节。
这里仅仅是我个人对FileZilla Server源码 0.9.34 版本的分析,能力有限,不足和错误之处还希望大家不吝斧正。
本片作为开篇,略过如何编译(该源码源码用VS2010编译),如何配置,如何使用。 FileZilla官网 提供了程序和源码下载(源码包含在程序中,安装时默认为不安装),以及编译步骤和注意事项,感兴趣的朋友可以自行去官网寻找或google。

感谢: 分析时参考了网友的系列文章《 FileZilla FTP服务器源代码分析 》,大家可以参照比对。

首先预览一下源码目录source文件夹下的大致文件布局。



6个子目录,核心的代码(线程、socket、命令等)都放在当前目录下。6个子目录及对应代码功能:

子目录功能
includes当前版本下只有一个子目录openssl,看名识意,不多解释
install安装脚本和资源
interface界面UI实现类
misc混杂类,比较重要的如md5,StdString等
res程序编译资源,目前只有一个icon
tinyxml著名的一款基于DOM模型小巧开源的xml解析器

当前source目录下源码按实现功能大致又分为以下几种类型:

功能分类包括的文件
网络全体文件名含socket的,Server.*,
线程文件名包含Thread的文件
辅助version.*,MFC64bitFix.*,conversion.*,config.h,service.cpp等除去网络和线程的文件

文件目录结构分析完了,面对众多.h.cpp文件,需要做一些去繁取精的操作。从无关紧要的地方开始,例如version.*。

version.*中声明定义了一个函数 CStdString GetVersionString() ,需要注意的就是CStdString这个类,它的实现在misc/stdString.h文件中,这个类文件较大,功能稍后部分再分析。说句实话,这个函数是很值得收藏的。

Thread.*定义了线程类CThread,只需要注意那个Run函数中对线程消息做了处理,有用的消息交由虚函数 OnThreadMessage 处理。

作为Visual Studio生成的C++代码中最常出现的两个文件stdafx.h和stdafx.cpp,我们势必需要首先弄清楚它们到底包含了哪些头文件,定义了哪些宏,什么了哪些函数以及结构体。

stdafx.h中包含了自己的config.h这个文件,顺便看一下这个文件的作用,代码很少目的有两个,强制使用unicode编译和检测是否安装了最新SDK。还包含了MFC64bitFix.h这个文件,也跟进去看看。定义了一个存储文件属性的结构体 CFileStatus64 ,以及操作它的若干全局函数,这个文件名有点怪,和包含的功能不匹配。
第55行遇到了条件宏 #ifdef MMGR ,编译条件中有定义,包含misc/mmgr.h文件。mmgr是用于管理和跟踪内存的代码,之后会重点详细分析。
conversion.h中声明的函数用于ANSI和UTF8字符的互相转换,不多解释。
AsyncSocketEx.h中实现了异步socket,之后的ControlSocket,AdminListenSocket等文件中什么的socket都是由CAsyncSocketEx类派生来的,之后分析。
至此,stdafx.h中头文件包含全部结束,下面就是宏定义了。

先补充一个知识点,各消息的值范围和作用见下图:


注册了 WM_FILEZILLA_THREADMSG 消息用来线程之 间通信,定义了 WM_FILEZILLA_SERVERMSG用于 进 程间通信,即FileZilla server.exe和FileZilla Server Interface.exe。
这里仅贴出两处源码中调用这两个消息的例子,便可得知后面定义的几个常数宏的用处。
// ControlSocket.cpp第400行
SendStatus(_T("could not send reply, disconnected."), 0);
m_pOwner->PostThreadMessage(WM_FILEZILLA_THREADMSG, FTM_DELSOCKET, m_userid);

//Server.cpp第813行器
int index = GetNextThreadNotificationID();
CServerThread *pThread = new CServerThread(WM_FILEZILLA_SERVERMSG + index);
m_ThreadNotificationIDs[index] = pThread;

从上面代码可以看出 PostThreadMessage的第二个参数wParam就是定义的数字宏,第三个参数是结构t_statusmsg,这些宏功能分别是:

FSM_STATUSMESSAGE:在管理窗口或log中显示并记录状态信息
FSM_CONNECTIONDATA:和连接相关的信息,如新用户连接,登录,退出等
FSM_THREADCANQUIT:退出线程
FSM_SEND:发送数据时用于管理窗口统计发送字节数
FSM_RECV:接受数据时用于管理窗口统计接收字节数
其余的就不多写了,宏名比较直观的显示出意思。

在往下定义了一系列的结构如 t_statusmsg, 之后用到的地方在详述,知道这些结构在哪个文件中定义的就行了。
接着就是extern HWND hMainWnd; 这个外联的句柄就是下一节将要提到的CServer的窗口类句柄。
最后定义了一个CCriticalSectionWrapper类和两个帮助检测临界区死锁的函数,尤其是前者,DEBUG版本时错误的使用将导致当前线程挂起。

SpeedLimit.*:  速度限制(包括时间段限制)

这里针对UI性比较强,FillBuffer这个函数将所有限制条件格式化成一个char字符串,ParseBuffer则是解析这个字符串,采用这个 类,可以轻松实现强大的自定义限速功能。

defs.h: 这个类定义了服务器的状态,如在线、离线、锁住 等。  


Options.*,OptionTypes.h 

OptionTypes.h中定义了一个结构数组 m_Optinons ,保存所有配置项信息,如是否使用SSL,同时在线最大用户数量,上传下载限速等等,所有这些大部分都被使用在Option那个对话框UI上。
t_option结构中有一个BOOL bOnlyLocal成员用于标示该项是否可以仅能够被本地连接修改,数组中只有最后两项Server name 和 server display name为TRUE,Options类就是操作配置文件的实体类(注意,它使用了tinyXML),服务器的配置文件存储在exe同级目录下,叫FileZilla Srver.xml。Options的主要操作是针对内存中的配置,只有与默认值不同的项才会存入配置文件中。
Options还有一个隐藏的friend窗体类  COptionsHelperWindow ,定义在cpp文件中,这个类用于通过用post WM_USER给窗体消息这种异步的方式去更新option实例,而不是options类自身。
有了Options类和OptionTypes.h中定义的配置类型,就可以通过诸如 m_pOptions->GetOptionVal(OPTION_ENABLELOGGING)这样的方法方便的获取到配置。 

FileLogger.*  日志 

这个类中包含Options类的一个对象指针,用来读取日志文件的相关配置。 

iputils.* 判断IP合法性以及是否处于某个过滤范围 

它采用了大名鼎鼎的boost库的regex来判断,这个库之后有时间一定要好好研究一下。 

autobanmanager.*  阻止用户继续登录的方法类文件 

AutoBan这个设置项是一个非常浪费资源的,因为它对每一个失败的ip都要记录查询内存中的两个map。 

Accounts.* 账户

Accounts.h中声明了3个类,t_directory,t_group,还有继承于t_group的t_user。
t_directory仅仅含有一些权限声明,相当于一个struct,被t_group和t_user使用。
剩余两个类主要做的事是对配置的读取分析,所有的数据都是基于字符串的。

permission.* 对用户、群组访问资源进行鉴权

权限配置信息记录在FileZilla Server.xml中。
服务器对每一个group和user都有权限限制,group权限优先于user权限,在 CheckFilePermissions  函数中可以看出。

conversion.* utf8和ansi字符的相互转化 


ExternalIpCheck.*  PASV模式

根据配置获取ip。

所有辅助文件已经分析完毕,下级节开始分析socket和线程类。


//

/

//


上一节讲述的基本都是些做辅助的代码,本节分析诸多socket类的父类CAsyncSocketEx和相关的Layer类。

PS:拼音打字错别字很多- -。

从CAsyncSocketEx和CAsyncSocketExLayer类文件开头注释部分写到:

如何使用?
-----------
和MFC的CAsyncSocket非常像,如果不需要强化CAsyncSocket,那么在需要使用的时候只需替换掉CAsyncSocket即可。

为什么这个类快一些?
-------------------
CAsyncSocketEx只是在分发通知事件消息的时候稍微快一点。
首先来了解一下CAsyncSocket是如何工作的。对每个线程使用CAsyncSocket就会相应有一个窗口被创建。CAsyncSocket利用那个窗口的句柄调用WSAsyncSocket 。直到这儿,CAsyncSocket和它的工作方式是一样的。但是CAsyncSocket对一个线程中的所有sockets仅使用一个windows消息(WM_SOCKET_NOTIFY)。当这个窗口收到 WM_SOCKET_NOTIFY 时,wParam参数包含socket句柄并且这个窗口使用map来查找一个CAsyncSocket实例。CAsyncSocketEx原理不同于此。它的辅助窗口(helper window)使用一定范围内不同的window消息(WM_USER到OXBFFF)并且对每个socket传递不同的消息给WSAAsyncSelect,当这个指定范围内消息被接收的时候,CAsyncSocketEx使用这个消息的索引减去WM_USER的值配合指向
CAsyncSocketEx实例数组的指针来查找。如你所见,CAsyncSocketEx以更加高效的方式来使用辅助窗口,因为它不需要使用缓慢的maps来查找自己的实例。然后,速度增加的并不多,但是当同时使用大量的sockets的时候它的效果可能很明显。

请注意这个变动并没有影响到原始数据吞吐效率,CAsyncSocketEx仅仅是分发通知消息的时候更加快速而已。

CAsyncSocketEx还提供了什么?
---------------------------
CAsyncSocketEx提供了一个灵活的层系统。一个例子就是代理层。创建一个代理层实例,配置并将其加入到CAsyncSocketEx实例的层链(layer chain),之后,你就能够通过代理进行连接。
好处:你不需要做很多变动就可以使用层系统。
另一个层就是当前正在开发(注:目前已经完成,作者忘记修正这个注释了)的SSL层用来进行SSL加密连接。

从作者的注释中我们可以大致了解这些类的功能和原理,源码我也仅挑若干个人比较感兴趣的部分稍微分析一下。

CAsyncSocketEx类和cAsyncSocketExLayer类互为友元类。
CAsyncSocketEx类中有大量的#ifndef NOLAYERS ... #endif ,FileZillaServer工程中没有定义NOLAYERS,所以这些代码都要被编译。

#ifndef NOLAYERS
     // Layer chain
    CAsyncSocketExLayer *m_pFirstLayer;
    CAsyncSocketExLayer *m_pLastLayer;

    friend CAsyncSocketExLayer;

     // Called by the layers to notify application of some events
     virtual  int OnLayerCallback(std::list<t_callbackMsg>& callbacks);
#endif  // NOLAYERS
上面那一段代码是为了构造了作者注释中所说的层链,每个CAsyncSocketEx实例可以通过调用 AddLayer  函数添加一个层。
成员变量m_pendingCallbacks保存的是所有等待被调用的回调信息。当 WindowProc  参数message等于WM_USER+2的时候,调用 OnLayerCallback,  CAsyncSocketEx该虚成员函数仅做了清理工作,实际任务需要派生类去派生处理。

相对于FD_READ作者又定义了#define FD_FORCEREAD (1<<15) 来跳过检测是否有数据等待。现在用伪代码描述一下 WindowProc 函数的工作:
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if (message>=WM_SOCKETEX_NOTIFY)
   {
       // 根据message-(WM_USER+3)的值查找socket ,WM_USER+3 == WM_SOCKETEX_NOTIFY
       if (!pSocket->m_pFirstLayer)
           // 分发通知消息,例如FD_READ,FD_CONNECT等
        else
           // 分发通知消息给最底层,即 pSocket->m_pLastLayer->CallEvent(nEvent, nErrorCode);
    }
     else  if (message == WM_USER)
         // 处理某一层发送的通知事件
     else  if (message == WM_USER+1)
         // 通知连接的状态,即调用虚函数OnConnect
     else  if (message == WM_USER + 2)
         // 处理等待的回调信息
     else  if (message == WM_TIMER)
         // 这种情况因为收到FD_CLOSE事件时仍然有数据未读取,导致调用 pSocket->ResendCloseNotify()重发关闭消息,这个函数启动了定时器。
        
// 重发FD_CLOSE消息通知socket关闭
}
CAsyncSocketEx类中虚函数,如 OnAccept OnSend On 打头的函数用于通知特定事件的发生状态,派生类如果需要获得这些状态信息,就可以自己派生这些函数。

全局的 m_spAsyncSocketExThreadDataList 则定义了一个 t_AsyncSocketExThreadData (即分发线程)的链表,也就是说FileZilla可以有多个分发线程,每个分发线程对应多个socket,即CAsyncSocketEx。 

举一个实际的场景:
在FileZillaServer启动时,缺省监听了两个端口:21和admin端口,因此就有两个socket,即两个CAsyncSocketEx。这两个CAsyncSocketEx共用一个分发线程: t_AsyncSocketExThreadData
当有用户通过FTP连接上server并通过get/mget命令下载文件时,这时FTP服务器会启动一个传输线程在一个临时端口进行监听,这时会增加一个CAsyncSocketEx,同时也增加一个负责这个CAsyncSocketEx的分发线程,因此 m_spAsyncSocketExThreadDataList 里也会增加一个结点。
这时的状况是:一个m_spAsyncSocketExThreadDataList链,两个t_AsyncSocketExThreadData,三个 CAsyncSocketEx。 

下一节分析核心代码。



这是分析的第三节,上一节主要讲了一些和socket基础操作相关的代码,本节将分析核心代码。

Service.cpp   系统服务程序

FileZillaServer可以选择是否注册成windows的服务程序,而这个服务程序的代码就是由service.cpp文件实现的。
WinMain是它的入口函数,在 WinMain里依次完成了下面几项任务:
  1. 参数解析
  2. 初始化某些数据比如端口
  3. 由SCM(服务控制管理器)启动服务,入口为ServiceMain函数;如果服务不存在,进入步骤4
  4. 根据参数设置服务,例如安装、启动、卸载等。
ServiceMain注册 ServiceCtrlHandler来处理服务的控制代码,在回调函数 ServiceCtrlHandler中自定义了128号控制码,用于向窗口"FileZilla Server Helper Window"发送重新读取配置的自定义消息 WM_FILEZILLA_RELOADCONFIG
serviceMain注册Handler成功之后,就启动自己的工作线程 ServiceExecutionThread,线程里创建了CServer对象,然后实际流程交由CServer。之后进入线程的消息循环并等待killServiceEvent信号以退出线程终止服务。
Service.cpp中 KillService函数中有一个变量hMainWnd,它是在stdafx.h中声明的,它具体是哪个窗口的句柄,干什么用,现在还是一无所知。


Server.*  真正的带头大哥

打开Server.h文件,开头就可以看到许多类的前置声明,类中声明了众多上一节提到的相关类对象(或集合如list),CServer类把所有核心类(线程和socket)集中起来使用。
上面已经提到Service的工作线程中调用了CServer的Create函数,我们就先从这里入手吧。
Create一开始就创建了一个窗口,标题为"FileZilla Server Helper Window",呵呵,是不是很熟悉?然后将这个窗口的句柄赋给全局变量hMainWnd,至此,终于大致了解了服务的框架骨骼。
之后又是一大堆初始化操作,包括两个定时器,需要特别注意的是创建服务线程CServerThread时候的提供的参数 WM_FILEZILLA_SERVERMSG + index,这个参数用以线程间通信,再上一节已经有过描述,稍后再具体分析。
在往下调用了 CreateListenSocket函数,这个函数根据Options类中获取的port、bindip、enablessl等参数创建监听ftp客户端连接的CListenSocket对象指针,并保存到m_ListenSocketList中。这里有一个很重要的函数 ShowStatus,它的任务是将信息发送给admin窗口和记录到log中。
最后调用 CreateAdminListenSocket函数创建监听admin客户端的socket,并存入m_AdminListenSocketList中。

CServer类的分析暂时中断一下,我们来分析上面涉及到的几个相关类:CServerThread,CListenSocket,CControlSocket,CTransferSocket。

CServerThread继承自CThread,构造函数有个int型参数,用来标识具体哪个线程的消息。注意,CThread本身并不是一个直接继承于任何线程类的类,它只是负责创建并管理线程的类。m_sInstanceList是static的成员变量,被所有的CServerThread对象共享,而且这个list存储的第一个值用于管理SL,为了标识它,作者又添加了一个BOOL成员变量m_bIsMaster,同样还有一个static临界区变量m_GlobalThreadsync用来同步它。如果当前对象是master,那么它还拥有一个用于实现PASV模式的CExternalIpCheck的类对象m_pExternalIpCheck,缺省值是不采用PASV的。

对CServerThread的重要的几个Public成员函数分析一下功能:
  • GetExternalIP : 调用m_pExternalIpCheck获取PASV的ip
  • AddSocket:给自己发送一个线程消息,该消息在OnThreadMessage函数中被处理,用来添加(SSL)socket连接

对CServerThread重要的几个非public成员函数分析一下功能:
  • AddNewSocket:将sokcet handle绑定到新new的CControlSocket对象socket上,并为当前socket分配一个唯一的用户ID。分配函数CalcUserID不算高效,尤其是连接用户数量比较大的时候再分配尤其明显。之后调用SendNotification准备发送包含连接用户的信息的消息给CServer,最后向连接的用户发送欢迎信息。
  • SendNotification:这个函数将需要发送的数据加入待发送list中,最牛的是它可以自动调节发送的效率。不过我发现一处小BUG,可能作者自己也没有注意到,这两处设置线程优先级貌似反了:
else  if (m_pendingNotifications.size() > 150 && m_throttled < 2)
{
    SetPriority(THREAD_PRIORITY_LOWEST);
    m_throttled = 2;
}
else  if (m_pendingNotifications.size() > 100 && !m_throttled)
{
    SetPriority(THREAD_PRIORITY_BELOW_NORMAL);
    m_throttled = 1;
}

  • OnThreadMessage:线程消息处理函数,如添加删除用户,解析命令,传输,控制,计时器等。

CListenSocket类功能很简单,如果一个连接被accept,那么从服务线程CServerThread列表中找到负载最小的线程,然后调用的 AddSocket函数,将这个连接交给这个CServerTread管理。

CControlSocket类负责与客户端交互。
它有一个int型成员变量m_ antiHammeringWaitTime,用来防止用户攻击(即无限次尝试登录),例如某用户在60秒内连续尝试登录10次失败,那么就把这个用户加入ban列表中,比如3000秒内拒绝再次登录等。 AntiHammerIncrease 函数中对这个变量的算法没看明白
if (m_status.hammerValue > 2000)
    m_antiHammeringWaitTime += 1000 * ( int)pow(1.3, (m_status.hammerValue / 400) - 5);
在用户登录的时候就去检测是否是“攻击”,代码如下:
    BOOL bResult = GetPeerName((SOCKADDR*)&sockAddr, &nSockAddrLen);
         if (bResult)
            m_pOwner->AntiHammerIncrease(sockAddr.sin_addr.s_addr);

         if (m_pOwner->m_pAutoBanManager->RegisterAttempt(htonl(sockAddr.sin_addr.s_addr)))
        {
            Send(_T("421 Temporarily banned for too many failed login attempts"));
            ForceClose(-1);
             return FALSE;
        }

PassCommand函数处理所有的命令,如USER、LIST、PASV、STOR等。当收到STOR命令时,如果是PASV模式,那么调用 m_transferstatus.socket->PasvTransfer(),否则新建一个CTransferSocket套接字赋给 m_transferstatus.socket,然后调用 SendTransferinfoNotification发送 TRANSFERMODE_RECEIVE消息。不管哪种方式,最后还是通过调用CTransferSocket的 InitTransfer函数实现文件传输。


好了,现在让我们恢复现场。
CServer类的消息处理函数 WindowProc,处理了各种消息,其中重要的是 WM_DESTROYWM_FILEZILLA_SERVERMSG。前者通知并等待所有线程退出,关闭socket,销毁资源,杀死定时器,做的都是清理工作。后者根据服务线程发送来的消息进入函数 OnServerMessage中,这个函数处理了所有服务管理的消息。可以看到,很多消息最后都是通过 m_pAdminInterface->SendCommand(2, 3, buffer, len)这句发送出去。CAdminInterface类管理CAdminSocket类的指针列表, SendCommand其实是调用CAdminSocket的 SendCommand将消息发送出去。函数中对admin socket做了自动管理,如果操作失败,就自动移除该socket。
CheckForTimeout每10秒由CServer的定时器调用一次,检测admin socket是否超时,如果超时,自动移除。CAdminSocket收到数据并解析成功之后,最终交由CServer的 ProcessCommand处理,该函数再一次根据Options里的设置对线程、socket进行一次校验和调整。 

我个人对ProcessCommand和SendCommand函数参数中的type或nID为int型有微议,因为这两个参数实际只占用了不到8个字节,写为int不利于理解,如果改成 int8一眼就能看出来这个参数具体占用几个字节。
BOOL CAdminSocket::SendCommand( int nType,  int nID,  const  void *pData,  int nDataLength)
{
     /* */
    t_data data;
    data.pData =  new unsigned  char[nDataLength + 5];
    *data.pData = nType;      //nType目前版本只要不为0就是合法的协议类型,代码中用到了1和2
    *data.pData |= nID << 2;  // nType和nID合用一个8字节
    data.dwOffset = 0;
    memcpy(data.pData + 1, &nDataLength, 4);
     /* */
}

下面重点分析一下ProcessCommand这个函数,用伪代码比较直观。
BOOL CServer::ProcessCommand(CAdminSocket *pAdminSocket,  int nID, unsigned  char *pData,  int nDataLength)
{
     switch(nID)
    { 
     case 2:
        if (!nDataLength)
            // 获取服务器状态
        else
            // 设置服务器状态并获取
        else
            // send error :wrong protocol type
        break;
      case 3:
         if (!nDataLength)
            // send error
         else  if (*pData == USERCONTROL_GETLIST)
            // 计算并格式化所有已连接用户的信息到unsigned char *buffer中并发送给admin
           
// 这些数据显示在admin UI下方的user list中
         else  if (*pData == USERCONTROL_KICK || *pData == USERCONTROL_BAN)
             // *pData共5个字节,第一个为具体协议类型,后四个为userID。
            
// 根据协议对userID进行操作,kick或者Ban掉。
         else
              // send error : wrong protocol type 
          break;
     case 5:
          if (!nDataLength)
             // 读取基本配置然后发送给admin
          else  if (*m_pOptions)
             // 解析配置字符串,创建初始化或调整CServerThread
            
// CreateListenSocket
            
// 创建admin监听sockets
           break;
      case 6:
           if (!nDataLength)
               // 读取user和group的权限配置
           else
               // 解析权限配置发送给admin
           break;
      case 8:
          pAdminSocket->SendCommand(1, 8, NULL, 0);
           break;
       default:
           // send error: unknow command
    }
     return  true;
}

这一节涵盖了众多核心代码,上面的分析相对来说还是比较粗略,所以,后面几节在对这些粗略和遗漏部分在做更为详细深入的挖掘,本节到这里就结束了。
因为都是看代码时临时写入笔记的,所有的分析都很杂乱,希望以后我有时间可以画一些图,重新做一次整理。
2010-7-22补充
图随便画了几张, 链接在此

PS: 本来上周就可以贴出来了,可是因为安装MAC系统造成C盘WINDOWS系统数据破坏无法启动,重装系统导致笔记丢失,这里只能补上,拖后了一周左右。

Feedback

# re: FileZilla Server源码分析(3) [未登录]  回复  更多评论   

2012-03-19 16:34 by  ww
SendNotification:这个函数将需要发送的数据加入待发送list中,最牛的是它可以自动调节发送的效率。不过我发现一处小BUG,可能作者自己也没有注意到,这两处设置线程优先级貌似反了 

这个他在注释中已经写了 
// Check if main thread can't handle number of notifications fast enough, throttle thread if neccessary 


是要让主系程 降低优先级的 并没有作者所说的反了

///

///

///



 本节的分析是基于本系列第二篇FileZilla Server源码分析(2之上,严格意义上来说是更为详细的分析,深入了解CAsyncSocketEx的实现,我将挑出重要的函数一一分析。 

    函数名都为红色粗体,并且带一对小括号,如果括号不含有字符“...”表示该函数无参数,否则有参数,具体什么参数不具体指明。变量均为黑色粗体。   


    首先来看一下该类的构造函数 CAsyncSocketEx() ,构造函数完成的是部分成员变量的初始化工作,其中最重要的是一个结构体变量 m_SocketData ,它的原型为:
   
    // Strucure to hold the socket data
     struct t_AsyncSocketExData
    {
        SOCKET hSocket;  // Socket handle
         int nSocketIndex;  // Index of socket, required by CAsyncSocketExHelperWindow
         int nFamily;
        addrinfo *addrInfo, *nextAddr;  //  Iterate through protocols on connect failure
         bool onCloseCalled;  //  Set to true on first received OnClose event
    } m_SocketData;

    还有 m_pLocalAsyncSocketExThreadData 的原型为:
// Pointer to the data of the local thread
     struct t_AsyncSocketExThreadData
    {
        CAsyncSocketExHelperWindow *m_pHelperWindow;
         int nInstanceCount;
        DWORD nThreadId;
        std::list<CAsyncSocketEx*> layerCloseNotify;
    } *m_pLocalAsyncSocketExThreadData;

    每个成员具体作用注释已经比较清楚地说明了,后面用到的时候再指出。除了层 NOLAYERS 编译(如果不明白,请看第二篇)此外还有一个宏条件编译需要注意
#ifndef NOSOCKETSTATES
    m_nPendingEvents = 0;  // socket当前未决的网络事件,例如FD_READ
    m_nState = notsock;  // socket当前状态
#endif  // NOSOCKETSTATES

    析构函数 ~CAsyncSocketEx() 调用函数 Close() 关闭socket,并调用 FreeAsyncSocketExInstance() 做清理工作。
     Close() 函数中关闭层m_pFirstLayer->Close(),之后关闭成员变量 m_SocketData.hSocket 并且从辅助窗口 m_pLocalAsyncSocketExThreadData->m_pHelperWindow 记录中移除掉这个socket,之后就是销毁各种资源如地址、代理层等,有一个细节,不明白的可以MSDN,不细说了。
   if (m_hAsyncGetHostByNameHandle)
        WSACancelAsyncRequest(m_hAsyncGetHostByNameHandle);
    m_hAsyncGetHostByNameHandle = NULL;

    再说 FreeAsyncSocketExInstance() 之前先说对应的函数InitAsyncSocketExInstance(),这两个函数干的活都和一个static变量m_spAsyncSocketExThreadDataList有关,一个初始化,一个销毁 m_pLocalAsyncSocketExThreadData保存了当前 线程的id和辅助窗口的指针。

     Create(...) 函数创建代理层或者自身的socket以及做绑定到辅助窗口等操作。如果定义了使用代理层,那么所有关于socket的操作都会被代理层拦截,如create,listen,connect,accpet,recv,send,但是不包括bind,因为代理层create的时候已经提前绑定过了。
     TriggerEvent(...) 这个函数用来触发程序员指定的网络事件,例如CControlSocket类中的 Send(...) 函数就调用了 TriggerEvent(FD_WRITE) 来触发写操作。它通过PosetMessage给辅助窗口,然后窗口通过消息处理函数 WindowProc(...) 处理这种种消息(详细请 参考第二节 )。
    
    与代理层相关的函数,如 AddLayer(...) RemoveAllLayers() 等,还有设置获取各种信息的函数如GetSockOpt()就不在详述了。

    下面再补充之前函数 WindowProc(...) 关于网络事件的详细处理,仅仅针对非代理层的处理:
// if (!pSocket->m_pFirstLayer)
// {
    switch (nEvent)
   {
     case FD_READ:
        if (pSocket->GetState() == connecting && !nErrorCode)
        {
            pSocket->m_nPendingEvents |= FD_READ;  // 如果正在连接,那么将读事件加入未决事件变量里
             break;
        }
         else  if (pSocket->GetState() == attached) // 已绑定成功的设置为连接成功
             pSocket->SetState(connected);
         if (pSocket->GetState() != connected)     // 如果还没有连接成功,跳出
              break;

         //  Ignore further FD_READ events after FD_CLOSE has been received
         if (pSocket->m_SocketData.onCloseCalled)
             break;
         if (pSocket->m_lEvent & FD_READ)
        {
             DWORD nBytes = 0;
              if (!nErrorCode)
              if (!pSocket->IOCtl(FIONREAD, &nBytes))  // 获取要可读的字节数
                  nErrorCode = WSAGetLastError();
              if (nErrorCode)
                  pSocket->SetState(aborted);     // 出错
              if (nBytes != 0 || nErrorCode != 0)  // 通知socket已经有数据可以读了
                  pSocket->OnReceive(nErrorCode);
         }
          break;
    case FD_FORCEREAD:
        // 除了不用获取去可读的字节数之外,完全可FD_READ一样,这是作者自定义的类型
          break;
    case FD_WRITE:
        // 前面的状态判断和FD_READ类似,不再详述
        if (pSocket->m_lEvent & FD_WRITE)
       {
             if (nErrorCode)
                pSocket->SetState(aborted);

            pSocket->OnSend(nErrorCode); // 通知socket已经有数据可以发送了
        }
         break;
     case FD_CONNECT:
         if (pSocket->GetState() == connecting)
        {
             if (nErrorCode && pSocket->m_SocketData.nextAddr)   // 有多个地址?
            {
                  if (pSocket->TryNextProtocol())   // 尝试下一个协议地址
                       break;
             }
                 pSocket->SetState(connected);
        }
         else  if (pSocket->GetState() == attached && !nErrorCode)
             pSocket->SetState(connected);
         if (pSocket->m_lEvent & FD_CONNECT)
             pSocket->OnConnect(nErrorCode);
         if (!nErrorCode)
        {
              // 判断未决事件中是否期望的读写事件,如果有,通知socket
              if ((pSocket->m_nPendingEvents&FD_READ) && pSocket->GetState() == connected)
                 pSocket->OnReceive(0);
              if ((pSocket->m_nPendingEvents&FD_FORCEREAD) && pSocket->GetState() == connected)
                 pSocket->OnReceive(0);
              if ((pSocket->m_nPendingEvents&FD_WRITE) && pSocket->GetState() == connected)
                  pSocket->OnSend(0);
         }
         pSocket->m_nPendingEvents = 0;
          break;
     case FD_ACCPET:
          // 如果不是监听或已经绑定状态,跳出
          if (pSocket->GetState() != listening && pSocket->GetState() != attached)
               break;
          if (pSocket->m_lEvent & FD_ACCEPT)
              pSocket->OnAccept(nErrorCode); // 通知
          break;
     case FD_CLOSE:
          // 没有连接或绑定,跳出
          if (pSocket->GetState() != connected && pSocket->GetState() != attached)
               break;

          //  If there are still bytes left to read, call OnReceive instead of
         
//  OnClose and trigger a new OnClose
         DWORD nBytes = 0;
          if (!nErrorCode && pSocket->IOCtl(FIONREAD, &nBytes))
         {
               // 作者的注释很清楚,如果关闭的时候还有数据可读,将当前pSocket->m_SocketData.onCloseCalled 设置为TRUE
              
// 以表示需要再一次调用关闭函数OnClose
               if (nBytes > 0)
              {
                    //  Just repeat message.
                   PostMessage(hWnd, message, wParam, lParam);
                   pSocket->m_SocketData.onCloseCalled =  true;                               
                   pSocket->OnReceive(WSAESHUTDOWN);
                    break;
               }
          }

          pSocket->SetState(nErrorCode?aborted:closed);
          pSocket->OnClose(nErrorCode);
           break;
   }
// }

   本节是对第二节的一个小补充,也算是对MS的CAsyncSocket类的一个另类剖析吧。


//

/

///



这些图都是随手简单画的,有不对之处请指正。

首先补充一张网络上拷贝来的FTP模型图


FileZillaServer用例图

FileZillaServer主要类图(都与SOCKET有关,其它如权限、配置等都被略去)


Feedback

# re: FileZilla Server源码分析(5)附图  回复  更多评论   

2010-07-22 17:46 by  乱78糟
晕死,把继承画成了聚合,哎。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值