这一次,我们将深入到通信协议的世界中,对比并讨论它们的属性并构建部件。我们将提供WebSockets和HTTP / 2的快速比较。 最后,我们分享一些关于如何选择网络协议。

概述

如今,拥有丰富动态用户界面的复杂网络应用程序被视为理所当然。这并不奇怪 - 互联网自成立以来已经走过了很长的一段路。

最初,互联网并不是为支持这种动态和复杂的网络应用程序而构建的。它被认为是HTML页面的集合,彼此链接以形成包含信息的“web”概念。一切都基本上围绕HTTP的所谓的请求/响应范式而建立。客户端加载一个页面,然后什么都不会发生,直到用户点击并导航到下一页。

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

使HTTP成为“双向”

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

服务器向客户端发送数据的最常见窍门之一称为长轮询。通过长轮询,客户端打开一个HTTP连接到服务器,该服务器保持打开状态直到发送响应。只要服务器有新的数据需要发送,它就会将其作为响应发送出去。

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

(function poll(){
   setTimeout(function(){
      $.ajax({ 
        url: 'https://api.example.com/endpoint', 
        success: function(data) {
          // Do something with `data`
          // ...

          //Setup the next poll recursively
          poll();
        }, 
        dataType: 'json'
      });
  }, 10000);
})();

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

其他技术涉及Flash或XHR多部分请求和所谓的htmlfiles。

所有这些解决方案都有相同的问题:它们承载HTTP的开销,这并不能使它们非常适合低延迟的应用程序。在浏览器或任何其他具有实时组件的在线游戏中考虑多人第一人称射击游戏。

WebSockets的引入

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

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

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

// 创建一个加密连接的WebSocket
var socket = new WebSocket('ws://websocket.example.com');

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

以下是初始请求头的简化示例:

GET ws://websocket.example.com/ HTTP/1.1
Origin: http://example.com
Connection: Upgrade
Host: websocket.example.com
Upgrade: websocket

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

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

// We'll be using the https://github.com/theturtle32/WebSocket-Node
// WebSocket implementation
var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
  // process HTTP request. 
});
server.listen(1337, function() { });

// create the server
wsServer = new WebSocketServer({
  httpServer: server
});

// WebSocket server
wsServer.on('request', function(request) {
  var connection = request.accept(null, request.origin);

  // This is the most important callback for us, we'll handle
  // all messages from users here.
  connection.on('message', function(message) {
      // Process WebSocket message
  });

  connection.on('close', function(connection) {
    // Connection closes
  });
});

连接建立后,服务器通过Upgrade进行回复:

HTTP/1.1 101 Switching Protocols
Date: Wed, 25 Oct 2017 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket

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

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

// Show a connected message when the WebSocket is opened.
socket.onopen = function(event) {
  console.log('WebSocket is connected.');
};

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

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

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

WebSocket URLs

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

网址具有特定schema语法。WebSocket URL特别之处在于它们不支持锚(#sample_anchor)。

对于HTTP风格的URL,相同的规则适用于WebSocket风格的URL。ws未加密,默认端口为80,而wss需要TLS加密并且端口443为默认端口。

成帧协议

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

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

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

  • fin(1比特):指示该帧是否构成消息的最终帧。大多数情况下,消息适合于一个帧,并且该位总是被设置。实验表明,Firefox在32K之后创建了第二个帧。
  • rsv1,rsv2,rsv3(每个1位):除非扩展协商定义了非零值的含义,否则必须为0。如果接收到非零值并且没有任何协商的扩展定义这种非零值的含义,则接收端点必须失败连接。
  • opcode(4位):说明帧代表什么。以下值目前正在使用中:

0x00:代表继续帧。
0x01:代表文本帧。
0x02:代表二进制帧。
0x08:该帧终止连接。
0x09:这个帧是一个ping。
0x0a:这个帧是一个pong。
(正如您所看到的,有足够的值未被使用;它们已被保留供将来使用)。

  • 掩码(1位):指示连接是否被屏蔽。就目前而言,从客户端到服务器的每条消息都必须被屏蔽,并且规范会在未被屏蔽的情况下终止连接。
  • payload_len(7比特):有效载荷的长度。 WebSocket帧包含以下长度括号:
    0-125表示有效载荷的长度。 126表示以下两个字节表示长度,127表示接下来的8个字节表示长度。所以有效负载的长度在〜7位,16位和64位括号内。
  • 屏蔽键(32位):从客户端发送到服务器的所有帧都被帧中包含的32位值屏蔽。
  • 有效载荷:最可能被掩盖的实际数据。它的长度是payload_len的长度。

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

关于帧的数据

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

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

socket.onopen = function(event) {
  socket.send('Some message'); // Sends data to server.
};

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

// Handle messages sent by the server.
socket.onmessage = function(event) {
  var message = event.data;
  console.log(message);
};

您可以使用Chrome DevTools中的Network选项卡轻松浏览WebSocket连接中每个帧中的数据:

碎片

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

合并帧的逻辑大致如下:

  • 接收第一帧
  • 记得操作码
  • 将帧有效载荷连接在一起,直到fin位被设置
  • 断言每个包的操作码都是零

分段的主要目的是在消息启动时允许发送未知大小的消息。通过分段,服务器可以选择合理大小的缓冲区,并且当缓冲区满时,将一个片段写入网络。分片的次要用例是多路复用,其中一个逻辑信道上的大消息接管整个输出信道是不可取的,因此多路复用需要自由将消息分成更小的片段以更好地共享输出渠道。

什么是心跳?

在握手之后的任何时候,客户端或服务器都可以选择向对方发送ping命令。当收到ping时,收件人必须尽快发回pong。这是一个心跳。您可以使用它来确保客户端仍处于连接状态。

ping或pong只是一个常规帧,但它是一个控制帧。 ping具有0x9的操作码,并且pongs具有0xA的操作码。当你得到一个ping之后,发回一个与ping完全相同的Payload Data的pong(对于ping和pongs,最大有效载荷长度是125)。你也可能在没有发送ping的情况下得到一个pong。如果发生,请忽略它。

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

处理错误

您可以通过监听错误事件来处理发生的任何错误。

它看起来像这样:

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

// Handle any error that occurs.
socket.onerror = function(error) {
  console.log('WebSocket Error: ' + error);
};

关闭连接

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

这是您如何启动从客户端关闭WebSocket连接的方式:

// Close if the connection is open.
if (socket.readyState === WebSocket.OPEN) {
    socket.close();
}

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

// Do necessary clean up.
socket.onclose = function(event) {
  console.log('Disconnected from WebSocket.');
};

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

connection.on('close', function(reasonCode, description) {
    // The connection is getting closed.
});

WebSockets和HTTP/2对比

尽管HTTP/2具有很多功能,但并不能完全取代现有推送/流媒体技术的需求。

关于HTTP/2的第一件重要事情是,它不能代替所有的HTTP。 动词,状态代码和大部分标题将保持与今天相同。HTTP/2是关于提高数据在线路上传输方式的效率。

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

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

这是Server-Sent Events(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连接可以包含多个同时打开的流,其中来自多个流的端点交织帧。
multiplexing

我们必须记住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具有优势,因为它已经以较少的开销构建用于双向能力(例如头)。

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

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

如果您的使用案例需要显示实时市场新闻,市场数据,聊天应用程序等,依靠HTTP/2 + SSE将为您提供高效的双向沟通​​渠道,同时获得留在HTTP世界的好处:

  • 当考虑到与现有Web基础架构的兼容性时,WebSocket通常会成为痛苦的源头,因为它将HTTP连接升级到与HTTP无关的完全不同的协议。
  • 规模和安全性:Web组件(防火墙,入侵检测,负载平衡器)是以HTTP为基础构建,维护和配置的,这是大型/关键应用程序在弹性,安全性和可伸缩性方面更喜欢的环境。

另外,您必须考虑浏览器支持。看看WebSocket:
图片描述

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

然而,HTTP/2的情况并不相同:
图片描述

  • 仅TLS(不是很糟糕)
  • 部分支持IE 11,但仅限于Windows 10
  • 仅在Safari中支持OSX 10.11+
  • 如果您可以通过ALPN进行协商,则仅支持HTTP/2(您的服务器需要明确支持)

SSE支持的更好:
图片描述

只有IE / Edge不提供支持。(好吧,Opera Mini既不支持SSE也不支持WebSocket,所以我们可以将其完全排除在外)。 在IE / Edge中有一些体面的polyfills用于SSE支持。


xupea
124 声望39 粉丝

致力于前端技术,Scrum敏捷开发和STEAM教育