1.前言
上一篇講到 socket 發生 FD_ACCEPT 事件時,處理流程到達輔助視窗的視窗過程。那麼 FD_ACCEPT 事件是如何處理的呢?本篇帶領大家一探究竟。
2.處理流程
首先跟蹤如下函數:
static LRESULT CALLBACK CAsyncSocketExHelperWindow::WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
void CListenSocket::OnAccept(int nErrorCode)
BOOL CAsyncSocketEx::Accept( CAsyncSocketEx& rConnectedSocket, SOCKADDR* lpSockAddr /*=NULL*/, int* lpSockAddrLen /*=NULL*/ )
void CServerThread::AddSocket(SOCKET sockethandle, bool ssl)
BOOL CThread::PostThreadMessage(UINT message, WPARAM wParam, LPARAM lParam)
int CServerThread::OnThreadMessage(UINT Msg, WPARAM wParam, LPARAM lParam)
void CServerThread::AddNewSocket(SOCKET sockethandle, bool ssl)
第 1 行的函數,是輔助視窗的視窗過程。當 FTP 伺服器的監聽端口發生 FD_ACCEPT 事件時,輔助視窗調用 CAsyncSocketEx::OnAccept(int ) 虛函數,但監聽 socket 已将該虛函數覆寫。是以,處理流暢來到第 2 行。 第 2 行的函數,主要做了兩個工作。第一工作是第 3 行,實際上接受用戶端的連接配接,得到一個已連接配接的套接字 sockethandle;第二個工作是第 4 行,将 sockethandle 交由某個 CServerThread 線程處理。 第 4 ~ 7 行的函數,即是線程如何處理 sockethandle 的過程。
void CServerThread::AddSocket(SOCKET sockethandle, bool ssl)
{
PostThreadMessage(WM_FILEZILLA_THREADMSG, ssl ? FTM_NEWSOCKET_SSL : FTM_NEWSOCKET, (LPARAM)sockethandle);
}
注意,此處的 PostThreadMessage 隻有 3 個參數,并不是 Win32 SDK 裡的函數。而是如下函數:
BOOL CThread::PostThreadMessage(UINT message, WPARAM wParam, LPARAM lParam)
{
BOOL res=::PostThreadMessage(m_dwThreadId, message, wParam, lParam);;
ASSERT(res);
return res;
}
可以看到,FTP 伺服器使用了投遞線程消息的方式,去處理已連接配接的套接字。那麼該線程必有 Windows 消息循環。 CThread 是 CServerThread 的基類,CThread 是線程的包裝器。可以發現,以下函數就是線程中的消息循環:
DWORD CThread::Run()
{
InitInstance();
SetEvent(m_hEventStarted);
m_started = true;
MSG msg;
while (GetMessage(&msg, 0, 0, 0)) {
// Since we do not handle keyboard events in the thread, don't translate messages.
if (!msg.hwnd)
OnThreadMessage(msg.message, msg.wParam, msg.lParam);
DispatchMessage(&msg);
}
DWORD res = ExitInstance();
delete this;
return res;
}
這個函數,使用了設計模式中的模版方法。在進入消息循環之前,派生類可以覆寫 InitInstance 虛函數完成指定的初始化任務;而在退出消息循環之後,派生類可以覆寫 ExitInstance 虛函數完成指定的析構任務。 重點看消息循環,當通過 ::PostThreadMessage 向指定線程投遞消息時,調用 GetMessage 得到的消息 msg,其 msg.hwnd == NULL。因為該消息不屬于任何視窗,而此後 DispatchMessage 也無法調用指定視窗的視窗過程。 是以,處理流程來到了 CServerThread::OnThreadMessage---->CServerThread::AddNewSocket。在 AddNewSocket 函數中,我們看到已連接配接的套接字 sockethandle 與一個 CControlSocket 對象關聯起來。沒錯,CControlSocket 是 CAsyncSocketEx 的派生類。此時,已連接配接的套接字,就與這個線程裡唯一的輔助視窗關聯起來。當用戶端通過這個套接字發送指令到伺服器時,系統發送 FD_READ 可讀通知到該線程的消息隊列,而 CThread::Run 中的 DispatchMessage 将把該消息發送給輔助視窗的視窗程式處理。 至此,sokcet 事件 FD_ACCPET 的大緻處理過程已經分析完畢。示意圖如下:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISOzkDMycDNxEzNwkDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
線程的選擇其實也是一大學問,涉及到負載均衡問題。這裡先不展開。下面來看一下,伺服器線程池的建立。
3.伺服器線程 CServerThread
可以看到,伺服器中的 CServerThread 是伺服器線程池中的線程。在 FTP 主線中,有一個主視窗,其句柄值為 hMainWnd。線程池中的所有線程,通過 PostMessage 與主線程通信。那主線程中,如何差別是哪個線程發送的消息呢?答案就在 CServerThread 的建立代碼中:
//Create the threads
int num = (int)m_pOptions->GetOptionVal(OPTION_THREADNUM);
for (int i = 0; i < num; ++i)
{
int index = GetNextThreadNotificationID();
CServerThread *pThread = new CServerThread(WM_FILEZILLA_SERVERMSG + index);
m_ThreadNotificationIDs[index] = pThread;
if (pThread->Create(THREAD_PRIORITY_NORMAL, CREATE_SUSPENDED))
{
pThread->ResumeThread();
m_ThreadArray.push_back(pThread);
}
}
每個 CServerThread 建立時,都得到了一個關聯的通知ID = WM_FILEZILLA_SERVERMSG + index,其中 index 是這個線程在主線程中的存儲位置索引。當特定線程使用 PostMessage 向主線程傳遞消息時,把 ID 作為消息值,即:
PostMessage(hMainWnd, ID, 0, 0)
當主線程收到消息時,把 ID 值減去 WM_FILEZILLA_SERVERMSG 即可得到是哪個線程發送的消息。
4.總結
至此,我們得出了 FTP 伺服器的整體通信機制:
已用戶端連接配接伺服器為例。首先,FTP伺服器建立了主視窗 hMainWnd 用于處理全局性的任務。然後當監聽 socket 建立的時候,輔助視窗 hHelperWnd 就建立了起來。 在每個擁有 CAsyncSocketEx 對象的線程中,都有輔助視窗,用于處理所有 socket 通知。 當用戶端連接配接伺服器時,hHelperWnd 收到 FD_ACCEPT 通知,并調用 accept 建立控制套接字。并把這個控制套接字關聯到某個 CServerThread 線程。這樣 ControlSocket 上的所有通知就由這個指定的 CServerThread 線程處理了。