Sucha's Blog ~ Archive for January, 2024

24年1月28日 周日 22:47

WebSocket(1)

研究了一下 WebSocket,在 HTTP/1.1 之上,提供了一个双工长连接,通过 header 的 Upgrade 来完成协议升级及参数交换,之后就都是 websocket 的 frame 了。其最基础的协议描述在 RFC6455,压缩部分的扩展在 RFC7962

之前研究了一下 C 写的 Websocket,验证了一下是可以正常工作的,但它 frame 的解析部分不好单独摘出来。

client 端的 html + js websocket data send/recv 其实很好写,browser 本来就有 WebSocket 的接口给调用,拉起本地的一个 html 就行了,但是 browser 无法指定 http header 的参数(包括 ws extension),发送 text、binary 的 frame 时,是通过 js 的 string、uint8 array 来指定的。

想着找一个可以直接提供解析 websocket frame 的库,虽然找到了 C 写的 wslay,但是实际使用过程中,感觉使用实在是繁琐。

比如 websocket frame 的结构如下:

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

回到之前说的 wslay,霓虹国写的东西太复杂,居然 header 没检测全都能返回好几种错误,我感觉这部分其实可以简单点,比如 header 都解析不了的时候其实可以提示数据不足(毕竟 header 数据都不足),缺点是放弃了一部分错误的检查。

在 HTTP/1.1 的协议升级到 websocket 之前,header 需要交换一些数据,比如 client 会发送如下的字段

Sec-WebSocket-Version: 13
Sec-WebSocket-Key: fSQMpewdgSIz1IhuhL3ERQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

说到 permessage-deflate,server 在 deflate payload 后构建 frame 时,需要设置 rsv1 bit 为 1,要不 client 会忽略 extensions 的描述。

还有 server 在 inflate frame payload 时,实际上认为不断喂进来的 payload 是连续的 data stream,因此不需要做 inflateEnd。

这个感觉也跟 HTTP/1.1 的实际处理类似,实际每个 http requet / response 都是一个单独的 inflate / deflate stream,websocket 升级了协议后,可以认为不断发送过来(出去)的 payload 是没有结束的 http body 数据,因此不需要结束这个 inflate / delfate stream。

其他比如 ping、pong、close frame 类型没怎么研究,不过感觉 close 没啥作用,也许是用于实际关闭 tcp 链接前的一个处理吧,也许给调用层处理,在 websocket frame 这一层感觉没什么必要,毕竟无论如何总是要处理没有发送 close 的资源注销的,那又何必多此一举要这个 close 呢。