Home > Article > Backend Development > WebSocket detailed introduction
In the process of building network applications, we often need to continuously communicate with the server to keep the information of both parties synchronized. Usually this kind of persistent communication is carried out without refreshing the page, consumes a certain amount of memory resources and stays in the background, and is invisible to the user. Before WebSocket appeared, we had the following solution:
A common continuous communication method in current Web applications, usually Use setInterval
or setTimeout
to implement. For example, if we want to regularly obtain and refresh the data on the page, we can combine Ajax to write the following implementation:
setInterval(function() { $.get("/path/to/server", function(data, status) { console.log(data); }); }, 10000);
The above program will request data from the server every 10 seconds and store the data after it arrives. This implementation method can usually meet simple needs, but it also has major flaws: when the network situation is unstable, the total time from the server receiving the request, sending the request to the client receiving the request may exceed 10 seconds. The requests are sent at 10-second intervals, which will cause the arrival order of the received data to be inconsistent with the sending order. So the polling method using setTimeout
appeared:
function poll() { setTimeout(function() { $.get("/path/to/server", function(data, status) { console.log(data); // 发起下一次请求 poll(); }); }, 10000); }
The program first sets the request after 10 seconds, and then initiates a second request every 10 seconds after the data is returned, and so on. In this case, although the time interval between two requests cannot be guaranteed to be a fixed value, the order in which the data arrives can be guaranteed.
Both the above two traditional polling methods have a serious flaw: the program will create a new HTTP request every time it is requested, but not every time can return the required new data. When the number of requests initiated at the same time reaches a certain number, it will cause a greater burden on the server. At this time we can use long polling to solve this problem.
Long polling and the server-sent events and WebSocket mentioned below cannot be implemented solely by client-side JavaScript. We also need server support and implementation of corresponding technologies.
The basic idea of long polling is that after each client sends a request, the server checks whether there is an update between the data returned last time and the data at the time of this request. If there is an update, it returns the new data and End this connection, otherwise the server hold will hold this connection until there is new data and then return the response. This long-term connection can be achieved by setting a larger
HTTP timeout`. The following is a simple long connection example:
Server (PHP):
<?php // 示例数据为data.txt $filename= dirname(__FILE__)."/data.txt"; // 从请求参数中获取上次请求到的数据的时间戳 $lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ; // 将文件的最后一次修改时间作为当前数据的时间戳 $currentmodif = filemtime($filename); // 当上次请求到的数据的时间戳*不旧于*当前文件的时间戳,使用循环"hold"住当前连接,并不断获取文件的修改时间 while ($currentmodif <= $lastmodif) { // 每次刷新文件信息的时间间隔为10秒 usleep(10000); // 清除文件信息缓存,保证每次获取的修改时间都是最新的修改时间 clearstatcache(); $currentmodif = filemtime($filename); } // 返回数据和最新的时间戳,结束此次连接 $response = array(); $response["msg"] =Date("h:i:s")." ".file_get_contents($filename); $response["timestamp"]= $currentmodif; echo json_encode($response); ?>
Client:
function longPoll (timestamp) { var _timestamp; $.get("/path/to/server?timestamp=" + timestamp) .done(function(res) { try { var data = JSON.parse(res); console.log(data.msg); _timestamp = data.timestamp; } catch (e) {} }) .always(function() { setTimeout(function() { longPoll(_timestamp || Date.now()/1000); }, 10000); }); }
Long polling can effectively solve the problems caused by traditional polling Bandwidth is wasted, but maintaining each connection comes at the cost of consuming server resources. Especially for the Apache+PHP server, due to the default number of worker threads
, when there are many long connections, the server cannot respond to new requests.
Server-Sent Event (hereinafter referred to as SSE)
is an integral part of the HTML 5 specification and can implement server-to-client communication. One-way data communication. With SSE, clients can automatically get data updates without having to send repeated HTTP requests. Once the connection is established, "events" are automatically pushed to the client. Server-side SSE generates and pushes events in the format of Event Stream
. The MIME type corresponding to the event stream is text/event-stream
, which contains four fields: event, data, id and retry. event represents the event type, data represents the message content, id is used to set the last event ID string
internal property of the client EventSource
object, and retry specifies the reconnection time.
Server (PHP):
<?php header("Content-Type: text/event-stream"); header("Cache-Control: no-cache"); // 每隔1秒发送一次服务器的当前时间 while (1) { $time = date("r"); echo "event: ping\n"; echo "data: The server time is: {$time}\n\n"; ob_flush(); flush(); sleep(1); } ?>
In the client, SSE is implemented through the EventSource
object. EventSource
contains five external properties: onerror, onmessage, onopen, readyState, url, and two internal properties: reconnection time
and last event ID string
. In the onerror attribute, we can capture and process errors, and onmessage
corresponds to the reception and processing of server events. In addition, you can also use the addEventListener
method to listen for events sent by the server and process them differently based on the event field.
Client:
var eventSource = new EventSource("/path/to/server"); eventSource.onmessage = function (e) { console.log(e.event, e.data); } // 或者 eventSource.addEventListener("ping", function(e) { console.log(e.event, e.data); }, false);
SSE has better real-time performance than polling, and its use is also very simple. However, SSE only supports one-way event push from server to client, and all versions of IE (including Microsoft Edge so far) do not support SSE. If you need to force support for IE and some mobile browsers, you can try EventSource Polyfill
(essentially still polling). The browser support of SSE is shown in the figure below:
Tradition Polling | Long Polling | Server Sent Event | WebSocket | |
---|---|---|---|---|
Almost all modern browsers | Almost all modern browsers | Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+ | IE 10+ Edge Firefox 4 + Chrome 4+ Safari 5+ Opera 11.5+ | |
Less CPU resources, more memory resources and bandwidth resources | Similar to traditional polling, but takes up less bandwidth | Similar to long polling, except that the server does not need to disconnect after each request is sent | No need to wait in a loop (long polling), CPU and memory resources are not measured by the number of clients, but by the number of client events. The best performance among the four methods. | |
occupies more memory resources and number of requests. | Similar to traditional polling. | Native implementation in the browser, occupying very little resources. | Same as Server-Sent Event. | |
Non-real time, delay depends on the request interval. | Same as traditional polling. | Non-real-time, default delay is 3 seconds, delay can be customized. | real time. | |
is very simple. | Requires server cooperation, and client implementation is very simple. | Requires server cooperation, and client implementation is even simpler than the first two. | Requires Socket program implementation and additional ports, and the client implementation is simple. |
Status Code | Name | Description |
---|---|---|
0–999 | Reserved section, unused. | |
1000 | CLOSE_NORMAL | Close normally; regardless of the purpose for which it was created, the link has been Successfully completed the task. |
1001 | CLOSE_GOING_AWAY | The terminal left, maybe because of a server error, or because the browser was opening the connected page Jump away. |
1002 | CLOSE_PROTOCOL_ERROR | Connection interrupted due to protocol error. |
CLOSE_UNSUPPORTED | The connection was disconnected due to receipt of a data type that is not allowed (e.g. a terminal that only receives text data received binary data). | |
Reserved. Its meaning may be defined in the future. | ||
CLOSE_NO_STATUS | Reserved. Indicates not received Expected status code. | |
CLOSE_ABNORMAL | Reserved. Used when the connection is abnormally closed when a status code is expected to be received (that is, No closing frame was sent). | |
Unsupported Data | The connection was disconnected due to receipt of data that did not conform to the format (such as the text message contained non-UTF-8 data). | |
Policy Violation | The connection was disconnected due to receipt of data that does not conform to the contract. This is A general status code, used in scenarios where the 1003 and 1009 status codes are not suitable. | |
CLOSE_TOO_LARGE | Due to receiving an excessively large data frame and disconnected. | |
Missing Extension | The client expected the server to agree on one or more extensions, but the server did not process it. Therefore, the client disconnects. | |
Internal Error | The client encountered an unexpected situation that prevented it from completing the request, so the service The server was disconnected. | |
Service Restart | The server was disconnected due to restart. | |
Try Again Later | The server is disconnected due to temporary reasons, such as the server is overloaded and some client connections are disconnected. | |
Reserved by the WebSocket standard for future use. | ||
TLS Handshake | Reserved. Indicates that the connection was unable to complete the TLS handshake while closed (e.g. unable to verify server certificate). | |
Reserved by the WebSocket standard for future use. | ||
is reserved for use by the WebSocket extension. | ||
may be used by libraries or frameworks. It should not be used by applications. Can be registered with IANA, first come first served. | ||
Can be used by applications. |
属性名 | 类型 | 描述 |
---|---|---|
binaryType | DOMString | 一个字符串表示被传输二进制的内容的类型。取值应当是"blob"或者"arraybuffer"。"blob"表示使用DOM Blob 对象,而"arraybuffer"表示使用 ArrayBuffer 对象。 |
bufferedAmount | unsigned long | 调用 send()) 方法将多字节数据加入到队列中等待传输,但是还未发出。该值会在所有队列数据被发送后重置为 0。而当连接关闭时不会设为0。如果持续调用send(),这个值会持续增长。只读。 |
extensions | DOMString | 服务器选定的扩展。目前这个属性只是一个空字符串,或者是一个包含所有扩展的列表。 |
onclose | EventListener | 用于监听连接关闭事件监听器。当 WebSocket 对象的readyState 状态变为 CLOSED 时会触发该事件。这个监听器会接收一个叫close的 CloseEvent 对象。 |
onerror | EventListener | 当错误发生时用于监听error事件的事件监听器。会接受一个名为“error”的event对象。 |
onmessage | EventListener | 一个用于消息事件的事件监听器,这一事件当有消息到达的时候该事件会触发。这个Listener会被传入一个名为"message"的 MessageEvent 对象。 |
onopen | EventListener | 一个用于连接打开事件的事件监听器。当readyState的值变为 OPEN 的时候会触发该事件。该事件表明这个连接已经准备好接受和发送数据。这个监听器会接受一个名为"open"的事件对象。 |
protocol | DOMString | 一个表明服务器选定的子协议名字的字符串。这个属性的取值会被取值为构造器传入的protocols参数。 |
readyState | unsigned short | 连接的当前状态。取值是 Ready state constants 之一。 只读。 |
url | DOMString | 传入构造器的URL。它必须是一个绝对地址的URL。只读。 |
实例对象的 onopen 属性,用于指定连接成功后的回调函数。
ws.onopen = function () { ws.send('Hello Server!'); }
如果要指定多个回调函数,可以使用addEventListener方法。
ws.addEventListener('open', function (event) { ws.send('Hello Server!'); });
实例对象的 onclose 属性,用于指定连接关闭后的回调函数。
ws.onclose = function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event }; ws.addEventListener("close", function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event });
实例对象的 onmessage 属性,用于指定收到服务器数据后的回调函数。
ws.onmessage = function(event) { var data = event.data; // 处理数据 }; ws.addEventListener("message", function(event) { var data = event.data; // 处理数据 });
注意,服务器数据可能是文本,也可能是 二进制数据(blob对象或Arraybuffer对象)。
ws.onmessage = function(event){ if(typeof event.data === String) { console.log("Received data string"); } if(event.data instanceof ArrayBuffer){ var buffer = event.data; console.log("Received arraybuffer"); } }
除了动态判断收到的数据类型,也可以使用 binaryType
属性,显式指定收到的二进制数据类型。
// 收到的是 blob 数据 ws.binaryType = "blob"; ws.onmessage = function(e) { console.log(e.data.size); }; // 收到的是 ArrayBuffer 数据 ws.binaryType = "arraybuffer"; ws.onmessage = function(e) { console.log(e.data.byteLength); };
这些常量是 readyState
属性的取值,可以用来描述 WebSocket 连接的状态。
常量 | 值 | 描述 |
---|---|---|
CONNECTING | 0 | 连接还没开启。 |
OPEN | 1 | 连接已开启并准备好进行通信。 |
CLOSING | 2 | 连接正在关闭的过程中。 |
CLOSED | 3 | 连接已经关闭,或者连接无法建立。 |
关闭 WebSocket 连接或停止正在进行的连接请求。如果连接的状态已经是 closed
,这个方法不会有任何效果
void close(in optional unsigned short code, in optional DOMString reason);
code 可选
一个数字值表示关闭连接的状态号,表示连接被关闭的原因。如果这个参数没有被指定,默认的取值是1000 (表示正常连接关闭)。 请看 CloseEvent 页面的 list of status codes来看默认的取值。
reason 可选
一个可读的字符串,表示连接被关闭的原因。这个字符串必须是不长于123字节的UTF-8 文本(不是字符)。
可能抛出的异常
INVALID_ACCESS_ERR:选定了无效的code。
SYNTAX_ERR:reason 字符串太长或者含有 unpaired surrogates
。
通过 WebSocket 连接向服务器发送数据。
void send(in DOMString data); void send(in ArrayBuffer data); void send(in Blob data);
data:要发送到服务器的数据。
可能抛出的异常:
INVALID_STATE_ERR:当前连接的状态不是OPEN。
SYNTAX_ERR:数据是一个包含 unpaired surrogates
的字符串。
发送文本的例子。
ws.send('your message');
发送 Blob 对象的例子。
var file = document .querySelector('input[type="file"]') .files[0]; ws.send(file);
发送 ArrayBuffer 对象的例子。
// Sending canvas ImageData as ArrayBuffer var img = canvas_context.getImageData(0, 0, 400, 320); var binary = new Uint8Array(img.data.length); for (var i = 0; i < img.data.length; i++) { binary[i] = img.data[i]; } ws.send(binary.buffer);
WebSocket 服务器的实现,可以查看维基百科的列表。
常用的 Node 实现有以下三种。
Socket.IO
µWebSockets
WebSocket-Node
WebSocket 是基于 TCP 的独立的协议。它与 HTTP 唯一的关系是它的握手是由 HTTP 服务器解释为一个 Upgrade 请求。
WebSocket协议试图在现有的 HTTP 基础设施上下文中解决现有的双向HTTP技术目标;同样,它被设计工作在HTTP端口80和443,也支持HTTP代理和中间件,
HTTP服务器需要发送一个“Upgrade”请求,即101 Switching Protocol到HTTP服务器,然后由服务器进行协议转换。
前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept
在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
避免服务端收到非法的 websocket 连接(比如 http 客户端不小心请求连接 websocket 服务,此时服务端可以直接拒绝连接)
确保服务端理解 websocket 连接。因为 ws 握手阶段采用的是 http 协议,因此可能 ws 连接是被一个 http 服务器处理并返回的,此时客户端可以通过 Sec-WebSocket-Key
来确保服务端认识 ws 协议。(并非百分百保险,比如总是存在那么些无聊的 http 服务器,光处理 Sec-WebSocket-Key
,但并没有实现 ws 协议。。。)
用浏览器里发起 ajax 请求,设置 header 时,Sec-WebSocket-Key
以及其他相关的 header 是被禁止的。这样可以避免客户端发送 ajax 请求时,意外请求协议升级(websocket upgrade)
可以防止反向代理(不理解 ws 协议)返回错误的数据。比如反向代理前后收到两次 ws 连接的升级请求,反向代理把第一次请求的返回给 cache 住,然后第二次请求到来时直接把 cache 住的请求给返回(无意义的返回)。
Sec-WebSocket-Key
主要目的并不是确保数据的安全性,因为 Sec-WebSocket-Key
、Sec-WebSocket-Accept
的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。
那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。
答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)
等问题。
相关推荐:
The above is the detailed content of WebSocket detailed introduction. For more information, please follow other related articles on the PHP Chinese website!