[2024][SnowCrystal]WebSocket

参考
RFC 6455
万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践
火の狐-Mozzila Developer Network WebSocket

🪻🍁🍃⛈️Introduce⛈️🍃🍁🪻 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

███████╗███╗   ██╗ ██████╗ ██╗    ██╗     ██████╗██████╗ ██╗   ██╗███████╗████████╗ █████╗ ██╗     
██╔════╝████╗  ██║██╔═══██╗██║    ██║    ██╔════╝██╔══██╗╚██╗ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██║     
███████╗██╔██╗ ██║██║   ██║██║ █╗ ██║    ██║     ██████╔╝ ╚████╔╝ ███████╗   ██║   ███████║██║     
╚════██║██║╚██╗██║██║   ██║██║███╗██║    ██║     ██╔══██╗  ╚██╔╝  ╚════██║   ██║   ██╔══██║██║     
███████║██║ ╚████║╚██████╔╝╚███╔███╔╝    ╚██████╗██║  ██║   ██║   ███████║   ██║   ██║  ██║███████╗
╚══════╝╚═╝  ╚═══╝ ╚═════╝  ╚══╝╚══╝      ╚═════╝╚═╝  ╚═╝   ╚═╝   ╚══════╝   ╚═╝   ╚═╝  ╚═╝╚══════╝

本文可视作对 RFC6455 的部分翻译,在此基础上,对 WS 的使用进行了部分演示。
本文中出现的 消息(message),帧(frame),段(fragement)语意如下:消息往往指的是完整的数据,比如一张图片,一个视频。既可以是二进制数据也可以是文本数据。而帧与段在本文中语意相同,都表示一个消息的一部分,若干个帧按照其发送顺序拼接成一个完整的消息。
一个帧往往对应了一个ws报文;但是有时候一个ws报文也能承载完整的消息。

早期,很多网站为了实现推送技术,所用的技术都是轮询(也叫短轮询)。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,然后服务器返回最新的数据给客户端。

在这里插入图片描述

一直以来,创建需要在客户端和服务器之间进行双向通信的网络应用程序(如即时消息和游戏应用程序)时,都需要滥用 HTTP 来轮询服务器进行更新,同时以不同的 HTTP 调用 [RFC6202] 发送上游通知。这会导致一些问题:

  • 服务器被迫为每个客户端使用多个不同的底层 TCP连接:一个连接用于向客户端发送信息,另一个连接用于接收每条信息。一个用于向客户端发送信息,而每个接收到的信息都需要一个新的连接。
  • 开销很大,每个客户端到服务器的每个消息都有可能有大量的 HTTP Headers;其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。
  • 客户端 js 需要维护一个从传出连接到传入连接的映射。出站连接到入站连接的映射,以跟踪回复。

更简单的解决方案是使用单个 TCP 连接来处理两个方向的流量。 这就是 WebSocket 协议提供的功能。 结合 WebSocket API [WSAPI],它为网页与远程服务器之间的双向通信提供了 HTTP 轮询的替代方案。这个技术可用于各种网络应用程序:游戏股票行情多用户同时编辑应用程序实时公开服务器端服务的用户界面等。

WebSocket 协议旨在取代现有的双向通信技术,这些技术使用 HTTP 作为传输层,以受益于现有的基础设施(代理、过滤、身份验证)。这些技术是在效率和可靠性之间权衡后实现的,因为 HTTP 最初并不打算用于双向通信。WebSocket 协议将在现有的 HTTP 基础架构的基础上实现现有双向 HTTP 技术的目标;因此,它被设计为可在 HTTP 端口 80 和 443 上运行,并支持 HTTP 代理,即使这意味着当前环境的一些特定复杂性。不过,WebSocket 的设计并不局限于 HTTP,未来的实现可以使用更简单的握手方式,通过专用端口,而无需重新发明整个协议。最后一点很重要,因为交互式信息传输的流量模式与标准 HTTP 流量并不完全一致,可能会给某些组件带来异常负载。

普遍认为,WebSocket的优点有如下几点:

  • 较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;
  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
  • 保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;
  • 更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;
  • 可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

🎋🥀Protocol Overview🥀🎋

The protocol has two parts: a handshake and the data transfer.The handshake from the client looks as follows:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

The handshake from the server looks as follows:

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

客户端请求的首行采用请求行格式。服务器响应的首行遵循状态行格式。请求行和状态行在 [RFC2616] 中定义。一旦客户端和服务器都发送了握手信息,如果握手成功,那么数据传输部分就开始了。这是一个双向通信通道,每一方都可以独立于另一方随意发送数据。

握手成功后,客户端和服务器以message单位传输数据 , WebSocket 消息并不一定与特定的网络层框架相对应,因为零散的消息可能会被代理合并或拆分。

一个帧 (Frame) 有一个相关类型。属于同一信息的每个帧都包含相同类型的数据。简单地说,有文本数据(解释为 UTF-8 文本)、二进制数据(由应用程序解释)和控制帧(不携带数据,而是用于协议级信号,如关闭连接的信号)的类型。这一版本的协议定义了六种帧类型,并为将来使用保留了十种。

🎋🥀Design Philosophy🥀🎋

WebSocket 协议的设计原则是尽量减少帧的传输(唯一的框架是使协议基于 framing 而不是基于流,并支持区分 Unicode 文本帧和二进制帧)。元数据将由应用程序层(HTTP)分配到 WebSocket 上,就像 HTTP 与 TCP 的关系。

从概念上讲,WebSocket 实际上只是 TCP 上的一层,它具有以下功能:

  • 为浏览器增加了original-based的安全模型
  • 增加了寻址和协议命名机制,以支持一个端口上的多种服务和一个 IP 地址上的多个主机名
  • 在 TCP 的基础上增加了 framing 机制,以回到 TCP 所建立的 IP 数据包机制,但没有长度限制
  • 包括一个额外的in-band关闭握手机制,其目的是在存在代理和其他的情况下工作

除此以外,WebSocket 没有增加任何功能。从根本上说,它的目的是在网络的限制条件下,尽可能地将原始 TCP 暴露给代码。此外,WebSocket 的设计还使其可以与 HTTP 服务器共享一个端口,方法是让握手成为一个有效的 HTTP 升级请求。

从概念上讲,我们可以使用其他协议来建立客户端-服务器消息传递,但 WebSockets 的目的是提供一个相对简单的协议,它可以与 HTTP 和已部署的 HTTP 基础设施(如代理)共存,并且在考虑到安全因素的情况下,尽可能地接近 TCP,以安全地与此类基础设施一起使用,同时有针对性地添加一些内容以简化使用并保持简单(如添加消息语义)。该协议具有可扩展性,未来的版本可能会引入更多的概念,如多路复用。

当从网页使用 WebSocket 协议时,WebSocket 协议使用网页浏览器使用的origin模型来限制哪些网页可以与 WebSocket 服务器进行通信。当然,如果 WebSocket 协议是由专用客户端直接使用(即不是通过网络浏览器从网页使用),那么origin模型就没有用了,因为客户端可以提供任意的origin字符串。

该协议旨在避免与 SMTP [RFC5321] 和 HTTP 等现有协议的服务器建立连接,同时允许 HTTP 服务器根据需要选择支持该协议。要做到这一点,就必须对握手过程进行详细的规定,并限制在握手结束前可插入连接的数据(从而限制服务器可受影响的程度)。

同样,当来自其他协议(尤其是 HTTP)的数据被发送到 WebSocket 服务器时,例如,HTML 表单被提交到 WebSocket 服务器时,WebSocket 也可能无法建立连接。这主要是通过要求服务器证明它读取了握手信息来实现的,而只有当握手信息包含只有 WebSocket 客户端才知道的部分时,服务器才能建立连接。

🎋🥀与 HTTP 的关系🥀🎋

WebSocket 协议是一个独立的基于 TCP 的协议。它与 HTTP 的唯一关系是,WS 的握手过程使用 HTTP,HTTP服务器根据HTTP标头中,与 WS 有关的字段信息来建立WS连接。WebSocket 协议一般使用 80 端口用于普通 WS 连接,443 端口可用于基于 TLS 的 WS 连接。

🎋🥀WS 的子协议🥀🎋

客户端可以通过在握手中加入 Sec-WebSocket-Protocol 字段,要求服务器使用特定的子协议。如果指定了该字段,服务器需要在其响应中包含相同的字段和所选子协议值之一才能建立连接。

这些子协议名称应按照规定进行注册。为避免潜在的冲突问题,建议使用包含子协议发起者域名的 ASCII 版本的名称。例如,如果示例公司要创建一个由 Web 上许多服务器执行的聊天子协议,他们可以将其命名为 chat.example.com 如果这个公司将他们的竞争子协议命名为 chat.example.org,那么这两个子协议的名称就会发生冲突。那么服务器就可以同时执行这两个子协议,服务器会根据客户端发送的值动态选择使用哪个子协议。

子协议可以通过更改子协议名称(例如,从 bookings.example.net更改为 v2.bookings.example.net)的方式进行向后不兼容的版本控制。WebSocket 客户端会认为这些子协议是完全独立的。可以通过重复使用相同的子协议字符串来实现向后兼容的版本控制,但要精心设计实际的子协议以支持这种可扩展性。

🎋🥀WS URI 🥀🎋

ws://<host>[:<port>]/<path>[?<query>]

Fragment标识符#在 WebSocket URI 中没有任何意义,不得在WS URI 中使用。与任何 URI 方案一样,在不表示片段开始的情况下,字符 , 必须转义为 %23。

Websocket 使用 wswss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。比如

ws://echo.websocket.org
wss://echo.websocket.org

WebSocket 与 HTTP 和 HTTPS 使用相同的 TCP 端口,可以绕过大多数防火墙的限制。如果使用 ws + tlswss ,则使用 HTTPS 端口默认为 443,否则 ,ws 使用 HTTP 端口,默认为 808080

⛈️📫💐HandShake💐📫⛈️ ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

🌸🌹Opening Handshake🌹🌸

Opening Handshake 旨在与基于 HTTP 的服务器端软件和代理兼容,以便与该服务器对话的 HTTP 客户端和与该服务器对话的 WebSocket 客户端都能使用一个端口。为此,WebSocket 客户端的握手是 HTTP 升级请求:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

根据 [RFC2616],客户端可按任何顺序发送握手中的标头字段,因此接收不同标头字段的顺序并不重要。GET 方法的 "Request-URI " 用于标识 WebSocket 连接的端点,这样既可以从一个 IP 地址为多个域提供服务,也可以由一个服务器为多个 WebSocket 端点提供服务。

根据 [RFC2616],客户端会在握手的 Host 标头字段中包含主机名,这样客户端和服务器就能确认应该使用哪台主机。

附加标头字段用于选择 WebSocket 协议中的选项。该版本中的典型选项包括子协议选择器 Sec-WebSocket-Protocol、客户端支持的扩展列表 Sec-WebSocket-ExtensionsOrigin 头信息字段等。Sec-WebSocket-Protocol请求头字段可用于指明客户端可接受哪些子协议(即在 WebSocket 协议上分层的应用级协议)。服务器从可接受的协议中选择一个或一个都不选,并在握手中回传该值,以表明它选择了该协议。

Sec-WebSocket-Protocol: chat

Origin标头字段用于防止在网络浏览器中使用 WebSocket API 的脚本未经授权跨源使用 WebSocket 服务器。服务器会被告知生成 WebSocket 连接请求的脚本来源。如果服务器不希望接受来自该来源的连接,它可以选择发送适当的 HTTP 错误代码来拒绝连接。该标头字段由浏览器客户端发送;对于非浏览器客户端,如果该标头字段对这些客户端的上下文有意义,也可以发送。

服务器必须向客户端证明它收到了客户端的 WebSocket 握手,这样服务器就不会接受非 WebSocket 连接。这就防止了攻击者通过使用 XMLHttpRequest 或表单提交向 WebSocket 服务器发送伪造的数据包来欺骗 WebSocket 服务器。

为了证明已收到客户端的握手信息,服务器必须获取两条信息,并将它们组合成一个响应。第一条信息来自客户端握手中的 Sec-WebSocket-Key标头字段:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

对于该标头字段,服务器必须将base64编码的值 trim掉开头和尾部的空白,然后与字符串形式的全局唯一标识符(Globally Unique Identifier ,GUID)258EAFA5-E914-47DA-95CA-C5AB0DC85B11进行拼接,不理解 WebSocket 协议的网络端点不太可能使用该字符串。然后,在服务器的握手过程中,将拼接字符串的 SHA-I 哈希值(160 位),进行 base64 编码。

具体来说,如果如上例所示,Sec-WebSocket-Key 头的值为 dGh11HNhbXBsZSBub25jZQ==,服务器将拼接字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 。输出

0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea

然后,对该值进行 base64 编码,得到值 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=。 然后,该值将在 Sec-WebSocket-Accept 头信息字段中相应给客户端。

服务器的握手比客户端的握手简单得多。第一行是 HTTP 状态行,状态代码为 101:

 HTTP/1.1 101 Switching Protocols

101 以外的任何状态代码都表示 WebSocket 握手尚未完成,HTTP 语义仍然适用,此时的TCP连接上仍然只有HTTP协议。

ConnectionUpgrade 标头字段完成 HTTP 协议转换,即通过HTTP更新到WS协议。Sec-WebSocket-Accept 头信息字段表示服务器是否愿意接受连接。如果存在,该标头字段必须包括在 Sec-WebSocket-Key 中发送的客户端 nonce 的哈希值以及预定义的 GUID。任何其他值都不能被解释为服务器接受连接。

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

WebSocket 客户端会对这些字段进行检查。如果 Sec-WebSocket-Accept 值与预期值不匹配,或缺少标头字段,或者 HTTP 状态代码不是 101,连接将不会建立,WebSocket 帧也不会被发送。

也可以包含Option字段。在此版本的协议中,WS 主要使用的Option字段是 Sec-WebSocket-Protocol,表示服务器选择的子协议。WebSocket 客户端会验证服务器是否包含了 WebSocket 客户端握手中指定的值之一。使用多个子协议的服务器必须确保根据客户机的握手选择一个,并在握手中指定。

Sec-WebSocket-Protocol: chat

服务器还可以设置与 cookie 相关的选项字段来 set cookie。参见 RFC 6265

🌸🌹Closing Handshake🌹🌸

关闭握手(Closing Handshake)比开始握手简单得多。任一对等方都可以发送一个包含指定控制序列数据的控制帧来进行关闭握手。关闭帧作为回应(如果尚未发送)。收到该控制帧后,第一个对等体就会关闭连接,并确信不会再收到任何数据。

在发送表示连接应关闭的控制帧后,对等端不会再发送任何数据;在收到表示连接应关闭的控制帧后,对等端会丢弃收到的任何数据。两个对等端同时启动这种握手是安全的。

关闭握手旨在补充 TCP 关闭握手(FIN/ACK),因为 TCP 关闭握手并不总是可靠的端到端,特别是在存在拦截代理和其他情况下。通过发送关闭帧并等待响应的关闭帧,可以避免在某些情况下不必要地丢失数据。例如,在某些平台上,如果套接字在接收队列中有数据的情况下被关闭,就会发送一个 RST 数据包,这将导致收到 RST 的一方的 recv() 失败,即使还有数据等待读取。

🌸🌹Client Requirements🌹🌸

要建立 WebSocket 连接,客户端需要打开一个连接并开始上述定义的握手。连接被定义为最初处于 CONNECTING(连接)状态。Client需要提供hostportresource namesecure标志,这些都是 WebSocket URI 的组成部分,以及要使用的protocolsextensions列表,这两个内容使用 Sec-WebSocket-Protocol以及Sec-WebSocket-Extensions 标头进行描述。此外,如果客户端是网络浏览器,还需要提供 origin 标头。

在受限制的环境中运行的客户端,例如与特定运营商绑定的手机上的浏览器,可以将连接管理 offload 给网络上的另一个代理。在这种情况下,就本规范而言,客户端既包括手机软件,也包括任何此类代理。

Client 要_建立 WebSocket 连接_时,以 URI 的形式给定一组 ( h o s t ,   p o r t ,   r e s o u r c e   n a m e ,   s e c u r e ) (host,~port,~resource~name,~secure) (host, port, resource name, secure),以及要使用的 protocolsextensions列表,如果是网络浏览器,还必须给定 origin,然后才能打开连接,开始握手,并读取服务器的握手回应。本节将介绍如何打开连接、在Opening handshake中应发送哪些内容以及如何解释服务器响应的具体要求。

  1. ( h o s t ,   p o r t ,   r e s o u r c e   n a m e ,   s e c u r e ) (host,~port,~resource~name,~secure) (host, port, resource name, secure)​ 中指定的所有字段都必须有效,任意一个不合法(参见 WS URI), 客户端则必须 Fail the WebSocket Connection ,连接终止于此

    If any of the components are invalid, the client MUST Fail the WebSocket Connection and abort these steps.

  2. 如果客户端主机已经与 host 上的端口 port 对所标识的远程主机建立了 WebSocket 连接,客户机也必须等待该连接建立或该连接失败。处于 CONNECTING 状态的连接不得超过一个。如果同时尝试与同一 IP 地址建立多个连接,客户端必须将其串行化,以保证在同一时间内不会有多个连接处于运行。

    如果 Client 无法确定远程主机的 IP 地址(例如,因为所有通信都是通过代理服务器进行的,而代理服务器本身会执行 DNS 查询),则客户端必须假定每个主机名都指向一个不同的远程主机,客户端应将同时待建连接的总数限制在一个合理的较低水平。客户端可能允许同时待建连接到 a.example.comb.example.com,但如果请求同时连接到一台主机的数量达到 30 个,则不能允许这种情况。在网络浏览器中,客户端在设置同时待处理连接数限制时,需要考虑用户打开的标签页数量。

    这样 js 就很难通过打开大量 WebSocket 连接到远程主机来实施拒绝服务攻击。服务器在受到攻击时,可以通过在关闭连接前暂停来进一步减少自身的负载,因为这将降低客户端重新连接的速度。

    客户端与单个远程主机建立 WebSocket 连接的数量不受限制。对于WS连接数量过多的主机,这些主机之后请求的WS连接服务器可以予以拒绝,或在负载过高时断开占用资源的连接。

Proxy

如果客户端在使用 WS 时被配置了代理,对于客户端想要连接的目标主机,host:port ,此时客户端应该连接到配置的代理,并通过代理与host:port打开 TCP 连接。比如,如果一个客户端使用代理转发所有的HTTP流量,现欲连接到 example.com ,此时客户端主机可能发送以下的请求行:

CONNECT example.com:80 HTTP/1.1
Host: example

另外,如果需要配置代理的用户认证,那么就可能是这样

CONNECT example.com:80 HTTP/1.1
Host: example.com
Proxy-authorization: Basic ZWRuYW1vZGU6bm9jYXBlcyE=

如果客户端未配置为使用代理,则应与 host:port 指定的主机的指定的端口直接打开 TCP 连接。

注意:如果服务端没有公开明确的 URI,以便为 WebSocket 连接选择一个与其他代理分开的代理,鼓励为 WebSocket 连接使用 SOCKS5 代理(如果有),如果没有,则优先使用为 HTTPS 连接配置的代理,而不是为 HTTP 连接配置的代理。就代理自动配置脚本而言,传递给函数的 URI 必须使用 WebSocket URI 定义,由 /host/、/port/、/resource name/ 和 /secure/ 标记构建而成。

↑End of Proxy↑

如果 secure 为 true,该通道上的所有进一步通信都必须通过加密隧道进行;客户端必须在打开连接后发送WS握手数据前在连接上执行 TLS 握手 。如果失败(例如,无法验证服务器证书),则客户端必须 Fail the WebSocket Connection 并中止连接。

一旦与服务器建立了连接(包括通过代理或 TLS 加密隧道建立的连接),客户端必须向服务器开始 Opening Handshake。握手由 HTTP 升级请求和一系列必需和可选的标头字段组成。该握手的要求如下:

  1. 握手必须通过合法的 HTTP 请求进行
  2. 请求方法必须是 GET 并且HTTP的版本至少为 HTTP/1.1
  3. 如果 Request-URI 是一个相对 URI 则必须与服务器规定的 r e s o u r c e   n a m e resource~name resource name 相匹配;如果是绝对 URI ,则 scheme 必须是 ws/wss 之一,主机地址,端口也必须匹配。
  4. 请求必须包含 Host 标头,值必须与 host 中的值一致;可选包含端口号,如果使用的不是默认端口号,比如 wss443 或者 ws80 ,则就需要显式声明端口号。
  5. 请求必须包含 Upgrade 请求头,其值必须为 websocket 关键字
  6. 请求必须包含 Connection 请求头,其值必须为 Upgrade 关键字
  7. 请求必须包含 Sec-WebSocket-Key 头部,其值必须是一个16 bytenonce ,并且使用 base64 进行编码;对于不同连接,需要生成不同的 nonce
  8. 如果客户端是浏览器,则请求头必须包含 Origin 头部,如果不是浏览器,那么如果在满足一定语义的情况下,可以选择使用 Origin 头部
  9. 请求必须包含 Sec-WebSocket-Version 头部,并且,该请求头的值必须为 13;值 9101112 并未用作 Sec-WebSocket-Version 的有效值。这些值在 IANA 注册表中被保留,但现在和将来都不会被使用。
  10. 请求可选包含 Sec-WebSocket-Protocol ,如果该请求头被提供,那么值可以用于指定一个或者多个子协议,这些子协议是客户端主机期望的,且不能为空串;如果指定多个,则使用 , 进行分隔。指定多个值时不能重复。字符范围必须在 U+0021U+007E 之间
  11. 请求可选包含 Sec-WebSocket-Extensions 头部,其作用将稍后进行介绍
  12. 请求可选包含如 cookie 以及与验证有关的字段,比如 Authentication 头部

一旦发送了 Opening Handshake 请求,在发送数据之前,客户端必须等待服务器的响应;在收到响应后,客户端必须对响应进行校验:

  1. 如果状态码是 101 ,则客户端可以按照 HTTP 的标准处理流程进行处理。另外,如果状态码是 401 Unauthorized 那么可能继续进行验证过程;如果状态码是 3xx ,则可以进行跳转。这些不是强制要求。
  2. 如果响应缺少 Upgrade 头部,或者该头部字段的值不是 ASCiiwebsocket (大小写不敏感),则客户端连接建立失败。连接建立终止
  3. 如果响应缺少 Connection 头部,或者该头部的值不是 ASCIIUpgrade 值(大小写不敏感),则客户端连接建立失败。连接建立终止
  4. 如果响应缺少 Sec-WebSocket-Accept 头部,或者该头部与通过Sec-WebSocket-Key请求头与 E914-47DA-95CA-C5AB0DC85B11拼接值计算出的 sha-1 bashe64 摘要不一致,则客户端连接建立失败。连接建立终止
  5. 如果响应包含 Sec-WebSocket-Extensions 头部,但是该头部的值在握手阶段并没有提到,即:服务端提供了一个客户端不支持的扩展,则客户端连接建立失败。连接建立终止
  6. 如果响应包含 Sec-WebSocket-Protocol 头部,同理,如果该头部的值在握手阶段并没有提到,即:服务端提供了客户端不支持的子协议,则客户端连接建立失败。连接建立终止

如果服务器的响应通过了上述验证,则表示_WebSocket 连接已建立_,WebSocket 连接处于 OPEN 状态。当前WS连接所使用的 Extensions ,可能是字符串,但通常来说,其值等于服务器握手提供的 Sec-WebSocket-Extensions头信息字段的值,如果服务器握手中没有该头信息字段,则其值为空。

当前WS连接所使用的 子协议 被定义为 Sec-WebSocket-Protocol 标头字段标头字段的值。此外,如果存在 Set-Cookie 标头,则以HTTP 中的处理为准。

🌸🌹Server-Side Requirements🌹🌸

服务器可将连接管理转交给网络上的其他代理,例如负载平衡器和反向代理。在这种情况下,就本规范而言,服务器被视为包括服务器端基础设施的所有部分,从第一个终止 TCP 连接的设备一直到处理请求和发送响应的服务器。举例说明: 一个数据中心可能有一台服务器,通过适当的握手对 WebSocket 请求做出响应,然后将连接传递给另一台服务器以实际处理数据帧。在本规范中,服务器是两台计算机的组合。

读取客户端的 Opening Handshake

客户端启动 WebSocket 连接时,会开始 Opening Handshake 。服务器必须至少解析该握手的一部分,以便获得必要的信息来生成握手的服务器部分(响应)。客户机的 Opening Handshake 包括以下部分。

如果服务器在读取握手报文过程中发现客户端发送的握手不符合下面的描述,包括但不限于任何违反为握手组成部分指定的 ABNF gramar 的行为,服务器必须停止处理客户端的握手,并返回一个带有适当错误代码(如 400 Bad Request)的 HTTP 响应。

客户端的 Opening Handshake 限制如下

  • 必须为HTTP/1.1 或更高版本的 GET 请求,包括一个 Request-URI ,该 URI 必须是一个 WS URI

  • 包含服务器权限信息的 Host HTTP标头信息字段

    A Host header field containing the server’s authority.

  • 包含 websocket 值的 Upgrade 标头,作为 ASCII 大小写不敏感值处理

  • 包含 Upgrade 值的 Connection 标头,作为 ASCII 大小写不敏感值处理

  • 包含Sec-WebSocket-Key 标头,其值为 base64 编码,解码后长度为 16 字节

  • 包含 Sec-WebSocket-Version 标头,值为 13

  • 可以包含 Origin 头信息。所有浏览器客户端都一定会发送该标头字段。缺少该标头的连接请求不应被视为来自浏览器客户端

  • 可以包含 Sec-WebSocket-Protocol 标头,该字段包含一个值列表,说明客户端希望使用的协议(按优先级从前到后排序)

  • 可选包含 Sec-WebSocket-Extensions 标头,该字段包含一个值列表,表明客户端希望使用哪些扩展。该标头字段的解释将在之后讨论

  • 可选择其他标头,如用于发送 cookie 或请求服务器验证的标头字段。根据[RFC2616],未知标头将被忽略

发送服务器端的 Opening handshake 响应

当客户端与服务器建立 WebSocket 连接时,服务器必须完成以下步骤以接受连接并发送服务器的 Opening handshake 响应:

  1. 如果连接打开在 HTTPS 端口上,则在连接上执行 TLS 握手。如果失败,则关闭连接;否则,连接的所有进一步通信(包括服务器的握手)都必须通过加密隧道进行。

  2. 服务器可执行额外的客户端身份验证步骤,例如,根据 [RFC2616] 中的描述,返回带有相应 WWW-Authenticate 标头的 401 Unauthorized 状态码

  3. 服务器可以使用 3xx 状态码重定向客户端 ;注意,这一步骤可以与上述可选的身份验证步骤一起、之前或之后进行。

  4. Establish the following information :

    • origin :客户端握手中的 Origin 标头表示建立连接的脚本的源。origin 将序列化为 ASCII 并转换为小写。服务器可使用此信息来决定是否接受传入的连接。如果服务器不验证来源,它将接受来自任何地方的连接。如果服务器不想接受该连接,它必须返回一个适当的 HTTP 错误代码(如 403 Forbidden),并中止本节所述的 WebSocket 握手过程。
    • key :客户端握手中的 Sec-WebSocket-Key 标头包含一个 base64 编码值,解码后长度为 16 字节。该(编码)值用于创建服务器握手,以表示接受连接。服务器无需对 Sec-WebSocket-Key 值进行 base64 解码。
    • version :客户端握手中的 Sec-WebSocket-Version 标头字段包括客户端试图与之通信的 WebSocket 协议版本。如果该版本与服务器能理解的版本不匹配,服务器必须中止本节所述的 WebSocket 握手,并发送一个适当的 HTTP 错误代码(如 426 Upgrade Required)和一个 Sec-WebSocket-Version 头信息字段,说明服务器能支持的版本。
    • resource name :服务器所提供服务的标识符。如果服务器提供多种服务,那么该值应从 GET 方法的 Request-URI中客户端握手时给出的资源名称中得出。如果请求的服务不可用,服务器必须发送适当的 HTTP 错误代码(如 404 Not Found)并中止 WebSocket 握手。
    • subprotocol :代表服务器准备使用的子协议,可以是单个值空值。选择的值必须来自客户端的握手,具体是从 Sec-WebSocket-Protocol 字段中选择一个服务器支持的。如果客户端的握手不包含这样的标头字段,或者服务器不同意客户端请求的任何子协议,则唯一可接受的值是空。如果客户端没有发送该字段,就等价于该字段为空,这意味着,如果服务器不希望同意建议的子协议之一,就不得在其响应中发回 Sec-WebSocket-Protocol 标头。就这些目的而言,空字符串与空值不同,也不是该字段的合法值。该标头字段值的 ABNF 为 (token),其中的结构和规则定义见 [RFC2616] 。
    • extensions :服务器准备使用的协议扩展列表(可能为空)。如果服务器支持多个扩展,那么该值必须从客户端的握手过程,具体是从 Sec-WebSocket-Extensions 字段中选择一个或多个值。如果客户端没有发送该字段,则等同于空值。就这些目的而言,空字符串与空值不同。
  5. 如果服务器接受传入的连接,它必须回复一个有效的 HTTP 响应,说明以下内容:

    1. 根据 RFC 2616,状态行的响应代码为 101 ,即 HTTP/1.1 101 Switching Protocols

    2. 根据 RFC 2616,响应的 Upgrade 标头字段的值为 websocket

    3. Connection 标头字段,值为 “Upgrade”。

    4. Sec-WebSocket-Accept 头信息字段。该标头字段的值是由客户端发送的Sec-Websocket-key的值与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11连接而成,通过对拼接后的值进行 SHA-1 哈希运算得到一个 20 字节的值,并对该 20 字节哈希值进行 base64 编码。

      **Note:**客户端发送的是 16 字节,服务器在拼接后再拼接魔数后再进行操作完就是 20 字节了

    5. 可选响应 Sec-WebSocket-Protocol 标头

  6. 可选响应 Sec-WebSocket-Extensions标头。如果要使用多个扩展名,可以在一个 Sec-WebSocket-Extensions 头信息字段中列出所有扩展名,也可以在 Sec-WebSocket-Extensions 头信息字段的多个实例之间进行分割。

这样就完成了服务器的握手。如果服务器在完成这些步骤后没有中止 WebSocket 握手,则服务器认为 WebSocket 连接已建立,WebSocket 连接处于 OPEN 状态。此时,服务器就可以开始发送和接收数据了。

🌸🌹WS 版本协商🌹🌸

本节提供了在客户机和服务器中支持多个 WebSocket 协议版本的一些指导。使用 WebSocket 版本宣告功能(Sec-WebSocket-Versionl 标头字段),客户端最初可以请求它喜欢的 WebSocket 协议版本(不一定是客户端支持的最新版本)。

如果服务器支持请求的版本,且握手信息有效,服务器将接受该版本。如果服务器不支持请求的版本,它必须回应一个或者多个 Sec-WebSocket-Version 头字段,其中包含服务器愿意使用的所有版本。此时,如果客户端支持其中一个版本,就可以使用新的版本值重复 WebSocket 握手。

下面的示例演示了上述版本协商:

客户端发送

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 25

然而服务器不支持 25,所以响应

HTTP/1.1 400 Bad Request
...
Sec-WebSocket-Version: 13, 8, 7

版本可以使用多个 Sec-WebSocket-Version 头部进行宣告,所以响应也可能是这样

HTTP/1.1 400 Bad Request
...
Sec-WebSocket-Version: 13
Sec-WebSocket-Version: 8, 7

客户端收到后,从中选择一个支持的版本,并重新开始握手

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
...
Sec-WebSocket-Version: 13

💐🎍Data Framing🎍💐 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

在 WebSocket 协议中,数据是通过帧序列传输的。为避免混淆网络代理(如拦截代理),并出于一些安全原因,客户端必须屏蔽它发送给服务器的所有帧,无论 WebSocket 协议是否通过 TLS 运行,都要进行屏蔽。服务器在收到未屏蔽的帧时必须关闭连接。

在这种情况下,服务器可以发送状态代码为 1002(协议错误)的关闭帧。服务器不得屏蔽发送给客户端的任何帧。如果检测到屏蔽帧,客户端必须关闭连接。在这种情况下,它可以使用第 7.4.1 节中定义的状态代码 1002(协议错误)。WS协议定义了一种带有操作码、有效载荷长度以及扩展数据应用数据 指定位置的帧类型,它们共同定义了 “有效载荷数据”。

客户端或服务器可以在握手完成后、端点发送关闭帧之前的任何时间内传输数据帧。

🎃🎼Base Framing Protocol🎼🎃

本节详细介绍的 ABNF [RFC5234]描述了数据传输部分的报文格式。与本文档其他部分不同,本节中的 ABNF 是以bit为单位的。每组比特的长度在注释中标明。

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 bit :表示这是报文的最后一个片段。 第一个片段也可能同时是最后一个。

  • RSV1, RSV2, RSV3 (分别为 1bit ) :必须为 0 ,除非协商的 extension 定义了非零值的语义;如果没有定义非零值的语义,那么如果收到非零值,则必须关闭连接。 (Fail the ws connection)

  • opcode 4b:操作码,用于定义载荷数据类型 (interpretation) ,如果端点收到未知的值,则必须关闭连接 (Fail the ws connection);这里定义了以下的值的意义(16进制):

    x0 :表示这是一个 接续帧 continuation frame

    x1 :表示这是一个 文本帧 text frame

    x2 :表示这是一个 二进制帧 binary frame

    x3-x7 :保留,用于未来定义的非控制帧(non-control frame)

    x8 :表示这是一个连接关闭帧 connection close

    x9 :表示这是一个 ping

    xA :表示这是一个 pong

    xB-xF :保留,用于定义控制帧 (control frame)

  • Mask 1b : 表示是否有效载荷数据 进行遮罩(Masking)。如果设置为 1,则 Masking-key字段中将包含一个掩码,该掩码用于解除对有效载荷数据的遮罩。所有从客户端发送到服务器的帧都将该位置 1。

  • Payload length 7b , 7+16b , 7+64b :有效载荷数据的长度(byte):

    • 如果为 0-125,则表示有效载荷长度。
    • 如果是 126,则以下 2 个字节解释为 16 位无符号整数,即为有效载荷长度。
    • 如果是 127,则以下 8 个字节解释为 64 位无符号整数(最显著位必须为 0),即为有效载荷长度。多字节长度以网络字节顺序表示。请注意,在任何情况下,都必须使用最少的字节数来编码长度,例如,124 字节长的字符串的长度不能编码为序列 126、0、124。扩展数据 "的长度可能为零,在这种情况下,有效载荷的长度就是 "应用数据 "的长度。
  • Masking-key 0b or 4b:从客户端发送到服务器的所有 frame 都会被一个包含在该帧中的 32 位 mask 进行遮罩。如果mask置为 1,则该字段存在;如果mask置 0,则该字段不存在。

  • Payload data x + y B : 有效载荷数据被定义为 “扩展数据” 与 "应用数据 "的拼接。

    • Extension data x bytes:如果没有协商任何扩展,则扩展数据总是为 0 个byte。如果协商了扩展,则必须指定数据长度;或者定义如何计算这个字段的长度。
    • Application data y bytes :任意长度的 “应用数据”,在任何 “扩展数据” 之后占用帧的剩余部分。应用数据 "的长度等于有效载荷长度减去 "扩展数据"的长度,所以不需要直接指明长度值。

需要注意的是,上述WS帧是二进制数据,而不是 ASCII 字符。因此,长度为 1 比特、值为 x0 / x1 的字段表示为单个比特,其值为 0 或 1,而不是 ASCII 编码中代表字符 "0 "或 "1 "的完整字节。

在这里,指定的编码是二进制编码,每个字段值都用指定的位数编码,每个字段的位数各不相同。

🎃🎼Client-to-Server Masking🎼🎃

掩码是客户端随机选择的 32 位值。在准备遮罩帧时,客户端必须从允许的 32 位值中选择一个新的掩码。掩码必须是不可预测的;因此,屏蔽密钥必须来自强大的熵源,而且给定帧的掩码不能让服务器/代理 轻易预测出后续帧的掩码。掩码的不可预测性对于防止恶意应用程序选择出现在网络上的字节至关重要。

屏蔽不影响 "有效载荷数据 "的长度 要将屏蔽数据转换为未屏蔽数据,或反之亦然,可采用以下算法。无论转换的方向如何,都采用相同的算法,例如,屏蔽数据的步骤与解除屏蔽数据的步骤相同。

🎃🎼分片🎼🎃

分片的主要目的是发送开始时允许发生大小未知的报文,而无需缓冲该报文。如果不能分片,那么端点就必须缓冲整个信息,以便在发送第一个字节前计算其长度。有了分片功能,服务器或代理可以选择一个合理大小的缓冲区,并在缓冲区满时向网络写入一个片段。

分片的另一个用例是多路复用,在这种情况下,一个逻辑通道上的较大报文不希望占用整个输出通道;因此多路复用需要自由地将报文分割成较小的片段,以便更好地共享输出信道。

除非扩展另有规定,否则帧没有语义。如果客户端和服务器没有协商任何扩展,或者协商了一些扩展,如果代理了解所有协商的扩展,并知道如何在存在这些扩展的情况下 拼接/分割 帧,那么代理就可以拼接/分割 帧。这意味着,在没有扩展的情况下,发送方和接收方不得依赖于特定帧边界的存在。

The following rules apply to fragmentation:

  • 未分片报文 FIN 位置1 并且 opcode 不是0组成。

  • 分片的消息帧 FIN 位置1opcode0 ,之后是相同情况的多个帧,最后该消息的最后一个帧, FIN 位置 1opcode仍然为 0

    从概念上讲,分片的多个帧组装成完整的消息,其有效载荷等于按顺序排列的片段有效载荷的总和;但是,在有扩展的情况下,这可能不成立,因为扩展定义了对 “扩展数据” 的特殊意义。例如,“扩展数据” 可能只出现在第一个片段的开头,并适用于随后的片段,也可能在每个片段中都有 “扩展数据”,但只适用于该特定片段。

  • 控制帧可以在分片报文中间插队;控制帧本身不得被分片。

  • 分片的消息必须按照其分片时的顺序发送给接收端

  • 一个报文的片段不得交错在另一个报文的片段之间,除非协商了用于标识不同消息的分片的扩展。

  • 端点必须能够处理分片报文中间的控制帧。

  • 发送方可为非控制报文创建任意大小的分片。

  • 客户端和服务器必须支持接收分片和未分片的报文。

  • 如果使用了任何保留位的值,而代理不知道这些值的含义,则代理不得更改报文的分片。

  • 若代理不知道已协商扩展的语义,则代理不得更改连接上下文中任何报文的分片。同样,如果代理没有看到导致 WebSocket 连接的 WebSocket 握手(也未被告知其内容),则代理不得更改此类连接的任何消息的片段。

根据这些规则,报文的所有片段都具有相同的类型,由第一个片段的操作码设定。由于控制帧不能被分片,因此报文中所有片段的类型必须是文本、二进制或一个保留操作码。

🎃🎼控制帧🎼🎃

目前,定义的控制帧opcode包括 0x8 Close0x9 Ping0xA Pong。操作码 0xB-0xF 保留给尚未定义的其他控制帧。控制帧用于交流 WebSocket 的状态。控制帧可以在分片报文中间插入。所有控制帧的有效载荷长度必须小于或等于 125 字节,并且不得分片。

Close

关闭帧包含一个 0x8 的操作码。关闭帧可能包含一个数据正文,表示关闭的原因,如端点关闭、端点收到的帧太大或端点收到的帧不符合端点预期的格式。

如果有正文,正文的前两个字节必须是一个 2 字节无符号整数(按网络字节顺序排列),这代表WS的状态码 status code。

在这两个字节整数之后,正文可能包含UTF-8 编码的数据,其值为 reason,本规范未定义其解释。这些数据不一定是人类可读的,但可能有助于调试或传递与打开连接的脚本相关的信息。由于无法保证数据的可读性,客户端不得将其显示给最终用户。

在发送和接收关闭消息后,端点会认为 WebSocket 连接已关闭,并必须关闭底层 TCP 连接。

服务器立即关闭底层 TCP 连接;客户端应等待服务器关闭连接,但也可以在发送和接收关闭消息后的任何时间关闭连接,例如,如果客户端在合理的时间内没有收到服务器的 TCP 关闭消息。

发送关闭帧后,应用程序不得再发送任何数据帧。

🎃🎼Keep-alive 机制🎼🎃

Ping 帧的操作码是0x9。Ping 帧可能包括 “应用数据”。端点收到 Ping 帧后,除非已经收到关闭帧,否则必须发送 Pong 帧作为回应。端点应在可行的情况下尽快响应 Pong 帧。端点可在连接建立后、连接关闭前的任何时间发送 Ping 帧。

Ping 帧既可作为保持更新,也可作为验证远程端点是否存活的参考。

Pong 帧包的操作码是 0xA 。为响应 Ping 帧而发送的 Pong 帧必须具有与被响应的 Ping 帧信息体中相同的 “应用数据”。如果端点接收到 Ping 帧,但尚未发送 Pong 帧来响应之前的 Ping 帧,则端点可选择只为最近处理的 Ping 帧发送 Pong 帧。

可以不经请求发送 Pong 帧。这是一种单向心跳。对非主动发送的 Pong 帧不作响应。

Ping-Pong 两种帧均属于控制帧

💐🌩️📫Sending and Receiving Data📫🌩️💐 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

要通过 WebSocket 连接发送由 data 组成的 WebSocket 消息,端点必须执行以下步骤。

  1. 端点必须确保 WebSocket 连接处于打开状态。如果 WebSocket 连接的状态在任何时候发生变化,则发送数据就止步于此,不会再进行下面步骤。
  2. 如果要发送的数据量较大,或在端点在开始发送数据时数据还未被完整构造,则发送端可以将现存数据封装成一系列的帧片段。
  3. 对文本或二进制数据,帧的操作码必须设置为适当值。
  4. 如果数据需要分片,则需要按照上述分片规则
  5. 如果数据来源于客户端,则客户端在发送前必须指定掩码

要接收 WebSocket 数据,端点必须监听底层网络连接。如果接收到控制帧,则必须按照规定处理该帧。接收到数据帧时,端点必须注意数据的类型,数据类型通过操作码定义。

如果该帧由未分片的报文组成,则表示已收到一个 WebSocket 报文。如果帧是片段报文的一部分,则后续数据帧的 “应用数据” 会被拼接起来。当收到 FIN 位置为的最后一个片段时,此时才表示已收到一个 WebSocket 报文,数据类型由分片报文的首个帧定义。随后收到的数据帧必须被解释为属于新的 WebSocket 报文。

💐🌩️📫Closing Connection📫🌩️💐 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

要关闭 WebSocket 连接,端点必须关闭底层 TCP 连接。端点应使用一种方法来干净利落地关闭 TCP 连接以及 TLS 会话,并丢弃可能已接收到的任何尾随字节。

端点可在必要时(如受到攻击时)要通过任何可用方法关闭连接。在大多数正常情况下,底层 TCP 连接应首先由服务器关闭,以便服务器而不是客户端保持 TIME WAIT 状态(因为这将阻止客户端在 2 个最大网段生命周期(2MSL)内重新打开连接,而服务器则不会受到相应影响,因为 TIME WAIT 连接会在新的序列号更高的 SYN 出现时立即重新打开)。

在异常情况下(如在合理的时间内未收到服务器的 TCP 关闭),客户端可以启动 TCP 关闭。因此,当服务器收到_关闭 WebSocket 连接_的指示时,应立即进入 TCP 关闭流程;而当客户端收到同样的指示时,它应等待服务器的 TCP 关闭。

🎋🍃Closing Handshake🍃🎋

在关闭时,报文的数据载荷要指定状态码,同时可以 添加一些说明,用于描述关闭原因 reason;同时,操作码置为x8 。不同端点的关闭行为不一,参考上述说明。

无论是发送还是接收关闭控制帧,都表示 WebSocket 关闭握手已开始 , WebSocket 连接处于关闭状态。当底层 TCP 连接关闭时,表示_WebSocket 连接已关闭_,WebSocket 连接处于关闭状态。如果 TCP 连接是在 WebSocket 关闭握手完成后关闭的,则表示 WebSocket 连接已_完全地_关闭。

根据Close 控制帧的定义,关闭控制帧可能包含一个状态代码,表示关闭的原因。WebSocket 连接的关闭可能由任一端点发起,也可能同时发起,此时 WebSocket 连接关闭代码就选择两个端点中,首先被收到的关闭帧的状态码。

根据Close 控制帧的定义,关闭控制帧可能包含一个状态代码,表示关闭的原因,后面是UTF-8 编码的数据,这些数据的解释由端点自行决定,本协议未作规定。WebSocket 连接的关闭可能由任一端点发起,也可能同时发起,此时 WebSocket 连接关闭代码就选择两个端点中,首先被收到的关闭帧的状态码

如果该关闭控制帧不包含状态代码,则 WS 关闭码就被视为 105。如果在没有收到任何关闭控制帧的情况下 (丢包,或者其他网络问题),WS 连接进入关闭状态, 则 WS 关闭码就被视为将被视为 1006

🎋🍃状态码🍃🎋

异常关闭可能由多种原因造成。这种关闭可能是瞬时错误造成的,在这种情况下,重新连接可能会导致连接良好并恢复正常运行。

异常关闭也可能是非瞬时性问题造成的,在这种情况下,如果每个已部署的客户机都出现异常关闭,并立即持续尝试重新连接,服务器就可能受到大量尝试重新连接的客户机的拒绝服务攻击。

这种情况的最终结果可能是服务无法及时恢复或恢复更加困难。为防止出现这种情况,客户端在异常关闭后尝试重新连接时,应使用某种形式的延迟。第一次重新连接尝试应随机延迟一段时间。选择随机延迟时间的参数由客户端决定;在 5 秒之间随机选择一个值是一个合理的初始延迟时间,但客户端可以根据实施经验和特定应用选择不同的延迟时间间隔。如果第一次重新连接尝试失败,随后的重新连接尝试应该使用截断二进制指数回退等方法,延迟时间越长越好。

关闭已建立的连接时(例如,在开局握手结束后发送关闭帧时),端点可以说明关闭的原因。本规范未定义端点对该原因的解释以及端点应根据该原因采取的行动。本规范定义了一组预定义的状态代码,并规定了扩展、框架和终端应用可使用的范围。状态代码和任何相关的文本信息都是关闭框架的可选组件。

端点在发送关闭帧时可以使用以下预定义的状态代码。

Status CodeDescription
1000表示正常关闭,即建立连接的目的已经达到
1001示端点正在 “离开” (Going away),如服务器宕机或浏览器已从页面导航离开
1003表示一个端点由于收到了它无法接受的数据类型(例如,一个只能理解文本数据的端点在收到二进制信息时可能会发送此信息)而终止连接
1004保留
1005保留,具体含义可能会在将来定义,不能在关闭控制帧中使用该状态码
1006保留,不能在关闭控制帧中使用该状态码。它被指定用于期望用状态码来表示连接异常关闭的应用程序,例如,在没有发送或接收关闭控制帧的情况下。
1007表示端点终止连接,因为它收到的报文数据与操作码类型不符,例如,文本报文中的非 UTF-8 数据
1008表示端点因收到违反其策略的消息而终止连接。这是一个通用状态代码,当没有其他更合适的状态代码(如 1003 或 1009)或需要隐藏有关策略的具体细节时,就会返回该代码。
1009表示端点正在终止连接,收到的报文太大,无法处理。
1010表示端点(客户端)终止连接,因为它希望服务器协商一个或多个扩展,但服务器没有在 WebSocket 握手的响应消息中返回这些扩展。需要的扩展列表应出现在关闭帧的 reason 部分;服务器不会使用此状态代码,因为它可能会导致 WebSocket 握手失败。
1011表示服务器正在终止连接,因为它遇到了意外情况,无法满足请求。
1015保留值,不能在关闭控制帧中使用该状态码;它被指定在应用程序中使用,该应用程序需要一个状态代码来表示由于服务器证书无法验证而导致 TLS 握手失败,连接已关闭。
0-999Status codes in the range 0-999 are not used.
1000-2999保留给本协议、其未来修订版以及在永久性和随时可用的公共规范中指定的扩展来定义。
3000-3999保留给代码库、框架和应用程序使用。这些状态代码直接在 IANA 注册。这里不给出解释或定义
4000-4999专用代码,因此不能注册。WebSocket 应用程序之间可通过事先协议使用这些代码。这里不给出解释或定义。

🎏🎐API🎐🎏 ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

Content From MDN WebSocket API

WebSocket 对象提供了用于创建和管理 WebSocket连接,以及可以通过该连接发送和接收数据的 API。使用 WebSocket() 构造函数来构造一个 WebSocket

let ws = new WebSocket("ws://tianqing/chat")

但是通常来说,我们可以直接给一个 WS URI 用于直接开启连接,构造函数的参数如下:

  • url:要连接的 URL。
  • protocols 可选一个协议字符串或者一个包含协议字符串的数组。这些字符串用于指定子协议,这样单个服务器可以实现多个 WebSocket 子协议(例如,你可能希望一台服务器能够根据指定的协议(protocol)处理不同类型的交互)。如果不指定协议字符串,则假定为空字符串。

该构造方法可能抛出异常:SECURITY_ERR正在尝试连接的端口被阻止。

这里使用 SpringMVC 后端搭建 WS 服务器,这里省略一些不太重要的部分,所以被简化为 伪代码

public void afterConnectionEstablished(WebSocketSession session) throws Exception {
       logger.info("WebSocket connection established");
       session.sendMessage(new TextMessage("我超,有人连上了!"));

    }

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
        Object payload = message.getPayload();
        if(message instanceof TextMessage) {
            logger.info("WebSocket  text message received: " + payload);
        } else if (message instanceof BinaryMessage) { //为了可读性
            logger.info("WebSocket binary message received" );
        }
    }

    @Override
    public void handleTransportError(...)  {
        logger.info("WebSocket transport error");
    }

    @Override
    public void afterConnectionClosed(....){
           logger.info("WebSocket connection closed");
    }

}

简单来说,在连接建立后 afterConnectionEstablished 在控制台输出WebSocket connection established 并向客户端通过 WS 发送文本信息 我超,有人连上了!,同时,当数据转发出错时,在连接关闭后, 均在控制台输出一些有关提示信息。

Note: 前端页面也必须在服务器上,不论是 NGiNX 或者是 VSCode 插件,亦或是直接放到 WS 服务器上。根据 🌸🌹Client Requirements🌹🌸 一节所述,如果客户端是浏览器,则必须带有有意义Origin 标头;如果不将页面放到服务器上,那么此个标头的值为 null 。判断客户端是否是浏览器的标准就是 User-Agent 标头。

后端的 ws 所在的服务器URI 为http://localhost:80/tianqing/chat

前端 js 代码如下

let ws = new WebSocket("ws://127.0.0.1:80/tianqing/chat")

        switch (ws.readyState) {
            case WebSocket.CONNECTING:
                // do something
                console.log('正在连接');
                
                break;
            case WebSocket.OPEN:
                // do something
                console.log('打开啦');
                
                break;
            case WebSocket.CLOSING:
                // do something
                console.log('正在关');
                
                break;
            case WebSocket.CLOSED:
                // do something
                console.log('Closed');
                
                break;
            default:
                // this never happens
                break;
        }
        ws.onopen = (e) => {
            console.log('打开啦');

        }
        ws.onmessage = (e) => {
            console.log('收到消息');
            console.log(e);

        }
        ws.send("这里是客户端")

注意, ,通过只读属性 readyState 判断WS 连接状态,不论switch执行时是什么状态,都对响应的状态输出debug 信息。最后,发送 这是客户端 给服务器。当然,也可以在浏览器运行后在控制台手动发送。

对于 message 事件,即从服务端收到消息,将事件源直接打印到控制台

 ws.onmessage = (e) => {
            console.log('收到消息');
            console.log(e);
        }

这里需要注意的是 跨源 问题,由于服务端使用的 URI 是 http://localhost:80 而我们建立 ws 所使用的URI是 ws://localhost:80,你当然也可以这样写

let ws = new WebSocket("http://localhost/tiangqing/chat")

但是在请求发送时仍然会自动使用 ws 作为 scheme ,虽然两种写法都可以,但是一定存在跨源问题。这一点可以通过浏览器的网络调试工具可以发现,WS 握手所使用的报文的 Sec-Fetch-Site 头部

GET ws://127.0.0.1/tianqing/chat
...
Sec-Fetch-Site: cross-site

所以,后端需要配置跨源以便我们进行操作。或者,将页面直接放到 WS 所在后端服务器上,建立 WS 连接时使用相对URI

let ws = new WebSocket("/tianqing/chat")

Spring WS CORS

在这里插入图片描述

内容符合预期,这里我给后端 WS 服务器配置了允许跨源 ;如果你使用的是相对URI 解决方式,则你看的的 Sec-Fetch-Site 的值应该是 same-site 。控制台情况如下

在这里插入图片描述

这里打印的是 onmessage 的事件源。之后,便可以调用 ws.send(...) 随意发送数据,这里欲图对一些特殊表情字符进行测试

ws.send("这里是客户端🪭🪭🥛")

显然是没有问题的

在这里插入图片描述

✨🎀二进制文件🎀✨

继续测试一波二进制文件,在页面中新定义 input:file 元素,在本地选择一个文件,然后通过 WS 推送到服务器端 ,input元素如下

<input type="file" onchange="fileHandler(this.files)">

事件处理函数如下

function fileHandler(files) {
  	let file = files[0]
    ws.send(file)
}

二进制文件可能因为过大而分片,所以,服务端需要配置 WS 消息允许分片,在 SpringMVC 这里,就是在实现函数 supportsPartialMessages 时返回 true

@Override
public boolean supportsPartialMessages() {
    return true;
}

如果分片,则客户端会将大片数据分配到若干小的报文中发送,所以,后端服务器将收到多个WS报文,此时,需要将二进制数据拼接起来,当收到的报文中 FIN 位置 1 ,则表示文件已经传输完毕,可以将数据写入到磁盘上,伪代码如下

private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 20);//类级字段


@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message){
    
    if(如果 message 是二进制数据)
        if(如果 FIN0)
        	将报文中的二进制数据拼接到 上述定义的 buffer 中
        else
            收到 FIN1 的报文,
            将最后一个报文的数据拼接到 buffer 后,
            
            将 buffer 中的数据写入到磁盘,然后根据🎃🎼分片🎼🎃一节中定义的,
            buffer.filp();
            // channel.write(buffer)
    
            将 buffer 重置,用于下一次使用
            buffer.clear()
}

需要强调的是,只有在数据被分片的情况下才会有 FIN0 的报文,否则FIN 永远为 1,详见🎃🎼分片🎼🎃 一节的定义。

如果需要获取文件名,则就需要搭配 RESTFul 或者其他方式;如果将文件名放在 URI 中,则就需要配合后端获取路径变量。

✨🎀WS 状态🎀✨

// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;

通过使用 ws.readyState 获得ws实例的当前状态,在之后调用任何 ws 实例的方法之前,都应该判断 ws 实例状态是否可以执行之后的方法。比如

if(ws.readyState === WebSocket.OPEN)
	ws.send(...)

✨🎀bufferedAmount🎀✨

WebSocket.bufferedAmount是一个只读属性,用于返回已经被send()方法放入队列但还没有被发送到网络中的数据的字节数。一旦队列中的所有数据被发送至网络,则该属性值将被重置为 0。但是,若在发送过程中连接被关闭,则属性值不会重置为 0。如果你不断地调用 send(),则该属性值会持续增长

An unsigned long

✨🎀extensions & protocol🎀✨

readonly attribute DOMString extensions;
readonly attribute DOMString protocol;

**WebSocket.extensions**是只读属性,返回服务器已选择的扩展值。目前,链接可以协定的扩展值只有空字符串或者一个扩展列表。

WebSocket.protocol 是个只读属性,用于返回服务器端选中的子协议的名字;这是一个在创建 WebSocket对象时,在参数 protocols 中指定的字符串,如果在实例创建时没有指定则该字段为空串。

✨🎀URI🎀✨

**WebSocket.url**是一个只读属性,返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径。

✨🎀事件🎀✨

  attribute EventHandler onopen;
  attribute EventHandler onerror;
  attribute EventHandler onclose;

**WebSocket.onopen**属性定义一个事件处理程序,当 WebSocket 的连接状态 readyState 变为 1 时调用;这意味着当前连接已经准备好发送和接受数据。这个事件处理程序通过 事件(建立连接时)触发。

WebSocket.onclose 属性返回一个事件监听器,这个事件监听器将在 WebSocket 连接的readyState 变为 CLOSED时被调用,它接收一个名字为“close”的 CloseEvent 事件。

ws.onclose = function(e){
    ...
}

CloseEvent 正是在 🎃🎼控制帧🎼🎃close 一节中的实现,其中 clsoeEvent.code 保存了 Closing Connection 一节中定义的状态码closeEvent.reason 保存的是 code 之后的 UTF-8 编码的数据。除此之外,closeEvent.wasClean 表示连接是否完全关闭,如 🎋🍃Closing Handshake🍃🎋 一节中所述,如果 TCP 连接是在 WebSocket 关闭握手完成后关闭的,则表示 WebSocket 连接已_完全地_关闭,wasClean 就为 true

CloseEvent 也继承了 Event

websocket的连接由于一些错误事件的发生 (例如无法发送一些数据) 而被关闭时,一个error事件将被引发。message 事件会在 WebSocket 接收到新消息时被触发。

✨🎀实例方法🎀✨

WebSocket.send() 方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的 data bytes 的大小来增加 bufferedAmount的值。若数据无法传输(例如数据需要缓存而缓冲区已满)时,ws会自行关闭。

send 支持三种参数类型

  • USVString :文本字符串。字符串将以 UTF-8 格式添加到缓冲区,并且 bufferedAmount 将加上该字符串以 UTF-8 格式编码时的字节数的值。
  • ArrayBuffer :以类型化数组对象形式发送底层二进制数据;其二进制数据内存将被缓存于缓冲区,bufferedAmount 将加上所需字节数的值。
  • Blob :Blob 类型将队列 blob 中的原始数据以二进制形式传输
  • ArrayBufferView :以二进制帧的形式发送任何 JavaScript 类数组对象

当连接未处于 OPEN 状态时,将抛出 INVALID_STATE_ERR 异常

WebSocket.close() 方法将尝试关闭 WS 连接。如果连接已经关闭,则此方法不执行任何操作。支持两个参数

  • code :一个数字状态码,它解释了连接关闭的原因。如果没有传这个参数,默认使用 1005。参见💐🌩️📫Closing Connection📫🌩️💐 一节
  • reason :一个人类可读的字符串,它解释了连接关闭的原因。这个 UTF-8 编码的字符串不能超过 123 个字节。参见💐🌩️📫Closing Connection📫🌩️💐 一节

如果 code 无效,未被 RFC 标准定义,则抛出 INVALID_ACCESS_ERR 异常,如果 reason 字符串超过 123 字节,则抛出 SYNTAX_ERR 异常。

  • 9
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天晴丶SnowCrystal

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值