分享更有价值
被信任是一种快乐

如何用Node手写WebSocket协议

文章页正文上

今天小编给大家分享一下如何用Node手写WebSocket协议的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。 我们知道,http 是一问一答的模式,客户端向服务器发送 http 请求,服务器返回 http 响应。这种模式对资源、数据的加载足够用,但是需要数据推送的场景就不合适了。有同学说,http2 不是有 server push 么?那只是推资源用的:比如浏览器请求了 html,服务端可以连带把 css 一起推送给浏览器。浏览器可以决定接不接收。对于即时通讯等实时性要求高的场景,就需要用 websocket 了。websocket 严格来说和 http 没什么关系,是另外一种协议格式。但是需要一次从 http 到 websocekt 的切换过程。切换过程详细来说是这样的:请求的时候带上这几个 header:

Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Key:Ia3dQjfWrAug/6qm7mTZOg==

前两个很容易理解,就是升级到 websocket 协议的意思。第三个 header 是保证安全用的一个 key。服务端返回这样的 header:

HTTP/1.1101SwitchingProtocols
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Accept:JkE58n3uIigYDMvC+KsBbGZsp1A=

和请求 header 类似,Sec-WebSocket-Accept 是对请求带过来的 Sec-WebSocket-Key 处理之后的结果。加入这个 header 的校验是为了确定对方一定是有 WebSocket 能力的,不然万一建立了连接对方却一直没消息,那不就白等了么。那 Sec-WebSocket-Key 经过什么处理能得到 Sec-WebSocket-Accept 呢?我用 node 实现了一下,是这样的:

constcrypto=require('crypto');

functionhashKey(key){
constsha1=crypto.createHash('sha1');
sha1.update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
returnsha1.digest('base64');
}

也就是用客户端传过来的 key,加上一个固定的字符串,经过 sha1 加密之后,转成 base64 的结果。这个字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是固定的,不信你搜搜看:随便找个有 websocket 的网站,比如知乎就有:过滤出 ws 类型的请求,看看这几个 header,是不是就是前面说的那些。这个 Sec-WebSocket-Key 是 wk60yiym2FEwCAMVZE3FgQ==而响应的 Sec-WebSocket-Accept 是 XRfPnS+8xl11QWZherej/dkHPHM=我们算算看:是不是一毛一样!这就是 websocket 升级协议时候的 Sec-WebSocket-Key 对应的 Sec-WebSocket-Accept 的计算过程。这一步之后就换到 websocket 的协议了,那是一个全新的协议:勾选 message 这一栏可以看到传输的消息,可以是文本、可以是二进制:全新的协议?那具体是什么样的协议呢?这样的:大家习惯的 http 协议是 key:value 的 header 带个 body 的:它是文本协议,每个 header 都是容易理解的字符。这样好懂是好懂,但是传输占的空间太大了。而 websocket 是二进制协议,一个字节可以用来存储很多信息:比如协议的第一个字节,就存储了 FIN(结束标志)、opcode(内容类型是 binary 还是 text) 等信息。第二个字节存储了 mask(是否有加密),payload(数据长度)。仅仅两个字节,存储了多少信息呀!这就是二进制协议比文本协议好的地方。我们看到的 weboscket 的 message 的收发,其实底层都是拼成这样的格式。只是浏览器帮我们解析了这种格式的协议数据。这就是 weboscket 的全部流程了。其实还是挺清晰的,一个切换协议的过程,然后是二进制的 weboscket 协议的收发。那我们就用 Node.js 自己实现一个 websocket 服务器吧!定义个 MyWebsocket 的 class:

const{EventEmitter}=require('events');
consthttp=require('http');

classMyWebsocketextendsEventEmitter{
constructor(options){
super(options);

constserver=http.createServer();
server.listen(options.port||8080);

server.on('upgrade',(req,socket)=>{

});
}
}

继承 EventEmitter 是为了可以用 emit 发送一些事件,外界可以通过 on 监听这个事件来处理。我们在构造函数里创建了一个 http 服务,当 ungrade 事件发生,也就是收到了 Connection: upgrade 的 header 的时候,返回切换协议的 header。返回的 header 前面已经见过了,就是要对 sec-websocket-key 做下处理。

server.on('upgrade',(req,socket)=>{
this.socket=socket;
socket.setKeepAlive(true);

constresHeaders=[
'HTTP/1.1101SwitchingProtocols',
'Upgrade:websocket',
'Connection:Upgrade',
'Sec-WebSocket-Accept:'+hashKey(req.headers['sec-websocket-key']),
'',
''
].join('rn');
socket.write(resHeaders);

socket.on('data',(data)=>{
console.log(data)
});
socket.on('close',(error)=>{
this.emit('close');
});
});

我们拿到 socket,返回上面的 header,其中 key 做的处理就是前面聊过的算法:

functionhashKey(key){
constsha1=crypto.createHash('sha1');
sha1.update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
returnsha1.digest('base64');
}

就这么简单,就已经完成协议切换了。不信我们试试看。引入我们实现的 ws 服务器,跑起来:

constMyWebSocket=require('./ws');
constws=newMyWebSocket({port:8080});

ws.on('data',(data)=>{
console.log('receivedata:'+data);
});

ws.on('close',(code,reason)=>{
console.log('close:',code,reason);
});

然后新建这样一个 html:

HTML>


用浏览器的 WebSocket api 建立连接,发送消息。用 npx http-server . 起个静态服务。然后浏览器访问这个 html:这时打开 devtools 你就会发现协议切换成功了:这 3 个 header 还有 101 状态码都是我们返回的。message 里也可以看到发送的消息:再去服务端看看,也收到了这个消息:只不过是 Buffer 的,也就是二进制的。接下来只要按照协议格式解析这个 Buffer,并且生成响应格式的协议数据 Buffer 返回就可以收发 websocket 数据了。这一部分还是比较麻烦的,我们一点点来看。我们需要第一个字节的后四位,也就是 opcode。这样写:

constbyte1=bufferData.readUInt8(0);
letopcode=byte1&0x0f;

读取 8 位无符号整数的内容,也就是一个字节的内容。参数是偏移的字节,这里是 0。通过位运算取出后四位,这就是 opcode 了。然后再处理第二个字节:第一位是 mask 标志位,后 7 位是 payload 长度。可以这样取:

constbyte2=bufferData.readUInt8(1);
conststr2=byte2.toString(2);
constMASK=str2[0];
letpayloadLength=parseInt(str2.substring(1),2);

还是用 buffer.readUInt8 读取一个字节的内容。先转成二进制字符串,这时第一位就是 mask,然后再截取后 7 位的子串,parseInt 成数字,这就是 payload 长度了。这样前两个字节的协议内容就解析完了。有同学可能问了,后面咋还有俩 payload 长度呢?这是因为数据不一定有多长,可能需要 16 位存长度,可能需要 32 位。于是 websocket 协议就规定了如果那个 7 位的内容不超过 125,那它就是 payload 长度。如果 7 位的内容是 126,那就不用它了,用后面的 16 位的内容作为 payload 长度。如果 7 位的内容是 127,也不用它了,用后面那个 64 位的内容作为 payload 长度。其实还是容易理解的,就是 3 个 if else。用代码写出来就是这样的:

letpayloadLength=parseInt(str2.substring(1),2);

letcurByteIndex=2;

if(payloadLength===126){
payloadLength=bufferData.readUInt16BE(2);
curByteIndex+=2;
}elseif(payloadLength===127){
payloadLength=bufferData.readBigUInt64BE(2);
curByteIndex+=8;
}

这里的 curByteIndex 是存储当前处理到第几个字节的。如果是 126,那就从第 3 个字节开始,读取 2 个字节也就是 16 位的长度,用 buffer.readUInt16BE 方法。如果是 127,那就从第 3 个字节开始,读取 8 个字节也就是 64 位的长度,用 buffer.readBigUInt64BE 方法。这样就拿到了 payload 的长度,然后再用这个长度去截取内容就好了。但在读取数据之前,还有个 mask 要处理,这个是用来给内容解密的:读 4 个字节,就是 mask key。再后面的就可以根据 payload 长度读出来。

letrealData=null;

if(MASK){
constmaskKey=bufferData.slice(curByteIndex,curByteIndex+4);
curByteIndex+=4;
constpayloadData=bufferData.slice(curByteIndex,curByteIndex+payloadLength);
realData=handleMask(maskKey,payloadData);
}else{
realData=bufferData.slice(curByteIndex,curByteIndex+payloadLength);;
}

然后用 mask key 来解密数据。这个算法也是固定的,用每个字节的 mask key 和数据的每一位做按位异或就好了:

functionhandleMask(maskBytes,data){
constpayload=Buffer.alloc(data.length);
for(leti=0;i

这样,我们就拿到了最终的数据!但是传给处理程序之前,还要根据类型来处理下,因为内容分几种类型,也就是 opcode 有几种值:

constOPCODES={
CONTINUE:0,
TEXT:1,//文本
BINARY:2,//二进制
CLOSE:8,
PING:9,
PONG:10,
};

我们只处理文本和二进制就好了:

handleRealData(opcode,realDataBuffer){
switch(opcode){
caseOPCODES.TEXT:
this.emit('data',realDataBuffer.toString('utf8'));
break;
caseOPCODES.BINARY:
this.emit('data',realDataBuffer);
break;
default:
this.emit('close');
break;
}
}

文本就转成 utf-8 的字符串,二进制数据就直接用 buffer 的数据。这样,处理程序里就能拿到解析后的数据。我们来试一下:之前我们已经能拿到 weboscket 协议内容的 buffer 了:而现在我们能正确解析出其中的数据:至此,我们 websocket 协议的解析成功了!这样的协议格式的数据叫做 frame,也就是帧:解析可以了,接下来我们再实现数据的发送。发送也是构造一样的 frame 格式。定义这样一个 send 方法:

send(data){
letopcode;
letbuffer;
if(Buffer.isBuffer(data)){
opcode=OPCODES.BINARY;
buffer=data;
}elseif(typeofdata==='string'){
opcode=OPCODES.TEXT;
buffer=Buffer.from(data,'utf8');
}else{
console.error('暂不支持发送的数据类型')
}
this.doSend(opcode,buffer);
}

doSend(opcode,bufferDatafer){
this.socket.write(encodeMessage(opcode,bufferDatafer));
}

根据发送的是文本还是二进制数据来对内容作处理。然后构造 webso免费云主机、域名cket 的 frame:

functionencodeMessage(opcode,payload){
//payload.length

我们只处理数据长度小于 125 的情况。第一个字节是 opcode,我们把第一位置 1 ,通过按位或的方式。服务端给客户端回消息不需要 mask,所以第二个字节就是 payload 长度。分别把这前两个字节的数据写到 buffer 里,指定不同的 offset:

bufferData.writeUInt8(byte1,0);
bufferData.writeUInt8(byte2,1);

之后把 payload 数据放在后面:

payload.copy(bufferData,2);

这样一个 websocket 的 frame 就构造完了。我们试一下:收到客户端消息后,每两秒回一个消息。收发消息都成功了!就这样,我们自己实现了一个 websocket 服务器,实现了 websocket 协议的解析和生成!完整代码如下:MyWebSocket:

//ws.js
const{EventEmitter}=require('events');
consthttp=require('http');
constcrypto=require('crypto');

functionhashKey(key){
constsha1=crypto.createHash('sha1');
sha1.update(key+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
returnsha1.digest('base64');
}

functionhandleMask(maskBytes,data){
constpayload=Buffer.alloc(data.length);
for(leti=0;i{
this.socket=socket;
socket.setKeepAlive(true);

constresHeaders=[
'HTTP/1.1101SwitchingProtocols',
'Upgrade:websocket',
'Connection:Upgrade',
'Sec-WebSocket-Accept:'+hashKey(req.headers['sec-websocket-key']),
'',
''
].join('rn');
socket.write(resHeaders);

socket.on('data',(data)=>{
this.processData(data);
//console.log(data);
});
socket.on('close',(error)=>{
this.emit('close');
});
});
}

handleRealData(opcode,realDataBuffer){
switch(opcode){
caseOPCODES.TEXT:
this.emit('data',realDataBuffer.toString('utf8'));
break;
caseOPCODES.BINARY:
this.emit('data',realDataBuffer);
break;
default:
this.emit('close');
break;
}
}

processData(bufferData){
constbyte1=bufferData.readUInt8(0);
letopcode=byte1&0x0f;

constbyte2=bufferData.readUInt8(1);
conststr2=byte2.toString(2);
constMASK=str2[0];

letcurByteIndex=2;

letpayloadLength=parseInt(str2.substring(1),2);
if(payloadLength===126){
payloadLength=bufferData.readUInt16BE(2);
curByteIndex+=2;
}elseif(payloadLength===127){
payloadLength=bufferData.readBigUInt64BE(2);
curByteIndex+=8;
}

letrealData=null;

if(MASK){
constmaskKey=bufferData.slice(curByteIndex,curByteIndex+4);
curByteIndex+=4;
constpayloadData=bufferData.slice(curByteIndex,curByteIndex+payloadLength);
realData=handleMask(maskKey,payloadData);
}

this.handleRealData(opcode,realData);
}

send(data){
letopcode;
letbuffer;
if(Buffer.isBuffer(data)){
opcode=OPCODES.BINARY;
buffer=data;
}elseif(typeofdata==='string'){
opcode=OPCODES.TEXT;
buffer=Buffer.from(data,'utf8');
}else{
console.error('暂不支持发送的数据类型')
}
this.doSend(opcode,buffer);
}

doSend(opcode,bufferDatafer){
this.socket.write(encodeMessage(opcode,bufferDatafer));
}
}

module.exports=MyWebsocket;

Index:

constMyWebSocket=require('./ws');
constws=newMyWebSocket({port:8080});

ws.on('data',(data)=>{
console.log('receivedata:'+data);
setInterval(()=>{
ws.send(data+''+Date.now());
},2000)
});

ws.on('close',(code,reason)=>{
console.log('close:',code,reason);
});

html:

HTML>


以上就是“如何用Node手写WebSocket协议”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注云技术行业资讯频道。

相关推荐: class类是不是es6语法

本文小编为大家详细介绍“class类是不是es6语法”,内容详细,步骤清晰,细节处理妥当,希望这篇“class类是不是es6语法”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。 class类是es6语法,是es6新增的一种特性。ES6…

文章页内容下
赞(0) 打赏
版权声明:本站采用知识共享、学习交流,不允许用于商业用途;文章由发布者自行承担一切责任,与本站无关。
文章页正文下
文章页评论上

云服务器、web空间可免费试用

宝塔面板主机、支持php,mysql等,SSL部署;安全高速企业专供99.999%稳定,另有高防主机、不限制内容等类型,具体可咨询QQ:360163164,Tel同微信:18905205712

主机选购导航云服务器试用

登录

找回密码

注册