读懂源码系列-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 的大致处理过程已经分析完毕。示意图如下:


        线程的选择其实也是一大学问,涉及到负载均衡问题。这里先不展开。下面来看一下,服务器线程池的建立。

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 线程处理了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值