CSocket,CAsyncSocket多線程退出時的一些注意事項(解決關閉WinSoket崩潰的問題)


     在最近修改代碼時發現,如果使用了CSocket(CAsyncSocket)對象進行網絡通信,在程序結束時關閉這個socket時程序就會崩潰。之前代碼是好的,改出來的問題。對比代碼和在網上找了些資料,確認CSocket(CAsyncSocket)對象在多線程使用時有些要注意的地方,這里稍微總結一下。簡單來說,如果在線程A中創建了CSocket(CAsyncSocket)對象,如果在其他線程中直接調用Close()方法關閉它,程序就會崩潰。如果需要在其他線程中關閉,需要做一下事情:

一.在創建Socket的線程中分離socket對象和當前線程的關聯。

1     g_pMySocket = new CMySocket; //CMySocket繼承CSocket或CAsyncSocket。
2     if (g_pMySocket && g_pMySocket->Create() == false)
3     {
4         MessageBox(_T("Create mysocket fail!"));
5     }
6     g_pMySocket->m_socket = g_pMySocket->Detach(); //m_socket為自定義成員變量。

二.在需要關閉的線程中再關聯socket對象。

1     g_pMySocket->Attach(g_pMySocket->m_socket);
2     //Do sth...
3     g_pMySocket->Close(); //

     修改起來很簡單,主要分析一下為何要這樣做.

     CSocket繼承自CAsyncSocket,相關代碼都差不多。首先在創建socket時會調用CAsyncSocket的create方法。create方法中調用CAsyncSocket::Socket的構造函數,在構造函數中會調用CAsyncSocket::AttachHandle,這是個靜態函數。主要就是在這個函數中會將當前線程和socket對象綁定,代碼如下:

 1 void PASCAL CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead)
 2 {
 3     /*
 4      * _afxSockThreadState 其實就是 AfxGetModuleThreadState(),它會返回當前線程的一個 AFX_MODULE_THREAD_STATE 對象, AFX_MODULE_THREAD_STATE
 5      * 是一個繼承自 CNoTrackObject 的類,它里面有以下幾個public的成員變量和socket有關:
 6      *            // WinSock specific thread state
 7      *        HWND m_hSocketWindow; //WinSock必須有一個窗口用於接收相關消息,這是個隱藏窗口,后面會看到。
 8      *    #ifdef _AFXDLL
 9      *        CEmbeddedButActsLikePtr<CMapPtrToPtr> m_pmapSocketHandle; //當前子類(自己寫的)socket對象和m_hSocket的map
10      *        CEmbeddedButActsLikePtr<CMapPtrToPtr> m_pmapDeadSockets; //當socket對象需要關閉時會把這個socket加到這個map中
11      *        CEmbeddedButActsLikePtr<CPtrList> m_plistSocketNotifications; //存儲當前socket的相關消息,如 WM_SOCKET_NOTIFY, WM_SOCKET_DEAD, PM_REMOVE
12      *    #else
13      *        CMapPtrToPtr* m_pmapSocketHandle;
14      *        CMapPtrToPtr* m_pmapDeadSockets;
15      *        CPtrList* m_plistSocketNotifications;
16      * CAsyncSocket中有很多方法都是靜態的,都是通過這個 _AFX_SOCK_THREAD_STATE 對象找到子類實例化的socket對象。
17      */
18     _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
19 
20     BOOL bEnable = AfxEnableMemoryTracking(FALSE);
21 
22     TRY 
23     {
24         if (!bDead) //默認bDead是false,走這個流程,在killsocket時bDead會為true
25         {
26             /*
27              * 默認當前線程是沒有wincoket對象的,所以先 ASSERT 一下,如果關閉soket是只調用 closesoket 方法,而不是使用
28              * Close()方法,這個ASSERT就會報錯,因為 closesoket 只是關閉了socket,而它和線程的關聯還沒有去掉。
29              */
30             ASSERT(CAsyncSocket::LookupHandle(hSocket, bDead) == NULL);
31             
32             if (pState->m_pmapSocketHandle->IsEmpty())
33             {
34                 ASSERT(pState->m_pmapDeadSockets->IsEmpty()); //確保當前線程沒有未正確關閉的socket。
35                 ASSERT(pState->m_hSocketWindow == NULL); //確保未有釋放的窗口對象。
36 
37                 //創建用於接收socket相關消息的隱藏窗口。
38                 CSocketWnd* pWnd = new CSocketWnd; 
39                 pWnd->m_hWnd = NULL;
40                 if (!pWnd->CreateEx(0, AfxRegisterWndClass(0), 
41                     _T("Socket Notification Sink"),
42                     WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL))
43                 {
44                     TRACE(traceSocket, 0, "Warning: unable to create socket notify window!\n");
45                     delete pWnd;
46                     AfxThrowResourceException();
47                 }
48 
49                 ASSERT(pWnd->m_hWnd != NULL);
50                 ASSERT(CWnd::FromHandlePermanent(pWnd->m_hWnd) == pWnd);
51                 pState->m_hSocketWindow = pWnd->m_hWnd;
52             }
53             
54             //保存當前子類socket和m_hSocket的關聯,pSocket傳入的是當前線程的this指針。hSocket就是m_hSocket,pSocket就是子類的socket指針。
55             pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
56         }
57         else
58         {
59             //當 KillSocket 的時候會走到這個分支。
60             void* pvCount;
61             INT_PTR nCount;
62             if (pState->m_pmapDeadSockets->Lookup((void*)hSocket, pvCount))
63             {
64                 nCount = (INT_PTR)pvCount;
65                 nCount++;
66             }
67             else
68                 nCount = 1;
69             pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
70         }
71     }
72     CATCH_ALL (e) 
73     { 
74         AfxEnableMemoryTracking(bEnable); 
75         THROW_LAST(); 
76     } 
77     END_CATCH_ALL
78 
79     AfxEnableMemoryTracking(bEnable);
80 }

     總的來說,創建Winsocket的時候大致會做一下三個關鍵步驟:

    1.將這個socket和當前執行線程關聯,標准socket作為key,子類WinSocket作為Value存在m_pmapSocketHandle中。

    2.創建一個隱藏窗口用於接收消息。

    3.調用 WSAAsyncSelect 向系統注冊,這個函數的作用就是當相關socket的事件發生時將的消息發給創建的隱藏窗口。CSocketWnd 類中定義了相關消息的處理函數,在消息響應函數中最終調用CAsyncSocket::DoCallBack方法,在這個方法中對不同的socket事件,如發生,接收,斷開等通過虛函數形式調用子類的相關方法。也就是你自己實現的相關方法,如OnReceive, OnClose等。

   從上面的過程可以看出為什么在不同線程中關閉winsocket就會出現異常了,以及為什么需要先要Detach再Attach一下。(重新綁定線程和Socket的關系。這個重新綁定不僅僅是pmapSocketHandle的key,value修改一下,而且還要重新創建CSocketWnd窗口,因為在Detach的時候這個窗口已經被銷毀了。)

   仔細想一下,微軟為什么要這么做?為什么多線程使用起來這么麻煩一下?其實這么做已是最好的方法了。微軟封裝了標准的socket以供我們使用,為了大多數時候使用的方便就結合和windows的消息機制。windows消息機制就是當事件發生時系統向指定的窗口發送一個消息,所以在創建winsocket的時候先創建了一個CSocketWnd窗口以接收相關消息。那么當窗口接收到消息后如何再通知給相關的socket呢?在winsocket類中如何得到socket對象從而調用其相關方法呢?我們看到在CSocketWnd消息處理函數中實際上調用的是CSocket靜態方法,其實在CSocket中有很多靜態方法,在這些靜態方法中如何得到具體的socet對象呢?就是通過

1 _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;

這句代碼,這句代碼在這些靜態函數中都有使用。在Socket的構造函數中將使用標准socket生成的m_hSocke對象t和子類的WinSocket對象指針通過AttachHandle靜態方法放在pState的m_pmapSocketHandle成員變量中。創建CSocketWnd窗口的線程,創建Socket的線程和接收Socket消息的線程其實都是同一個線程,如圖所示:

所以CSocketWnd接收到消息后_afxSockThreadState得到線程對象通過m_pmapSocketHandle就能找到子類的CSocket對象,從而調用相關的虛函數,實現發送,接收,斷開等功能。

這里總結下使用Winsocket需要注意的一些地方:

1.創建Socket時要在最好在主線程中創建,因為創建Socket時創建了一個隱藏的窗口,如果創建Socket的這個線程很快就結束了,那Socket的隱藏窗口也就自動被釋放了,無法再接收到相關消息了。這種情況下就需要調用Detach和Attach方法重新綁定一下線程,Attach方法會再創建一個隱藏窗口和當前線程關聯。

2.關閉Soket時最好用WinSocket的Close()方法,而不是使用socket的closesocket()方法。因為Close()方法會銷毀創建的隱藏窗口,取消WinSocket和當前線程的關聯。如果只使用closesocket方法,就會引起內存泄漏和如果當前線程再創建WinSocket就可能會出錯(因為上次綁定的Socket還未去除)。

 

 

 

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM