雙向通訊之websocket

朵兒兮發表於2019-05-07

簡介

定義一個API,用以在網頁瀏覽器和伺服器之間建立socket連線,這個連線是持久的,兩邊可以在任意時間開始傳送資料

  • HTML5開始提供的一種瀏覽器和伺服器之間進行全雙工通訊的網路技術
  • 屬於應用層協議,基於TCP,並複用http的握手通訊
  • 優點

    支援雙向通訊,實時性強

    更好的二進位制支援

    沒有同源限制,客戶端可以與任意伺服器通訊

    較少的控制開銷(建立後,ws客戶端\服務端進行資料交換時,協議控制的資料包頭部較小)

實戰

  • 客戶端
let socket = new WebSocket('ws://localhost:9999');
socket.onopen = () => { // 連線成功後的回撥
    socket.send('hello')
}
socket.onmessage = (event) => { };// 接收到伺服器資料時的回撥  

socket.onclose = function(event) { };// 連線關閉時的回撥

socket.onerror = function(event) { };// 報錯時的回撥
複製程式碼
  • 伺服器端
let webSocketServer = require('ws').Server;
let server = new webSocketServer({port: 8888}); // 支援跨域  埠號不能衝突

server.on('connection', (socket) => { // 連線成功回撥
    socket.on('message', (msg) => { // 監聽客戶端傳送的訊息
        socket.send(msg); // 向客戶端返回訊息
    });
});
複製程式碼

websocket如何建立連線

客戶端通過HTTP請求與WebSocket服務端協商升級協議。協議升級完成後,後續的資料交換則遵照WebSocket的協議。

  • 客戶端申請協議升級(標準的http報文的格式 只支援get方法)
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade  表示要升級協議
Upgrade: websocket  要升級的協議
Sec-WebSocket-Version: 13  協議版本
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA== 與服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,確保服務端理解websocket連線, 避免惡意\無意等非法連線。
複製程式碼
  • 伺服器端響應協議升級
HTTP/1.1 101 Switching Protocols  // 101標識協議轉換
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E= 
到此完成協議升級,後續的資料互動都按照新的協議來。
複製程式碼

Sec-WebSocket-Accept計算公式

const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 常量
const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE).digest('base64'); // crypto提供通用的加密和雜湊演算法
複製程式碼

資料幀格式

客戶端和伺服器端通訊的最小單位是幀, 由1或多個幀組成一條完整的訊息 客戶端: 將訊息切割為多個幀傳送伺服器端 伺服器端:接收訊息幀, 並將關聯的幀重新組裝成完整的訊息

  單位是位元
  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 ...                |
 
複製程式碼
  • FIN:1個位元 表示是否是訊息(message)的最後一個分片(fragment),1代表是,0代表不是

  • RSV1, RSV2, RSV3:各佔1個位元。一般情況下全為0。只有當客戶端、服務端協商採用WebSocket擴充套件時,這三個標誌位可以非0,且值的含義由擴充套件進行定義,否則連線出錯。

  • Opcode: 4個位元。操作程式碼,即如何解析後續的資料載荷(data payload)。如果操作程式碼是不認識的,那麼接收端應該斷開連線(fail the connection)

    %x0:延續幀。表示本次資料傳輸採用了資料分片,當前收到的資料幀為其中一個資料分片。

    %x1:文字幀(frame)

    %x2:二進位制幀(frame)

    %x3-7:保留的操作程式碼,用於後續定義的非控制幀。

    %x8:連線斷開。

    %x9:一個ping操作。

    %xA:一個pong操作。

    %xB-F:保留的操作程式碼,用於後續定義的控制幀。

  • Mask: 1個位元。表示是否要對資料載荷進行掩碼操作

    從客戶端向服務端傳送資料時,Mask都是1,即需要對資料進行掩碼操作;從服務端向客戶端傳送資料時,不需要對資料進行掩碼操作

    如果服務端接收到的資料沒有進行過掩碼操作,服務端需要斷開連線。

    如果Mask是1,那麼在Masking-key中會定義一個掩碼鍵(masking-key),並用這個掩碼鍵來對資料載荷進行反掩碼。

  • Payload length:資料載荷的長度,單位是位元組。為7位,或7+16位,或7+64位。

    x為0~125:資料的長度為x位元組。

    x為126:後續2個位元組代表一個16位的無符號整數,該無符號整數的值為資料的長度。

    x為127:後續8個位元組代表一個64位的無符號整數(最高位為0),該無符號整數的值為資料的長度。

    如果payload length佔用了多個位元組的話,payload length的二進位制表達採用網路序(big endian,重要的位在前)

  • Masking-key:0或4位元組(32位),所有從客戶端傳送到服務端的資料幀,資料載荷都進行了掩碼操作,Mask為1,且攜帶了4位元組的Masking-key。如果Mask為0,則沒有Masking-key。載荷資料的長度,不包括mask key的長度

  • Payload data:(x+y) 位元組 載荷資料:包括了擴充套件資料、應用資料。其中,擴充套件資料x位元組,應用資料y位元組。

    擴充套件資料:如果沒有協商使用擴充套件的話,擴充套件資料資料為0位元組。所有的擴充套件都必須宣告擴充套件資料的長度,或者可以如何計算出擴充套件資料的長度。此外,擴充套件如何使用必須在握手階段就協商好。如果擴充套件資料存在,那麼載荷資料長度必須將擴充套件資料的長度包含在內。

    應用資料:任意的應用資料,在擴充套件資料之後(如果存在擴充套件資料),佔據了資料幀剩餘的位置。載荷資料長度 減去 擴充套件資料長度,就得到應用資料的長度。

例項

let net = require('net'); // 實現tcp協議
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const crypto = require('crypto');
let server = net.createServer((socket) => {
    socket.once('data', (data) => { // once只會執行一次回撥
        data = data.toString(); // data為請求流
        if (data.match(/Connection: Upgrade/)) { // 說明需要請求升級協議
            let rows = data.split('\r\n'); //按分割符分開
            rows = rows.slice(1, -2); //去掉請求行和尾部的二個分隔符
            // 獲取請求頭
            let headers = {};
            rows.reduce((memo, item) => { 
                let [key, value] = item.split(': ');
                memo[key] = value;
                return memo;
            }, headers);
            // console.log(headers, 'headers');
            if(headers['Sec-WebSocket-Version'] === '13'){ // 需要升級為13版本
                let SecWebSocketKey = headers['Sec-WebSocket-Key'];
                let SecWebSocketAccept = crypto.createHash('sha1').update(SecWebSocketKey + CODE).digest('base64');
                let response = [
                    'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${SecWebSocketAccept}`,
                    '\r\n', // 響應頭和響應體之間有兩個\r\n
                ].join('\r\n');
                socket.write(response); // 返回響應頭給客戶端 表明握手成功
                // 後面格式基於websocket協議
                socket.on('data', (buffers) => { // data預設為buffer型別
                    
                    let fin = buffers[0]&0b10000000 == 0b10000000; // 獲取第一個位元組的第一位  即結束位的值
                    let opcode = buffers[0]&0b00001111; // 獲取第一個位元組的後四位 即操作碼 
                    let ismask = buffers[1]&0b10000000; // 是否進行掩碼
                    let payloadLength = buffers[1]&0b01111111; 
                    let payload;
                    if (payloadLength<=125) {
                        if (ismask) {
                            let mask = buffers.slice(2,6); // 掩碼鍵
                            payload = buffers.slice(6); // 攜帶的真實資料
                             // console.log(payload, 'before unmask');
                            unmask(payload, mask); // 對資料進行反掩碼
                            // console.log(payload.toString(), 'unmask');
                        } else {
                            payload = buffers.slice(2);
                        }
                    } else if (payloadLength<=126) {
                        // ....
                    }
                    
                   

                    // 拼接響應幀
                    let res = Buffer.alloc(2+payload.length); 
                    res[0] = 0b10000000|opcode; 
                    res[1] = payloadLength;
                    payload.copy(res, 2);
                    socket.write(res);
                });
            }
        }
    });
});
function unmask(payload,mask){ // mask為4個位元組長度
    const length = payload.length;
    for (let i=0;i<length;i++) {
        payload[i]^=mask[i&3]; // i&3等價於1%4
    }
}
server.listen(9999);

複製程式碼

相關文章