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

2018-02-03 10:27:11来源:https://www.oschina.net/translate/how-does-javascript-actual作者:开源中国翻译文章人点击

分享

这是专门探索 JavaScript 及其所构建的组件的系列文章的第5部分。在识别和描述核心元素的过程中,我们还分享了构建 SessionStack (这是一个轻量级 JavaScript 应用程序,为了保持竞争力它必须是健壮和高性能的)时使用的一些经验法则。


如果你错过了前面的章节,你可以在这里找到它们:



解读 JavaScript 之引擎、运行时和堆栈调用


解读 JavaScript 之 V8 引擎及优化代码的 5 个技巧


解读 JavaScript 之内存管理和常见内存泄露处理


解读 JavaScript 之事件循环和异步编程



这一次,我们将深入到通信协议的领域,映射和探讨他们的属性,并在此过程中构建部分组件。我们将提供一个 WebSockets 和 HTTP/2 的快速比较。最后,我们分享一些关于如何选择网络协议的方法。



引言

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


最初,互联网并不是为了支持这种动态和复杂的 Web 应用而设计的。它被认为是一系列 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 或 XHRmultipart request 和所谓的 htmlfiles 。


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



WebSockets 的引入

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




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


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


// Create a new WebSocket with an encrypted connection.
var socket = new WebSocket('ws://websocket.example.com');

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


这个方案只是打开 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 标头进行通信。


我们来看看如何在 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
});
});

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


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:// 。


URL 具有特定方案的语法。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 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 非常简单:


var socket = new WebSocket('ws://websocket.example.com');
socket.onopen = function(event) {
socket.send('Some message'); // Sends data to server.
};

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


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

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




分片

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


连接帧的逻辑大致如下:



接收第一帧


记住操作码


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


断言每个包的操作码是零



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



最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台