本文同步自我的博客园:http://hustskyking.cnblogs.com
P.S:文章代码花样紊乱,也不知道是什么缘由,还望@segmentFault的兄弟看下~
在上一篇进步到了 web 通信的种种体式格局,包括 轮询、长衔接 以及种种 HTML5 中提到的手腕。本文将细致描述 WebSocket协定 在 web通信 中的完成。
一、WebSocket 协定
1. 概述
websocket协定许可不受信用的客户端代码在可控的收集环境中掌握长途主机。该协定包括一个握手和一个基础音讯分帧、分层经由过程TCP。简单点说,经由过程握手应对今后,竖立平安的信息管道,这类体式格局显著优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协定包括两个方面,握手链接(handshake)和数据传输(data transfer)。
2. 握手衔接
这部份比较简单,就像路上碰到熟人问好。
Client:嘿,老大,有火没?(烟递了过去)
Server:哈,有啊,来~ (点上)
Client:火柴啊,也行!(烟点上,考证终了)
握手衔接中,client 先主动伸手:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。
Server 看到 Client 打召唤今后,悄悄地通知 Client 他已知道了,趁便也打个召唤。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Server 返回了 Sec-WebSocket-Accept 这个应对,这个应对内容是经由过程肯定的体式格局天生的。天生算法是:
mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 这是算法中要用到的牢固字符串
accept = base64( sha1( key + mask ) );
key 和 mask 串接今后经由 SHA-1 处置惩罚,处置惩罚后的数据再经由一次 Base64 加密。剖析行动:
1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
-> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2. s = sha1(t)
-> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6
0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
3. base64(s)
-> "s3pPLMBiTxaQ9kYGzzhZRbK"
上面 Server 端返回的 HTTP 状况码是 101,假如不是 101 ,那就申明握手一开始就失利了~
下面就来个 demo,跟服务器握个手:
var crypto = require('crypto');
require('net').createServer(function(o){
var key;
o.on('data',function(e){
if(!key){
// 握手
// 应对部份,代码先省略
console.log(e.toString());
}else{
};
});
}).listen(8000);
客户端代码:
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
console.log(e);
};
上面当然是一串不完整的代码,目标是演示握手过程当中,客户端给服务端打召唤。在掌握台我们能够看到:
看起来很熟悉吧,实在就是发送了一个 HTTP 要求,这个我们在浏览器的 Network 中也能够看到:
然则 WebSocket协定 并非 HTTP 协定,刚开始考证的时刻借用了 HTTP 的头,衔接胜利今后的通信就不是 HTTP 了,不信你用 fiddler2 抓包尝尝,肯定是拿不到的,背面的通信部份是基于 TCP 的衔接。
服务器要胜利的举行通信,必需有应对,往下看:
//服务器顺序
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{
console.log(e);
};
});
}).listen(8000);
关于crypto模块,能够看看官方文档,上面的代码应当是很好邃晓的,服务器应对今后,Client 拿到 Sec-WebSocket-Accept ,然后当地做一次考证,假如考证经由过程了,就会触发 onopen 函数。
//客户端顺序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onopen=function(e){
console.log("握手胜利");
};
3. 数据帧花样
官方文档供应了一个结构图
0 1 2 3
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 ... |
+---------------------------------------------------------------+
第一眼瞟到这张图恐怕是要吐血,假如大学修正计算机收集这门课应当不会对这东西生疏,数据传输协定嘛,是须要定义字节长度及相干寄义的。
FIN 1bit 示意信息的末了一帧,flag,也就是标记符
RSV 1-3 1bit each 今后备用的 默许都为 0
Opcode 4bit 帧范例,稍后细说
Mask 1bit 掩码,是不是加密数据,默许必需置为1 (这里很蛋疼)
Payload 7bit 数据的长度
Masking-key 1 or 4 bit 掩码
Payload data (x + y) bytes 数据
Extension data x bytes 扩大数据
Application data y bytes 顺序数据
每一帧的传输都是顺从这个协定划定规矩的,知道了这个协定,那末剖析就不会太难了,这里我就直接拿了次碳酸钴同砚的代码。
4. 数据帧的剖析和编码
数据帧的剖析代码:
function decodeDataFrame(e){
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)
i+=4, //长度平常用四字节的整型,前四个字节一般为长整形留空的
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;
}
数据帧的编码:
//NodeJS
function encodeDataFrame(e){
var s=[],o=new Buffer(e.PayloadData),l=o.length;
//输入第一个字节
s.push((e.FIN<<7)+e.Opcode);
//输入第二个字节,推断它的长度并放入响应的后续长度音讯
//永久不运用掩码
if(l<126) s.push(l);
else if(l<0x10000) s.push(126,(l&0xFF00)>>2,l&0xFF);
else s.push(
127, 0,0,0,0, //8字节数据,前4字节平常没用留空
(l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF
);
//返回头部份和数据部份的兼并缓冲区
return Buffer.concat([new Buffer(s),o]);
}
有些童鞋能够没有邃晓,应当剖析哪些数据。这的剖析使命主如果服务端处置惩罚,客户端送过去的数据是二进制流情势的,比方:
var ws = new WebSocket("ws://127.0.0.1:8000/");
ws.onopen = function(){
ws.send("握手胜利");
};
Server 收到的信息是如许的:
一个放在Buffer花样的二进制流。而当我们输出的时刻剖析这个二进制流:
//服务器顺序
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{
// 输出之前剖析帧
console.log(decodeDataFrame(e));
};
});
}).listen(8000);
5. 衔接的掌握
上面我买了个关子,提到的Opcode,没有细致申明,官方文档也给了一张表:
|Opcode | Meaning | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
次碳酸钴给出的剖析函数,获得的数据花样是:
{
FIN: 1,
Opcode: 1,
Mask: 1,
PayloadLength: 4,
MaskingKey: [ 159, 18, 207, 93 ],
PayLoadData: 'test'
}
那末能够对应上面检察,此帧的作用就是发送文本,为文本帧。由于衔接是基于 TCP 的,直接封闭 TCP 衔接,这个通道就封闭了,不过 WebSocket 设想的还比较人性化,封闭之前还跟你打一声召唤,在服务器端,能够推断frame的Opcode:
var frame=decodeDataFrame(e);
console.log(frame);
if(frame.Opcode==8){
o.end(); //断开衔接
}
客户端和服务端交互的数据(帧)花样都是一样的,只需客户端发送 ws.close()
, 服务器就会实行上面的操纵。相反,假如服务器给客户端也发送一样的封闭帧(close frame):
o.write(encodeDataFrame({
FIN:1,
Opcode:8,
PayloadData:buf
}));
客户端就会响应 onclose 函数,如许的交互还算是有规有矩,不容易失足。
二、注意事项
1. WebSocket URIs
很多人能够只是到 ws://text.com:8888
,但事实上 websocket 协定地点是能够加 path 和 query 的。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
假如运用的是 wss 协定,那末 URI 将会以平安体式格局衔接。 这里的 wss 大小写不敏感。
2. 协定中过剩的部份(吐槽)
握手要求中包括Sec-WebSocket-Key字段,明眼人一下就可以看出来是websocket衔接,而且这个字段的加密体式格局在服务器也是牢固的,假如他人想黑你,不会太难。
再就是谁人mask掩码,既然强迫加密了,另有必要让开发者处置惩罚这个东西么?直接封装到内部不就行了?
3. 与 TCP 和 HTTP 之间的关联
WebSocket协定是一个基于TCP的协定,就是握手链接的时刻跟HTTP相干(发了一个HTTP要求),这个要求被Server切换到(Upgrade)websocket协定了。websocket把 80 端口作为默许websocket衔接端口,而websocket的运转运用的是443端口。
三、参考资料
- http://tools.ietf.org/html/rfc6455 web standard – The WebSocket Protocol
- http://www.w3.org/TR/websockets/ W3.ORG – WebSockets