浅析nodejs实现Websocket的数据接收与发送


Posted in NodeJs onNovember 19, 2015

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。在WebSocket API中,浏览器和服务器只需要要做一个握手(handshaking)的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

WebSocket是一个通信的协议,分为服务器和客户端。服务器放在后台,保持与客户端的长连接,完成双方通信的任务。客户端一般都是实现在支持HTML5浏览器核心中,通过提供JavascriptAPI使用网页可以建立websocket连接。

在我写这篇文章里:基于html5和nodejs相结合实现websocket即使通讯,里面主要是借助了nodejs-websocket这个插件,后来还用了socket.io做了些demo,但是,这些都是借助于别人封装好的插件做出来的,websocket到底是怎么实现的呢?之前真没想过,最近看朴灵大神的《深入浅析node.js》时候,看到了websocket那一节,看了websocket的数据帧定义,想着用nodejs实现。经过一番折腾实现了。

客户端的代码就不说了,websocket的API还是很简单的,就通过onmessage、onopen、onclose,以及send方法就可以实现了。
websocket api通过onmessage、onopen、onclose以及send方法实现客户端的代码。具体详情就不多说了。

主要说服务端的代码:

首先是协议的升级,这个比较简单,就简述一下:当在客户端执行new Websocket("ws://XXX.com/")的时候,客户端就会发起请求报文进行握手申请,报文中有个很重要的key就是Sec-WebSocket-Key,服务端获取到key,然后将这个key与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相连,对新的字符串通过sha1安全散列算法计算出结果后,再进行base64编码,并且将结果放在请求头的"Sec-WebSocket-Accept"中返回即可完成握手。具体请看代码:

server.on('upgrade', function (req, socket, upgradeHead) {
 var key = req.headers['sec-websocket-key'];
 key = crypto.createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
 var headers = [
 'HTTP/1.1 101 Switching Protocols',
 'Upgrade: websocket',
 'Connection: Upgrade',
 'Sec-WebSocket-Accept: ' + key
 ];
 socket.setNoDelay(true);
 socket.write(headers.join("\r\n") + "\r\n\r\n", 'ascii');
 var ws = new WebSocket(socket);
 webSocketCollector.push(ws);
 callback(ws);
});

upgrade事件其实是http这个模块的封装,再往底层就是net模块的实现,其实都差不多,如果直接用net模块来实现的话,就是监听net.createServer返回的server对象的data事件,接收到的第一份数据就是客户端发来的升级请求报文。

上面那段代码就完成了websocket的握手,然后就可以开始数据传输了。

看数据传输之前,先看看websocket数据帧的定义(因为觉得深入浅出nodejs里的帧定义图最容易理解,所以就贴这张了):

浅析nodejs实现Websocket的数据接收与发送 

上面的图中,每一列就是一个字节,一个字节总共是8位,每一位就是一个二进制数,不同位的值会对应不同的意义。

fin:指示这个是消息的最后片段。第一个片段可能也是最后的片段。如果为1即为最后片段,(其实这个位的用途我个人有点疑惑,按照书上以及网上查的资料,当数据被分片的时候,不同片应该都会有fin位,会根据fin为是不是0来判断是否为最后一帧,但是实际实现中却发现,当数据比较大需要分片时,服务端收到的数据就只有第一帧是有fin位为1,其他帧则整个帧都是数据段,也就是说,感觉这个fin位似乎用不上,至少我自己写的demo中是通过数据长度来判断是否到了最后一帧,完全没用到这个fin位是否为1来判断)

rsv1、rsv2、rsv3:各占一个位,用于扩展协商,基本上不怎么需要理,一般都是0

opcode:占四个位,可以表示0~15的十进制,0表示为附加数据帧,1表示为文本数据帧,2表示二进制数据帧,8表示发送一个连接关闭的数据帧,9表示ping,10表示pong,ping和pong都是用于心跳检测,当一端发送ping时,另一端必须响应pong表示自己仍处于响应状态。

masked:占一个位,表示是否进行掩码处理,客户端发送给服务端时为1,服务端发送给客户端时为0

payload length:占7位,或者7+16位、或者7+64位。如果第二个字节的后面七个位的十进制值小于或等于125,则直接用这七个位表示数据长度;如果该值为126,说明 125<数据长度<65535(16个位能描述的最大值,也就是16个1的时候),就用第三个字节及第四个字节即16个位来表示;如果该值为127,则说明数据长度已经大于65535,16个位也已经不足以描述数据长度了,就用第三到第十个字节这八个字节来描述数据长度。

masking key:当masked为1的时候才存在,用于对我们需要的数据进行解密。

payload data:我们需要的数据,如果masked为1,该数据会被加密,要通过masking key进行异或运算解密才能获取到真实数据。

帧定义解释完了,就可以根据数据来进行解析了,当有data过来的时候,先获取需要的数据信息,下面这段代码将获取到数据在data里的位置,以及数据长度,masking key以及opcode:

WebSocket.prototype.handleDataStat = function (data) {
 if (!this.stat) {
 var dataIndex = 2; //数据索引,因为第一个字节和第二个字节肯定不为数据,所以初始值为2
 var secondByte = data[1]; //代表masked位和可能是payloadLength位的第二个字节
 var hasMask = secondByte >= 128; //如果大于或等于128,说明masked位为1
 secondByte -= hasMask ? 128 : 0; //如果有掩码,需要将掩码那一位去掉
 var dataLength, maskedData;
 //如果为126,则后面16位长的数据为数据长度,如果为127,则后面64位长的数据为数据长度
 if (secondByte == 126) {
  dataIndex += 2;
  dataLength = data.readUInt16BE(2);
 } else if (secondByte == 127) {
  dataIndex += 8;
  dataLength = data.readUInt32BE(2) + data.readUInt32BE(6);
 } else {
  dataLength = secondByte;
 }
 //如果有掩码,则获取32位的二进制masking key,同时更新index
 if (hasMask) {
  maskedData = data.slice(dataIndex, dataIndex + 4);
  dataIndex += 4;
 }
 //数据量最大为10kb
 if (dataLength > 10240) {
  this.send("Warning : data limit 10kb");
 } else {
  //计算到此处时,dataIndex为数据位的起始位置,dataLength为数据长度,maskedData为二进制的解密数据
  this.stat = {
  index: dataIndex,
  totalLength: dataLength,
  length: dataLength,
  maskedData: maskedData,
  opcode: parseInt(data[0].toString(16).split("")[1] , 16) //获取第一个字节的opcode位
  };
 }
 } else {
 this.stat.index = 0;
 }
};

代码中均有注释,理解起来应该不难,直接看下一步,获取到数据信息后,就要对数据进行实际解析了:

经过上面handleDataStat方法的处理,stat中已经有了data的相关数据,先判断opcode,如果为9说明是客户端发起的ping心跳检测,直接返回pong响应,如果为10则为服务端发起的心跳检测。如果有masking key,则遍历数据段,对每个字节都与masking key的字节进行异或运算(网上看到一个说法很形象:就是轮流发生X关系),^符号就是进行异或运算啦。如果没有masking key则直接通过slice方法把数据截取下来。

获取到数据后,放进datas里保存,因为有可能数据被分片了,所以再将stat里的长度减去当前数据长度,只有当stat里的长度为0的时候,说明当前帧为最后一帧,然后通过Buffer.concat将所有数据合并,此时再判断一下opcode,如果opcode为8,则说明客户端发起了一个关闭请求,而我们获取到的数据则是关闭原因。如果不为8,则这数据就是我们需要的数据。然后再将stat重置为null,datas数组置空即可。至此,我们的数据解析就完成了。

WebSocket.prototype.dataHandle = function (data) {
 this.handleDataStat(data);
 var stat;
 if (!(stat = this.stat)) return;
 //如果opcode为9,则发送pong响应,如果opcode为10则置pingtimes为0
 if (stat.opcode === 9 || stat.opcode === 10) {
 (stat.opcode === 9) ? (this.sendPong()) : (this.pingTimes = 0);
 this.reset();
 return;
 }
 var result;
 if (stat.maskedData) {
 result = new Buffer(data.length-stat.index);
 for (var i = stat.index, j = 0; i < data.length; i++, j++) {
  //对每个字节进行异或运算,masked是4个字节,所以%4,借此循环
  result[j] = data[i] ^ stat.maskedData[j % 4];
 }
 } else {
 result = data.slice(stat.index, data.length);
 }
 this.datas.push(result);
 stat.length -= (data.length - stat.index);
 //当长度为0,说明当前帧为最后帧
 if (stat.length == 0) {
 var buf = Buffer.concat(this.datas, stat.totalLength);
 if (stat.opcode == 8) {
  this.close(buf.toString());
 } else {
  this.emit("message", buf.toString());
 }
 this.reset();
 }
};

完成了客户端发来的数据解析,还需要一个服务端发数据至客户端的方法,也就是按照上面所说的帧定义来组装数据并且发送出去。下面的代码中基本上每一行都有注释,应该还是比较容易理解的。

//数据发送
WebSocket.prototype.send = function (message) {
 if(this.state !== "OPEN") return;
 message = String(message);
 var length = Buffer.byteLength(message);
// 数据的起始位置,如果数据长度16位也无法描述,则用64位,即8字节,如果16位能描述则用2字节,否则用第二个字节描述
 var index = 2 + (length > 65535 ? 8 : (length > 125 ? 2 : 0));
// 定义buffer,长度为描述字节长度 + message长度
 var buffer = new Buffer(index + length);
// 第一个字节,fin位为1,opcode为1
 buffer[0] = 129;
// 因为是由服务端发至客户端,所以无需masked掩码
 if (length > 65535) {
 buffer[1] = 127;
// 长度超过65535的则由8个字节表示,因为4个字节能表达的长度为4294967295,已经完全够用,因此直接将前面4个字节置0
 buffer.writeUInt32BE(0, 2);
 buffer.writeUInt32BE(length, 6);
 } else if (length > 125) {
 buffer[1] = 126;
// 长度超过125的话就由2个字节表示
 buffer.writeUInt16BE(length, 2);
 } else {
 buffer[1] = length;
 }
// 写入正文
 buffer.write(message, index);
 this.socket.write(buffer);
};

最后还要实现一个功能,就是心跳检测:防止服务端长时间不与客户端交互而导致客户端关闭连接,所以每隔十秒都会发送一次ping进行心跳检测

//每隔10秒进行一次心跳检测,若连续发出三次心跳却没收到响应则关闭socket
WebSocket.prototype.checkHeartBeat = function () {
 var that = this;
 setTimeout(function () {
 if (that.state !== "OPEN") return;
 if (that.pingTimes >= 3) {
  that.close("time out");
  return;
 }
 //记录心跳次数
 that.pingTimes++;
 that.sendPing();
 that.checkHeartBeat();
 }, 10000);
};
WebSocket.prototype.sendPing = function () {
 this.socket.write(new Buffer(['0x89', '0x0']))
};
WebSocket.prototype.sendPong = function () {
 this.socket.write(new Buffer(['0x8A', '0x0']))
};

至此,整个websocket的实现就完成了,此demo只是大概实现了一下websocket而已,在安全之类方面肯定还是有很多问题,若是真正生产环境中还是用socket.io这类成熟的插件比较好。不过这还是很值得一学的。

以上内容就是小编给大家分享的浅析nodejs实现Websocket的数据接收与发送的全部内容,希望大家喜欢。

NodeJs 相关文章推荐
NodeJS Express框架中处理404页面一个方式
May 28 NodeJs
nodejs获取本机内网和外网ip地址的实现代码
Jun 01 NodeJs
Nodejs中读取中文文件编码问题、发送邮件和定时任务实例
Jan 01 NodeJs
nodejs开发微博实例
Mar 25 NodeJs
NodeJS学习笔记之Module的简介
Mar 24 NodeJs
NodeJS处理Express中异步错误
Mar 26 NodeJs
nodejs async异步常用函数总结(推荐)
Nov 17 NodeJs
nodejs超出最大的调用栈错误问题
Dec 27 NodeJs
NodeJS父进程与子进程资源共享原理与实现方法
Mar 16 NodeJs
nodejs更新package.json中的dependencies依赖到最新版本的方法
Oct 10 NodeJs
NodeJs实现简易WEB上传下载服务器
Aug 10 NodeJs
nodejs dgram模块广播+组播的实现示例
Nov 04 NodeJs
Nodejs实战心得之eventproxy模块控制并发
Oct 27 #NodeJs
浅谈Nodejs观察者模式
Oct 13 #NodeJs
使用Angular和Nodejs、socket.io搭建聊天室及多人聊天室
Aug 21 #NodeJs
nodejs创建web服务器之hello world程序
Aug 20 #NodeJs
windows下安装nodejs及框架express
Aug 07 #NodeJs
使用DNode实现php和nodejs之间通信的简单实例
Jul 06 #NodeJs
iPhone手机上搭建nodejs服务器步骤方法
Jul 06 #NodeJs
You might like
PHP学习 运算符与运算符优先级
2008/06/15 PHP
php 随机生成10位字符代码
2009/03/26 PHP
php实现信用卡校验位算法THE LUHN MOD-10示例
2014/05/07 PHP
脚本收藏iframe
2006/07/21 Javascript
js控制容器隐藏出现防止样式变化的两种方法
2014/04/25 Javascript
yepnope.js使用详解及示例分享
2014/06/23 Javascript
jQuery on()方法示例及jquery on()方法的优点
2015/08/27 Javascript
javascript检测移动设备横竖屏
2016/05/21 Javascript
js实现把图片的绝对路径转为base64字符串、blob对象再上传
2016/12/29 Javascript
详解jQuery事件
2017/01/13 Javascript
Avalonjs 实现简单购物车功能(实例代码)
2017/02/07 Javascript
详解如何在vue项目中引入elementUI组件
2018/02/11 Javascript
js根据json数据中的某一个属性来给数据分组的方法
2018/10/08 Javascript
node-red File读取好保存实例讲解
2019/09/11 Javascript
layui字体图标 loading图标静止不旋转的解决方法
2019/09/23 Javascript
放弃 Python 转向 Go语言有人给出了 9 大理由
2017/10/20 Python
读取本地json文件,解析json(实例讲解)
2017/12/06 Python
微信跳一跳python自动代码解读1.0
2018/01/12 Python
Python3.6日志Logging模块简单用法示例
2018/06/14 Python
对django views中 request, response的常用操作详解
2019/07/17 Python
使用批处理脚本自动生成并上传NuGet包(操作方法)
2019/11/19 Python
关于ZeroMQ 三种模式python3实现方式
2019/12/23 Python
Python基于百度AI实现OCR文字识别
2020/04/02 Python
Python 合并拼接字符串的方法
2020/07/28 Python
详解python tkinter包获取本地绝对路径(以获取图片并展示)
2020/09/04 Python
旧时光糖果:Old Time Candy
2018/02/05 全球购物
澳大利亚珍珠首饰购物网站:Vayo Pearls
2019/03/11 全球购物
Timberland澳大利亚官网:全球领先的户外品牌
2019/12/10 全球购物
心理健康教育心得体会
2013/12/29 职场文书
中学生自我评价范文
2014/02/08 职场文书
客运企业隐患排查工作方案
2014/06/06 职场文书
使用springboot暴露oracle数据接口的问题
2021/05/07 Oracle
如何使用flask将模型部署为服务
2021/05/13 Python
使用Pytorch训练two-head网络的操作
2021/05/28 Python
Django实现聊天机器人
2021/05/31 Python
Ajax异步刷新功能及简单案例
2021/11/20 Javascript