Android上保持Socket長(zhǎng)連接

0.Thanks

性能優(yōu)化十六之Wake_Lock喚醒鎖以及JobScheduler使用
安卓 java 判斷socket斷開
android保持服務(wù)不休眠(持續(xù)運(yùn)行)以及喚醒屏幕的方法
Android API 19 及以上版本AlarmManager setRepeating 不準(zhǔn)或只執(zhí)行一次的解決方案

1.概述

前陣子接到一個(gè)用戶反饋說,公司的一款機(jī)器人,很容易掉線。這款產(chǎn)品上,跑的是Android5.1系統(tǒng),用的是Socket與自己服務(wù)器保持著長(zhǎng)鏈接。于是開始排查問題。。。

我們知道,一般我們?cè)诳蛻舳松鲜褂眯奶鴧f(xié)議,跟服務(wù)器保持著長(zhǎng)連接。所以,先從這里下手,懷疑是服務(wù)器心跳協(xié)議那段代碼有問題。然后實(shí)際上不是。

后面繼續(xù)找,發(fā)現(xiàn)了幾個(gè)問題:

  • 問題1,心跳包是客戶端發(fā)送的,而服務(wù)器收到心跳包是沒有回包的,這樣會(huì)存在一個(gè)情況,有時(shí)候因網(wǎng)絡(luò)原因,心跳包一直有發(fā)送,但客戶端并不知道是否成功,而使得socket鏈接被服務(wù)器判定為超時(shí),就被服務(wù)器斷開了。

  • 問題2,接著上面的問題,socket被服務(wù)器判斷為超時(shí),服務(wù)器主動(dòng)斷開socket,但是,客戶端并沒有收到fin包,也就是socket斷開的信號(hào)。為啥沒有收到4次揮手,嗯,有機(jī)會(huì)要深究。

  • 問題3,發(fā)送心跳包是有間隔的,并不是一支持續(xù)地發(fā)送,但從客戶端本地的日志來看,發(fā)現(xiàn)在鎖屏的時(shí)候,心跳包的發(fā)送并不是按照程序那樣子去跑。程序?qū)懙氖敲?分鐘跑一次心跳,但是,鎖屏后,這個(gè)時(shí)間就不是5分鐘,有可能是10分鐘

2.解決問題1:修改心跳協(xié)議

心跳協(xié)議是讓雙方知道,雙方都在線,既然是雙方,心跳也應(yīng)該是有回包的。所以,后面把心跳協(xié)議改成,服務(wù)器收到心跳包,也應(yīng)該回一個(gè)收到心跳包的通知。考慮到有時(shí)候,會(huì)因?yàn)榫W(wǎng)絡(luò)的緣故,會(huì)丟包,所以,在客戶端上增加重試的邏輯。所以現(xiàn)在變成,先發(fā)送心跳包,在N秒內(nèi)若沒有收到回復(fù),則再發(fā)送一次。重試M次。

3.解決問題2:判斷socket是否已經(jīng)斷開了

1)isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()方法來判斷

  • 這種方式只是本地判斷,只是本地操作connect()或close()方法后保存的一個(gè)狀態(tài),
    對(duì)于遠(yuǎn)程服務(wù)器主動(dòng)斷開就沒有用了

2)sendUrgentData()判斷遠(yuǎn)程服務(wù)器是否斷開Socket

  • 往輸出流發(fā)送一個(gè)字節(jié)的數(shù)據(jù),只要對(duì)方Socket的SO_OOBINLINE屬性沒有打開,就會(huì)自動(dòng)舍棄這個(gè)字節(jié),SO_OOBINLINE屬性默認(rèn)情況下就是關(guān)閉的!

  • 但是,經(jīng)測(cè)試,這個(gè)方法行不通。但網(wǎng)上說這個(gè)方法有用,唉,可能我的是假socket。

3)通過OutputStream.write

  • 有人說,發(fā)送心跳協(xié)議的時(shí)候,try catch一下,如果有報(bào)異常,就說明是斷開了。實(shí)際并不然,實(shí)際測(cè)試中,
    服務(wù)器斷開socket,還是可以發(fā)包,并且不會(huì)報(bào)異常

既然以上的辦法都無(wú)法判斷,只能依靠心跳協(xié)議了,只要服務(wù)器沒有按心跳的邏輯回包,就判斷已經(jīng)斷開并主動(dòng)去鏈接。實(shí)際測(cè)試中,修該后的心跳協(xié)議,是可以很好地保持長(zhǎng)鏈接。

4.解決問題3:系統(tǒng)休眠定時(shí)器不準(zhǔn)

系統(tǒng)鎖屏后一段時(shí)間,系統(tǒng)會(huì)進(jìn)入一個(gè)休眠狀態(tài),此狀態(tài)下,系統(tǒng)會(huì)把線程掛起,所以Timer定時(shí)器會(huì)不準(zhǔn)確。

清楚問題了,就好解決??梢酝ㄟ^以下解決:

1)準(zhǔn)確定時(shí)

  • 系統(tǒng)休眠導(dǎo)致定時(shí)器不準(zhǔn)備,那有沒有辦法可以讓定時(shí)器正常工作呢?有!
    在API19,可以這樣做,親測(cè)起效,還有,你還需監(jiān)聽此Action的廣播:
/**
* 設(shè)置定時(shí)器
* @param timeMs    毫秒,既是,多少毫秒后,觸發(fā)
* @param action    觸發(fā),以廣播形式,傳入廣播的action
*/
private void setAlarm(int timeMs,String action) {
   try {
       AlarmManager am = (AlarmManager) XHoneyBotApplicationLike.getInstance().getApplication().getSystemService(Context.ALARM_SERVICE);
       Intent intent = new Intent(action);
       PendingIntent sender = PendingIntent.getBroadcast(XHoneyBotApplicationLike.getInstance().getApplication(), 0, intent,PendingIntent.FLAG_CANCEL_CURRENT);
       if (am!=null) {
           if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
               //參數(shù)2是開始時(shí)間、參數(shù)3是允許系統(tǒng)延遲的時(shí)間
               am.setWindow(AlarmManager.RTC, System.currentTimeMillis() + timeMs, 500, sender);
           } else {
               am.setRepeating(AlarmManager.RTC, System.currentTimeMillis() + timeMs, 500, sender);
           }
       }
   } catch (Exception e){
       e.printStackTrace();
   }
}
/**
* 取消對(duì)應(yīng)action的定時(shí)器
* @param action    action
*/
private void canalAlarm(String action,Context context) {
   try {
       Intent intent = new Intent(action);
       PendingIntent pi = PendingIntent.getBroadcast(context.getApplicationContext(), 0, intent,PendingIntent.FLAG_CANCEL_CURRENT);
       AlarmManager am = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
       if (am!=null)
           am.cancel(pi);
   } catch (Exception e){
       e.printStackTrace();
   }
}

2)添加喚醒鎖

  • 在Android中還有一種喚醒鎖的機(jī)制,詳情可以百度學(xué)習(xí)一下。其實(shí)大概就是,你可以使用喚醒鎖
    強(qiáng)制讓系統(tǒng)不進(jìn)行休眠!
/**
 * Thanks:  http://blog.csdn.net/wzj0808/article/details/52608940
 * 保持系統(tǒng)CUP Active
 */
private static PowerManager.WakeLock wl;
public static void setCPUAliveLock(Context context) {
    try {
        if (wl!=null)
            releaseKeepAliveOnScreenOff();
        PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE);
        if (pm!=null) {
            wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ScreenOff");
            wl.acquire();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public static void releaseCPUAliveLock() {
    try {
        if (wl!=null)
            wl.release();
        wl = null;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

喚醒后,CPU處于ACTIVE狀態(tài),然后執(zhí)行任務(wù),執(zhí)行完成后,便釋放鎖。

PS,如果在鎖屏的時(shí)候,一直喚醒CPU,在實(shí)際應(yīng)用中,會(huì)出現(xiàn)一個(gè)非常嚴(yán)重的問題:耗電!
所以,推介是,使用定時(shí)器,在定時(shí)器定觸發(fā)的時(shí)候,喚醒CPU,進(jìn)行心跳,心跳邏輯跑完后,就釋放鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容