深入探索WebSockets

作者:ably.io 2018年12月08日 19:01ably.io 标签:JavaScript  Node.js 

在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上有一个html页面。 如果不深入到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”结尾 - 此帧的消息数据。 大型消息可能会将数据分成几帧,在这种情况下,您需要跟踪到目前为止收到的内容,并在数据全部到达后将数据分组。

原文地址:https://www.ably.io/concepts/websockets

2条评论