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

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

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

(0)

相关推荐

  • Python通过websocket与js客户端通信示例分析

    具体的 websocket 介绍可见 http://zh.wikipedia.org/wiki/WebSocket 这里,介绍如何使用 Python 与前端 js 进行通信. websocket 使用 HTTP 协议完成握手之后,不通过 HTTP 直接进行 websocket 通信. 于是,使用 websocket 大致两个步骤:使用 HTTP 握手,通信. js 处理 websocket 要使用 ws 模块: Python 处理则使用 socket 模块建立 TCP 连接即可,比一般的 soc

  • 详解WebSocket+spring示例demo(已使用sockJs库)

    1.简介 作为下一代的Web标准,HTML5拥有许多引人注目的新特性,如 Canvas.本地存储.多媒体编程接口.WebSocket等等.这其中有"Web的 TCP"之称的 WebSocket格外吸引开发人员的注意.WebSocket的出现使得浏览器提供对 Socket的支持成为可能,从而在浏览器和服务器之间提供了一个基于TCP连接的双向通道.Web开发人员可以非常方便地使用WebSocket构建实时 web 应用,开发人员的手中从此又多了一柄神兵利器. Web 应用的信息交互过程通常

  • JavaScript之WebSocket技术详解

    概述 HTTP协议是一种无状态协议,服务器端本身不具有识别客户端的能力,必须借助外部机制,比如session和cookie,才能与特定客户端保持对话.这多多少少带来一些不便,尤其在服务器端与客户端需要持续交换数据的场合(比如网络聊天),更是如此.为了解决这个问题,HTML5提出了浏览器的WebSocket API. WebSocket的主要作用是,允许服务器端与客户端进行全双工(full-duplex)的通信.举例来说,HTTP协议有点像发电子邮件,发出后必须等待对方回信:WebSocket则是

  • Javascript WebSocket使用实例介绍(简明入门教程)

    一旦你了解了网络套接字与WEB服务器的连接,你将可以从浏览器发送数据到服务器并且可以接收由服务器返回的响应数据. 以下是创建一个新的WebSocket对象的API: 复制代码 代码如下: var Socket = new WebSocket(url, [protocal] ); 这里第一个参数是指要连接的URL,第二个参数是可选的,如果需要的话,则是指定一个的服务器支持的协议. WEB Socket属性: 属性 说明 Socket.readyState readyState的代表的ReadOnl

  • JS实现websocket长轮询实时消息提示的效果

    效果图如下: 参考代码如下: jsp代码: <%@ page contentType="text/html;charset=UTF-8" language="java"%> <div class="page-header navbar navbar-fixed-top"> <div class="page-header-inner"> <div class="page-log

  • 基于html5和nodejs相结合实现websocket即使通讯

    最近都在学习HTML5,做canvas游戏之类的,发现HTML5中除了canvas这个强大的工具外,还有WebSocket也很值得注意.可以用来做双屏互动游戏,何为双屏互动游戏?就是通过移动端设备来控制PC端网页游戏.这样的话就要用到实时通讯了,而WebSocket无疑是最合适的.WebSocket相较于HTTP来说,有很多的优点,主要表现在WebSocket只建立一个TCP连接,可以主动推送数据到客户端,而且还有更轻量级的协议头,减少数据传送量.所以WebSocket暂时来说是实时通讯的最佳协

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

    WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术.在WebSocket API中,浏览器和服务器只需要要做一个握手(handshaking)的动作,然后,浏览器和服务器之间就形成了一条快速通道.两者之间就直接可以数据互相传送. WebSocket是一个通信的协议,分为服务器和客户端.服务器放在后台,保持与客户端的长连接,完成双方通信的任务.客户端一般都是实现在支持HTML5浏览器核心中,通过提供JavascriptAPI使用网页可以建立websocket连接.

  • 原生nodejs使用websocket代码分享

    安装: npm  install  ws 服务端(nodejs): var WebSocketServer = require('ws').Server, wss = new WebSocketServer({ port: 8080 }); wss.on('connection', function (ws) { console.log('client connected'); ws.on('message', function (message) { console.log(message);

  • nodejs body-parser 解析post数据实例

    安装 $ npm install body-parser API var bodyPaeser =require('body-parser') 可以通过body-parser 对象创建中间件,当接收到客户端请求时所有的中间件都会给req.body 添加属性,请求体为空,则解析为空{} (或者出现错误). bodyParser.json(options) 中间件只会解析 json ,允许请求提任意Unicode编码支持 gzip 和 deflate 编码. options 一个对象,有以下属性 i

  • 详解如何在Node.js的httpServer中接收前端发送的arraybuffer数据

    最近使用了protobuf进行数据交互,发送在node.js接收前端的二进制数据出现了数据错误等问题.后来发现思路上面的问题,在req.on('data',()=>{})事件中的处理不适当才引发数据错乱.借此发 我先直接贴正确接收二进制数据代码 const server = http.createServer((req, res) => { if(req.method==='OPTIONS'){ res.setHeader("Access-Control-Allow-Origin&q

  • NodeJS落地WebSocket实践前端架构师破局技术

    目录 网络协议进化 Socket.IO? ws 模块实现 服务端 客户端 Express 集成 WebSocket 实例 浏览器 服务端 消息广播 安全与认证 Token 连接认证 wss 支持 BFF 应用 网络协议进化 HTTP 协议是前端最熟悉的网络通信协议.我们通常的打开网页,请求接口,都属于 HTTP 请求. HTTP 请求的特点是:请求-> 响应.客户端发起请求,服务端收到请求后进行响应,一次请求就完成了.也就是说,HTTP 请求必须由客户端发起,服务端才能被动响应. 除此之外,发起

  • java使用Socket类接收和发送数据

    网络应用分为客户端和服务端两部分,而Socket类是负责处理客户端通信的Java类.通过这个类可以连接到指定IP或域名的服务器上,并且可以和服务器互相发送和接受数据.在本文及后面的数篇文章中将详细讨论Socket类的使用,内容包括Socket类基础.各式各样的连接方式.get和set方法.连接过程中的超时以及关闭网络连接等. 在本文中,我们将讨论使用Socket类的基本步骤和方法.一般网络客户端程序在连接服务程序时要进行以下三步操作. 连接服务器 发送和接收数据 关闭网络连接 一.连接服务器 在

  • nodejs 十六进制字符串型数据与btye型数据相互转换

    byte型转换十六进制字符串 /** * byte型转换十六进制 * @param b * @returns {string} * @constructor */ const Bytes2HexString = (b)=> { let hexs = ""; for (let i = 0; i < b.length; i++) { let hex = (b[i]).toString(16); if (hex.length === 1) { hexs = '0' + hex;

  • Python3 串口接收与发送16进制数据包的实例

    如下所示: import serial import string import binascii s=serial.Serial('com4',9600) s.open() #接收 n=s.inwaiting() if n: data= str(binascii.b2a_hex(s.read(n)))[2:-1] print(data) #发送 d=bytes.fromhex('10 11 12 34 3f') s.write(d) s.close() 以上这篇Python3 串口接收与发送1

  • Layui数据表格 前后端json数据接收的方法

    先上效果图: 前端数据表格: <div class="x-body"> <%-- 数据表格 --%> <table class="layui-table" lay-data="{ id:'test', url:'/menu/page', page:true, limits: [10,20,50], //每页条数的选择项,默认:[10,20,30,40,50,60,70,80,90] limit: 10, //每页默认显示的数量

  • 对Layer弹窗使用及返回数据接收的实例详解

    Layer做为弹窗,主页面代码: 注意1. callback()方法 返回字符串,再data:JSON.parse(res)转为ajax提交时的json参数 注意2. textarea 上使用append方法没有效果,改写: var str = $("#area").val() + "\n"+d; $("#area").val(str); //引入js 必须要有(1.8以上版本) jquery <script src="__PUB

随机推荐