正如你在第 11 章中了解到的,HTTP 是围绕请求-响应模型设计的。所有 HTTP 通信都是由客户端向服务器发出请求而发起的。然后,服务器用请求的数据响应客户机。在网络的早期,这种模式是可行的,因为网站是链接到其他静态 HTML 页面的静态 HTML 页面。然而,网络已经进化,网站不再仅仅是静态页面。
像 Ajax 这样的技术使 web 变得动态和数据驱动,并使一类 web 应用能够与本地应用相媲美。Ajax 调用仍然发出 HTTP 请求,但是它们不是从服务器检索整个文档,而是只请求一小部分数据来更新现有页面。Ajax 调用更快,因为它们每个请求传输的字节更少。它们还通过平滑更新当前页面而不是强制刷新整个页面来改善用户体验。
对于 Ajax 带来的一切,它仍然有很大的改进空间。首先,每个 Ajax 请求都是一个完整的 HTTP 请求。这意味着,如果应用使用 Ajax 只是为了向服务器报告信息(例如,一个分析应用),服务器仍然会浪费时间发回一个空响应。
Ajax 的第二个主要限制是所有的通信仍然必须由客户端发起。客户端发起的通信,被称为拉技术,对于客户端总是想要服务器上可用的最新信息的应用来说是低效的。这些类型的应用更适合推送技术,在这种技术中,通信是由服务器发起的。很适合推动技术发展的应用的例子有体育行情、聊天程序、股票行情和社交媒体新闻。Ajax 请求可以通过多种方式欺骗推送技术,但这些都是不体面的攻击。例如,客户端可以定期向服务器发出请求,但这是非常低效的,因为许多服务器响应可能不包含任何更新。另一种技术,称为长轮询,涉及客户端向服务器发出请求。如果没有新数据,连接就保持打开状态。一旦数据变得可用,服务器将它发送回客户机并关闭连接。然后,客户端立即发出另一个请求,确保打开的连接始终可用于推送数据。由于与服务器的重复连接,长轮询也是低效的。
近年来,HTML5 引入了几种新的浏览器技术,更好地促进了推送技术。这些技术中最突出的是 WebSockets 。WebSockets 使浏览器能够通过全双工通信信道与服务器通信。这意味着客户端和服务器可以同时传输数据。此外,一旦建立了连接,WebSockets 允许客户端和服务器直接通信,而无需发送请求和响应头。基于浏览器的游戏和其他实时应用是 WebSockets 提供的性能提升的最大受益者。
本章介绍了 WebSockets API,并展示了如何使用 Node.js 构建 WebSockets 应用。Socket.IO在 WebSockets 之上提供了一个抽象层,就像 Connect 和 Express 构建在 Node 的http模块上一样。Socket.IO还依靠 Ajax 轮询等技术,为不支持 WebSockets 的旧浏览器提供实时功能。最后,本章最后展示了如何将Socket.IO与 Express 服务器集成。
WebSockets API
尽管客户端开发不是本书的重点,但在创建任何 Node 应用之前,有必要解释一下 WebSockets API。本节解释如何在浏览器中使用 WebSockets。值得注意的是,WebSockets 是 HTML5 相对较新的特性。旧的浏览器,甚至一些当前的浏览器,都不支持 WebSockets。要确定您的浏览器是否支持 WebSockets,请咨询www.caniuse.com。该网站提供了有关哪些浏览器支持特定功能的信息。本节中显示的示例假设您的浏览器支持 WebSockets。
打开 WebSocket
WebSockets 是通过清单 13-1 中的WebSocket()构造函数创建的。构造函数的第一个参数是 WebSocket 将连接到的 URL。当构建 WebSocket 时,它会立即尝试连接到所提供的 URL。没有办法阻止或推迟连接尝试。构造之后,WebSocket 的 URL 可以通过它的url属性访问。WebSocket URLs 看起来就像你习惯的 HTTP URLs 然而,WebSockets 使用ws或wss协议。标准 WebSockets 使用ws协议,默认情况下使用端口 80。另一方面,安全 WebSockets 使用wss协议,默认端口为 443。
清单 13-1 。 WebSocket()构造函数
WebSocket(url, [protocols])构造函数的第二个参数protocols是可选的。如果指定了它,它应该是一个字符串或字符串数组。字符串是子协议名称。使用子协议允许单个服务器同时处理不同的协议。
关闭 WebSockets
要关闭 WebSocket 连接,使用close()方法,其语法如清单 13-2 所示。close()带两个参数,code和reason,都是可选的。code参数是一个数字状态代码,而reason是一个描述close事件环境的字符串。close的支持值如表 13-1 所示。通常,close()是不带参数调用的。
清单 13-2 。WebSocket close()方法
socket.close([code], [reason])表 13-1 。close()支持的状态代码
|
状态代码
|
描述
| | --- | --- | | 0-999 | 保留。 | | 1000 | 正常关闭。在正常情况下,当 WebSocket 关闭时会使用此代码。 | | 第 1001 章 | 离开。可能是服务器出现故障,或者是浏览器离开了该页面。 | | 第 1002 章 | 由于协议错误,连接关闭。 | | 第 1003 章 | 由于收到端点不知道如何处理的数据,连接被终止。一个例子是在需要文本时接收二进制数据。 | | 第 1004 章 | 由于收到过大的数据帧,连接被关闭。 | | 第 1005 章 | 保留。此代码表示没有提供状态代码,尽管应该提供状态代码。 | | 第 1006 章 | 保留。此代码表示连接异常关闭。 | | 1007-1999 | 为 WebSocket 标准的未来版本保留。 | | 2000 年至 2999 年 | 为 WebSocket 扩展保留。 | | 3000-3999 | 这些代码应该由库和框架使用,而不是应用。 | | 4000-4999 | 这些代码可供应用使用。 |
检查 WebSocket 的状态
web socket 的状态可以通过它的readyState属性随时检查。在 WebSocket 的生命周期中,它可以处于表 13-2 中描述的四种可能状态之一。
表 13-2 。WebSocket 的 readyState 属性的可能值
|
状态
|
描述
|
| --- | --- |
| 连接 | 当构造 WebSocket 时,它会尝试连接到它的 URL。在此期间,它被视为处于connecting状态。处于connecting状态的 WebSocket 的readyState值为0。 |
| 打开 | WebSocket 成功连接到它的 URL 后,它进入open状态。WebSocket 必须处于open状态,以便通过网络发送和接收数据。处于open状态的 WebSocket 的readyState值为1。 |
| 关闭 | 当 WebSocket 关闭时,它必须首先与希望断开连接的远程主机通信。在此通信期间,WebSocket 被认为处于closing状态。处于closing状态的 WebSocket 的readyState值为2。 |
| 关闭 | WebSocket 一旦成功断开连接,就会进入closed状态。处于closed状态的 WebSocket 的readyState值为3。 |
因为硬编码常量值不是好的编程实践,所以 WebSocket 接口定义了表示可能的readyState值的静态常量。清单 13-3 展示了如何使用这些常量通过switch语句来评估连接的状态。
清单 13-3 。使用readyState属性确定 WebSocket 的状态
switch (socket.readyState) {
case WebSocket.CONNECTING:
// in connecting state
break;
case WebSocket.OPEN:
// in open state
break;
case WebSocket.CLOSING:
// in closing state
break;
case WebSocket.CLOSED:
// in closed state
break;
default:
// this never happens
break;
}open事件
当 WebSocket 转换到open状态时,它的open事件被触发。清单 13-4 中显示了一个open事件处理程序的例子。事件对象是传递给事件处理程序的唯一参数。
清单 13-4 。一个示例open事件处理程序
socket.onopen = function(event) {
// handle open event
};WebSocket 事件处理程序也可以使用addEventListener()方法来创建。清单 13-5 展示了如何使用addEventListener()来附加同一个open事件处理程序。这种替代语法比onopen更可取,因为它允许多个处理程序附加到同一个事件。
清单 13-5 。使用addEventListener()附加一个open事件处理程序
socket.addEventListener("open", function(event) {
// handle open event
});message事件
当 WebSocket 接收到新数据时,会触发一个message事件。接收到的数据可以通过message事件的data属性获得。清单 13-6 中显示了一个message事件处理程序的例子。在本例中,addEventListener()用于附加事件,但也可以使用onmessage。如果正在接收二进制数据,则在调用事件处理程序之前,应该相应地设置 WebSocket 的binaryType属性。
清单 13-6 。一个示例message事件处理程序
socket.addEventListener("message", function(event) {
var data = event.data;
// process data as string, Blob, or ArrayBuffer
});
注意除了处理字符串数据,WebSockets 还支持两种类型的二进制数据——二进制大型对象( Blob s)和ArrayBuffers。然而,一个单独的 WebSocket 一次只能处理两种二进制格式中的一种。当一个 WebSocket 被创建时,它最初被设置为处理Blob数据。WebSocket 的binaryType属性用于在Blob和ArrayBuffer支持之间进行选择。为了处理Blob数据,WebSocket 的binaryType应该在读取数据之前设置为"blob"。类似地,在试图读取一个ArrayBuffer之前,应当将binaryType设置为"arraybuffer"。
close事件
当 WebSocket 关闭时,会触发一个close事件。传递给close处理程序的事件对象有三个属性,名为code、reason和wasClean。code和reason字段对应于传递给close()的相同名称的自变量。wasClean字段是一个布尔值,它指示连接是否被干净地关闭。一般情况下,wasClean就是true。清单 13-7 中显示了一个close事件处理程序的例子。
清单 13-7 。一个示例close事件处理程序
socket.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});error事件
当 WebSocket 遇到问题时,会触发一个error事件。传递给处理程序的事件是一个标准的错误对象,包括name和message属性。一个 WebSocket error事件处理程序的例子如清单 13-8 所示。
清单 13-8 。一个示例error事件处理程序
socket.addEventListener("error", function(event) {
// handle error event
});发送数据
WebSockets 通过send()方法传输数据,该方法有三种风格——一种用于发送 UTF-8 字符串数据,第二种用于发送ArrayBuffer,第三种用于发送Blob数据。所有三个版本的send()都有一个参数,它代表要传输的数据。send()的语法如清单 13-9 中的所示。
清单 13-9 。使用 WebSocket 的send()方法
socket.send(data)Node 中的 WebSockets
Node 核心不支持 WebSocket,但幸运的是在npm注册表中有大量的第三方 web socket 模块。尽管您可以自由选择任何想要的模块,但本书中的示例使用了ws模块。这一决定背后的理由是,ws速度快、受欢迎、得到很好的支持,并且被用于本章后面将要讨论的Socket.IO库中。
为了演示ws模块是如何工作的,让我们先来看一个例子。清单 13-10 中的代码是一个使用ws、http和connect模块构建的 WebSocket echo 服务器。此服务器接受端口 8000 上的 HTTP 和 WebSocket 连接。Connect 的static中间件允许通过 HTTP 从public子目录提供任意静态内容,而ws处理 WebSocket 连接。
清单 13-10 。使用ws、http和connect模块构建的 WebSocket Echo 服务器
var http = require("http");
var connect = require("connect");
var app = connect();
var WebSocketServer = require("ws").Server;
var server;
var wsServer;
app.use(connect.static("public"));
server = http.createServer(app);
wsServer = new WebSocketServer({
server: server
});
wsServer.on("connection", function(ws) {
ws.on("message", function(message, flags) {
ws.send(message, flags);
});
});
server.listen(8000);要创建服务器的 WebSocket 组件,我们必须首先导入ws模块的Server()构造函数。构造函数存储在清单 13-10 中的WebSocketServer变量中。接下来,通过调用构造函数创建 WebSocket 服务器的实例wsServer。HTTP 服务器server被传递给构造函数,允许 WebSockets 和 HTTP 在同一个端口上共存。从技术上讲,通过将{port: 8000}传递给WebSocketServer()构造函数,可以构建一个没有http和connect的纯 WebSocket 服务器。
当接收到 WebSocket 连接时,调用connection事件处理程序。该处理程序接受一个 WebSocket 实例ws作为它唯一的参数。WebSocket 附加了一个用于从客户端接收数据的message事件处理程序。当接收到数据时,使用 WebSocket 的send()方法将消息及其相关标志简单地回显到客户端。消息标志用于指示消息是否包含二进制数据等信息。
WebSocket 客户端
ws模块还允许创建 WebSockets 客户端。清单 13-10 中与 echo 服务器一起工作的客户端在清单 13-11 中显示。客户端首先导入ws模块作为变量WebSocket。在示例的第二行,构建了一个 WebSocket,它连接到本地机器的端口 8000。回想一下,WebSocket 客户端会立即尝试连接到传递给构造函数的 URL。因此,我们没有告诉 WebSocket 进行连接,而是简单地设置了一个open事件处理程序。一旦建立了连接,open事件处理程序就将字符串"Hello!"发送给服务器。
清单 13-11 。与清单 13-10 中的服务器协同工作的 WebSocket 客户端
var WebSocket = require("ws");
var ws = new WebSocket("ws://localhost:8000");
ws.on("open", function() {
ws.send("Hello!");
});
ws.on("message", function(data, flags) {
console.log("Server says:");
console.log(data);
ws.close();
});一旦服务器接收到消息,它将把它回显给客户机。为了处理传入的数据,我们还必须设置一个message事件处理程序。在清单 13-11 中,message处理程序将数据显示到屏幕上,然后使用close()关闭 WebSocket。
一个 HTML 客户端
因为示例服务器支持 HTTP 和 WebSockets,所以我们可以提供嵌入了 WebSocket 功能的 HTML 页面。清单 13-12 中显示了一个使用 echo 服务器的示例页面。HTML5 页面包含用于连接和断开服务器的按钮,以及用于键入和发送消息的文本字段和按钮。最初,只有Connect按钮被激活。连接后,Connect按钮被禁用,其他控件被启用。然后你可以输入一些文本并按下Send按钮。然后,数据将被发送到服务器,回显并显示在页面上。为了测试这个页面,首先将它作为test.htm保存在 echo 服务器的public子目录中。服务器运行时,只需导航至http://localhost:8000/test.htm。
清单 13-12 。与清单 13-10 中的服务器协同工作的 HTML 客户端
<!DOCTYPE html>
<html lang="en">
<head>
<title>WebSocket Echo Client</title>
<meta charset="UTF-8" />
<script>
"use strict";
// Initialize everything when the window finishes loading
window.addEventListener("load", function(event) {
var status = document.getElementById("status");
var open = document.getElementById("open");
var close = document.getElementById("close");
var send = document.getElementById("send");
var text = document.getElementById("text");
var message = document.getElementById("message");
var socket;
status.textContent = "Not Connected";
close.disabled = true;
send.disabled = true;
// Create a new connection when the Connect button is clicked
open.addEventListener("click", function(event) {
open.disabled = true;
socket = new WebSocket("ws://localhost:8000");
socket.addEventListener("open", function(event) {
close.disabled = false;
send.disabled = false;
status.textContent = "Connected";
});
// Display messages received from the server
socket.addEventListener("message", function(event) {
message.textContent = "Server Says: " + event.data;
});
// Display any errors that occur
socket.addEventListener("error", function(event) {
message.textContent = "Error: " + event;
});
socket.addEventListener("close", function(event) {
open.disabled = false;
status.textContent = "Not Connected";
});
});
// Close the connection when the Disconnect button is clicked
close.addEventListener("click", function(event) {
close.disabled = true;
send.disabled = true;
message.textContent = "";
socket.close();
});
// Send text to the server when the Send button is clicked
send.addEventListener("click", function(event) {
socket.send(text.value);
text.value = "";
});
});
</script>
</head>
<body>
Status: <span id="status"></span><br />
<input id="open" type="button" value="Connect" />
<input id="close" type="button" value="Disconnect" /><br />
<input id="send" type="button" value="Send" />
<input id="text" /><br />
<span id="message"></span>
</body>
</html>检查 WebSocket 连接
您可能想知道 HTTP 和 WebSockets 如何同时监听同一个端口。原因是初始 WebSocket 连接是通过 HTTP 进行的。图 13-1 展示了从 Chrome 开发者工具的角度来看 WebSocket 连接的样子。图像的顶部显示了来自清单 13-12 的实际测试页面。图的底部显示了 Chrome 开发者工具,并显示了两个记录的网络请求。第一个请求test.htm,只是下载测试页面。标记为localhost的第二个请求在网页上按下Connect按钮时发生。该请求发送 WebSocket 头和一个Upgrade头,这使得将来的通信能够通过 WebSocket 协议进行。通过检查响应状态代码和头,您可以看到连接成功地从 HTTP 切换到 WebSocket 协议。
图 13-1 。使用 Chrome 的开发工具检查 WebSocket 连接
插座。IO
本章前面已经解释了 WebSockets 的众多好处。然而,它们最大的缺点可能是缺乏浏览器支持,尤其是在传统浏览器中。进入Socket.IO,一个自称为“实时应用的跨浏览器 WebSocket”的 JavaScript 库Socket.IO通过提供心跳和超时等附加功能,在 WebSockets 之上增加了另一个抽象层。这些功能通常用于实时应用,可以使用 WebSockets 实现,但不是标准的一部分。
Socket.IO的真正优势在于它能够在完全不支持 WebSockets 的旧浏览器上维护相同的 API。当本地 WebSockets 不可用时,这可以通过依靠旧技术来实现,如 Adobe Flash Sockets、Ajax long polling 和 JSONP polling。通过提供回退机制,Socket.IO可以与 Internet Explorer 5.5 等传统浏览器一起工作。它的灵活性使它成为npm注册表中第五大明星模块,同时被超过 700 个npm模块所依赖。
创建套接字。IO 服务器
Socket.IO和ws一样,很容易和http模块结合。清单 13-13 显示了另一个组合了 HTTP 和 WebSockets(通过Socket.IO)的 echo 服务器。清单 13-13 的第三行导入了Socket.IO模块。Socket.IO listen()方法强制Socket.IO监听 HTTP 服务器server。然后由listen()、io返回的值用于配置应用的 WebSockets 部分。
清单 13-13 。使用http、connect和Socket.IO的 Echo 服务器
var http = require("http");
var connect = require("connect");
var socketio = require("socket.io");
var app = connect();
var server;
var io;
app.use(connect.static("public"));
server = http.createServer(app);
io = socketio.listen(server);
io.on("connection", function(socket) {
socket.on("message", function(data) {
socket.emit("echo", data);
});
});
server.listen(8000);一个connection事件处理程序处理传入的 WebSocket 连接。与ws非常相似,连接处理程序将 WebSocket 作为其唯一的参数。接下来,注意message事件处理程序。当新数据通过 WebSocket 到达时,将调用该处理程序。然而,与标准的 WebSockets 不同,Socket.IO允许任意命名的事件。这意味着我们可以监听foo事件,而不是message事件。不管事件的名称是什么,收到的数据都会传递给事件处理程序。然后,通过发出一个echo事件,数据被回显到客户端。同样,事件名称是任意的。另外,注意数据是使用熟悉的EventEmitter语法的emit()方法发送的。
创建套接字。IO 客户端
Socket.IO还附带了可用于浏览器开发的客户端脚本。清单 13-14 提供了一个示例页面,它可以与清单 13-13 中的服务器对话。将该页面放在 echo 服务器的public子目录中。首先要注意的是文档头中包含的Socket.IO脚本。该脚本由服务器端模块自动处理,不需要添加到public目录中。
清单 13-14 。与清单 13-13 中的服务器协同工作的Socket.IO客户端
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<body>
<script>
var socket = io.connect("http://localhost");
socket.emit("message", "Hello!");
socket.on("echo", function(data) {
document.write(data);
});
</script>
</body>
</html>接下来要检查的是内嵌的<script>标签。这就是Socket.IO应用逻辑。当页面被加载时,使用io.connect()方法来建立到服务器的连接。注意,这个连接是使用 HTTP URL 建立的,而不是使用ws协议。然后使用emit()方法向服务器发送一个message事件。同样,事件名称的选择是任意的,但是客户机和服务器必须在名称上达成一致。由于服务器将发回一个echo事件,我们做的最后一件事是创建一个echo事件处理程序,它将接收到的消息打印到文档中。
插座。IO 和 Express
集成Socket.IO和 Express 非常简单。其实和把Socket.IO和http整合在一起,连接起来没多大区别。清单 13-15 展示了这是如何完成的。唯一的主要区别是,Express 被导入并用于创建app变量和附加中间件,而不是 Connect。仅仅为了举例,一个快速路由也被添加到现有的 echo 服务器中。清单 13-14 中的客户端页面仍然可以在这个例子中使用,无需修改。
清单 13-15 。使用Socket.IO和 Express 构建的 Echo 服务器
var express = require("express");
var http = require("http");
var socketio = require("socket.io");
var app = express();
var server = http.createServer(app);
var io = socketio.listen(server);
app.use(express.static("public"));
app.get("/foo", function(req, res, next) {
res.send(200, {
body: "Hello from foo!"
});
});
io.on("connection", function(socket) {
socket.on("message", function(data) {
socket.emit("echo", data);
});
});
server.listen(8000);摘要
本章讲述了实时网络的概念。这个领域最大的玩家无疑是 WebSockets。WebSockets 通过在客户机和服务器之间提供双向通信而无需发送 HTTP 头,提供了一流的性能。然而,虽然 WebSockets 提供了潜在的巨大性能提升,但它们是相对较新的标准,在传统浏览器中不受支持。因此,本章还介绍了Socket.IO,这是一个跨浏览器的 WebSocket 模块,它通过依靠其他效率较低的数据传输机制来支持旧浏览器。此外,本章还向您展示了如何将Socket.IO与第 11 章和第 12 章中涵盖的其他技术相集成。在下一章中,您将学习如何访问数据库,以及如何将它们与到目前为止您已经学习过的所有 Node 模块集成在一起。
