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


Posted in Javascript onFebruary 11, 2020

本文实例讲述了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程序设计有所帮助。

Javascript 相关文章推荐
用javascript来实现动画导航效果的代码
Dec 16 Javascript
JavaScript内核之基本概念
Oct 21 Javascript
精通JavaScript的this关键字
May 28 Javascript
Webpack打包慢问题的完美解决方法
Mar 16 Javascript
详解Node.js实现301、302重定向服务
Apr 07 Javascript
详解Vue2.0之去掉组件click事件的native修饰
Apr 20 Javascript
JavaScript的继承实现小结
May 07 Javascript
详解Angular模板引用变量及其作用域
Nov 23 Javascript
微信小程序之onLaunch与onload异步问题详解
Mar 28 Javascript
微信小程序实现跳转的几种方式总结(推荐)
Apr 24 Javascript
javascript如何实现create方法
Nov 04 Javascript
JavaScript 获取滚动条位置并将页面滑动到锚点
Feb 08 Javascript
JavaScript实现拖拽功能
Feb 11 #Javascript
node.js中 mysql 增删改查操作及async,await处理实例分析
Feb 11 #Javascript
基于Angular 8和Bootstrap 4实现动态主题切换的示例代码
Feb 11 #Javascript
原生js实现点击轮播切换图片
Feb 11 #Javascript
node.js中process进程的概念和child_process子进程模块的使用方法示例
Feb 11 #Javascript
小程序如何定位所在城市及发起周边搜索
Feb 11 #Javascript
JS+DIV实现拖动效果
Feb 11 #Javascript
You might like
PHP 编写大型网站问题集
2010/05/07 PHP
一个经典实用的PHP图像处理类分享
2014/11/18 PHP
在PHP程序中使用Rust扩展的方法
2015/07/03 PHP
PHP常见过waf webshell以及最简单的检测方法
2019/05/21 PHP
Aster vs Newbee BO5 第一场2.19
2021/03/10 DOTA
JavaScript 模式之工厂模式(Factory)应用介绍
2012/11/15 Javascript
原始XMLHttpRequest方法详情回顾
2013/11/28 Javascript
探究JavaScript函数式编程的乐趣
2015/12/14 Javascript
JavaScript实现字符串与日期的互相转换及日期的格式化
2016/03/07 Javascript
AngularJS在IE8的不支持的解决方法
2016/05/13 Javascript
layui table复选框禁止某几条勾选的实例
2019/09/20 Javascript
在Echarts图中给坐标轴加一个标识线markLine
2020/07/20 Javascript
快速了解Python开发中的cookie及简单代码示例
2018/01/17 Python
Python基于Flask框架配置依赖包信息的项目迁移部署
2018/03/02 Python
使用anaconda的pip安装第三方python包的操作步骤
2018/06/11 Python
python实现事件驱动
2018/11/21 Python
django 外键model的互相读取方法
2018/12/15 Python
在python里协程使用同步锁Lock的实例
2019/02/19 Python
python实现得到当前登录用户信息的方法
2019/06/21 Python
python中下标和切片的使用方法解析
2019/08/27 Python
numpy中三维数组中加入元素后的位置详解
2019/11/28 Python
python 循环数据赋值实例
2019/12/02 Python
Pycharm 2020年最新激活码(亲测有效)
2020/09/18 Python
python实现UDP协议下的文件传输
2020/03/20 Python
Python变量及数据类型用法原理汇总
2020/08/06 Python
详解pycharm2020.1.1专业版安装指南(推荐)
2020/08/07 Python
python如何调用php文件中的函数详解
2020/12/29 Python
美国便宜的横幅和标志印刷在线:Best of Signs
2019/05/29 全球购物
研究生求职推荐信范文
2013/11/30 职场文书
党员学习中共十八大思想报告
2014/09/12 职场文书
幼儿教师师德师风自我剖析材料
2014/09/29 职场文书
2015元旦晚会主持词(开场白+结束语)
2014/12/14 职场文书
解决Golang中goroutine执行速度的问题
2021/05/02 Golang
MySQL 表空间碎片的概念及相关问题解决
2021/05/07 MySQL
python文件目录操作之os模块
2021/05/08 Python
直播实况, OMG破敌三路五十分钟大战神技局摩托车
2022/04/01 DOTA