FileZilla Server源码分析(3)

这是分析的第三节,上一节主要讲了一些和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源码分析(3))