解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

Posted 前端大学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解读 JavaScript 之深入探索 WebSockets 和 HTTP/2相关的知识,希望对你有一定的参考价值。

译者:凉凉_, Tocy, 无若

https://www.oschina.net/translate/how-does-javascript-actually-work-part-5

引言

如今,拥有丰富、动态 UI 的复杂 Web 应用被认为是理所当然的。这并不奇怪 - 互联网自成立以来,已经走过很长一段路了。

最初,互联网并不是为了支持这种动态和复杂的 Web 应用而设计的。它被认为是一系列 html 页面的集合,相互链接形成一个包含信息的 “Web” 概念。一切都基本上建立在所谓的 HTTP 请求/响应范例之上的。客户端加载一个页面,之后什么也不会发生,直到用户点击并导航到下一页面。

大约在 2005 年,AJAX 被引入,许多人开始探索在客户端和服务器之间建立双向连接的可能性。尽管如此,所有 HTTP 通信都是由客户端操纵的,这需要用户交互或周期性轮询以从服务器加载新数据。

让 HTTP 变成“双向”交互

让服务器“主动”向客户端发送数据的技术已经存在相当长的一段时间了。 “Push” 和 “Comet" 等等。

最常见的一种黑客攻击方法是让服务器产生一种需要向客户端发送数据的错觉,这称为长轮询。通过长时间轮询,客户端打开一个 HTTP 连接到服务器,保持打开直到发送响应。只要服务器有新的数据需要发送,它就会作为响应发送。

让我们看看一个非常简单的长轮询片段:

 
   
   
 
  1. (function poll(){

  2.   setTimeout(function(){

  3.      $.ajax({

  4.        url: 'https://api.example.com/endpoint',

  5.        success: function(data) {

  6.          // Do something with `data`

  7.          // ...

  8.          //Setup the next poll recursively

  9.          poll();

  10.        },

  11.        dataType: 'json'

  12.      });

  13.  }, 10000);

  14. })();

这是一个基本的自动执行功能,在第一次执行后自动运行。它设置了 10 秒的时间间隔,在每次异步 Ajax 调用服务器之后,回调再次调用 ajax 。

其他技术涉及 Flash 或 XHR multipart request 和所谓的 htmlfiles 。

所有这些解决方案都有相同的问题:它们承载了 HTTP 的开销,都不适合低延迟的应用。想想那些浏览器上的多人第一人称射击游戏或任何其他在线游戏中使用的实时组件是如何实现的。

WebSockets 的引入

WebSocket 规范定义了在 Web 浏览器和服务器之间建立“套接字”连接的 API 。 简而言之,客户端和服务器之间有一个长久的连接,双方可以随时开始发送数据。

客户端通过被称为 WebSocket 握手的过程建立一个 WebSocket 连接。 此过程从客户端向服务器发送常规 HTTP 请求开始。 此请求中包含 Upgrade 标头,通知服务器客户端希望建立 WebSocket 连接。

我们来看看如何在客户端打开一个 WebSocket 连接:

 
   
   
 
  1. // Create a new WebSocket with an encrypted connection.

  2. var socket = new WebSocket('ws://websocket.example.com');

WebSocket URL 使用 ws 方案。也有用于安全 WebSocket 连接的 wss ,相当于 HTTPS 。

这个方案只是打开 websocket.example.com 的 WebSocket 连接的过程的开始。

这是初始请求标头的简单示例。

 
   
   
 
  1. GET ws://websocket.example.com/ HTTP/1.1

  2. Origin: http://example.com

  3. Connection: Upgrade

  4. Host: websocket.example.com

  5. Upgrade: websocket

如果服务器支持 WebSocket 协议,它将同意升级,并通过响应中的 Upgrade 标头进行通信。

我们来看看如何在 Node.JS 中实现这个功能:

 
   
   
 
  1. // We'll be using the https://github.com/theturtle32/WebSocket-Node

  2. // WebSocket implementation

  3. var WebSocketServer = require('websocket').server;

  4. var http = require('http');

  5. var server = http.createServer(function(request, response) {

  6.  // process HTTP request.

  7. });

  8. server.listen(1337, function() { });

  9. // create the server

  10. wsServer = new WebSocketServer({

  11.  httpServer: server

  12. });

  13. // WebSocket server

  14. wsServer.on('request', function(request) {

  15.  var connection = request.accept(null, request.origin);

  16.  // This is the most important callback for us, we'll handle

  17.  // all messages from users here.

  18.  connection.on('message', function(message) {

  19.      // Process WebSocket message

  20.  });

  21.  connection.on('close', function(connection) {

  22.    // Connection closes

  23.  });

  24. });

连接建立后,服务器通过升级回复:

 
   
   
 
  1. HTTP/1.1 101 Switching Protocols

  2. Date: Wed, 25 Oct 2017 10:07:34 GMT

  3. Connection: Upgrade

  4. Upgrade: WebSocket

建立连接后,open 事件将在客户端的 WebSocket 实例上触发:

 
   
   
 
  1. var socket = new WebSocket('ws://websocket.example.com');

  2. // Show a connected message when the WebSocket is opened.

  3. socket.onopen = function(event) {

  4.  console.log('WebSocket is connected.');

  5. };

现在握手已完成,初始 HTTP 连接被替换为使用相同底层 TCP / IP 连接的 WebSocket 连接。 此时,任何一方都可以开始发送数据。

使用 WebSocket ,你可以随心所欲地传输数据,而不用考虑与传统 HTTP 请求相关的开销。数据是通过一个 WebSocket 以消息进行传输的,每个消息由一个或多个包含你正在发送数据(有效负载)的帧组成。为确保消息在到达客户端时能够被正确地重建,每个帧是以 4-12 字节的数据做为前缀的。使用这种基于帧的消息传输系统有助于减少传输中非有效载荷的数据量,从而显着减少延迟。

注意:值得注意的是,一旦接收到所有的帧并且原始消息有效载荷已经被重建之后,客户端将仅收到关于新消息的通知。

WebSocket URLs

之前我们简单提及:WebSockets 引入了一个新的 URL 方案。实际上,他们引入了两个新的方案:ws:// 和 wss:// 。

URL 具有特定方案的语法。WebSocket URL 是特殊的,它们不支持锚点(#sample_anchor)。

相比于 HTTP 风格的 URL ,同样的规则也适用于 WebSocket 风格的 URL 。ws 是未加密的,默认端口为 80 ,而 wss 需要使用 TLS 加密,默认端口为 443 。

帧协议

让我们更深入地了解帧协议。这是 RFC 为我们提供的:

 
   
   
 
  1.  0                   1                   2                   3

  2.  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

  3. +-+-+-+-+-------+-+-------------+-------------------------------+

  4. |F|R|R|R| opcode|M| Payload len |    Extended payload length    |

  5. |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |

  6. |N|V|V|V|       |S|             |   (if payload len==126/127)   |

  7. | |1|2|3|       |K|             |                               |

  8. +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

  9. |     Extended payload length continued, if payload len == 127  |

  10. + - - - - - - - - - - - - - - - +-------------------------------+

  11. |                               |Masking-key, if MASK set to 1  |

  12. +-------------------------------+-------------------------------+

  13. | Masking-key (continued)       |          Payload Data         |

  14. +-------------------------------- - - - - - - - - - - - - - - - +

  15. :                     Payload Data continued ...                :

  16. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

  17. |                     Payload Data continued ...                |

  18. +---------------------------------------------------------------+

从 RFC 指定的 WebSocket 版本开始,每个数据包前只有一个标题。不过,这是一个相当复杂的标题。这里是它的构建块解释:

  • fin(1 bit):指示该帧是否构成该消息的最终帧。大多数情况下,消息适合于一个单一的帧,这一点总是默认设置的。实验表明,Firefox 在 32K 之后创建了第二个帧。

  • rsv1,rsv2,rsv3(1 bit each):必须为0,除非扩展里协商定义了非零值的含义。如果收到一个非零值,并且协商的扩展中没有一个定义这个非零值的含义,那么接收端必须抛出失败连接。

  • opcode(4bits):展示了帧表示什么。以下值目前正在使用: 0x00:这个帧继续前面的有效载荷。 0x01:此帧包含文本数据。 0x02:这个帧包含二进制数据。 0x08:这个帧终止连接。 0x09:这个帧是一个 ping 。 0x0a:这个帧是一个 pong 。 (正如你所看到的,有足够的值未被使用,它们已被保留供将来使用)。

  • mask(1 bit):指示连接是否掩盖。就目前而言,从客户端到服务器的每条消息都必须掩盖,如果规范没有掩盖,规范就会终止连接。

  • payload_len(7 bits):有效载荷的长度。 WebSocket 的帧有以下长度括号: 0-125 表示有效载荷的长度。 126 表示以下两个字节表示长度,127 表示接下来的 8 个字节表示长度。所以有效负载的长度在 〜7bit,16bit 和 64bit 括号内。

  • masking-key(32 bits):从客户端发送到服务器的所有帧都被帧中包含的 32 位值掩盖。

  • payload:最可能被掩盖的实际数据。它的长度是 payload_len 的长度。

为什么 WebSocket 是基于帧而不是基于流?我不知道,就像你一样,我很想了解更多,所以如果你有想法,请随时在下面的回复中添加评论和资源。另外,关于这个主题的讨论可以在 HackerNews 上找到。

关于帧的数据

如上所述,数据可以被分割成多个帧。 传输数据的第一帧有一个操作码,表示正在传输什么类型的数据。 这是必要的,因为 JavaScript 在开始规范时几乎不存在对二进制数据的支持。 0x01 表示 utf-8 编码的文本数据,0x02 是二进制数据。大多数人会发送 JSON ,在这种情况下,你可能要选择文本操作码。 当你发送二进制数据时,它将在浏览器特定的 Blob 中表示。

通过 WebSocket 发送数据的 API 非常简单:

 
   
   
 
  1. var socket = new WebSocket('ws://websocket.example.com');

  2. socket.onopen = function(event) {

  3.  socket.send('Some message'); // Sends data to server.

  4. };

当 WebSocket 正在接收数据时(在客户端),消息事件被触发。 这个事件包含一个名为 data 的属性,可以用来访问消息的内容。

 
   
   
 
  1. // Handle messages sent by the server.

  2. socket.onmessage = function(event) {

  3.  var message = event.data;

  4.  console.log(message);

  5. };

你可以使用 Chrome 开发工具中的“网络”选项卡轻松浏览 WebSocket 连接中每个帧中的数据:

解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

分片

有效载荷数据可以分成多个单独的帧。接收端应该缓冲它们直到 fin 位被设置。所以你可以通过 11 个 6 (头长度)包+每个 1 字节来传送字符串 “Hello World” 。控件包不允许使用分片。但是,规范要求你能够处理交错控制帧。这是在 TCP 包按任意顺序到达的情况下。

连接帧的逻辑大致如下:

  • 接收第一帧

  • 记住操作码

  • 将帧有效负载连接在一起,直到 fin 位被设置

  • 断言每个包的操作码是零

分片的主要目的是在消息启动时允许发送未知大小的消息。有了分片,服务器可能会选择一个合理大小的缓冲区,当缓冲区满时,将一个帧写入网络。分片的二次使用情况是多路复用,在一个逻辑信道上的大消息接管整个输出信道是不可取的,所以多路复用需要自由将消息分成较小的片段以更好地共享输出渠道。

什么是跳动检测?

在握手之后的任何时候,客户端或者服务器都可以选择向对方发送 ping 帧。 当收到一个 ping 帧,收件人必须尽快发回一个 pong 帧。 这是一次跳动。 你可以使用它来确保客户端保持着连接。

ping 帧或 pong 帧只是一个常规的帧,但它是一个控制帧。 ping 帧具有 0x9 的操作码,并且 pong 帧具有 0xA 的操作码。 当你得到一个 ping 帧,发回一个 pong 帧与 ping 帧完全相同的有效载荷数据(对于 pings 和 pongs ,最大有效载荷长度是 125 )。 你也可能会得到一个 pong 帧返回,而无需再发送一个 ping 帧。如果它发生就忽略它。

跳动检测可能是非常有用的。 有些服务(如负载均衡器)会终止空闲连接。 另外,接收方无法查看远端是否已经终止。 只有在下一个发送时你会意识到出了问题。

错误处理

你可以通过监听 error 事件来处理所有错误。

代码如下:

 
   
   
 
  1. var socket = new WebSocket('ws://websocket.example.com');

  2. // Handle any error that occurs.

  3. socket.onerror = function(error) {

  4.  console.log('WebSocket Error: ' + error);

  5. };

关闭连接

要关闭连接,客户端或服务器应发送包含操作码 0x8 的数据的控制帧。 一旦接收到这样的帧,对方发送一个关闭帧作为响应。 第一个动作做完,然后关闭连接。然后丢弃关闭连接后收到的任何其他数据。

这里展示的是如何从客户端关闭 WebSocket 连接:

 
   
   
 
  1. // Close if the connection is open.

  2. if (socket.readyState === WebSocket.OPEN) {

  3.    socket.close();

  4. }

另外,为了在完成关闭之后执行其他清理,可以将事件侦听器附加到关闭事件:

 
   
   
 
  1. // Do necessary clean up.

  2. socket.onclose = function(event) {

  3.  console.log('Disconnected from WebSocket.');

  4. };

服务器必须监听关闭事件以便在需要时处理它:

 
   
   
 
  1. connection.on('close', function(reasonCode, description) {

  2.    // The connection is getting closed.

  3. });

如何比较 WebSockets 和 HTTP/2 ?

虽然 HTTP/2 提供了很多机制,它并不能完全取代现有的推/流媒体技术的需要。

关于 HTTP/2 的第一个重要的事情是它并不能替代所有的 HTTP 。verb、状态码和大部分头信息将保持与目前版本一致。HTTP/2 是意在提升数据在线路上传输的效率。

现在,如果我们比较下 HTTP/2 和 WebSocket ,我们可以看到很多类似之处:

解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

正如我们上面看到的,HTTP/2 引入了 Server Push ,它使服务器能够主动地将资源推送到客户端缓存。但是,它并不允许将数据推送到客户端应用程序本身。服务器推送只能由浏览器处理,不会在应用程序代码中弹出服务器数据,这意味着应用程序没有 API 来获取这些事件的通知。

这是服务器发送事件(SSE)变得非常有用的地方。 SSE 是一种机制,允许服务器在建立客户端 - 服务器连接后异步地将数据推送到客户端。只要有新的“数据块”可用,服务器就可以决定发送数据。它可以被认为是一种单向的发布 - 订阅模式。它还提供了一个标准的 JavaScript 客户端 API ,名为 EventSource ,在大多数现代浏览器中实现,作为 W3C 的 HTML5 标准的一部分。请注意,不支持 EventSource API 的浏览器可以轻松地使用 polyfilled 方案来解决。

由于 SSE 基于 HTTP ,因此它与 HTTP/2 非常合适,可以结合使用以实现最佳效果:HTTP/2 处理基于多路复用流的高效传输层,SSE 将 API 提供给应用以启用数据推送。

为了充分理解 Streams 和 Multiplexing 是什么,首先看一下 IETF 的定义:“stream” 是 HTTP/2 连接中在客户端和服务器之间交换的一个独立的,双向的帧序列。其主要特征之一是单个 HTTP/ 2 连接可以包含多个同时打开的流,其中来自多个流的端点交错帧。

解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

我们必须记住,SSE 是基于 HTTP 的。这意味着在 HTTP/2 中,不仅可以将多个 SSE 流交织到单个 TCP 连接上,而且还可以通过多个 SSE 流(服务器到客户端的推送)和多个客户端请求(客户端到服务器)。多亏了有 HTTP/2 和 SSE 的存在,现在我们有一个纯粹的 HTTP 双向连接和一个简单的 API 就可以让应用程序代码注册到服务器推送服务上。在比较 SSE 和 WebSocket 时,缺乏双向能力往往被认为是一个主要的缺陷。多亏了有 HTTP/2,不再有这种情况。这样就可以跳过 WebSocket ,而坚持使用基于 HTTP 的信号机制。

如何选择 WebSocket 和 HTTP/2 ?

WebSockets 肯定会在 HTTP/2 + SSE 的领域中生存下来,主要是因为它是一种已经被很好地应用的技术,并且在非常具体的使用情况下,它比 HTTP/2 更具优势,因为它已经被构建用于具有较少开销(如报头)的双向功能。

假设你想建立一个大型多人在线游戏,需要来自连接两端的大量消息。在这种情况下,WebSockets 的性能会好很多。

一般情况下,只要需要客户端和服务器之间的真正低延迟,接近实时的连接,就使用 WebSocket 。请记住,这可能需要重新考虑如何构建服务器端应用程序,以及将焦点转移到队列事件等技术上。

如果你使用的方案需要显示实时的市场消息,市场数据,聊天应用程序等,依靠 HTTP/2 + SSE 将为你提供高效的双向通信渠道,同时获得留在 HTTP 领域的各种好处:

  • 当考虑到与现有 Web 基础设施的兼容性时,WebSocket 通常会变成一个痛苦的源头,因为它将 HTTP 连接升级到完全不同于 HTTP 的协议。

  • 规模和安全性:Web 组件(防火墙,入侵检测,负载均衡)是以 HTTP 为基础构建,维护和配置的,这是大型/关键应用程序在弹性,安全性和可伸缩性方面更喜欢的环境。

另外,你必须考虑到浏览器的支持。 看看 WebSocket :

解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

实际上相当不错,不是吗?

然而,HTTP/2 的情况并不相同:

解读 JavaScript 之深入探索 WebSockets 和 HTTP/2

  • 仅 TLS(不是那么糟糕)

  • 部分支持 IE 11 ,但只在 Windows 10 上

  • 仅在 OSX 10.11+ 上支持 Safari

  • 如果你可以通过 ALPN 协商,可以只支持 HTTP/2(这是你的服务器需要明确支持的东西)

SSE 的支持比较好:

只有 IE / Edge 不提供支持。(Opera Mini 既不支持 SSE 也不支持 WebSocket ,所以我们可以将其完全排除在外)。 在 IE / Edge 中也有一些像样的 polyfill 提供 SSE 支持。

推荐阅读:

1.

2.

回复“面试题”“ajax”等词,可看文章;

其它功能正在完善,不定期更新....


觉得本文对你有帮助?请分享给更多人

关注「」,提升前端技能

以上是关于解读 JavaScript 之深入探索 WebSockets 和 HTTP/2的主要内容,如果未能解决你的问题,请参考以下文章

WebSocket实战之——JavaScript例子

解读2015之自然语言处理篇:持续探索 稳中前行

Text-to-Image with Diffusion models的巅峰之作:深入解读​ DALL·E 2

Redis之RedisTemplate的序列化方式深入解读

深入探索Redis之底层数据结构

第四期技术分享:JAVA之深入浅出设计模式 + 移动技术前沿之探索Flutter框架