场景之心跳应用

一、心跳概述

常见的IM类应用,比如游戏,直播,聊天室或者客服系统,一般都要依靠服务端做消息中转,将从发送方接受的消息推送给接收方,为保证可靠,快速到达对端,⼤部分IM使⽤长连接建⽴通道,并且建⽴TCP连接和用户设备的映射关系,长连接⼀旦建⽴,就会⼀直存在,除非意外被中断,并依靠该链接接受和推送消息。
心跳机制存在的意义:这个长连接并不是物理意义上的连接,⽽是⼀个无感知的虚拟TCP连接,即使断开两端也不会感知,因此心跳机制来让连接在出现问题的时候,迅速的让对端感知到,心跳要做的就是快速不间断的识别来探测连接可⽤性。

1、心跳机制在长连接维护的必要性

  • 降低服务端维持连接的开销:服务端除了维持⽹络连接和用户设备的映射之外,还会保存⼀些包括app版本号,os系统类型等信息,这些信息第⼀次建立连接发送,以后都维持到服务端缓存,如果存在⼤量的断开的连接,就会造成具柄和缓存的浪费,因此⼼跳机制保证了在连接断开后尽早的清理服务端连接使⽤的资源。

  • ⽀持客户端断线重连:⼀般移动设备连接的都是运营商的内网,⽽IPv4地址总共约为43亿个,因此为了节省IP地址资源,运营商通常会采⽤NAT⼿段,为联⽹设备分配内⽹IP,变成了内网IP:port-外网IP:port的映射,使得内网IP的⼿机能和外网联通,但为了提⾼IP利⽤率,如果⼀个连接⼀段时间没有数据收发,运营商就会把这个映射从NAT映射表清除掉,为了避免长连接被被运营商下掉,就需要长跳机制定期发送⼀些数据对连接让运营商以为这个链接还在用从而进行保活。

2、心跳实现方式

一般有三种:

  • TCP层⾯的keep-alive,发送端在发送完数据给接收方之后,接收方会启动一个超时计时器,每当计时器超时就会发送探测报⽂给发送方,心跳包不携带数据,累计10次心跳包没有回应则认为链接失效断开了。协议栈实现使⽤资源最少,但是心跳间隔设置不灵活,TCP的keep-Alive只代表链接层,不代表应⽤层可⽤,比如代码死锁,同样tcp链接可用,但是服务已经不可用了。
  • 应⽤层心跳:应⽤层⼼跳实际上就是客⼾端每隔⼀定时间间隔,向IM服务端发送⼀个业务层的数据包告知自⾝存活。相比TCP心跳,更灵活设置心跳间隔和更可⽤状态的保活。
  • 智能心跳:避免NAT超时,只能将心跳间隔设置为小于所有网络环境下NAT超时的最短时间。但是定期心跳,会有CPU和网络流量的浪费,因此提出智能心跳,根据网络环境自动调整心跳间隔的时间,逐渐逼近NAT超时临界点,优化心跳间隔。

3、总结

⼼跳作⽤:

  • 降低服务端连接维护⽆效连接的开销
  • ⽀持客⼾端快速识别⽆效连接,快速重连
  • 连接保活,防⽌被运营商NAT超时端开。

问题:可否结合TCP的keep-Alive和应⽤层心跳使⽤?
tcp的keep-alive解决网络可⽤性,应⽤层keep-alive解决⽹络和服务可⽤性,但是应⽤层无法区分网络还是服务问题,因此加上tcp心跳包可以进⼀步区分,tcp心跳探测到链接正常,而此时应用层通信出现问题,则可能是服务出现问题。

二、心跳实现示例

1、注册中心

微服务项目中为了让客⼾端知道具体服务部署的地址,通常要使用服务注册中心。

(1)、注册中心解决问题

  • 提供服务地址的存储
  • 存储内容发⽣变化,变更内容推送到客⼾端

通过注册中心,即使需要扩容,或者摘除节点,也不⽤重启客⼾端服务器。

  • 客户端会与注册中心建⽴连接,并且告诉注册中心,它对哪⼀组服务感兴趣;
  • 服务端向注册中心注册服务后,注册中心会将最新的服务注册信息通知给客⼾端;
  • 客⼾端拿到服务端的地址之后就可以向服务端发起调⽤请求了。

(2)、服务状态如何管理

主动探测:服务要打开⼀个端口,然后由注册中心每隔⼀段时间(比如30秒)探测这些端口是否可⽤,如果不可⽤就从服务列表摘除。
问题:

  • 服务节点都需要开放⼀个统⼀端⼝给注册中心探测,端⼝可能被互相占⽤。
  • 服务器部署很多,探测对于注册中⼼的代价也很高,服务不可⽤的时候还会有延迟。

心跳机制:服务节点注册到服务中心后,会按照⼀定的时间间隔向注册中心发送心跳包,注册中心收到后会更新最新续约时间,启动⼀个定时器,如果⼀定时间还没有收到心跳,就认为服务不可⽤。服务也需要检测是否存活,那么也可以考虑使⽤心跳机制来检测

2、聊天室心跳应用和实现

在即时通信系统中,比如聊天室,一般都是采用发送方与服务器建立一条TCP连接,接收方也会与服务器建立一条TCP连接。
首先用户有⼀个登陆的过程:

  • tcp客户端与服务端通过三次握⼿建立tcp连接。
  • 基于该连接客户端发送登陆请求。
  • 服务端对登陆请求进⾏解析和判断,如果合法,就将当前用户uid和标识当前tcp连接的socket描述符(也就是fd)建⽴映射关系。
  • 这个映射关系⼀般是保存在本地缓存中。
  • 然后当服务端收到要发送给这个用户的消息时,先从缓存中根据uid查找fd,如果找到,就基于fd将消息推送出去。

具体的即使通信系统应用场景可以了解:实时在线在线人数

(1)、心跳实现逻辑

因为要基于该TCP连接发送消息,所以要先保证该连接的可靠性,因此要使用如上中所说要通过心跳及时的探测到链接的是否仍有效。
逻辑如下:

  • 用户与服务器建立TCP连接
  • 用户每隔几秒向服务器发送心跳包
  • 服务端在收到心跳包之后,更新用户的最后在线时间
  • 服务端同时有一个定时脚本,定时脚本每隔一段时间执行一次扫描所有的用户的最后在线时间与现在时间对比,如果超过设置的超时时间,则重连或者清空相应服务端缓存资源给出对应操作。

(2)、hash结构具体实现

实现上:可以用redis的hash结构,配合记录所有的用户的最后在线时间。

hash结构中字符串是一个key对应一个value,value中通常只有一个对应key的数据,而hash中,把很多个数据(field:value)存到一个value中,Hash类型可以看成具有String Key和String Value的map容器添加和删除操作都是O(1)(平均)的复杂度,每个hash可以存储232-1 键值对(40多亿),操作复杂度和存储上限一般不会有问题。
hash的key为chat-room,表示这是聊天室key。
当用户发送心跳,设置hash字段,field为user_id,value为当前系统时间戳

hset chat-room user_id time.Now().Unix()

服务端定时脚本检测逻辑如下

res = hgetall chat-room # 将hash所有字段fields和value保存在res字典中
for field, value := range res: # 遍历所有字段,也就是查看所有用户的上一次心跳时间
    if time.Now().Unix() - value > 60 * 3:  # 如果上一次心跳距离现在超过3分钟,则认为连接断开,连接资源。
        clearConnect()