细说WebSocket - Node篇

在上一篇提高到了 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.comSec-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 0xea3. 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. 数据帧的解析和编码

数据帧的解析代码:

decodeDataFrame Function

数据帧的编码:

encodeDataFrame Function

有些童鞋可能没有明白,应该解析哪些数据。这的解析任务主要是服务端处理,客户端送过去的数据是二进制流形式的,比如:

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  |
-+--------+-------------------------------------+-----------|

decodeDataFrame 解析数据,得到的数据格式是:

{     FIN: 1,     Opcode: 1,     Mask: 1,     PayloadLength: 4,     MaskingKey: [ 159, 18, 207, 93 ],     PayLoadData: '握手成功' }

那么可以对应上面查看,此帧的作用就是发送文本,为文本帧。因为连接是基于 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连接,而且这个字段的加密方式在服务器也是固定的,如果别人想黑你,不会太难。

再就是那个 MaskingKey 掩码,既然强制加密了(Mask为1表示加密,加密方式就是 MaskingKey 与 PayLoadData 进行异或处理),还有必要让开发者处理这个东西么?直接封装到内部不就行了?

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

四、特别感谢

再次感谢 次碳酸钴 跟我交流了几个小时 : ),本文部分 node 代码参考自他的博客。

下次将以php作为后台,讲解websocket的相关知识。

原文发布于微信公众号 - php(phpdaily)

原文发表时间:2015-10-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数据小魔方

R语言爬虫实战——知乎live课程数据爬取实战

本文是一篇R语言爬虫实战练习篇,同样使用httr包来完成,结合cookies登录、表单提交、json数据包来完成整个数据爬取过程,无需书写复杂的xpath、cs...

3166
来自专栏月色的自留地

分享一个很通用c语言的Makefile

1484
来自专栏iOS 开发杂谈

iOS如何使用TestFlight进行App Beta版测试

TestFlight的测试方式分为两种: 一种是内部测试; 一种是外部测试。 从功能上讲,这两种方式都可以为测试人员在测试阶段对你的App进行测试。但是既...

632
来自专栏小樱的经验随笔

Kali Linux菜单中各工具功能大全

各工具kali官方简介(竖排):https://tools.kali.org/tools-listing 名称 类型 使用模式 功能 功能评价 dm...

5199
来自专栏Python

网络基础之网络协议篇

一、初识网络协议 网络协议是为计算机网络中进行数据交换而建立的规则、标准或者说是约定的集合。因为不同用户的数据终端可能采取的字符集是不同的,两者需要进行通信,必...

1669
来自专栏陈满iOS

iOS设备唯一标识符关于UDID代替方案:基于UUID和SSKeychain生成唯一标识符新方法

是一串由40位16进制数组成的字符串,用以标识唯一的设备,现在想通过代码获取是不可能的了,如果你想看看你设备的UDID,可以通过iTunes来查看。

672
来自专栏Python中文社区

使用python实现后台系统的JWT认证

專 欄 ❈ 茶客furu声,Python中文社区专栏作者 博客: http://www.jianshu.com/p/537b356d34c9 ❈ ...

3855
来自专栏Script Boy (CN-SIMO)

Qt入门之基础篇 ( 一 ) :Qt4及Qt5的下载与安装

转载请注明出处:CN_Simo. 导语: Qt是一个跨平台的C++图形界面应用程序框架。它提供给开发者建立图形用户界面所需的功能,广泛用于开发GUI程序,也可用...

3821
来自专栏blackheart的专栏

[认证授权] 2.OAuth2授权(续) & JWT(JSON Web Token)

1 RFC6749还有哪些可以完善的? 1.1 撤销Token 在上篇[认证授权] 1.OAuth2授权 中介绍到了OAuth2可以帮我们解决第三方Client...

2425
来自专栏日常学python

用Python实现模拟登录正方教务系统抢课

最近学校开始选课,但是如果选课时间与自己的事情冲突,这时候就可以使用Python脚本自助抢课,抢课的第一步即是模拟登录,需要模拟登录后保存登录信息然后再进行操作...

650

扫描关注云+社区