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 相关文章推荐
js null,undefined,字符串小结
Aug 21 Javascript
jQuery Ajax使用实例
Apr 16 Javascript
JS实现网页Div层Clone拖拽效果
Sep 26 Javascript
JavaScript弹窗基础篇
Apr 27 Javascript
AngularJS中指令的四种基本形式实例分析
Nov 22 Javascript
原生js实现打字动画游戏
Feb 04 Javascript
BootStrap点击保存后实现模态框自动关闭的思路(模态框)
Sep 26 Javascript
对node.js中render和send的用法详解
May 14 Javascript
ios设备中angularjs无法改变页面title的解决方法
Sep 13 Javascript
Vue2.x通用编辑组件的封装及应用详解
May 28 Javascript
vue+element 模态框表格形式的可编辑表单实现
Jun 07 Javascript
24行JavaScript代码实现Redux的方法实例
Nov 17 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
咖啡与水的关系
2021/03/03 冲泡冲煮
用PHP实现ODBC数据分页显示一例
2006/10/09 PHP
用 PHP5 轻松解析 XML
2006/12/04 PHP
php安全之直接用$获取值而不$_GET 字符转义
2012/06/03 PHP
PHP5中实现多态的两种方法实例分享
2014/04/21 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(三)
2014/06/23 PHP
php链表用法实例分析
2015/07/09 PHP
php实现多维数组排序的方法示例
2017/03/23 PHP
PHP实现求两个字符串最长公共子串的方法示例
2017/11/17 PHP
2则自己编写的jQuery特效分享
2015/02/26 Javascript
jQuery实现的手风琴侧边菜单效果
2017/03/29 jQuery
jquery.rotate.js实现可选抽奖次数和中奖内容的转盘抽奖代码
2017/08/23 jQuery
详解webpack + vue + node 打造单页面(入门篇)
2017/09/23 Javascript
vuejs项目打包之后的首屏加载优化及打包之后出现的问题
2018/04/01 Javascript
微信小程序轮播图swiper代码详解
2020/12/01 Javascript
[49:27]2018DOTA2亚洲邀请赛 4.4 淘汰赛 TNC vs VG 第一场
2018/04/05 DOTA
python实现批量转换文件编码(批转换编码示例)
2014/01/23 Python
python 删除大文件中的某一行(最有效率的方法)
2017/08/19 Python
人脸识别经典算法一 特征脸方法(Eigenface)
2018/03/13 Python
python使用筛选法计算小于给定数字的所有素数
2018/03/19 Python
Python实现基于TCP UDP协议的IPv4 IPv6模式客户端和服务端功能示例
2018/03/22 Python
python用for循环求和的方法总结
2019/07/08 Python
python basemap 画出经纬度并标定的实例
2019/07/09 Python
django 自定义过滤器(filter)处理较为复杂的变量方法
2019/08/12 Python
Python逐行读取文件内容的方法总结
2020/02/14 Python
Django 项目通过加载不同env文件来区分不同环境
2020/02/17 Python
前端隐藏出边界内容的实现方法
2016/04/14 HTML / CSS
数控技术应届生求职信
2013/11/13 职场文书
青年志愿者活动总结
2014/04/26 职场文书
幸福家庭标语
2014/06/27 职场文书
八达岭长城导游词
2015/01/30 职场文书
欠款纠纷起诉状
2015/05/19 职场文书
2019财务管理制度最新范本!
2019/07/09 职场文书
《没有任何借口》读后感:完美的执行能力
2020/01/07 职场文书
详解Java分布式事务的 6 种解决方案
2021/06/26 Java/Android
HTML静态页面获取url参数和UserAgent的实现
2022/08/05 HTML / CSS