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 相关文章推荐
json简单介绍
Jun 10 Javascript
javascript 节点排序 2
Jan 31 Javascript
在js(jquery)中获得文本框焦点和失去焦点的方法
Dec 04 Javascript
js 去掉空格实例 Trim() LTrim() RTrim()
Jan 07 Javascript
jquery实现可横向和竖向展开的动态下滑菜单效果
Aug 24 Javascript
Angular2表单自定义验证器的实现
Oct 19 Javascript
非常优秀的JS图片轮播插件Swiper的用法
Jan 03 Javascript
JavaScript中的toString()和toLocaleString()方法的区别
Feb 15 Javascript
微信小程序中input标签详解及简单实例
May 18 Javascript
利用纯JS实现像素逐渐显示的方法示例
Aug 14 Javascript
vue之浏览器存储方法封装实例
Mar 15 Javascript
JS内部事件机制之单线程原理
Jul 02 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的autoLoad自动加载机制
2012/09/27 PHP
PHP判断是否为空的几个函数对比
2015/04/21 PHP
浅谈php和js中json的编码和解码
2016/10/24 PHP
PHP封装的非对称加密RSA算法示例
2018/05/28 PHP
关于setInterval、setTimeout在jQuery中的使用注意事项
2011/09/28 Javascript
js捕获鼠标右键菜单中的粘帖事件实现代码
2013/04/01 Javascript
node.js中的path.resolve方法使用说明
2014/12/08 Javascript
浅谈jQuery构造函数分析
2015/05/11 Javascript
JS判断图片是否加载完成方法汇总(最新版)
2016/05/13 Javascript
基于Node的React图片上传组件实现实例代码
2017/05/10 Javascript
js实现放大镜特效
2017/05/18 Javascript
Javascript之图片的延迟加载的实例详解
2017/07/24 Javascript
基于webpack4.X从零搭建React脚手架的方法步骤
2018/12/23 Javascript
微信小程序如何修改本地缓存key中单个数据的详解
2019/04/26 Javascript
Element-UI中关于table表格的那些骚操作(小结)
2019/08/15 Javascript
seajs和requirejs模块化简单案例分析
2019/08/26 Javascript
小程序如何获取多个formId实现详解
2019/09/20 Javascript
ES6函数和数组用法实例分析
2020/05/23 Javascript
javascript前端实现多视频上传
2020/12/13 Javascript
利用Python自动监控网站并发送邮件告警的方法
2016/08/24 Python
python数据结构之链表的实例讲解
2017/07/25 Python
Python 实现淘宝秒杀的示例代码
2018/01/02 Python
Python根据指定日期计算后n天,前n天是哪一天的方法
2018/05/29 Python
解决pycharm 误删掉项目文件的处理方法
2018/10/22 Python
Python 函数返回值的示例代码
2019/03/11 Python
运用PyTorch动手搭建一个共享单车预测器
2019/08/06 Python
python 图像增强算法实现详解
2021/01/24 Python
CSS3中媒体查询结合rem布局适配手机屏幕
2019/06/10 HTML / CSS
JD Sports比利时官网:英国领先的运动鞋和运动服饰零售商
2018/10/10 全球购物
写一个函数返回1+2+3+…+n的值(假定结果不会超过长整型变量的范围)
2014/09/05 面试题
Unix如何在一行中运行多个命令
2015/05/29 面试题
大学生个人求职信例文
2014/07/07 职场文书
学校与家长安全责任书
2014/07/23 职场文书
公务员政审材料
2014/12/23 职场文书
MySQL 数据丢失排查案例
2021/05/08 MySQL
浅谈python中的多态
2021/06/15 Python