因为历史缘由,在建立一个具备双向通讯机制的 web 应用程序时,须要利用到 HTTP 轮询的方式。围绕轮询产生了 “短轮询” 和 “长轮询”。html
浏览器赋予了脚本网络通讯的编程接口 XMLHttpRequest
,以及定时器接口 setTimeout
。所以,客户端脚本能够每隔一段时间就主动的向服务器发起请求,询问是否有新的信息产生:jquery
使用短轮询的方式有一个缺点,因为客户端并不知道服务器端什么时候会产生新的消息,所以它只有每隔一段时间不停的向服务器询问 “有新信息了吗”。而长轮询的工做方式能够是这样:web
能够看到 “长轮询” 相较于 “短轮询” 能够减小大量无用的请求,而且客户端接收到新消息的时机将会有可能提早。算法
咱们知道 HTTP 协议在开发的时候,并非为了双向通讯程序准备的,起初的 web 的工做方式只是 “请求-返回” 就够了。编程
可是因为人们须要提升 web 应用程序的用户体验,以及 web 技术自己的便捷性 - 不须要另外的安装软件,使得浏览器也须要为脚本提供一个双向通讯的功能,好比在浏览器中作一个 IM(Instant Message)应用或者游戏。api
经过 “长、短轮询” 模拟的双向通讯,有几个显而易见的缺点:跨域
因而,须要一种能够在 “浏览器-服务器” 模型中,提供简单易用的双向通讯机制的技术,而肩负这个任务的,就是 WebSocket
浏览器
协议分为两部分:“握手” 和 “数据传输”。缓存
客户端发出的握手信息相似:安全
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
服务端回应的握手信息类式:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
客户端的握手请求由 请求行(Request-Line) 开始。客户端的回应由 状态行(Status-Line) 开始。请求行和状态行的生产式见 RFC2616。
首行以后的部分,都是没有顺序要求的 HTTP Headers。其中的一些 HTTP头 的意思稍后将会介绍,不过也可包括例子中没有说起的头信息,好比 Cookies 信息,见 RFC6265。HTTP头的格式以及解析方式见 RFC2616
一旦客户端和服务端都发送了它们的握手信息,握手过程就完成了,随后就开始数据传输部分。由于这是一个双向的通讯,因此客户端和服务端均可以首先发出信息。
在数据传输时,客户端和服务器都使用 “消息 Message” 的概念去表示一个个数据单元,而消息又由一个个 “帧 frame” 组成。这里的帧并非对应到具体的网络层上的帧。
一个帧有一个与之相关的类型。属于同一个消息的每一个帧都有相同的数据类型。粗略的说,有文本类型(以 UTF-8 编码 RFC3629)和二进制类型(能够表示图片或者其余应用程序所需的类型),控制帧(不是传递具体的应用程序数据,而是表示一个协议级别的指令或者信号)。协议中定义了 6 中帧类型,而且保留了 10 种类型为了之后的使用。
握手部分的设计目的就是兼容现有的基于 HTTP 的服务端组件(web 服务器软件)或者中间件(代理服务器软件)。这样一个端口就能够同时接受普通的 HTTP 请求或则 WebSocket 请求了。为了这个目的,WebSocket 客户端的握手是一个 HTTP 升级版的请求(HTTP Upgrade request):
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 终节点。
Host 头中的服务器名称可让客户端标识出哪一个站点是其须要访问的,也使得服务器得知哪一个站点是客户端须要请求的。
其他的头信息是用于配置 WebSocket 协议的选项。典型的一些选项就是,子协议选项 Sec-WebSocket-Protocol
、列出客户端支出的扩展 Sec-WebSocket-Extensions
、源标识 Origin
等。Sec-WebSocket-Protocol
子协议选项,是用于标识客户端想和服务端使用哪种子协议(都是应用层的协议,好比 chat 表示采用 “聊天” 这个应用层协议)。客户端能够在 Sec-WebSocket-Protocol
提供几个供服务端选择的子协议,这样服务端从中选取一个(或者一个都不选),并在返回的握手信息中指明,好比:
Sec-WebSocket-Protocol: chat
Origin
能够预防在浏览器中运行的脚本,在未经 WebSocket 服务器容许的状况下,对其发送跨域的请求。浏览器脚本在使用浏览器提供的 WebSocket 接口对一个 WebSocket 服务发起链接请求时,浏览器会在请求的 Origin
中标识出发出请求的脚本所属的源,而后 WebSocket 在接受到浏览器的链接请求以后,就能够根据其中的源去选择是否接受当前的请求。
好比咱们有一个 WebSocket 服务运行在 http://websocket.example.com
,而后你打开一个网页 http://another.example.com
,在个 another 的页面中,有一段脚本试图向咱们的 WebSocket 服务发起连接,那么浏览器在其请求的头中,就会标注请求的源为 http://another.example.com
,这样咱们就能够在本身的服务中选择接收或者拒绝该请求。
服务端为了告知客户端它已经接收到了客户端的握手请求,服务端须要返回一个握手响应。在服务端的握手响应中,须要包含两部分的信息。第一部分的信息来自于客户端的握手请求中的 Sec-WebSocket-Key
头字段:
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
客户端握手请求中的 Sec-WebSocket-Key
头字段中的内容是采用的 base64 编码 RFC4648 的。服务端并不须要将这个值进行反编码,只须要将客户端传来的这个值首先去除首尾的空白,而后和一段固定的 GUID RFC4122 字符串进行链接,固定的 GUID 字符串为 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
。链接后的结果使用 SHA-1(160数位)FIPS.180-3 进行一个哈希操做,对哈希操做的结果,采用 base64 进行编码,而后做为服务端响应握手的一部分返回给浏览器。
好比一个具体的例子:
Sec-WebSocket-Key
头字段的值为 dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Key
字段的内容为 dGhlIHNhbXBsZSBub25jZQ==
,注意先后没有空白dGhlIHNhbXBsZSBub25jZQ==
和一段固定的 GUID 字符串进行链接,新的字符串为 dGhlIHNhbXBsZSBub25jZQ==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
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept
中。服务端的握手响应和客户端的握手请求很是的相似。第一行是 HTTP状态行,状态码是 101
:
HTTP/1.1 101 Switching Protocols
任何其余的非 101
表示 WebSocket 握手尚未结束,客户端须要使用原有的 HTTP 的方式去响应那些状态码。状态行以后,就是头字段。
Connection
和 Upgrade
头字段完成了对 HTTP 的升级。Sec-WebSocket-Accept
中的值表示了服务端是否接受了客户端的请求。若是它不为空,那么它的值包含了客户端在其握手请求中 Sec-WebSocket-Key
头字段所带的值、以及一段预约义的 GUID 字符串(上面已经介绍过怎么由两者合成新字符串的)。任何其余的值都被认为服务器拒绝了请求。服务端的握手响应相似:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
这些字符须要被 WebSocket 的客户端(通常就是浏览器)检查核对以后,才能决定是否继续执行相应的客户端脚本,或者其余接下来的动做。
可选的头字段也能够被包含在服务端的握手响应中。在这个版本的协议中,主要的可选头字段就是 Sec-WebSocket-Protocol
,它能够指出服务端选择哪个子协议。客户端须要验证服务端选择的子协议,是不是其当初的握手请求中的 Sec-WebSocket-Protocol
中的一个。做为服务端,必须确保选的是客户端握手请求中的几个子协议中的一个:
Sec-WebSocket-Protocol: chat
服务端也能够设置 cookie 见RFC6265,可是这不是必须的。
关闭握手的操做也很简单。
任意一端均可以选择关闭握手过程。须要关闭握手的一方经过发送一个特定的控制序列(第 5 节会描述)去开始一个关闭握手的过程。一端一旦接受到了来自另外一端的请求关闭控制帧后,接收到关闭请求的一端若是尚未返回一个做为响应的关闭帧的话,那么它须要先发送一个关闭帧。在接受到了对方响应的关闭帧以后,发起关闭请求的那一端就能够关闭链接了。
在发送了请求关闭控制序列以后,发送请求的一端将不能够再发送其余的数据内容;一样的,一但接收到了一端的请求关闭控制序列以后,来自那一端的其余数据内容将被忽略。注意这里的说的是数据内容,控制帧仍是能够响应的。不然就下面一句就没有意义了。
两边同时发起关闭请求也是能够的。
之因此须要这样作,是由于客户端和服务器之间可能还存在其余的中间件。一段关闭以后,也须要通知另外一端也和中间件断开链接。
WebSocket 协议的设计理念就是提供极小的帧结构(帧结构存在的目的就是使得协议是基于帧的,而不是基于流的,同时帧能够区分 Unicode 文本和二进制的数据)。它指望能够在应用层中使得元数据能够被放置到 WebSocket 层上,也就是说,给应用层提供一个将数据直接放在 TCP 层上的机会,再简单的说就能够给浏览器脚本提供一个使用受限的 Raw TCP 的机会。
从概念上来讲,WebSocket 只是一个创建于 TCP 之上的层,它提供了下面的功能:
从概念上将,就只有上述的几个用处。不过 WebSocket 能够很好的和 HTTP 协议一同协做,而且能够充分的利用现有的 web 基础设施,好比代理。WebSocket 的目的就是让简单的事情变得更加的简单。
协议被设计成可扩展的,未来的版本中将极可能会添加关于多路复用的概念。
WebSocket 协议使用源模型(origin model),这样浏览器中的一个页面中的脚本须要访问其余源的资源时将会有所限制。若是是在一个 WebSocket 客户端中直接使用了 WebSocet(而不是在浏览器中),源模型就没有什么做用,由于客户端能够设置其为任意的值。
而且协议的设计目的也是不但愿干扰到其余协议的工做,由于只有经过特定的握手步骤才能创建 WebSocket 链接。另外因为握手的步骤,其余已经存在的协议也不会干扰到 WebSocket 协议的工做。好比在一个 HTTP 表单中,若是表单的地址是一个 WebSocket 服务的话,将不会创建链接,由于到目前本文成文为止,在浏览器中是不能够经过 HTML 和 Javascript APIs 去设置 Sec-
头的。
WebSocket 是一个独立的基于 TCP 的协议,它与 HTTP 之间的惟一关系就是它的握手请求能够做为一个升级请求(Upgrade request)经由 HTTP 服务器解释(也就是可使用 Nginx 反向代理一个 WebSocket)。
默认状况下,WebSocket 协议使用 80 端口做为通常请求的端口,端口 443 做为基于传输加密层连(TLS)RFC2818 接的端口
由于 WebSocket 服务一般使用 80 和 443 端口,而 HTTP 服务一般也是这两个端口,那么为了将 WebSocket 服务和 HTTP 服务部署到同一个 IP 上,能够限定流量从同一个入口处进入,而后在入口处对流量进行管理,概况的说就是使用反向代理或者是负载均衡。
在使用 WebSocket 协议链接到一个 WebSocket 服务器时,客户端能够指定其 Sec-WebSocket-Protocol
为其所指望采用的子协议集合,而服务端则能够在此集合中选取一个并返回给客户端。
这个子协议的名称应该遵循第 11 节中的内容。为了防止潜在的冲突问题,应该在域名的基础上加上服务组织者的名称(或者服务名称)以及协议的版本。好比 v2.bookings.example.net
对应的就是 版本号-服务组织(或服务名)-域名
见原文 section-2
在这份技术说明中,定义了两种 URI 方案,使用 ABNF 语法 RFC 5234,以及 URI 技术说明 RFC3986 中的生产式。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] host = <host, defined in [RFC3986], Section 3.2.2> port = <port, defined in [RFC3986], Section 3.2.3> path = <path-abempty, defined in [RFC3986], Section 3.3> query = <query, defined in [RFC3986], Section 3.4>
端口部分是可选的;“ws” 默认使用的端口是 80,“wss” 默认使用的端口是 443。
若是资源标识符(URI)的方案(scheme)部分使用的是大小写不敏感的 “wss” 的话,那么就说这个 URI 是 “可靠的 secure”,而且说明 “可靠标记(secure flag)已经被设置”。
“资源名称 resource-name” 也就是 4.1 节中的 /resource name/
,能够按下面的部分(顺序)链接:
片断标识符(fragment identifier) “#” 在 WebSocket URIs 的上下文是没有意义的,不能出如今 URIs 中。在 WebSocket 的 URI 中,若是出现了字符 “#” 须要使用 %23 进行转义。
为了创建一个 WebSocket 链接,由客户端打开一个链接而后发送这一节中定义的握手信息。链接初始的初始状态被定义为 “链接中 CONNECTING
”。客户端须要提供 /host/,/port/,/resource name/ 和 /secure/ 标记,这些都是上一节中的 WebSocket URI 中的组件,若是有的话,还须要加上使用的 /protocols/ 和 /extensions/。另外,若是客户端是浏览器,它还须要提供 /origin/。
链接开始前须要的设定信息为(/host/, /port/, /resource name/ 和 /secure/)以及须要使用的 /protocols/ 和 /extensions/,若是在浏览器下还有 /origin/。这些设定信息选定好了以后,就必须打开一个网络链接,发送握手信息,而后读取服务端返回的握手信息。具体的网络链接应该如何被打开,如何发送握手信息,如何解释服务端的握手响应,这些将在接下来的部分讨论。咱们接下来的文字中,将使用第 3 节中定义的项目名称,好比 “/host/” 和 “/secure/”。
能够经过 /host/ 和 /port/ 这一对 URI 组件去标识一个 WebSocket 链接。这一部分的意思就是,若是能够肯定服务端的 IP,那么就使用 “服务端 IP + port” 去标识一个链接。这样的话,若是已经存在一个链接是 “链接中 CONNECTING
” 的状态,那么其余具备相同标识的链接必须等待那个正在链接中的链接完成握手后,或是握手失败后关闭了链接后,才能够尝试和服务器创建链接。任什么时候候只能有一个具备相同的标识的链接是 “正在链接中” 的状态。
可是若是客户端没法知道服务器的IP(好比,全部的链接都是经过代理服务器完成的,而 DNS 解析部分是交由代理服务器去完成),那么客户端就必须假设每个主机名称对应到了一个独立服务器,而且客户端必须对同时等待链接的的链接数进行控制(好比,在没法获知服务器 IP 的状况下,能够认为 a.example.com
和 b.example.com
是两台不一样的服务器,可是若是每台服务器都有三十个须要同时发生的链接的话,可能就应该不被容许)
注意:这就使得脚本想要执行 “拒绝服务攻击 denial-of-service attack” 变得困难,否则的话脚本只须要简单的对一个 WebSocket 服务器打开不少的链接就能够了。服务端也能够进一步的有一个队列的概念,这样将暂时没法处理的链接放到队列中暂停,而不是将它们马上关闭,这样就能够减小客户端重连的比率。
注意:对于客户端和服务器之间的链接数是没有限制的。在一个客户端请数目(根据 IP)达到了服务端的限定值或者服务端资源紧缺的时候,服务端能够拒绝或者关闭客户端链接。
使用代理:若是客户端但愿在使用 WebSocket 的时候使用代理的话,客户端须要链接到代理服务器并要求代理服务器根据其指定的 /host/,/port/ 对远程服务器打开一个 TCP 链接,有兴趣的能够看 Tunneling TCP based protocols through Web proxy servers。
若是可能的话,客户端能够首选适用于 HTTPS 的代理设置。
若是但愿使用 PAC 脚本的话,WebSocket URIs 必须根据第 3 节说的规则。
注意:在使用 PAC 的时候,WebSocket 协议是能够特别标注出来的,使用 “ws” 和 “wss”。
若是网络链接没法打开,不管是由于代理的缘由仍是直连的网络问题,客户端必须将链接动做标记为失败,并终止接下来的行为。
若是设置了 /secure/,那么客户端在和服务端创建了链接以后,必需要先进行 TLS 握手,TLS 握手成功后,才能够进行 WebSocket 握手。若是 TLS 握手失败(好比服务端证书不能经过验证),那么客户端必须关闭链接,终止其后的 WebSocket 握手。在 TLS 握手成功后,全部和服务的数据交换(包括 WebSocket 握手),都必须创建在 TLS 的加密隧道上。
客户端在使用 TLS 时必须使用 “服务器名称标记扩展 Server Name Indication extension” RFC6066
一旦客户端和服务端的链接创建好(包括经由代理或者经过 TLS 加密隧道),客户端必须向服务端发送 WebSocket 握手信息。握手内容包括了 HTTP 升级请求和一些必选以及可选的头字段。握手的细节以下:
握手必须是一个有效的 HTTP 请求,有效的 HTTP 请求的定义见 RFC2616
请求的方法必须是 GET
,而且 HTTP 的版本必须至少是 1.1
好比,若是 WebSocket 的 URI 是 ws://example.com/chat
,那么请求的第一行必须是 GET /chat HTTP/1.1
。
请求的 Request-URI
部分必须遵循第 3 节中定义的 /resource name/ 的定义。可使相对路径或者绝对路径,好比:
相对路径:GET /chat HTTP/1.1
中间的 /chat
就是请求的 Request-URI
,也是 /resource name/
绝对路径:GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1
,其中的 /resource name/ 就是 /pub/WWW/TheProject.html
感谢 @forl 的指正
绝对路径解析以后会有 /resource name/,/host/ 或者可能会有 /port/。/resource name/ 可能会有查询参数的,只不过例子中没有。
请求必须有一个 |Host| 头字段,它的值是 /host/ 主机名称加上 /port/ 端口名称(当不是使用的默认端口时必须显式的指明)
请求必须有一个 |Upgrade| 头字段,它的值必须是 websocket
这个关键字(keyword)
请求必须有一个 |Connection| 头字段,它的值必须是 Upgrade
这个标记(token)
请求必须有一个 |Sec-WebSocket-Key| 头字段,它的值必须是一个噪音值,由 16 个字节的随机数通过 base64 编码而成。每一个链接的噪音必须是不一样且随机的。
注意:做为一个例子,若是选择的随机 16 个字节的值是 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10
,那么头字段中的值将是 AQIDBAUGBwgJCgsMDQ4PEC==
若是链接来自浏览器客户端,那么 |Origin| RFC6454 就是必须的。若是链接不是来自于一个浏览器客户端,那么这个值就是可选的。这个值表示的是发起链接的代码在运行时所属的源。关于源是由哪些部分组成的,见 RFC6454。
做为一个例子,若是代码是从 http://cdn.jquery.com
下载的,可是运行时所属的源是 http://example.com
,若是代码向 ww2.example.com
发起链接,那么请求中 |Origin| 的值将是 http://example.com
。
请求必须有一个 |Sec-WebSocket-Version| 头字段,它的值必须是 13
请求能够有一个可选的头字段 |Sec-WebSocket-Protocol|。若是包含了这个头字段,它的值表示的是客户端但愿使用的子协议,按子协议的名称使用逗号分隔。组成这个值的元素必须是非空的字符串,而且取值范围在 U+0021 到 U+007E 之间,不能够包含定义在 RFC2616 的分隔字符(separator character),而且每一个以逗号分隔的元素之间必须相互不重复。
请求能够有一个可选的头字段 |Sec-WebSocket-Extensions|。若是包含了这个字段,它的值表示的是客户端但愿使用的协议级别的扩展,具体的介绍以及它的格式在第 9 节
请求能够包含其余可选的头字段,好比 cookies RFC6265,或者认证相关的头字段,好比 |Authorization| 定义在 RFC2616,它们的处理方式就参照定义它们的技术说明中的描述。
一旦客户端的握手请求发送完成后,客户端必须等待服务端的握手响应,在此期间不能够向服务器传输任何数据。客户端必须按照下面的描述去验证服务端的握手响应:
若是服务端传来的状态码不是 101,那么客户端能够按照通常的 HTTP 请求处理状态码的方式去处理。好比服务端传来 401 状态码,客户端能够执行一个受权验证;或者服务端回传的是 3xx 的状态码,那么客户端能够进行重定向(可是客户端不是非得这么作)。若是是 101 的话,就接着下面的步骤。
若是服务端回传的握手中没有 |Upgrade| 头字段或者 |Upgrade| 都字段的值不是 ASCII 大小写不敏感的 websocket
的话,客户端必须标记 WebSocket 链接为失败。
若是服务端回传的握手中没有 |Connection| 头字段或者 |Connection| 的头字段内容不是大小写敏感的 Upgrade
的话,客户端必须表示 WebSocket 链接为失败。
若是服务端的回传握手中没有 |Sec-WebSocket-Accept| 头字段或者 |Sec-WebSocket-Accept| 头字段的内容不是 |Sec-WebSocket-Key| 的内容(字符串,不是 base64 解码后的)联结上字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
的字符串进行 SHA-1 得出的字节再 base64 编码获得的字符串的话,客户端必须标记 WebSocket 链接为失败。
简单的说就是客户端也必须按照服务端生成 |Sec-WebSocket-Accept| 头字段值的方式也生成一个字符串,与服务端回传的进行对比,若是不一样就标记链接为失败的。
若是服务端回传的 |Sec-WebSocket-Extensions| 头字段的内容不是客户端握手请求中的扩展集合中的元素或者 null
的话,客户端必须标记链接为失败。这个头字段的解析规则在第 9 节中进行了描述。
好比客户端的握手请求中的指望使用的扩展集合为:
Sec-WebSocket-Extensions: bar; baz=2
那么服务端能够选择使用其中的某个(些)扩展,经过在回传的 |Sec-WebSocket-Extensions| 头字段中代表:
Sec-WebSocket-Extensions: bar; baz=2
上面的服务端返回表示都使用。也可使用其中的一个:
Sec-WebSocket-Extensions: bar
若是服务端但愿表示一个都不使用,即表示 null
,那么服务端回传的信息中将不能够包含 |Sec-WebSocket-Extensions|。
失败的界定就是,若是客户端握手请求中有 |Sec-WebSocket-Extensions|,可是服务端返回的 |Sec-WebSocket-Extensions| 中包含了客户端请求中没有包含的值,那么必须标记链接为失败。服务端的返回中不包含 |Sec-WebSocket-Extensions| 是能够的,表示客户端和服务端之间将不使用任何扩展。
若是客户端在握手请求中包含了子协议头字段 |Sec-WebSocket-Protocol|,其中的值表示客户端但愿使用的子协议的集合。若是服务端回传信息的 |Sec-WebSocket-Protocol| 值不属于客户端握手请求中的子协议集合的话,那么客户端必须标记链接为失败。
若是服务端的握手响应不符合 4.2.2 小节中的服务端握手定义的话,客户端必须标记链接为失败。
请注意,根据 RFC2616 技术说明,请求和响应的中全部头字段的名称都是大小写不敏感的(不区分大小写)。
若是服务端的响应符合上述的描述的话,那么就说明 WebSocket 的链接已经创建了,而且链接的状态变为 “OPEN 状态”。另外,服务端的握手响应中也能够包含 cookie 信息,cookie 信息被称为是 “服务端开始握手的 cookie 设置”。
WebSocket 服务器可能会卸下一些对链接的管理操做,而将这些管理操做交由网络中的其余代理,好比负载均衡服务器或者反向代理服务器。对于这种状况,在这个技术说明中,将组成服务端的基础设施的全部部分合起来视为一个总体。
好比,在一个数据中心,会有一个服务器专门用户响应客户端的握手请求,在握手成功以后将链接转交给实际处理任务的服务器。在这份技术说明中,服务端指代的就是这里的两台机器的组成的总体。
当客户端发起一个 WebSocket 请求时,它会发送握手过程种属于它那一部分的内容。服务端必须解析客户端提交的握手请求,以从中得到生成服务端响应内容的必要的信息。
客户端的握手请求有接下来的几部分构成。服务端在读取客户端请求时,发现握手的内容和下面的描述不相符(注意 RFC2616,头字段的顺序是不重要的),包括但不限于那些不符合相关 ABNF 语法描述的内容时,必须中止对请求的解析并返回一个具备适当的状态码 HTTP 响应(好比 400 Bad Request)。
必须是 HTTP/1.1 或者以上的 GET 请求,包含一个 “请求资源标识符 Request-URI”,请求资源标识符遵循第 3 节中定义的 /resource name/。
一个 |Host| 头字段,向服务器指明须要访问的服务名称(域名)
一个 |Upgrade| 头字段,值为大小写不敏感的 websocket
字符串
一个 |Connection| 头字段,它的值是大小写不敏感的字符串 Upgrade
一个 |Sec-WebSocket-Key| 头字段,它的值是一段使用 base64 编码Section 4 of [RFC4648] 后的字符串,解码以后是 16 个字节的长度。
一个 |Sec-WebSocket-Version| 头字段,它的值是 13.
可选的,一个 |Origin| 头字段。这个是全部浏览器客户度必须发送的。若是服务端限定只能由浏览器做为其客户端的话,在缺乏这个字段的状况下,能够认定这个握手请求不是由浏览器发起的,反之则不行。
可选的,一个 |Sec-WebSocket-Protocol| 头字段。由一些值组成的列表,这些值是客户端但愿使用的子协议,按照优先级从左往右排序。
可选的,一个 |Sec-WebSocket-Extensions| 头字段。有一些值组成的列表,这些值是客户端但愿使用的扩展。具体的表示在第 9 节。
可选的,其余头字段,好比那些用于向服务端发送 cookie 或则认证信息。未知的头字段将被忽略 RFC2616
当客户端对服务端创建了一个 WebSocket 链接以后,服务端必须完成接下来的步骤,以此去接受客户端的链接,并回应客户端的握手。
若是链接发生在 HTTPS(基于 TLS 的 HTTP)端口上,那么要执行一个 TLS 握手。若是 TLS 握手失败,就必须关闭链接;不然的话以后的全部通讯都必须创建在加密隧道上。
服务端能够对客户端执行另外的受权认证,好比经过返回 401 状态码和 对应的 |WWW-Authenticate|,相关描述在 RFC2616
服务端也能够对客户端进行重定向,使用 3xx 状态码 RFC2616。注意这一步也能够发生在上一步以前。
确认下面的信息:
/origin/ 客户端握手请求中的 |origin| 头字段代表了脚本在发起请求时所处的源。源被序列化成 ASCII 而且被转换成了小写。服务端能够选择性地使用这个信息去决定是否接受这个链接请求。若是服务端不验证源的话,那么它将接收来自任何地方的请求。
若是服务端不想接收这个链接的话,它必须返回适当的 HTTP 错误状态码(好比 403 Forbidden)而且终止接下来的 WebSocket 握手过程。更详细的内容,见第 10 节 /key/ 客户端握手请求中的 |Sec-WebSocket-Key| 头字段包含了一个使用 base64 编码后的值,若是解码的话,这个值是 16 字节长的。这个编码后的值用于服务端生成表示其接收客户端请求的内容。服务端没有必要去将这个值进行解码。 /version/ 客户端握手请求中的 |Sec-WebSocket-Version| 头字段包含了客户端但愿进行通讯的 WebSocket 协议的版本号。若是服务端不能理解这个版本号的话,那么它必须终止接下来的握手过程,并给客户端返回一个适当的 HTTP 错误状态码
(好比 426 Upgrade Required),同时在返回的信息中包含一个 |Sec-WebSocket-Version| 头字段,经过其值指明服务端可以理解的协议版本号。 /subprotocol/ 服务端能够选择接受其中一个子协议,或者 null。子协议的选取必须来自客户端的握手信息中的 |Sec-WebSocket-Protocol| 头字段的元素集合。若是客户端没有发送 |Sec-WebSocket-Protocol| 头字段,或者客户端发送的 |Sec-WebSocket-Protocol|
头字段中没有一个能够被当前服务端接受的话,服务端惟一能够返回值就是 null。不发送这个头字段就表示其值是 null。注意,空字符串并不表示这里的 null 而且根据 RFC2616 中的 ABNF 定义,空字符串也是不合法的。根据协议中的描述,客户端握手请求中的
|Sec-WebSocket-Protocol| 是一个可选的头字段,因此若是服务端必须使用这个头字段的话,能够选择性的拒绝客户端的链接请求。 /extensions/ 一个能够为空的列表,表示客户端但愿使用的协议级别的扩展。若是服务端支持多个扩展,那么必须从客户端握手请求中的 |Sec-WebSocket-Extensions| 按需选择多个其支持的扩展。若是客户端没有发送次头字段,则表示这个字段的值是 null,
空字符并不表示 null。返回的 |Sec-WebSocket-Extensions| 值中不能够包含客户端不支持的扩展。这个字段值的选择和解释将在第 9 节中讨论
若是服务端选择接受来自客户端的链接,它必须回答一个有效的 HTTP 响应:
一个状态行,包含了响应码 101。好比 HTTP/1.1 101 Switching Protocols 一个 |Upgrade| 头字段,值为 websocket 一个 |Connection| 头字段,值为 Upgrade 一个 |Sec-WebSocket-Accept| 头字段。这个值经过链接定义在 4.2.2 节中的第 4 步的 /key/ 和字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,链接后的字符串运用 SHA-1 获得一个 20 字节的值,
最后使用 base64 将这 20 个字节的内容编码,获得最后的用于返回的字符串。 相应的 ABNF 定义以下: Sec-WebSocket-Accept = base64-value-non-empty base64-value-non-empty = (1*base64-data [ base64-padding ]) | base64-padding base64-data = 4base64-character base64-padding = (2base64-character "==") | (3base64-character "=") base64-character = ALPHA | DIGIT | "+" | "/" 注意:做为一个例子,若是来自客户端握手请求中的 |Sec-WebSocket-Key| 的值是 dGhlIHNhbXBsZSBub25jZQ== 的话,那么服务端须要将 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 字符串追加到其后,
变成 dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11,再对这个链接后的字符串运用 SHA-1 哈希获得这些内容
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| 的字段值。 可选的,一个 |Sec-WebSocket-Protocol| 头字段,它的值已经在第 4.2.2 节中的第 4 步定义了 可选的,一个 |Sec-WebSocket-Extensions| 头字段,它的值已经在第4.2.2 节中的第 4 步定义了。若是有服务端选择了多个扩展,能够将它们分别放在 |Sec-WebSocket-Extensions| 头字段中,或者合并到一块儿放到一个
|Sec-WebSocket-Extensions| 头字段中。
这样就完成了服务端的握手。若是服务端没有发生终止的完成了全部的握手步骤,那么服务端就能够认为链接已经创建了,而且 WebSocket 链接的状态变为 OPEN。在这时,客户端和服务端就能够开始发送(或者接收)数据了。
这一节中将使用定义在 Section 2.1 of [RFC2616] ABNF 语法/规则,包括隐含的 LWS 规则(implied *LWS rule)。为了便于阅读,这里给出 LWS 的简单定义:任意数量的空格,水平 tab 或者换行(换行指的是 CR(carriage return) 后面跟着 LF(linefeed),使用转义字符表示就是 \r\n
)。
注意,接下来的一些 ABNF 约定将运用于这一节。一些规则的名称与与之对应的头字段相关。这些规则表示相应的头字段的值的语法,好比 Sec-WebSocket-Key ABNF 规则,它描述了 |Sec-WebSocket-Key| 头字段的值的语法。名字中具备 -Client
后缀的 ABNF 规则,表示的是客户端向服务端发送请求时的字段值语法;名字中具备 -Server
后缀的 ABNF 规则,表示的是服务端向客户端发送请求时的字段值语法。好比 ABNF 规则 Sec-WebSocket-Protocol-Client 描述了 |Sec-WebSocket-Protocol| 存在与由客户端发送到服务端的请求中的语法。
接下来新头字段能够在握手期间由客户端发往服务端:
Sec-WebSocket-Key = base64-value-non-empty Sec-WebSocket-Extensions = extension-list Sec-WebSocket-Protocol-Client = 1#token Sec-WebSocket-Version-Client = version base64-value-non-empty = (1*base64-data [ base64-padding ]) | base64-padding base64-data = 4base64-character base64-padding = (2base64-character "==") | (3base64-character "=") base64-character = ALPHA | DIGIT | "+" | "/" extension-list = 1#extension extension = extension-token *( ";" extension-param ) extension-token = registered-token registered-token = token extension-param = token [ "=" (token | quoted-string) ] ; When using the quoted-string syntax variant, the value ; after quoted-string unescaping MUST conform to the ; 'token' ABNF. NZDIGIT = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" version = DIGIT | (NZDIGIT DIGIT) | ("1" DIGIT DIGIT) | ("2" DIGIT DIGIT) ; Limited to 0-255 range, with no leading zeros
下面的新字段能够在握手期间由服务端发往客户端:
Sec-WebSocket-Extensions = extension-list Sec-WebSocket-Accept = base64-value-non-empty Sec-WebSocket-Protocol-Server = token Sec-WebSocket-Version-Server = 1#version
这一节对在客户端和服务端之间提供多个版本的 WebSocket 协议提供了一些指导意见。
使用 WebSocket 的版本公告能力(|Sec-WebSocket-Version| 头字段),客户端能够指明它指望的采用的协议版本(不必定就是客户端已经支持的最新版本)。若是服务端支持相应的请求版本号的话,则握手能够继续,若是服务端不支持请求的版本号,它必须回应一个(或多个) |Sec-WebSocket-Version| 头字段,包含全部它支持的版本。这时,若是客户端也支持服务端的其中一个协议的话,它就可使用新的版本号去重复客户端握手的步骤。
下面的例子能够做为上文提到的版本协商的演示:
客户端发送:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade ... Sec-WebSocket-Version: 25
服务端的返回看起来相似:
HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13, 8, 7
注意,服务器也能够返回下面的内容:
HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13 Sec-WebSocket-Version: 8, 7
客户端如今就能够从新采用版本 13 (若是客户端也支持的话)进行握手请求了:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade ... Sec-WebSocket-Version: 13
在 WebSocket 协议中,数据的传输使用一连串的帧。为了使得中间件不至于混淆(好比代理服务器)以及为了第 10.3 节将讨论安全缘由,客户端必须将要发送到服务端的帧进行掩码,掩码将在第 5.3 节详细讨论。(注意,无论 WebSocket 有没有运行在 TLS 之上,都必须有掩码操做)服务端一旦接收到没有进行掩码的帧的话,必须关闭链接。这种状况下,服务端能够发送一个关闭帧,包含一个状态码 1002(协议错误 protocol error),相关定义在 Section 7.4.1。服务端没必要对发送到客户端的任何帧进行掩码。若是客户端接收到了服务端的掩码后的帧,客户端必须关闭链接。在这个状况下,客户端能够向服务器发送关闭帧,包含状态码 1002(协议错误 protocol error),相关定义在 Section 7.4.1。(这些规则可能在未来技术说明中没有严格要求)
基础帧协议经过操做码(opcode)定义了一个帧类型,一个有效负荷长度,以及特定的位置存放 “扩展数据 Extension data” 和 “应用数据 Application data”,扩展数据和应用数据合起来定义了 “有效负荷数据 Payload data”。某些数位和操做码是保留的,为了未来的使用。
在客户端和服务端完成了握手以后,以及任意一端发送的关闭帧(在第 5.5.1 节介绍)以前,客户端能够和服务端均可以在任什么时候间发送数据帧。
这一节中将使用 ABNF 详细定义数据传输的格式。(注意,和这文档中的其余 ABNF 不一样,这一节中 ABNF 操做的是一组数位。每一组数位的长度将以注释的形式存在。当数据在网络中传输时,最高有效位是在 ABNF 的最左边(大端序))。下面的文本图像能够给出关于帧的一个高层概览。若是下面的文本插图和后的 ABNF 描述发送冲突时,以插图为准。
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: 各 1 个数位
必须是 0,除非有扩展赋予了这些数位非 0 值的意义。若是接收到了一个非 0 的值而且没有扩展赋予这些非 0 值的意义,那么接收端须要标记链接为失败。
操做码:4 个数位
定义了如何解释 “有效负荷数据 Payload data”。若是接收到一个未知的操做码,接收端必须标记 WebSocket 为失败。定义了以下的操做码:
%x0
表示这是一个继续帧(continuation frame)%x1
表示这是一个文本帧 (text frame)%x2
表示这是一个二进制帧 (binary frame)%x3-7
为未来的非控制帧(non-control frame)而保留的%x8
表示这是一个链接关闭帧 (connection close)%x9
表示这是一个 ping 帧%xA
表示这是一个 pong 帧xB-F
为未来的控制帧(control frame)而保留的掩码标识 Mask:1 个数位
定义了 “有效负荷数据” 是不是被掩码的。若是被设置为 1,那么在 masking-key
部分将有一个掩码钥匙(masking key),而且使用这个掩码钥匙去将 “有效负荷数据” 进行反掩码操做(第 5.3 节描述)。全部的由客户端发往服务端的帧此数位都被设置成 1。
有效负荷长度(Payload length): 七、7+16 或者 7+64 数位
表示了 “有效负荷数据 Payload data” 的长度,以字节为单位:若是是 0-125,那么就直接表示了负荷长度。若是是 126,那么接下来的两个字节表示的 16 位无符号整型数则是负荷长度。若是是 127,则接下来的 8 个字节表示的 64 位无符号整型数则是负荷长度。表示长度的数值的字节是按网络字节序(network byte order 即大端序)表示的。注意在全部状况下,必须使用最小的负荷长度,好比,对于一个 124 字节长度的字符串,长度不能够编码成 126,0,124。负荷长度是 “扩展数据 Extension data” 长度 + “应用数据Application data” 长度 。“扩展数据” 的长度能够是 0,那么此时 “应用数据” 的长度就是负荷长度。
掩码钥匙 Masking key:0 或者 4 个数位
全部由客户端发往服务端的帧中的内容都必须使用一个 32 位的值进行掩码。这个字段有值的时候(占 4 个数位)仅当掩码标识位设置成了 1,若是掩码标识位设置为 0,则此字段没有值(占 0 个数位)。对于进一步掩码操做,见第 5.3 节。
有效负荷数据 Payload data:(x+y) 字节 byte
“有效负荷数据” 的定义是 “扩展数据” 联合 “应用数据”。
扩展数据 Extension data: x 字节
“扩展数据是” 0 个字节的,除非协商了一个扩展。任何的扩展都必须提供 “扩展数据” 的长度或者该长度应该如何计算,以及在握手阶段如何使用 “扩展数据” 进行扩展协商。若是 “扩展数据” 存在,那么它的长度被包含在了负荷长度中。
应用数据 Application data: y字节
能够是任意的 “应用数据”,它在一个帧的范围内紧接着 “扩展数据”。“应用数据” 的长度等于负荷长度减去 “扩展数据” 的长度
基础帧协议经过接下来的 ABNF RFC5234 来定义其形式。一个重要的注意点就是下面的 ABNF 表示的是二进制数据,而不是其表面上的字符串。好比, %x0 和 %x1 各表示一个数位,数位上的值为 0 和 1,而不是表示的字符 “0” 和 “1” 的 ASCII 编码。RFC5234 没有定义 ABNF 的字符编码。在这里,ABNF 被特定了使用的是二进制编码,这里二进制编码的意思就是每个值都被编码成具备特定数量的数位,具体的数量因不一样的字段而异。
ws-frame = frame-fin ; 1 bit in length frame-rsv1 ; 1 bit in length frame-rsv2 ; 1 bit in length frame-rsv3 ; 1 bit in length frame-opcode ; 4 bits in length frame-masked ; 1 bit in length frame-payload-length ; either 7, 7+16, ; or 7+64 bits in ; length [ frame-masking-key ] ; 32 bits in length frame-payload-data ; n*8 bits in ; length, where ; n >= 0 frame-fin = %x0 ; more frames of this message follow / %x1 ; final frame of this message ; 1 bit in length frame-rsv1 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-rsv2 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-rsv3 = %x0 / %x1 ; 1 bit in length, MUST be 0 unless ; negotiated otherwise frame-opcode = frame-opcode-non-control / frame-opcode-control / frame-opcode-cont frame-opcode-cont = %x0 ; frame continuation frame-opcode-non-control= %x1 ; text frame / %x2 ; binary frame / %x3-7 ; 4 bits in length, ; reserved for further non-control frames frame-opcode-control = %x8 ; connection close / %x9 ; ping / %xA ; pong / %xB-F ; reserved for further control ; frames ; 4 bits in length frame-masked = %x0 ; frame is not masked, no frame-masking-key / %x1 ; frame is masked, frame-masking-key present ; 1 bit in length frame-payload-length = ( %x00-7D ) / ( %x7E frame-payload-length-16 ) / ( %x7F frame-payload-length-63 ) ; 7, 7+16, or 7+64 bits in length, ; respectively frame-payload-length-16 = %x0000-FFFF ; 16 bits in length frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF ; 64 bits in length frame-masking-key = 4( %x00-FF ) ; present only if frame-masked is 1 ; 32 bits in length frame-payload-data = (frame-masked-extension-data frame-masked-application-data) ; when frame-masked is 1 / (frame-unmasked-extension-data frame-unmasked-application-data) ; when frame-masked is 0 frame-masked-extension-data = *( %x00-FF ) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 frame-masked-application-data = *( %x00-FF ) ; n*8 bits in length, where n >= 0 frame-unmasked-extension-data = *( %x00-FF ) ; reserved for future extensibility ; n*8 bits in length, where n >= 0 frame-unmasked-application-data = *( %x00-FF ) ; n*8 bits in length, where n >= 0
一个被掩码的帧须要将掩码标识位(第 5.2 节定义)设置为 1。
掩码钥匙 masking key 整个都在帧中,就像第 5.2 节定义的。它用于对 “有效负荷数据” 进行掩码操做,包括 “扩展数据” 和 “应用数据”。
掩码钥匙由客户端随机选取一个 32 位的值。在每次准备对帧进行掩码操做时,客户端必须选择在可选的 32 位数值集合中选取一个新的掩码钥匙。掩码钥匙的值须要是不可被预测的;所以,掩码钥匙必须来源于一个具备很强保密性质的生成器,而且 服务器/代理 不可以轻易的预测到一连串的帧中使用的掩码钥匙。不可预测的掩码钥匙能够防止恶意程序在帧的传输过程当中探测到掩码钥匙的内容。RFC4086 具体讨论了为何对于一个安全性比较敏感的应用程序须要使用一个很强保密性质的生成器。
掩码不会影响 “有效负载数据” 的长度。为了将掩码后的数据进行反掩码,或者倒过来,可使用下面的算法。一样的算法适用于不一样方向发来的帧,好比,对于掩码和反掩码使用相同的步骤。
传输数据中的每 8 个数位的字节 i (transformed-octet-i),生成方式是经过原数据中的每 8 个数位的字节 i (original-octet-i)与以 i 与 4 取模后的数位为索引的掩码钥匙中的 8 为字节 j(masking-key-octet-j) 进行异或(XOR)操做:
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
负载的长度不包括掩码钥匙的长度,它是 “有效负载数据 Payload data” 的长度,好比,位于掩码钥匙后的字节的长度。
消息碎片化的目的就是容许发送那些在发送时不知道其缓冲的长度的消息。若是消息不能被碎片化,那么一端就必须将消息整个地载入内存缓冲,这样在发送消息前才能够计算出消息的字节长度。有了碎片化的机制,服务端或者中间件就能够选取其适用的内存缓冲长度,而后当缓冲满了以后就发送一个消息碎片。
碎片机制带来的另外一个好处就是能够方便实现多路复用。没有多路复用的话,就须要将一整个大的消息放在一个逻辑通道中发送,这样会占用整个输出通道。多路复用须要能够将消息分割成小的碎片,使这些小的碎片能够共享输出通道。(注意多路复用的扩展在这片文档中并无进行描述)
除非运用了特定的扩展,不然帧是没有特定的语义的。在客户端和服务端协商了某个扩展,或者客户端和服务端没有协商扩展的状况下,中间件都有可能将帧进行 合并/分隔。也就是说,在客户端和服务端没有协商某个扩展时,双方都不该该猜想帧与帧之间的边界。注:这里的某个扩展的意思就是赋予了帧特定的语义的扩展,好比多路复用扩展。
下面的规则解释了如何进行碎片化:
一个没有被碎片化的消息只包含一个帧,而且帧的 FIN 数位被设置为 1,且操做码 opcode 不为 0。
一个碎片化的消息包含了一个 FIN 未被置为 0 的帧,且这个帧的 opcode 不为 0,在这个帧以后,将有 0 个或者多个 FIN 为 0 且 opcode 为 0 的帧,最后以一个 FIN 为 1 和 opcode 为 0 的帧结束。对于一个碎片化后的消息,它的有效负荷就等于将碎片化后的帧的有效负荷按顺序链接起来;不过当存在扩展时,这一点就不必定正确了,由于扩展可能会设置帧的 “扩展数据”。在没有 “扩展数据” 的状况下,下面的例子演示了碎片化是如何工做的。
例子:对于一个以三个帧发送的文本消息,其第一个帧的 opcode 是 0x1 而且 FIN 位是 0,第二个帧的 opcode 是 0x0 且 FIN 位是 0,第三个帧的 opcode 是 0x0 且 FIN 位是 1。
控制帧(见第 5.5 节),可能会夹杂在消息帧之间。控制帧是不能被碎片化的。
消息帧必须以其被发送时的顺序传递到接收端。
不一样消息的消息帧之间不能够相互夹杂,除非协商了一个定义了如何解释这种夹杂行为的扩展。
发送端能够建立任意大小的非控制帧。
客户端和服务端必须支持发送和接受碎片化或者非碎片化的消息。
一个控制帧是不能够被碎片化的,中间件必须不能够试图将控制帧进行碎片化。
若是帧中使用了 RSV 数位,可是中间件不理解其中的任意的 RSV 数位 的值时,它必须不能够改变消息的原有的碎片化帧。
在中间件不能肯定客户端和服务端进行了哪些扩展协商的状况下,中间件必须不能够修改原有的碎片化帧。
最后,组成消息的全部帧都是相同的数据类型,在第一个帧中的 opcode 中指明。由于控制帧不能被碎片化,组成消息的碎片类型必须是文本、二进制、或者其余的保留类型。
注意:若是控制帧不能夹杂在消息帧的话,那么将致使 ping 的结果产生延迟,好比在处理了一个很是长的消息后才响应 ping 控制帧时。所以,要求在处理消息帧的期间能够响应控制帧。
重点注意:在没有扩展的状况下,接收端为了处理消息不是非得缓冲全部的帧。好比若是使用了 流API (streaming API),数据帧能够直接传递给应用层。不过这样假设并不必定在全部的扩展中都适用。
控制帧是经过它的 opcode 的最高有效位是 1 去肯定的。当前已经定义了的控制帧包括 0x8 (close)
,0x9 (Ping)
,0xA (Pong)
。操做码 0xB-0xF
是为未来的控制帧保留的,目前还没有定义。
控制帧是为了在 WebSocket 中通讯链接状态。控制帧能够夹杂在消息帧之间发送。
全部的控制帧的负载长度都必须是 125 字节,而且不能被碎片化。
关闭帧的操做码 opcode 是 0x8
。
关闭帧能够包含消息体(经过帧的 “应用数据” 部分)去表示关闭的缘由,好比一端正在关闭服务,一端接收到的帧过大,或者一端接收到了不遵循格式的帧。若是有消息体的话,消息体的前两个字节必须是无符号的整型数(采用网络字节序),以此整型数去表示状态码 /code/ 定义在第 7.4 节。在两个字节的无符号整型数以后,能够跟上以 UTF-8 编码的数据表示 /reason/,/reason/ 数据的具体解释方式此文档并无定义。而且 /reason/ 的内容不必定是人类可读的数据,只要是有利于发起链接的脚本进行调试就能够。由于 /reason/ 并不必定就是人类可读的,因此客户端必须不将此内容展现给最终用户。
客户端发送的每个帧都必须按照第 5.3 节中的内容进行掩码。
应用程序在发送了关闭帧以后就不能够再发送其余数据帧了。
若是接收到关闭帧的一端以前没有发送过关闭帧的话,那么它必须发送一个关闭帧做为响应。(当发送一个关闭帧做为响应的时候,发送端一般在做为响应的关闭帧中采用和其接收到的关闭帧相同的状态码)。而且响应必须尽快的发送。一端能够延迟关闭帧的发送,好比一个重要的消息已经发送了一半,那么能够在消息的剩余部分发送完以后再发送关闭帧。可是做为首先发送了关闭帧,并在等待另外一端进行关闭响应的那一端来讲,并不必定保证其会继续处理数据内容。
在发送和接收到了关闭帧以后,一端就能够认为 WebSocket 链接已经关闭,而且必须关闭底层相关的 TCP 链接。若是是服务端首先发送了关闭帧,那么在接收到客户端返回的关闭帧以后,服务端必须当即关闭底层相关的 TCP 链接;可是若是是客户端首先发送了关闭帧,并接收到了服务端返回的关闭帧以后,能够选择其认为合适的时间关闭链接,好比,在一段时间内没有接收到服务端的 TCP 关闭握手。
若是客户端和服务端同时发送了关闭消息,那么它们两端都将会接收到来自对方的关闭消息,那么它们就能够认为 WebSocket 链接已经关闭,而且关闭底层相关的 TCP 链接。
Ping 帧的操做码是 0x9
Ping 帧也能够有 “应用数据”
一旦接收到了 Ping 帧,接收到的一端必须发送一个 Pong 帧做为响应,除非它已经接收到了关闭帧。响应的一端必须尽快的作出响应。Pong 帧定义在第 5.5.3 节。
一端能够在链接创建以后,到链接关闭以前的任意时间点发送 Ping 帧。
注意:Ping 帧的目的能够是保持链接(keepalive)或者是验证服务端是否仍是有响应的。
Pong 帧的操做码是 0xA
第 5.5.2 节的要求同时适用于 Ping 帧和 Pong 帧。
Pong 帧的 “应用数据” 中的内容必须和其响应的 Ping 帧中的 “应用数据” 的内容相同。
若是一端接收到了 Ping 帧而且在没有来得及响应的时候又接收到了新的 Ping 帧,那么响应端能够选择最近的 Ping 帧做为响应的对象。
Pong 帧能够在未被主动请求的状况下发送给对方。这被认为是单向的心跳包。单向心跳包是得不到响应的。
数据帧(好比,非控制帧)是经过操做码的最高有效位是 0 来肯定的。当前已经定义的数据帧包括 0x1 (文本)
,0x2 (二进制)
。操做码 0x3-0x7
是为了未来的非控制帧的使用而保留的。
数据帧承载了 “应用层 application-layer” 或者 “扩展层 extension-layer” 的数据。操做码决定了数据的表现形式。
文本 Text
“有效负载数据 Payload data” 是以 UTF-8 编码的文本。注意,做为整个文本消息的一部分的部分文本帧可能包含了部分的 UTF-8 序列;可是整个的消息的内容必须是一个有效的 UTF-8 序列。对于无效的 UTF-8 消息的处理在第 8.1 节中描述。
二进制 Binary
“有效负荷数据 Payload data” 是仅由应用层来决定的任意二进制内容。
一个单个帧的没有进行掩码的文本消息
一个单个帧的掩码后的消息
一个碎片化的没有掩码的文本消息
没有掩码的 Ping 请求和其掩码后的响应
256 个字节的二进制消息,使用单个未掩码的帧
64 Kb 的二进制消息,使用单个未掩码的帧
协议被设计为容许扩展,扩展能够在基础协议的功能上添加更多的功能。通讯双方必须在握手期间完成扩展的协商。在这份技术说明中,为扩展提供使用的部分为:操做码 0x3 到 0x七、以及 0xB 到 0xF,“扩展数据 Extension data” 字段,frame-rsv一、frame-rsv二、frame-rsv3 这三个位于帧头部的数位。关于扩展协商的详细在第 9.1 节中讨论。下面的列表是关于扩展的预期使用形式,不过它既不完整也不规范:
为了在 WebSocket 链接上发送由 /data/ 组成的 WebSocket 消息,发送端必须按下的步骤去执行:
发送端必须肯定当前的 WebSocket 链接的状态是 OPEN(见 第 4.1 和 4.2 节)。在任什么时候间点,若是链接的状态改变了,那么发送端必须终止下面的步骤。
发送端必须使用 WebSocket 帧将 /data/ 按第 5.2 节中描述的形式包裹起来。若是数据太大,或者在发送时不能整个地获取需发送数据,那么发送端能够按照第 5.4 节中描述的,将数据分割成一连串的帧进行发送。
包含数据的第一个帧的操做码必须设置为适当的数据类型,以便接收端能够肯定用文本仍是二进制来解释其接收到的数据,数据类型定义在第 5.2 节。
在消息的最后一个包含数据的帧中必须将其 FIN 设置为 1,相关定义在第 5.2 节。
若是数据是由客户端发送的,那么数据在发送前必须按照第 5.2 中定义的方式进行掩码。
若是链接中进行了扩展协商,那么额外的扩展相关的处理将会应用到帧上。
帧必须经由 WebSocket 底层相关的网络链接发送。
为了接收 WebSocket 数据,接收端必须监听底层相关的网络链接。接收到的数据必须按照第 5.2 节中定义的格式进行解析。若是接收到的是一个控制帧,那么必须按照第 5.5 节中的定义去处理。一旦接收到第 5.6 节中定义的数据帧,接收端必须注意数据帧的类型 /type/,这点根据帧的 opcode,定义在第 5.2 节。“应用数据 Application data” 被定义为消息的数据 /data/。若是帧是一个没有被碎片化的帧,定义在第 5.4 节,那么就说明一个消息已经被彻底接收了,即知道了其类型 /type/ 和数据 /data/。若是帧是碎片化消息的一部分,那么其随后的帧的 “应用数据” 链接在一块儿组成消息的数据 /data/。当最后一个碎片化的帧被接收时,也就是帧的 FIN 位为 1 时,代表一个 WebSocket 消息已经被彻底接收了,其数据 /data/ 就是全部相关碎片化的帧的 “应用数据” 链接到一块儿的值,而 /type/ 就是第一个或者其余组成消息的碎片化帧的操做码。以后的帧必须被解释为属于一个新的消息。
扩展(第 9 节)可能会更改数据被读取的方式,特别是如何界定消息之间的边界。扩展在有效负荷中的 “应用数据” 以前添加的 “扩展数据” 也可能会修改 “应用数据” 的内容(好比进行了压缩)。
服务端必须未来自客户端的帧进行反掩码,操做定义在第 5.3 节。
为了关闭 WebSocket 链接,一端能够关闭底层的 TCP 链接。一端在关闭链接的时候必须干净的关闭,好比 TLS 会话,尽量的丢弃全部已经接收可是还没有处理的字节。一端能够在须要的时候以任意的理由去关闭链接,好比在收到攻击时。
底层的 TCP 链接,在通常状况下应该由服务端先进行关闭,而客户端则须要在一段时间内等待服务端的 TCP 关闭,若是超过了客户端的等待时间,客户端则能够关闭 TCP 链接。
一个以使用 Berkeley sockets 的 C 语言的例子演示如何干净的关闭链接:首先一端须要调用对 socket 调用 shutdown() 函数,并以 SHUT_WR 为函数的参数,而后调用 recv() 函数直到其返回值为 0,最后调用 close() 函数关闭 socket。
为了关闭开始 WebSocket 关闭握手,须要关闭的一端必须选择一个状态码(第 7.4 节)/code/ 和可选的关闭缘由 (第 7.1.6 节)/reason/,而后按照第 5.5.1 节中的描述发送一个关闭帧,帧的状态码以及缘由就是以前选取的 /code/ 和 /reason/。一旦一端发送并接收到了关闭帧,就能够按照第 7.1.1 节中定义的内容关闭 WebSocket 链接。
一旦任何一端发送或者接收到关闭帧,就代表 WebSocket 关闭握手已经开始,而且 WebSocket 链接的状态变为 CLOSING。
当底层的 TCP 链接已经关闭时,就代表 WebSocket 链接已经关闭,而且 WebSocket 链接的状态变为 CLOSED。若是 TCP 链接在 WebSocket 关闭握手完成以后才进行关闭,就说明关闭是干净(cleanly)的。不然的话就说明 WebSocket 链接已经关闭,但不是干净地(cleanly)。
与第 5.5.1 节和第 7.4 节中定义的同样,一个关闭帧能够包含一个关闭状态码,以此代表关闭的缘由。WebSocket 的关闭能够由任意一端发起,或者同时发起。返回的关闭帧的状态码与接收到的关闭帧的状态码相同。若是关闭帧没有包含状态码,那么就认为其状态码是 1005。若是一端发现 WebSocket 链接已经关闭可是没有收到关闭帧,那么就认为此时的状态码是 1006。
注意:两端的关闭帧的状态码没必要相同。好比,若是远程的一端发送了一个关闭帧,可是本地的应用程序尚未读取位于接收缓存中的关闭帧,而且应用程序也发送了一个关闭帧,那么两端都将会达到 “发送了” 和 “接收到” 关闭帧的状态。每一端都会看到来自另外一端的具备不一样状态码的关闭帧。所以,两端能够没必要要求发送和接收到的关闭帧的状态码是相同的,这样两端就能够大概同时进行 WebSocket 链接的关闭了。
与第 5.5.1 节和第 7.4 节中定义的相同,关闭帧能够包含一个状态码,而且在状态码以后能够跟随以 UTF-8 编码的数据,具体这些数据应该如何被解释依赖于对端的实现,本协议并无明确的定义。每一端均可以发起 WebSocket 关闭,或者同时发起。WebSocket 链接关闭缘由的定义就是跟随在关闭状态码以后的以 UTF-8 编码的数据,响应的关闭帧中的 /reason/ 内容来自请求的关闭帧中 /reason/,并与之相同。若是没有定义这些 UTF-8 数据,那么关闭的缘由就是空字符串。
注意:遵循第 7.1.5 节中描述的逻辑,两端没必要要求发送和接收的关闭帧的 /reason/ 是相同的。
由于某种算法或者特定的需求使得一端须要将 WebSocket 链接表示位失败。为了达到这个目的,客户端必须关闭 WebSocket 链接,而且能够将问题以适当的方式反馈给用户(对于开发者来讲可能很是重要)。一样的,服务端为了达到这个目的也必须关闭 WebSocket 链接,而且使用日志记录下发生的问题。
若是在但愿将 WebSocket 链接标记为失败以前,WebSocket 链接已经创建的话,那么一端在关闭 WebSocket 链接以前应该发送关闭帧,并带上适当的状态码(第 7.4 节)。若是一端认为另外一端不可能有能力去接受和处理关闭帧时,好比 WebSocket 链接还没有创建,那么能够省略发送关闭帧的过程。若是一端标记了 WebSocket 链接为失败的,那么它不能够再接受和处理来自远程的数据(包括响应一个关闭帧)。
除了上面的状况或者应用层须要(好比,使用了 WebSocket API 的脚本),客户端不该该关闭链接。
由于某种算法或者在开始握手的实际运做过程当中,须要标记 WebSocket 链接为失败。为了达到这个目的,客户端必须按照第 7.1.7 节中描述的内容将 WebSocket 链接标记为失败。
若是在任意时间点,底层的传输层链接发送了丢失,那么客户端必须将 WebSocket 链接标记为失败。
除了上面的状况或者特定的应用层须要(好比,使用了 WebSocket API 的脚本),客户端不能够关闭链接。
由于某种算法或者在握手期间终止 WebSocket 链接,服务端必须按照第 7.1.1 节的描述去关闭 WebSocket 链接。
异常关闭可能有不少的缘由引发。好比一个短暂的错误致使的异常关闭,在这种状况下,经过重连可使用一个没有问题的链接,而后继续正常的操做。然而异常也多是一个由非短暂的问题引发的,若是全部发布的客户端在经历了一个异常关闭以后,马上不断的试图向服务器发起重连,若是有大量的客户端在试图重连的话,那么服务器将有可能面对拒绝服务攻击(denial-of-service attack)。这样形成的结果就是服务将没法在短时间内恢复。
为了防止这个问题出现,客户端应该在发生了异常关闭以后进行重连时使用一些补偿机制。
第一个重连应该延迟,在一个随机时间后进行。产生用于延迟的随机时间的参数由客户端去决定,初始的重连延迟能够在 0 到 5 秒之间随机选取。客户端能够根据实际应用的状况去决定具体的随机值。
若是第一次的重连失败,那么接下来的重连应该使用一个更长的延迟,可使用一些已有的方法,好比 truncated binary exponential backoff
服务端能够在其需求的时候对 WebSocket 链接进行关闭。客户端不该该随意的关闭 WebSocket 链接。当须要进行关闭的时候,须要遵循第 7.1.2 节中定义的过程。
当关闭已经创建的链接时(好比在握手完成后发送关闭帧),请求关闭的一端必须代表关闭的缘由。如何解释缘由,以及对于缘由应该采起什么动做,都是这份技术说明中没有定义的。这份技术说明中定义了一组预约义的状态码,以及扩展、框架、最终应用程序使用的状态码范围。状态码相关的缘由 /reason/ 在关闭帧中是可选的。
当发送关闭帧的时候,一端能够采用下面的预约义的状态码:
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 代表了客户端但愿服务端协商一个或多个扩展,可是服务端在返回的握手信息中包含协商信息。扩展的列表必须出如今其发送给服务端的关闭帧的 /reason/ 中。注意这个状态码并不被服务端使用。 1011 1011 代表了一端遇到了异常状况使得其没法完成请求而须要关闭链接。 1015 1015 是一个保留值,而且它必须不能够做为状态码在关闭帧中使用,在应用程序须要一个状态码去代表执行 TLS 握手失败时,可使用它(好比服务端的证书没有经过验证)。
0-999
在 0-999 之间的状态码是不被使用的
1000-2999
在 1000-2999 之间的状态码是本协议保留的,而且扩展能够在其公开的技术说明中使用。
3000-3999
在 3000-3999 之间的状态码是为库、框架、应用程序保留的。这些状态码能够直接经过 IANA 进行注册。状态码的具体表示意义为在本协议中定义。
4000-4999
在 4000-4999 之间的状态码是为了私有使用而保留的,所以不能够被注册。相应状态码的使用及其意义能够在 WebSocket 应用程序之间事先商议好。这些状态码的意义在本协议中未定义。
当一端在以 UTF-8 编码解释接收到的数据,可是发现其实不是有效的 UTF-8 编码时,一端必须标记 WebSocket 链接为失败。这个规则适用于握手以及随后的数据传输阶段。
在这份技术说明中,客户端是能够请求使用扩展的,而且服务端能够受理客户端请求的扩展中的一个或者全部扩展。服务端响应的扩展必须属于客户端请求的扩展列表。若是扩展协商中包含了相应的扩展参数,那么参数的选择和应用必须按照具体的扩展的技术说明中描述的方式。
客户端经过包含 |Sec-WebSocket-Extensions| 去请求扩展,此字段名遵循普通的 HTTP 头字段的规则 RFC2616], Section 4.2,其内容的形式经由下面的 ABNF RFC2616 表达式给出定义。注意,着一节中的 ABNF 语法规则遵循 RFC2616,包括了 “隐含的 *LWS 规则”。若是一端接收到的值不符合下面的 ABNF,那么接收端必须马上标记 WebSocket 链接为失败。
Sec-WebSocket-Extensions = extension-list extension-list = 1#extension extension = extension-token *( ";" extension-param ) extension-token = registered-token registered-token = token extension-param = token [ "=" (token | quoted-string) ] ;When using the quoted-string syntax variant, the value after quoted-string unescaping MUST conform to the ;'token' ABNF.
注意,和其余的 HTTP 头字段同样,这些头字段也能够分隔成多行,或者由多行合并。所以下面的两个是等价的:
Sec-WebSocket-Extensions: foo Sec-WebSocket-Extensions: bar; baz=2
等价于
Sec-WebSocket-Extensions: foo, bar; baz=2
任何的 extension-token 好比使用已注册的 token(见第 11.4 节)。为扩展提供的参数好比遵循相应扩展的定义。注意,客户端只是提供它但愿使用的扩展,除非服务端从中选择了一个或多个代表其也但愿使用,不然客户端不能够私自的使用。
注意,扩展的在列表中顺序是重要的。多个扩展之间的交互方式,可能在具体定义了扩展的文档中进行了描述。若是没有定义描述了多个扩展之间应该如何交互,那么排在靠前位置的扩展应该最早被考虑使用。在服务端响应中列出的扩展将是链接实际将会使用的扩展。扩展之间修改数据或者帧的操做顺序,应该假设和扩展在服务端握手响应中的扩展列表中出现的顺序相同。
好比,若是有两个扩展 “foo” 和 “bar”,而且在服务端发送的 |Sec-WebSocket-Extensions| 的值为 “foo, bar”,那么对数据的操做总体来看就是 bar(foo(data))
,对于数据或者帧的修改过程看起来像是 “栈 stack”。
一个关于受理扩展头字段的非规范化的例子:
Sec-WebSocket-Extensions: deflate-stream Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream Sec-WebSocket-Extensions: private-extension
服务端受理一个或者多个扩展,经过 |Sec-WebSocket-Extensions| 头字段包含一个或者多个来自客户端请求中的扩展。扩展参数的解释,以及服务端如何正确响应客户端的参数,都在各自扩展的定义中描述。
扩展提供了一个插件的机制,以提供额外的协议功能。这份文档没有定义任何的扩展,可是实现时可使用独立定义在其余文档的扩展。
这一节描述了一些 WebSocket 协议在使用中须要注意的问题。问题被分红了不一样的小节。
WebSocket 能够抵御运行在被信任的应用程序(好比浏览器)中的恶意 Javascript 脚本,好比,经过检查 |Origin| 头字段。不过当面对具备更多功能的客户端时就不能采用此方法了(检查 |Origin| 头字段)。
这份协议能够适用于运行在 web 页面中的脚本,也能够直接被主机所使用。那些主机能够由于自身的目的发送一个伪造的 |Origin| 头字段,以此迷惑服务器。服务端所以服务器不该该信息任何的客户端输入。
例子:若是服务端使用了客户端的 SQL 查询语句,全部的输入文本在提交到 SQL 服务器以前必须进行跳脱操做(escape),减小服务端被 SQL 注入的风险。
服务端没必要接收来自互联网的全部请求,能够仅仅受理包含特定源的请求。若是请求的源不符合服务端的接收范围,那么服务端应该在对客户端的握手响应中包含状态码 “403 Forbidden”。
|Origin| 的做用是能够预防来自运行在可信任的客户端中的 Javascript 的恶意攻击。客户端自己能够链接到服务器,经过 |Origin| 的机制决定是否将通讯的权限交给 Javascript 应用。这么作的目的不是针对非浏览器的链接,而是杜绝运行在被信任的浏览器可能的潜在威胁 - Javascript 脚本伪造 WebSocket 链接。
除了一端的终节点会收到攻击以外,基础设施中的其余部分,好比代理,也可能会收到攻击。
针对代理的攻击其实是针对那些在实现上有缺陷的代理服务器,有缺陷的代理服务器的工做方式相似:
在链接创建完成后,你发送了相似下面的文本:
GET /script.js HTTP/1.1 Host: target.com
(更多更深刻的描述见 Talking)
这段文本首先是传到代理服务器的,代理服务器正确的工做方式是应该将此文本直接转发给 IP 为 2.2.2.2 的服务器。但是,有缺陷的代理会认为这是一个 HTTP 请求,须要采用 HTTP 代理的机制,进而访问了 target.com 并获取了 /script.js。
这种错误的工做方式并非你所指望的。可是不可能一一检查网络中全部可能存在此问题的代理,因此最好的方式就是将客户端发送的内容都进行掩码操做,这样就不会出现那种让有缺陷的代理服务器产生迷惑的内容了。
在协议实现中,可能会有一些客观的限制,好比特定平台的限制,这些限制与帧的大小或者全部帧合并后的消息的大小相关(好比,恶意的终节点能够经过发送单个很大的帧(2**60),或者发送不少很小的帧可是这些帧组成的消息很是大,以此来耗尽另外一方的资源)。所以在实现中,一端应该强制使用一些限制,限制帧的大小,以及许多帧最后组成的消息的大小。
这份协议没有规定任何方式可被用于服务端在握手期间对客户端进行认证。WebSocket 服务端可使用任何在普通 HTTP 服务端中使用的对客户端的认证方式,好比 cookie,HTTP 认证,或者 TLS 认证。
WebSocket 协议的保密性和完整性是经过将其运行在 TLS 上达到的。WebSocket 实现必须支持 TLS 并在须要的时候使用它。
对于使用 TLS 的链接,TLS 提供的大部分好处都是基于 TLS 握手阶段协商的算法的强度。好比,一些 TLS 加密算法没有保证信息的保密性。为了使安全达到合适的程度,客户端应该只使用高强度的 TLS 算法。W3C.REC-wsc-ui-20100812 具体讨论了什么是高强度的 TLS 算法,RFC5246 的附录 A.5 和 附录 D.3 提供了一些指导意见。
客户端和服务端接收的数据都必须通过验证。若是在任意时间点上,一端接收到了没法理解的或者违反标准的数据,或者发现了不安全的数据,或者在握手期间接收到了非指望的值(好比错误的路径或者源),则能够关闭 TCP 链接。若是接收到无效数据时 WebSocket 链接已经创建,那么一端在关闭 WebSocket 链接以前,应该向另外一端发送一个带有适当的状态码的关闭帧。经过使用具备适当状态码的关闭帧,能够帮助定位问题。若是在握手期间接收到了无效的数据,那么服务端应该返回适当的 HTTP 状态码 RFC2616。
一个典型的安全问题就是当发送的数据采用了错误的编码时。这份协议中规定了,文本数据包含的必须是 UTF-8 编码的数据。应用程序须要经过一个长度去肯定帧序列的传输什么时候结束,可是这个长度每每在事先很差肯定(碎片化的消息)。这就给检查文本消息是否采用了正确的编码带来了困难,由于必须等到消息的全部碎片帧都接受完成了,才能够检查它们组成的消息的编码是否正确。不过若是不检查编码的话,就不能确保接收的数据能够被正确的解释,并会带来潜在的安全问题。
这份文档中描述的 WebSocket 握手并不依赖于 SHA-1 算法的安全属性,好比抗碰撞性或者在 RFC4270 中描述的 second pre-image attack。