谈谈IOCP模型下tcp套接字资源的释放

     Windosw系统下IOCP模型的网络收发效率高,CPU占用低,是前端网络服务器开发最推荐使用的方式,但是对于初学者来说,特别是我这种英文原版资料理解吃力的人来说,刚开始会觉得很简单, 就那么几个有限的函数,启动几个工作线程,就跑起来了。

     但是无论是做tcp-server端还是tcp-client,套接字资源的释放都是一个令人头痛的问题,稍微不注意就是系统内存访问出问题,程序崩溃。

     最近搞流媒体数据的分发,数据量大,并发多,需要用到IOCP,就遇到了这个问题,查了一些资料,有一些教训。希望与朋友们分享一点经验,希望帮到有缘人。毕竟我也是从广大网友的经验中不断吸取经验,才对IOCP运用有了点粗浅认识。

      代码是最好的老师, 对我理解学习IOCP帮助最大的应该是博主TTGuoying了,大家可以移步:

         https://www.cnblogs.com/tanguoying/p/8439701.html

         https://github.com/TTGuoying/IOCPServer

      (作为IOCP学习,这篇也值得一看:https://blog.csdn.net/canlynetsky/article/details/108646680

代码提供了一个完整的TCP-SERVER的网络框架。作者封装了一个IOCPBase类,IOCPBase以虚成员方式,定义了一系列的网络事件回调函数:

    // 新连接
    virtual void OnConnectionEstablished(IOCP_SocketContext *connSockContext) = 0;

    // 连接关闭
    virtual void OnConnectionClosed(IOCP_SocketContext *connSockContext) = 0;

    // 连接上发生错误
    virtual void OnConnectionError(IOCP_SocketContext *connSockContext, int error) = 0;

    // 读操作完成
    virtual int OnRecvCompleted(IOCP_SocketContext *connSockContext, IOCP_IOContext *ioContext, DWORD dwRxLen) = 0;

    // 写操作完成
    virtual void OnSendCompleted(IOCP_SocketContext *connSockContext, IOCP_IOContext *ioContext, DWORD dwTxLen) = 0;    

 

  用户层只需要继承这个类,在回调函数中处理自己的业务逻辑就可以了。

   我觉得作者设计中,最出色的地方是在IOCPBase的设计中, 把iocp_key值和IOCP_IOContext定义的非常清晰,让人易于理解,下面是我的一个总结:

  /**************************************************************************************************************
**
**      系统中的SOCKET分为: listen_socket 和 peerSocket
**      每个socket( listen_socket 或 peerSocket)对应一个IOCP_SocketContext
**      每发起一个IO请求,分配一个新的IOCP_IOContext
**      IOCP采用的预分配peerSocket的方式,每Post一个Accept之前, 都预先新分配一个socket【IOCP_IOContext::peerSocket】,所以AcceptEx函数使用的IOCP_IOContext::peerSocket是一个预分配的peerSocket, 不是isten_socket本身

**        所有IOCP_SocketContext共享一个静态成员IOContextPool, IOCP_IOContext位于IOContextPool中
***     通过IOCP_SocketContext统一进行申请,IOCP_SocketContext销毁时,所有分配给该IOCP_SocketContex的IOCP_IOContext归还到共享的IOContextPool中
**        
**        IOContextPool在程序退出时, 通过析构函数中释放所有IOCP_IOContext
**        IOContextPool 的使用,让释放的 IOCP_IOContext得以重复利用,减少的内存的分配释放
**    *************************************************************************************************************/

本文的重点在于, 如何处理那些因为网络原因(掉电,防火墙屏蔽),网络链路事实本身已经不存在的套接字的释放。当然, 最好的办法是增加一个线程, 对每个套接字进行检查,一旦心跳信号超时,就判定为网络资源可以释放了。我也是这样做的, 我使用了一个全局的std::set来记录所有客户端对应的 IOCP_SocketContext, 新建了一个线程对这个全局std::set<IOCP_SocketContext*>进行定期扫描,检查心跳是否超时。

问题来了:  当检测到心跳是否超时, 我如何释放该IOCP_SocketContext对应的资源?

网上有很多建议:

    1)有说在心跳线程使用shutdown优雅的通知客户端关闭, 我方就可以收到客户端close信号

       但问题是本身就是考虑网络上出现的故障, 所以对方的close信号是没指望了。

       还有就是,万一对方故意不执行close呢?所以这个适用于正常的通信操作,通过shutdown保证发出的网络数据完整对到达对方后在关闭套接字。

   2) 在心跳线程中直接close套接字, 那么处于IOPending状态的icop-handle就会被唤醒,GetQueuedCompletionStatus返回FALSE, GetLastError()=64(64,指定的网络名不再可用)

    3) 使用CancIOEx唤醒处于IOPending状态GetLastError()=ERROR_OPERATION_ABORTED

     我使用的是2, 但是问题来了,我在做收发测试时, 则程序老是容易Crash掉。

     反复debug发现,作者的IOCPBase类中, 如果心跳线程中close调server端的socket时,如果有多个PendingIO存在, 就会释放多少次OCP_SocketContext资源,程序Crash问题就在这里了,DoClose函数中出现了OCP_SocketContext资源多次释放的bug;

     为此, 我给OCP_SocketContext增加了个引用计数器, 每次投递PendingIO,就在增加一次,收到一次回调,就减少一个引用计数。当引用计数=0时, 才执行真正的资源释放。

     常规的做法是, 系统中总是保持唯一一个PendingRecv的存在,当 PendingRecv有信号时,不要释放应用计数,而是继续投递一个PendingRecv, 这样PendingRecv始终占用一个应用计数,OCP_SocketContext不会被释放, 当然如果投递失败子计数器减1。

       心跳程序中检查有无数据需要发送, 有就投递一个PendingSend,投递成功则OCP_SocketContext引用计数加1,投递完成后应用计数减1.

     如果采用使用CancIOEx唤醒处于IOPending状态,应该也可以, 只是唤醒后GetLastError()返回的值不通而已;

  前者返回 ERROR_NETNAME_DELETED(64)

   后者返回 ERROR_OPERATION_ABORTED(995)

  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
结构层次及相互联系 (1)、工作线程:响应连接的IO投递返回并负责投递读请求,并将IO返回结果投递给处理线程,可设定参数决定工作线程数量; (2)、处理线程:处理线程调用回调函数将信息传递给应用层或协议栈,可设定参数决定工作处理数量; (3)、看守线程:响应Accept事件调用AcceptEx,检测连接和心跳超时 ,将信息投递给工作线程,模块仅有一个看守线程。 1. 技术要求 (1)、线程同步:Lock指令、临界段; (2)、主要Socket API:WSASend、WSARecv、AcceptEx、DisconnectEx; (3)、内存管理:连接池(句柄重用)、内存池; (4)、数据0拷贝:通过内置处理线程,上层应用可以避免自建线程池及复制数据的过程。同时提供GBuf内存分配功能,应用层获得分配地址及填充数据之后亦可直接投递给内核/驱动层; (5)、数据顺序同步:同一个连接同时只有一个处理线程响应其IO事件; (6)、IO请求投递:单投递读、多投递写; (7)、0缓冲读投递:可条件编译实现,以适用大规模连接要求。 (8)、超时机制:可设置空连接(连接不发送数据)超时时间以防止DOS攻击,也可设置心跳超时时间防止网络故障导致的现有连接成为虚连接避免耗尽系统资源。 (9)、接口技术:API、回调函数、客户句柄(客户连接句柄)。 (10)、主、被动发送:不使用HASH、MAP及LIST技术,即可提供安全可靠高效的客户连接句柄,以实现服务器端主被动发送数据功能; (11)、PerHandleData的回收不以IO投递的计数器或链表来做依据但仍能安全回收,同时尽量避免在高频的读写操作时做其他无关的操作以提高读写效率。 (12)、处理线程和工作线程有着良好分工界限,繁重的工作交给处理线程完成,工作线程工作量最大限度的减少,仅响应投递返回及读投递的操作; (13)、支持AWE,模块自动识别AWE是否开启(需手动开启),“否”则使用虚拟内存机制。 2. 功能要求 (1)、多IP多端口监听,每个监听可设置不同的回调函数,以高效的区别处理数据 (2)、可设置每秒最大的连接并发量和空连接(连接不发数据)超时时间以防止DOS攻击造成的服务瘫痪、具有心跳处理(防网络异常造成的虚连接)功能 (3)、不加协议的透明传输,可适用广泛的网络通讯环境 (4)、可现实主、被动发送数据,但不会因兼顾主动发送而额外增加降低效率的工作 (5)、内置处理线程,上层应用可不必自建线程池处理数据,所有IO事件按顺序调用回调函数并可以在回调函数内直接处理数据,不必担心多线程造成的接收数据乱序的问题。 (6)、高效率的数据对应关联机制,在初次连接并根据登录数据设置每个连接对应的宿主(Owner)之后,再接收的数据即可立即获得该连接对应的宿主,而不必再做额外的查询工作,并且模块内部采用的是指针关联方式,对于长连接、主动发送的服务器系统而言是高效率的。 (7)、可兼容IPv6 3. 注意事项 因硬件环境和应用环境不同,不合理的配置会出现效率及性能上的问题,因此以下情况出现时,请务必与作者联系以确保获得更好的参数配置: (1)、连接量超过1000个的。超过的应结合具体硬件配置和网络带宽等因素综合设定运行参数。 (2)、带宽使用率超过20%的。工作线程和处理线程数量的设置也是综合考虑数据吞吐量和数据处理负载的因素来设置的,过多的线程会在调度上浪费时间,同时也应该综合考虑线程优先级别来设置工作线程和处理线程数量,两者的设置也不一定能相等。 (3)、服务器端有主动发送需求的、短连接(含网络故障造成的连接断开)出现频率高的。 压力测试工具介绍: 一、 使用G-TcpClient模块 二、 可以设定间隔时间发起大规模长、短连接 三、 可以发起密集数据包,包括即时和定时发送,1M的光纤带宽最大可以达到100K/S(单向)以上,100M本地网最大可以达到10M/S(单向)以上 四、 数据发送仅由一个独立线程但当,每点击一次Connect就创建一个线程根据当前参数发起连接。 五、 测试前提:服务器接收客户端数据后立即原样返回给客户端

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值