大漠落日

while(!dead) study++;
posts - 46, comments - 126, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

FileZilla Server源码分析(3)

Posted on 2010-06-10 13:38 乱78糟 阅读(2260) 评论(1)  编辑 收藏 引用 所属分类: 开源
这是分析的第三节,上一节主要讲了一些和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(18, 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


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

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理