欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

node.js 使用 net 模块模拟 websocket 握手进行数据传递操作示例

程序员文章站 2022-05-14 15:17:15
本文实例讲述了node.js 使用 net 模块模拟 websocket 握手进行数据传递操作。分享给大家供大家参考,具体如下: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程序设计有所帮助。