问题:低延迟的客户端-服务器及服务器-客户端连接
Web 在很大程度上是围绕 HTTP 的所谓请求/响应范式构建的。客户端加载网页后,在用户点击下一个网页之前,系统不会执行任何操作。大约在 2005 年左右,AJAX 开始让 Web 变得更加动态。不过,所有 HTTP 通信都是由客户端引导的,这需要用户互动或定期轮询才能从服务器加载新数据。
能够让服务器在知道有新数据可用时立即将数据发送到客户端的技术已经存在很长一段时间了。它们使用诸如“Push”或 Comet 之类的名称。为了营造服务器发起连接的假象,最常用的一种方法是长轮询。使用长轮询时,客户端会打开与服务器的 HTTP 连接,并将其保持打开状态,直到发送响应。每当服务器实际有新数据时,都会发送响应(其他技术涉及 Flash、XHR 多部分请求和所谓的 htmlfiles)。长轮询和其他技术非常有效。您每天都在 Gmail 聊天等应用中使用它们。
然而,所有这些解决方法都存在一个共同的问题:它们带有 HTTP 的开销,因此它们并不适合低延迟应用。例如,浏览器中的多人第一人称射击游戏,或包含实时组件的任何其他在线游戏。
WebSocket 简介:将套接字引入 Web
WebSocket 规范定义了一个 API,用于在网络浏览器和服务器之间建立“套接字”连接。通俗地讲:客户端与服务器之间存在持久连接,并且双方可以随时开始发送数据。
使用入门
只需调用 WebSocket 构造函数即可打开 WebSocket 连接:
var connection = new WebSocket('ws://html5rocks.websocket.org/echo', ['soap', 'xmpp']);
请注意 ws:
。这是 WebSocket 连接的新网址架构。还有用于安全 WebSocket 连接的 wss:
,就像 https:
用于安全 HTTP 连接一样。
通过立即将一些事件处理脚本附加到连接,您可以知道何时打开连接、收到传入消息或出现错误。
第二个参数接受可选的子协议。它可以是字符串,也可以是字符串数组。每个字符串都应表示一个子协议名称,并且服务器仅接受数组中传递的子协议之一。您可以通过访问 WebSocket 对象的 protocol
属性来确定接受的子协议。
子协议名称必须是 IANA 注册表中已注册的子协议名称之一。截至 2012 年 2 月,目前只有一个注册子协议名称 (soap)。
// When the connection is open, send some data to the server
connection.onopen = function () {
connection.send('Ping'); // Send the message 'Ping' to the server
};
// Log errors
connection.onerror = function (error) {
console.log('WebSocket Error ' + error);
};
// Log messages from the server
connection.onmessage = function (e) {
console.log('Server: ' + e.data);
};
与服务器通信
一旦建立了与服务器的连接(即 open
事件触发时),我们就可以开始使用连接对象上的 send('your message')
方法向服务器发送数据。它过去仅支持字符串,但在最新规范中,它现在也可以发送二进制消息。如需发送二进制数据,您可以使用 Blob
或 ArrayBuffer
对象。
// Sending String
connection.send('your message');
// 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];
}
connection.send(binary.buffer);
// Sending file as Blob
var file = document.querySelector('input[type="file"]').files[0];
connection.send(file);
同样,服务器可能随时向我们发送消息。每当发生这种情况时,系统都会触发 onmessage
回调。回调会接收事件对象,并且可以通过 data
属性访问实际消息。
WebSocket 也可以接收采用最新规范的二进制消息。二进制帧可以采用 Blob
或 ArrayBuffer
格式接收。如需指定收到的二进制文件的格式,请将 WebSocket 对象的 binaryType 属性设置为“blob”或“arraybuffer”。默认格式为“blob”。(您无需在发送时对齐 binaryType 参数。)
// Setting binaryType to accept received binary as either 'blob' or 'arraybuffer'
connection.binaryType = 'arraybuffer';
connection.onmessage = function(e) {
console.log(e.data.byteLength); // ArrayBuffer object if binary
};
WebSocket 的另一项新功能是扩展程序。使用扩展程序,您可以发送压缩、多路复用等帧。您可以通过在打开事件后检查 WebSocket 对象的 extensions 属性来查找服务器接受的扩展程序。截至 2012 年 2 月,还没有正式发布的扩展程序规范。
// Determining accepted extensions
console.log(connection.extensions);
跨源通信
作为一种新型协议,跨源通信已直接内置到 WebSocket 中。虽然您仍应确保仅与您信任的客户端和服务器通信,但 WebSocket 支持任何网域中的各方进行通信。服务器决定是将其服务提供给所有客户端,还是仅提供给位于一组明确定义的网域中的客户端。
代理服务器
每项新技术都会带来一系列新问题。对于 WebSocket,则是与代理服务器的兼容性,代理服务器在大多数公司网络中充当 HTTP 连接的中介。WebSocket 协议使用 HTTP 升级系统(通常用于 HTTP/SSL)将 HTTP 连接“升级”为 WebSocket 连接。某些代理服务器不喜欢这样做,会断开连接。因此,即使给定客户端使用 WebSocket 协议,也可能无法建立连接。这使得下一部分变得更加重要 :)
立即使用 WebSocket
WebSocket 仍是一项新兴技术,并未在所有浏览器中全面实现。不过,当 WebSocket 不可用时,您可以通过使用上述某种回退方案的库使用 WebSocket。在该领域非常流行的一个库是 socket.io,它附带了该协议的客户端和服务器实现,并包含回退机制(截至 2012 年 2 月,socket.io 尚不支持二进制消息传递)。还有一些商业解决方案,例如 PusherApp,它可以通过提供 HTTP API 来轻松集成到任何 Web 环境,以便向客户端发送 WebSocket 消息。与纯 WebSocket 相比,由于需要额外的 HTTP 请求,因此始终会产生额外的开销。
服务器端
使用 WebSocket 可为服务器端应用创建全新的使用模式。虽然 LAMP 等传统服务器堆栈是围绕 HTTP 请求/响应周期设计的,但它们通常无法很好地处理大量打开的 WebSocket 连接。要同时保持大量连接处于打开状态,需要一种能以低性能成本接收高并发数据的架构。此类架构通常围绕线程或所谓的非阻塞 IO 进行设计。
服务器端实现
- Node.js
- Java
- Ruby
- Python
- Erlang
- C++
- .NET
协议版本
WebSocket 的线协议(握手和客户端与服务器之间的数据传输)现已采用 RFC6455。最新的 Chrome 浏览器和 Android 版 Chrome 浏览器与 RFC6455 完全兼容,包括二进制消息传递。此外,Firefox 将与版本 11 兼容,Internet Explorer 将与版本 10 兼容。您仍然可以使用较低的协议版本,但我们不建议这样做,因为这些版本已知存在漏洞。如果您有旧版 WebSocket 协议的服务器实现,我们建议您将其升级到最新版本。
使用场景
每当您需要在客户端和服务器之间建立真正低延迟的近乎实时连接时,请使用 WebSocket。请注意,这可能需要您重新思考如何构建服务器端应用,并将重点放在事件队列等技术上。一些示例用例包括:
- 多人在线游戏
- 聊天应用
- 体育赛事实时滚动字幕
- 实时更新的社交媒体动态