本文为 WebSocket 协议的第七章,本文翻译的主要内容为 WebSocket 连接关闭相关内容。
要关闭 WebSocket 连接
,终端需要关闭底层的 TCP 连接。终端需要使用一个方法来干净的关闭TCP连接,还有 TLS 会话,如果可能的话,抛弃后面可能受到的任意字符。终端可能会在需要的时候,通过任何方式来关闭连接,例如在收到攻击时。
在底层的 TCP 连接中,通常大多数情况下,服务端应该先关闭,所以是服务端而不是客户端保持 TIME_WAIT 状态(因为客户端先关闭的话,这会阻止服务端在2 MSL 内重新打开这条连接,而如果服务器处于 TIME_WAIT 状态下,如果收到了一个带有更大序列号的新的 SYN 包时,也能够立即响应重新打开连接,从而不会对服务器产生影响)。反常情况(例如在合理的时间后,服务端收到一个 TCP 关闭包)下,客户端应该开始关闭 TCP 连接。像这样的,当服务端进入关闭 WebSocket 连接
状态时,它应该立刻准备关闭 TCP 连接,然后当客户端客户端准备关闭连接时,他应该等待服务端的 TCP 关闭包。
用 C 语言的 Berkeley socket 作为例子来展示如何彻底的关闭连接,一端需要用 SHUP_WR
调用 shutdown()
方法,调用 recv()
直到获得一个值为 0 的表示对面也准备有序关闭连接的返回值,然后最后调用 close()
来关闭 socket 通道。
用一个状态码 code
(第 7.4 节)和一个可选的关闭原因 reason
(第 7.1.6 节)来开始 WebSocket 关闭握手
,终端必须发送一个在第 5.5.1 节中描述的一样的关闭帧,将状态码设置为 code
字段,将关闭原因设置为 reaons
字段。一旦终端已经发送和收到了关闭控制帧,那么终端应该像第 7.1.1 节中定义的一样关闭 WebSocket 连接
。
在发送或者收到了关闭帧时,我们可以说已经开始 WebSocket 关闭握手
,并且 WebSocket 连接的状态已经到了“关闭中”(CLOSING)状态。
当底层的 TCP 连接关闭后,我们可以说WebSocket 连接已关闭
,并且 WebSocket 连接已经到了”关闭“(CLOSED)状态。如果 TCP 连接在 WebSocket 关闭握手完成之后已经关闭,那么我们可以说 WebSocket 连接已经被彻底
关闭。
如果 WebSocket 连接没有被建立,我们也说WebSocket已经关闭
,但是不彻底
。
就像在第 5.5.1 和第 7.4 节中定义的一样,关闭帧可以包含一个关闭的状态码和指定的原因。WebSocket 连接的关闭可能是同时由另一个终端发起。WebSocket 关闭状态码
是在第 7.4 节中定义的在第一关闭帧中的由实现该协议的应用程序接收的状态码。如果关闭帧中没有包含状态码,WebSocket 关闭状态码
被默认为1005。如果WebSocket 已经关闭
并且终端没有收到任何的关闭帧(例如发生了可能底层的传输连接突然丢失的情况),那么WebSocket 关闭状态码
被默认为1006。
注:两个终端可能没有就WebSocket 关闭状态码
的值达成一致。例如:如果远端发送一个关闭帧,但是本地应用没有从它的 socket 缓冲区中读到关闭帧的数据,同时本地应用单独的决定关闭连接并且发送了一个关闭帧,那么两个终端都发送了并且会收到一个关闭帧,同时不会发送更多的关闭帧。每一个终端会看到另一个终端发送过来的WebSocket 关闭状态码
的状态码。像这样的,在这个示例里面,有可能两个终端都没有协商过WebSocket 关闭状态码
,两个终端都几乎在同一时间单独开始 WebSocket 关闭握手
。
像第 5.5.1 节和第 7.4 节中定义的一样,一个关闭帧可能包含一个用于关闭的表示原因的状态码,然后是 UTF-8 编码的数据,数据的解析方式是留给终端来解释,而不在这个协议中定义。一个正在关闭中的 WebSocket 连接可能是同时从另一端开始的。WebSocket 连接关闭原因
是实现了该协议的应用收到的紧跟在状态码(第 7.4 节)之后的包含在第一个关闭控制帧中的 UTF-8 编码数据。如果在关闭控制帧中没有这些数据,那么WebSocket 连接关闭原因
的值就是一个空字符串。
注:和在第 7.1.5 中被提到的逻辑一样,两个终端可能没有协商过WebSocket 连接关闭原因
。
某些算法和规范要求终端有WebSocket 连接失效
。为了实现这些,客户端必须关闭 WebSocket 连接
,并且可以用一个合适的方式向用户上报相关问题(尤其是对开发者有帮助的内容)。相似的,为了实现这个,服务端必须关闭 WebSocket 连接
,并且应该用日志记录这个问题。
如果在此之前WebSocket 已经建立连接
,此时终端需要让WebSocket 连接失效
,那么在进行关闭 WebSocket 连接
之前,终端需要发送一个包含恰当的状态码(第 7.4 节)。终端在确认另一端没有能力接收或者处理关闭帧时,可能会选择省略发送关闭帧,从而在一开始就进入正常错误流程导致 WebSocket 连接关闭。终端在接到WebSocket 连接失效
的指令后,不能继续尝试处理来自另一端的数据(包括响应的关闭帧)。
除了上面说到的场景和应用层指定的场景(例如:脚本使用了 WebSocket 的 API)外,客户端不应该关闭连接。
在开始握手中的某些特定算法,需要客户端让WebSocket 连接失效
。为了实现这些,客户端必须像第 7.1.7 节中定义的一样让WebSocket 连接失败。
如果任意一端底层的传输连接意外丢失,客户端必须让WebSocket 连接失败
。
除了上面指定的情况和应用层的约束(例如,脚本使用了 WebSocket 的 API)外,客户端不应该关闭连接。
在开始监建立连接握手时,有些算法要求或者推荐服务端终端 WebSocket 连接
。为了实现这些,服务端必须关闭 WebSocket 连接
(第 7.1.1 节)。
导致异常关闭的原因有很多。例如是由于一个临时的错误导致的关闭,在这种情况下能够恢复就能够带来一个稳定的连接,恢复正常的操作。有些问题也有可能是一个非临时的问题导致的,在这种情况下如果每个客户端都遇到了异常的关闭,客户端立刻重试连接并且不间断情况下,服务端可能会收到由于大量客户端重新连接带来的拒绝服务攻击。最终的结果就是这个方案可能会导致服务没有办法及时的恢复,或者让服务恢复变得困难的多。
为了避免这个问题,客户端应该在异常终端尝试恢复连接时,使用在这一节中定义的一些备选策略。
第一次尝试恢复连接应该在一个随机长度时间后。随机事件的参数如何选择,这个交给客户端来决定;选择 0 到 5 秒之间的随机值是一个合理的初始延时,但是客户端可以根据自己的经验和特定的应用来选择不同长度的时间延时。
如果第一次重试连接失败,接下来的连接的延时应该变大,使用如截断二进制指数退避方法(译者注:解决以太网碰撞算法,见截断二进制质数退避算法)等来进行设置这个延时。
服务端可以在任意需要时关闭 WebSocket 连接。客户端不应该任意关闭 WebSocket 连接。在任一情况中,终端要发起关闭都必须遵循开始 WebSocket 连接关闭
的步骤。
当关闭一个连接时(如:在开始握手已经完成后,发送一个关闭帧),终端可能会说明关闭的原因。终端的这个原因的描述和终端应该采取的行动,在这个文档中都没有说明。这个文档提前定义了一些可能用于扩展、框架和终端应用的状态码和状态码范围。这些状态码和任何有关联的的文本消息在关闭帧中都是可选的。
在发送一个关闭帧时,终端可以提前定义如下的状态码。
1000
1000 表示一个正常的关闭,意味着连接建立的目标已经完成了。
1001
1001 表示终端已经“走开”,例如服务器停机了或者在浏览器中离开了这个页面。
1002
1002 表示终端由于协议错误中止了连接。
1003
1003 表示终端由于收到了一个不支持的数据类型的数据(如终端只能怪理解文本数据,但是收到了一个二进制数据)从而关闭连接。
1004
保留字段。这意味着这个状态码可能会在将来被定义。
1005
1005 是一个保留值并且不能被终端当做一个关闭帧的状态码。这个状态码是为了给上层应用表示当前没有状态码。
1006
1006 是一个保留值并且不能被终端当做一个关闭帧的状态码。这个状态码是为了给上层应用表示连接被异常关闭如没有发送或者接受一个关闭帧这种场景的使用而设计的。
1007
1007 表示终端因为收到了类型不连续的消息(如非 UTF-8 编码的文本消息)导致的连接关闭。
1008
1008 表示终端是因为收到了一个违反政策的消息导致的连接关闭。这是一个通用的状态码,可以在没有什么合适的状态码(如 1003 或者 1009)时或者可能需要隐藏关于政策的具体信息时返回。
1009
1009 表示终端由于收到了一个太大的消息无法进行处理从而关闭连接。
1010
1010 表示终端(客户端)因为预期与服务端协商一个或者多个扩展,但是服务端在 WebSocket 握手中没有响应这个导致的关闭。需要的扩展清单应该出现在关闭帧的原因(reason)
字段中。
1001
1001 表示服务端因为遇到了一个意外的条件阻止它完成这个请求从而导致连接关闭。
1015
1015 是一个保留值,不能被终端设置到关闭帧的状态码中。这个状态码是用于上层应用来表示连接失败是因为 TLS 握手失败(如服务端证书没有被验证过)导致的关闭的。
0-999
0-999 的状态码都没有被使用。
1000-2999
1000-2999 的状态码是在这个文档、将来的修订和扩展中定义的保留字段,用于永久的可用的公共文档。
3000-3999
3000-3999 的状态码是保留给库、框架和应用使用的。这些状态码被IANA直接注册了。这些状态码在这篇文档中没有进行解释。
4000-4999
40000-4999 的状态码是保留下来私用的,因此这些状态码不能被注册。这些状态码可以使用在 WebSocket 应用之前的协议上。这些状态码在这篇文档中没有进行解释。