websocket 调研

websocket 介绍

背景

在 Websocket 诞生之前,服务器的数据需要传达至客户端,通常需要由客户端轮询或长轮询(Long-Polling)的方式获取。

WebSocket 协议主要为了解决基于 HTTP/1.x 的 Web 应用无法实现服务端向客户端主动推送的问题, 为了兼容现有的设施, WebSocket 协议使用与 HTTP 协议相同的端口, 并使用 HTTP Upgrade 机制来进行 WebSocket 握手, 当握手完成之后, 通信双方便可以按照 WebSocket 协议的方式进行交互。

简介

WebSocket 使用 TCP 作为传输层协议, WebSocket 使得客户端和服务器之间保持长连接,通过 Websocket 握手建立 Websocket 连接,通过 Websocket 挥手 关闭 Websocket 连接,可采用 ping-pong 保活,使用 帧 传输数据。

在 WebSocket 协议中, 帧 (frame) 是通信双方数据传输的基本单元, 与其它网络协议相同, frame 由 Header 和 Payload 两部分构成, frame 有多种类型, frame 的类型由其头部的 Opcode 字段来指示。

WebSocket 的 frame 可以分为两类:

  1. 用于传输控制信息的 frame (如通知对方关闭 WebSocket 连接),
  2. 用于传输应用数据的 frame, 使用 WebSocket 协议通信的双方都需要首先进行握手,

注意:只有当握手成功之后才开始使用 frame 传输数据

WebSocket 支持在 TCP 上层引入 TLS,ws 和 wss 的关系就类似 http 和 https 的关系。

websocket 握手

客户端发起:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Key:必选项, 由客户端随机生成的 16 字节值, 然后做 base64 编码
  • Sec-WebSocket-Version : 必选项,表示 WebSocket 协议的版本
  • Sec-WebSocket-Protocol :可选项,通信的子协议
  • Sec-WebSocket-Extensions :可选项,扩展协议

服务器响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat

服务器若支持 WebSocket 协议, 并同意与客户端握手, 则应返回 101 的 HTTP 状态码, 表示同意协议升级, 同时设置 Upgrade 字段的值为 websocket, 并将 Connection 字段的值设置为 Upgrade 。

  • Sec-WebSocket-Accept:经过服务器确认,并且加密转换(签名)的 Sec-WebSocket-Key;

Sec-WebSocket-Accept 的计算方法:

  • 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  • 通过 SHA1 计算出摘要,并转成 base64 字符串。

客户端对服务器响应的校验:

  1. 检查服务端返回的状态码是否为 101
  2. 检查服务端返回的响应是否包含 Upgrade 字段
  3. 检查 Upgrade 字段的值是否为 websocket(大小写不敏感)
  4. 校验服务端返回的 Sec-WebSocket-Accept 字段的值是否合法(注:采用相同的Sec-WebSocket-Accept 的计算方法,确保服务器返回的 Sec-WebSocket-Accept 和 客户端本地生成的 Sec-WebSocket-Accept 一致)
  5. 若 Sec-WebSocket-Protocol 存在,则校验服务端返回的 Header 中包含的 Sec-WebSocket-Protocol 中的值是否属于客户端发起的 Sec-WebSocket-Protocol 的值列表中的值
  6. 若 Sec-WebSocket-Extensions 存在,则校验服务端返回的 Header 中包含的 Sec-WebSocket-Extensions 中的值是否属于客户端发起的 Sec-WebSocket-Extensions 的值列表中的值

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

websocket 帧

      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, 长度为 1 比特, 该标志位用于指示当前的 frame 是消息的最后一个分段。( WebSocket 支持将长消息切分为若干个 frame 发送, 长消息的最后一个 frame 的 FIN 字段为 1,其他 frame 的 FIN 字段都为 0)
  • RSV 1 ~ 3, 这三个字段为保留字段, 每个字段的长度为 1 比特, 只有在 WebSocket 扩展时用, 若不启用扩展, 则该三个字段应置为 1, 若接收方收到 RSV 1 ~ 3 不全为 0 的 frame, 并且双方没有协商使用 WebSocket 协议扩展, 则接收方应立即终止 WebSocket 连接
  • Opcode, 长度为 4 比特, 该字段将指示 frame 的类型, RFC 6455 定义的 Opcode 共有如下几种:
    • 0x0, continuation frame (延续帧,text frame或binary frame后接一个或多个continuation frame,需要组合起来才完整)
    • 0x1, text frame (文本帧)
    • 0x2, binary frame (二进制帧)
    • 0x3 ~ 7, 目前保留, 以后将用作更多的非控制类 frame
    • 0x8, close frame, 用于关闭 WebSocket 连接
    • 0x9, ping frame (心跳保活帧)
    • 0xA, pong frame (心跳保活应答帧)
    • 0xB ~ F, 目前保留, 以后将用作更多的控制类 frame

continuation frame 举例:在传递数据时, 会先收到一个 binary frame, 它的 FIN 是0, 后续的数据会以 continuation frame 的形式发送, 直到最后一个frame的 FIN位是 1 的 continuation frame 结束, 中间不会穿插其它的 frame 。同理, text frame 也是如此实现。

  • Mask, 长度为 1 比特, 该字段是一个标志位, 用于指示 frame 的数据 (Payload) 是否使用掩码掩盖, RFC 6455 规定当且仅当由客户端向服务端发送的 frame, 需要使用掩码覆盖, 掩码覆盖主要为了解决代理缓存污染攻击 (详见 RFC 6455 Section 10.3)
  • Payload Len, 以字节为单位指示 frame Payload 的长度, 该字段的长度为 7 比特 或 7 + 16 比特 或 7 + 64 比特。
    • 当 Payload 的实际长度在 [0, 125] 时, 则 Payload Len 字段的长度为 7 比特, 它的值直接代表了 Payload 的实际长度;
    • 当 Payload 的实际长度为 126 时, 则 Payload Len 后跟随的 16 位将被解释为 16-bit 的无符号整数, 该整数的值指示 Payload 的实际长度;
    • 当 Payload 的实际长度为 127 时, 其后的 64 比特将被解释为 64-bit 的无符号整数, 该整数的值指示 Payload 的实际长度。

关于 Payload Len 如何实现:先读 7 bit 识别是以上3种的哪一种长度类型,再决定是否向后读 16 bit 或 64 bit 。

  • Masking-key, 该字段为可选字段, 当 Mask 标志位为 1 时, 代表这是一个掩码覆盖的 frame, 此时 Masking-key 字段存在, 其长度为 32 位, RFC 6455 规定所有由客户端发往服务端的 frame 都必须使用掩码覆盖, 即 对于所有由客户端发往服务端的 frame, 该字段都必须存在 , 该字段的值是由客户端使用熵值足够大的随机数发生器生成。若 Mask 标识位 0, 则 frame 中将设置 Masking-key 。

  • Payload, 该字段的长度是任意的, 该字段即为 frame 的数据部分, 若通信双方协商使用了 WebSocket 扩展, 则该扩展数据 (Extension data) 也将存放在此处, 扩展数据 + 应用数据, 它们的长度和为 Payload Len 字段指示的值。

WebSocket closing handshake

  1. websocket handshake 从一端发送一个 close control frame 开始。
  2. 发送一个 close control frame 或收到一个 close control frame 都意味着 websocket handshake 开始,并且 websocket connection 进入 CLOSING 状态。
  3. 收到 close control frame 的一端需答复对端一个 close control frame,并关闭 TCP connection。
  4. 收到 close control frame 答复的端,关闭 TCP connection,完成 TCP 挥手后,TCP connection 关闭。
  5. 当 TCP connection 关闭后,websocket connection 关闭,并且 websocket connection 进入 CLOSED 状态。
  6. 如果 TCP conncetion 是在 WebSocket closing handshake 之后完成,那么 Websocket connection 可以说是 干净地关闭的。

websocket server

nodejs 的 websocket server

ws 是一个第三方的 websocket 通信模块,需要安装 npm i ws

const WebSocket = require('ws')
const WebSocketServer = WebSocket.Server;

// wss is WebSocket.Server
wss.on("connection", function(ws, request) {
    // ws is WebSocket
    ws.on("message", (data, isBinary) => {
        if (isBinary) {
            console.log("recv binary data");
            ws.send("recv binary data success", {mask: false, binary: true, compress: false, fin: true}, (error) => {
                if (error) {
                    console.log("send data callback : error = ${error}");
                }
            });
        } else {
            console.log("recv text data");
            ws.send("recv text data success", {mask: false, binary: false, compress: false, fin: true}, (error) => {
                if (error) {
                    console.log("send data callback : error = ${error}");
                }
            });
        }
    });

    ws.on("ping", (data) => {
        // keepalive
        ws.pong(data, false, (err) => {
            if (err) {
                console.log("pong error=${err}");
            }
        });
        console.log("pong");
    });

    ws.on("pong", (data) => {
        // keepalive
        console.log("ping");
    });

    ws.on("close", (code, reason) => {
        console.log("websocket close");
    });
});

nginx 反向代理

《Nginx官方文档:WebSocket proxying》

为了将客户机和服务器之间的连接从 HTTP/1.1转换为 WebSocket,使用 HTTP/1.1中提供的协议切换机制。

HTTP/1.1中提供的协议切换机制:客户端通过请求中的 "Upgrade" header请求协议切换。

本来在 nginx 中,如上所述,包括“Upgrade” 和 “Connection”在内的 hop-by-hop headers 不会从客户机传递到被代理的服务器,因此,为了让被代理的服务器知道客户机将协议切换到 WebSocket 的意图,必须显式地传递这些消息头:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

nginx 配置:


# upstream in http config field
upstream WebsocketServer {
    127.0.0.1:1106;
}

# location in server config field
location /webscoket_test/ {
    proxy_pass WebsocketServer;
    proxy_http_version 1.1;
    proxy_read_timeout  3600s; # 超时设置,可采用心跳ping/pong保活
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

扩展知识

End-to-end headers

  • End-to-end headers 将被传输到请求或响应的最终接收者。
  • 该类型的消息头会被代理转发
  • 该类型的消息头会被缓存

Hop-by-hop headers

  • Hop-by-hop headers 只对单个传输级别的连接有意义,并且不由缓存存储或由代理转发。
  • 该类型的消息头不会被代理转发
  • 该类型的消息头不会被缓存

来源:hop-by-hop

HTTP/1.1 中 hop-to-hop 类型的消息头:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailers
  • Transfer-Encoding
  • Upgrade

注意

  • 该升级机制只是 HTTP/1.1 有效,HTTP/2 已不支持该机制
  • nginx 配置代理转发,默认不转发 hop-to-hop 类型的消息头 。
  • nginx 会断开长时间没有数据传输的连接

测试工具

wscat

oktools

http://oktools.net/websocket

websocket test

websocket client

JavaScript 的 websocket client

<!DOCTYPE html>
<html>
<head>
  <title>websocket</title>
</head>
<body>

  <script type="text/javascript">
    // 浏览器提供 WebSocket 对象
    var ws = new WebSocket('ws://localhost:1106')

    // 发送
    ws.onopen = function() {
      ws.send('hello')
    }

    // 接收
    ws.onmessage = function(message) { 
      alert(message.data) 
      if (message.data === 'hello') {
          ws.close()
      }
    }
  </script>

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

推荐阅读更多精彩内容