JS实时通信三把斧系列之一: websocket

本文同步发表在豆米的博客:豆米的博客

前言

前端的学习路线永远不会缺少实时通信这个领域,为了给自己填充这块知识.顺便可以完成公司的长连接项目,我开始学习系统学习这块领域的知识.整个系列按照实时通信的实现方式来学习,目前能够应用在实际项目中有如下三种方式:

  1. websocket: JS实时通信三把斧系列之一: websocket
  2. socket.io: JS实时通信三把斧系列之一: socket.io
  3. EventSource: JS实时通信三把斧系列之一: eventSource

今天第一篇文章便是介绍websocket以及对应的简单应用,整个系列文章对应的demo代码在这里: 传送门

1. websocket协议简单介绍

在这里不打算详细介绍整个协议的内容.根据以前协议的学习思路,我挑重点使用问答方式来介绍该协议.

1.1. 协议运行在OSI的哪层?

应用层,WebSocket协议是一个独立的基于TCP的协议。 它与HTTP唯一的关系是它的握手是由HTTP服务器解释为一个Upgrade请求

1.2. 协议运行的标准端口号是多少?

默认情况下,WebSocket协议使用端口80用于常规的WebSocket连接和端口443用于WebSocket连接的在传输层安全(TLS)RFC2818之上的隧道化口

1.3. 协议是如何工作的?

协议的工作流程可以参考下图:

其中帧的一些重要字段需要解释一下:

1. Upgrade:`upgrade`是HTTP1.1中用于定义转换协议的`header`域。它表示,如果服务器支持的话,客户端希望使用现有的「网络层」已经建立好的这个「连接(此处是 TCP 连接)」,切换到另外一个「应用层」(此处是 WebSocket)协议.
2. Connection:`Upgrade`固定字段。Connection还有其他字段,可以自己给自己科普一下
3. Sec-WebSocket-Key:用来发送给服务器使用(服务器会使用此字段组装成另一个key值放在握手返回信息里发送客户端)
4. Sec-WebSocket-Protocol:标识了客户端支持的子协议的列表
5. Sec-WebSocket-Version:标识了客户端支持的WS协议的版本列表,如果服务器不支持这个版本,必须回应自己支持的版本
6. Origin:作安全使用,防止跨站攻击,浏览器一般会使用这个来标识原始域。
7. Sec-WebSocket-Accept:服务器响应,包含Sec-WebSocket-Key 的签名值,证明它支持请求的协议版本

关于Sec-WebSocket-Key和Sec-WebSocket-Accept的计算是这样的:

所有兼容RFC 6455 的WebSocket 服务器都使用相同的算法计算客户端挑战的答案:将Sec-WebSocket-Key 的内容与标准定义的唯一GUID字符(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)串拼接起来,计算出SHA1散列值,结果是一个base-64编码的字符串,把这个字符串发给客户端即可

用代码就是实现如下:

const key = crypto.createHash('sha1')
      .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
      .digest('base64')

至于为什么需要这么一个步骤,可以参考理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性,引用如下:

Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基础的防护,减少恶意连接、意外连接。

作用大致归纳如下:

1)避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接);
2)确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。);
3)用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其他相关的header是被禁止的。这样可以避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade);
4)可以防止反向代理(不理解ws协议)返回错误的数据。比如反向代理前后收到两次ws连接的升级请求,反向代理把第一次请求的返回给cache住,然后第二次请求到来时直接把cache住的请求给返回(无意义的返回);
5)Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。

强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并没有实际性的保证

1.4. 协议传输的帧格式是什么?

帧格式定义的格式如下:

各个字段的解释如下:

  1. FIN: 1bit,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断

  2. RSV1,RSV2,RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉WebSocket连接。在ws中就用到了RSV1来表示是否消息压缩了的。

  3. opcode:4 bit。表示被传输帧的类型:

    • %x0 表示连续消息片断
    • %x1 表示文本消息片断
    • %x2 表未二进制消息片断
    • %x3-7 为将来的非控制消息片断保留的操作码
    • %x8 表示连接关闭
    • %x9 表示心跳检查的ping
    • %xA 表示心跳检查的pong
    • %xB-F 为将来的控制消息片断的保留操作码
  4. Mask: 1 bit。定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1

  5. Payload length:传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度

  6. Masking-key:0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在

  7. Extension data: x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内

  8. Application data: y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度

  9. Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和

更多细节请参考数据帧

针对上面的各个字段的介绍,有一个Mask的需要说一下:

掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

首先,假设:

original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。

算法描述为:
original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

即:
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

用代码实现:

  const mask = (source, mask, output, offset, length) => {
    for (var i = 0; i < length; i++) {
      output[offset + i] = source[i] ^ mask[i & 3];
    }
  };

解掩码是反过来的操作:

  const unmask = (buffer, mask) => {
    // Required until https://github.com/nodejs/node/issues/9006 is resolved.
    const length = buffer.length;
    for (var i = 0; i < length; i++) {
      buffer[i] ^= mask[i & 3];
    }
  };

同样的为什么需要掩码操作,也可以参考之前的那篇文章:理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性,完整的我就不列举了,重点是:

WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。

那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。

答案还是两个字: 安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题

2. websocket在nodejs端的实现

我们选取star数最多的websocket实现:websockets/ws来看看代码的大致实现流程:

整个代码的实现提供了四个实例:

  1. websocket
  2. websocket-server
  3. receiver
  4. sender

根据四个实例,以及协议的实现,我花了一些时间画了下面的实现流程图(除了sender实例):

从我们开始调用new WebSocket.Server({ server })之后:

  1. [webscket.Server实例]
  1. [websocket.Websocket实例]
image
  1. [websocket.Receiver实例]

因为这个Receiver实例实在太大,所以截成两张图片:

image

第二张图片是:

image

Note: 针对上面的流程图有几个点需要说下

  1. nodejs提供了upgrade事件: https://nodejs.org/api/http.html#http_event_upgrade
  2. websocket.Server实例会在收到ws报文之后实例化一个websocket实例,从而跳到第二张图片
  3. websocket.Websocket实例会在建立的socket上监听到data事件之后跳到第三张和第四张图片也就是Receiver实例,并在Receiver实例中处理掉数据

3. websocket在nodejs端的应用

demo代码如下: ws.js

我们在命令行中执行:

node ws.js

之后打开wireshark,可以看到浏览器和服务端的通信过程

  1. 客户端发送握手请求
image
  1. 服务端响应握手成功
image
  1. chrome浏览器显示的请求
image
  1. 客户端发送数据,不带mask
image
  1. 客户端发送数据,带mask
image
  1. 客户端关闭连接-不带mask
image
  1. 客户端关闭连接-mask
image

Note: chrome实现的原生websocket客户端不支持发送ping/pong包来维持心跳

兼容性

下一篇文章: JS实时通信三把斧系列之一: socket.io

参考

  1. https://chenjianlong.gitbooks.io/rfc-6455-websocket-protocol-in-chinese/content/section7/section7.html
  2. http://tools.ietf.org/html/rfc6455
  3. https://github.com/websockets/ws/blob/master/doc/ws.md
  4. https://juejin.im/entry/5a5c559c518825734859ee5e
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • 一、内容概览 WebSocket的出现,使得浏览器具备了实时双向通信的能力。本文由浅入深,介绍了WebSocket...
    Calvin李阅读 2,522评论 2 10
  • 1 述 WebSocket是一种网络通信协议WebSocket 协议在2008年诞生,2011年成为国际标准。HT...
    凯玲之恋阅读 680评论 0 0
  • 我说去喜马拉雅南麓看日出 你说买车还差三万三 我说卖了房子开家花店好不好 你说千山万水总需要一个家 我说夏天海边傍...
    董哈哈来了阅读 483评论 0 2
  • “要么出众,要么出局”,上大学以来这句话一直是我的座右铭,我坚信这句话,每当我迷茫的时候这句话总可以给我带来一股力...
    晨儿oo阅读 240评论 0 0
  • 本期编辑:冷眼观史 江湖头条:自在飞花轻似梦 作者:落花倾雪 早春时节的天气,变幻无常,李花林里,花香四溢。寒风袭...
    冷眼观史阅读 1,437评论 0 28