CSocket是MFC在CAsyncSocket基础上派生的一个同步阻塞Socket的封装类。它是如何又把CAsyncSocket变成同步的,而且还能响应同样的Socket事件呢?
其实很简单,CSocket在Connect()返回WSAEWOULDBLOCK错误时,不是在OnConnect(),OnReceive()这些事件终端函数里去等待。你先必须明白Socket事件是如何到达这些事件函数里的。这些事件处理函数是CSocketWnd窗口对象回调的,而窗口对象收到来自Socket的事件,又是靠线程消息队列分发过来的。总之,Socket事件首先是作为一个消息发给CSocketWnd窗口对象,这个消息肯定需要经过线程消息队列的分发,最终CSocketWnd窗口对象收到这些消息就调用相应的回调函数(OnConnect()等)。
所以,CSocket在调用Connect()之后,如果返回一个WSAEWOULDBLOCK错误时,它马上调用一个用于提取消息的函数PumpMessage(...),就是从当前线程的消息队列里取关心的消息.
PumnMessage会遇到下面几种情况:
1 提取出了(从消息队列中移出来Remove),用户正在使用的一个Socket发送的WM_SOCKET_NOTIFY消息和对应的
FD_XXX事件,返回True.
2 提取出了(从消息队列中移出来Remove),用户正在使用的一个Socket发送的WM_SOCKET_NOTIFY消息和对应的
FD_Close事件,返回True.
3 提取出了(从消息队列中移出来Remove),PumpMessage(..)设定的定时器的WM_TIMER消息,TimeOut事件为
CSocket的一个成员变量,m_nTimeOut=2000ms,返回True
4 用户调用了CancelBlockingCall() 设置错误代码为WSAEINTR(被中断了),返回False
5 用户一直没有取到用户正在使用的一个Socket发送的WM_SOCKET_NOTIFY消息和对应的FD_XXX事件,但是取到
了同一个线程中的其他Socket的WM_SOCKET_NOTIFY消息及其对应的消息,则将这些消息,加入到一个辅助性
的队列中去,以后处理.
6 没有取到任何WM_SOCKET_NOTIFY消息,则开始查看(不是取出来,而是查看)本线程的消息队列中是否有其他
消息,如果有的话,调用虚函数OnMessagePending(),来处理这些消息(OnMessagePending()用户可以自定义
在阻塞时,用户想要处理的消息),如果没有,则调用WaitMessage()开始等待消息的到来.
代码说明如下:
A 先看Connect,因为Connect的阻塞的实现和Accept,Receive,ReceiveFrom,Send,SendTo都有点不同.
也许你们会奇怪为何是ConnectHelper(...),而不是Connect(...).其实ConnectHelper(...)才是Connect(..)
真正调用的东西,如下:
BOOL CAsyncSocket::Connect(const SOCKADDR* lpSockAddr, int nSockAddrLen)
{ return ConnectHelper(lpSockAddr, nSockAddrLen); }
//ConnectHelper( ... )为一虚函数
//继承自CAsyncSocket,Csocket有重新定义过.
//这也是为什么CSocket是Public继承的原因
BOOL CSocket::ConnectHelper( ... )
{
//一旦调用 就先检查当前是否有一个阻塞操作正在进行
//如果是,立马返回,并设置错误代码.
......
......
m_nConnectError = -1;
//注意它只调用了一次CAsyncSocket::ConnectHelper( ... )
if( !CAsyncSocket::ConnectHelper( ... ) )
{
//由于Connect(...)要求自己和Server进行三步握手
//即需要发送一个Packet,而且等待回复(这个也就是
//涉及连接和发送数据操作的Socket API会阻塞的原因)
//所以CAsyncSocket::ConnectHelper(...)会立即返回,
//并且设置错误为WSAEWOULDBLOCK(调用该函数会导致阻塞)
if( WSAGetLastError() == WSAEWOULDBLOCK )
{
//进入消息循环,以从线程消息队列里查看FD_CONNECT消息,
//收到FD_CONNECT消息(在PumpMessage中会修改m_nConnectError),返回
//或者WM_TIMER/FD_CLOSE(return true,但不会修改m_nConnectError),
//继续调用PumpMessage来取消息
//或者错误,那么就返回socket_error
while( PumpMessages( FD_CONNECT ) )
{
if (m_nConnectError != -1)
{
WSASetLastError(m_nConnectError);
return (m_nConnectError == 0);
}
} //end while
}
return false;
}
return true;
}
//在PumpMessages中会设置一个定时器,时间为m_nTimeOut=2000ms
//当在这个时间之内,依然没有得到消息的话,就返回
BOOL CSocket::PumpMessages( UINT uStopFlag )
{
//一旦进入这个函数,就设置Socket当前状态为阻塞
BOOL bBlocking = TRUE;
m_pbBlocking = &bBlocking;
....................
.....................
....................
CWinThread* pThread = AfxGetThread();
//bBlocking是一个标志,
// 用来判断用户是否取消对Connect()的调用
//即是否调用CancelBlockingCall()
while( bBlocking )
{
//#define WM_SOCKET_NOTIFY 0x0373
//#define WM_SOCKET_DEAD 0x0374
MSG msg;
//在此处只是取WM_SOCKET_NOTIFY 和 WM_SOCKET_DEAD消息
if (::PeekMessage(&msg, pState->m_hSocketWindow,WM_SOCKET_NOTIFY, WM_SOCKET_DEAD,
PM_REMOVE))
{
if (msg.message == WM_SOCKET_NOTIFY && (SOCKET)msg.wParam == m_hSocket)
{
//这个是PumpMessage的第2种情况
if (WSAGETSELECTEVENT(msg.lParam) == FD_CLOSE)
{ break;}
//这个是PumpMessage的第1种情况
if(WSAGETSELECTEVENT(msg.lParam) == uStopFlag )
{ ......; break;}
}
//这个是PumpMessage的第5种情况
if (msg.wParam != 0 || msg.lParam != 0)
CSocket::AuxQueueAdd(msg.message, msg.wParam, msg.lParam);
}
//这个是PumpMessage的第3种情况
else if (::PeekMessage(&msg, pState->m_hSocketWindow,WM_TIMER, WM_TIMER, PM_REMOVE))
{ break;}
//这个是PumpMessage的第6种情况
if (bPeek && ::PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if (OnMessagePending())
{
}
else
{
WaitMessage();
.....
}
}
}//end while
////这个是PumpMessage的第4种情况
if (!bBlocking)
{
WSASetLastError(WSAEINTR);
return FALSE;
}
m_pbBlocking = NULL;
//将WM_SOCKET_NOTIFY消息发送到Creat CSocketWnd线程的消息队列中
//以便处理其他的Socket消息
::PostMessage(pState->m_hSocketWindow, WM_SOCKET_NOTIFY, 0, 0);
return TRUE;
}
B 再看Receive(..)
//其实CSocket的这种实现方式决定了,应用程序不能够在一个线程中Create一个socket,
//然后创建一个新的线程来专门Receive,因为这个新的线程将永远不能取到FD_Read的事件,
//因为并不是在这个线程中创建的CSocketWnd对象,它无法接受到发送到CSocketWnd的消息
//(在Windows中接受消息的主体是窗口)
//Receive和Connect的实现方式的最大区别为
//Connect 是不断的调用PumpMessage(..)
//而Receive则不断的调用自身
int CSocket::Receive(void* lpBuf, int nBufLen, int nFlags)
{
if (m_pbBlocking != NULL)
{
WSASetLastError(WSAEINPROGRESS);
return FALSE;
}
int nResult;
while ((nResult = CAsyncSocket::Receive(lpBuf, nBufLen, nFlags)) == SOCKET_ERROR)
{
if (GetLastError() == WSAEWOULDBLOCK)
{
//一旦提取到FD_READ///FD_CLOSE///WM_TIMER时
// 就再次调用CAsyncSocket::Receive(...)
if (!PumpMessages(FD_READ))
return SOCKET_ERROR;
}
else
return SOCKET_ERROR;
}
return nResult;
}
最后,总结一下自己对CSocket的看法,
1 虽然它解决了结束阻塞线程的方法,调用CancelBlockingCall,但是多线程模式根本就不适合于CSocket
2 CSocket和CAsyncSocket利用Windows的消息模式将前台的界面处理和后台的网络通信都整合到消息传递模型
下,但是很明显,一旦后台的网络过于繁忙,则前台的处理可能就无法顾及,所以CSocket也就只能小打小闹.