从keep-alive原理分析TCP游戏服务端心跳包的实用功能

发表于2016-01-27
评论5 4.8k浏览

从keep-alive原理分析TCP游戏服务端心跳包的实用功能 ...

  游戏服务器常常有心跳包的设计。
  我们的心跳包就是为了防止Socket断开连接,或是TCP的连接断开吗?
  答案是否定的,TCP连接的通道是个虚拟的,连接的维持靠的是两端TCP软件对连接状态的维护。
  TCP 连接自身有维护连接的机制,说白了就是自身有长时间没有数据包情况下的判断连接是否还存在的检测,清除死连接,即使在没有数据来往的时候,TCP也就可以(在启动TCP这个功能的前提下)自动发包检测是否连接正常,这个不需要我们处理。

  服务端设计心跳包的目的:
  探知对端应用是否存活,服务端客户端都可以发心跳包,一般都是客户端发送心跳包,服务端用于判断客户端是否在线,从而对服务端内存缓存数据进行清理(玩家下线等);问题在于,通过TCP四次握手断开的设定,我们也是可以通过Socket的read方法来判断TCP连接是否断开,从而做出相应的清理内存动作,那么为什么我们还需要使用客户端发送心跳包来判断呢?

  第一种判断客户端是否在线策略:
  直接监控TCP传输协议的返回值,通过返回值处理应用层的存活判断
  比如在C++当中
  使用poll的IO复用方法时:
  if(fds.revents & POLLERR)
  if(fds.events & POLLDHUP)
  通过上述判断可以探知TCP连接的正确性从而在服务器也关闭对应的连接
  close() on the file descriptor will release resources that are still being reserved on behalf of the socket. 此时调用close()函数才会释放相关的资源。


  1. /*如果客户端关闭连接,则服务器也关闭对应的连接,并将用户总数减1*/
  2. users[fds[i].fd] = users[fds[user_counter].fd];
  3. close(fds[i].fd);
  4. fds[i] = fds[user_counter];
  5. i--;
  6. user_counter--;
  7. printf("a client leftn");
复制代码
  又比如在Java中:
  在Java的阻塞编程中:通过

  1. ServerSocket ss = new ServerSocket(10021);
  2. Socket so = ss.accept();
  3. // 获取相关流对象
  4. InputStream in = so.getInputStream();
  5. byte[] bytes = new byte[1024];
  6. int num = in.read(bytes);
  7. if(num == -1)//表明读到了流的末尾,事实也就是client端断开了连接,比如调用close()
  8. {
  9.      so.close();
  10. }
复制代码
  在Java的非阻塞编程当中:通过
  1. SelectionKey key = selector.register(socketChannel,ops,handle);
  2. SocketChannel socketChanel = (SocketChannel)key.channel();
  3. ByteBuffer buffer = ByteBuffer.allocate(1024);
  4. int num = socketChannel.read(buffer);
  5. if(num == -1)
  6. {
  7.      key.channel().close();
  8. }
复制代码
  上述连接处理方式,返回-1也好,收到POLLERR POLLDHUP也好,都是收到了客户端的fin或者rst之后的反应,所以根据四次分手原则,我们调用close方法,发送fin给客户端。
  上面这种策略通过TCP协议的返回值来得知客户端TCP断开,从而得知客户端掉线。
  当前提是如果提前根据ip或者mac做了记录,所以可以在服务器端收到TCP连接中断的消息后,调用close,并且通过socket得到玩家socket数据(具体如IP地址),从而获得user信息从而清除数据。

  那么这种方式有什么不完美呢?或者说有什么缺陷呢?
  主要原因就是TCP的断开可能有时候是不能瞬时探知的,甚至是不能探知的,也可能有很长时间的延迟,如果前端没有正常的断开TCP连接,四次握手没有发起,服务端无从得知客户端的掉线,那么就要依靠开启TCP的keep alive机制,but TCP协议的keep alive机制是不建议开启的,即使开启了默认的时间间隔是2h,泪奔!如果要服务端维持一个2h的死链接,那是可怕的,如果我们调整了时间间隔,也是有problem的,因为TCP本身就不建议TCP层的心跳检测,因为这可能导致一个完好的TCP连接在中间网络中断的情况下重启⊙▂⊙。

  下面有必要先介绍下TCP的keep-alive机制。
  关于TCP自己的keep-alive机制,参看TCP/IP详解当中有如下介绍:
  使用keeplive机制,可以检测到网络异常。

  一、什么是keepalive定时器?
  在一个空闲的(idle)TCP连接上,没有任何的数据流,许多TCP/IP的初学者都对此感到惊奇。也就是说,如果TCP连接两端没有任何一个进程在向对方发送数据,那么在这两个TCP模块之间没有任何的数据交换。你可能在其它的网络协议中发现有轮询(polling),但在TCP中它不存在。言外之意就是我们只要启动一个客户端进程,同服务器建立了TCP连接,不管你离开几小时,几天,几星期或是几个月,连接依旧存在。中间的路由器可能崩溃或者重启,电话线可能go down或者back up,只要连接两端的主机没有重启,连接依旧保持建立。
  这就可以认为不管是客户端的还是服务器端的应用程序都没有应用程序级(application-level)的定时器来探测连接的不活动状态(inactivity),从而引起任何一个应用程序的终止。然而有的时候,服务器需要知道客户端主机是否已崩溃并且关闭,或者崩溃但重启。许多实现提供了存活定时器来完成这个任务。
  存活定时器是一个包含争议的特征。许多人认为,即使需要这个特征,这种对对方的轮询也应该由应用程序来完成,而不是由TCP中实现。此外,如果两个终端系统之间的某个中间网络上有连接的暂时中断,那么存活选项(option)就能够引起两个进程间一个良好连接的终止。例如,如果正好在某个中间路由器崩溃、重启的时候发送存活探测,TCP就将会认为客户端主机已经崩溃,但事实并非如此。
    存活(keepalive)并不是TCP规范的一部分。在Host Requirements RFC罗列有不使用它的三个理由:(1)在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped),(2)它们消费了不必要的宽带,(3)在以数据包计费的互联网上它们(额外)花费金钱。然而,在许多的实现中提供了存活定时器。
  一些服务器应用程序可能代表客户端占用资源,它们需要知道客户端主机是否崩溃。存活定时器可以为这些应用程序提供探测服务。Telnet服务器和Rlogin服务器的许多版本都默认提供存活选项。
  个人计算机用户使用TCP/IP协议通过Telnet登录一台主机,这是能够说明需要使用存活定时器的一个常用例子。如果某个用户在使用结束时只是关掉了电源,而没有注销(log off),那么他就留下了一个半打开(half-open)的连接。在图18.16,我们看到如何在一个半打开连接上通过发送数据,得到一个复位(reset)返回,但那是在客户端,是由客户端发送的数据。如果客户端消失,留给了服务器端半打开的连接,并且服务器又在等待客户端的数据,那么等待将永远持续下去。存活特征的目的就是在服务器端检测这种半打开连接。

  二、keepalive如何工作?
  在此描述中,我们称使用存活选项的那一段为服务器,另一端为客户端。也可以在客户端设置该选项,且没有不允许这样做的理由,但通常设置在服务器。如果连接两端都需要探测对方是否消失,那么就可以在两端同时设置(比如NFS)。
  若在一个给定连接上,两小时之内无任何活动,服务器便向客户端发送一个探测段。(我们将在下面的例子中看到探测段的样子。)客户端主机必须是下列四种状态之一:
  1) 客户端主机依旧活跃(up)运行,并且从服务器可到达。从客户端TCP的正常响应,服务器知道对方仍然活跃。服务器的TCP为接下来的两小时复位存活定时器,如果在这两个小时到期之前,连接上发生应用程序的通信,则定时器重新为往下的两小时复位,并且接着交换数据。
  2) 客户端已经崩溃,或者已经关闭(down),或者正在重启过程中。在这两种情况下,它的TCP都不会响应。服务器没有收到对其发出探测的响应,并且在75秒之后超时。服务器将总共发送10个这样的探测,每个探测75秒。如果没有收到一个响应,它就认为客户端主机已经关闭并终止连接。
  3) 客户端曾经崩溃,但已经重启。这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。
  4) 客户端主机活跃运行,但从服务器不可到达。这与状态2类似,因为TCP无法区别它们两个。它所能表明的仅是未收到对其探测的回复。
  服务器不必担心客户端主机被关闭然后重启的情况(这里指的是操作员执行的正常关闭,而不是主机的崩溃)。当系统被操作员关闭时,所有的应用程序进程(也就是客户端进程)都将被终止,客户端TCP会在连接上发送一个FIN。收到这个FIN后,服务器TCP向服务器进程报告一个文件结束,以允许服务器检测这种状态。
  在第一种状态下,服务器应用程序不知道存活探测是否发生。凡事都是由TCP层处理的,存活探测对应用程序透明,直到后面2,3,4三种状态发生。在这三种状态下,通过服务器的TCP,返回给服务器应用程序错误信息。(通常服务器向网络发出一个读请求,等待客户端的数据。如果存活特征返回一个错误信息,则将该信息作为读操作的返回值返回给服务器。)在状态2,错误信息类似于“连接超时”。状态3则为“连接被对方复位”。第四种状态看起来像连接超时,或者根据是否收到与该连接相关的ICMP错误信息,而可能返回其它的错误信息。
  具体来说:
  在TCP协议的机制里面,本身的心跳包机制,也就是TCP协议中的SO_KEEPALIVE,系统默认是设置2小时的心跳频率。需要用要用setsockopt将SOL_SOCKET.SO_KEEPALIVE设置为1才是打开,并且可以设置三个参数tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl,
  分别表示连接闲置多久开始发keepalive的ACK包、发几个ACK包不回复才当对方死了、两个ACK包之间间隔多长。
  TCP协议会向对方发一个带有ACK标志的空数据包(KeepAlive探针),对方在收到ACK包以后,如果连接一切正常,应该回复一个ACK;如果连接出现错误了(例如对方重启了,连接状态丢失),则应当回复一个RST;如果对方没有回复,服务器每隔多少时间再发ACK,如果连续多个包都被无视了,说明连接被断开了。
  “心跳检测包”是属于TCP协议底层的检测机制,上位机软件只是解析显示网口的有用数据包,收到心跳包报文属于TCP协议层的数据,一般软件不会将它直接在应用层显示出来,所以看不到。以太网中的“心跳包”可以通过“以太网抓包软件”分析TCP/IP协议层的数据流看到。报文名称”TCP Keep-Alive”。
  一些比较可靠的以太网转串口模块,都有心跳包的检测,比如致远电子的ZNE-100TL模块,配置“心跳包检测”间隔时间设为“10”秒,使用一款”wireshark”的抓包软件来实际查看下TCP/IP协议层“心跳包”数据。
  看了上面的内容,使用TCP自己的keep-alive机制,也是可以实现连接维持,通过TCP传输层的心跳包探知两端TCP连接的正确性,从而得知应用层的情况(TCP在,应用一定在,TCP不在了,应用一定不在了),但是这不是最优选择!
  那么既然有TCP的心跳机制,我们为什么还要在应用层实现自己的心跳检测机制呢?
  网友Raynor 所说:
  tcpip详解卷1有网络异常中断的3种情况,比如os回收端口的时候发送的rst包,如果os挂了就不会发这个消息了。 另外如果网络异常,有可能收到路由器返回的icmp主机不可达消息从而关闭连接。 keepalive之所以不靠谱,是因为需要从socket error获知连接断开,而且是被动断开。 而应用层自己实现的heartbeat可以自主检测超时,更方便修改超时时间和断开前处理。
  以及网友李乐所说:
  keepalive设计初衷清除和回收死亡时间长的连接,不适合实时性高的场合,而且它会先要求连接一定时间内没有活动,周期长,这样其实已经断开很长一段时间,没有及时性。而且keepalive好像还不能主动通知应用层,需要主动调用api去检测异常。

  二:使用自己应用层的心跳包,上述方法用于正常情况下的TCP连接维护,
  场景举例如下:在游戏服务器当中,内存中维护着众多玩家的在线数据,以方便调用,比如玩家的英雄队伍信息,玩家的世界位置信息,在玩家下线的时候,服务器必须知道并且清除掉数据(不然就会出现一个已经下线的玩家出现在世界上),在上述举例中,在服务器端收到TCP连接中断的消息后,调用close,期间可以通过socket得到玩家socket数据,从而获得user信息(如果提前根据ip或者mac做了记录)从而清除数据。
  但是如果不是正常的玩家下线,TCP的四次握手没有成功,比如网络直接中断,client端的TCP协议的fin包没有发出去,服务端就不能及时探知玩家下线,如果依赖上面讲的TCP自己的keep alive探测机制,时间较长,做不到及时处理下线玩家,并且性能不佳,因为TCP/IP的设计者本身就不支持TCP的心跳,因为这可能因为中间网络的短暂中断导致两端良好的TCP连接断开。所以一般不启用TCP的心跳机制,那我们怎么处理这些异常下线呢?如果不处理,服务端将一直维护着TCP死连接,导致网络资源(一台服务器可以支持的TCP连接有限)和内存资源(内存中可能维护着该玩家的数据)的占用,所以就要用到应用层的心跳包了
  下面解释下应用层的心跳包:
  心跳包,通常是客户端每隔一小段时间向服务器发送的一个数据包,通知服务器自己仍然在线,服务器与客户端之间每隔一段时间 进行一次交互,来判断这个链接是否有效,并传输一些可能有必要的数据。通常是在建立了一个TCP的socket连接后无法保证这个连接是否持续有效,这时候两边应用会通过定时发送心跳包来保证连接是有效的。因按照一定的时间间隔发送,类似于心跳,所以叫做心跳包。事实上为了保持长连接(长连接指的是建立一次TCP连接之后,就认为连接有效,利用这个连接去不断传输数据,不断开TCP连接),至于包的内容,是没有特别规定的,不过一般都是很小的包,或者只是包含包头的一个空包。


从keep-alive原理分析TCP游戏服务端心跳包的实用功能 ...


  那么心跳包的意义就在于方便的在服务端管理客户端的在线情况,并且可以防止TCP的死连接问题,避免出现长时间不在线的死链接仍然出现在服务端的管理任务中。
  再举下面一个例子说明下为什么TCP自身的四次握手断开机制不能完全保证应用程序探知连接断开从而避免死连接。
  (1)做一个游戏客户端和服务器端的测试,手动强制关闭客户端进程,然后查看服务器的情况,结果往往是,服务器收到了客户端关闭的事件。其实, 这样会忽略一个问题,没有触发异常中断事件,比如网络中断。
    (2)手动关闭客户端进程,事实上并不能测试出想要的结果,因为进程是在应用层的,所以,这种测试方法不能保证网络驱动层也不发送数据报文给服务器。经过测试会发现,当应用层强制结束进程时,对于TCP连接,驱动层会发送reset数据包!而服务器收到这个数据包就可以正常关闭了!
  (3)那么,如果网络异常甚至直接拔掉网线呢,服务器收不到这个数据包,就会导致死连接存在!
  (4)所以,心跳包是必要的,或者使用TCP协议本身的Keep-alive来设置(但是keep-alive不够好)
  我们不能误解TCP连接如同一条绳子,一方断开了,另外一方必然会知道的。事实上TCP连接,这个“面向连接”的物理连接并不存在,它只是抽象出来的概念,是一个虚拟的连接,对于物理层,对于网线、光纤而言,不存在连接不连接的概念,因为,对它们而言,无非就是一些电流脉冲而已,具体就是一个一个的IP报文。
  TCP的连接,不过是通过ACK、SEQ这些机制来模拟实现的,具体比如差错重传,拥塞控制。
    那么心跳包的检测发送处理对服务器资源的耗费怎么判断?
  这个要看心跳包发送的频率,我们可以自行设置。
  另外这里有个例程,模拟了socket心跳包的C语言实现:
  Socket心跳包异常检测的C语言实现,服务器与客户端代码案例

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引