用OkHttp實現WebSocket長連接
今天給大家帶來一篇老文章,介紹WebSocket,大家可以了解了解。
前言
最近老板又來新需求了,要做一個物聯網相關的app,其中有個需求是客戶端需要收發服務器不定期發出的消息。
內心OS:
- 這咋整呢?通過接口輪詢?定時訪問接口,有數據就更新?
- 不行不行,這樣浪費資源了,還耗電,會導致很多請求都是無效的網絡操作。
- 那就長連接唄?WebSocket協議好像不錯,通過握手建立長連接后,可以隨時收發服務器的消息。那就它了!
- 怎么集成呢?正好前段時間復習OkHttp源碼的時候發現了它是支持Websocket協議的,那就用它試試吧!
開淦!
WebSocket介紹
先簡單介紹下WebSocket。我們都知道Http是處于應用層的一個通信協議,但是只支持單向主動通信,做不到服務器主動向客戶端推送消息。而且Http是無狀態的,即每次通信都沒有關聯性,導致跟服務器關系不緊密。
為了解決和服務器長時間通信的痛點呢,HTML5規范引出了WebSocket協議(知道這名字咋來的吧,人家HTML5規范引出的,隨爸姓),是一種建立在TCP協議基礎上的全雙工通信的協議。他跟Http同屬于應用層協議,下層還是需要通過TCP建立連接。
但是,WebSocket在TCP連接建立后,還要通過Http進行一次握手,也就是通過Http發送一條GET請求消息給服務器,告訴服務器我要建立WebSocket連接了,你準備好哦,具體做法就是在頭部信息中添加相關參數。然后服務器響應我知道了,并且將連接協議改成WebSocket,開始建立長連接。
這里貼上請求頭和響應頭信息,從網上找了一張圖:
3851594110877_.pic.jpg
簡單說明下參數:
- URL一般是以ws或者wss開頭,ws對應Websocket協議,wss對應在TLS之上的WebSocket。類似于Http和Https的關系。
- 請求方法為GET方法。
- Connection:Upgrade,表示客戶端要連接升級,不用Http協議。
- Upgrade:websocket, 表示客戶端要升級建立Websocket連接。
- Sec-Websocket-Key:key, 這個key是隨機生成的,服務器會通過這個參數驗證該請求是否有效。
- Sec-WebSocket-Version:13, websocket使用的協議,一般就是13。
- Sec-webSocket-Extension:permessage-deflate,客戶端指定的一些擴展協議,比如這里permessage-deflate就是WebSocket的一種壓縮協議。
- 響應碼101,表示響應協議升級,后續的數據交互都按照Upgradet指定的WebSocket協議來。
OkHttp實現
添加OkHttp依賴
- implementation("com.squareup.okhttp3:okhttp:4.7.2")
實現代碼
首先是初始化OkHttpClient和WebSocket實例:
- /**
- * 初始化WebSocket
- */
- public void init() {
- mWbSocketUrl = "ws://echo.websocket.org";
- mClient = new OkHttpClient.Builder()
- .pingInterval(10, TimeUnit.SECONDS)
- .build();
- Request request = new Request.Builder()
- .url(mWbSocketUrl)
- .build();
- mWebSocket = mClient.newWebSocket(request, new WsListener());
- }
這里主要是配置了OkHttp的一些參數,以及WebSocket的連接地址。其中newWebSocket方法就是進行WebSocket的初始化和連接。
這里要注意的點是pingInterval方法的配置,這個方法主要是用來設置WebSocket連接的保活。相信做過長連接的同學都知道,一個長連接一般要隔幾秒發送一條消息告訴服務器我在線,而服務器也會回復一個消息表示收到了,這樣就確認了連接正常,客戶端和服務器端都在線。
如果服務器沒有按時收到這個消息那么服務器可能就會主動關閉這個連接,節約資源。客戶端沒有正常收到這個返回的消息,也會做一些類似重連的操作,所以這個保活消息非常重要。
我們稱這個消息叫作心跳包,一般用PING,PONG表示,像乒乓球一樣,一來一回。所以這里的pingInterval就是設置心跳包發送的間隔時間,設置了這個方法之后,OkHttp就會自動幫我們發送心跳包事件,也就是ping包。當間隔時間到了,沒有收到pong包的話,監聽事件中的onFailure方法就會被調用,此時我們就可以進行重連。
但是由于實際業務需求不一樣,以及okhttp中心跳包事件給予我們權限較少,所以我們也可以自己完成心跳包事件,即在WebSocket連接成功之后,開始定時發送ping包,在下一次發送ping包之前檢查上一個pong包是否收到,如果沒收到,就視為異常,開始重連。感興趣的同學可以看看文末的相關源碼。
建立連接后,我們就可以正常發送和讀取消息了,也就是在上文WsListener監聽事件中表現:
- //監聽事件,用于收消息,監聽連接的狀態
- class WsListener extends WebSocketListener {
- @Override
- public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
- super.onClosed(webSocket, code, reason);
- }
- @Override
- public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
- super.onClosing(webSocket, code, reason);
- }
- @Override
- public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
- super.onFailure(webSocket, t, response);
- }
- @Override
- public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
- super.onMessage(webSocket, text);
- Log.e(TAG, "客戶端收到消息:" + text);
- onWSDataChanged(DATE_NORMAL, text);
- //測試發消息
- webSocket.send("我是客戶端,你好啊");
- }
- @Override
- public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
- super.onMessage(webSocket, bytes);
- }
- @Override
- public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
- super.onOpen(webSocket, response);
- Log.e(TAG,"連接成功!");
- }
- }
- //發送String消息
- public void send(final String message) {
- if (mWebSocket != null) {
- mWebSocket.send(message);
- }
- }
- /**
- * 發送byte消息
- * @param message
- */
- public void send(final ByteString message) {
- if (mWebSocket != null) {
- mWebSocket.send(message);
- }
- }
- //主動斷開連接
- public void disconnect(int code, String reason) {
- if (mWebSocket != null)
- mWebSocket.close(code, reason);
- }
這里要注意,回調的方法都是在子線程回調的,如果需要更新UI,需要切換到主線程。
基本操作就這么多,還是很簡單的吧,初始化Websocket——連接——連接成功——收發消息。
其中WebSocket類是一個操作接口,主要提供了以下幾個方法
- send(text: String)發送一個String類型的消息
- send(bytes: ByteString) 發送一個二進制類型的消息
- close(code: Int, reason: String?)關閉WebSocket連接
如果有同學想測試下WebSocket的功能但是又沒有實際的服務器,怎么辦呢?其實OkHttp官方有一個MockWebSocket服務,可以用來模擬服務端,下面我們一起試一下:
模擬服務器
首先集成MockWebSocket服務庫:
- implementation 'com.squareup.okhttp3:mockwebserver:4.7.2'
然后就可以新建MockWebServer,并加入MockResponse作為接收消息的響應。
- MockWebServer mMockWebServer = new MockWebServer();
- MockResponse response = new MockResponse()
- .withWebSocketUpgrade(new WebSocketListener() {
- @Override
- public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
- super.onOpen(webSocket, response);
- //有客戶端連接時回調
- Log.e(TAG, "服務器收到客戶端連接成功回調:");
- mWebSocket = webSocket;
- mWebSocket.send("我是服務器,你好呀");
- }
- @Override
- public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
- super.onMessage(webSocket, text);
- Log.e(TAG, "服務器收到消息:" + text);
- }
- @Override
- public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
- super.onClosed(webSocket, code, reason);
- Log.e(TAG, "onClosed:");
- }
- });
- mMockWebServer.enqueue(response);
這里服務器端在收到客戶端連接成功消息后,給客戶端發送了一條消息。要注意的是這段代碼要在子線程執行,因為主線程不能進行網絡操作。
然后就可以去初始化Websocket客戶端了:
- //獲取連接url,初始化websocket客戶端
- String websocketUrl = "ws://" + mMockWebServer.getHostName() + ":" + mMockWebServer.getPort() + "/";
- WSManager.getInstance().init(websocketUrl);
ok,運行項目
//運行結果 E/jimu: mWbSocketUrl=ws://localhost:38355/ E/jimu: 服務器收到客戶端連接成功回調: E/jimu: 連接成功! E/jimu: 客戶端收到消息:我是服務器,你好呀 E/jimu: 服務器收到消息:我是客戶端,你好啊
參考
https://github.com/square/okhttp
本文轉載自微信公眾號「碼上積木,可以通過以下二維碼關注。轉載本文請聯系碼上積木公眾號。