CSocket
CSocket
CSocket是MFC在CAsyncSocket基礎上派生的一個同步阻塞Socket的封裝類,它的定義包含在
參考圖片
其實很簡單,CSocket在Connect返回WSAEWOULDBLOCK錯誤時,不是在OnConnect,OnReceive這些事件終端函數里去等待。你先必須明白Socket事件是如何到達這些事件函數里的。這些事件處理函數是CSocketWnd窗口對象回調的,而窗口對象收到來自Socket的事件,又是靠線程消息隊列分發過來的。總之,Socket事件首先是作為一個消息發給CSocketWnd窗口對象,這個消息肯定需要經過線程消息隊列的分發,最終CSocketWnd窗口對象收到這些消息就調用相應的回調函數(OnConnect)等)。
所以,CSocket在調用Connect之後,如果返回一個WSAEWOULDBLOCK錯誤時,它馬上調用一個用於提取消息的函數PumpMessage(...),就是從當前線程的消息隊列里取關心的消息.
PumpMessage會遇到下面幾種情況:
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);
returnFALSE;
}
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模式與socket API模式的最大區別在於它實現了:
1、將socket事件消息化
2、用消息阻塞方式實現同步
3、用序列化方式讀寫數據以防止死鎖,簡化讀取數據模型(流數據)。
然後使用消息通信的方式,實現在新建線程中讀寫數據。
通過消息、線程事件等同步機制 實現UI線程與CSocket對象所在工作線程的通信,並不會影響UI的響應。
在BOOL CSocket::PumpMessages(UINT uStopFlag)中
看OnMessagePending:
但上面針對的都是當前線程的WM_PAIT消息,而不是跨線程的,而多線程模式下的SOCKET都是放在工作線程中執行的!