這是分析的第三節,上一節主要講了一些和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系統數據破壞無法啟動,重裝系統導致筆記丟失,這里只能補上,拖后了一周左右。