WindowsSocket心跳实现

0   引言 
在采用 TCP连接的 C/S结构的系统中,当通信的一方正常关闭或退出时,另一方能收到相应的连接
断开的通知,然后进行必要的处理;但如果任意一方发生所谓的“非优雅断开”,如:意外崩溃、死机、
拔掉网线或路由器故障时,另一方无法得知 TCP 连接已经失效,除非继续在此连接上不断地发送数据,
经过若干时间后导致错误返回。但在很多时候,更希望服务器端和客户端都能及时有效地检测到网络连
接的非正常断开,然后完成一些必要的清理工作并把错误报告给用户。 
如何及时有效地检测到通信一方的非正常断开,采用的方法是通过通信的一方或双方发送心跳包来
告诉对方网络通信是否正常或已断开。 
1   心跳原理 
在基于电路交换的网络中,有专用的控制信令通道,能够及时发现通路断开、故障,而 TCP/IP网络
中,链路的连通只在连接双方记录状态,物理通道内不存在一个实际的连接链路,通信的双方只能定时
发送简单的信息给另一方,并根据超时来判断线路是长时间空闲还是已断开。这种通过每隔一定时间发
送一个固定信息给对方,对方收到后回复一个固定信息,告诉对方“我还在”的方式非常类似于心跳,所
发送的这种简单信息就称为“心跳包”。 
心跳包的发送,通常有两种技术:一种是由用户在应用层实现的心跳包,另一种是由 TCP 协议层提
供的 KeepAlive  。  
基于Windows Socket的网络通信中的心跳机制原理及其实现 
 
18 
2   应用层自己实现的心跳包 
由应用程序自己发送心跳包来检测连接是否正常,大致的方法是:服务器在一个 Timer 事件中定时
向客户端发送一个短小精悍的数据包,然后启动一个低级别的线程,在该线程中不断检测客户端的回应,
如果在一定时间内没有收到客户端的回应,即认为客户端已经掉线;同样,如果客户端在一定时间内没
有收到服务器的心跳包,则认为连接不可用。 
以下代码给出在 Delphi 中使用 ServerSocket、ClientSocket 进行网络通信时,如何在服务端实现应用
层心跳包: 
//定义一个 SocketData 记录类型,用于保存客户端信息 
Type SocketData = Record 
    IP: string;             //客户端 IP 
    StartTime: Cardinal;    //每次向客户端发送心跳包的当前时间 
    IsConnected: Boolean;   //是否正与服务端保持连接 
  end; 
  PSocketData = ^SocketData;  //定义一个指向 SocketData 的指针类型  
 
//保存客户端信息,并创建线程,检测客户端是否有回应 
procedure TForm1.ServerSktClientConnect(Sender: TObject; 
  Socket: TCustomWinSocket); 
var P: PSocketData; 
begin 
  New(p); 
  p.IP := Socket.RemoteAddress; 
  p.IsConnected := true; 
  Socket.Data := p;               
  if not Timer1.Enabled then 
  begin 
    MyThread := TCheckTimeOut.Create(ServerSkt, Panel1);  
    Timer1.Enabled := true; 
  end; 
end; 
 
//在定时器的 Timer事件中,每隔 2 秒向所有客户端发送一次心跳包 
procedure TForm1.Timer1Timer(Sender: TObject); 
var 
  i, ActConns: integer; 
  CSocket: TCustomWinSocket; 
begin 
  ActConns := ServerSkt.Socket.ActiveConnections; 
  caption := '连接数:' + inttostr(ActConns); 
  for i := 0 to pred(ActConns) do       
  begin  
基于Windows Socket的网络通信中的心跳机制原理及其实现 
 
19 
    CSocket := ServerSkt.Socket.Connections[i]; 
    CSocket.SendText('Msgtest'); 
    if PSocketData(CSocket.Data).IsConnected then 
    begin 
      PSocketData(CSocket.Data).StartTime := GetTickCount; 
      PSocketData(CSocket.Data).IsConnected := false; 
    end; 
  end; 
end; 
 
//如果收到客户端的特定回应,由表示该客户处于连接状态 
procedure TForm1.ServerSktClientRead(Sender: TObject; 
  Socket: TCustomWinSocket); 
var RecTxt: string; 
begin 
  RecTxt := Socket.ReceiveText; 
  if RecTxt = 'OK' then          
  begin 
    PSocketData(Socket.Data).IsConnected := true; 
    Memo1.Lines.Add(RecTxt); 
  end; 
end; 
 
//线程入口,用于检测客户端是否在规定时间内向服务端回应 
procedure TCheckTimeOut.Execute; 
begin 
  while true do 
  begin 
    Synchronize(CheckConnect); 
    if terminated then exit; 
  end; 
end; 
 
//检查所有客户端,是否在 5 000(ms)内向服务端发送回应信息 
procedure TCheckTimeOut.CheckConnect; 
var i: integer; 
begin 
  for i := 0 to FServerSocket.Socket.ActiveConnections - 1 do 
    if not PSocketData(FServerSocket.Socket.Connections[i].Data).IsConnected then 
      if (GetTickCount - PSocketData(FServerSocket.Socket.Connections[i].Data)^.StartTime) > 5 000 
then 
      begin  
基于Windows Socket的网络通信中的心跳机制原理及其实现 
 
20 
        Dispose(PSocketData(FServerSocket.Socket.Connections[i].Data)); 
        FServerSocket.Socket.Connections[i].Close; 
      end; 
end; 
 
对于客户端而言,只要在收到服务端的心跳包后,简单地发送一个回应信息即可,代码略。 
3  TCP的 KeepAlive  保活机制 
因为要考虑到一个服务器通常会连接多个客户端,因此由用户在应用层自己实现心跳包,代码较多
且稍显复杂,而利用 TCP/IP 协议层为内置的 KeepAlive 功能来实现心跳功能则简单得多。 
不论是服务端还是客户端,一方开启 KeepAlive功能后,就会自动在规定时间内向对方发送心跳包,
而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。 
因为开启 KeepAlive 功能需要消耗额外的宽带和流量,所以 TCP 协议层默认并不开启 KeepAlive 功
能,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,KeepAlive 设置不合理时可能会
因为短暂的网络波动而断开健康的 TCP连接。并且,默认的 KeepAlive超时需要 7,200,000 MilliSeconds,
即 2 小时,探测次数为 5 次。对于很多服务端应用程序来说,2 小时的空闲时间太长。因此,我们需要手
工开启 KeepAlive 功能并设置合理的 KeepAlive 参数。 
以下代码给出在 Delphi 中使用 ServerSocket、ClientSocket 进行网络通信时,如何在服务端通过添加
KeepAlive 功能来自动实现心跳机制。 
//定义心跳常量 
Const  
  IOC_IN = $80000000; 
  IOC_VENDOR = $18000000; 
  IOC_out = $40000000; 
  SIO_KEEPALIVE_VALS = IOC_IN or IOC_VENDOR or 4; 
  DATA_BUFSIZE = 8192; 
 
//定义 KeepAlive 数据结构 
Type 
  TTCP_KEEPALIVE = packed record 
    onoff: integer; 
    keepalivetime: integer; 
    keepaliveinterval: integer; 
  end; 
 
//  开启 KeepAlive 保活机制,每隔 3 秒向客户端发送一次心跳包 
procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); 
var 
  opt: integer; 
  klive, outKlive: TTCP_KEEPALIVE; 
begin 
  opt := 1;  

21 
    if setsockopt(Socket.SocketHandle,SOL_SOCKET, SO_KEEPALIVE, @opt, SizeOf(opt)) <> 0 then 
    begin 
      Showmessage('setsockopt KeepAlive Error!'); 
    end; 
    klive.onoff := 1;  
    klive.keepalivetime := 3000; 
    klive.keepaliveinterval := 1; 
if WSAIoctl( Socket.SocketHandle, SIO_KEEPALIVE_VALS, @klive, 
             SizeOf(TTCP_KEEPALIVE), @outKlive, 
             SizeOf(TTCP_KEEPALIVE), @opt,0,nil) = SOCKET_ERROR then 
    begin 
      Showmessage('WSAIoctl KeepAlive Error!'); 
    end; 
end; 
 
其中,Windows Socket API函数 Setsockopt 用于设置套接口的选项,而此处则用来开启 KeepAlive功
能,WSAIoctl 函数则用于设置 KeepAlive 超时及心跳包发送次数。 
由于是在 ServerSocket 的 OnClientConnect 事件中使用 Socket 参数来设置每个连接上来的客户端的
KeepAlive,所以上述代码同样支持多客户连接的情况。 
在开启了 KeepAlive  后, 一旦客户端死机、 拔网线等“非优雅”退出, 就会触发服务端的 OnClientError
事件,然后在此事件中完成必要的“善后”处理工作: 
procedure TForm1.ServerSocket1ClientError(Sender: TObject;Socket: TCustomWinSocket; ErrorEvent: 
TErrorEvent;  var ErrorCode: Integer); 
begin 
  ErrorCode:=0; 
  Showmessage('客户端 '+Socket.RemoteAddress+'非正常退出,断开连接'); 
  …………        //“善后”处理工作  
  Socket.Close; 
end; 
4   结束语 
实践证明,利用 TCP 本身支持的 KeepAlive 功能实现断线检测,比用户自己在应用层实现检测更方
便有效,而且探测时带宽消耗很小。在没有数据传输时依赖 KeepAlive 确保断线检测,在传输数据时 TCP
会通过超时判断是否断线。 
在网络通信应用系统开发中,根据实际需要也可以在客户端开启 KeepAlive,对服务端的非正常断开
进行及时有效地检测。 

posted @ 2012-08-06 19:27  qimi  阅读(2208)  评论(0编辑  收藏  举报