[转]基于Windows Socket 的网络通信中的心跳机制原理

http://blog.sina.com.cn/s/blog_608740170100wni3.html
引言
在采用TCP 连接的C/S 结构的系统中,当通信的一方正常关闭或退出时,另一方能收到相应的连接
断开的通知,然后进行必要的处理;但如果任意一方发生所谓的“非优雅断开”,如:意外崩溃、死机、
拔掉网线或路由器故障时,另一方无法得知TCP 连接已经失效,除非继续在此连接上不断地发送数据,
经过若干时间后导致错误返回。但在很多时候,更希望服务器端和客户端都能及时有效地检测到网络连
接的非正常断开,然后完成一些必要的清理工作并把错误报告给用户。
如何及时有效地检测到通信一方的非正常断开,采用的方法是通过通信的一方或双方发送心跳包来
告诉对方网络通信是否正常或已断开。
1. 心跳原理
在基于电路交换的网络中,有专用的控制信令通道,能够及时发现通路断开、故障,而TCP/IP 网络
中,链路的连通只在连接双方记录状态,物理通道内不存在一个实际的连接链路,通信的双方只能定时
发送简单的信息给另一方,并根据超时来判断线路是长时间空闲还是已断开。这种通过每隔一定时间发
送一个固定信息给对方,对方收到后回复一个固定信息,告诉对方“我还在”的方式非常类似于心跳,所
发送的这种简单信息就称为“心跳包”。
心跳包的发送,通常有两种技术:一种是由用户在应用层实现的心跳包,另一种是由TCP 协议层提
供的KeepAlive 。
2. 应用层自己实现的心跳包
由应用程序自己发送心跳包来检测连接是否正常,大致的方法是:服务器在一个Timer 事件中定时
向客户端发送一个短小精悍的数据包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应,
如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没
有收到服务器的心跳包,则认为连接不可用。

3. TCP 的KeepAlive 保活机制
因为要考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多
且稍显复杂,而利用TCP/IP 协议层为内置的KeepAlive 功能来实现心跳功能则简单得多。
不论是服务端还是客户端,一方开启KeepAlive 功能后,就会自动在规定时间内向对方发送心跳包,
而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。
因为开启KeepAlive 功能需要消耗额外的宽带和流量,所以TCP 协议层默认并不开启KeepAlive 功
能,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,KeepAlive 设置不合理时可能会
因为短暂的网络波动而断开健康的TCP 连接。并且,默认的KeepAlive 超时需要7,200,000 MilliSeconds,
即2 小时,探测次数为5 次。对于很多服务端应用程序来说,2 小时的空闲时间太长。因此,我们需要手
工开启KeepAlive 功能并设置合理的KeepAlive 参数。

目前主要有三种方法来实现用户掉线检测:SO_KEEPALIVE ,SIO_KEEPALIVE_VALS 和Heart-Beat线程。
下面我就上面的三种方法来做一下介绍。
(1)SO_KEEPALIVE 机制
这是socket库提供的功能,设置接口是setsockopt API:
BOOL bSet=TRUE;
setsockopt(hSocket,SOL_SOCKET,SO_KEEPALIVE,(const char*)&bSet,sizeof(BOOL));
根据MSDN的文档,如果为socket设置了KEEPALIVE选项,TCP/IP栈在检测到对方掉线后,
任何在该socket上进行的调用(发送/接受调用)就会立刻返回,错误号是WSAENETRESET ;
同时,此后的任何在该socket句柄的调用会立刻失败,并返回WSAENOTCONN错误。
该机制的缺点也很明显:
默认设置是空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测!
当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口!
关联了完成端口的socket可能会忽略掉该套接字选项。

(2)SIO_KEEPALIVE_VALS 机制
这是从彭博兄那里学到一个机制拉,设置接口是WSAIoctl API:
DWORD dwError = 0L ;
tcp_keepalive sKA_Settings = {0}, sReturned = {0} ;
sKA_Settings.onoff = 1 ;
sKA_Settings.keepalivetime = 5500 ; // Keep Alive in 5.5 sec.
sKA_Settings.keepaliveinterval = 3000 ; // Resend if No-Reply
if (WSAIoctl(skNewConnection, SIO_KEEPALIVE_VALS, &sKA_Settings,
sizeof(sKA_Settings), &sReturned, sizeof(sReturned), &dwBytes,
NULL, NULL) != 0)
{
dwError = WSAGetLastError() ;
}
实现时需要添加tcp_keepalive and SIO_KEEPALIVE_VALS的定义文件MSTCPiP.h
该选项不同于SO_KEEPALIVE 机制的就是它是针对单个连接的,对系统其他的套接
口并不影响。
针对完成端口的socket,设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。
当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作
进行后,马上就会返回错误告诉上层这个连接已经断开了.如果检测到断开的时候,在这个连接
上有正在PENDING的IO操作,则马上会失败返回.

该机制的缺点:
不通用啦。MS的API只能用于Windows拉。不过,呵呵用彭博兄的评论就是:
优雅一些^_^.

(3)Heart-Beat线程
没说的。自己写一个后台线程,实现Heart-Beat包,客户端受到该包后,立刻返回相应的反馈 包。
该方法的好处是通用,但缺点就是会改变现有的通讯协议!
有开发网络应用经历的人都知道,网络中的接收和发送数据都是使用WINDOWS中的SOCKET进行实现。但是如果此套接字已经断开,那发送数据和接收数据的时候就一定会有问题。可是如何判断这个套接字是否还可以使用呢?
  有人一定想到使用Send函数中的返回结果来进行判断。如果返回的长度和自己发送出去的长度一致,那就说明这个套接字是可用的,否则此套接字一定出现了问题。但是我们并不是无时无刻的发送数据呀。如何解决呢?
  其实TCP中已经为我们实现了一个叫做心跳的机制。如果你设置了心跳,那TCP就会在一定的时间(比如你设置的是3秒钟)内发送你设置的次数的心跳(比如说2次),并且此信息不会影响你自己定义的协议。
  在VC中实现心跳的例子很多,可是在DLEPHI中一直没有相应的代码。下面我是我使用DELPHI编写的关于心跳的代码(以IOCP为例),希望对大家有帮助。
定义心跳常量
const
IOC_IN =$80000000;
IOC_VENDOR =$18000000;
IOC_out =$40000000;
SIO_KEEPALIVE_VALS =IOC_IN or IOC_VENDOR or 4;
var
inKeepAlive,OutKeepAlive:TTCP_KEEPALIVE;
实现代码是在Acceptsc:= WSAAccept(Listensc, nil, nil, nil, 0);代码的后面加入:
opt:=1;
if setsockopt(Acceptsc,SOL_SOCKET,SO_KEEPALIVE,@opt,sizeof(opt))=SOCKET_ERROR then
begin
closesocket(Acceptsc);
end;
inKeepAlive.onoff:=1;
//设置3秒钟时间间隔
  inKeepAlive.keepalivetime:=3000;
//设置每3秒中发送1次的心跳
inKeepAlive.keepaliveinterval:=1;
insize:=sizeof(TTCP_KEEPALIVE);
outsize:=sizeof(TTCP_KEEPALIVE);
if WSAIoctl(Accept,SIO_KEEPALIVE_VALS,@inKeepAlive,insize,@outKeepAlive,outsize,@outByte,nil,nil)=SOCKET_ERROR then
begin
closesocket(Acceptsc);
end;
如果加入以上的代码以后,系统会每3秒中加入一次的心跳。并且如果客户端断线以后(网线断),函数GetQueuedCompletionStatus会返回FALSE。
if (GetQueuedCompletionStatus(CompletionPort, BytesTransferred,DWORD(PerHandleData), POverlapped(PerIoData), INFINITE) = False) then
begin
//在这里处理客户端断线信息。
   continue;
end;
以上就是我使用心跳的方法,此方法我已经在我的网络游戏中使用。情况稳定!
网络程序容易出现死连接,连接以经无效了,但是连接状态还是ESTABLISHED状态,如何识别连接以经失效就需要使用


------------------------------------------------------------------------------------------------------
下面是方法2的一段可用代码(经过僧僧鞋子 测试 可用!!!)
---------------------------------------------------------------------------------------
保活机制(心跳机制)下面是一个代码效果还是挺好的,感谢cooldiyer的资料

typedef struct STcpKeepAlive2   
{
DWORD onoff;
DWORD keepalivetime;
DWORD keepaliveinterval;

}TCP_KEEP_ALIVE2;

VOID SetKeepLive(SOCKET m_Socket)
{
const char chOpt = 1; // True
// Set KeepAlive 开启保活机制, 防止服务端产生死连接
if (setsockopt(m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) == 0)
{
// 设置超时详细信息
TCP_KEEP_ALIVE2 klive;
klive.onoff = 1; // 启用保活
klive.keepalivetime = 1000 * 15; // 3分钟超时 Keep Alive
klive.keepaliveinterval = 1000 * 5; // 重试间隔为5秒 Resend if No-Reply
WSAIoctl
(
m_Socket,
SIO_KEEPALIVE_VALS,
&klive,
sizeof(TCP_KEEP_ALIVE2),
NULL,
0,
(unsigned long *)&chOpt,
0,
NULL
);
}
}

其中SIO_KEEPALIVE_VALS的值定义为: const int SIO_KEEPALIVE_VALS =IOC_IN | IOC_VENDOR | 4;

注:SIO_KEEPALIVE_VALS 只有在winsock2下才可使用

#include <winsock2.h>
#include <stdio.h>
#include <iostream.h>

SOCKET ser;
SOCKET client;
SOCKADDR_IN clientaddr;
int len=sizeof(SOCKADDR);
char buff[1024];
DWORD WINAPI Fun1(LPVOID lpParameter);
void main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD( 2, 2 );

err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return;
}

if ( LOBYTE( wsaData.wVersion ) != 2 ||
HIBYTE( wsaData.wVersion ) != 2 ) {
WSACleanup( );
return;
}

ser=socket(AF_INET,SOCK_STREAM,0);
int TimeOut=5000;//设置接收超时6秒
if(::setsockopt(ser,SOL_SOCKET,SO_RCVTIMEO,(char *)&TimeOut,sizeof(TimeOut))==SOCKET_ERROR)
{
return ;
}

SOCKADDR_IN seraddr;
seraddr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
seraddr.sin_family=AF_INET;
seraddr.sin_port=htons(12345);
bind(ser,(SOCKADDR*)&seraddr,sizeof(SOCKADDR));

listen(ser,5);
while (1)
{
stop:client=accept(ser,(SOCKADDR*)&clientaddr,&len);

DWORD lngPassTime=0;
while(lngPassTime<=2500)
{
DWORD lngStartTime=GetTickCount();
if(recv(client,buff,1024,0)==SOCKET_ERROR)
{
cout<<WSAGetLastError()<<endl;
break;
}
else
cout<<buff<<endl;
lngPassTime=GetTickCount()-lngStartTime;

send(client,"收到!",strlen("收到")+1,0);
}
closesocket(client);
closesocket(client);
goto stop;
}
return;
}


用vc怎么发个心跳包来检测对方是否掉线
2009-04-03 15:02
//设置KeepAlive
BOOL bKeepAlive = TRUE;
nRet = ::setsockopt(m_sockDesc, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(bKeepAlive));
if (nRet !=0)
{
sprintf(m_pszError, "Winsock error : %s (Error Code %d)\n ", "Socket SetOpt failed ", WSAGetLastError());
return FALSE;
}

//设置KeepAlive检测时间和次数
TCP_KEEPALIVE inKeepAlive = {0}; //输入参数
unsigned long ulInLen = sizeof(TCP_KEEPALIVE);

TCP_KEEPALIVE outKeepAlive = {0}; //输出参数
unsigned long ulOutLen = sizeof(TCP_KEEPALIVE);

unsigned long ulBytesReturn = 0;

//设置socket的keep alive为10秒,并且发送次数为3次
inKeepAlive.onoff = 1;
inKeepAlive.keepaliveinterval = 10000; //两次KeepAlive探测间的时间间隔
inKeepAlive.keepalivetime = 3; //开始首次KeepAlive探测前的TCP空闭时间

nRet = WSAIoctl(m_sockDesc,
SIO_KEEPALIVE_VALS,
(LPVOID)&inKeepAlive,
ulInLen,
(LPVOID)&outKeepAlive,
ulOutLen,
&ulBytesReturn,
NULL,
NULL);
if(SOCKET_ERROR == nRet)
{
sprintf(m_pszError, "Winsock error : %s (Error Code %d)\n ", "Nonblocking socket call error ", WSAGetLastError());
return FALSE;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值