WebSocket协议以及ws源码分析

¿

本文包括如下内容:

  • WebSocket协议第四章 - 连接握手
  • WebSocket协议第五章 - 数据帧
  • nodejs ws库源码分析 - 连接握手过程
  • nodejs ws库源码分析 - 数据帧解析过程

参考

WebSocket 协议深入探究

ws - github

本文对WebSocket的概念、定义、解释和用途等基础知识不会涉及, 稍微偏干一点, 篇幅较长, markdown大约800行, 阅读需要耐心

1. 连接握手过程

关于WebSocket有一句很常见的话: Websocket复用了HTTP的握手通道, 它具体指的是:

客户端通过HTTP请求与WebSocket服务器协商升级协议, 协议升级完成后, 后续的数据交换则遵照WebSocket协议

1.1 客户端: 申请协议升级

首先由客户端换发起协议升级请求, 根据WebSocket协议规范, 请求头必须包含如下的内容

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
复制代码
  • 请求行: 请求方法必须是GET, HTTP版本至少是1.1
  • 请求必须含有Host
  • 如果请求来自浏览器客户端, 必须包含Origin
  • 请求必须含有Connection, 其值必须含有"Upgrade"记号
  • 请求必须含有Upgrade, 其值必须含有"websocket"关键字
  • 请求必须含有Sec-Websocket-Version, 其值必须是13
  • 请求必须含有Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接

1.2 服务器: 响应协议升级

服务器返回的响应头必须包含如下的内容

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
复制代码
  • 响应行: HTTP/1.1 101 Switching Protocols
  • 响应必须含有Upgrade, 其值为"weboscket"
  • 响应必须含有Connection, 其值为"Upgrade"
  • 响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来

1.3 Sec-WebSocket-Key/Accept的计算

规范提到:

Sec-WebSocket-Key值由一个随机生成的16字节的随机数通过base64(见RFC4648的第四章)编码得到的

例如, 随机选择的16个字节为:

// 十六进制 数字1~16
0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10
复制代码

通过base64编码后值为: AQIDBAUGBwgJCgsMDQ4PEA==

测试代码如下:

const list = Array.from({ length: 16 }, (v, index) => ++index)
const key = Buffer.from(list)
console.log(key.toString('base64'))
// AQIDBAUGBwgJCgsMDQ4PEA==
复制代码

Sec-WebSocket-Accept值的计算方式为:

  1. Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
  2. 通过SHA1计算出摘要, 并转成base64字符串

此处不需要纠结神奇字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11, 它就是一个GUID, 没准儿是写RFC的时候随机生成的

测试代码如下:

const crypto = require('crypto')

function hashWebSocketKey (key) {
  const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

  return crypto.createHash('sha1')
    .update(key + GUID)
    .digest('base64')
}

console.log(hashWebSocketKey('w4v7O6xFTi36lq3RNcgctw=='))
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=
复制代码

1.4 Sec-WebSocket-Key的作用

前面简单提到他的作用为: 提供基础的防护, 减少恶意连接, 进一步阐述如下:

  • Key可以避免服务器收到非法的WebSocket连接, 比如http请求连接到websocket, 此时服务端可以直接拒绝
  • Key可以用来初步确保服务器认识ws协议, 但也不能排除有的http服务器只处理Sec-WebSocket-Key, 并不实现ws协议
  • Key可以避免反向代理缓存
  • 在浏览器中发起ajax请求, Sec-Websocket-Key以及相关header是被禁止的, 这样可以避免客户端发送ajax请求时, 意外请求协议升级

最终需要强调的是: Sec-WebSocket-Key/Accept并不是用来保证数据的安全性, 因为其计算/转换公式都是公开的, 而且非常简单, 最主要的作用是预防一些意外的情况

2. 数据帧

WebSocket通信的最小单位是帧, 由一个或多个帧组成一条完整的消息, 交换数据的过程中, 发送端和接收端需要做的事情如下:

  1. 发送端: 将消息切割成多个帧, 并发送给服务端
  2. 接收端: 接受消息帧, 并将关联的帧重新组装成完整的消息

数据帧格式作为核心内容, 一眼看去似乎难以理解, 但本文作者下死命令了, 必须理解, 冲冲冲

2.1 数据帧格式详解

  • FIN: 占1bit

    • 0表示不是消息的最后一个分片
    • 1表示是消息的最后一个分片
  • RSV1, RSV2, RSV3: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错

  • Opcode: 占4bit

    • %x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片
    • %x1: 表示这是一个文本帧
    • %x2: 表示这是一个二进制帧
    • %x3-7: 保留的操作代码, 用于后续定义的非控制帧
    • %x8: 表示连接断开
    • %x9: 表示这是一个心跳请求(ping)
    • %xA: 表示这是一个心跳响应(pong)
    • %xB-F: 保留的操作代码, 用于后续定义的非控制帧
  • Mask: 占1bit

    • 0表示不对数据载荷进行掩码异或操作
    • 1表示对数据载荷进行掩码异或操作
  • Payload length: 占7或7+16或7+64bit

    • 0~125: 数据长度等于该值
    • 126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度
    • 127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度

你可能感兴趣的:(网络,java,markdown)