Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 398|回复: 0

CSocket,CAsyncSocket多线程退出时的一些注意事项(解决关闭WinSoket崩溃的问题)

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-7-17 10:52:27 | 显示全部楼层 |阅读模式

         在最近修改代码时发现,如果使用了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还未去除)。

     

     

     

     

     

      

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-1 00:53 , Processed in 0.064568 second(s), 30 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表