WebSocket的话题
上次介绍了基于代理的推送型消息传输协议 MQTT。在网上搜索 MQTT 时,经常会看到与 MQTT 搭配使用的技术要素 “WebSocket” 这一关键词。本次将简单介绍一下这个 WebSocket。
什么是WebSocket
WebSocket 是一种基于 HTTP 的叠加协议,可在已建立的 HTTP/HTTPS 连接上实现任意长度、任意格式数据的双向通信。它已作为 RFC6445 在 IETF 完成标准化。
之前在 “HTTP 的话题” 中介绍过 “HTTP/2 是在基于 TCP 运行的 HTTP 之上,实现了类似 TCP 功能的协议”,从定位上来说,WebSocket 也与之非常相似。不过,WebSocket 的实现比 HTTP/2 更为简单。例如,HTTP/2 能在单个 HTTP 会话中处理多条虚拟连接,而 WebSocket 的虚拟连接与 HTTP 会话是 1:1 的对应关系。两者以相同的方法论解决同一问题,却因切入角度不同形成了看似相似却又不同的标准(而且提出者都是谷歌!),不过这在计算机领域却是常见的现象。
WebSocket 是专为以 JavaScript 为首的脚本语言使用而开发的。在 JavaScript API 中提供了 WebSocket 对象,通过对该对象执行 open、send、message、close 等读写操作,开发者能够像在原始 TCP 连接上通信一样进行编程。
需要注意的是,“看起来像原始 TCP” 指的是源代码层面的表现,WebSocket 归根结底是运行在 HTTP/HTTPS 之上的叠加协议,并非向 JavaScript 提供 socket API 的技术。设计上的一大顾虑是,若将 WebSocket 用作 socket 的替代品,可能会被用于端口扫描、代理伪装等黑客攻击。为防范这一点,WebSocket 引入了一些看似奇特的规格(※注)。
※注:诸如「Sec-WebSocket-Key」及「负载掩码」等概念,本文将不深入探讨其细节。
为什么需要这样的东西呢
正如 “HTTP 的话题” 中介绍的那样,HTTP 最初是为了实现网络文档共享而开发的。其基本工作原理采用请求(客户端 / 浏览器)→响应(服务器)的往返模式。在 HTTP/1.0 及之后版本中,可通过 POST 命令实现客户端向服务器方向的数据传输。HTTP/1.1 及之后版本支持使用 Chunked Encoding,允许在数据长度不确定的情况下启动通信。但无论哪个版本,服务器向客户端的通信始终只能作为对请求的响应来发送,且必须等待请求完成后才能返回响应。
也就是说,原则上HTTP无法在任意时间向客户端方向传输任意数据。因此,在AJAX中,曾经使用过一种称为轮询的实现方式,即每隔10秒等固定时间间隔向服务器发送碎片化的请求以获取响应,但这种方式不可避免地存在响应时间的上限。而且,如果为了缩短响应时间而缩小轮询间隔,通信效率又会降低,两者相互矛盾。
我好像记得很久以前的电话连接TSS系统也做过类似的事情,不禁让人思考人类到底在做什么。随着“互联网”的使用方式从“网页浏览”发展到消息传递、聊天等双向实时交互,HTTP的局限性愈发明显。为了快速(※注)解决这一问题,开发了WebSocket;而为了更深入地解决这一问题,开发了HTTP/2,这就是它们的定位。
※注:原本应该是一种“快速解决办法”的WebSocket,却在RFC草案阶段被反复批评,其规范多次变更。由于基于多次修订中的草案版本的实现已经发布到市场上,因此要涵盖包括这些过渡草案版本在内的所有规范,变得非常困难。
为什么不能用普通的Socket套接字呢?
这一点在 “ HTTP 的话题” 中也提到过,我认为最主要的原因在于,如今的 “互联网” 是基于 “只要能浏览网页就行” 这一前提发展普及的,因此形成了充斥着代理服务器(Proxy)、网络地址转换(NAT)、防火墙(Firewall)等重重壁垒的现状。要打破已经形成的壁垒(让全世界的互联网设备同时升级为 IPv6 兼容!),或是在壁垒上开设新的通用接口(让全世界的互联网设备开放 HTTP 以外的新端口),我认为都不现实。但既然存在 “能够穿透 NAT 和代理服务器”“也支持安全防护” 的 HTTP 协议作为基础,那么利用它实现协同效应才是现实的选择。
1995 年制定的 IPv6 标准中,包含了旨在实现 “此类应用” 的诸多设计,例如支持一对多通信的 Multicast 、无需依赖连接状态即可追踪数据包的 Flow Labe 、强制要求实现安全功能的 IPsec(AH/ESP) 等。然而现实中,人们却跳过了这些设计,选择了一种从技术角度堪称 “畸形” 的方案 ——“在 HTTP/TLS 之上搭载类似 TCP 的协议”。这果然也是 “常见现象”(※注)。
※注:所谓 “常见现象” 的典型程度,例:为了在仅能处理连续最多 76 个英数字符的串行线路上传输邮件而开发的UUCP,被直接作为SMTP移植到互联网上;为了在 SMTP 上处理任意二进制数据,开发了将数据转换为 76 字符宽度英数字符的BASE64 编码;出于安全考虑又实现了SMTP over TLS;结果导致 “将 HTML 转换为 76 字符宽度英数字符的 BASE64 编码,加密后通过 TCP 传输” 这种无意义的操作成为日常。
WebSocket 的工作原理
关于 WebSocket,已经可以通过日语获取丰富的信息,因此本博客不会深入探讨其工作序列或格式细节,仅阐述最基本的工作原理。
WebSocket 的初始化由客户端的请求与服务器的响应往返开始,这一阶段的通信格式本身就是 HTTP。与 “纯粹的” HTTP 不同的是,此时会附加 Connection: Upgrade 标头(※注)。通过该标头,即便在请求~响应完成后,TCP/TLS 连接仍会保持;不过,此时在连接上传输的不再是 HTTP,而是 WebSocket 协议(为明确这一点,还会附加 Upgrade: WebSocket 标头)—— 双方通过这种方式达成共识。
※注:在普通的 HTTP 协议中,该字段要么是Connection: Close,要么是Connection: Keep-Alive。
{C}
WebSocket 的会话建立
初始化完成后,数据仅需在连接上双向传输即可。但与普通的 TCP 套接字不同,此时传输的并非 “单纯的字节流(Byte Stream)”,而是会附加简单的标头以指示数据单位长度。这一点与 TLS 中的消息 / 记录结构略有相似。当 WebSocket 在 TLS 上运行时,WebSocket 标头会被 TLS 记录包裹,再附加 TCP 标头后以 IP 数据包的形式传输。虽然会让人感觉这是某种多此一举或绕远路的操作,但考虑到 WebSocket 本就是在层层叠叠的技术基础上开发出来的,这也是无可奈何的事。
{C}
WebSocket 的标头
WebSocket的标头负载长度采用了一种稍显特殊的编码方式。如果负载长度在0到125字节之间,就用1个字节来表示;如果“负载长度”字段的值是126,那么就会附加一个16位的扩展长度字段;如果是127,则会附加一个32位的扩展长度字段。这一规则与日前提到的 MQTT 消息长度规则似是而非,但两者都与臭名昭著的 OSI ASN.1 BER 规则中的可变长编码有几分相似。
{C}
WebSocket的长度编码规则
如果第一个字节的值小于125,那么这个值就是负载长度本身。
如果第一个字节的值为126,则后续会附加一个16位的长度字段;
如果第一个字节的值为127,则后续会附加一个32位的长度字段。
字节顺序为 Big-Endian
{C}
MQTT 的消息长度编码规则
消息长度是按每7位存储的,bit7 用于指示后续是否还有字段。
字节顺序为 Little-Endian
{C}
ASN.1的长度编码规则
如果第一个字节的bit7为0,那么bit0到bit6表示的就是长度本身;
如果第一个字节的bit7为1,那么bit0到bit6表示后续长度字段的长度。
字节顺序为 Big-Endian
OSI 中大量使用的可变长标头设计,在 IETF 中评价极差,被批判为 “片面追求传输效率”、“忽视实现效率”、“电话工程师的诅咒” 等。尽管 IPv6 采用了近乎偏执地贯彻 64 位边界的固定长标头设计(※注),但 20 年后人们却再次评价 “果然还是可变长编码更高效”—— 这不禁让人产生 “人类究竟在做什么” 的困惑,但这同样是 “常见现象”。技术方法论中本就没有绝对的好坏,其评价取决于与使用环境的适配性,而环境始终在不断变化。
不过,存在三种似是而非的长度编码方式,真想让人说一句 “差不多得了”。
※注:所谓 “偏执” 到什么程度呢?以存储 MAC 地址的源 / 目标链路层地址选项字段为例:该字段由1 字节类型(1 或 2)、1 字节标头长度后接 MAC 地址构成。当 MAC 地址为 48bit(6 字节)时,标头总长度恰好为 64bit(8 字节),完全对齐 64 位边界。但这种设计因过度紧凑而缺乏识别 MAC 地址类型 / 长度的字段。例如,若要传输 IEEE802.15.4 中使用的 64bit(EUI-64)地址,会超出 2 字节,需按 64bit 单位向上取整,导致标头长度变为 16 字节(2×64bit)。更麻烦的是,地址长度和填充长度未在任何字段中标识,只能通过协议框架外的信息(如链路层驱动描述符)查询地址长度 —— 这实在称不上 “优雅” 的设计。我认为这是一种因过度追求 64 位边界 “整齐性” 而显得勉强的规格。
总结
WebSocket 是迫于需求而开发的技术,但正如多次提到的,它是 “层层叠叠堆叠起来的产物”、“技术上堪称畸形”。而这样的技术之所以被需要,我认为恰恰如实反映了 IPv6 的现状。说起来,上次提到的 MQTT 也是如此 —— 在 IETF 构想的 “理想中的下一代互联网” 中,本应通过多播技术实现的功能,如今却要向 1000 个会话订阅者重复发送 1000 次相同消息,这种 “无意义的操作” 本应是可以避免的。
我并非想说 IPv6 是失败的或毫无意义的。鉴于扩大地址空间是刚需,它迟早会逐步取代 IPv4。然而,诸如多播、任意播、流标签、IPsec 等作为 “下一代互联网标准制定” 这一宏大工程核心的技术,本是 IETF 年轻工程师们寄予未来的 “播种”,最终却未能发芽。
不过,这在 10 年、20 年后会变成什么样还不得而知。或许那时人们会说:“居然还在用代理服务器分发数据?使用多播不是常识吗!” 或者 “竟然还有系统在使用 TLS?使用 IPsec 不是常识吗!”。毕竟技术的最佳解决方案会随着环境的变化而不断改变。
相关连接
