node.js 使用 net 模塊模擬 websocket 握手,進行數據傳遞。


websocket 是一種讓瀏覽器與服務器之間建立持久的連接,並能進行雙向數據傳輸的一種協議。

websocket 屬性應用層協議,基於tcp傳輸協議,並復用http的握手通道。

 

一、如何進行websocket連接。

websocket復用了http的握手通道,客戶端通過http請求與服務端進行協商,升級協議。協議升級完后,后面的數據交換則遵照websocket協議。

1、客戶端申請協議升級

Request URL: ws://localhost:8888/
Request Method: GET
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: uR5YP/BMO6M24tAFcmHeXw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Connection: Upgrade 表示要升級協議

Upgrade: websocket 表示升級到websocket協議

Sec-WebSocket-Version: 13 表示websocket的版本

Sec-WebSocket-Key 表示websocket的驗證,防止惡意的連接,與服務端響應的Sec-WebSocket-Accept是配套。

 

2、服務端響應協議升級

Status Code: 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: eS92kXpBNI6fWsCkj6WxH6QeoHs=
Upgrade: websocket

Status Code:101 表示狀態碼,協議切換。

Sec-WebSocket-Accept 表示服務端響應的校驗,與客戶端的Sec-WebSocket-Key是配套的。

 

3、Sec-WebSocket-Accept是如何計算的

將 Sec-WebSocket-Key 的值與 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

然后通過sha1計算,再轉成base64。

const crypto = require('crypto');

function getSecWebSocketAccept(key) {
    return crypto.createHash('sha1')
        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
        .digest('base64');
}

console.log(getSecWebSocketAccept('uR5YP/BMO6M24tAFcmHeXw=='));

  

4、協議升級完后,后續的數據傳輸就需要按websocket協議來走。

websocket客戶端與服務端通信的最小單位是 幀,由1個或多個幀組成完整的消息。

客戶端:將消息切割成多個幀,發送給服務端。

服務端:接收到消息幀,將幀重新組裝成完整的消息。

 

5、數據幀的格式

單位是1個比特位,FIN,PSV1,PSV2,PSV3 占1個比特位,opcode占4個比特位。

 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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 ...                |
+---------------------------------------------------------------+

FIN  占1位,用來表示該幀是否是最后一幀,1表示是,0表示不是。

RSV1,RSV2,RSV3  分別占1位,一般情況下全為0,擴展使用,值的含義由擴展進行定義。

opcode 占4位,表示如何解析后面的數據載荷(Payload Data)。

  %x0 表示一個延續幀,opcode為0時,表示數據傳輸采用了數據分片,當前的數據幀只是其中一個數據分片。

  %x1 表示這是一個文本幀

  %x2 表示這是一個二進制幀

  %x3-7 保留的操作代碼,用於定義后續的非控制幀。

  %x8 表示連接斷開

  %x9 表示這是一個ping操作

  %xA 表示這是一個pong操作

  %xB-F 保留的操作代碼,用於定義后續的控制幀。

MASK 占1位,表示是否要對數據載荷進行掩碼操作。

  客戶端向服務端發數據,需要對數據進行掩碼操作,服務端向客戶端發數據,不需要對數據進行掩碼操作。

  如果Mask為1,則Masking-key中會定義一個掩碼鍵,通過該掩碼鍵對數據載荷進行反掩碼。客戶端發送給服務端的數據幀,MASK都是1。

Payload len 為7位,或7+16位,或7+64位,表示數據載荷的長度,單位字節。

  如果Payload len=0~125,表示,數據的長度為0~125字節。

  如果Payload len=126,表示,后續的2個字節代表一個16位的無符號整數,該整數表示數據的長度。

  如果Payload len=127,表示,后續的8個字節代表一個64位的無符號整數,該整數表示數據的長度。

  如果Payload len占用多個字節,Payload len的二進制表達采用Big-endian。

Masking-key 占0或32位,客戶端向服務端發送數據幀,數據載荷都進行了掩碼操作,Mask為1,且帶了4字節的Masking-key。如果Mask為0,則沒有Masking-key。

注意數據載荷的長度,不包括Masking-key的長度。

 

6、掩碼的算法

Masking-key掩碼鍵是由客戶端生成的32位隨機數,掩碼操作不會影響數據載荷的長度。

function unmask(buffer, mask) {
    const length = buffer.length;
    for (var i = 0; i < length; i++) {
        buffer[i] ^= mask[i & 3];
    }
}

  

7、實現websocket的握手

const crypto = require('crypto');
const net = require('net');

//計算websocket校驗
function getSecWebSocketAccept(key) {
    return crypto.createHash('sha1')
        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
        .digest('base64');
}

//掩碼操作
function unmask(buffer, mask) {
    const length = buffer.length;
    for (var i = 0; i < length; i++) {
        buffer[i] ^= mask[i & 3];
    }
}

//創建一個tcp服務器
let server = net.createServer(function (socket) {

    socket.once('data', function (data) {
        data = data.toString();

        //查看請求頭中是否有升級websocket協議的頭信息
        if (data.match(/Upgrade: websocket/)) {
            let rows = data.split('\r\n');
            //去掉第一行的請求行
            //去掉請求頭的尾部兩個空行
            rows = rows.slice(1, -2);
            let headers = {};
            rows.forEach(function (value) {
                let [k, v] = value.split(': ');
                headers[k] = v;
            });
            //判斷websocket的版本
            if (headers['Sec-WebSocket-Version'] == 13) {
                let secWebSocketKey = headers['Sec-WebSocket-Key'];
                //計算websocket校驗
                let secWebSocketAccept = getSecWebSocketAccept(secWebSocketKey);
                //服務端響應的內容
                let res = [
                    'HTTP/1.1 101 Switching Protocols',
                    'Upgrade: websocket',
                    `Sec-WebSocket-Accept: ${secWebSocketAccept}`,
                    'Connection: Upgrade',
                    '\r\n'
                ].join('\r\n');
                //給客戶端發送響應內容
                socket.write(res);

                //注意這里不要斷開連接,繼續監聽'data'事件
                socket.on('data', function (buffer) {
                    //注意buffer的最小單位是一個字節
                    //取第一個字節的第一位,判斷是否是結束位
                    let fin = (buffer[0] & 0b10000000) === 0b10000000;
                    //取第一個字節的后四位,得到的一個是十進制數
                    let opcode = buffer[0] & 0b00001111;
                    //取第二個字節的第一位是否是1,判斷是否掩碼操作
                    let mask = buffer[1] & 0b100000000 === 0b100000000;
                    //載荷數據的長度
                    let payloadLength = buffer[1] & 0b01111111;
                    //掩碼鍵,占4個字節
                    let maskingKey = buffer.slice(2, 6);
                    //載荷數據,就是客戶端發送的實際數據
                    let payloadData = buffer.slice(6);

                    //對數據進行解碼處理
                    unmask(payloadData, maskingKey);

                    //向客戶端響應數據
                    let send = Buffer.alloc(2 + payloadData.length);
                    //0b10000000表示發送結束
                    send[0] = opcode | 0b10000000;
                    //載荷數據的長度
                    send[1] = payloadData.length;
                    payloadData.copy(send, 2);
                    socket.write(send);
                });
            }
        }
    });

    socket.on('error', function (err) {
        console.log(err);
    });

    socket.on('end', function () {
        console.log('連接結束');
    });

    socket.on('close', function () {
        console.log('連接關閉');
    });
});

//監聽8888端口
server.listen(8888);

index.html的代碼:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<script>
    var ws = new WebSocket('ws://localhost:8888');
    ws.onopen = function () {
        console.log('連接成功');
        ws.send('你好服務端');
    };
    ws.onmessage = function (ev) {
        console.log('接收數據', ev.data);
    };
    ws.onclose = function () {
        console.log('連接斷開');
    };
</script>
</body>
</html>

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM