Android WebSocket長連接的實(shí)現(xiàn)

一、為什么需要 WebSocket

初次接觸 WebSocket 的人,都會問同樣的問題:我們已經(jīng)有了 HTTP 協(xié)議,為什么還需要另一個(gè)協(xié)議?它能帶來什么好處?

答案很簡單,因?yàn)?HTTP 協(xié)議有一個(gè)缺陷:通信只能由客戶端發(fā)起。

舉例來說,我們想了解今天的天氣,只能是客戶端向服務(wù)器發(fā)出請求,服務(wù)器返回查詢結(jié)果。HTTP 協(xié)議做不到服務(wù)器主動向客戶端推送信息。

這種單向請求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。我們只能使用["輪詢"]:每隔一段時(shí)候,就發(fā)出一個(gè)詢問,了解服務(wù)器有沒有新的信息,最典型的場景就是聊天室。

輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開)。因此,開發(fā)工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發(fā)明的。

二、 WebSocket的簡介

WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了。

它的最大特點(diǎn)就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于[“服務(wù)器推送技術(shù)”]的一種。


bg2017051502.png

WebSocket的特點(diǎn)包括:

  1. 建立在 TCP 協(xié)議之上,服務(wù)器端的實(shí)現(xiàn)比較容易。

2.與 HTTP 協(xié)議有著良好的兼容性。默認(rèn)端口也是80和443,并且握手階段采用 HTTP 協(xié)議,因此握手時(shí)不容易屏蔽,能通過各種 HTTP 代理服務(wù)器。

3.支持雙向通信,實(shí)時(shí)性更強(qiáng)

4.數(shù)據(jù)格式比較輕量,性能開銷小,通信高效。

5.可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù)。

6.沒有同源限制,客戶端可以與任意服務(wù)器通信。

7.協(xié)議標(biāo)識符是ws(如果加密,則為wss),服務(wù)器網(wǎng)址就是 URL。

ws://echo.websocket.org
bg2017051503.jpg

三、WebSocket 實(shí)現(xiàn)Android客戶端與服務(wù)器的長連接

Android客戶端選擇已經(jīng)成熟的框架,Java-WebSocket,GitHub地址:https://github.com/TooTallNate/Java-WebSocket

(一)引入Java-WebSocket

1、build.gradle中加入

  implementation "org.java-websocket:Java-WebSocket:1.5.1"

2、加入網(wǎng)絡(luò)請求權(quán)限

     <uses-permission android:name="android.permission.INTERNET" />

3、新建客戶端類

新建一個(gè)客戶端類并繼承WebSocketClient,需要實(shí)現(xiàn)它的四個(gè)抽象方法、構(gòu)造函數(shù)和onSetSSLParameters方法

     public class JWebSocketClient extends WebSocketClient {

    @Override
    protected void onSetSSLParameters(SSLParameters sslParameters) {
//        super.onSetSSLParameters(sslParameters);
    }

    public JWebSocketClient(URI serverUri) {
        super(serverUri, new Draft_6455());
    }

    @Override
    public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
    }

    @Override
    public void onMessage(String message) {//接收到消息時(shí)調(diào)用
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
    }

    @Override
    public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
    }
}

其中onOpen()方法在websocket連接開啟時(shí)調(diào)用,onMessage()方法在接收到消息時(shí)調(diào)用,onClose()方法在連接斷開時(shí)調(diào)用,onError()方法在連接出錯(cuò)時(shí)調(diào)用。構(gòu)造方法中的new Draft_6455()代表使用的協(xié)議版本,這里可以不寫或者寫成這樣即可。

4、建立websocket連接

建立連接只需要初始化此客戶端再調(diào)用連接方法,需要注意的是WebSocketClient對象是不能重復(fù)使用的,所以不能重復(fù)初始化,其他地方只能調(diào)用當(dāng)前這個(gè)Client。

URI uri = URI.create("ws://*******");
JWebSocketClient client = new JWebSocketClient(uri) {
    @Override
    public void onMessage(String message) {
        //message就是接收到的消息
        Log.e("JWebSClientService", message);
    }
};

為了方便對接收到的消息進(jìn)行處理,可以在這重寫onMessage()方法。初始化客戶端時(shí)需要傳入websocket地址(測試地址:ws://echo.websocket.org),websocket協(xié)議地址大致是這樣的,協(xié)議標(biāo)識符是ws(如果加密,則為wss)

ws:// ip地址 : 端口號

連接時(shí)可以使用connect()方法或connectBlocking()方法,建議使用connectBlocking()方法,connectBlocking多出一個(gè)等待操作,會先連接再發(fā)送。

try {
    client.connectBlocking();
} catch (InterruptedException e) {
    e.printStackTrace();
}

5、發(fā)送消息

發(fā)送消息只需要調(diào)用send()方法,如下

if (client != null && client.isOpen()) {
    client.send("你好");
}

6、關(guān)閉socket連接

關(guān)閉連接調(diào)用close()方法,最后為了避免重復(fù)實(shí)例化WebSocketClient對象,關(guān)閉時(shí)一定要將對象置空。

/**
 * 斷開連接
 */
private void closeConnect() {
    try {
        if (null != client) {
            client.close();
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        client = null;
    }
}

(二)WebSocket的封裝

1、新建一個(gè)客戶端類并繼承WebSocketClient

public class JWebSocketClient extends WebSocketClient {

    @Override
    protected void onSetSSLParameters(SSLParameters sslParameters) {
//        super.onSetSSLParameters(sslParameters);
    }

    public JWebSocketClient(URI serverUri) {
        super(serverUri, new Draft_6455());
    }

    @Override
    public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
    }

    @Override
    public void onMessage(String message) {//接收到消息時(shí)調(diào)用
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
    }

    @Override
    public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
    }
}

2、新建WebSocketEvent,用于傳遞消息事件

public class WebSocketEvent {
    private String message;

    public WebSocketEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

3、新建WebSocketService服務(wù),用于消息管理和保持長連接狀態(tài)

public class WebSocketService extends Service {
    private final static String TAG = WebSocketService.class.getSimpleName();

    public JWebSocketClient client;
    private JWebSocketClientBinder mBinder = new JWebSocketClientBinder();
    private final static int GRAY_SERVICE_ID = 1001;

    private static final long CLOSE_RECON_TIME = 100;//連接斷開或者連接錯(cuò)誤立即重連

    //用于Activity和service通訊
    public class JWebSocketClientBinder extends Binder {
        public WebSocketService getService() {
            return WebSocketService.this;
        }
    }

    //灰色保活
    public static class GrayInnerService extends Service {

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            startForeground(GRAY_SERVICE_ID, new Notification());
            stopForeground(true);
            stopSelf();
            return super.onStartCommand(intent, flags, startId);
        }

        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        LogUtil.i(TAG, "WebSocketService onBind");
        return mBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
            //初始化WebSocket
            initSocketClient();
            mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);//開啟心跳檢測

        //設(shè)置service為前臺服務(wù),提高優(yōu)先級
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            //Android4.3以下 ,隱藏Notification上的圖標(biāo)
            startForeground(GRAY_SERVICE_ID, new Notification());
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            //Android4.3 - Android8.0,隱藏Notification上的圖標(biāo)
            Intent innerIntent = new Intent(this, GrayInnerService.class);
            startService(innerIntent);
            startForeground(GRAY_SERVICE_ID, new Notification());
        } else {
           //Android8.0以上app啟動后通知欄會出現(xiàn)一條"正在運(yùn)行"的通知
            NotificationChannel channel = new NotificationChannel(NotificationUtil.channel_id, NotificationUtil.channel_name,
                    NotificationManager.IMPORTANCE_HIGH);
            NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            if (manager != null) {
                manager.createNotificationChannel(channel);
                Notification notification = new Notification.Builder(getApplicationContext(), NotificationUtil.channel_id_tai_bang).build();
                startForeground(GRAY_SERVICE_ID, notification);
            }
        }
        return START_STICKY;
    }

    private void initSocketClient() {
        String url = BuildConfig.WS_PERFIX;
        URI uri = URI.create(url);
        client = new JWebSocketClient(uri) {
            @Override
            public void onMessage(String message) {
                //message就是接收到的消息
                LogUtil.i(TAG, "WebSocketService收到的消息:" + message);

                EventBus.getDefault().post(new WebSocketEvent(message));
        }

            @Override
            public void onOpen(ServerHandshake handShakeData) {//在webSocket連接開啟時(shí)調(diào)用
                LogUtil.i(TAG, "WebSocket 連接成功");
            }

            @Override
            public void onClose(int code, String reason, boolean remote) {//在連接斷開時(shí)調(diào)用
                LogUtil.e(TAG, "onClose() 連接斷開_reason:" + reason);

                mHandler.removeCallbacks(heartBeatRunnable);
                mHandler.postDelayed(heartBeatRunnable, CLOSE_RECON_TIME);//開啟心跳檢測
            }

            @Override
            public void onError(Exception ex) {//在連接出錯(cuò)時(shí)調(diào)用
                LogUtil.e(TAG, "onError() 連接出錯(cuò):" + ex.getMessage());

                mHandler.removeCallbacks(heartBeatRunnable);
                mHandler.postDelayed(heartBeatRunnable, CLOSE_RECON_TIME);//開啟心跳檢測
            }
        };
        connect();
    }

    /**
     * 連接WebSocket
     */
    private void connect() {
        new Thread() {
            @Override
            public void run() {
                try {
                    //connectBlocking多出一個(gè)等待操作,會先連接再發(fā)送,否則未連接發(fā)送會報(bào)錯(cuò)
                    client.connectBlocking();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    /**
     * 發(fā)送消息
     */
    public void sendMsg(String msg) {
        if (null != client) {
            LogUtil.i(TAG, "發(fā)送的消息:" + msg);
            try {
                client.send(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public boolean onUnbind(Intent intent) {
        LogUtil.e(TAG, "Service onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        closeConnect();
        super.onDestroy();
    }

    /**
     * 斷開連接
     */
    public void closeConnect() {
        mHandler.removeCallbacks(heartBeatRunnable);
        try {
            if (null != client) {
                client.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            client = null;
        }
    }


    //    -------------------------------------WebSocket心跳檢測------------------------------------------------
    private static final long HEART_BEAT_RATE = 10 * 1000;//每隔10秒進(jìn)行一次對長連接的心跳檢測
    private Handler mHandler = new Handler();
    private Runnable heartBeatRunnable = new Runnable() {
        @Override
        public void run() {
            if (client != null) {
                if (client.isClosed()) {
                    reconnectWs();
                    LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):已關(guān)閉");
                } else if (client.isOpen()) {
                    LogUtil.d(TAG, "心跳包檢測WebSocket連接狀態(tài):已連接");
                } else {
                    LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):已斷開");
                }
            } else {
                //如果client已為空,重新初始化連接
                initSocketClient();
                LogUtil.e(TAG, "心跳包檢測WebSocket連接狀態(tài):client已為空,重新初始化連接");
            }
            //每隔一定的時(shí)間,對長連接進(jìn)行一次心跳檢測
            mHandler.postDelayed(this, HEART_BEAT_RATE);
        }
    };

    /**
     * 開啟重連
     */
    private void reconnectWs() {
        mHandler.removeCallbacks(heartBeatRunnable);
        new Thread() {
            @Override
            public void run() {
                try {
                    LogUtil.e(TAG, "開啟重連");
                    client.reconnectBlocking();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

4、在Application中開啟WebSocketService服務(wù)

    /**
     * 開啟并綁定WebSocket服務(wù)
     */
    public void startWebSocketService() {
        Intent bindIntent = new Intent(this, WebSocketService.class);
        startService(bindIntent);
        bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
    }

5、通過WebSocketService向服務(wù)器發(fā)送消息

     if (NsApplication.getInstance().getWebSocketService() != null &&
                NsApplication.getInstance().getWebSocketService().client != null && NsApplication.getInstance().getWebSocketService().client.isOpen()) {
            JSONObject jsonObject = new JSONObject();
       NsApplication.getInstance().getWebSocketService().sendMsg(jsonObject );
        } else {
            LogUtil.e(TAG, "WebSocket連接已斷開");
        }
    }

6、通過WebSocketService接收服務(wù)器發(fā)來的消息

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMessageEvent(WebSocketEvent event) {
        if (event != null) {
            LogUtil.e(TAG, "接收消息內(nèi)容:" + event.getMessage());
            }
    }

7、Application

public class NsApplication extends Application {
    private final static String TAG = NsApplication.class.getSimpleName();

    private static NsApplication instance;
    private static final String DEVICE_TOKEN = "device_token";//設(shè)備token
    public WebSocketService mWebSocketService;
    //    -------------------------------------WebSocket發(fā)送空消息心跳檢測------------------------------------------------
    private static final long HEART_BEAT_RATE = 60 * 1000;//每隔1分鐘發(fā)送空消息保持WebSocket長連接

    public static NsApplication getInstance() {
        if (instance == null) {
            instance = new NsApplication();
        }
        return instance;
    }
    private Handler mHandler = new Handler();


    private Runnable webSocketRunnable = new Runnable() {
        @Override
        public void run() {
            if (mWebSocketService != null &&
                    mWebSocketService.client != null && mWebSocketService.client.isOpen()) {
                try {
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("from","");
                    jsonObject.put("to", "");

                    LogUtil.e(TAG, "JSONObject:" + jsonObject.toString());
                    mWebSocketService.sendMsg(jsonObject.toString());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            //每隔一定的時(shí)間,對長連接進(jìn)行一次心跳檢測
            mHandler.postDelayed(this, HEART_BEAT_RATE);
        }
    };


    public WebSocketService getWebSocketService() {
        return mWebSocketService;
    }

    /**
     * 開啟并綁定WebSocket服務(wù)
     */
    public void startWebSocketService(String deviceToken) {
        Intent bindIntent = new Intent(this, WebSocketService.class);
        bindIntent.putExtra(DEVICE_TOKEN, deviceToken);
        startService(bindIntent);
        bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);

        mHandler.removeCallbacks(webSocketRunnable);
        mHandler.postDelayed(webSocketRunnable, HEART_BEAT_RATE);//開啟心跳檢測
    }


    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            //服務(wù)與活動成功綁定
            mWebSocketService = ((WebSocketService.JWebSocketClientBinder) iBinder).getService();
            LogUtil.e(TAG, "WebSocket服務(wù)與Application成功綁定");
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            //服務(wù)與活動斷開
            mWebSocketService = null;
            LogUtil.e(TAG, "WebSocket服務(wù)與Application成功斷開");
        }
    };
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評論 2 374