之所以有本系列的分析,是因为两点:

  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有关,其它如权限、配置等都被略去)

FileZilla Server源码分析(1)相关推荐

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

    之所以有本系列的分析,是因为两点: FileZilla 是目前非常火爆的开源ftp项目,整个项目采用C++代码编写,代码紧凑可读性高,值得学习(缺陷是注释太少). 网络上已有的对该源码的分析基于的版本 ...

  2. FileZilla Server源码分析

    FileZilla Server源码分析(1) 之所以有本系列的分析,是因为两点: FileZilla 是目前非常火爆的开源ftp项目,整个项目采用C++代码编写,代码紧凑可读性高,值得学习(缺陷是注 ...

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

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

  4. 【SpringCloud系列】服务注册与发现 - Eureka Server源码分析(2)

    3.6.Eureka Server 源码分析 上一篇文章简单介绍了 Eureka 的一些概念,今天咱们来看看其原理和源码,首先先看 Eureka Server 的原理. 3.6.1.Eureka Se ...

  5. FileZilla Server源码解析之LIST命令

    FileZilla Server源码解析之LIST命令 如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 FileZilla版本:Fil ...

  6. Appium Server源码分析之作为Bootstrap客户端

    Appium Server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的REST协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把 ...

  7. TinyHttpd----超轻量型Http Server源码分析

    tinyhttpd是一个超轻量型Http Server,使用C语言开发,全部代码只有502行(包括注释),附带一个简单的Client,可以通过阅读这段代码理解一个 Http Server 的本质.下载 ...

  8. Docker源码分析(五):Docker Server的创建

    http://www.infoq.com/cn/articles/docker-source-code-analysis-part5 1.Docker Server简介 Docker架构中,Docke ...

  9. Golang http之server源码详解

    http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response).HTTP server–简而言之就是一个支持http协议的服务,http是一个相对简单的 ...

最新文章

  1. ES6 Generator 初体验
  2. 《图解HTTP》读书笔记--第8章 确认访问用户身份的认证
  3. 【NLP】一份相当全面的BERT模型精讲
  4. 去重的Set解不出“斯诺登的密码”(洛谷P1603题题解,Java语言描述)
  5. 扫雷打开的初始区域递归_Python:游戏:写一个和 XP 上一模一样的“扫雷”!
  6. 东大OJ-1051-旅行家的预算
  7. 走美团特色的无人配送道路,王慧文:久久为功才能做成
  8. [Beego模型] 五、构造查询
  9. LeetCode 34. Search for a Range
  10. 上海财经大学计算机考研好不好,五所不错的财经类大学,不歧视,不压分,你会考虑吗?...
  11. IIS服务器重启三种方法
  12. macos显卡测试软件,GpuTest显卡测试软件 for Mac 64位
  13. 自变量与因变量相关分析不显著能做回归分析吗?
  14. 小米和美的互投,大明湖畔的董明珠怎么办?
  15. Android调用QQ加群代码
  16. linux listener.ora 位置,Oracel数据库 listener.ora和tnsnames.ora配置
  17. Android星座运势案例
  18. 字节飞书提前批一面(凉的透透的)
  19. 全栈Python自动化测试学习资料【付费资源、看到即赚到,】
  20. Android 获取内部存储中音乐文件的正确姿势

热门文章

  1. 文件随机或顺序读写原理深入浅出
  2. 什么是MIL、SIL、PIL和HIL?
  3. 室外温度已达34度,本博客提供自助风扇服务
  4. 实用工具和网站合集分享
  5. Ubuntu18.04中如何更换为清华的镜像源
  6. Hive常用函数介绍(窗口函数)
  7. Android开屏视频广告
  8. 首期“赛迪区块链技术与应用培训班”成功举办
  9. 洛谷P1024 [NOIP2001 提高组] 一元三次方程求解 C++ 思路加代码
  10. 2021中国 IoT 物联网平台研究报告 |艾瑞咨询