心跳包机制整理汇总

【背景】

现需要实现这样的功能:有多个客户端连着同一个服务器。服务器和客户端之间需要“互相”知道彼此的连接状态。比如在某一时刻,服务器需要知道当前有多少个客户端正在和其通信;某一个时刻,某个客户端需要知道自己是否和服务器保持连接。如果在某一时刻,一个客户端关闭了,服务端应能及时感觉到;同样,如果服务端被关闭,所有的客户端应能及时感觉到,并作出一些反应。

1.从程序的角度看待TCP掉线

TCP掉线的原因可能多种多样、不一而足,比如,客人的电脑突然断电、OS崩溃、路由器重启、网线接触不良、因为P2P下载软件而导致网络资源短缺、Internet网络的不稳定等等,但是从程序的角度来说,我们可以总结为两种情况:程序能立即感知的掉线和程序不能立即感知的掉线。

程序能立即感知的掉线:也就是说客户端一掉线,服务器端的某个读写对应的TCP连接的线程就会抛出异常,这种情况相对容易处理。

程序不能立即感知的掉线我们都知道,TCP连接的建立,需要经过三次握手;而TCP连接的断开,需要经过四次挥手。掉线通常没什么大不了的,掉就掉了呗,只要四次挥手顺利完成后,服务器和客户端分别做一些善后处理就可以。

       麻烦的事情在于,连接在没有机会完成4次挥手时已经断开了(比如当客人的电脑系统死机,或客人电脑与服务器之间的某处物理网线断开),而服务端以为客户端还正常在线,而客户端也自以为还正常在线。这种程序对现实状态的错误判断有可能引发诸多悲剧。比如,在此情况下,客户端发一个指令给服务器,服务器因为没有收到而一直处于等待指令的状态;而客户端了,以为服务器已经收到了,也就一直处于等待服务端回复的状态。如果程序的其它部分需要依据当前的状态来做后续的操作,那就可能会出问题,因为程序对当前连接状态的判断是错误的。

       毫无疑问,这种对连接状态错误的判断所持续的时间越久,带来可能的危害就越大。当然,如果我们不做任何额外的处理措施,服务器到最后也能感受到客户端的掉线,但是,这个时间可能已经过去了几分钟甚至几十分钟。对于大多数应用来说,这是不可忍受的。 所以,针对这种不能立即感知掉线的情况,我们要做的补救措施,就是帮助程序尽快地获知tcp连接已断开的信息。

       首先,我们可以在Socket上通过Socket.IOControl方法设置KeepAliveValues,来控制底层TCP的保活机制,比如,设定2秒钟检测一次,超过10秒检测失败时抛出异常。

    byte[] inOptionValues = FillKeepAliveStruct(1, 10000, 2000);
    socket.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);

      据我们的经验,这种设定可以解决一部分问题,但是仍然会有一些连接在断开后,远远超过10秒才被感知掉。所以,这个补救措施还是远远不够的。我们还需要在应用层加入我们自己的TCP连接状态检测机制,这种机制就是通常所说的“心跳”。

2."心跳"机制
心跳机制的原理很简单:客户端每隔N秒向服务端发送一个心跳消息,服务端收到心跳消息后,回复同样的心跳消息给客户端。如果服务端或客户端在M秒(M>N)内都没有收到包括心跳消息在内的任何消息,即心跳超时,我们就认为目标TCP连接已经断开了。

由于不同的应用程序对感知TCP掉线的灵敏度不一样,所以,N和M的值就可以设定的不一样。灵敏度要求越高,N和M就要越小;灵敏度要求越低,N和M就可以越大。而要求灵敏度越高,也是有代价的,那就是需要更频繁地发送心跳消息,如果有几千个连接同时频繁地发送心跳消息,那么其所消耗的资源也是不能忽略的。

当然,网络环境(如延迟的大小)的好坏,也对会对N和M的值的设定产生影响,比如,网络延迟较大,那么N与M之间的差值也应该越大(比如,M是N的3倍)。否则,可能会产生误判 -- 即TCP连接没有断开,只是因为网络延迟大才及时没收到心跳消息,我们却认为连接已经断开了。

ESFramework内置了心跳机制,当心跳超时时,服务端会触发IUserManager的SomeOneTimeOuted事件,来通知我们的应用程序。

在服务器端,UserManager通过ESBasic.Threading.Application.HeartBeatChecker来对心跳进行检测,而HeartBeatChecker的SurviveSpanInSecs属性可以用于设置我们所描述的M值。

在客户端,则通过ESPlus.Application.Basic.Passive.HeartBeater来向服务器定时发送心跳消息,而HeartBeater的DetectSpanInSecs属性可以用于设置N值。

当我们在使用Rapid引擎时,Rapid引擎已经将心跳机制的组件为我们组装好了。由于RapidServerEngine和RapidPassiveEngine没有暴露出HeartBeatChecker和HeartBeater,所以,我们不能直接通过HeartBeatChecker和HeartBeater设定M和N的值,但是,RapidServerEngine和RapidPassiveEngine分别提供了HeartbeatTimeoutInSecs属性和HeartBeatSpanInSecs属性来间接地设定M和N。

3.必须关闭掉线的TCP连接
无论是普通掉线(立即感知)还是心跳超时掉线(非立即感知),都需要关闭对应的TCP连接以释放系统资源。

ITcpServerEngine接口提供了CloseOneConnection方法以关闭目标连接。     

  ///<summary>
/// 主动关闭连接,将触发SomeOneDisconnected事件。
///</summary>
void CloseOneConnection(UserAddress adderss, DisconnectedType disconnectedType);

      当普通掉线时,ITcpServerEngine会自动关闭了TCP连接;但是,当心跳超时掉线时,我们需要自己手动关闭对应的连接。幸运的是,ESPlus.Application.Basic空间下的组件会自动帮我们关闭超时掉线的连接。所以,使用Rapid引擎的我们也不用再自己手动关闭超时掉线的TCP连接了。

另外要提醒一点,当TCP连接超时掉线时,使用Rapid引擎的服务端会首先触发IUserManager的SomeOneTimeOuted事件,接着再触发IUserManager的SomeOneDisconnected事件(由于ESPlus调用CloseOneConnection方法时触发)。

4.UDP与"心跳"
前面介绍的都是关于TCP的掉线的问题,下面我们看看UDP。

由于UDP是无连接的协议,所以,当我们在使用ESFramework的UDP引擎的时候,几乎肯定是需要配备心跳机制的,使用心跳消息确认客户端还在线,以保证服务端不会过早释放对应的Session或长期保留已失效的Session。

ESFramework中的心跳机制相关的组件是与协议无关的,所以既可以用于TCP应用,也可用于UDP应用。

在ESFramework 开发手册(04) -- 可靠的P2P 一文中介绍的P2P通道如果是基于UDP的,则ESPlus内部也启动了心跳机制,以保证在基于UDP的P2P通道断开时,ESPlus能尽快感知,并关闭对应的P2P通道。

5.关闭心跳机制
比如,在LAN中进行通信的分布式系统,由于网络延迟和意外掉线的几率微乎其微,所以,可以考虑关闭心跳机制。再比如,当我们断点调试客户端程序时,由于断点时间太久,服务端会判断为客户端已经心跳超时掉线了,在这种情况下,也可以关闭心跳机制。那么如何关闭心跳机制了?可以这样做:

将RapidPassiveEngine的HeartBeatSpanInSecs属性设置为0。这样客户端就不会发送定时的心跳消息了。
将RapidServerEngine的HeartbeatTimeoutInSecs属性设置为小于等于0。这表示服务端将不再做心跳超时检查。

【思考】

看到这个需求,直观上的反应就是在服务端维护一个在线列表。当服务端的监听器监听到一个连接,就把该连接对应的客户端信息加入这个在线列表。这样就完成了对上线状况的记录。但下一个问题是如何让服务器知道客户端的离线状况呢?我们可能会想到,让客户端在关闭前发送一个消息到服务端,服务端收到消息后就把客户端置为离线状态。但是,在更多情况下,客户端并不是这么“友好”地关闭的。应用程序崩溃、网络连接被重置、机器死机等情况下,客户端来不及发送“离线通知”给服务端就挂掉了。这时,需要有一套机制,能让服务端和客户端彼此对对方的在线状态保持清醒。

 

【概念】

何谓“心跳”? 心跳就是指“活着”的客户端或服务端每隔一定的时间就互相发送接收一个消息,告诉对方自己“活着”。当客户端或服务端超过一定的时间间隔尚未收到对方的“心跳”消息,就认为对方“死了”。这就是“心跳机制”的核心思想。

 

【设计实现】

在客户端,除了 UI 外,需要三个线程在后台工作。

1,自动连接的线程。该线程可以实现每隔指定时间就检查一次连接状态,如果发现当前是“离线”状态,就自动发起向服务端的一次连接。

  1         private void ThreadConnect()

复制代码
 2         {
 3             do
 4             {
 5 
 6                 if (!_bConnected)
 7                 {
 8                     _bConnected = _sender.Connect(_ip, _port);
 9 
10                     if (_bConnected)
11                     {
12 
13                         Thread threadSendAndReceivePulseMessage = new Thread(new ThreadStart(ThreadSendAndReceivePulseMessage));
14                         threadSendAndReceivePulseMessage.IsBackground = true;
15                         threadSendAndReceivePulseMessage.Start();
16 
17                         Thread threadCheckPulseCount = new Thread(new ThreadStart(ThreadCheckPulseCount));
18                         threadCheckPulseCount.IsBackground = true;
19                         threadCheckPulseCount.Start();
20 
21                         _pulseCount = 0;
22 
23                         OnConnected(new EventArgs());
24                     }
25 
26                 }
27                 Thread.Sleep(_connectInterval);
28 
29             }
30             while (_bWorking && _bAutoReconnect);
31         }
复制代码

 

 2,收发“心跳”消息的线程。该线程和服务端进行收发心跳消息。注意每收到服务器发来的消息,应将心跳计数器置零。心跳计数器的含义是已经隔了多少个心跳周期没收到心跳消息了。

 

  1         private void ThreadSendAndReceivePulseMessage()

复制代码
 2         {
 3             while (_bWorking && _bConnected)
 4             {
 5 
 6                 string recv =  _sender.Receive(64);
 7 
 8 
 9                 if (recv == "PULSE")
10                 {
11                     _pulseCount = 0;
12 
13                     _sender.Send("ALIVE");
14                 }
15                 else
16                 { 
17                     _bConnected = false;
18                     _sender.Close();
19                     
20                 }
21                 Thread.Sleep(10);
22             }
23         }
复制代码

 

 3,检查心跳计数器的值的线程。该线程每隔指定的时间间隔就检查一次心跳计数器,当发现已经超过指定心跳周期(比如3次)未接收到心跳消息,就认为是离线了,则进行相应的处理。

 

  1         private void ThreadCheckPulseCount()

复制代码
 2         {
 3             while (_bWorking && _bConnected)
 4             {
 5                 Thread.Sleep(_pulseInterval);
 6 
 7                 _pulseCount++;
 8 
 9                 if (_pulseCount > _maxPulseCount)
10                 {
11                     _bConnected = false;
12                     _sender.Close();
13                 }
14 
15                 if (!_bConnected)
16                 {
17                     OnDisconnected(new EventArgs());
18                 }
19             }
20         }
复制代码

 

 

 在服务端,设计思想类似,需要维护一个“在线列表”,并及时和客户端通信,此处省略代码。

原文地址:https://www.cnblogs.com/jarvise/p/4687950.html