2019年3月10日 星期日

WebSocket 一些重要觀念

WebSocket  定義在 RFC6455 文件當中, 有兩個重要的章節:
1. 交握協定: https://tools.ietf.org/html/rfc6455#section-1.3
WebSocket 使用的 tcp port 跟 http 相同, 當使用者用 http GET 方法時, 其中攜帶的標頭(header)就包含升級(Upgrade)至 websocket 的動作, 標籤(Sec-WebSocket-Key)後面附帶一串 base64 明文,每次開啟交握協定時都會不同,以一組 base64 明碼為例:
         dGhlIHNhbXBsZSBub25jZQ==
如果將它解開,其實裏面是一組 16 位元組的密碼.假設客戶端所發送封包內容是:
        GET /xxx HTTP/1.1
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
 http 伺服器端收到封包偵測到 Sec-WebSocket-Key, 就將明碼串接一個共通識別碼(GUID)形成長字串:
        dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
用雜湊函數 sha1 計算並轉成 base64 明文簡化成:
        s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
伺服器端產生一個封包把它放在 Sec-WebSocket-Accept: 標籤的後面, 回應用戶端切換通信協定,完成交握程序
        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. 封包格式: https://tools.ietf.org/html/rfc6455#section-5.1
一旦交握完成, 雙方封包內就不再用 http 的標頭(header), 而是轉成 WebSocket 格式的標頭, WebSocket 客戶端送的資料必須使用 xor 加密, 所附帶 4 位元組(byte)遮蔽碼負責把資料每 4 個位元組循環作加密運算(xor)轉成亂碼,伺服器端則利用這 4 個遮蔽碼同樣每 4 個位元組循環作解密運算(xor)轉成明文.
標頭第 1 個位元組分上半部與下半部:
        上半部最高位元(MSB) 標示該封包是否是最後一筆封包, 後 3  個位元保留未使用
        下半部 4 個位元代表該封包的格式(opcode),分別如下:
            0000  表示是上一封包的延續
            0001 表示文字格式(ascII code)
            0010 表示16進制(binary code)
            1000 表示關閉 Websocket 通道
            1001 表示 ping 封包
            1010 表示 pong 封包
            其他保留未使用
        用 c++ 語言描述:
        bool  fin   =   p[0] & 0x80;
        auto opcode = p[0] & 0xf ;
        if( fin ) printf("final frame\n");
        switch(opcode){
          case 0 : printf("continue frame\n") ;break;
          case 1 : printf("text frame\n")      ;break;
          case 2 : printf("binary frame\n")  ;break;
          case 8 : printf("close frame\n")    ;throw "WebSocket: Connection close!";
          case 9 : printf("ping frame\n")     ;break;
          case 10: printf("pong frame\n")  ;break;
          default: printf("reserved frame\n") ;break;
        } 
標頭第 2 個位元組指示隨後的資料, 最高位元(MSB)代表是否資料經過 xor 運算, 剩下7個位元是資料長度數值, 當數值小於 126, 就當成是資料長度(payload length), 如果等於 126, 那要用隨後的另兩個位元組當資料長度, 但如果大於 126(也就是等於 127)時, 就用隨後 8 個位元組當資料長度.客戶端送來的資料必須用 xor 運算過, 因此資料長度後面,必定先緊隨 4 個位元組的遮蔽碼(mask key), 其後才是資料(payload). 而伺服器傳給客戶端並不需要加密, 也就無需遮蔽碼, 因此長度後面緊跟的就是資料(payload)了, 簡單用 c++ 語言來解碼:             
        if (p[1] & 0x80) { // maskbit should be true
           p[1] &= 0x7f; // data in buffer will be de-mask
           auto wslen = p[1];// length value copy
           p += 2; // don't need the first 2 bytes in header
           long long payloadlen;// 8 bytes integer to get payload length
           if (wslen < 126 )  payloadlen = wslen;
           else { // 126: following 2bytes, 127: following 8 bytes
               payloadlen = *p ++;
               payloadlen = (payloadlen << 8) + *p ++;     
               if (wslen>126)  for (auto i=0; i < 6; i++) payloadlen = (payloadlen << 8) + *p ++;
           }
           unsigned char maskkey[4]; // to store maskkey
           for (auto i=0; i < 4; i++ ) maskkey = p[i]; // maskkey copy
           for (auto i=0; i < payloadlen; i++) p[i] = p[i+4] ^ maskkey[i%4]; // decode & move
           size -= 4; // size of buffer was reduced by 4 bytes of maskkey
           auto &payload = p; // payload now point to data
        }
3. 客戶端可以從 http 伺服器端接收一個 index.html 檔案, 編寫的內容像是:
<html><head><title>WebSocket 發送與接收示範</title><meta charset="UTF-8"></head>
    <script>
     const ws = new WebSocket('ws://127.0.0.1:5000');// port 必須與 http 相同
      ws.addEventListener('open', () => {
           msg = "Hello, thhis is a test message.";
           console.log("Send:  " + msg);
           ws.send(msg);
       });
       ws.addEventListener('message', event => {
           console.log("Get::  " + event.data);
           ws.close();
       });
    </script>
    <body>打開瀏覽器的 Console 視窗觀看結果</body>
</html>
內嵌的 Javascript 是一個用來開啟 WebSocket 通道的程式碼, 假設 http 伺服器使用 tcp port = 5000, 將上述的 index.html 放在伺服器的 www 根目錄底下, 用瀏覽器開啟 URL 的頁面 http://127.0.0.1:5000/index.html

沒有留言: