JavaScript工做原理(五):深刻了解WebSockets,HTTP/2和SSE,以及如何选择

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

概述

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

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

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

使HTTP成为“双向”

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

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

咱们来看看一个很是简单的长轮询片断的样子:api

(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支持。

相关文章
相关标签/搜索