如果TCP连接被对方正常关闭,也就是说,对方是正确地调用了close或者shutdown的话,那么Recv或Send调用就能马上返回,并且报错。这是由于close或者shutdown有个正常的关闭过程,会告诉对方“TCP连接已经关闭,你不需要再发送或者接受消息了”。
但是,如果连接被意外断开,客户端并没有正常关闭socket。双方并未按照协议上的四次挥手去断开连接。那么这时候正在执行Recv或Send操作的一方就会因为没有任何连接中断的通知而一直等待下去,也就是会被长时间卡住。像这种如果一方已经关闭或异常终止连接,而另一方却不知道,我们将这样的TCP连接称为半打开的。
解决意外中断办法都是利用保活机制。而保活机制分又可以让底层实现也可自己实现。
一种是应用层的心跳机制;
还有一种就是启用TCP的keepAlive机制;
以服务器端为例,如果当前server端检测到超过一定时间没有数据传输,那么会向client端发送一个keep-alive packet(该keep-alive packet就是ACK和当前TCP序列号减一的组合),此时client端应该为以下三种情况之一:
1. client端仍然存在,网络连接状况良好。此时client端会返回一个ACK。server端接收到ACK后重置计时器(复位存活定时器),在一定时间后再发送探测。如果连接上一直有数据传输,那么在该时间基础上向后推延一段时间。
2. 客户端异常关闭,或是网络断开。在这两种情况下,client端都不会响应。服务器没有收到对其发出探测的响应,并且在一定时间(系统默认为1000 ms)后重复发送keep-alive packet,并且重复发送一定次数。
3. 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位(RST),从而引起服务器对连接的终止。
在应用层socket编程的表现为:当tcp检测到对端socket不再可用时(不能发出探测包,或探测包没有收到ACK的响应包),select/epoll会返回socket可读,并且在recv时返回-1,同时置上errno为ETIMEDOUT。
范例:
- int keepAlive = 1; // 开启keepalive属性
- int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测
- int keepInterval = 5; // 探测时发包的时间间隔为5 秒
- int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发.
- setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive));
- setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle));
- setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval));
- setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));