深入探索WebSockets

42次阅读

共计 5564 个字符,预计需要花费 14 分钟才能阅读完成。

WebSockets 简介
在 2008 年中期,开发人员 Michael Carter 和 Ian Hickson 特别敏锐地感受到 Comet 在实施任何真正强大的东西时所带来的痛苦和局限。通过在 IRC 和 W3C 邮件列表上的合作,他们制定了一项计划,在网络上引入现代实时双向通信的新标准,因此创造了“WebSocket”这个名称。
这个想法进入了 W3C HTML 草案标准,不久之后,Michael Carter 写了一篇文章,将 Comet 社区介绍给 WebSockets。2010 年,谷歌 Chrome 4 是第一个提供对 WebSockets 全面支持的浏览器,其他浏览器供应商也在接下来的几年中采用了这种方式。2011 年,RFC 6455 – WebSocket 协议 – 发布到 IETF 网站。
今天,所有主流浏览器都完全支持 WebSockets,甚至包括 Internet Explorer 10 和 11. 此外,自 2013 年以来,iOS 和 Android 上的浏览器都支持 WebSockets,这意味着总而言之,WebSocket 支持的现代环境非常健康。大多数“物联网”或 IoT 也在某些版本的 Android 上运行,因此从 2018 年开始,其他类型设备上的 WebSocket 支持也相当普遍。
那么究竟什么是 WebSockets 呢?
简而言之,WebSockets 是一个构建在设备 TCP / IP 堆栈之上的传输层。目的是为 Web 应用程序开发人员提供本质上尽可能接近原始的 TCP 通信层,同时添加一些抽象来消除某些差异。它们还满足了这样一个事实,即网络具有额外的安全考虑因素,必须将其考虑在内以保护消费者和服务提供者。
您可能听说 WebSockets 同时被称为“传输”和“协议”。前者更准确,因为虽然它们是一种协议,因为必须遵守一套严格的规则来建立通信并包含所传输的数据,但该标准并没有对如何构建实际数据有效载荷采取任何规定。事实上,规范的一部分包括客户端和服务器就一个协议达成一致的规范,传输的数据将通过该协议进行格式化和解释。该标准将这些称为“子协议”,以避免术语中含糊不清的问题。子协议的示例是 JSON,XML,MQTT,WAMP 等。这些不仅可以确保数据的结构方式,还可以确保通信必须开始,继续并最终终止的方式。只要双方都了解协议所包含的内容,任何事情都会发生。WebSocket 仅提供传输层,通过该传输层可以实现该消息传递过程,这就是为什么大多数常见的子协议不是基于 WebSocket 的通信所独有的。
关于身份验证和授权的快速说明
把 WebSockets 看作是一个建立在 TCP / IP 之上的薄层,超出基本握手和消息框架规范的任何东西都需要在每个应用程序或每个库的基础上处理。引用 RFC:
此协议未规定服务器在 WebSocket 握手期间可以对客户端进行身份验证的任何特定方式。WebSocket 服务器可以使用通用 HTTP 服务器可用的任何客户端身份验证机制,例如 cookie,HTTP 身份验证或 TLS 身份验证。
简而言之,您仍然可以使用的基于 HTTP 的身份验证方法,或使用 MQTT 或 WAMP 等子协议,这两种子协议都提供身份验证和授权方法。
用 HTTP 做连接
定义 WebSocket 标准时的一个早期考虑因素是确保它“与网络”很好地协同工作。这意味着认识到 Web 通常使用 URL 而不是 IP 地址和端口号进行寻址,并且 WebSocket 连接应该能够使用 Web 请求相同的基于 HTTP 的任何其他类型进行初始握手。
这是一个简单的 HTTP GET 请求中发生的事情。
假设在 http://www.example.com/index….。如果不深入到 HTTP 协议本身,就足以知道请求必须从所谓的 Request-Line 开始,然后是一系列键值对标题行,每一行都告诉服务器一些关于什么的信息。期望在随后的请求有效负载中跟随头数据,以及它可以从客户端得到的关于它能够理解的响应类型的内容。
请求中的第一个令牌是 HTTP 方法,它告诉服务器客户端针对引用的 URL 尝试的操作类型。当客户端仅请求服务器向其提供由指定 URL 引用的资源的副本时,使用 GET 方法。
根据 HTTP RFC 格式化的请求标头的系统示例如下所示:
GET /index.html HTTP/1.1
Host: www.example.com
收到请求标头后,服务器然后格式化一个以状态行开头的响应标头,然后是一组键值标头对,为客户端提供来自服务器的补充信息,关于服务器的请求。响应。“状态行”告诉客户端 HTTP 状态代码(如果没有问题,通常为 200),并提供解释状态代码的简短“原因”文本描述。接下来出现键值标题对,然后是请求的实际数据(除非状态代码表明由于某种原因无法满足请求)。
HTTP/1.1 200 OK
Date: Wed, 1 Aug 2018 16:03:29 GMT
Content-Length: 291
Content-Type: text/html
(additional headers…)

(response payload continues here…)
那么你可能会问,这与 WebSockets 有什么关系呢?
抛弃 HTTP 以获得更合适的东西
在发出 HTTP 请求并接收响应时,涉及的实际双向网络通信通过活动的 TCP / IP 套接字进行。浏览器中请求的 Web URL 通过全局 DNS 系统映射到 IP 地址,HTTP 请求的默认端口为 80. 这意味着虽然 Web URL 已输入浏览器,但实际通信是通过 TCP 进行的 / IP,使用类似于 123.11.85.9:80 的 IP 地址和端口组合。
我们现在知道,WebSockets 也建立在 TCP 堆栈之上,这意味着我们所需要的只是客户端和服务器共同同意保持套接字连接打开并重新利用它以进行持续通信的方式。如果他们这样做,就可以发送和接收的二进制数据。
要开始重新调整 TCP 套接字以进行 WebSocket 通信,客户端可以包含专门为此类用例发明的标准请求标头:
GET /index.html HTTP/1.1
Host: www.example.com
Connection: Upgrade
Upgrade: websocket

Connection 标头告诉服务器客户端希望协商套接字使用方式的更改。随附的值 Upgrade 表示当前通过 TCP 使用的传输协议应该更改。现在服务器知道客户端想要通过活动 TCP 套接字升级当前正在使用的协议,服务器知道要查找相应的升级头,这将告诉它客户端想要使用哪个传输协议的剩余生命周期 连接。一旦服务器将 websocket 视为 Upgrade 标头的值,它就知道 WebSocket 握手过程已经开始。
请注意,如果您想了解本文中介绍的更多详细信息,请参阅 RFC 6455 中概述了握手过程(以及其他所有内容)。
避免有趣的麻烦
除了上面描述的内容之外,WebSocket 握手的第一部分涉及证明这实际上是一个正确的 WebSocket 升级握手,并且该过程不是通过客户端或可能通过某种中间欺骗来规避或模拟的。位于中间的代理服务器。
启动升级到 WebSocket 连接时,客户端必须包含 Sec-WebSocket-Key 标头,该标头具有该客户端唯一的值。这是一个例子:
Sec-WebSocket-Key: BOq0IliaPZlnbMHEBYtdjmKIL38=
如果使用现代浏览器中提供的 WebSocket 类,上面的内容将自动处理。您只需在服务器端查找它并生成响应。
响应时,服务器必须将特殊 GUID 值 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 附加到密钥,生成结果字符串的 SHA- 1 哈希值,然后将其包含为 Sec 的 base-64 编码值。它包含在响应中的 WebSocket-Accept 标头:
Sec-WebSocket-Accept: 5fXT1W3UfPusBQv/h6c4hnwTJzk=
在 Node.js WebSocket 服务器中,我们可以编写一个函数来生成这个值,如下所示:
const crypto = require(‘crypto’);

function generateAcceptValue (acceptKey) {
return crypto
.createHash(‘sha1’)
.update(acceptKey + ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’, ‘binary’)
.digest(‘base64’);
}
然后我们只需要调用这个函数,传递 Sec-WebSocket-Key 头的值作为参数,并在发送响应时将函数返回值设置为 Sec-WebSocket-Accept 头的值。
要完成握手,请将适当的 HTTP 响应头写入客户端套接字。一个简单的响应看起来像这样:
HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: m9raz0Lr21hfqAitCxWigVwhppA=
到目前为止,我们还没有完成握手 – 还有很多事情要考虑。
子协议 – 统一语言
客户端和服务器通常需要在给定消息内以及从一个消息到下一个消息的一段时间内,就它们如何格式化,解释和组织数据本身的兼容策略达成一致。这就是子协议(前面提到过)的用武之地。如果客户端知道它可以处理一个或多个特定的应用程序级协议(例如 WAMP,MQTT 等),它可以包含它理解的协议列表。发出初始 HTTP 请求。如果它这样做,则服务器需要选择其中一个协议并将其包含在响应头中,否则将使握手失败并终止连接。
子协议请求标头示例:
Sec-WebSocket-Protocol: mqtt, wamp
服务器在响应中发出的示例倒数标题:
Sec-WebSocket-Protocol: wamp
请注意,服务器必须从客户端提供的列表中精确选择一种协议。选择多个将意味着服务器无法可靠或一致地解释后续 WebSocket 消息中的数据。例如,如果服务器选择了 json-ld 和 json-schema。两者都是基于 JSON 标准构建的数据格式,并且会有许多边缘情况,其中一个可能被解释为另一个,从而在处理数据时导致意外错误。虽然不可否认本身不是消息传递协议,但该示例仍然适用。
当客户端和服务器都实现为从一开始就使用通用消息传递协议时,可以在初始请求中省略 Sec-WebSocket-Protocol 标头,在这种情况下服务器可以忽略此步骤。在实现通用服务,基础结构和工具时,子协议协商是最有用的,在这些服务,基础结构和工具中,一旦建立了 WebSocket 连接,就无法保证客户端和服务器都能相互理解。
通用协议的标准化名称应在 IANA 注册中心注册,用于 WebSocket 子协议名称,在本文撰写时,已经注册了 36 个名称,包括 soap,xmpp,wamp,mqtt 等。尽管注册表是将子协议名称映射到其解释的规范来源,但唯一严格的要求是客户端和服务器就其相互选择的子协议实际意味着什么达成一致,无论它是否出现在 IANA 注册表中。
请注意,如果客户端请求使用子协议但未提供服务器可以支持的任何内容,则服务器必须发送失败响应并关闭连接。
WebSocket 扩展
还有一个标题用于定义数据有效负载编码和成帧方式的扩展,但在本文时,只存在一种标准化扩展类型,它提供了一种 WebSocket – 等同于消息中的 gzip 压缩。扩展可能发挥作用的另一个例子是多路复用 – 使用单个套接字来交错多个并发通信流。
WebSocket 扩展是一个有点高级的主题,并且超出了本文的范围。现在,它足以知道它们是什么,以及它们如何适应图片。
客户端 – 在浏览器中使用 WebSockets
WebSocket API 在 WHATWG HTML Living Standard 中定义,实际上非常简单易用。构造 WebSocket 需要一行代码:
const ws = new WebSocket(‘ws://example.org’);
注意使用 ws,你通常有 http 方案。您也可以选择使用 wss,通常使用 https。这些协议与 WebSocket 规范一起引入,旨在表示 HTTP 连接,其中包括升级连接以使用 WebSockets 的请求。
创建 WebSocket 对象本身并没有做很多事情。连接是异步建立的,因此您需要在发送任何消息之前侦听握手的完成,并且还包括从服务器接收的消息的侦听器:
ws.addEventListener(‘open’, () => {
// Send a message to the WebSocket server
ws.send(‘Hello!’);
});

ws.addEventListener(‘message’, event => {
// The `event` object is a typical DOM event object, and the message data sent
// by the server is stored in the `data` property
console.log(‘Received:’, event.data);
});
还有错误和关闭事件。连接终止时 WebSockets 不会自动恢复 – 这是您需要自己实现的,并且是存在许多客户端库的原因之一。虽然 WebSocket 类简单易用,但它实际上只是一个基本的构建块。必须单独实现对不同子协议或消息传递通道等附加功能的支持。
生成和解析 WebSocket 消息帧
一旦将握手响应发送到客户端,客户端和服务器就可以使用他们选择的子协议(如果有的话)开始通信。
WebSocket 消息在名为“frames”的包中传递,这些包以消息头开头,并以“payload”结尾 – 此帧的消息数据。大型消息可能会将数据分成几帧,在这种情况下,您需要跟踪到目前为止收到的内容,并在数据全部到达后将数据分组。
翻译的很乱,但愿对你有点帮助

正文完
 0