WebSocket Androidクライアント実装の詳細(一)--接続の確立と再接続


WebSocket Androidクライアント実装の詳細(一)-接続の確立と再接続
今年の会社の最初の需要はwebsocketに基づいてクライアントメッセージセンターを書くことで、今では長い間運転手のようなネットワーク環境で1日平均8回連続していて、いいと思っています.その时に书いた时、その心はつらかったですね.主に初めて书いたので、どこから手をつけたらいいか分からなかったので、方向がありませんでした.だからここではできるだけ詳しく皆さんと分かち合います.
本編の内容は比較的に多いので、まずダンスをして体を温めます.
以下の手順で説明するつもりです
  • 全体の流れの一つの概括は大体の構想を理解する.
  • 大体の流れを細分化し、徐々に実現する.

  • 前言
    ここでは特にWebSocketサービス端は会社のオンラインプロジェクトなのでurlと具体的な協議は全部消しましたが、できるだけ皆さんに説明します.demoはテストしたことがあります.
    私たちはまず荒々しく流れを話して、大まかな方向を把握して、それから細部の実現を深く説明します.ここでまず疑問に答えます.なぜ私たちはSocketではなくWebSocketを使うのでしょうか.WebSocketはアプリケーション層プロトコルです.多くのものが規定されています.私たちは直接彼の規定に従って使えばいいのです.Socketは転送層とアプリケーション層の抽象層です.多くのものは私たちが自分で規定しなければなりません.相対的に面倒です.だから、ここではWebSocketを使っています.
    WebSocketはアプリケーション層プロトコルである以上、私たちは自分で実現することはできないに違いないので、最初のステップはこのプロトコルを実現したフレームワークを探す必要があります.ここで私が使っているnv-websocket-client、apiは紹介しません.ライブラリのreadmeは詳しく紹介しました.後で直接使用します.
    通信プロトコルについて便利にするために、ここではjsonを使用しています.
    次に、私たちがすべきことを簡単に説明します.
    ユーザーログインプロセス
    最初のステップはユーザーがアカウントのパスワードを入力してログインに成功した後、私たちはwebsocketプロトコルを通じて接続を確立し、接続に失敗したコールバックの時に接続に成功するまで再接続を試み、もちろんこの再接続の時間間隔は私が再接続の失敗回数によって一定の規則で書いた具体的な後に話します.
    第2のステップでは、接続が成功すると、バックグラウンドで長接続送信要求によってそのユーザの身分、すなわち上図の授権を検証する必要がある.前のユーザ登録が成功した以上、一般的に授権は失敗しないので、ここでは授権失敗について処理していない.授権に成功した後、私たちは心拍を開き、同期データ要求をサービス側に送信してまだ受信していないメッセージを取得する.
    クライアント送信要求プロセス
    第1のステップは、要求パラメータを要求オブジェクトにカプセル化する、タイムアウトタスクを追加し、その要求のコールバックをコールバックセットに追加する.
    ここで説明する必要があるのは、要求パラメータをカプセル化する際に、ここに2つのパラメータseqIdとreqCountが追加されたことです.ここでは、長接続要求によってサービス側が応答するときに対応するコールバックを見つけるために、各要求はサービス側に一意の識別を伝えて要求を識別する必要があります.ここで私が使用しているseqIdは、要求が成功した後、サービス側がseqIdを再送します.このseqIdをkeyとしてコールバックセットから対応するコールバックを見つける.一方、reqCountの場合は主にリクエストがタイムアウトする場合について、リクエストがタイムアウトすると、2回目のリクエストのときにreqCount++をrequestに入れ、同じリクエスト回数が3回以上のときにhttp補償チャネルを歩くことを約束します.requestのreqCount>3のとき、私たちはhttpを通じてリクエストを送信し、応答に応じて対応結果をコールバックします.
    第2のステップは、要求を開始する、成功または失敗した場合、seqIdによって対応するコールバック実行を見つけ、コールバックセットからコールバックを除去し、タイムアウトタスクをキャンセルする.タイムアウトするとseqIdによって対応するコールバックが取得するコールバックセットから削除され、リクエスト回数が3回以下であれば再度websocketによってリクエストを試行し、3回以上httpによってリクエストが成功失敗した場合に対応するコールバックが実行すると判断する.
    サービス側アクティブプッシュメッセージプロセス
    まず、ここでサービス側がプッシュするメッセージはイベントにすぎず、具体的なメッセージは携帯されないことを説明する.
    第1のステップは、notifyにおけるイベントタイプに基づいて対応する処理クラスを見つける、一般的には、ここでは対応するデータを同期する必要がある.
    次にeventbusで対応するuiインタフェースの更新を通知します
    ステップ3 ackが必要な場合は、ackリクエストを送信します.
    上はただ1つの概括で、ドキドキに対して、再接続して、ここに多くの細部に注意しなければならない次の節を送って詳しく説明します
    具体的な実装
    理論は終わり、次にクライアントコードを一歩一歩実現します.まず依存を追加します
        compile 'com.neovisionaries:nv-websocket-client:2.2'

    次に、グローバルコールのために単一のWsManager管理websocketを作成します.
    public class WsManager {
    
        private static WsManager mInstance;
    
        private WsManager() {
        }
    
        public static WsManager getInstance(){
            if(mInstance == null){
                synchronized (WsManager.class){
                    if(mInstance == null){
                        mInstance = new WsManager();
                    }
                }
            }
            return mInstance;
        }
    }

    接続の確立
    次に接続コードを追加します.ここではWebSocketプロトコルの操作についてnv-websocket-clientを使用しています.私も詳細なコメントを追加しました.readmeファイルを一度読んでもいいですか.
    public class WsManager {
        private static WsManager mInstance;
        private final String TAG = this.getClass().getSimpleName();
    
        /** * WebSocket config */
        private static final int FRAME_QUEUE_SIZE = 5;
        private static final int CONNECT_TIMEOUT = 5000;
        private static final String DEF_TEST_URL = "     ";//       
        private static final String DEF_RELEASE_URL = "     ";//       
        private static final String DEF_URL = BuildConfig.DEBUG ? DEF_TEST_URL : DEF_RELEASE_URL;
        private String url;
    
        private WsStatus mStatus;
        private WebSocket ws;
        private WsListener mListener;
    
        private WsManager() {
        }
    
        public static WsManager getInstance(){
            if(mInstance == null){
                synchronized (WsManager.class){
                    if(mInstance == null){
                        mInstance = new WsManager();
                    }
                }
            }
            return mInstance;
        }
    
        public void init(){
            try {
              /** * configUrl              *            app       http         , *   app                  ,  6                        */
                String configUrl = "";
                url = TextUtils.isEmpty(configUrl) ? DEF_URL : configUrl;
                ws = new WebSocketFactory().createSocket(url, CONNECT_TIMEOUT)
                    .setFrameQueueSize(FRAME_QUEUE_SIZE)//         5
                    .setMissingCloseFrameAllowed(false)//                   
                    .addListener(mListener = new WsListener())//      
                    .connectAsynchronously();//    
                setStatus(WsStatus.CONNECTING);
                Logger.t(TAG).d("     ");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    
        /** *           WebSocketAdapter,          * onTextMessage        * onConnected      * onConnectError      * onDisconnected      */
        class WsListener extends WebSocketAdapter{
            @Override
            public void onTextMessage(WebSocket websocket, String text) throws Exception {
                super.onTextMessage(websocket, text);
                Logger.t(TAG).d(text);
            }
    
    
            @Override
            public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
                throws Exception {
                super.onConnected(websocket, headers);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_SUCCESS);
            }
    
    
            @Override
            public void onConnectError(WebSocket websocket, WebSocketException exception)
                throws Exception {
                super.onConnectError(websocket, exception);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_FAIL);
            }
    
    
            @Override
            public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer)
                throws Exception {
                super.onDisconnected(websocket, serverCloseFrame, clientCloseFrame, closedByServer);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_FAIL);
            }
        }
    
        private void setStatus(WsStatus status){
            this.mStatus = status;
        }
    
        private WsStatus getStatus(){
            return mStatus;
        }
    
        public void disconnect(){
            if(ws != null)
            ws.disconnect();
        }
    }
    public enum WsStatus {
        CONNECT_SUCCESS,//    
        CONNECT_FAIL,//    
        CONNECTING;//    
    }

    注記から分かるように、ここではappが起動するときにhttpリクエストでWebSocket接続アドレスを取得し、取得に失敗するとローカルのデフォルトurlで接続を確立します.そして内部は自分で1つのwebsocket状態を維持した後に要求を送信して再接続する時に使います.
    実は接続アドレスを取得するこの場所は最適化することができて、appが起動する時先に前回取得した時間を比較して6時間より大きいならhttpを通じてwebsocketの接続アドレスを取得することを要求して、このアドレスはリストであるべきで、それからローカルに保存して、接続する時私達は先にアドレスをpingして、時間の最も短いアドレスアクセスを選択することができます.もし私たちが2番目に短いアドレスに接続できなければ、このようにします.しかし、ここでは簡単な方法でやりました.
    接続コードがどこで呼び出されるかについては、メインインタフェースonCreate()を選択したとき、一般的にメインインタフェースに入ることができるので、ユーザーが登録に成功したことを意味します.
    WsManager.getInstance().init();

    接続を切断するとメインインタフェースonDestroy()で呼び出されます
    WsManager.getInstance().disconnect();

    さいけつごう
    接続の確立に成功すれば失敗があり、失敗した場合には再接続が必要である.では、再接続のタイミング、再接続の戦略、現在再接続すべきかどうかの判断をそれぞれ説明する.
    再接続のタイミングには次のような状況があります.再接続を試してみる必要があります.
  • アプリケーションネットワークの切り替え.具体的には、利用可能なネットワーク状態の切り替えです.例えば、4 g切wifi接続が切断されると、再接続が必要になります.
  • アプリケーションがフロントに戻ると、接続が切断すると再接続が必要と判断するが、これはアプリケーションがフロントに戻るときの接続をできるだけ安定させることである.
  • 接続が失敗したり、接続が切断されたりした場合、これは説明できません.
  • 心拍数連続3回失敗時.もちろんこの連続失敗は3回も自分で定義したので、みんなは自分のappの状況によってカスタマイズすることができます.

  • 前の3つの状況を先に示します.ドキドキが失敗しました.これは後でクライアントに要求を送ってからにしましょう.
    上は再接続が必要な情景を話したが,今は具体的な再接続戦略について話した.
    ここでは、最小再接続時間間隔minと最大再接続時間間隔maxを定義し、再接続回数が3回以下の場合は最小再接続時間間隔minで再接続を試み、再接続回数が3回以上の場合はデフォルトのアドレスDEF_に置き換えます.URLは、再接続時間間隔をmin*(再接続回数-2)で最大max以下にインクリメント.
    最後の現在再接続すべきかどうかの判断もあります
  • ユーザがログインするか否かは、ローカルにキャッシュされたユーザ情報があるか否かで判断することができる.再接続に成功すると、WebSocketを介してサーバにユーザ情報を送信認証する必要があるため、ここではログインに成功する必要があります.
  • の現在の接続が利用可能かどうかは、nv-websocket-clientライブラリのapiによってws.isOpen()を判断する.
  • は現在接続中の状態ではない、ここでは自己メンテナンスの状態からgetStatus() != WsStatus.CONNECTINGを判断する.
  • 現在のネットワークが利用可能である.

  • 次はショーだ前と同じコードはここでは省略します
    public class WsManager {
    
        .....           .....
    
        /** *           WebSocketAdapter,          * onTextMessage        * onConnected      * onConnectError      * onDisconnected      */
        class WsListener extends WebSocketAdapter {
            @Override
            public void onTextMessage(WebSocket websocket, String text) throws Exception {
                super.onTextMessage(websocket, text);
                Logger.t(TAG).d(text);
            }
    
    
            @Override
            public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
                throws Exception {
                super.onConnected(websocket, headers);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_SUCCESS);
                cancelReconnect();//           ,       
            }
    
    
            @Override
            public void onConnectError(WebSocket websocket, WebSocketException exception)
                throws Exception {
                super.onConnectError(websocket, exception);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_FAIL);
                reconnect();//             
            }
    
    
            @Override
            public void onDisconnected(WebSocket websocket, WebSocketFrame serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer)
                throws Exception {
                super.onDisconnected(websocket, serverCloseFrame, clientCloseFrame, closedByServer);
                Logger.t(TAG).d("    ");
                setStatus(WsStatus.CONNECT_FAIL);
                reconnect();//             
            }
        }
    
    
        private void setStatus(WsStatus status) {
            this.mStatus = status;
        }
    
    
        private WsStatus getStatus() {
            return mStatus;
        }
    
    
        public void disconnect() {
            if (ws != null) {
                ws.disconnect();
            }
        }
    
    
        private Handler mHandler = new Handler();
    
        private int reconnectCount = 0;//    
        private long minInterval = 3000;//        
        private long maxInterval = 60000;//        
    
    
        public void reconnect() {
            if (!isNetConnect()) {
                reconnectCount = 0;
                Logger.t(TAG).d("         ");
                return;
            }
    
            //                                              
            //        demo     
            if (ws != null &&
                !ws.isOpen() &&//       
                getStatus() != WsStatus.CONNECTING) {//        
    
                reconnectCount++;
                setStatus(WsStatus.CONNECTING);
    
                long reconnectTime = minInterval;
                if (reconnectCount > 3) {
                    url = DEF_URL;
                    long temp = minInterval * (reconnectCount - 2);
                    reconnectTime = temp > maxInterval ? maxInterval : temp;
                }
    
                Logger.t(TAG).d("     %d   ,    %d -- url:%s", reconnectCount, reconnectTime, url);
                mHandler.postDelayed(mReconnectTask, reconnectTime);
            }
        }
    
    
        private Runnable mReconnectTask = new Runnable() {
    
            @Override
            public void run() {
                try {
                    ws = new WebSocketFactory().createSocket(url, CONNECT_TIMEOUT)
                        .setFrameQueueSize(FRAME_QUEUE_SIZE)//         5
                        .setMissingCloseFrameAllowed(false)//                   
                        .addListener(mListener = new WsListener())//      
                        .connectAsynchronously();//    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        };
    
    
        private void cancelReconnect() {
            reconnectCount = 0;
            mHandler.removeCallbacks(mReconnectTask);
        }
    
    
        private boolean isNetConnect() {
            ConnectivityManager connectivity = (ConnectivityManager) WsApplication.getContext()
                .getSystemService(Context.CONNECTIVITY_SERVICE);
            if (connectivity != null) {
                NetworkInfo info = connectivity.getActiveNetworkInfo();
                if (info != null && info.isConnected()) {
                    //         
                    if (info.getState() == NetworkInfo.State.CONNECTED) {
                        //           
                        return true;
                    }
                }
            }
            return false;
        }
    }

    上記のコードはhandlerによって一定時間間隔の再接続を実現する、その後、WsListenerの傍受中のonConnectError()onDisconnected()reconnect()を呼び出して再接続を実現し、onConnected()cancelReconnect()を呼び出して再接続をキャンセルして再接続回数を初期化する.
    したがって、再接続が必要な場合、reconnect()の方法を呼び出し、失敗した場合、onConnectError()onDisconnected()のコールバックが再びreconnect()を呼び出して再接続を実現し、成功した場合、onConnected()cancelReconnect()を呼び出して再接続をキャンセルし、再接続回数を初期化する.
    また、ここでは、再接続が必要なシナリオ3を実現する、接続失敗や接続切断イベントを受信したときに再接続を行う.
    次にシナリオ1と2を実現します
  • アプリケーションネットワークの切り替え.具体的には、利用可能なネットワーク状態の切り替えです.例えば、4 g切wifi接続が切断されると、再接続が必要になります.
  • アプリケーションがフロントに戻ると、接続が切断すると再接続が必要と判断するが、これはアプリケーションがフロントに戻るときの接続をできるだけ安定させることである.

  • 利用可能なネットワークの切り替えについては、ここではブロードキャストによるリスニングによる再接続を実現する
    public class NetStatusReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
    
                //          
                ConnectivityManager connectivityManager
                    = (ConnectivityManager) WsApplication.getContext()
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
                //           
                NetworkInfo info = connectivityManager.getActiveNetworkInfo();
    
                if (info != null && info.isAvailable()) {
                    Logger.t("WsManager").d("         ,      ");
                    WsManager.getInstance().reconnect();//wify 4g    websocket
                }
    
            }
        }
    }

    フロントに戻る場合の再接続を適用する.Application.ActivityLifecycleCallbacksによりappフロントバックグラウンド切り替えリスニングを実現する
    public class ForegroundCallbacks implements Application.ActivityLifecycleCallbacks {
    
        public static final long CHECK_DELAY = 600;
        public static final String TAG = ForegroundCallbacks.class.getName();
        private static ForegroundCallbacks instance;
        private boolean foreground = false, paused = true;
        private Handler handler = new Handler();
        private List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
        private Runnable check;
    
        public static ForegroundCallbacks init(Application application) {
            if (instance == null) {
                instance = new ForegroundCallbacks();
                application.registerActivityLifecycleCallbacks(instance);
            }
            return instance;
        }
    
        public static ForegroundCallbacks get(Application application) {
            if (instance == null) {
                init(application);
            }
            return instance;
        }
    
        public static ForegroundCallbacks get(Context ctx) {
            if (instance == null) {
                Context appCtx = ctx.getApplicationContext();
                if (appCtx instanceof Application) {
                    init((Application) appCtx);
                }
                throw new IllegalStateException(
                        "Foreground is not initialised and " +
                                "cannot obtain the Application object");
            }
            return instance;
        }
    
        public static ForegroundCallbacks get() {
    
            return instance;
        }
    
        public boolean isForeground() {
            return foreground;
        }
    
        public boolean isBackground() {
            return !foreground;
        }
    
        public void addListener(Listener listener) {
            listeners.add(listener);
        }
    
        public void removeListener(Listener listener) {
            listeners.remove(listener);
        }
    
        @Override
        public void onActivityResumed(Activity activity) {
            paused = false;
            boolean wasBackground = !foreground;
            foreground = true;
            if (check != null)
                handler.removeCallbacks(check);
            if (wasBackground) {
    
                for (Listener l : listeners) {
                    try {
                        l.onBecameForeground();
                    } catch (Exception exc) {
    
                    }
                }
            } else {
    
            }
        }
    
        @Override
        public void onActivityPaused(Activity activity) {
            paused = true;
    
            if (check != null)
                handler.removeCallbacks(check);
            handler.postDelayed(check = new Runnable() {
                @Override
                public void run() {
                    if (foreground && paused) {
                        foreground = false;
                        for (Listener l : listeners) {
                            try {
                                l.onBecameBackground();
                            } catch (Exception exc) {
    
                            }
                        }
                    } else {
    
                    }
                }
            }, CHECK_DELAY);
        }
    
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }
    
        @Override
        public void onActivityStarted(Activity activity) {
        }
    
        @Override
        public void onActivityStopped(Activity activity) {
        }
    
        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }
    
        @Override
        public void onActivityDestroyed(Activity activity) {
        }
    
        public interface Listener {
            public void onBecameForeground();
    
            public void onBecameBackground();
        }
    }

    次にアプリケーションでリスニングを初期化し、アプリケーションがフロントに戻ったときに再接続を試みます.
    public class WsApplication extends Application {
    
    
        @Override
        public void onCreate() {
            super.onCreate();
            initAppStatusListener();
        }
    
        private void initAppStatusListener() {
            ForegroundCallbacks.init(this).addListener(new ForegroundCallbacks.Listener() {
                @Override
                public void onBecameForeground() {
                    Logger.t("WsManager").d("            ");
                    WsManager.getInstance().reconnect();
                }
    
                @Override
                public void onBecameBackground() {
    
                }
            });
        }
    }

    ここで接続の確立と再接続が完了すると、クライアントからの要求とサービス側のアクティブな通知メッセージが残っている.
    WebSocketクライアントの実装を書き上げるつもりだったが、今では半分しか残っていないので、いっそ2つに分けておこう.次はクライアントの送信要求とサービス側の自発的な通知メッセージを紹介する.
    ここに本編のソースコードWebSocketアンドロイドクライアント実装の詳細を添付する(一)–接続確立と再接続ソースコード転送ゲート