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代码
Jan 01 Javascript
学习ExtJS(一) 之基础前提
Oct 07 Javascript
Js 回车换行处理的办法及replace方法应用
Jan 24 Javascript
JavaScript中var关键字的使用详解
Aug 14 Javascript
js文本框输入内容智能提示效果
Dec 02 Javascript
使用Script元素发送JSONP请求的方法
Jun 12 Javascript
jquery点赞功能实现代码 点个赞吧!
May 29 jQuery
js 获取html5的data属性实现方法
Jul 28 Javascript
vue-自定义组件传值的实例讲解
Sep 18 Javascript
JS面向对象编程基础篇(三) 继承操作实例详解
Mar 03 Javascript
JavaScript ECMA-262-3 深入解析(一):执行上下文实例分析
Apr 25 Javascript
vue3+typeScript穿梭框的实现示例
Dec 29 Vue.js
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
Thinkphp和onethink实现微信支付插件
2016/04/13 PHP
JavaScript开发规范要求(规范化代码)
2010/08/16 Javascript
关于JS字符串函数String.replace()
2013/04/07 Javascript
jquery单行文字向上滚动效果示例
2014/03/06 Javascript
js中匿名函数的创建与调用方法分析
2014/12/19 Javascript
js实现两点之间画线的方法
2015/05/12 Javascript
jQuery根据用户电脑是mac还是pc加载对应样式的方法
2015/06/26 Javascript
JavaScript+html5 canvas实现本地截图教程
2020/04/16 Javascript
JS验证逗号隔开可以是中文字母数字
2016/04/22 Javascript
JS去除空格和换行的正则表达式(推荐)
2016/06/14 Javascript
解决前端跨域问题方案汇总
2016/11/20 Javascript
JavaScript之class继承_动力节点Java学院整理
2017/07/03 Javascript
javaScript中封装的各种写法示例(推荐)
2017/07/03 Javascript
Node.js readline 逐行读取、写入文件内容的示例
2018/03/01 Javascript
vue iView 上传组件之手动上传功能
2018/03/16 Javascript
JS实现普通轮播图特效
2020/01/01 Javascript
动态实现element ui的el-table某列数据不同样式的示例
2021/01/22 Javascript
[00:52]DOTA2第二届亚洲邀请赛预选赛宣传片
2017/01/13 DOTA
在Python的gevent框架下执行异步的Solr查询的教程
2015/04/16 Python
在Django框架中编写Contact表单的教程
2015/07/17 Python
全面了解python字符串和字典
2016/07/07 Python
Python两个内置函数 locals 和globals(学习笔记)
2016/08/28 Python
Python探索之SocketServer详解
2017/10/28 Python
python2.7实现爬虫网页数据
2018/05/25 Python
django1.11.1 models 数据库同步方法
2018/05/30 Python
对python中for、if、while的区别与比较方法
2018/06/25 Python
解决python中使用plot画图,图不显示的问题
2018/07/04 Python
结合OpenCV与TensorFlow进行人脸识别的实现
2019/10/10 Python
Python数据可视化图实现过程详解
2020/06/12 Python
世界著名的顶级牛排:Omaha Steak(奥马哈牛排)
2016/09/20 全球购物
咖啡厅商业计划书
2014/09/15 职场文书
2014年仓库管理工作总结
2014/12/17 职场文书
小区环境卫生倡议书
2015/04/29 职场文书
2015年新农村建设指导员工作总结
2015/07/24 职场文书
PHP实现考试倒计时功能代码
2021/04/16 PHP
win10识别不了U盘怎么办 win10系统读取U盘失败的解决办法
2022/08/05 数码科技