读懂源码系列-FileZilla Server 设计原则分析-socket 事件处理流程(4)
程序员文章站
2022-07-12 12:51:00
...
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 的大致处理过程已经分析完毕。示意图如下:
线程的选择其实也是一大学问,涉及到负载均衡问题。这里先不展开。下面来看一下,服务器线程池的建立。
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 线程处理了。