分类: 嵌入式
2015-05-07 18:00:49
TCP是一种面向连接的协议,连接的建立和断开需要通过收发相应的分节来实现。某些时候,由于网络的故障或是一方主机的突然崩溃而另一方无法检测到,以致始终保持着不存在的连接。下面介绍一种方法来检测这种异常断开的情况
1) 在TCP协议中提供了KEEPALIVE检测。该选项使能后,在一个TCP连接上,若指定的一段时间内没有数据交换,则自动发送分节等待对方确认。
SO_KEEPALIVE
:
该选项设置是否打开探测
TCP_KEEPIDLE
:
开始发送探测分节前等待的空闲时间
TCP_KEEPINTVL:
两次发送探测分节的时间间隔
TCP_KEEPCNT:
判定断开前发送探测分节的次数
2) 设定探测相关选项值
int
keepalive =
1; //
打开探测
int
keepidle =
60; //
开始探测前的空闲等待时间
int
keepintvl =
10; //
发送探测分节的时间间隔
int
keepcnt =
3; //
发送探测分节的次数
3) 设置套接字的属性
if
(setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepalive, sizeof
(keepalive) <
0)
{
perror(“fail
to set
SO_KEEPALIVE”);
exit(-1);
}
if
(setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, (void *) &keepidle, sizeof
(keepidle) <
0)
{
perror(“fail
to set
SO_KEEPIDLE”);
exit(-1);
}
if
(setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, (void *)&keepintvl, sizeof
(keepintvl) <
0)
{
perror(“fail
to set
SO_KEEPINTVL”);
exit(-1);
}
if
(setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, (void *)&keepcnt, sizeof (keepcnt)
<
0)
{
perror(“fail
to set
SO_KEEPALIVE”);
exit(-1);
}
一旦打开KEEPALIVE探测,当TCP连接异常断开后,对sockfd进行recv操作会返回-1,并且errno的值为ETIMEDOUT。
这样一来就可以很方便的在应用程序中检测TCP连接的情况,如果检测到异常断开最简单的处理就是关闭连接。
==========================================================
TCP连接中可能出现的异常断开情况工作
2010-05-19
10:31:21 阅读180
评论0 字号:大中小订阅
1.TCP连接中可能出现的异常断开情况
假设存在这样一种情况:在两个不同的主机Machine1、Machine2系统上分别运行两个应用程序Application1、Application2,在Application1与Application2的进程中存在一个TCP链接TCPLink。它们的实际传输取决于物理链路的沟通PhysiLink。
图一:TCP通信情况模拟图
1.1程序/进程异常
如果TCPLink异常而Application1正常,TCPLink会被关掉并且告诉Application2,Application2也就关闭了该异常的TCPLink。这种情况会在TCPLink异常后的一次Socket调用中通过返回值(C/C++)或者异常代码(C#)得知。因此在做程序开发的时候比较容易处理。
1.2物理链路异常
如果出现Machine1或者Machine2任何一个系统死机:假设Machine1系统异常,此时Machine2无法知道此TCP连接的失效,并一直认为连接正常。如果网络硬件故障(如网线拔掉、交换机断电):Machine1与Machine2都无法知道此TCP连接的失效,并一直认为连接正常。
以上这两种情况在编程时会变的非常糟糕,因为TCP连接将一直被认为有效,所有对此TCP
Socket的调用都会正确返回,这显然是错误的。并且这种错误情况通常会持续很久。
2.异常断开情况影响分析
对于程序/进程异常,由于Socket调用中可以得到返回值。因此在做程序开发的时候比较容易处理。
对于物理链路异常,如果Machine1系统异常,如果Application2是FTP之类的服务器程序倒也无妨(一个连接存在时间比较长对它没有多大影响),如果是需要实时知道连接用户状态的即时通讯类服务器或者Application2是客户端则就会产生一系列的问题了。如果Machine1与Machine2都异常,Application1和Application2都会一直等下去,两端需要进行相似的处理。
3.异常断开情况的判断与处理
对于这种情况在MSDN里面是这样处理的,原文如下:
如果您需要确定连接的当前状态,请进行非阻止、零字节的
Send 调用。如果该调用成功返回或引发 WAEWOULDBLOCK 错误代码
(10035),则该套接字仍然处于连接状态;否则,该套接字不再处于连接状态。
但是,在试验中发现,这种处理方法在很多时候根本无效,尤其对发生在物理链路层上的问题,很多情况下无法检测出网络已经异常断开了。
下面探讨一下能够使用的判断与处理方式以及优缺点。
1
2
3
3.1定时发送简单约定帧
一般是服务器程序和客户端程序达成某种协议,客户端定时向服务器发送很小的数据包,即约定的简单帧,来告诉自己的状态,而服务器端程序则需要在每次收到用户的后更新用户超时的时间计数,当用户的时间计数超过指定时间,就可以认为这个用户已经系统异常终止,而终止之间的连接,并转告其他用户。客户端也可以通过接收服务器端返回的小数据包来判断服务器端的状态。
3.2
Ping +
Send/Receive
用Ping命令来判断网络本身状态,即确定物理链路层的状态。同时,用应用程序层的Send和Receive来进行程序/进程异常的判断。通过这两种方式的组合,一般能够正确得到网络的状态。当网络发生故障时,能够准确的定位其状态。
但是,当网络正常、应用程序正常时,对端的操作系统的设置可能会影响上述判断。即,当对端禁止向其发送Ping命令时,我们Ping的结果将始终为不通。考虑这种情况下,该方法存在一定的缺陷。
3.3KeepAlive-Timer
由于在应用层进行判断存在各种困难,那么是否可以考虑使用TCP底层的一些特性呢?通过思考,我想到可以利用TCP底层协议的KeepAlive-Timer进行网络状态的判断。但是需要改造。通过改造后,这将是一个比较可靠的判断方式。这将在下面作为重点单独介绍。
4.KeepAlive-Timer
4
4.1
TCP/IP协议结构以及底层定时器
网络协议通常分不同层次进行开发,每一层分别负责不同的通信功能。一个协议族,比如TCP/IP,是一组不同层次上的多个协议的组合。TCP/IP通常被认为是一个四层协议系统。
图二:TCP/IP协议族的四个层次以及不同层次的协议
上面的每一层分别负责不同的功能。链路层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。它们一起处理传输媒介(如网线)的物理接口细节。网络层,处理分组在网络中的活动。运输层主,要为两台主机上的应用程序提供端到端的通信。在TCP/IP协议族中,有两个互不相同的传输协议:TCP和UDP。TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。应用层,负责处理特定的应用程序细节。
关键点:应用程序位于应用层,TCP协议在运输层,在数据流从运输层传递到链路层的过程中,TCP协议本身的底层实现正是我们要利用的所在。
TCP协议在实现的时候为每条连接建立了七个定时器。按照它们在一条连接生存期内出现的次序,分别为:connection
establishment timer(连接建立定时器)、retransmission timer(重传定时器)、delayed ACK
timer(延迟ACK定时器)、persist timer (持续定时器)、keepalive timer(保活定时器)、FIN_WAIT _ 2
timer、TIME_WAIT timer。
4.2 KeepAlive-Timer (保活定时器)
在《TCP/IP协议详解
卷2:实现》中,这样描述KeepAlive-Tmer:
KeepAlive-Tmer在应用进程选取了Socket的SO_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,保活定时器超时,向对端发送连接探测报文段,强迫对端响应。如果收到了期待的响应,
TCP可确定对端主机工作正常,在该连接再次空闲超过2小时之前,TCP不会再进行保活测试。如果收到的是其他响应,TCP可确定对端主机已重启。如果连续若干次保活测试都未收到响应,TCP就假定对端主机已崩溃,尽管它无法区分是主机故障(例如,系统崩溃而尚未重启),还是连接故障(例如,中间的路由器发生故障或电话线断了)。
4.3KeepAlive-Timer工作机理分析
1
2
3
4
5
5.1
5.2
5.3
5.3.1 保活定时器在2小时空闲后超时
收到一个报文段后,将复位连接的保活定时器,重设为2小时,并清零连接的空闲计数器。如果保活定时器超时(收到最后一个报文段2小时后),并且置位了Socket的保活选项,则TCP将向对端发送连接探测报文段。如果定时器超时,且未置位Socket的保活选项,则TCP将只复位定时器,重设为2小时,不向对端发送连接探测报文段。当然,如果应用进程调用了close,即使连接已空闲了2小时,TCP也不会发送连接探测报文段。
5.3.2进行保活测试
当保活定时器发送连接探测报文后,如果对端无响应,TCP最多以75秒的间隔发送9个连接探测报文段。TCP在确认连接已死亡之前必须发送多个连接探测报文段的一个原因是,对端的响应很可能是不带数据的纯ACK报文段,TCP无法保证此类报文段的可靠传输,因此,连接探测报文段的响应有可能丢失。如果连接总的空闲时间大于或等于2小时加10分钟,连接将被丢弃。从0秒起,每隔75秒连续9次发送连接探测报文段,直至600秒。675秒时(定时器2小时超时后的11.25分钟)连接被丢弃。
图三:保活定时器判断对端是否可达
4.4利用KeepAlive-Timer
6
7
8
9
10
10.1
10.2
10.3
通过上面的分析知道,如果在2个小时没有数据传送,TCP协议会给对端发送一个Keep-Alive数据报,使用的序列号是曾经发出的最后一个报文的最后一个字节的序列号,对端如果收到这个数据,回送一个TCP的ACK,确认这个字节已经收到,这样就知道此连接没有被断开。如果一段时间没有收到对方的响应,会进行重试,每隔75秒探测一次,重试9次后,没有收到回应的话,就会断开这个连接。
但2个小时对于我们的项目来说显然太长了。我们必须缩短这个时间。
我们要做的就是,在TCP认为的空闲2小时到达之前,模拟keepAlive-Timer的数据结构,使其按照我们的要求空闲时间、探测间隔来判断TCP的连接状态。
通过利用Socket类的IOControl()函数可以达到上述的目的:在C#中,其语法为:
public int IOControl ( IOControlCode ioControlCode, byte[]
optionInValue, byte[] optionOutValue
)
其中主要参数的意义如下:
ioControlCode :一个
IOControlCode
值,它指定要执行的低级操作模式的控制代码。
optionInValue
:Byte 类型的数组,包含操作要求的输入数据。
将IOControlCode的值设置为KeepAlive就可以得到对该操作的控制。对于inOptionValues的定义,可以通过查找Wsocket2的文档找到答案:它是一个如下的结构体:
Struct
tcp_keepalive
{
u_long onoff; //是否启用Keep-Alive
u_long
keepalivetime; //多长时间后开始第一次探测(单位:毫秒)
u_long
keepaliveinterval; //探测时间间隔(单位:毫秒)
}
在C#中,将一个tcp_keepalive结构的内容按照顺序写入Byte数组中,然后传递给IOControl函数,我们就可以使用该函数来对网络状态进行准确的判断了。
可以这样使用:在发送数据前,使用IOControl来确保物理层的状态正确,如果不正确,则通过异常捕获来得到断开的信息,然后进行必要的信息显示,主备切换等工作。
===================================
WinSock TCP keepalive的机理及使用
TCP 是面向连接的 , 在实际应用中通常都需要检测对端是否还处于连接中。如果已断开连接,主要分为以下几种情况:
1. 连接的对端正常关闭,即使用 closesocket 关闭连接。
2. 连接的对端非正常关闭,包括对端异常关闭,网络断开等情况。
对于第一种情况,很好判断,但是对于第二种情况,可能会要麻烦一些。在网上找到了一些文章,大致有以下两种解决方法:
? 自己编写心跳包程序
简单的说也就是在自己的程序中加入一条线程,定时向对端发送数据包,查看是否有 ACK ,如果有则连接正常,没有的话则连接断开。
? 使用 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. 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。(这条摘抄自http://www.cppblog.com/zhangyq/archive/2010/02/28/108615.html ,我自己并不太明白)。
了解了 keep alive 大致的原理,下来看看在程序中怎么用,怎么设置参数:
view
plaincopy to clipboardprint?
#include
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;
}
#include
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;
}
其中, setsockopt 设置了 keepalive 模式,但是系统对 keepalive 默认的参数可能不符合我们的要求,比如空闲 2 小时后才探测对端是否活跃,所以 WSAIoctl 函数通过 tcp_keepalive 结构体对这些参数进行了相应设置。 tcp_keepalive 这 个 结构体在 mstcpip.h 头文件中有定义:
struct
tcp_keepalive {
ULONG onoff ; // 是否开启 keepalive
ULONG keepalivetime ; // 多长时间( ms )没有数据就开始 send 心跳包
ULONG keepaliveinterval ; // 每隔多长时间( ms ) send 一个心跳包,
// 发 5 次 (2000 XP 2003 默认 ), 10 次 (Vista 后系统默认 )
};
这个结构体设置了空闲检测时间,及检测时重复发送的间隔时间。详细的可以查询
msdn : 。
按照 msdn 上的说法,这些参数也可以通过在注册表里设置,分别为:
HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\KeepAliveTime
HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\KeepAliveInterval
另外,有些人可能已经发现了, tcp_keepalive 这个结构体中没有对重试次数这个参数的设置,这个参数可以通过注册表来设置,具体位置为:
HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpMaxDataRetransmissions
关于在注册表中设置这几个参数,我在 XP 和 Server2008 系统中都没有找到, msdn 上说貌似只是支持 server 2003 ,我这里没有实验,具体不太清楚。
设置好 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 虚拟通路会被重设,远端正在接受的调用就都会失败”。不知道正确与否,感觉有一定的道理,暂时记录下来。