CSocket类是CAsyncSocket类的派生类,它继承了windows socket API封装函数。实现了比CAsyncSocket类对windows socket更高层的抽象。它与类CSocketFile和CArchive共同合作完成对发送数据和接受数据的管理,CSocket类提供了对于同步操作CArchive对象非常重要的阻塞功能,是程序员在管理数据的发送和接收的工作变得简单。
CSocket类提供了阻塞的访问方式,这对于CArchive类的同步操作是必需的,其成员函数如receive、 send、receivefrom、sendto和accept不会像winsock中的函数一样返回WSAEWOULDBLOCK的错误,这些函数会自己等待直到操作完成。如果在这些阻塞函数将会完成等待并返回WSAEINTR的错误。
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也就只能小打小闹.
解释如何组合 CSocket 对象、CSocketFile 对象和 CArchive 对象以简化通过 Windows 套接字发送和接收数据。
对于套接字,存档并不附加到标准的 CFile 对象(通常与磁盘文件关联),而是附加到 CSocketFile 对象。CSocketFile 对象不是连接到磁盘文件,而是连接到CSocket 对象。
一个 CArchive 对象管理一个缓冲区。当存储(发送)存档的缓冲区已满时,关联的CFile 对象写出缓冲区的内容。刷新附加到套接字的存档缓冲区相当于发送消息。当加载(接收)存档的缓冲区已满时,CFile 对象停止读取直到该缓冲区再次可用。
CSocketFile 类从 CFile 派生,但它并不支持 CFile 成员函数,如定位函数Seek 、GetLength 、 SetLength 等,锁定函数LockRange 和UnlockRange ,或 GetPosition 函数。每个 CSocketFile 对象必须要做的事是,将字节序列写入或读入关联的CSocket 对象,或从此对象写出或读出字节序列。因为不涉及文件,Seek 和 GetPosition 等操作没有意义。 CSocketFile 从CFile 派生,因此它通常会继承所有这些成员函数。为防止发生这种情况,在CSocketFile 中重写不受支持的CFile 成员函数以引发 CNotSupportedException。
CSocketFile 对象调用其 CSocket 对象的成员函数来发送或接收数据
这看起来很复杂,其目的是使您不必亲自管理套接字的细节。您创建套接字、文件和存档,然后通过将数据插入存档或从存档提取数据,开始发送或接收数据。CArchive、CSocketFile 和 CSocket 管理后台的细节。
CSocket 对象实际是一个两状态对象:有时异步(通常状态)有时同步。处于异步状态时,套接字可以从框架接收异步通知。然而,在操作(如接收或发送数据)过程中,套接字变为同步的。这意味着在同步操作完成之前,套接字不会接收进一步的异步通知。由于套接字切换模式,请执行类似下面的操作:
CMySocket::OnReceive( ){ // ... ar > > str; // ...}
如果 CSocket 没有实现为两状态对象,则在您处理前面通知的同时,有可能接收到同类事件的附加通知。例如,在处理OnReceive时,可能收到OnReceive通知。在上面的代码片段中,从存档提取str可能导致递归。通过切换状态,CSocket 用防止附加通知的方法防止递归。一般规则是通知内没有通知。
CHATTER 和CHATSRVR 示例应用程序阐释了这种用法。有关 MFC 示例的源代码和信息,请参见MFC 示例。
注意 CSocketFile 也可以作为一个没有 CArchive 对象的(有限)文件使用。默认情况下, CSocketFile 构造函数的 bArchiveCompatible 参数为 TRUE 。这指定文件对象用于存档。若要使用没有存档的文件对象,请在 bArchiveCompatible 参数中传递 FALSE 。
在“存档兼容”模式下, CSocketFile 对象可提供更好的性能并能减少“死锁”的危险。当发送套接字和接收套接字都在等待对方或等待公共资源时,就会发生死锁现象。如果CArchive 对象用处理CFile 对象的方式处理 CSocketFile ,也可能发生这种情况。处理CFile 时,存档可假定只要它接收到的字节数比所请求的少,则说明已到达文件尾。而处理CSocketFile 时,数据是基于消息的,缓冲区可包含多条消息,因此,接收的字节数比请求的字节数少并不能说明已到达文件尾。应用程序在此情况下并不阻塞(而使用CFile 时可能阻塞),它可继续从缓冲区读取消息直到缓冲区变空。在这种情况下,CArchive 中的 IsBufferEmpty 函数有助于监视存档缓冲区的状态。
转自:http://blog.csdn.net/caowei880123/article/details/8295152