-
CSocket
鎖定
CSocket是MFC在CAsyncSocket基礎上派生的一個同步阻塞Socket的封裝類,它的定義包含在afxsock.h中。它是如何又把CAsyncSocket變成同步的,而且還能響應同樣的Socket事件呢?
其實很簡單,CSocket在Connect()返回WSAEWOULDBLOCK錯誤時,不是在OnConnect(),OnReceive()這些事件終端函數里去等待。你先必須明白Socket事件是如何到達這些事件函數裏的。這些事件處理函數是CSocketWnd窗口對象回調的,而窗口對象收到來自Socket的事件,又是靠線程消息隊列分發過來的。總之,Socket事件首先是作為一個消息發給CSocketWnd窗口對象,這個消息肯定需要經過線程消息隊列的分發,最終CSocketWnd窗口對象收到這些消息就調用相應的回調函數(OnConnect()等)。
所以,CSocket在調用Connect()之後,如果返回一個WSAEWOULDBLOCK錯誤時,它馬上調用一個用於提取消息的函數PumpMessage(...),就是從當前線程的消息隊列裏取關心的消息.
- 外文名
- CSocket
- 所屬學科
- 計算機
CSocket情況
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()開始等待消息的到來.
CSocket説明
代碼説明如下:
A 先看Connect,因為Connect的阻塞的實現和Accept,Receive,ReceiveFrom,Send,SendTo都有點不同.
也許你們會奇怪為何是ConnectHelper(...),而不是Connect(...).其實ConnectHelper(...)才是Connect(..)
CSocket調用
真正調用的東西,如下:
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總結
CSocket模式與socket API模式的最大區別在於它實現了:
1、將socket事件消息化
2、用消息阻塞方式實現同步
多線程編程時,由於CSocket不能跨線程使用,所以,新建工作線程中會有一個CSocket對象,而該對象會重新與已知SOCKET句柄重新Attach,實現在新的線程中以CSocket對象的方式編程。
然後使用消息通信的方式,實現在新建線程中讀寫數據。
通過消息、線程事件等同步機制 實現UI線程與CSocket對象所在工作線程的通信,並不會影響UI的響應。
在BOOL CSocket::PumpMessages(UINT uStopFlag)中
//... if(bPeek&&:: PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) { if(OnMessagePending()) {// //allowuser-interfaceupdates ASSERT( pThread); pThread->OnIdle(-1); } else { bPeek=FALSE; } } //...
看OnMessagePending:
BOOLCSocket::OnMessagePending() { MSGmsg; if(:: PeekMessage(&msg,NULL,WM_PAINT,WM_PAINT,PM_REMOVE)) {//重新處理當前線程所有窗口的 WM_PAINT消息, :: DispatchMessage(&msg);//重新派送 returnFALSE; //usuallyreturnTRUE,butOnIdleusuallycausesWM_PAINTs //(通常返回TRUE,就會導致OnIdle被執行,但是OnIdle通常導致多次 WM_PAINT消息) } returnFALSE; }
但上面針對的都是當前線程的WM_PAIT消息,而不是跨線程的,而多線程模式下的SOCKET都是放在工作線程中執行的!