初試websocket

一直以來對實時通信挺感興趣,本週就抽空了解了一下websocket。javascript

websocket

WebSocket是一種網絡傳輸協議,可在單個TCP鏈接上進行全雙工通訊,位於OSI模型的應用層。html

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就能夠建立持久性的鏈接,並進行雙向數據傳輸。java

實踐是最好的老師,爲了瞭解websocket具體實現,打算用websocket與``java socket進行通訊。java socket使用的是傳輸層協議,而websocket是應用層協議,這就須要咱們手動處理數據。web

首先要了解的就是websocket的握手過程和數據幀格式。算法

websocket握手過程

請求

websocket使用http協議進行握手,首先使用http協議發送請求報文,主要是詢問服務器是否支持websocket服務,請求頭主要信息以下:數組

GET ws://localhost:7000/ HTTP/1.1
Host: localhost:7000
Connection: Upgrade
Sec-WebSocket-Key: kvMm3tIaxXRCmGHuY01eQw==
Sec-WebSocket-Version: 13
Upgrade: websocket

這裏最重要的就是Sec-WebSocket-Key,這是客戶端生成的隨機字符串並base64編碼,服務端要對此編碼進行響應。瀏覽器

響應

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
WebSocket-Location: ws://127.0.0.1:9527
Sec-WebSocket-Accept: Mf7ptCXn+TYF9XtDt8w+j9FCBEg=

最重要的是Sec-WebSocket-Accept,客戶端會對此進行驗證,不符合驗證規則都會被視爲服務端拒絕鏈接。生成規則爲客戶端Sec-WebSocket-Key去除首尾空白,鏈接固定字符串(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)以後,使用sha-1進行hash操做,結果再用base64編碼便可。服務器

java代碼實現:websocket

public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "WebSocket-Location: ws://127.0.0.1:9527\r\n";

 ServerSocket serverSocket = new ServerSocket(7000);

        while (true) {
            Socket socket = serverSocket.accept();
            // 開啓一個新線程
            Thread thread = new Thread(() -> {
                // 響應握手信息
                try {
                    // 讀取請求頭
                    byte[] bytes = new byte[10000000];
                    socket.getInputStream().read(bytes);
                    String requestHeaders = new String(bytes, StandardCharsets.UTF_8);

                    // 獲取請求頭中的
                    String webSocketKey = "";
                    for (String header : requestHeaders.split("\r\n")) {
                        if (header.startsWith("Sec-WebSocket-Key")) {
                            webSocketKey = header.split(":")[1].trim();
                        }
                    }

                    // 將webSocketKey 與 magicKey 拼接用sha1加密以後在進行base64編碼
                    String value = webSocketKey + magicKey;
                    String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

                    // 寫入返回頭 握手結束 成功創建鏈接
                    String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
                    socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
                    System.out.println("握手成功,成功創建鏈接");
                }
            }
        }

首先新建ServerSocket監聽7000端口,當tcp鏈接創建時,從inputStream中讀取客戶端發送的http字節數組,將其轉換爲字符串,此時requestHeaders的值應爲http請求頭。
image.png
從中提取出Sec-WebSocket-Key的值,再根據規則生成webSocketAccept ,拼接到定義好的RESPONSE_HEADERS,將此數據寫入socket的outputStream,客戶端收到並驗證經過後,即成功創建鏈接。網絡

數據幀格式

成功進行握手後,爲了正常通訊,還須要瞭解websocket的數據幀格式:
image.png

  • FIN:1 bit
    表示這是否是消息的最後一幀。第一幀也有多是最後一幀。 0: 還有後續幀 。1:最後一幀
  • RSV一、RSV二、RSV3:1 bit
    擴展字段,除非一個擴展通過協商賦予了非零值的某種含義,不然必須爲0
  • opcode:4 bit
    解釋 payload data 的類型,若是收到識別不了的opcode,直接斷開。分類值以下: 0:連續的幀. 1:text幀. 2:binary幀 .3 - 7:爲非控制幀而預留的 .8:關閉握手幀 .9:ping幀.A:pong幀 .B - F:爲非控制幀而預留的
  • MASK:1 bit
    標識 Payload data 是否通過掩碼處理,若是是 1,Masking-key域的數據即爲掩碼密鑰,用於解碼Payload data。協議規定客戶端數據須要進行掩碼處理,因此此位爲1
  • Payload len:7 bit | 7+16 bit | 7+64 bit
    表示了 「有效負荷數據 Payload data」,以字節爲單位: - 若是是 0~125,那麼就直接表示了 payload 長度 - 若是是 126(二進制111 1110),那麼 先存儲 0x7E(=126)接下來的兩個字節表示的 16位無符號整型數的值就是 payload 長度 - 若是是 127,那麼 先存儲 0x7F(=127)接下來的八個字節表示的 64位無符號整型數的值就是 payload 長度.
  • Masking-key:0 | 4 bytes 掩碼密鑰,全部從客戶端發送到服務端的幀都包含一個 32bits 的掩碼(若是mask被設置成1),不然爲0。一旦掩碼被設置,全部接收到的 payload data 都必須與該值以一種算法作異或運算來獲取真實值。
  • Payload data 應用發送的數據信息

基於websocket的數據幀,咱們須要實現兩個方法,一是提取數據幀中的數據,二是將數據轉化爲數據幀。

解碼數據幀

核心思想就是根據控制字段來肯定數據字段的讀取方式,將其讀取並解碼。

/**
     * 將字節數組解碼爲字符串
     * @param bytes     websocket幀字節數組
     * @return          解析爲字符串
     */
    public static String decodeMessage(byte[] bytes) {
        int col = 0;
        boolean isMask = false;
        int dataStart = 2;

        // 提取websocket幀中的mask位
        if ((bytes[1] & 0x80) == 0x80) {
            isMask = true;
        }

        // 提取playload len
        int len = bytes[1] & 0x7f;

        byte[] maskKey = new byte[4];
        if (len == 126) {
            // 若是爲126 繼續日後兩個字節讀取做爲playload len
            len = bytes[2];
            len = (len << 8) + bytes[3];
            // 如mask爲1 向後讀取4個字節做爲maskKey
            if (isMask) {
                maskKey[0] = bytes[4];
                maskKey[1] = bytes[5];
                maskKey[2] = bytes[6];
                maskKey[3] = bytes[7];
                // payload data 開始的位置在maskKey以後
                dataStart = 8;
            } else {
                dataStart = 4;
            }
        } else if (len == 127) {
            // 若是爲126 繼續日後八個字節讀取做爲playload len
            // 這裏跳過bytes[2]~bytes[5]
            len = bytes[6];
            len = (len << 8) + bytes[7];
            len = (len << 8) + bytes[8];
            len = (len << 8) + bytes[9];

            if (isMask) {
                maskKey[0] = bytes[10];
                maskKey[1] = bytes[11];
                maskKey[2] = bytes[12];
                maskKey[3] = bytes[13];
                dataStart = 14;
            } else {
                dataStart = 10;
            }
        } else {
            // 既不是126也不是127 說明長度僅佔七位 不用處理
            if (isMask) {
                maskKey[0] = bytes[2];
                maskKey[1] = bytes[3];
                maskKey[2] = bytes[4];
                maskKey[3] = bytes[5];
                dataStart = 6;
            } else {
                dataStart = 2;
            }
        }

        // 讀取payload data 並根據isMask判讀是否進行mask加密
        for (int i = 0, count = 0; i < len; i++) {
            byte t1 = maskKey[count];
            byte t2 = bytes[i + dataStart]; // 從datastart 開始讀取data
            char c = isMask ? (char) (((~t1) & t2) | (t1 & (~t2))) : (char) t2; // isMask爲真,進行mask加密
            bufferRes[col++] = c;
            count = (count + 1) % 4;
        }
        bufferRes[col++] = '\0';
        return new String(bufferRes);
    }

編碼信息爲數據幀

核心思想就是根據信息格式將要發送的數據轉化爲websocket數據幀。

/**
     * 將message編碼爲websocket幀
     * @param message       字符信息
     * @param isMask        是否進行mask加密
     * @param result        保存幀的字節數組
     * @return              字節長度
     */
    public static int encodeMessage(String message, boolean isMask, byte[] result) {
        int dataEnd = 0;
        // 幀的第一個字節爲類型, 設置爲默認類型爲text幀
        result[dataEnd++] = (byte) 0x81;
        byte[] maskKey = new byte[4];

        // 獲取message 的字節數組
        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);

        // isMask爲真 設置mask位爲1
        if (isMask) {
            result[dataEnd] = (byte) 0x80;
        }
        
        // 判斷數據的長度
        long dataLen = messageBytes.length;
        
        if (dataLen < 126) {
            // 小於126字節 直接賦值
            result[dataEnd++] |= dataLen & 0x7f;
        } else if (dataLen < 65536) {
            // 小於65536字節,日後賦值兩個字節
            result[dataEnd++] |= 0x7E;
            result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF);
        } else if (dataLen < 0xFFFFFFFF) {
            // 小於0xFFFFFFFF個字節,日後賦值八個字節
            // 避免數據過大 跳過4個字節
            result[dataEnd++] |= 0x7F;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;

            result[dataEnd++] = (byte) ((dataLen >> 24) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 16) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF);
        }

        if (isMask) {
            // 若是isMask爲真 將數據進行mask加密再保存到幀中
            new Random().nextBytes(maskKey);
            result[dataEnd++] = maskKey[0];
            result[dataEnd++] = maskKey[1];
            result[dataEnd++] = maskKey[2];
            result[dataEnd++] = maskKey[3];
            for (int i = 0, count = 0; i < dataLen; i++) {
                byte t1 = maskKey[count];
                byte t2 = messageBytes[i];
                result[dataEnd++] = (byte) (((~t1) & t2) | (t1 & (~t2)));
                count = (count + 1) % 4;
            }
        } else {
            // 直接保存到幀中
            for (int i = 0; i < dataLen; i++) {
                result[dataEnd++] = messageBytes[i];
            }
        }
        return dataEnd;
    }

實驗

java端提供ServerSocket用於創建socket鏈接,鏈接成功後,對websocket作出握手響應,讀取websocket幀時解碼讀取信息,發送信息時轉化爲websocket幀

public static int userCount = 0;
    public static char[] bufferRes = new char[131072];
    public static Scanner sc = new Scanner(System.in);

    public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "WebSocket-Location: ws://127.0.0.1:9527\r\n";

    public static String magicKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(7000);

        while (true) {
            Socket socket = serverSocket.accept();
            userCount++;
            // 開啓一個新線程
            Thread thread = new Thread(() -> {
                // 響應握手信息
                try {
                    // 讀取請求頭
                    byte[] bytes = new byte[10000000];
                    socket.getInputStream().read(bytes);
                    String requestHeaders = new String(bytes, StandardCharsets.UTF_8);

                    // 獲取請求頭中的
                    String webSocketKey = "";
                    for (String header : requestHeaders.split("\r\n")) {
                        if (header.startsWith("Sec-WebSocket-Key")) {
                            webSocketKey = header.split(":")[1].trim();
                        }
                    }

                    // 將webSocketKey 與 magicKey 拼接用sha1加密以後在進行base64編碼
                    String value = webSocketKey + magicKey;
                    String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

                    // 寫入返回頭 握手結束 成功創建鏈接
                    String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
                    socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
                    System.out.println("握手成功,成功創建鏈接");

                    // 接受客戶端信息
                    while (true) {
                        System.out.println("讀取信息");
                        socket.getInputStream().read(bytes);
                        String message = decodeMessage(bytes);
                        System.out.println("讀取到的信息爲:" + message);

                        System.out.println("請回覆信息");
                        String res = sc.next();
                        byte[] result = new byte[10000000];
                        int len = encodeMessage(res, false, result);
                        socket.getOutputStream().write(result, 0, len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("finish Read");
            });
            thread.setName("用戶" + userCount);
            thread.start();
        }

    }

客戶端用簡單js代碼實現(感謝趙凱強同窗提供)。

<!DOCTYPE html>
<html>
<head>
    <title>websocket test</title>
</head>
<body>

    <ul id="ul">
    </ul>
    <input id="input" type="input" />
    <button onclick="onClick()">發送</button>

    <script type="text/javascript">
        var ws = new WebSocket("ws://localhost:7000");

        ws.onopen = function(evt) {
              console.log("Connection open ..."); 
        };

        ws.onmessage = function(evt) {
              console.log( "Received Message: " + evt.data);
              addLi('接受: ' + evt.data);
        };

        ws.onclose = function(evt) {
              console.log("Connection closed.");
        };

        // 點擊發送 讀取input的值打印到控制檯上
        function onClick() {
            var value = '發送: ' + document.getElementById("input").value;
            ws.send(value);
            document.getElementById("input").value = '';
            addLi(value);
        }

        // 一個方法 sring 當調用時, 把字符串插入到列表中
        function addLi(value) {
            document.getElementById("ul").innerHTML += "<li>" + value + "</li>";
        }   
    </script>
</body>
</html>

效果:
image.png
image.png

https://zhuanlan.zhihu.com/p/...

相關文章
相關標籤/搜索