目錄
- socket 簡介
- 創(chuàng)建連接
- 接收消息
- 發(fā)送消息
- 斷開連接
- 進(jìn)度灰色毖渖荩活
- IPC
- 自定義權(quán)限廣播
- 重試機制
- 進(jìn)程異逞档玻恢復(fù)
前段時間公司項目有個大版本準(zhǔn)備對IM(消息通信)模塊升級们拙。雖然需求緊急但server同事任堅持自定義消息協(xié)議來實現(xiàn)一套通信框架腐泻。這里對Android端實現(xiàn)做下總結(jié)桐经,僅供交流拓哟。
socket 簡介
socket就是我們常說的套接字几缭。網(wǎng)絡(luò)上具有唯一標(biāo)識的IP地址和端口組合在一起才能構(gòu)成唯一能識別的標(biāo)識符套接字河泳。根據(jù)不同的的底層協(xié)議,Socket的實現(xiàn)是多樣化的年栓。常見的Socket類型為流套接字(streamsocket)和數(shù)據(jù)報套接字(datagramsocket)拆挥;數(shù)據(jù)報套接字使用UDP協(xié)議,提供數(shù)據(jù)打包發(fā)送服務(wù)某抓。流套接字將TCP作為其端對端協(xié)議竿刁,提供了一個可信賴的字節(jié)流服務(wù),本文將介紹流套接字的簡單應(yīng)用搪缨。
創(chuàng)建連接
這里給出一個簡單通信模型食拜,圖片來源于網(wǎng)絡(luò)。
- sample code 這里只貼出了client端代碼
try {
LogUtil.d(TAG, " connecting ip=%s , port = %d", ip, port);
while (true) {
try {
mSocket = new Socket();
mSocket.setKeepAlive(true);
mSocket.setSoTimeout(2 * 3 * 60 * 1000);//inputStream read 超時時間
mSocket.setTcpNoDelay(true);
mSocket.connect(new InetSocketAddress(ip, port));
if (mSocket.isConnected()) {
dataIS = new DataInputStream(mSocket.getInputStream());
dataOS = new DataOutputStream(mSocket.getOutputStream());
connectState = true;
}
this.mCallback.onConnect(this);
break;//connect sucess
} catch (IOException e) {
mRetryPolicy.retry(e);
//間隔5秒副编,重連负甸。
Thread.sleep(5000);
LogUtil.e(TAG, " connect IOException =%s , and retry count = %d", e.getMessage(), mRetryPolicy.getCurrentRetryCount());
}
}
} catch (Exception e) {
//重試后,仍然失敗了∩氪回調(diào)失敗打月。
connectState = false;
e.printStackTrace();
LogUtil.e(TAG, " connect IOException = " + e.getMessage());
mCallback.onConnectFailed(e);
}
接收消息
創(chuàng)建成功后就開始接收處理消息。
while (isConnected()) {
try {
int type = dataIS.readByte();//讀取1位
int length = dataIS.readChar();//讀取2位標(biāo)記第三段數(shù)據(jù)長度
byte[] data = new byte[length];
LogUtil.i(TAG, " receiveData connected receiveData type = %d, ", type);
dataIS.readFully(data);
mCallback.onReceive(type, data);
} catch (SocketTimeoutException e) {
LogUtil.e(TAG, " receiveData SocketTimeoutException = " + e.getMessage());
e.printStackTrace();
break;
} catch (IOException e) {
LogUtil.e(TAG, " receiveData IOException = " + e.getMessage());
e.printStackTrace();
break;//異常后蚕捉,退出循環(huán)
}
}
//通知異常奏篙,并重連。
發(fā)送消息
在連接狀態(tài)下向服務(wù)器發(fā)送字節(jié)流消息迫淹。
// 1.同步處理 2.異趁赝ǎ或未連接狀態(tài)下,則回調(diào)并通知重連
final byte[] bytes = {1,2,3,4,5}; //test data
synchronized (TcpClient.class) {
if (isConnected()) {
try {
byte type = 1;
dataOS.writeByte(type);
dataOS.writeChar(bytes.length);
dataOS.write(bytes);
dataOS.flush();
LogUtil.i(TAG, "send success msg : %s", Arrays.toString(bytes));
} catch (final IOException e) {
callback.onFailed(e);
disConnect(true);
e.printStackTrace();
}
} else {
callback.onFailed(new Exception("socket is not connected"));
disConnect(true);
LogUtil.i(TAG, "socket is not connected !");
}
}
斷開連接
//關(guān)閉流
closeInputStream(dataIS);
closeOutputStream(dataOS);
if (mSocket != null) {
try {
mSocket.shutdownInput();
mSocket.shutdownOutput();
mSocket.close();
mSocket = null;
} catch (Exception e) {
e.printStackTrace();
}
}
if (mCallback != null && needRec) {
mCallback.onDisconnect();
}
mState = STATE_DISCONNECT;
進(jìn)度灰色绷舶荆活
由于項目中通信模塊運行在獨立的進(jìn)程中肺稀,為避免進(jìn)程被意外干掉,這里將其優(yōu)先級提高已達(dá)到報活效果应民。
if (Build.VERSION.SDK_INT < 18) {//18以下话原,可直接設(shè)置為前臺service
startForeground(GRAY_SERVICE_ID, new Notification());
} else if (Build.VERSION.SDK_INT <= 24) {
Intent innerIntent = new Intent(this, GrayInnerService.class);
startService(innerIntent);
startForeground(GRAY_SERVICE_ID, new Notification());
}
在看看GrayInnerService實現(xiàn)
/**
* 測試結(jié)果:API<=24可行。25/7.1.1版本诲锹,通知欄正常狀態(tài)下看不到icon繁仁,但滑下來就看得到icon。
* 給 API >= 18 的平臺上用的灰色惫樵埃活手段
*/
public static class GrayInnerService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startForeground(GRAY_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
代碼中可以看出黄虱,啟動一個相同id的前臺GrayInnerService然后立即stopSelf,再以同樣方式啟動主service蔓倍,這樣運行起來的service就已經(jīng)是達(dá)到前臺服務(wù)優(yōu)先級了悬钳,一般情況都?xì)⒉涣怂?/p>
IPC(進(jìn)程間通信)
這里IPC比較簡單盐捷,就是使用bundle實現(xiàn)service到broadcast之間的跨進(jìn)程通信偶翅。此處略過,詳細(xì)請查看代碼碉渡。
自定義廣播權(quán)限
出于安全考慮或者其它特殊需求聚谁,可能會限制廣播接收者,所以這里就用到了自定義權(quán)限滞诺。
- manifest.xml中定義并使用
<permission
android:name="android.intent.permission.im.receiver_permission"
android:protectionLevel="normal" />
<uses-permission android:name="android.intent.permission.im.receiver_permission" />
- 發(fā)送帶權(quán)限的廣播
contexts.sendBroadcast(intent, "android.intent.permission.im.receiver_permission");
這樣只有申明了此權(quán)限的廣播才能接收到消息形导。
重試機制
這里參考volley框架的重試機制,在創(chuàng)建連接時若失敗5次之內(nèi)會嘗試重連习霹。具體實現(xiàn)請參考DefaultRetryPolicy類朵耕。
<h3 id="catch_crash">進(jìn)程異常恢復(fù)</h3>
實現(xiàn)Thread.UncaughtExceptionHandler接口淋叶,處理異常并恢復(fù)阎曹。
@Override
public void uncaughtException(Thread thread, Throwable ex) {
// crash統(tǒng)計打點
if (ex != null) {
ex.printStackTrace();
// 保存錯誤報告文件
saveCrashInfoToFile(ex);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LogUtil.i(TAG, "uncaughtException crash recovering");
// 檢查socket狀態(tài)并重連
ImServiceHelper.getInstance(mContext).reConnect();
}
代碼地址
補充
- 弱網(wǎng)情況。
目前弱網(wǎng)情況未做處理,超時異常會捕獲处嫌,然后進(jìn)入重連機制栅贴。
給個處理建議:將所有未發(fā)送成功的請求緩存到隊列,待網(wǎng)絡(luò)恢復(fù)后自動發(fā)送熏迹。 - 連接異常斷開檐薯,如何恢復(fù)。
除了上面提到了連接重試注暗,及進(jìn)程異程陈疲恢復(fù);代碼中捕獲了任何socket異常友存,并進(jìn)入重連機制祷膳。 - 健壯的心跳策略。
心跳策略最好是雙向的屡立,即服務(wù)器和客戶端都應(yīng)有心跳直晨。
由于demo代碼這塊沒完善,這里簡單說下實現(xiàn)思路: 空閑狀態(tài)膨俐,每三分鐘發(fā)送一個心跳包勇皇。
server每三分鐘發(fā)一次心跳"ping"到client,client收到后回發(fā)一
個"pong"給server焚刺,這就完成了一次心跳檢測敛摘。但如果在這三分鐘之
內(nèi)收到client發(fā)來的數(shù)據(jù),則證明長連接處于正常狀態(tài)乳愉,需要重新記
時三分鐘再發(fā)送心跳兄淫。 client處理策略可以和server一致。