1. 程式人生 > >websocket學習(二)

websocket學習(二)

websocket客戶端,服務端通訊的最小單位是幀,由1個或者多個幀組成一個條完成的訊息(message)

  • 傳送端,將訊息切割成多個幀,併發送給接收端
  • 接收端,接受訊息幀,並將關聯的症組裝成完整的訊息

websocket資料幀格式

單位是位元,比如FIN,RSV1各佔一個bit,opcode佔據四個bit。

 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: 一個bit,如果是1,表這是message的最後一個分片(fragment),如果是0,表示不是message的最後一個分片。
  • RSV1,RSV2,RSV3:各佔一個bit,一般情況寫全部為0.當客戶端,服務端協商採用Websocket擴充套件是,這三個標誌可以非0,且值的含義可以自由擴充套件進行定義。如果出現非0值,且並沒有採用Websocket擴充套件,連結出錯
  • opcode:4個bit,操作碼。opcode的值決定了應該如何解析後續的資料(data payload)。 如果操作程式碼是不認識的,那麼接收端應該斷開連結
    • %x0:表示一個延續幀。當Opcode為0時,表示本次資料傳輸採用了資料分片,當前收到的資料幀為其中一個數據分片。
    • %x1:表示這是一個文字幀(frame)
    • %x2:表示這是一個二進位制幀(frame)
    • %x3-7:保留的操作程式碼,用於後續定義的非控制幀。
    • %x8:表示連線斷開。
    • %x9:表示這是一個ping操作。
    • %xA:表示這是一個pong操作。
    • %xB-F:保留的操作程式碼,用於後續定義的控制幀。
  • Mask:一個bit,表示是否要對資料載荷進行掩碼操作
    • 客戶端像服務端傳送資料時,需要對資料進行掩碼操作,從服務端像客戶端傳送資料是,不需要對資料進行掩碼操作,如果服務端接收到的資料沒有進行掩碼操作,伺服器需要斷開連線。
    • 如果Mask是1,那麼在Masking-key中會頂一個一個掩碼鍵(masking key)。並用這個掩碼鍵來對資料進行反掩碼。所有的客戶端傳送到服務端的資料正,mask都是1
  • Payload length: 資料載荷的長度。單位是位元組。為7為,或者7+16為,或7+64為。
    • Payload length=x為0~125:資料的長度為x位元組。
    • Payload length=x為126:後續2個位元組代表一個16位的無符號整數,該無符號整數的值為資料的長度。
    • Payload length=x為127:後續8個位元組代表一個64位的無符號整數(最高位為0),該無符號整數的值為資料的長度。
    • 如果payload length佔用了多個位元組的話,payload length的二進位制表達採用網路序(big endian,重要的位在前)
  • Masking-key: 或4位元組(32位) 所有從客戶端傳送到服務端的資料幀,資料載荷都進行了掩碼操作,Mask為1,且攜帶了4位元組的Masking-key。如果Mask為0,則沒有Masking-key。載荷資料的長度,不包括mask key的長度
  • Payload data:(x+y) 位元組
    • 載荷資料:包括了擴充套件資料、應用資料。其中,擴充套件資料x位元組,應用資料y位元組。
    • 擴充套件資料:如果沒有協商使用擴充套件的話,擴充套件資料資料為0位元組。所有的擴充套件都必須宣告擴充套件資料的長度,或者可以如何計算出擴充套件資料的長度。此外,擴充套件如何使用必須在握手階段就協商好。如果擴充套件資料存在,那麼載荷資料長度必須將擴充套件資料的長度包含在內。
    • 應用資料:任意的應用資料,在擴充套件資料之後(如果存在擴充套件資料),佔據了資料幀剩餘的位置。載荷資料長度 減去 擴充套件資料長度,就得到應用資料的長度。

掩碼演算法

掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操作不會影響資料載荷的長度。掩碼、反掩碼操作都採用如下演算法:

  • 對索引i模以4得到j,因為掩碼一共就是四個位元組
  • 對原來的索引進行異或對應的掩碼位元組
  • 異或就是兩個數的二進位制形式,按位對比,相同取0,不同取1
function unmask(buffer, mask) {
      const length = buffer.length;
      for (let i = 0; i < length; i++) {
          buffer[i] ^= mask[i & 3];
      }
  }

栗子

這裡使用tcp實現聊天室功能

  • 新建目錄,新建index.html, server.js

  • 客戶端:index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <div>hello webscoket</div>
        <script>
            // 建立一個socket物件,相當於買了一部手機
            // socket 插座
            let socket = new WebSocket('ws://localhost:9999')
            // 如果伺服器連結成功,會觸發此事件
            socket.onopen = function () {
                console.log('連結已經建立好了')
                socket.send(('你好'))
            }
            // 如果客戶端接收到訊息,會觸發這個事件
            socket.onmessage = (e)  => {
                console.log(e.data)
            }
        </script>
    </body>
    </html>
    
  • 服務端:server.js

    let net = require('net')
    let code = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
    let crypto = require('crypto')
    
    let express = require('express')
    let app = express()
    app.use(express.static(__dirname))
    app.listen(8080)
    let server = net.createServer(( socket ) => {
        // 監聽客戶端發來的資訊
        socket.once('data', data => {
            data = data.toString();
            // data 內容大概如下
            /*  請求頭
                Request Method: GET
                Connection: Upgrade // 請求升級協議
                Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
                Sec-WebSocket-Key: BdHBU6Vo/0OMycVshRE3iQ== 
                Sec-WebSocket-Version: 13 // 升級的版本
                Upgrade: websocket // 升級的型別
    
                */
            let lines = data.split('\r\n')
            let headers = lines.slice(1, -2)
            // 處理字串為一個物件 獲取請求頭
            headers = headers.reduce((headers, line) => {
                let [ key, val ] = line.split(': ')
                headers[key] = val
                return headers
            }, {});
            // console.log('headers', headers) // 打印出來的header如下
            /* 
            { Host: 'localhost:9999',
                Connection: 'Upgrade',
                Pragma: 'no-cache',
                'Cache-Control': 'no-cache',
                'User-Agent':
                'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36',
                Upgrade: 'websocket',
                Origin: 'http://localhost:8080',
                'Sec-WebSocket-Version': '13',
                'Accept-Encoding': 'gzip, deflate, br',
                'Accept-Language': 'zh,zh-CN;q=0.9,en;q=0.8,en-AU;q=0.7',
                Cookie:
                'visit=10; visit.sig=VVIZcIDkunpRwQDicJGbWKrQhTE; hello=bf5b7610-d783-11e8-aa65-354f8f3423d3',
                'Sec-WebSocket-Key': 'qmuFW2gfEl4prcOwCqg7tA==',
                'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits' }
             */
            // 判斷是否升級協議
            // 如果Upgrade的知識websocket的話,則需要升級協議
            if (headers.Upgrade === 'websocket') {
                let key = headers['Sec-WebSocket-Key']
                let accept = crypto.createHash('sha1').update(key + code).digest('base64')
                // 處理響應頭
                let response = [
                    "HTTP/1.1 101 Switching Protocols",
                    "Upgrade: websocket",
                    "Connection: Upgrade",
                    `Sec-WebSocket-Accept: ${accept}`,
                    '\r\n'
                ].join('\r\n')
               
                socket.write(response)
                // 切換協議成功,開始監聽客戶端發來的訊息
                socket.on('data', function (bufs)  {
                    let FIN = (bufs[0] & 0b10000000) === 0b10000000 // 是否最後一幀
                    let opcode = bufs[0] & 0b00001111 // 操作碼 1文字,
                    let isMasked = (bufs[1] & 0b100000000) === 0b100000000 // 對方是否掩碼
                    let payloadLength = bufs[1] & 0b01111111;//取得負載資料的長度
                    let mask = bufs.slice(2, 6);//掩碼 4個位元組
                    let payload = bufs.slice(6);//負載資料
                    // console.log('FIN', FIN) // true
                    // console.log('opcode', opcode) // 1
                    // console.log(FIN, opcode, isMasked, payloadLength, mask, payload) 
                    // true 1 false 6 <Buffer 04 6e 75 fe> <Buffer e0 d3 d5 1b a1 d3>
    
                    for (let i = 0; i < payload.length; i++) {
                        payload[i] ^= mask[i % 4] // 為運算 相同是0, 不想同是1
                    }
    
                    // 處理返回值
                    // console.log(FIN, opcode, isMasked, payloadLength, mask, payload.toString()) 
                    let response = Buffer.alloc( 2 + payload.length )
                    response[0] = 0b10000001
                    response[1] = payload.length
                    // 將payload buf物件從下標2開始拷貝到response中
                    payload.copy(response, 2)
                    socket.write(response)
                })
            }
        })
    })
    server.listen(9999)
    

    總結

    其實我也不是很懂,就是勉勉強強知道絲絲,還要學習呀。

    嗯麼麼,附上完成程式碼