IOCP完全开发经验总结(二):几个重要问题分析(中)

7 篇文章 1 订阅
6 篇文章 0 订阅
本文详细介绍了TCP连接的四种断开类型及其处理方式,包括客户端主动断开、异常断开、服务器主动断开和网络问题断开。强调了心跳机制在网络问题断开时的重要性,以及资源释放和重用可能导致的问题。此外,还提到了包结构定义以应对数据粘包问题,确保数据正确传输。
摘要由CSDN通过智能技术生成

优雅的处理连接断开

据我目前遇到的断开类型共有4种:客户端主动断开、客户端异常断开、服务器主动断开和网络出现问题断开。只要系统检测到连接断开后,你在这个socket上投递的所有IOContext都会从队列中返回,只是返回值会不同。

1、客户端主动断开

一般是客户端调用closesocket函数,这种断开服务器会收到断开的标志,所以服务器上处理很简单:每个你在此socket上投递的IOContext都会从GetQueuedCompletionStatus返回,且函数本身返回TRUE,你传递的dwBytesTransfered会设置为0。

2、客户端异常断开

一般是客户端异常退出、或者进程被杀导致的断开,但由于网络是畅通的,所以服务端仍然能收到断开信号,只是和客户端主动断开不同的是GetQueuedCompletionStatus会返回FALSE,GetLastError函数会返回 ERROR_NETNAME_DELETED 错误。

3、服务器主动断开

服务器在调用closesocket后导致的断开,和客户端异常断开不同的是,GetQueuedCompletionStatus会返回FALSE,GetLastError函数会返回 ERROR_CONNECTION_ABORTED 错误。

4、网络问题断开

这个就比较麻烦了,因为这种断开服务端是无法检测到的(比如网络断开、网络切换等无法发送断开信号时),这种断开检测只能使用心跳机制(服务端发送数据后客户端肯定要有回应,如果谁也不发数据,连接就会一直存在),我看过TCP协议,协议本身是有心跳机制的,可惜windows下好像并没有实现,我自己在网上搜过(setsockopt设置SO_RCVTIMEO,KEEPALIVE),不起任何作用,大家也不用浪费时间了,自己实现一个心跳吧,也不是很难,每过一段时间发送一个字节就行,只要是网络断开了,一般经过15到30秒服务端就能检测到,GetQueuedCompletionStatus会返回FALSE,GetLastError函数会返回 ERROR_SEM_TIMEOUT (信号灯超时)错误。

5、如何处理

这里要注意小猪的IOCP源码有个错误,因为所有投递到此socket的IOContext都会返回(包括RecvIOContext和SendIOContext),小猪源码里把关闭时所有的IOContext所对应的SocketContext都会回收一次,资源重复释放,直接挂掉。所以,我们只需要在RecvContext里释放对应的SocketContext资源即可,如果判断是SendIOContext,就不能再处理了。对了,别忘了释放掉自身IOContext资源。
还有一点非常注意,不管什么类型的断开,服务端必须要调用一次closesocket,否则资源会一直占用(亲测)!
在我的源码里处理断开有点麻烦,除了要注意上面的以外,还有一点:
GetQueuedCompletionStatus返回的RecvIOContext,只是会调用closesocket且给业务工作线程发送一个disconnect事件,并不会释放SocketContext资源,这是因为我自己的业务里,SocketContext还保存了一个客户数据指针,所以业务处理完毕前不能释放,只有业务线程收到disconnect事件后,再去释放SocketContext资源。否则,会产生野指针问题。

如何标志每一个连接(SocketContext)

这个问题也是非常容易忽略,出现概率也比较小,但是不能不注意。
如果你的工程中,处理各种IO和SocketContext的线程和业务线程是同一个,且其他线程不会使用某个SocketContext时,则不用担心这个问题,因为你永远都不会给一个断开的socket发送信息(正常情况下),但如果不是同一个,则要注意了:上面说过一点,断开的socket会一直占用系统资源(包括socket句柄),你调用closesocket后才会释放掉,且会被系统重用!!!重用!!!重用!!!问题就产生于这里,据我测试重用的概率是非常高的(有一次居然隔了三四个socket就会重用之前的句柄),举个例子比较容易懂:假设你的业务线程很慢,有一个客户断开了,处理客户端开的线程非常快速的检测到并closesocket掉了,然后又新连接进来N个socket,正好占用了前面close掉的socket句柄,这时你的某个业务线程才开始处理之前客户的一些业务代码,处理完毕后会给此Socket发送结果。但此socket已经非彼socket了,你给一个无效的socket发送数据还好,系统顶多给你个ERROR,你给一个陌生的socket发送陌生的数据,又是一个大失误,问题你还不知道错误在哪里,根本无法找到!
所以解决这个问题的方法是,你给每个SocketContext设置一个类似Cookie一样的东西,一般会想到GUID,但我的项目里用了一个自增uint类型的值,这也够用了,每次业务线程发送数据时只要携带这个唯一的Cookie,而IOCP进行Send时对队列中已存在的SocketContext的Cookie进行比较,一致时发送即可。

粘包处理–包结构定义

这个问题其实不难,因为TCP是基于流的,系统收到的数据有可能经过优化,几个包的数据粘在一起,或者一个包分了几次才收到,所以要自己定义一个数据结构。我没有把粘包处理代码放在IOCP核心代码里,这样大家可以根据自己的喜好去定义包的格式。大家可以参考我定义的格式:

包识别码 | 包Flag | 包数据长度 | 包数据

包识别码:只有匹配到这个识别码,才表示这是个有效包。
包Flag:相当于包的类型,我自己定义了几种:系统包、加密包、通信包、房间处理包、业务包等等。
包数据长度:保存了后面包的数据长度。
包数据:具体的数据(可能是加密的)。
有了这个结构体,自然就很容易把粘包分开了。只是具体写代码时,要注意跳过无效的数据,还要注意包实际数据长度不够时,要把数据保存至缓冲区等待下一次数据的到来。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值