WebSocket是HTML5提供的一种网络技术,用于浏览器和服务器之间的全双工通信。在WebSocket API中,浏览器和服务器只需要做一个握手,然后在浏览器和服务器之间形成一个快速通道。数据可以在它们之间直接传输。
WebSocket是一种通信协议,分为服务器端和客户端。服务器放置在后台,与客户端保持长时间的连接,完成双方的通信任务。一般在支持HTML5的浏览器内核中实现客户端,通过提供JavascriptAPI和使用网页可以建立websocket连接。
在这篇文章中,我写道:为了实现基于html5和nodejs结合的websocket,即使进行了通信,也主要使用插件nodejs-websocket,后来用socket.io做了一些演示,不过这些都是借助别人打包的插件做的。websocket是如何实现的?以前真的没有想到。最近看大神朴灵的《深入浅析node.js》的时候看到了websocket部分,看了websocket数据帧定义,想到用nodejs来实现。经过一番磨难,它实现了。
客户端代码就不提了,websocket API还是很简单的,可以通过onmessage、onopen、onclose、send方法实现。websocket api通过onmessage、onopen、onclose和send方法实现客户端代码。我就不细说了。
主要说服务器代码:
首先是协议的升级,比较简单。简单描述一下:在客户端执行New WebSocket(' ws ://XXX.com/')时,客户端会发起一个申请握手的请求消息,消息中一个重要的密钥是Sec-WebSocket-Key,由服务器获取。然后,用字符串258 eafa 5-e914-47da-95ca-c5ab 0dc 85 b 11连接这个密钥,通过sha1安全哈希算法计算新字符串的结果,然后用base64编码,在请求头的‘Sec-WebSocket-Accept’中返回结果,完成握手。有关详细信息,请参见代码:
server.on('upgrade ',function (req,socket,upgrade head){ var key=req . headers[' sec-web socket-key '];key=crypto.createHash('sha1 ')。更新(键‘258 eafa 5-E914-47DA-95CA-c5ab 0dc 85 b 11’)。摘要(' base64 ');var头=[ 'HTTP/1.1 101交换协议',' Upgrade: websocket ',' Connection: Upgrade ',' Sec-WebSocket-Accept: '键];socket . setnodeley(true);socket . write(headers . join(' r n ')' r n r n ',' ascii ');var ws=new WebSocket(socket);websocketcollector . push(ws);回调(ws);});升级事件实际上是http模块的封装,底层是net模块的实现,几乎是一样的。如果直接由net模块实现,则是监控net.createServer返回的服务器对象的数据事件,收到的第一个数据是客户端发送的升级请求消息。
上面的代码完成了websocket的握手,然后就可以开始数据传输了。
先看websocket数据帧的定义再看数据传输(因为我觉得nodejs中的帧定义图最容易理解,所以贴了这张):
上图中,每一列为一个字节,一个字节共8位,每一位为二进制数,不同位的值会对应不同的含义。
Fin:表示这是消息的最后一个片段。第一个剪辑也可能是最后一个剪辑。如果是1,则是最后一个片段。(其实我有点搞不清楚这位的目的。根据书本和网上找到的数据,当数据碎片化时,不同的切片应该有fin位,会根据fin是否为0来判断是否是最后一帧。但是在实际实现中发现,当数据较大,需要分片时,只有服务器收到的第一帧数据有fin位1,整个帧是其他帧的数据段。也就是说,我感觉这个手指头好像没有用。至少在我自己写的演示中,我用数据长度来判断是否到达了最后一帧,根本没有用这个fin位来判断是否是1。)
Rsv1、rsv2、rsv3:各占用一位,用于扩展协商。基本上不需要太多理由,一般为0。
操作码:占用四位,可以代表0~15的十进制。0表示附加数据帧,1表示文本数据帧,2表示二进制数据帧,8表示具有封闭连接的数据帧,9表示ping,10表示pong。ping和pong都用于心跳检测。当一端发送ping时,另一端必须响应pong,表示仍处于响应状态。
Masked:占用一位,表示是否屏蔽;当客户端将其发送到服务器时为1,当服务器将其发送到客户端时为0。
有效载荷长度:7位,或7 16位,或7 64位。如果第二个字节最后七位的十进制值小于或等于125,则数据长度直接由这七位表示。如果取值为126,则表示125的数据长度为65535(可以用16位描述的最大值,即有16个1时),由第三个字节和第四个字节表示,即16位;如果该值为127,则意味着数据长度已经大于65535,16位已经不足以描述数据长度,因此从第三个字节到第十个字节用8个字节来描述数据长度。
屏蔽密钥:只有屏蔽为1时才存在,用于解密我们需要的数据。
有效载荷数据:我们需要的数据,如果屏蔽为1,数据将被加密,真正的数据只能通过屏蔽密钥进行异或解密才能获得。
解释完框架定义后,可以根据数据进行解析。有数据时,先获取需要的数据信息。以下代码将获得数据在数据中的位置,以及数据长度、屏蔽键和操作码:
WebSocket . prototype . handledatastat=function(data){ if(!this . stat){ var dataIndex=2;//数据索引,因为第一个字节和第二个字节肯定不是数据,初始值为2 var second byte=data[1];//表示屏蔽位和可能是payloadLength位的第二个字节;var hasMask=secondByte=128//如果大于等于128,说明屏蔽位是1秒字节-=hasMask?128 : 0;//如果有掩码,需要从掩码位中删除vardatalength和掩码数据。//如果是126,接下来16位的数据就是数据长度;如果是127,接下来64位的数据是数据长度if(第二字节==126){数据索引=2;dataLength=data . readuint16be(2);} else if(second byte==127){ dataIndex=8;dataLength=data . readuint 32 be(2)data . readuint 32 be(6);} else { dataLength=secondByte}//如果有掩码,获取32位二进制掩码密钥,更新索引if(has mask){ masked data=data . slice(数据索引,数据索引4);dataIndex=4;}//如果(datalength 10240) {this,则最大数据量为10kb。发送(“警告:数据限制10kb”);} else {//当在这里计算时,dataIndex是数据位的起始位置,dataLength是数据长度,maskedData是二进制解密数据this.stat={index:data index,totallength3360data length,Length : datalength,maskedData 3360 maskedData,opcode : par sent(data[0])。tostring (16)。split(')[1],16)//获取第一个字节的操作码位};} } else { this . stat . index=0;}};代码中有注释,应该不难理解。直接看下一步。获取数据信息后,需要对数据进行实际分析:
上述handleDataStat方法处理后,Stat中已经存在数据的相关数据。首先,确定操作码。如果是9,说明是客户端发起的ping心跳检测,直接返回pong响应。如果是10,则是服务器发起的心跳检测。如果有屏蔽键,遍历数据段,用屏蔽键的字节对每个字节进行异或运算(网上有一个生动的说法:X关系依次出现),符号为异或运算。如果没有屏蔽键,数据会被切片法直接截取。
获取数据后,放入数据集中保存。因为数据可能是分段的,所以从stat中的长度减去当前数据长度。只有当stat中的长度为0时,才意味着当前帧是最后一帧,然后通过Buffer.concat合并所有数据,此时再次判断操作码。如果操作码是8,说明客户端发起了关机请求,我们得到的数据就是关机原因。如果不是8,那么这个数据就是我们需要的。然后将stat重置为null,并清空数据数组。至此,我们的数据分析完成。
WebSocket . prototype . datahandle=function(数据){ this.handleDataStat(数据);var statif(!(stat=this.stat))返回;//如果操作码为9,则发送乒乓响应;如果操作码为10,则将pingtimes设置为0 (stat。操作码===9 | | stat。操作码==10) {(stat。操作码===9)?(this . SendPong()):(this . PingTimes=0);this . reset();返回;} var结果;if(stat . maskeddata){ result=new Buffer(data . length-stat . index);for (var i=stat.index,j=0;一.数据.长度;I,j) {//对每个字节进行异或运算,屏蔽为4个字节,所以为%4,这样结果[j]=data [I] stat。屏蔽数据[j% 4]被循环;} } else { result=data . slice(stat . index,data . length);} this.datas.push(结果);stat . length-=(data . length-stat . index);//当长度为0时,表示当前帧是最后一帧if (stat。length==0) {varbuf=buffer。concat(这个。数据统计。总计长度);if(stat . opcode==8){ this . close(buf . tostring());} else { this.emit('message ',buf . tostring());} this . reset();}};对客户端发送的数据进行分析后,需要一种从服务器向客户端发送数据的方法,即按照上面的帧定义组装数据并发送出去。下面的代码基本上每行都有注释,应该很容易理解。
//发送数据WebSocket . prototype . send=function(message){ if(this。状态!=='OPEN ')返回;消息=字符串(消息);可变长度=缓冲区字节长度(消息);//数据的起始位置。如果数据长度为16位,则为64位,即8字节。如果可以描述16位,那就是2字节。否则,第二个字节将用于描述var index=2(长度65535?8 :(长度125?2 : 0));//定义缓冲区,其长度为描述字节消息的长度。var buffer=新缓冲区(索引长度);//第一个字节,fin位为1,操作码为1 buffer[0]=129;//因为是从服务器发送到客户端,所以不需要屏蔽掩码if(长度65535){ buffer[1]=127;//如果长度超过65535,则用8字节表示。因为4字节表示的长度是4294967295,已经足够了,所以前4个字节直接设置为0 buffer.writeUInt32BE(0,2)。buffer.writeUInt32BE(长度,6);} else if(长度125){ buffer[1]=126;//如果长度超过125,buffer.writeUInt16BE(长度,2)将由2个字节表示;} else { buffer[1]=长度;}//写入文本缓冲区. write(消息,索引);this.socket.write(缓冲区);};最后,我们需要实现一个功能,那就是心跳检测:防止客户端因为服务器长时间不与客户端交互而关闭连接,所以每十秒就会发送一次ping进行心跳检测。
//每10秒检测一次心跳。如果心跳连续发出三次,但没有收到响应,socketwebsocket . prototype . check heart=function(){ var that=this;setTimeout(函数(){ if (that.state!=='OPEN ')返回;if(that . Pingtimes=3){ that . close('超时');返回;}//记录心跳次数that . ping times;that . SendPing();that . check心跳();}, 10000);};WebSocket . prototype . send ping=function(){ this . socket . write(new Buffer(['0x 89 ','0x 0 ']))};WebSocket . prototype . send pong=function(){ this . socket . write(new Buffer(['0x8A ','0x 0 ']))};至此,websocket的实现已经完成。这个演示只是大致实现了websocket,但是在安全性等方面还存在很多问题。如果是真实的生产环境,最好使用socket.io等成熟的插件,但还是值得学习的。
以上内容是边肖共享的nodejs实现的Websocket数据接收和发送的全部内容。希望大家喜欢。