天天看點

讀懂源碼系列-FileZilla Server 設計原則分析-socket 事件處理流程(4)

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 的大緻處理過程已經分析完畢。示意圖如下:

讀懂源碼系列-FileZilla Server 設計原則分析-socket 事件處理流程(4)

        線程的選擇其實也是一大學問,涉及到負載均衡問題。這裡先不展開。下面來看一下,伺服器線程池的建立。

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 伺服器的整體通信機制:

讀懂源碼系列-FileZilla Server 設計原則分析-socket 事件處理流程(4)

        已用戶端連接配接伺服器為例。首先,FTP伺服器建立了主視窗 hMainWnd 用于處理全局性的任務。然後當監聽 socket 建立的時候,輔助視窗 hHelperWnd 就建立了起來。 在每個擁有 CAsyncSocketEx 對象的線程中,都有輔助視窗,用于處理所有 socket 通知。         當用戶端連接配接伺服器時,hHelperWnd 收到 FD_ACCEPT 通知,并調用 accept 建立控制套接字。并把這個控制套接字關聯到某個 CServerThread 線程。這樣 ControlSocket 上的所有通知就由這個指定的 CServerThread 線程處理了。

繼續閱讀