这是分析的第三节,上一节主要讲了一些和socket基础操作相关的代码,本节将分析核心代码。
Service.cpp 系统服务程序
FileZillaServer可以选择是否注册成windows的服务程序,而这个服务程序的代码就是由service.cpp文件实现的。
WinMain是它的入口函数,在
WinMain里依次完成了下面几项任务:
- 参数解析
- 初始化某些数据比如端口
- 由SCM(服务控制管理器)启动服务,入口为ServiceMain函数;如果服务不存在,进入步骤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_DESTROY和
WM_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系统数据破坏无法启动,重装系统导致笔记丢失,这里只能补上,拖后了一周左右。