知道了怎么握手只是让客户端和服务器创建链接而已,WebSocket真正麻烦的地方是在数据的传输上!为了环保,它使用了特定格式的数据帧,这个数据帧须要本身去解析(固然也有别人编写好的库能够用)。虽然官方文档描述的很详细,可是看起来仍是蛋疼。
当客户端向服务器发送一个数据时服务器收到一个数据帧,好比下面的程序 //客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onopen=function(e){
ws.send("次碳酸钴"); //发送数据
};//服务器程序
这里是直接把接收到的数据输出了,获得这样一个东西
var crypto=require('crypto');
var WS='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){ //握手
key=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
key=crypto.createHash('sha1').update(key+WS).digest('base64');
o.write('HTTP/1.1 101 Switching Protocols\r\n');
o.write('Upgrade: websocket\r\n');
o.write('Connection: Upgrade\r\n');
o.write('Sec-WebSocket-Accept: '+key+'\r\n');
o.write('\r\n');
}else onmessage(e); //接收并交给处理函数
});
}).listen(8000);
function onmessage(e){
console.log(e); //把数据输出到控制台
};
这就是一个完整的数据帧,直接的16进制数据咱们固然没法直接阅读,须要按照数据帧的格式把它里面的数据取出来才行。对于这个数据帧,官方文档提供了一个结构图 0 1 2 3
光拿出这个实在很难看懂,顶部数字用十进制而不是八进制太让人蛋疼了。固然官方文档在后面的描述中也有详细介绍,看完后再回头来看图表才能看明白。其实WebSocket目前还不太完善,不少实验性的东西,因此彻底按照官方文档来理解是蛋疼的。这里就说我本身的理解。
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
如今再看左上角上面的图标,左上角的四个小列,也就是4位,第一位是FIN,后面三位是RSV1到3。官方文档上说RSV是预留的空间,正常为0,这就意味着,正常状况下他们能够当作0填充,那么前4位只有第一位的FIN须要设置,FIN表示帧结束,因为这篇中它不重要就不特别介绍了。接着后面的四位是储存opcode的值,这个opcode是标识数据类型的。这样数据的第一个字节咱们就能理解它的含义了,看上面16进制的数据的第一个字节81换成二进制是1000001,第一个1是FIN的值,最后一个1是opcode的值。
接着是第二个字节的数据,它由1位的MASK和7位的PayloadLen组成,MASK标识这个数据帧的数据是否使用掩码,PayloadLen表示数据部分的长度。可是PayloadLen只有7位,换成无符号整型的话只有0到127的取值,这么小的数值固然没法描述较大的数据,所以规定当数据长度小于或等于125时候它才做为数据长度的描述,若是这个值为126,则时候后面的两个字节来储存储存数据长度,若是为127则用后面四个字节来储存数据长度。因此上面的图片第一行的最右侧那块和第二行看起来有些颓然。从咱们的示例数据来看,第二个字节的8C中80是最高位为1,这意味着MASK为1,后面的C表示这个数据部分有12个字节。
再接着是上面图表中的MaskingKey,它占四个字节,储存掩码的实体部分。可是只有在前面的MASK被设置为1时候才存在这个数据,不然不使用掩码也就没有这个数据了。看咱们的示例数据,因为前面的MASK为1,因此3到6字节的“79 77 3d 41”是数据的掩码实体。
最后是数据部分,若是掩码存在,那么全部数据都须要与掩码作一次异或运算,四个字节的掩码与全部数据字节轮流发生性关系。若是不存在掩码,那么后面的数据就能够直接使用。
这样数据帧就解析完了。下面是我写的数据帧解析的程序,请不要吐槽代码没优化 function decodeDataFrame(e){
既然有了解析程序,那么咱们就能够把上面实例服务器端的onmessage方法修改一下
var i=0,j,s,frame={
//解析前两个字节的基本数据
FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
PayloadLength:e[i++]&0x7F
};
//处理特殊长度126和127
if(frame.PayloadLength==126)
frame.length=(e[i++]<<8)+e[i++];
if(frame.PayloadLength==127)
frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
//判断是否使用掩码
if(frame.Mask){
//获取掩码实体
frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
//对数据和掩码作异或运算
for(j=0,s=[];j<frame.PayloadLength;j++)
s.push(e[i+j]^frame.MaskingKey[j%4]);
}else s=e.slice(i,frame.PayloadLength); //不然直接使用数据
//数组转换成缓冲区来使用
s=new Buffer(s);
//若是有必要则把缓冲区转换成字符串来使用
if(frame.Opcode==1)s=s.toString();
//设置上数据部分
frame.PayloadData=s;
//返回数据帧
return frame;
};function onmessage(e){
e=decodeDataFrame(e); //解析数据帧
console.log(e); //把数据帧输出到控制台
};
这样服务器接收客户端穿过了的数据就没问题了。嘛,这篇文章就只说接收,至于从服务器发送到客户的状况会有更复杂的状况出现,咱下一篇再说。web