找回密码
 立即注册
首页 业界区 业界 细说WebSocket - Node篇

细说WebSocket - Node篇

晦险忿 2025-5-29 17:01:32
在上一篇提高到了 web 通信的各种方式,包括 轮询、长连接 以及各种 HTML5 中提到的手段。本文将详细描述 WebSocket协议 在 web通讯 中的实现。
一、WebSocket 协议

1. 概述

websocket协议允许不受信用的客户端代码在可控的网络环境中控制远程主机。该协议包含一个握手和一个基本消息分帧、分层通过TCP。简单点说,通过握手应答之后,建立安全的信息管道,这种方式明显优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协议包括两个方面,握手链接(handshake)和数据传输(data transfer)。
2. 握手连接

这部分比较简单,就像路上遇到熟人问好。
  1. Client:嘿,大哥,有火没?(烟递了过去)
  2. Server:哈,有啊,来~ (点上)
  3. Client:火柴啊,也行!(烟点上,验证完毕)
复制代码
握手连接中,client 先主动伸手:
  1. GET /chat HTTP/1.1
  2. Host: server.example.com
  3. Upgrade: websocket
  4. Connection: Upgrade
  5. Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  6. Origin: http://example.com
  7. Sec-WebSocket-Protocol: chat, superchat
  8. Sec-WebSocket-Version: 13
复制代码
客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。 Server 看到 Client 打招呼之后,悄悄地告诉 Client 他已经知道了,顺便也打个招呼。
  1. HTTP/1.1 101 Switching Protocols
  2. Upgrade: websocket
  3. Connection: Upgrade
  4. Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  5. Sec-WebSocket-Protocol: chat
复制代码
Server 返回了 Sec-WebSocket-Accept 这个应答,这个应答内容是通过一定的方式生成的。生成算法是:
  1. mask  = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // 这是算法中要用到的固定字符串
  2. accept = base64( sha1( key + mask ) );
复制代码
key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。分解动作:
  1. 1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  2.    -> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  3. 2. s = sha1(t)
  4.    -> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6
  5.       0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
  6. 3. base64(s)
  7.    -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
复制代码
上面 Server 端返回的 HTTP 状态码是 101,如果不是 101 ,那就说明握手一开始就失败了~
下面就来个 demo,跟服务器握个手:
  1. var crypto = require('crypto');
  2. require('net').createServer(function(o){
  3.     var key;
  4.     o.on('data',function(e){
  5.         if(!key){
  6.             // 握手
  7.             // 应答部分,代码先省略
  8.             console.log(e.toString());
  9.         }else{
  10.         };
  11.     });
  12. }).listen(8000);
复制代码
客户端代码:
  1. var ws=new WebSocket("ws://127.0.0.1:8000");
  2. ws.onerror=function(e){
  3.   console.log(e);
  4. };
复制代码
上面当然是一串不完整的代码,目的是演示握手过程中,客户端给服务端打招呼。在控制台我们可以看到:
1.jpeg

看起来很熟悉吧,其实就是发送了一个 HTTP 请求,这个我们在浏览器的 Network 中也可以看到:
2.jpeg

但是 WebSocket协议 并不是 HTTP 协议,刚开始验证的时候借用了 HTTP 的头,连接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包试试,肯定是拿不到的,后面的通信部分是基于 TCP 的连接。
服务器要成功的进行通信,必须有应答,往下看:
  1. //服务器程序
  2. var crypto = require('crypto');
  3. var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  4. require('net').createServer(function(o){
  5.     var key;
  6.     o.on('data',function(e){
  7.         if(!key){
  8.             //握手
  9.             key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
  10.             key = crypto.createHash('sha1').update(key + WS).digest('base64');
  11.             o.write('HTTP/1.1 101 Switching Protocols\r\n');
  12.             o.write('Upgrade: websocket\r\n');
  13.             o.write('Connection: Upgrade\r\n');
  14.             o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
  15.             o.write('\r\n');
  16.         }else{
  17.             console.log(e);
  18.         };
  19.     });
  20. }).listen(8000);
复制代码
关于crypto模块,可以看看官方文档,上面的代码应该是很好理解的,服务器应答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。
  1. //客户端程序
  2. var ws=new WebSocket("ws://127.0.0.1:8000/");
  3. ws.onopen=function(e){
  4.     console.log("握手成功");
  5. };
复制代码
可以看到
3.jpeg

3. 数据帧格式

官方文档提供了一个结构图
  1.   0                   1                   2                   3
  2.   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
  3. +-+-+-+-+-------+-+-------------+-------------------------------+
  4. |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
  5. |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
  6. |N|V|V|V|       |S|             |   (if payload len==126/127)   |
  7. | |1|2|3|       |K|             |                               |
  8. +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
  9. |     Extended payload length continued, if payload len == 127  |
  10. + - - - - - - - - - - - - - - - +-------------------------------+
  11. |                               |Masking-key, if MASK set to 1  |
  12. +-------------------------------+-------------------------------+
  13. | Masking-key (continued)       |          Payload Data         |
  14. +-------------------------------- - - - - - - - - - - - - - - - +
  15. :                     Payload Data continued ...                :
  16. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
  17. |                     Payload Data continued ...                |
  18. +---------------------------------------------------------------+
复制代码
第一眼瞟到这张图恐怕是要吐血,如果大学修改计算机网络这门课应该不会对这东西陌生,数据传输协议嘛,是需要定义字节长度及相关含义的。
  1. FIN      1bit 表示信息的最后一帧,flag,也就是标记符
  2. RSV 1-3  1bit each 以后备用的 默认都为 0
  3. Opcode   4bit 帧类型,稍后细说
  4. Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
  5. Payload  7bit 数据的长度
  6. Masking-key      1 or 4 bit 掩码
  7. Payload data     (x + y) bytes 数据
  8. Extension data   x bytes  扩展数据
  9. Application data y bytes  程序数据
复制代码
每一帧的传输都是遵从这个协议规则的,知道了这个协议,那么解析就不会太难了,下面我就直接拿了次碳酸钴同学的代码。
4. 数据帧的解析和编码

数据帧的解析代码:
4.gif
5.gif
[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++]
您需要登录后才可以回帖 登录 | 立即注册