基於OKHttp的websocket封裝使用


Demo源碼請點擊(網絡庫中websocket部分)

1.背景

一般使用到websocket協議的應用場景都是持續保持長連接,直到業務處理完畢,不再需要保持連接時,則close掉連接。那麼官方給出的指導使用文檔足咦。我近期工作上接到的任務是,通過websocket協議,流式接收數據。一次任務結束後要求關掉連接,下次任務再重新建立連接。要求減少服務器併發連接數,忽略建立連接的資源消耗。所以本篇文章是介紹再實例化連接的client之後,不關閉分發執行service的前提下,每次都新建websocket連接。整個源碼放在了我封裝的網絡請求框架裏。

2.源碼解析

2.1基礎封裝

public class WebSocketClient {
    private Request request;
    private OkHttpClient client;
    private WebSocket webSocket;

    public WebSocketClient() {
        client = new OkHttpClient();
        request = new Request.Builder()
                .url("ws://echo.websocket.org")
                .build();
    }

    public OkHttpClient getClient() {
        return client;
    }

    public void start(WebSocketListener listener) {
        client.dispatcher().cancelAll();
        HLogger.d("request id = " + request.toString());
        HLogger.d("listener id = " + listener.toString());
        webSocket = client.newWebSocket(request, listener);
        HLogger.d("webSocket id = " + webSocket.toString());
    }

    public void close() {
        if (webSocket != null) {
            webSocket.close(1000, null);
        }
        client.dispatcher().executorService().shutdown();
    }
}

以上是對Request、OkHttpClient、WebSocket的封裝。基本保證只實例化一份Request、OkHttpClient,每次新任務newWebSocket。回調listener可以複用。

2.2使用

	HsjWebSocketListener listener = new HsjWebSocketListener();
    StringBuilder stringBuilder = new StringBuilder();
    
    public void initSocket(View view) {
        webSocketClient = new WebSocketClient();
    }
    
    public void webSocket(View view) {
        if (webSocketClient == null) {
            Toast.makeText(this, "請初始化Socket", Toast.LENGTH_SHORT).show();
            return;
        }
        stringBuilder.setLength(0);
        stringBuilder.append(System.currentTimeMillis() + "-onClick\n");
        output();
        webSocketClient.start(listener);
    }

    public void closeSocket(View view) {
        webSocketClient.close();
    }
    
    private class HsjWebSocketListener extends WebSocketListener {
        @Override
        public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
            stringBuilder.append(System.currentTimeMillis() + "\n");
            webSocket.send("hello world");
            webSocket.send("welcome");
            webSocket.send(ByteString.decodeHex("adef"));
            webSocket.close(1000, "再見");
        }

        @Override
        public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
            stringBuilder.append(System.currentTimeMillis() + "-onMessage: " + text + "\n");
            output();
        }

        @Override
        public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
            stringBuilder.append(System.currentTimeMillis() + "-onMessage byteString: " + bytes + "\n");
            output();
        }

        @Override
        public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
            HLogger.d("onFailure: " + t.getMessage());
            stringBuilder.append(System.currentTimeMillis() + "-onFailure: " + t.getMessage() + "\n");
            output();
        }

        @Override
        public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
            stringBuilder.append(System.currentTimeMillis() + "-onClosing: " + code + "/" + reason + "\n");
            output();
        }

        @Override
        public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
            stringBuilder.append(System.currentTimeMillis() + "-onClosed: " + code + "/" + reason + "\n");
            output();
        }
    }

    private void output() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textView.setText(stringBuilder.toString());
            }
        });
    }
  1. 先實現一個WebSocketListener,並聲明實例化它listener。這是準備工作。
  2. 實例化WebSocketClient。
  3. 任務開始調用webSocketClient.start(listener);
  4. 若再一個新任務,再次調用webSocketClient.start(listener);
  5. 若所有任務結束,頁面關閉等情況。調用webSocketClient.close();關閉client。
  6. 單次任務,一般會在onMessage方法中返回是否是最後一條數據信息的標記,若是本次連接的最後一條信息返回。可以主動webSocket.close(1000, “再見”);這個方法只是關閉掉本次連接的websocket。

3.相關知識點

3.1 websocket協議

websocket協議是基於http協議的升級。也就是說客戶端先與服務器端通過http協議幾次握手建立連接。連接建立後okhttp中封裝的websocket框架中,WebSocketListener的onOpen會被回調,即此時已經建立好長連接,可以進行具體業務通信了。

3.1.1、客戶端:申請協議升級

建立連接時的通信協議採用的是標準的HTTP報文格式,且只支持GET方法。

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

重點請求首部意義如下:

  • Connection: Upgrade:表示要升級協議
  • Upgrade: websocket:表示要升級到websocket協議。
  • Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,裏面包含服務端支持的版本號。
  • Sec-WebSocket-Key:與後面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。

注意,上面請求省略了部分非重點請求首部。由於是標準的HTTP請求,類似Host、Origin、Cookie等請求首部會照常發送。在握手階段,可以通過相關請求首部進行 安全限制、權限校驗等。

3.1.2、服務端:響應協議升級

服務端返回內容如下,狀態代碼101表示協議切換。到此完成協議升級,協議升級完成後,後續的數據交換則遵照WebSocket的協議。

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

備註:每個header都以\r\n結尾,並且最後一行加上一個額外的空行\r\n。此外,服務端迴應的HTTP狀態碼只能在握手階段使用。過了握手階段後,就只能採用特定的錯誤碼。

3.1.3、數據幀

webscoket協議中客戶端、服務器端通信是以數據幀的格式,非http協議中文本信息格式。
WebSocket客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。

發送端:將消息切割成多個幀,併發送給服務端;
接收端:接收消息幀,並將關聯的幀重新組裝成完整的消息;

數據幀格式概覽

 0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

具體對數據幀的解讀,各位可以另行搜索相關文章。

3.2 返回數據

websocket回調的WebSocketListener中,各方法返回的數據都在非主線程中。所以若有界面展示需要更新UI的需求,需要通過handler或者runOnUiThread拋到主線程中更新。

Demo源碼請點擊(網絡庫中websocket部分)

發佈了39 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章