在上一篇提高到了 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+xOo="
复制代码 上面 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. 数据帧的解析和编码
数据帧的解析代码:
[code]function decodeDataFrame(e){ var i=0,j,s,frame={ //解析前两个字节的基本数据 FIN:e>>7,Opcode:e[i++]&15,Mask:e>>7, PayloadLength:e[i++]&0x7F }; //处理特殊长度126和127 if(frame.PayloadLength==126) frame.PayloadLength=(e[i++] |