[转]基于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; 
}

猜你喜欢

转载自jacky-dai.iteye.com/blog/1471747
今日推荐