This is the fifth article in a series dedicated to exploring JavaScript and the components it is built upon.
Related free learning recommendations: javascript (Video)
This time, we will delve into the realm of communication protocols, mapping and exploring their properties, and building some components in the process. Quick comparison between WebSockets and HTTP/2. Finally, we share some tips on how to choose a network protocol.
Introduction
Today, complex web applications with rich feature-rich, dynamic UI are taken for granted. It’s no surprise – the internet has come a long way since its inception.
Originally, the Internet was not built to support such dynamic and complex web applications. It is considered a collection of HTML pages that are linked to each other to form a "web" concept that contains information. Everything is built around the so-called request/response paradigm of HTTP. The client loads a page and then nothing happens until the user clicks and navigates to the next page.
Around 2005, AJAX was introduced, and many people began to explore the possibility of establishing a two-way connection between the client and the server. Nonetheless, all HTTP communication is directed by the client, which requires user interaction or periodic polling to load new data from the server.
Making HTTP a "two-way" interaction
Technology that allows the server to "actively" send data to the client has been around for quite some time. For example "Push" and "Comet".
One of the most common hacking methods is to give the server the illusion that it needs to send data to the client, which is called long polling. With long polling, the client opens an HTTP connection to the server and keeps it open until a response is sent. This is sent as a response whenever the server has new data to send.
Look at what a very simple long polling code snippet looks like:
(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); })();
This is basically a self-executing function that sets 10 seconds the first time it runs immediately Interval, after each asynchronous Ajax call to the server, the callback will call Ajax again.
Other technologies involve Flash or XHR multipart requests and so-called htmlfiles.
However, all of these workspaces have the same problem: they all come with the overhead of HTTP, which makes them unsuitable for low-latency applications. Think of an in-browser multiplayer first-person shooter or any other online game with a real-time component.
Introduction of WebSockets
The WebSocket specification defines an API for establishing a "socket" connection between a web browser and a server. Simply put: there is a long-lasting connection between the client and the server, and both parties can start sending data at any time.
The client establishes a WebSocket connection through the WebSocket Handshake process. The process begins with the client sending a regular HTTP request to the server, which contains an Upgrade header that informs the server that the client wishes to establish a WebSocket connection.
The client establishes a WebSocket connection as follows:
// Create a new WebSocket with an encrypted connection. var socket = new WebSocket('ws://websocket.example.com')
WebSocket url uses the ws scheme. There is also wss for secure WebSocket connections, equivalent to HTTPS.
This solution is just the beginning of opening a WebSocket connection to websocket.example.com.
Here is a simplified example of the initial request headers:
If the server supports the WebSocket protocol, it will agree to the upgrade and pass the Upgrade header in the response communicate.
Node.js implementation:
After the connection is established, the server responds by updating the content in the header:
Once the connection is established, the open event will be fired on the client WebSocket instance:
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.'); };
Now that the handshake is complete, the initial HTTP connection is connected to the WebSocket using the same underlying TCP/IP connection replace. At this point, both parties can start sending data.
使用 WebSockets,可以传输任意数量的数据,而不会产生与传统 HTTP 请求相关的开销。数据作为消息通过 WebSocket 传输,每个消息由一个或多个帧组成,其中包含正在发送的数据(有效负载)。为了确保消息在到达客户端时能够正确地进行重构,每一帧都以负载的4-12字节数据为前缀, 使用这种基于帧的消息传递系统有助于减少传输的非有效负载数据量,从而大大的减少延迟。
注意:值得注意的是,只有在接收到所有帧并重构了原始消息负载之后,客户机才会收到关于新消息的通知。
WebSocket URLs
之前简要提到过 WebSockets 引入了一个新的URL方案。实际上,他们引入了两个新的方案:ws:// 和wss://。
url 具有特定方案的语法。WebSocket url 的特殊之处在于它们不支持锚点(#sample_anchor)。
同样的规则适用于 WebSocket 风格的url和 HTTP 风格的 url。ws 是未加密的,默认端口为80,而 wss 需要TLS加密,默认端口为 443。
帧协议
更深入地了解帧协议,这是 RFC 为我们提供的:
在RFC 指定的 WebSocket 版本中,每个包前面只有一个报头。然而,这是一个相当复杂的报头。以下是它的构建模块:
FIN
:1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧,Firefox 在 32K 之后创建了第二个帧。RSV1,RSV2,RSV3
:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。Opcode
:4bit,解释 Payload 数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:
Mask
:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。Masking-key
:域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。Payload_len
:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。Masking-key
:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。Payload data
:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。为什么 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
位。因此,可以将字符串“Hello World”发送到11个包中,每个包的长度为6(报头长度)+ 1字节。控件包不允许分片。但是,规范希望能够处理交错的控制帧。这是TCP包以任意顺序到达的情况。
连接帧的逻辑大致如下:
分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。
什么是跳动检测?
主要目的是保障客户端 websocket 与服务端连接状态,该程序有心跳检测及自动重连机制,当网络断开或者后端服务问题造成客户端websocket断开,程序会自动尝试重新连接直到再次连接成功。
在使用原生websocket的时候,如果设备网络断开,不会触发任何函数,前端程序无法得知当前连接已经断开。这个时候如果调用 websocket.send 方法,浏览器就会发现消息发不出去,便会立刻或者一定短时间后(不同浏览器或者浏览器版本可能表现不同)触发 onclose 函数。
后端 websocket 服务也可能出现异常,连接断开后前端也并没有收到通知,因此需要前端定时发送心跳消息 ping,后端收到 ping 类型的消息,立马返回 pong 消息,告知前端连接正常。如果一定时间没收到pong消息,就说明连接不正常,前端便会执行重连。
为了解决以上两个问题,以前端作为主动方,定时发送 ping 消息,用于检测网络和前后端连接问题。一旦发现异常,前端持续执行重连逻辑,直到重连成功。
错误处理
以通过监听 error 事件来处理所有错误:
var socket = new WebSocket('ws://websocket.example.com'); // Handle any error that occurs. socket.onerror = function(error) { console.log('WebSocket Error: ' + error); };
关闭连接
要关闭连接,客户机或服务器都应该发送包含操作码0x8
的数据的控制帧。当接收到这样一个帧时,另一个对等点发送一个关闭帧作为响应,然后第一个对等点关闭连接,关闭连接后接收到的任何其他数据都将被丢弃:
// 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 。verb、状态码和大部分头信息将保持与目前版本一致。HTTP/2 是意在提升数据在线路上传输的效率。
比较HTTP/2和WebSocket,可以看到很多相似之处:
正如我们在上面看到的,HTTP/2引入了 Server Push,它使服务器能够主动地将资源发送到客户机缓存。但是,它不允许将数据下推到客户机应用程序本身,服务器推送只由浏览器处理,不会在应用程序代码中弹出,这意味着应用程序没有API来获取这些事件的通知。
这就是服务器发送事件(SSE)变得非常有用的地方。SSE 是一种机制,它允许服务器在建立客户机-服务器连接之后异步地将数据推送到客户机。然后,只要有新的“数据块”可用,服务器就可以决定发送数据。它可以看作是单向发布-订阅模式。它还提供了一个名为 EventSource API 的标准JavaScript,作为W3C HTML5标准的一部分,在大多数现代浏览器中实现。不支持 EventSource API 的浏览器可以轻松地使用 polyfilled 方案来解决。
由于 SSE 基于 HTTP ,因此它与 HTTP/2 非常合适,可以结合使用以实现最佳效果:HTTP/2 处理基于多路复用流的高效传输层,SSE 将 API 提供给应用以启用数据推送。
为了理解 Streams 和 Multiplexing 是什么,首先看一下`IETF
定义:“stream”是在HTTP/2 连接中客户机和服务器之间交换的独立的、双向的帧序列。它的一个主要特征是,一个HTTP/2 连接可以包含多个并发打开的流,任何一个端点都可以从多个流中交错帧。
SSE is based on HTTP, which means that in HTTP/2, not only can multiple SSE streams be interleaved onto a single TCP connection, but also multiple SSE streams (server-to-client push) and multiple clients can be side request (client to server). Because of HTTP/2 and SSE, there is now a pure HTTP bidirectional connection and a simple API for application code to register with the server push service. When comparing SSE and WebSocket, the lack of bidirectional capabilities is often cited as a major drawback. With HTTP/2, this is no longer the case. This allows you to skip WebSocket and stick with the HTTP-based signaling mechanism.
How to choose WebSocket and HTTP/2?
WebSockets will survive in the field of HTTP/2 SSE, mainly because it is an already well-established technology, and in very specific use cases it has advantages over HTTP/2 because it has been built for bidirectional functionality with less overhead (such as headers).
Suppose you are building a massively multiplayer online game that requires a large number of messages from both ends of the connection. In this case, WebSockets perform much better.
In general, use WebSockets whenever you need a truly low latency, near real-time connection between client and server, which may require rethinking how you build server-side applications, As well as shifting focus to technologies such as queued events.
If you use a solution that needs to display real-time market news, market data, chat applications, etc., relying on HTTP/2 SSE will provide you with an efficient two-way communication channel while getting the various benefits of staying in the HTTP realm:
The above is the detailed content of JavaScript deep dive into websockets and HTTP/2 with SSE + how to choose the right path!. For more information, please follow other related articles on the PHP Chinese website!