IM心跳策略
心跳的字段定义
- minHeart 最小心跳,本地默认120秒,服务器定120秒
- maxHeart 最大心跳,本地默认580秒,服务器定580秒
- startHeart 起步心跳,本地默认240秒,服务器定240秒
心跳信息字段
- networkTag 当前网络类型,如CMCC-4G
- stabled 稳定心跳的标志位,true表示稳定心跳
- stabledSuccessCount 稳定心跳连续成功次数,这里是在心跳稳定一段时间后,再尝试上调的时候用,例如stabledSuccessCount > 50的时候,稳定心跳尝试上调
- failedCount 心跳连续失败次数,当failedCount >= 3的时候,才会认为当前心跳是不可用的,会尝试下调,如果心跳一直失败,那么failedCount是不断累计递增
- successCount 心跳连续成功次数,心跳成功后就递增1,同时successCount > 2的时候会把failedCount清零
- curMinHeart 当前心跳探测区间极小值
- curMaxHeart 当前心跳探测区间极大值
- curHeart 当前心跳
- successHeartList 成功心跳列表,每次心跳成功后,会把当前的成功心跳记录进来
重置心跳
- 当TCP连接有除了心跳包以外的消息包在进行传输(read)时候,就认为该TCP连接在这个时刻仍然有效,在程序中read到消息包数据后会对数据进行短时间处理(ms级别),然后再write数据,只有收到同步通知,或者单推的时候本地发现消息已经同步,那此时就不会write,不过这种情况发生的概率比较小,所以心跳是在write数据出去的时候进行重置,这里不在read数据的时候重置心跳是为了避免在弱网环境下,数据包要在网络中传输几分钟,导致服务器连接超时,然后把TCP连接误断的这种情况
- 如果心跳包在write的时候进行重置,当遇到此TCP已经是无效连接,但是服务器和客户端都没有感知到这中情况,那么客户端对于write出去的消息会有一个超时检测(20s,但是消息ack没有超时检测),write数据出去后收不到响应的回馈,20s超时到期,此时会通过心跳来验证TCP连接的有效性,心跳超时就进行断线重连,所以这里会有60秒以上的消息延迟
- TCP无效连接,如果是客户端的消息ack数据发送出去但是服务端没有收到,那么将遇到两种情况,第一是服务器连接超时端开,第二是客户端下一个心跳检测发现TCP连接是无效的,然后断线重连,这里会有最多一个心跳周期的延迟
心跳策略图
触发心跳上调
- 探测期间的心跳发送成功并及时收到服务器的响应,这时候会执行心跳上调
- 稳定一定的时间后尝试上调(有待优化)
心跳上调策略
- 记录成功心跳的信息
- successHeartList.add(curHeart);successCount++;
- if (successCount >= 2) failedCount = 0;心跳连续成功两次,才认为当前心跳在该网络环境下运行稳定
- 把当前的心跳信息更新到文件中。
- if (stabled == true) stabledSuccessCount++;
- 如果当前心跳不是稳定心跳,那么执行以下操作:
- 从成功心跳列表中筛选比当前心跳大一级的心跳周期作为当前心跳:curHeart = successHeart;
- 如果1没有筛选出结果,则用二分法进行上调:
(1)curMinHeart = curHeart;
(2)if (curMaxHeart < curMinHeart) curMaxHeart = curMinHeart;
(3)curHeart = (curMinHeart + curMaxHeart) / 2;
- 判断curHeart > maxHeart,如果是则curHeart = maxHeart;stabled = true;这是用来异常过滤
- 检测心跳探测区间是否达到机值条件:
- if (curMaxHeart - curMinHeart <= 10 && stabled == false) curHeart = curMinHeart;
- 如果已经达到极值条件(curMaxHeart - curMinHeart <= 10),那么stabled = true;
- 使用curHeart进行下一个心跳的发送
触发心跳下调
- 心跳下调无非是TCP连接断线导致心跳下调,但并不是所有的TCP断线都要下调心跳,当前遇到会导致TCP断线的情况有以下几种:
- 心跳超时主动断开TCP连接(socket closed),此时应该下调心跳
- IM SDK初始化会主动断开TCP并重新连接(socket closed),不应该下调心跳
- 本地网络断开造成 TCP连接被动断开(Software caused connection abort,socket closed),这里分为两种情况,第一个是网络切换,那么这时候是网络断开,然后再重新连上的一个过程,应用能明显的感知到这个过程(网络切换广播),TCP连接在网络切换的时候会被动断开,这时候在下调心跳之前要先检测下本地网络是否可用,如果不可用则不进行心跳下调,其实因为本地网络断开导致的TCP断线是不应该下调心跳的,这里多了个检测就是为了在一定程度上过滤掉一部分因为本地网络断开导致的心跳误下调;还有一种是modem其实已经断网了,此时modem可能在进行重连,但是并没有网络切换广播,此时应用层是无感知的,但是TCP连接可以立马感知到,并被动断开,这时候检测本地网络也是可用的(不准),所以这时候会导致心跳误下调,Android sdk接口判断本地网络是否可用其实是不准确的,如果接口返回不可用,那么本地网络一定是不可用的,如果接口返回可用,那网络还不一定真的可用,因为接口检测的只是设备本地网络而已,如果连接上一个假wifi(需要验证密码),那么设备到wifi路由器这段网络是通的,但是wifi路由器到外网是不通的,这时候设备是感知不到的,通过ping才能准确的知道网络是否真的可用,当手机卡欠费的时候,本地接口也是返回网络可用,道理类似
- 服务器close造成TCP连接被动断开(read返回-1),此时会下调心跳
- 其他网络原因造成的TCP连接被动断开(connection reset等),此时会下调心跳
- TLV数据解析错误主动断开TCP连接,不应该下调心跳
- 除了以上6中原因会造成TCP断开,如果还有其他原因在成TCP断开,需要检测三个条件才满足心跳下调的条件:第一是当前心跳是否已经启动,第二是当前设备本地网络是否可用,第三是TCP断开前,已经持续连接超过一个最小心跳周期的时间,满足以上三个条件才进行下调心跳,否则不下调
心跳下调策略
- 记录心跳失败信息:
- 从successHeartList移除当前心跳锁对应的心跳周期;stabledSuccessCount;successCount;failedCount++;
- 把当前的心跳信息更新到文件中。
- if (stabled == true && failedCount >= 3)那么执行以下操作:
- stabled = false;
- 从successHeartList筛选比当前心跳小一级的心跳heart
- if ((minHeart + curHeart) / 2 < heart) selectedHeart = heart;curHeart = selectedHeart;
- 如果没有筛选到适合条件的selectedHeart,那么就进行二分法下调:
(1)currentMaxHeart = currentHeart;
(2)currentMinHeart = minHeart;
(3)currentHeart = (currentMinHeart + currentMaxHeart) / 2;
- if (stabled == false && failedCount >= 3)那么执行以下操作:
- 从successHeartList筛选比当前心跳小一级的心跳heart;
- if ((minHeart + curHeart) / 2 < heart) selectedHeart = heart;curHeart = selectedHeart;
- 如果没有筛选到适合条件的selectedHeart,那么就进行二分法下调:
(1)currentMaxHeart = currentHeart;
(2)if (currentMaxHeart < curentMinHeart) currentMaxHeart = currentMinHeart;
(3)currentHeart = (currentMinHeart + currentMaxHeart) / 2;
- 检测心跳探测区间是否达到极值条件:
- if (curMaxHeart - curMinHeart <= 10 && stabled == false) currentMinHeart = minHeart;
- 之所以加入这个判断是为了当心跳一直失败下调,但是curMinHeart和curMaxHeart又很接近导致二分法无法下调的时候,就直接把curHeart设置成minHeart
稳定心跳
- 有效的稳定心跳是NAT临界值
- 探测心跳达到最大心跳值的时候认为是稳定心跳
- 探测心跳满足二分法的极值条件(curMaxHeart - curMinHeart < 10)的时候认为是稳定心跳
- 探测心跳达到最小心跳值的时候认为是稳定心跳
- 当探测到稳定心跳之后,正式使用的心跳值会在探测到的稳定心跳的基础上扣除20秒,但是扣除后的心跳值一定要在最大值和最小值之间,避开临界值
Android机子上存在的问题
- 对于系统APP发起的alarm,在android原生系统不会存在alarm被对齐的问题,因为android系统对于系统app发起的alarm会设置alarm的flag为FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED,在Android6.0以上系统AlarmManagerService会在doze模式下忽略有该flag的alrm,因此不会被延迟唤醒,至于AlarmClokc,flag是FLAG_WAKE_FROM_IDLE,doze模式下也不会对该flag的alarm做延迟唤醒
- 在Android6.0系统以上,休眠的时候alarm是会被延迟执行的,可通过加入系统白名单的方式来避免,Google的GCM就是默认系统白名单,但是在手机上,系统白名单尝试过,并没有用;手表是原生的Android系统,可以尝试加入白名单更加可靠
- alarm的对齐唤醒:国内的手机厂商例如华为,魅族,小米都是自定制的android系统,对于AlarmManager都有对齐唤醒策略,因此会导致心跳alarm的时间不准确,例如设置了270秒alarm一次,但是在这些手机上可能要推迟到300秒才能唤醒,那么问题来了,如果NAT超时时间是2分钟,而这些手机的alarm最小间隔是5分钟,那就坑了,永远无法探测到最佳心跳,你设置120秒的alarm,手机系统也给你延迟到5分钟才执行alarm,不过这种情况只有在手机休眠的时候才会对齐唤醒,在手机不休眠的时候,我侧过,alarm计时还是准确的