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 使用 net 模块模拟 websocket 握手,进行数据传递。的更多相关文章
- node.js中ws模块创建服务端和客户端,网页WebSocket客户端
首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...
- node.js入门(二) 模块 事件驱动
模块化结构 node.js 使用了 CommonJS 定义的模块系统.不同的功能组件被划分成不同的模块.应用可以根据自己的需要来选择使用合适的模块.每个模块都会暴露一些公共的方法或属性.模块使用者直接 ...
- 利用Node.js的Net模块实现一个命令行多人聊天室
1.net模块基本API 要使用Node.js的net模块实现一个命令行聊天室,就必须先了解NET模块的API使用.NET模块API分为两大类:Server和Socket类.工厂方法. Server类 ...
- Node.js的Formidable模块的使用
今天总结了下Node.js的Formidable模块的使用,下面做一些简要的说明. 1) 创建Formidable.IncomingForm对象 var form = new formidab ...
- Node.js入门:模块机制
CommonJS规范 早在Netscape诞生不久后,JavaScript就一直在探索本地编程的路,Rhino是其代表产物.无奈那时服务端JavaScript走的路均是参考众多服务器端语言来 ...
- Node.js的net模块
net模块提供了一个异步网络包装器,用于TCP网络编程,它包含了创建服务器和客户端的方法 创建TCP服务器 net.createServer方法 创建客户端去连接服务器 net.connect方法 简 ...
- node.js中express模块创建服务器和http模块客户端发请求
首先下载express模块,命令行输入 npm install express 1.node.js中express模块创建服务端 在js代码同文件位置新建一个文件夹(www_root),里面存放网页文 ...
- node.js中net模块创建服务器和客户端(TCP)
node.js中net模块创建服务器和客户端 1.node.js中net模块创建服务器(net.createServer) // 将net模块 引入进来 var net = require(" ...
- 第5月第10天 node.js的request模块
1.node.js的request模块 http://www.cnblogs.com/meteoric_cry/archive/2012/08/18/2645530.html
随机推荐
- Java Web参考资料
Maven Maven学习 Eclipse 使用Maven 构建Web项目的最佳方式:总的来说就是先要修改项目的Project facets来更改Java版本.Servlet版本. 错误Cannot ...
- dependency walker检查dll依赖关系目录设置的问题
废话少说,直接上图 图中来看,似乎IESHIMS.DLL文件不存在报错,实际是因为没有加载IESHIMS.DLL所在的路径. 在我的电脑里面搜索有两个同名的dll,一个是32位的,一个是64位的. C ...
- access数据库编号转换成统一3位数长度方法,不足3位前面补零
select C_CUN+Format(Val(NZ(C_LB)),"000") from LBM 这条SQL只能在access数据库中执行,因为sql不支持NZ函数,而且c_lb ...
- 涨姿势:深入 foreach循环
我们知道集合中的遍历都是通过迭代(iterator)完成的. 也许有人说,不一定非要使用迭代,如: List<String> list = new LinkedList<String ...
- Alpha阶段项目规划
Alpha阶段任务 概述 我们团队采访学长并听从学长的意见之后,决定根据第一版的phylab项目进行重构.但由于第一版的phylab项目在github上的代码仅仅只有alpha版本,我们接手之后进行了 ...
- [java,2017-05-04] 创建word文档
package test; import java.text.SimpleDateFormat; import java.util.Date; import com.aspose.words.Data ...
- java实现pdf按页切分成图片
package com.ces.component.pictrueCut.entity; import java.awt.Image; import java.awt.Rectangle; impor ...
- 异步请求Ajax(取得json数据)
异步请求Ajax 没有学习Ajax之前请求数据的时候都是整个页面全部刷新了一次,也就是每次请求都会重新请求所有的资源.但是在很多时候不需要页面全部刷新,仅仅是需要页面的局部数据刷新即可,此时需要发送异 ...
- Eclipse 中Git的使用及如何解决冲突
1. 如何导入已有Git项目 1.1 File——>import… 出现以下界面 1.2 找到Git,然后双击‘Project from Git.或者点击next 1.3 双击Clone URI ...
- 初学Python的奇葩用法
ming_piao= 11ming_yeji= 586319ming_age= 34ming_gongling= 10ming_yanjiang= 81 qiang_piao= 7qiang_yeji ...