TCP 是面向连接的 , 在实际应用中通常都需要检测对端是否还处于连接中。如果已断开连接,主要分为以下几种情况:
1.连接的对端正常关闭,即使用 closesocket 关闭连接。
2.连接的对端非正常关闭,包括对端异常关闭,网络断开(待机),掉电等情况。
对于第一种情况,很好判断,但是对于第二种情况,可能会要麻烦一些。在网上找到了一些文章,大致有以下两种解决方法:
1.自己编写心跳包程序
简单的说也就是在自己的程序中加入一条线程,定时向对端发送数据包,查看是否有 ACK ,如果有则连接正常,没有的话则连接断开。
2.使用 TCP 的 keepalive 机制
这个需要在 WinSock 编程时对当前 SOCKET 进行相应设置即可,比较方便。
为了方便起见,我这里采用 keepalive 机制,下面我就以 WinSock 上我实验得到的结果来大致讲一下其机理和使用方法。
首先说一下 keepalive 来判断异常断开的原理,其实 keepalive 的原理就是 TCP 内嵌的一个心跳包。
以服务器端为例,如果当前 server 端检测到超过一定时间(默认是 7,200,000 milliseconds ,也就是 2 个小时)没有数据传输,那么会 向client 端发送一个 keep-alive packet (该 keep-alive packet 就是 ACK 和当前TCP 序列号减一的组合),此时 client 端应该为以下三种情况之一:
1. client 端仍然存在,网络连接状况良好。此时 client 端会返回一个 ACK 。 server 端接收到 ACK 后重置计时器,在 2 小时后再发送探测。如果 2 小时内连接上有数据传输,那么在该时间基础上向后推延 2 个小时。
2. 客户端掉电,或是网络断开。在这两种情况下, client 端都不会响应。服务器没有收到对其发出探测的响应,并且在一定时间(系统默认为 1000 ms )后重复发送 keep-alive packet ,并且重复发送一定次数(2000 XP 2003 系统默认为 5 次 , Vista 后的系统默认为 10 次)。
3. 客户端崩溃。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。
了解了 keep alive 大致的原理,下来看看在程序中怎么用,怎么设置参数:
- #include <mstcpip.h>
- BOOL bKeepAlive = TRUE;
- int nRet = setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE,(char*)&bKeepAlive, sizeof(bKeepAlive));
- if (nRet == SOCKET_ERROR)
- {
- TRACE(L"setsockopt failed: %d/n", WSAGetLastError());
- return FALSE;
- }
- // set KeepAlive parameter
- tcp_keepalive alive_in;
- tcp_keepalive alive_out;
- alive_in.keepalivetime = 500; // 0.5s
- alive_in.keepaliveinterval = 1000; //1s
- alive_in.onoff = TRUE;
- unsigned long ulBytesReturn = 0;
- nRet = WSAIoctl(sock, SIO_KEEPALIVE_VALS, &alive_in, sizeof(alive_in),&alive_out, sizeof(alive_out), &ulBytesReturn, NULL, NULL);
- if (nRet == SOCKET_ERROR)
- {
- TRACE(L"WSAIoctl failed: %d/n", WSAGetLastError());
- return FALSE;
- }
对于实用程序来说,2小时的空闲时间太长。因此,我们需要手工开启Keepalive功能并设置合理的Keepalive参数。在XP和WIN2003系统上,可以针对单独的socket来设置,但是在windows 2000,不能单独设置,如果设置,那么影响是整个系统的所有socket。
struct tcp_keepalive
{
ULONG onoff ; // 是否开启 keepalive
ULONG keepalivetime ; // 多长时间( ms )没有数据就开始 send 心跳包
ULONG keepaliveinterval ; // 每隔多长时间( ms ) send 一个心跳包, 发 5 次 (2000 XP 2003 默认 ), 10 次 (Vista 后系统默认 )
};
此处的keepalivetime表示的是TCP连接处于畅通时候的探测频率,一旦探测包没有返回,就以keepaliveinterval的频率发送。
对于Win2K/XP/2003,可以从下面的注册表项找到影响整个系统所有连接的keepalive参数:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
“KeepAliveTime”=dword:006ddd00
“KeepAliveInterval”=dword:000003e8
“MaxDataRetries”=”5″
设置好 keepalive 以后,我们通过实验来看看当 client 异常退出或是网络断掉的情况下, keepalive 怎么通知我们异常断开的情况。这里采用 select 模式,实验环境为 XP 系统和 Win7 系统,几种情况返回值如下:
1. 正常断开
select 函数正常返回, recv 函数返回 0
2. 异常断开
a)程序异常退出,如 client 端重启,应用非正常关闭等
select 函数正常返回, recv 函数返回 SOCKET_ERROR , WSAGetLastError () 得到的结果为WSAECONNRESET(10054) 。
b)网络断开,电脑掉电
结果同上: select 函数正常返回, recv 函数返回 SOCKET_ERROR , WSAGetLastError() 得到的结果为WSAECONNRESET(10054) 。
P.S. 网上有些文章中写的 WSAGetLastError() 得到的结果为 ETIMEDOUT ,我这里不太清楚为什么和我这里得到的不太一样。
另外,在实验中,我发现了一个和以前理解的不太相同的地方,在这里也记录下来:
对于程序异常退出的情况(这里所说的异常退出包括程序异常关闭、崩溃重启等情况,但不包括系统待机休眠),实际上在不开启 keepalive 的情况下也是可以检测到的 ,我这里测试得到在不开启 keepalive 的情况下,异常关闭 client 端程序, server 端 recv 函数会立即返回 SOCKET_ERROR , last error 同样 为WSAECONNRESET 。但是对于网络断开及系统待机休眠的情况,则必须设置 keepalive 才能检测到 ,并且当网络重新连接或者系统恢复后,SOCKET连接并不能恢复。
具体原因我这里也不太清楚,看到有一篇文章是这样写的:“异常关闭下, SOCKET 虚拟通路会被重设,远端正在接受的调用就都会失败”。不知道正确与否,感觉有一定的道理,暂时记录下来。