关闭

FileZilla Server源码分析(1)

标签: serversocketftp服务器usersockets工作
5474人阅读 评论(0) 收藏 举报
分类:
之所以有本系列的分析,是因为两点:
  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类中虚函数,如OnAcceptOnSendOn打头的函数用于通知特定事件的发生状态,派生类如果需要获得这些状态信息,就可以自己派生这些函数。

全局的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系统数据破坏无法启动,重装系统导致笔记丢失,这里只能补上,拖后了一周左右。


    本节的分析是基于本系列第二篇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有关,其它如权限、配置等都被略去)


0
0

猜你在找
【直播】机器学习&数据挖掘7周实训--韦玮
【套餐】系统集成项目管理工程师顺利通关--徐朋
【直播】3小时掌握Docker最佳实战-徐西宁
【套餐】机器学习系列套餐(算法+实战)--唐宇迪
【直播】计算机视觉原理及实战--屈教授
【套餐】微信订阅号+服务号Java版 v2.0--翟东平
【直播】机器学习之矩阵--黄博士
【套餐】微信订阅号+服务号Java版 v2.0--翟东平
【直播】机器学习之凸优化--马博士
【套餐】Javascript 设计模式实战--曾亮
查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:686432次
    • 积分:7669
    • 等级:
    • 排名:第2729名
    • 原创:35篇
    • 转载:390篇
    • 译文:0篇
    • 评论:21条
    最新评论