0

0

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

php中文网

php中文网

发布时间:2016-05-16 15:31:18

|

2282人浏览过

|

来源于php中文网

原创

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里的帧定义图最容易理解,所以就贴这张了):

 

上面的图中,每一列就是一个字节,一个字节总共是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

Upscale
Upscale

AI图片放大工具

下载

payload length:占7位,或者7+16位、或者7+64位。如果第二个字节的后面七个位的十进制值小于或等于125,则直接用这七个位表示数据长度;如果该值为126,说明 125

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的数据接收与发送的全部内容,希望大家喜欢。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
全国统一发票查询平台入口合集
全国统一发票查询平台入口合集

本专题整合了全国统一发票查询入口地址合集,阅读专题下面的文章了解更多详细入口。

12

2026.02.03

短剧入口地址汇总
短剧入口地址汇总

本专题整合了短剧app推荐平台,阅读专题下面的文章了解更多详细入口。

19

2026.02.03

植物大战僵尸版本入口地址汇总
植物大战僵尸版本入口地址汇总

本专题整合了植物大战僵尸版本入口地址汇总,前往文章中寻找想要的答案。

11

2026.02.03

c语言中/相关合集
c语言中/相关合集

本专题整合了c语言中/的用法、含义解释。阅读专题下面的文章了解更多详细内容。

2

2026.02.03

漫蛙漫画网页版入口与正版在线阅读 漫蛙MANWA官网访问专题
漫蛙漫画网页版入口与正版在线阅读 漫蛙MANWA官网访问专题

本专题围绕漫蛙漫画(Manwa / Manwa2)官网网页版入口进行整理,涵盖漫蛙漫画官方主页访问方式、网页版在线阅读入口、台版正版漫画浏览说明及基础使用指引,帮助用户快速进入漫蛙漫画官网,稳定在线阅读正版漫画内容,避免误入非官方页面。

8

2026.02.03

Yandex官网入口与俄罗斯搜索引擎访问指南 Yandex中文登录与网页版入口
Yandex官网入口与俄罗斯搜索引擎访问指南 Yandex中文登录与网页版入口

本专题汇总了俄罗斯知名搜索引擎 Yandex 的官网入口、免登录访问地址、中文登录方法与网页版使用指南,帮助用户稳定访问 Yandex 官网,并提供一站式入口汇总。无论是登录入口还是在线搜索,用户都能快速获取最新稳定的访问链接与使用指南。

92

2026.02.03

Java 设计模式与重构实践
Java 设计模式与重构实践

本专题专注讲解 Java 中常用的设计模式,包括单例模式、工厂模式、观察者模式、策略模式等,并结合代码重构实践,帮助学习者掌握 如何运用设计模式优化代码结构,提高代码的可读性、可维护性和扩展性。通过具体示例,展示设计模式如何解决实际开发中的复杂问题。

2

2026.02.03

C# 并发与异步编程
C# 并发与异步编程

本专题系统讲解 C# 异步编程与并发控制,重点介绍 async 和 await 关键字、Task 类、线程池管理、并发数据结构、死锁与线程安全问题。通过多个实战项目,帮助学习者掌握 如何在 C# 中编写高效的异步代码,提升应用的并发性能与响应速度。

2

2026.02.03

Python 强化学习与深度Q网络(DQN)
Python 强化学习与深度Q网络(DQN)

本专题深入讲解 Python 在强化学习(Reinforcement Learning)中的应用,重点介绍 深度Q网络(DQN) 及其实现方法,涵盖 Q-learning 算法、深度学习与神经网络的结合、环境模拟与奖励机制设计、探索与利用的平衡等。通过构建一个简单的游戏AI,帮助学习者掌握 如何使用 Python 训练智能体在动态环境中作出决策。

2

2026.02.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 10.2万人学习

CSS3 教程
CSS3 教程

共18课时 | 5.1万人学习

Go语言实战之 GraphQL
Go语言实战之 GraphQL

共10课时 | 0.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号