WebSocket原理與實踐(二)---WebSocket協議

龍恩0707發表於2018-03-08

WebSocket原理與實踐(二)---WebSocket協議

    WebSocket協議是為了解決web即時應用中伺服器與客戶端瀏覽器全雙工通訊問題而設計的。協議定義ws和wss協議,分別為普通請求和基於SSL的安全傳輸, ws埠是80,wss的埠為443.

WebSocket協議由兩部分組成,握手和資料傳輸。

2-1 握手
WS的握手使用HTTP來實現的。客戶端的握手訊息是一個普通的,帶有Upgrade頭的,HTTP Request的訊息。
先來看看如下程式碼:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>websocket</title>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
  </head>
  <body>
    <script type="text/javascript">
      var wsUrl = "wss://echo.websocket.org";
      var ws = new WebSocket(wsUrl);
      ws.onopen = function() {
        console.log('open');
      };
      ws.onmessage = function(msg) {
        console.log(msg.data);
      }
      ws.onclose = function() {
        console.log('已經被關閉了');
      }
    </script>
  </body>
</html>

頁面執行後,我們可以看到連結到 wss://echo.websocket.org 期間記錄的一個握手協議。先來看看客戶端傳送http的請求頭:

GET /chat HTTP/1.1
Host:echo.websocket.org
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Key:ALS2AoBJtUup67heKDgzFg==
Origin:file://
Sec-WebSocket-Version:13

伺服器響應的頭欄位

Connection:Upgrade
Sec-WebSocket-Accept:qyzx/EgbRK15QNmr5PhpMQrPZMM=
Server: Kaazing Gateway
Upgrade:websocket

下面是請求和響應頭欄位的含義:
Upgrade: websocket, 告訴伺服器這個HTTP連結是升級的WebSocket協議。
Connection:Upgrade 告知伺服器當前請求連結是升級的。
Origin: 該欄位是用來防止客戶端瀏覽器使用指令碼進行未授權的跨源攻擊,伺服器要根據這個欄位是否接受客戶端的socket連結。
可以返回一個HTTP錯誤狀態碼來拒絕連線。

Sec-WebSocket-Key: ALS2AoBJtUup67heKDgzFg==
Sec-WebSocket-Accept: qyzx/EgbRK15QNmr5PhpMQrPZMM=

Sec-WebSocket-Key 的值是一串長度為24的字串是客戶端隨機生成的base64編碼的字串,它傳送給伺服器,伺服器需要使用它經過一定的運算規則生成伺服器的key,然後把伺服器的key發到客戶端去,客戶端驗證正確後,握手成功。

握手的具體原理:當我們客戶端執行 new WebSocket(''wss://echo.websocket.org')的時候,客戶端就會發起請求報文進行握手申請,報文中有一個key就是
Sec-WebSocket-Key,伺服器獲取到key,會將這個key與字串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相連,對新的字串通過sha1安全雜湊演算法計算出結果後,再進行Base64編碼,並且將結果放在請求頭的"Sec-WebSocket-Accept",最後返回給客戶端,
客戶端進行驗證後,握手成功。握手成功後就可以開始資料傳輸了。

下面是實現一個簡單的握手協議的demo,程式碼如下:

### 目錄結構如下:

demo
  |--- hands.html
  |--- hands.js

hands.html 程式碼如下:

<html>
<head>
  <title>WebSocket Demo</title>
</head>
<body>
  <script type="text/javascript">
    var ws = new WebSocket("ws://127.0.0.1:8000");
    ws.onerror = function(e) {
      console.log(e);
    };
    ws.onopen = function() {
      console.log('握手成功');
    }
  </script>
</body>
</html>

hands.js 程式碼如下:

var crypto = require('crypto');

var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

require('net').createServer(function(o) {
  var key;
  o.on('data', function(e) {
    if (!key) {
      console.log(e);

      key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
      console.log(key);
      
      // WS的字串 加上 key, 變成新的字串後做一次sha1運算,最後轉換成Base64
      key = crypto.createHash('sha1').update(key+WS).digest('base64');
      console.log(key);

      // 輸出欄位資料,返回到客戶端,
      o.write('HTTP/1.1 101 Switching Protocol\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept:' +key+'\r\n');
      // 輸出空行,使HTTP頭結束
      o.write('\r\n');
    } else {
      // 資料處理
    }
  })
}).listen(8000);

首先在命令列中 進入相對應專案目錄後,執行 node hands.js, 然後開啟 hands.html 執行一下即可看到 命令列中列印出來如下資訊:

$ node hands.js
<Buffer 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a 38 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 ... >
+iHlfGTolBaWYpnyTIw22g==
W7IEsdQtwv8EP2204kssK/6pg+c=

然後在瀏覽器中檢視請求頭如下資訊:

Request Headers:

Connection:Upgrade
Host:127.0.0.1:8000
Origin:file://
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:+iHlfGTolBaWYpnyTIw22g==
Sec-WebSocket-Version:13
Upgrade:websocket

響應頭如下資訊:

Response Headers:

Connection:Upgrade
Sec-WebSocket-Accept:W7IEsdQtwv8EP2204kssK/6pg+c=
Upgrade:websocket

如上資訊可以看到,獲取報文中的key程式碼:
key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
console.log(key); // 列印 +iHlfGTolBaWYpnyTIw22g==

和 Request Headers:中的 Sec-WebSocket-Key 值是一樣的,該值是瀏覽器自動生成的,然後獲取該值後,與 '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',相連,對新的字串通過sha1安全雜湊演算法計算出結果後,再進行Base64編碼,
並且將結果放在請求頭的"Sec-WebSocket-Accept",最後返回給客戶端,客戶端進行驗證後,握手成功。在瀏覽器中可以看到列印出 握手成功了。

github上檢視demo

相關文章