1. 程式人生 > 程式設計 >node.js 使用 net 模組模擬 websocket 握手進行資料傳遞操作示例

node.js 使用 net 模組模擬 websocket 握手進行資料傳遞操作示例

本文例項講述了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>

希望本文所述對大家node.js程式設計有所幫助。