IM心跳策略
心跳的字段定義
- minHeart 最小心跳,本地默認(rèn)120秒,服務(wù)器定120秒
- maxHeart 最大心跳,本地默認(rèn)580秒,服務(wù)器定580秒
- startHeart 起步心跳,本地默認(rèn)240秒,服務(wù)器定240秒
心跳信息字段
- networkTag 當(dāng)前網(wǎng)絡(luò)類型,如CMCC-4G
- stabled 穩(wěn)定心跳的標(biāo)志位,true表示穩(wěn)定心跳
- stabledSuccessCount 穩(wěn)定心跳連續(xù)成功次數(shù),這里是在心跳穩(wěn)定一段時(shí)間后,再嘗試上調(diào)的時(shí)候用,例如stabledSuccessCount > 50的時(shí)候,穩(wěn)定心跳嘗試上調(diào)
- failedCount 心跳連續(xù)失敗次數(shù),當(dāng)failedCount >= 3的時(shí)候,才會(huì)認(rèn)為當(dāng)前心跳是不可用的,會(huì)嘗試下調(diào),如果心跳一直失敗,那么failedCount是不斷累計(jì)遞增
- successCount 心跳連續(xù)成功次數(shù),心跳成功后就遞增1,同時(shí)successCount > 2的時(shí)候會(huì)把failedCount清零
- curMinHeart 當(dāng)前心跳探測(cè)區(qū)間極小值
- curMaxHeart 當(dāng)前心跳探測(cè)區(qū)間極大值
- curHeart 當(dāng)前心跳
- successHeartList 成功心跳列表,每次心跳成功后,會(huì)把當(dāng)前的成功心跳記錄進(jìn)來
重置心跳
- 當(dāng)TCP連接有除了心跳包以外的消息包在進(jìn)行傳輸(read)時(shí)候,就認(rèn)為該TCP連接在這個(gè)時(shí)刻仍然有效,在程序中read到消息包數(shù)據(jù)后會(huì)對(duì)數(shù)據(jù)進(jìn)行短時(shí)間處理(ms級(jí)別),然后再write數(shù)據(jù),只有收到同步通知,或者單推的時(shí)候本地發(fā)現(xiàn)消息已經(jīng)同步,那此時(shí)就不會(huì)write,不過這種情況發(fā)生的概率比較小,所以心跳是在write數(shù)據(jù)出去的時(shí)候進(jìn)行重置,這里不在read數(shù)據(jù)的時(shí)候重置心跳是為了避免在弱網(wǎng)環(huán)境下,數(shù)據(jù)包要在網(wǎng)絡(luò)中傳輸幾分鐘,導(dǎo)致服務(wù)器連接超時(shí),然后把TCP連接誤斷的這種情況
- 如果心跳包在write的時(shí)候進(jìn)行重置,當(dāng)遇到此TCP已經(jīng)是無效連接,但是服務(wù)器和客戶端都沒有感知到這中情況,那么客戶端對(duì)于write出去的消息會(huì)有一個(gè)超時(shí)檢測(cè)(20s,但是消息ack沒有超時(shí)檢測(cè)),write數(shù)據(jù)出去后收不到響應(yīng)的回饋,20s超時(shí)到期,此時(shí)會(huì)通過心跳來驗(yàn)證TCP連接的有效性,心跳超時(shí)就進(jìn)行斷線重連,所以這里會(huì)有60秒以上的消息延遲
- TCP無效連接,如果是客戶端的消息ack數(shù)據(jù)發(fā)送出去但是服務(wù)端沒有收到,那么將遇到兩種情況,第一是服務(wù)器連接超時(shí)端開,第二是客戶端下一個(gè)心跳檢測(cè)發(fā)現(xiàn)TCP連接是無效的,然后斷線重連,這里會(huì)有最多一個(gè)心跳周期的延遲
心跳策略圖
這里寫圖片描述
觸發(fā)心跳上調(diào)
- 探測(cè)期間的心跳發(fā)送成功并及時(shí)收到服務(wù)器的響應(yīng),這時(shí)候會(huì)執(zhí)行心跳上調(diào)
- 穩(wěn)定一定的時(shí)間后嘗試上調(diào)(有待優(yōu)化)
心跳上調(diào)策略
- 記錄成功心跳的信息
- successHeartList.add(curHeart);successCount++;
- if (successCount >= 2) failedCount = 0;心跳連續(xù)成功兩次,才認(rèn)為當(dāng)前心跳在該網(wǎng)絡(luò)環(huán)境下運(yùn)行穩(wěn)定
- 把當(dāng)前的心跳信息更新到文件中。
- if (stabled == true) stabledSuccessCount++;
- 如果當(dāng)前心跳不是穩(wěn)定心跳,那么執(zhí)行以下操作:
- 從成功心跳列表中篩選比當(dāng)前心跳大一級(jí)的心跳周期作為當(dāng)前心跳:curHeart = successHeart;
- 如果1沒有篩選出結(jié)果,則用二分法進(jìn)行上調(diào):
(1)curMinHeart = curHeart;
(2)if (curMaxHeart < curMinHeart) curMaxHeart = curMinHeart;
(3)curHeart = (curMinHeart + curMaxHeart) / 2;
- 判斷curHeart > maxHeart,如果是則curHeart = maxHeart;stabled = true;這是用來異常過濾
- 檢測(cè)心跳探測(cè)區(qū)間是否達(dá)到機(jī)值條件:
- if (curMaxHeart - curMinHeart <= 10 && stabled == false) curHeart = curMinHeart;
- 如果已經(jīng)達(dá)到極值條件(curMaxHeart - curMinHeart <= 10),那么stabled = true;
- 使用curHeart進(jìn)行下一個(gè)心跳的發(fā)送
觸發(fā)心跳下調(diào)
- 心跳下調(diào)無非是TCP連接斷線導(dǎo)致心跳下調(diào),但并不是所有的TCP斷線都要下調(diào)心跳,當(dāng)前遇到會(huì)導(dǎo)致TCP斷線的情況有以下幾種:
- 心跳超時(shí)主動(dòng)斷開TCP連接(socket closed),此時(shí)應(yīng)該下調(diào)心跳
- IM SDK初始化會(huì)主動(dòng)斷開TCP并重新連接(socket closed),不應(yīng)該下調(diào)心跳
- 本地網(wǎng)絡(luò)斷開造成 TCP連接被動(dòng)斷開(Software caused connection abort,socket closed),這里分為兩種情況,第一個(gè)是網(wǎng)絡(luò)切換,那么這時(shí)候是網(wǎng)絡(luò)斷開,然后再重新連上的一個(gè)過程,應(yīng)用能明顯的感知到這個(gè)過程(網(wǎng)絡(luò)切換廣播),TCP連接在網(wǎng)絡(luò)切換的時(shí)候會(huì)被動(dòng)斷開,這時(shí)候在下調(diào)心跳之前要先檢測(cè)下本地網(wǎng)絡(luò)是否可用,如果不可用則不進(jìn)行心跳下調(diào),其實(shí)因?yàn)楸镜鼐W(wǎng)絡(luò)斷開導(dǎo)致的TCP斷線是不應(yīng)該下調(diào)心跳的,這里多了個(gè)檢測(cè)就是為了在一定程度上過濾掉一部分因?yàn)楸镜鼐W(wǎng)絡(luò)斷開導(dǎo)致的心跳誤下調(diào);還有一種是modem其實(shí)已經(jīng)斷網(wǎng)了,此時(shí)modem可能在進(jìn)行重連,但是并沒有網(wǎng)絡(luò)切換廣播,此時(shí)應(yīng)用層是無感知的,但是TCP連接可以立馬感知到,并被動(dòng)斷開,這時(shí)候檢測(cè)本地網(wǎng)絡(luò)也是可用的(不準(zhǔn)),所以這時(shí)候會(huì)導(dǎo)致心跳誤下調(diào),Android sdk接口判斷本地網(wǎng)絡(luò)是否可用其實(shí)是不準(zhǔn)確的,如果接口返回不可用,那么本地網(wǎng)絡(luò)一定是不可用的,如果接口返回可用,那網(wǎng)絡(luò)還不一定真的可用,因?yàn)榻涌跈z測(cè)的只是設(shè)備本地網(wǎng)絡(luò)而已,如果連接上一個(gè)假wifi(需要驗(yàn)證密碼),那么設(shè)備到wifi路由器這段網(wǎng)絡(luò)是通的,但是wifi路由器到外網(wǎng)是不通的,這時(shí)候設(shè)備是感知不到的,通過ping才能準(zhǔn)確的知道網(wǎng)絡(luò)是否真的可用,當(dāng)手機(jī)卡欠費(fèi)的時(shí)候,本地接口也是返回網(wǎng)絡(luò)可用,道理類似
- 服務(wù)器close造成TCP連接被動(dòng)斷開(read返回-1),此時(shí)會(huì)下調(diào)心跳
- 其他網(wǎng)絡(luò)原因造成的TCP連接被動(dòng)斷開(connection reset等),此時(shí)會(huì)下調(diào)心跳
- TLV數(shù)據(jù)解析錯(cuò)誤主動(dòng)斷開TCP連接,不應(yīng)該下調(diào)心跳
- 除了以上6中原因會(huì)造成TCP斷開,如果還有其他原因在成TCP斷開,需要檢測(cè)三個(gè)條件才滿足心跳下調(diào)的條件:第一是當(dāng)前心跳是否已經(jīng)啟動(dòng),第二是當(dāng)前設(shè)備本地網(wǎng)絡(luò)是否可用,第三是TCP斷開前,已經(jīng)持續(xù)連接超過一個(gè)最小心跳周期的時(shí)間,滿足以上三個(gè)條件才進(jìn)行下調(diào)心跳,否則不下調(diào)
心跳下調(diào)策略
- 記錄心跳失敗信息:
- 從successHeartList移除當(dāng)前心跳鎖對(duì)應(yīng)的心跳周期;stabledSuccessCount;successCount;failedCount++;
- 把當(dāng)前的心跳信息更新到文件中。
- if (stabled == true && failedCount >= 3)那么執(zhí)行以下操作:
- stabled = false;
- 從successHeartList篩選比當(dāng)前心跳小一級(jí)的心跳heart
- if ((minHeart + curHeart) / 2 < heart) selectedHeart = heart;curHeart = selectedHeart;
- 如果沒有篩選到適合條件的selectedHeart,那么就進(jìn)行二分法下調(diào):
(1)currentMaxHeart = currentHeart;
(2)currentMinHeart = minHeart;
(3)currentHeart = (currentMinHeart + currentMaxHeart) / 2;
- if (stabled == false && failedCount >= 3)那么執(zhí)行以下操作:
- 從successHeartList篩選比當(dāng)前心跳小一級(jí)的心跳heart;
- if ((minHeart + curHeart) / 2 < heart) selectedHeart = heart;curHeart = selectedHeart;
- 如果沒有篩選到適合條件的selectedHeart,那么就進(jìn)行二分法下調(diào):
(1)currentMaxHeart = currentHeart;
(2)if (currentMaxHeart < curentMinHeart) currentMaxHeart = currentMinHeart;
(3)currentHeart = (currentMinHeart + currentMaxHeart) / 2;
- 檢測(cè)心跳探測(cè)區(qū)間是否達(dá)到極值條件:
- if (curMaxHeart - curMinHeart <= 10 && stabled == false) currentMinHeart = minHeart;
- 之所以加入這個(gè)判斷是為了當(dāng)心跳一直失敗下調(diào),但是curMinHeart和curMaxHeart又很接近導(dǎo)致二分法無法下調(diào)的時(shí)候,就直接把curHeart設(shè)置成minHeart
穩(wěn)定心跳
- 有效的穩(wěn)定心跳是NAT臨界值
- 探測(cè)心跳達(dá)到最大心跳值的時(shí)候認(rèn)為是穩(wěn)定心跳
- 探測(cè)心跳滿足二分法的極值條件(curMaxHeart - curMinHeart < 10)的時(shí)候認(rèn)為是穩(wěn)定心跳
- 探測(cè)心跳達(dá)到最小心跳值的時(shí)候認(rèn)為是穩(wěn)定心跳
- 當(dāng)探測(cè)到穩(wěn)定心跳之后,正式使用的心跳值會(huì)在探測(cè)到的穩(wěn)定心跳的基礎(chǔ)上扣除20秒,但是扣除后的心跳值一定要在最大值和最小值之間,避開臨界值
Android機(jī)子上存在的問題
- 對(duì)于系統(tǒng)APP發(fā)起的alarm,在android原生系統(tǒng)不會(huì)存在alarm被對(duì)齊的問題,因?yàn)閍ndroid系統(tǒng)對(duì)于系統(tǒng)app發(fā)起的alarm會(huì)設(shè)置alarm的flag為FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED,在Android6.0以上系統(tǒng)AlarmManagerService會(huì)在doze模式下忽略有該flag的alrm,因此不會(huì)被延遲喚醒,至于AlarmClokc,flag是FLAG_WAKE_FROM_IDLE,doze模式下也不會(huì)對(duì)該flag的alarm做延遲喚醒
- 在Android6.0系統(tǒng)以上,休眠的時(shí)候alarm是會(huì)被延遲執(zhí)行的,可通過加入系統(tǒng)白名單的方式來避免,Google的GCM就是默認(rèn)系統(tǒng)白名單,但是在手機(jī)上,系統(tǒng)白名單嘗試過,并沒有用;手表是原生的Android系統(tǒng),可以嘗試加入白名單更加可靠
- alarm的對(duì)齊喚醒:國(guó)內(nèi)的手機(jī)廠商例如華為,魅族,小米都是自定制的android系統(tǒng),對(duì)于AlarmManager都有對(duì)齊喚醒策略,因此會(huì)導(dǎo)致心跳alarm的時(shí)間不準(zhǔn)確,例如設(shè)置了270秒alarm一次,但是在這些手機(jī)上可能要推遲到300秒才能喚醒,那么問題來了,如果NAT超時(shí)時(shí)間是2分鐘,而這些手機(jī)的alarm最小間隔是5分鐘,那就坑了,永遠(yuǎn)無法探測(cè)到最佳心跳,你設(shè)置120秒的alarm,手機(jī)系統(tǒng)也給你延遲到5分鐘才執(zhí)行alarm,不過這種情況只有在手機(jī)休眠的時(shí)候才會(huì)對(duì)齊喚醒,在手機(jī)不休眠的時(shí)候,我側(cè)過,alarm計(jì)時(shí)還是準(zhǔn)確的