android封裝signalR的demo
後端用的是c#,所以長連結這塊用的是signalR。公司的前端是用flutter的,也有執行緒的signalR的外掛。可惜會出現一些問題,決定自己封裝一個。這裡就簡單介紹一下android原生封裝signalR吧
這邊實現了,心跳機制,斷線重連,訊息去重發送,連線狀態等。
先封裝了hubConnection,然後在這層實現了心跳。這一塊必須得扯上後端,後端實現了一個方法,收到什麼引數,馬上就把這個引數傳回來。然後就用這個方法實現心跳,傳送一個訊息給伺服器,伺服器收到這個訊息。記錄下發出時間和接收時間,不小於自己設定的時間間隔,則認定網路狀態有效。當心跳無效的時候就把連線狀態置為false,表示連線斷開。其實他提供了一個回撥oncloed。當連線關閉的時候會呼叫這個回撥。但是不能太依賴這個,所以自己寫了心跳來確保連線。下為心跳的邏輯。
while(isRunning){ long ping = System.currentTimeMillis()/1000; //傳送心跳包 try{ hubConnection.send("Echo",String.valueOf(ping)); }catch (Exception e){ connectStatus = false; }//心跳延時 try { Thread.sleep(heartDelay); } catch (InterruptedException e) { e.printStackTrace(); } //最後一次接收訊息時間小於傳送心跳時間, //起碼在心跳時間內,沒有收到包。 if(lastRecvTime < ping){long delay = System.currentTimeMillis()/1000 - ping; //時間差大於重連時間的時候,判定為超時,連線狀態置為false if(delay > KeepAliveTimeOutSecond){ connectStatus = false; }else { connectStatus = true; } }else { connectStatus = true; } }
這個isRunning則表明需不需要進行心跳檢測,當連線斷開的時候當然是不必要的啦。(ps,來自後端大佬的一個建議,死迴圈執行緒裡要加一個try,避免他因為錯誤而中斷迴圈)。
然後開放了三個方法,開始連線,斷開連線,傳送訊息。
/** * 開放的三個方法 * */ public void send(String method,Object... message){try{ hubConnection.send(method,message); }catch (Exception e){ connectStatus = false; } } public void stopConnect(){ isRunning = false; connectStatus = false; hubConnection.stop(); } public void startConnect(){ Log.i(TAG,"start connect this message from SignalRSession"); hubConnection = HubConnectionBuilder.create(url) .build(); setOn(); hubConnection.start().blockingAwait(); heartCheck(); isRunning = true; }
傳送訊息就不多說了,就是包一下。這裡加try是為了保證特殊原因連線丟失的情況下,呼叫send方法不會出錯。
斷開連線的時候把心跳迴圈停掉,連線狀態也是理所當然的變成false,然後是hubConnection的stop。
建立連線的話,就是把url傳入,這裡的url是在這個類初始化的時候拿到的。setOn是我自己寫的建立監聽的函式,傳送過來訊息都會在setOn中收到,然後通過handler發出去。然後開始的時候要建立心跳連線。當然這塊可以放到初始化裡。可以優化下。
public SignalRChannel(String url1, android.os.Handler handler) { this.url = url1; this.receiveHandler = handler; }
這是這個類的構造器,url用來建立連線就不多說。這個handler是為了傳送訊息以及更上層接收訊息。
到此為止,第一層封裝完了。
接下來是第二層,實現了斷線重連,訊息去重,記錄資料庫等操作。資料庫選用的框架用的是room。
這一塊操作比較多,可能會講的有點亂。到時候可以看看我的demo消化下。
public ReliableClient(String url1, Context context) { this.url = url1; this.context = context; //建立資料庫,如果存在不會重複建立 db = Room.databaseBuilder(context, AppDatabase.class, "database-name").build(); recordDb = Room.databaseBuilder(context, recordDatabase.class,"database-name1").build(); loadData(); logFile = new LogFile(context); Thread t = new Thread(runnableSend); t.start(); }
這個是構造器,第一個資料庫用來存收到的資料,第二個資料庫用來處理進度(處理到第幾個資料了) 。loadData是獲取進度,即剛剛的資料庫。logFile是我自己寫的類,用於寫日誌。然後這個執行緒啟動的是短線重連。這裡一個ReliableClient可以用單例來實現。
private void loadData() { Runnable runnable = new Runnable() { @Override public void run() { if(recordDb.recordDao().databaseCount()<1){ //資料庫沒有資料,設定為預設值 curRecvSeq = -1; authMessage = null; Log.i(TAG,"load <1 "); }else if(recordDb.recordDao().databaseCount() == 1){ //資料庫一條資料,取這條資料 recordData messageData = recordDb.recordDao().getRecord(); curRecvSeq = messageData.curRecvSeq; authMessage = new AuthRequest(messageData.ClientType,messageData.Token,messageData.UserId,messageData.Version); if(authMessage.ClientType == -1){ authMessage = null; } Log.i(TAG,"load = 1 "+curRecvSeq); }else { Log.i(TAG,"qweq: "+recordDb.recordDao().databaseCount()); //資料庫很多資料,取最後一條的資料 recordData messageData = recordDb.recordDao().getRecord(); curRecvSeq = messageData.curRecvSeq; authMessage = new AuthRequest(messageData.ClientType,messageData.Token,messageData.UserId,messageData.Version); recordDb.recordDao().deleteAll(); recordData record1 = new recordData(); record1.Token = authMessage.Token; record1.curRecvSeq = messageData.curRecvSeq; record1.Version = authMessage.version; record1.ClientType = authMessage.ClientType; record1.UserId = authMessage.UserId; recordDb.recordDao().insertAll(record1); if(authMessage.ClientType == -1){ authMessage = null; } Log.i(TAG,"load > 1 "+curRecvSeq); } } }; new Thread(runnable).start(); if(curRecvSeq != -1){ //如果有操作記錄,那麼查詢資料庫,取出未處理的資料,發給flutter。 List<MessageData> messageDataList = db.userDao().getAll(); for(MessageData messageData : messageDataList){ //未操作資料壓入雜湊表 hTable.put(messageData.seq,messageData); curRecvSeq ++; } } Log.i(TAG,"load msg :"+curRecvSeq); }
這個資料庫理論上只能存在一條資料,因為是記錄嘛,然後這裡的邏輯是,當資料庫沒有資料時,給他一個預設值,標記為初次啟動。當一條資料的時候讀取這條資料。當出現不可抗力時,出現了多條資料,取出最後一條資料,然後刪庫不跑路。把這最後一條記錄插進資料庫。這個記錄是為了獲取登入資訊的,賬號,token等。這樣他從後臺啟動起來的時候,還是處於連線狀態。
下面是斷線重連機制以及訊息傳送佇列機制
private Runnable runnableSend = new Runnable() { @Override public void run() { while(isRunning){ try {//重新整理連線狀態 if(signalRChannel == null || !signalRChannel.isConnected()){ try{ reConnect(); }catch (Exception e){ e.printStackTrace(); continue; } } while(!sendMessageQueue.isEmpty()){ //傳送訊息 SendMessage sendMessage = sendMessageQueue.poll(); signalRChannel.send(sendMessage.method, sendMessage.message); } if(!logFile.fileStatus){ logFile.openLog(); } Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } } };
這裡還是跑一個死迴圈執行緒,反覆確認連線狀態,如果斷開連線的話,執行重連。訊息傳送也比較簡單,放在一個佇列裡,順便傳送出去。
下面是重連,比較簡單就這樣看吧。
private void reConnect() { if(signalRChannel != null){ signalRChannel.stopConnect(); } signalRChannel = new SignalRChannel(url,receiveHandler); signalRChannel.startConnect(); if(authMessage!=null){ signalRChannel.send("Auth",authMessage); } }
下面是三個開放給外部的方法
//傳送訊息 public void send(String method,Object... messages){ /** * queue * */ if(method.equals("Echo")){ long time = System.currentTimeMillis()/1000; signalRChannel.send(method,String.valueOf(time/1000)); }else { SendMessage sendMessage = new SendMessage(method,messages); sendMessageQueue.offer(sendMessage); } } //登入 public void LogIn(AuthRequest authRequest){ this.authMessage = authRequest; //todo: write file // signalRChannel.send("Auth",authMessage); } //登出 public void LogOut(){ authMessage = null; if(signalRChannel != null){ signalRChannel.stopConnect(); } if(logFile.fileStatus){ logFile.closeLog(); } }
傳送訊息的時候給他壓進訊息佇列裡,等一段時間傳送。當然我這裡設定時間是4秒,有點不合理,這個需要自己改一下。
然後這裡的登入登出只是狀態登出了,長連結是一直存在的。
登出是先把authMessage清空,然後斷開重連一下,就斷開了。
登入只是記錄下他的登入資訊,因為我們登入走的是另外的方法。
這裡大致是這樣了,其他很多程式碼都是跟我們自己的業務相關,我會覺得不具有參考性,就不列出來了。
最後貼一下demo地址:https://github.com/libo1223/signalR