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中去掉数组中的重复值的实现方法
Aug 03 Javascript
js调用后台servlet方法实例
Jun 09 Javascript
js给页面加style无效果的解决方法
Jan 20 Javascript
jquery+CSS实现的水平布局多级网页菜单效果
Aug 24 Javascript
jquery UI Datepicker时间控件的使用方法(基础版)
Nov 07 Javascript
JavaScript知识点总结(四)之逻辑OR运算符详解
May 31 Javascript
Summernote实现图片上传功能的简单方法
Jul 11 Javascript
JavaScript中利用构造器函数模拟类的方法
Feb 16 Javascript
js实现拖拽功能
Mar 01 Javascript
微信小程序 实例开发总结
Apr 26 Javascript
javaScript中封装的各种写法示例(推荐)
Jul 03 Javascript
浅析vue插槽和作用域插槽的理解
Apr 22 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打开文件fopen函数的使用说明
2013/07/05 PHP
PHP实现图片旋转效果实例代码
2014/10/01 PHP
PHP设计模式之策略模式原理与用法实例分析
2019/04/04 PHP
响应鼠标变换表格背景或者颜色的代码
2009/03/30 Javascript
js自执行函数的几种不同写法的比较
2012/08/16 Javascript
javascript ready和load事件的区别示例介绍
2013/08/30 Javascript
JS获取数组最大值、最小值及长度的方法
2015/11/24 Javascript
原生js配合cookie制作保存路径的拖拽
2015/12/29 Javascript
JS读取XML文件数据并以table形式显示数据的方法(兼容IE与火狐)
2016/06/02 Javascript
微信小程序  modal弹框组件详解
2016/10/27 Javascript
PWA介绍及快速上手搭建一个PWA应用的方法
2019/01/27 Javascript
Vue中多个元素、组件的过渡及列表过渡的方法示例
2019/02/13 Javascript
Python提取Linux内核源代码的目录结构实现方法
2016/06/24 Python
python中利用xml.dom模块解析xml的方法教程
2017/05/24 Python
python 3.6 +pyMysql 操作mysql数据库(实例讲解)
2017/12/20 Python
Windows下安装Scrapy
2018/10/17 Python
python 一个figure上显示多个图像的实例
2019/07/08 Python
Pytorch之contiguous的用法
2019/12/31 Python
将matplotlib绘图嵌入pyqt的方法示例
2020/01/08 Python
关于tf.matmul() 和tf.multiply() 的区别说明
2020/06/18 Python
Django实现随机图形验证码的示例
2020/10/15 Python
国际领先的学术出版商:Springer
2017/01/11 全球购物
飞利浦西班牙官方网站:Philips西班牙
2020/02/17 全球购物
四年级数学教学反思
2014/02/02 职场文书
顶碗少年教学反思
2014/02/21 职场文书
禁止高声喧哗的标语
2014/06/11 职场文书
股指期货心得体会
2014/09/10 职场文书
信息技术教研组工作总结
2015/08/13 职场文书
预备党员表决心的话
2015/09/22 职场文书
导游词之泰山玉皇顶
2019/12/23 职场文书
使用Redis实现实时排行榜功能
2021/07/02 Redis
利用 SQL Server 过滤索引提高查询语句的性能分析
2021/07/15 SQL Server
一篇文章看懂MySQL主从复制与读写分离
2021/11/07 MySQL
oracle重置序列从0开始递增1
2022/02/28 Oracle
MySQL实战记录之如何快速定位慢SQL
2022/03/23 MySQL
python自动获取微信公众号最新文章的实现代码
2022/07/15 Python