1. 程式人生 > 實用技巧 >android封裝signalR的demo

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