这一章开始与书中前几章不同。以前,这些章节主要集中在 Node.js 核心及其功能上。这是为了更好地理解 Node.js 中包含的基础架构和平台可用性。然而,Node.js 之所以取得巨大成功,是因为第三方模块的生态系统以及它们提供的可扩展性。本章以及接下来的章节将会让你体验 Node.js 社区,以及它能为你的应用开发提供什么。本章从讨论 WebSockets 开始。
在 WebSockets 出现之前,客户端和服务器之间有许多类似 WebSocket 的通信方式。其中许多都使用了某种形式的从客户端到服务器的轮询,客户端连接到服务器,然后服务器要么直接用一个状态进行响应,要么长时间保持 HTTP 连接打开以等待事件。这会产生许多 HTTP 请求,并且不是客户端和服务器之间的完全双向通信。因此,HTML 5 规范起草了 WebSocket 协议,以允许这种具有持久连接的双向通信。
WebSockets 基于 WebSocket 协议,定义为与远程主机的双向通信,或 TCP 上的双向通信。WebSocket 通信是基于消息的,这使得它比 TCP 流等通信机制更容易处理。乍一看,WebSocket 实现可能看起来像一个 HTTP 实例,但接口的 HTTP 部分只是为了在客户端和服务器之间创建一个握手,并随后将连接升级到 WebSocket 协议。一旦握手成功,客户端和服务器都能够向对方发送消息。WebSocket 消息由帧组成,根据协议,帧是确定发送何种类型消息的信息部分。这些可以是内容的类型(二进制或文本),也可以是用于发出连接应该关闭的信号的控制帧。通过使用安全套接字层 (SSL) 连接的ws:// URI 方案和wss://来访问 WebSocket 端点。
WebSocket 之所以在 Node.js 中蓬勃发展,是因为 Node.js 的事件驱动特性以及手动或通过第三方工具快速高效地创建 web socket 服务器的能力。由于与 Node.js 的这种天然匹配,进入 WebSockets 世界的障碍使得用 Node.js 创建支持 WebSocket 的服务器变得很容易
您在第 4 章中简要地看到了如何创建一个升级的 WebSocket 连接,但是本章将展示如何利用不同的框架和技术来构建一个完整的 WebSocket 应用。您将涉及的一些主题包括:
- 使用第三方模块构建 WebSocket 服务器
- 监听客户端上的事件
- 用 WebSockets 构建 API
- 使用 WebSockets 从服务器传递事件
- 在浏览器中处理这些事件并创建双向通信
- 用 WebSockets 构建多用户应用
如果您不想创建自己的服务器,作为 Node.js 开发人员,您可以使用几种 WebSocket 实现。通过不创建自己的服务器,你会牺牲一些东西而获得另外一些东西。你牺牲了从概念到产品对服务的完全控制,但是如果你正在使用的模块得到很好的支持,你会得到围绕该模块的社区。本章将关注其中的两个模块:WebSocket-Node 和 Socket。IO 。两者都有强大的社区,开发人员可以向其寻求可靠的实现;然而,插座。IO 已经成为许多 WebSocket 开发者的首选。
8-1.用 WebSocket-Node 实现 WebSocket 服务器
问题
您想要开始使用 WebSocket-Node 模块 来创建 WebSocket 服务器。
解决办法
当您第一次转向 WebSocket-Node 来满足您的 WebSocket 需求时,您会发现您有机会利用一个关于如何格式化 WebSocket 的框架,因为它主要是 WebSocket 协议的 JavaScript 实现。
要开始使用这个 Node.js 模块,您首先需要从 npm 注册表安装,'npm install websocket【T4]。'一旦你安装了这个,你就可以像清单 8-1 中所示的那样使用它,你可以看到你扩展了一个 web 服务器来利用升级后的 WebSockets 连接。
清单 8-1 。升级 Web 服务器以使用 WebSockets
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
console.log('connected');
connection.send('yo');
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});它是如何工作的
当您使用 WebSocket-Node 创建 WebSocket 服务器时,您可以用一个简单易用的 API 完成很多事情。首先,您正在创建一个 HTTP 服务器。这是一个要求,因为 HTTP 连接必须升级,以便通过握手过程成功创建 WebSocket 连接。然后您需要在您的解决方案中创建一个 WebSocket 服务器配置对象serverConfig。
这个配置 将用于确定您的服务器将处理的 WebSocket 通信的类型。该配置上可供设置的选项如表 8-1 所示。这些默认值与您在 WebSocket 服务器中设置和使用的选项合并。
表 8-1 。WebSocket 服务器配置选项
| [计]选项 | 描述 |
|---|---|
| 集合碎片 | 这告诉服务器自动组装分段的消息,然后在“消息”事件中发出完整的消息。如果这不是真的,那么帧将在“帧”事件中发出,客户端需要自己将这些帧组装在一起。默认值:真 |
| 。自动接受连接 | 这告诉服务器是否接受任何 WebSocket 连接,而不管客户端指定的路径或协议。在大多数情况下应该避免这种情况,因为您最好检查请求以检查允许的来源和协议。默认值:false |
| 。关闭超时 | 这是发送关闭帧后等待的毫秒数,以查看在关闭套接字之前是否返回了确认。默认值:5000 |
| 。禁用算法 | 这将决定是否使用 Nagle 算法。该算法允许通过在传输前插入一小段延迟来将较小的数据包聚合在一起。默认值:真(无延迟) |
| 。dropConnectionOnKeepaliveTimeout | 这将告知 WebSocket 服务器断开与无法在. keepaliveGracePeriod 内响应 keepalive ping 的客户端的连接。默认值:true |
| 。分割阈值 | 如果传出帧大于这个数字,那么它将被分段。默认值:0x4000 (16KB) |
| 。fragmentOutgoingMessages | 此设置决定是否对超过 fragmentationThreshold 选项的邮件进行分段。默认值:真 |
| 。http server(http 服务器) | 这是您将要升级 WebSocket 协议连接的服务器。此选项是必需的。默认值:空 |
| 保持活力 | 此计时器将在每个指定的. keepaliveInterval 向所有客户端发送 ping 命令。默认值:true |
| 。keepaliveGracePeriod | 这是在发送 keepalive ping 后断开连接前等待的时间,以毫秒为单位。默认值:10000 |
| 。keepaliveInterval | 向连接的客户端发送 keepalive ping 的时间(毫秒)。默认值:20000 |
| 。maxReceivedFrameSize | 此选项用于设置 WebSocket 消息帧的最大帧大小阈值。默认值:0x10000(十六进制)= 64 千字节 |
| 。maxReceivedMessageSize | 这是为了设置邮件的最大大小。这仅适用于以下情况。assembleFragments 设置为 true。默认值:0x100000 (1 MB) |
| 。useNativeKeepalive | 这将告诉服务器使用 TCP keepalive,而不是 WebSocket ping 和 pong 数据包。不同的是,TCP keepalive 略小,减少了带宽。如果设置为 true,那么。keepaliveGracePeriod 和。dropConnectionOnKeepaliveTimeout 被忽略。默认值:false |
一旦使用 HTTP 服务器设置了配置,就可以通过调用new WebSocketServer([config])来实例化一个新的 WebSocket 服务器,这里的【config】表示您可以选择传入配置选项。在您的解决方案中,然后调用新 WebSocket 服务器的.mount()方法,这将合并选项并绑定到 HTTP 服务器的“upgrade”事件。
WebSocket 服务器可用的另一种方法是unmount(),它将取消从 HTTP 服务器升级到 WebSocket 协议的能力,但不会影响任何现有的连接。closeAllConnections() 是另一种方法,即优雅地关闭所有连接;shutdown()关闭所有连接并从服务器卸载。
有几个事件你也可以听。在您的示例中,您使用了“request”、“connect”和“close”事件。
当您没有将配置选项'autoAcceptConnections '设置为真时,将发出'request'事件。这将使您有机会检查传入的 WebSocket 请求,以保证您的目标是连接到所需的源和协议。然后,您可以选择accept()或reject()请求。你可以看到在这个例子中,accept()方法带参数。accept()方法可以接受三个参数:协议、来源和 cookies。该协议将只接受来自同一协议的 WebSocket 连接的数据。origin 允许您将 WebSocket 通信限制到指定的主机。参数中的 cookies 必须是名称/值对伴随请求的数组。
一旦请求被接受,服务器就会发出'connect'事件。然后,该事件将在已处理事件的回调中传递要处理的WebSocketConnection对象。
当与 WebSocket 服务器的连接因任何原因关闭时,会发出'close'事件。它不仅会传递WebSocketConnection对象,还会将关闭原因和描述传递给事件处理程序的回调。
您已经看到了如何使用 WebSocket-Node 创建到 WebSocket 服务器的连接,WebSocket-Node 是 web socket 实现的第三方模块。现在,您将研究两种与 WebSocket 服务器通信的方法,一种是使用 Node.js 客户机,另一种是从 web 应用上的客户机。
注意 WebSockets 并不完全适用于所有的浏览器。直到 Internet Explorer 版本 10,Internet Explorer 才实现该协议。Opera Mini(通过 7.0 版)和 Android 浏览器(通过 4.2 版)不支持该协议。除此之外,其他浏览器的一些旧版本不支持最新的实现。更多信息,请查看http://caniuse.com/#feat=websockets。
8-2.在客户端监听 WebSocket 事件
问题
您希望能够作为客户机与 WebSocket 服务器通信。
解决办法
有几种方法可以连接到 WebSocket 连接,您将在本解决方案中看到其中的两种方法。实现这一点的一种方法是利用第三方框架 WebSocket-Node 来创建一个客户端应用,它将连接到 WebSocket 服务器并在两个端点之间进行通信。这在清单 8-2 中显示,并且不同于更典型的利用网页(你将在第 8-5 节中更详细地介绍)连接到 WebSocket 服务器并继续使用该协议进行通信的方法。
清单 8-2 。使用 WebSocket-Node 创建 WebSocket 客户端
/**
* A WebSocket Client
*/
var WebSocketClient = require('websocket').client;
var client = new WebSocketClient();
client.on('connectFailed', function(error) {
console.log('Connect Error: ' + error.toString());
});
client.on('connect', function(connection) {
console.log('woot: WebSocket client connected');
connection.on('error', function(error) {
console.log(error);
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
connection.on('message', function(message) {
switch (message.type) {
case 'utf8':
console.log('from server: ', message.utf8Data);
break;
default:
console.log(JSON.stringify(message));
break;
}
});
connection.send('heyo');
});
client.connect('ws://localhost:8080/', 'echo-protocol');作为清单 8-2 中 WebSocket-Node 实现的替代方案,您可以创建一个 WebSocket 客户端,它将使用类似于清单 8-3 中所示的 HTML 页面进行连接。
清单 8-3 。WebSocket 客户端 HTML 页面
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
};
};
</script>
</body>
</html>它是如何工作的
首先,您使用 WebSocket-Node 附带的 Node.js 应用中可用的WebSocketClient创建了一个客户机。当您调用new WebSocketClient();时,这个客户端为您创建到 WebSocket 服务器的升级连接。该构造函数将接受一个选项对象并用默认选项扩展该对象,如表 8-2 所示。
表 8-2 。WebSocketClient 选项
| [计]选项 | 描述 |
|---|---|
| 。集合碎片 | 这告诉客户端自动将碎片帧组装成一个完整的消息。默认值:真 |
| 。关闭超时 | 这是等待的时间,以毫秒为单位,直到连接在没有收到响应后关闭。默认值:5000 |
| 。禁用算法 | 这表明是否禁用 Nagle 算法,该算法将在发送消息之前设置一个小的延迟,以减少 HTTP 流量。默认值:真 |
| 。fragmentOutgoingMessages | 这将导致传出的消息大于集合。fragmentation 要分段的阈值。默认值:真 |
| 。分割阈值 | 这是将帧分割成片段的大小限制。默认值:16KB |
| 。webSocketVersion | 这是在此连接中使用的 WebSocket 协议的指定版本。默认值:13 |
| 。maxReceivedFrameSize | 这将设置通过 WebSocket 协议接收的帧的最大大小。默认值:1 MB |
| 。maxReceivedMessageSize | 这是通过协议接收的消息的最大大小。仅当。assembleFragments 选项设置为 true。默认值:8 MB |
| 。选项 | 该对象可以包含用于安全连接的传输层安全性(TLS)信息。 |
一旦创建了 WebSocket 客户端,就可以监听通过连接传输的事件和消息。在你的解决方案中,你监听一个'connect'事件。该事件将在回调中接收连接对象,然后您将使用该对象向服务器发送和接收数据。连接是通过调用。connect()web socket 客户端上的功能。这将接受您希望将端点绑定到的 URL 和协议。
为了向 WebSocket 服务器传输消息,您利用了connection.send()方法。该方法将接受两个参数:第一个是您希望发送的数据,第二个是回调函数(可选)。数据将被处理以检查数据是否是缓冲区。如果数据是缓冲区,它们将通过调用。sendBytes()连接的方法;否则,它将尝试使用连接的。sendUTF()方法如果数据可以用。toString()方法。那个。sendBytes()或。sendUTF()方法是传递回调的地方。您可以在清单 8-4 中看到 send 方法的 WebSocket-Node 实现的内部工作方式。
清单 8-4 。WebSocketClient 发送方法
WebSocketConnection.prototype.send = function(data, cb) {
if (Buffer.isBuffer(data)) {
this.sendBytes(data, cb);
}
else if (typeof(data['toString']) === 'function') {
this.sendUTF(data, cb);
}
else {
throw new Error("Data provided must either be a Node Buffer or implement toString()")
}
};您还可以收听“消息”活动。这个事件是从 WebSocket 服务器发出的,在您的示例中,您检查了收到的消息类型。检查类型允许您适当地处理消息,无论它是 utf8 字符串还是其他格式。使用 WebSocket-Node 附带的WebSocketClient是为 Node.js 应用构建进程间通信的好方法。但是,您可能希望使用 HTML 页面来创建 WebSocket 客户端。
通过利用 web 浏览器中 WebSocket 对象中可用的WebSocketClient或本地 WebSockets,您可以创建一个到 WebSocket 服务器的有用的客户端连接。
8-3.构建 WebSocket API
问题
您希望构建一个利用 WebSockets 的应用,但是您需要创建一个非常适合 WebSocket 范例的 API。
解决办法
用 WebSockets 创建一个 API 似乎与另一个 API 方法不同,比如表述性状态转移(REST) 。这是因为,虽然你可以想象在你的应用中有多条路由,但是使用 WebSockets 你无法访问在 RESTful 设计中指示动作的 HTTP 动词。有几种方法仍然可以构建一个有组织的 API。在清单 8-5 中,你可以看到你构建了一个 WebSocket 服务器,与本章第一节中创建的服务器没有什么不同,它包含了一些额外的处理来自客户端的消息中发送的数据。
清单 8-5 。使用 WebSocket 服务器进行路由处理
/**
* using WebSocket-Node
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
wsserver.on('connect', function(connection) {
connection.send('yo');
});
wsserver.on('request', function(req) {
if (req.requestedProtocols[0] == 'echo-protocol') {
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
var rt = JSON.parse(message.utf8Data);
switch (rt.path) {
case 'route_a':
console.log('something cool on route a');
break;
case 'route_b':
console.log('something cool on route b', rt);
break;
default:
console.log('something awesome always can happen');
break;
}
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
} else {
console.log('protocol not acceptable');
}
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
});一旦您在您的服务器上创建了这个路由处理,您就可以构建一个更符合逻辑的模型,通过 WebSocket 连接从客户端发送消息,如清单 8-6 中的所示。
清单 8-6 。手动路线
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
ws.send(JSON.stringify({ status: 'ok', path: 'route_b', action: 'update'}));
};
};
</script>
</body>
</html>虽然这通常是使用 WebSockets 实现某种路由或 API 设计的成功策略,但是这种对象路由概念也有替代方案。一种替代方法是利用 WebSocket-Node WebSocketRouter对象。该对象允许您在基于 WebSocket 的 Node.js 应用中轻松地为不同的路径或协议指定单独的路由。这种类型的服务器如清单 8-7 所示。
清单 8-7 。一个 WebSocketRouter 服务器
/**
* WebSockets API
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server,
WebSocketRouter = require('websocket').router;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var router = new WebSocketRouter();
router.attachServer(wsserver);
router.mount('*', 'echo-protocol', function(request) {
console.log('mounted to echo protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('routed message');
});
conn.send('hey');
});
router.mount('*', 'update-protocol', function(request) {
console.log('mounted to update protocol');
var conn = request.accept(request.origin);
conn.on('message', function(message) {
console.log('update all the things');
});
});清单 8-8 展示了如何在 HTML 页面中构建一个 HTTP 客户端,它将展示如何从WebSocketRouter服务器指定路由。
清单 8-8 。WebSocketRouter HTTP 客户端
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok', path: 'route_a'}));
};
var wsupdate = new WebSocket('ws://localhost:8080', 'update-protocol');
wsupdate.onopen = function() {
wsupdate.send('update');
};
};
</script>
</body>
</html>它是如何工作的
在研究了 WebSockets 的这两种 API 实现之后,您会立即注意到这些解决方案并没有什么过于复杂的地方。两者的基础都是在给定来自服务器的特定消息的情况下,指定要解决的动作。
在清单 8-4 中,您创建了一个服务器,处理从客户端传递到服务器的 JavaScript 对象符号(JSON) 对象。这要求您设计希望在 API 中提供的路线和动作。如果您愿意,您甚至可以通过相应地提供路线和动作来模仿 REST API。例如,如果您有一个希望通过 API 访问的用户配置文件,您可以构建一组如下所示的对象:
{ route: '/user/profile', action: 'GET' }
{ route: '/user/profile', action: 'POST' }
{ route: '/user/profile', action: 'PUT' }
{ route: '/user/profile', action: 'DELETE'}这将由您的服务器通过解析传入的消息来处理,然后像您在解决方案的switch(rt.path) {...}中所做的那样处理路由。您可以看到,这个构建 WebSocket API 的解决方案非常适合许多需求,尤其是如果您只实现一个协议来处理 API 指令的话。当然,您可以隔离由不同 WebSocket 协议处理的路由。为此,WebSocket-Node 中有一个特性使得用一个WebSocketServer实例访问不同的协议变得更加容易。
在您的解决方案中,您构建了一个路由,它可以处理从客户端提供给它的任何路径(' * '),但是可以通过单独处理不同的协议来处理不同的路由。这意味着,如果您的应用中有一个逻辑分离,比如一个用于用户更新的 API 和一个用于产品更新的 API,您可以用一个单独的协议轻松地将它们分开。您只需在客户机上创建一个新的 WebSocket,它指向您的服务器并为每一项传递特定的协议。
var users = new WebSocket('ws://my.wsserver.co', 'users_protocol');
var products = new WebSocket('ws://my.wsserver.co', 'product_protocol');从这里开始,您不再关心数据中路由的所有细节,尽管您仍然需要知道您希望通过 WebSocket 连接驱动的动作和特定事件,但是您知道如果您正在访问特定的 WebSocket 协议,您将被隔离到应用逻辑集。事实上,正如您在示例中看到的,整个路由在服务器上是隔离的。显然,分离对象类型是一种方法,但是您可以想象分离每种类型的更新/获取消息的可能性也是可能的。对于大多数情况来说,这可能太细了,但是在一个聊天室的例子中,您可能有一个'sendmessage_protocol'和一个'getmessage_protocol',并且完全独立地处理 get 和 send 操作。
在 Node.js 应用中,围绕 WebSocket 连接构建 API 的方式基本上是无限的,这允许您自由地创建自己认为合适的应用。
到目前为止,本章的大部分内容都基于 WebSocket-Node 模块及其实现。从这里开始,您将研究 Socket。IO,这是另一个非常流行的框架,用于构建基于 WebSocket 的 Node.js 应用。
8-4.使用插座。WebSocket 通信的 IO
问题
您希望通过利用套接字来构建基于 WebSocket 的 Node.js 应用。IO 模块。
解决办法
插座。IO 是一个完全可操作且非常受欢迎的框架,它将 WebSockets 与 Socket 的 Node.js.Implementations 结合使用。IO 可以采取多种形式,但最流行的是以类似于 Node.js 事件模型的方式在客户机和服务器之间传递消息。首先要安装 Socket。使用“$ npm install socket.io”命令通过 npm 进行 IO。来构建套接字。IO 服务器,您可以遵循清单 8-9 中所示的示例,该示例展示了在您实现 Node.js 套接字时可以使用的各种方法。IO 服务器。
清单 8-9 。实现套接字。IO 服务器
/**
* Socket.io Server
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-4-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-4-1.html');
}
res.writeHead(200);
res.end(data);
});
}
// General
io.sockets.on('connection', function (socket) {
socket.broadcast.emit('big_news'); // Emits to all others except this socket.
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
//namespaced
var users = io.of('/users').on('connection', function(socket) {
socket.emit('user message', {
that: 'only',
'/users': 'will get'
});
users.emit('users message', {
all: 'in',
'/users': 'will get'
});
});插座的原因之一。IO 变得如此流行是因为它有一个嵌入的客户端模块,您可以在绑定到您的服务器的 HTML 页面中使用它。这允许毫不费力地连接到使用 Socket.IO 创建的 WebSocket 服务器。实现此连接需要添加一个 JavaScript 文件引用,然后使用套接字绑定到 WebSocket 服务器。IO 特定绑定,与 web 标准中的new WebSocket()实例化相反。
清单 8-10 。一个插座。IO 客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
socket.on('big_news', function(data) {
console.log('holy cow!');
});
var users = io.connect('http://localhost/users');
users.on('connect', function() {
users.emit('users yo');
});
</script>
</head>
<body>
</body>
</html>它是如何工作的
插座。在构建 WebSocket 服务器时,IO 可以为您做很多事情,但是它也提供了构建 Node.js 应用时所需的灵活性。就像 WebSocket-Node 一样,它抽象出握手来创建升级的 WebSocket 连接,不仅在服务器上,而且在包含套接字的客户端上。HTML 中的 IO JavaScript 文件。Socket.IO 也是独一无二的,因为它不仅利用了 WebSocket 协议的强大功能,而且还依赖于其他双向通信方法,比如 Flash sockets、long-polling 和 iframes。它将这样做,以便您可以构建您的应用,创建 WebSocket 通信结构 ,并且仍然能够依赖套接字。IO 通信,即使在旧的浏览器或不支持 WebSocket 协议的浏览器上。
在清单 8-8 中,你使用 Socket.IO 创建了一个 WebSocket 服务器。IO 包。然后,您使用本机 Node.js http 模块创建了一个简单的 HTTP 服务器;这是为了提供来自清单 8-9 的 HTML 页面,您计划用它来连接 WebSocket 服务器。
实例化套接字时。在您的代码中,您可以通过告诉新对象在 HTTP 服务器上进行监听来做到这一点,但是您也可以只传入一个监听端口。现在您可以访问插座了。IO API。在服务器上,有几个您用来与客户端通信的事件。
首先,您监听了“连接”事件 ,该事件在服务器接收到来自客户端的连接时发出。从该事件的回调中,您可以访问绑定到该特定连接的单个套接字。这个套接字是您的 WebSocket 通信可以发生的地方。
您执行的第一个通信是广播消息 。该消息通过调用socket.broadcast.emit('big_news');,来触发,调用socket.broadcast.emit('big_news');,会将消息'big_news''发送到连接到套接字的所有套接字。IO 服务器,发送广播的连接除外。接下来你通过使用socket.emit('news', { hello: 'world' });方法发出一个事件‘新闻’。可以在客户端监听该事件,然后可以在客户端处理与消息一起传输的数据。这类似于WebSocket.send()方法,您将在下一节中看到更详细的内容。您在“连接”事件回调中使用的最后一个方法是绑定到从客户端发出的任意事件消息。这与绑定到任何事件的方式相同。
然后,创建了一个绑定到名称空间的 WebSocket 连接。这将有助于创建类似于上一节中概述的示例的 API。您可以通过调用io.of('/path')绑定到名称空间。这将把该路径上的所有连接路由到指定的处理程序。您可以像在解决方案中一样命名这些名称空间var users = io.on('/users');.这很有用,因为您可以只在用户的名称空间上调用事件,比如当您通过调用users.emit('users message'...).向所有用户发出消息时
要在客户机上接收和传输消息,只需向 socket.io.js 文件添加一个 JavaScript 引用。这将为您提供对 I/O 对象的访问,然后您可以使用该对象连接到您的 Socket.IOserver。同样,就像服务器名称空间一样,您可以通过使用路径:var users = io.connect(' http://localhost/users ');连接到特定的路由。
通过这个实现,您可以看到如何利用套接字。IO 构建 Node.js WebSocket 服务器和客户端。IO 利用自己的 API 来发送和接收消息。但是,如果你选择使用 WebSocket 标准.send()而不是.emit()方法,Socket。木卫一将支持这一点。在接下来的几节中,您将进一步了解如何利用 WebSocket 对象跨连接发送和处理消息。
8-5.在浏览器中处理 WebSocket 事件
问题
您希望在浏览器中利用 WebSocket 事件。
解决办法
在本章的前面,您看到了如何使用 WebSocket-Node 模块构建 WebSocket 客户端。您还看到了可以在 web 浏览器中或通过使用 Socket 建立这些 WebSocket 连接的情况。浏览器中的 IO。清单 8-11 展示了如何在网络浏览器中直接使用 WebSocket API。在这种情况下,你应该运行一个类似于清单 8-1 所示的 WebSocket 服务器。
清单 8-11 。浏览器中的 WebSocket API
<!doctype html>
<html>
<head>
</head>
<body>
<h3>WebSockets!</h3>
<script>
window.onload = function() {
var ws = new WebSocket('ws://localhost:8080', 'echo-protocol');
ws.onopen = function() {
console.log('opened');
};
ws.onmessage = function(event) {
console.log(event);
ws.send(JSON.stringify({ status: 'ok'}));
console.log(ws.binaryType);
console.log(ws.bufferedAmount);
console.log(ws.protocol);
console.log(ws.url);
console.log(ws.readyState);
};
ws.onerror = function() {
console.log('oh no! an error has occured');
}
ws.onclose = function() {
console.log('connection closed');
}
};
</script>
</body>
</html>它是如何工作的
您可以通过简单地绑定到 WebSocket 服务器的端点并请求正确的协议来创建一个 HTML 格式的 WebSocket 客户端,在您的例子中,该协议被称为‘echo-protocol’。这是通过在网页的 JavaScript 中创建一个new WebSocket(<url>, <protocol>);对象来实现的。这个新的 WebSocket 对象可以访问几个事件和属性。可用的 WebSocket 方法有。close()和.send(),分别关闭连接或发送消息。您在解决方案中绑定到的事件是.onmessage和.onopen。那个。onopen一旦连接打开,就发出事件,这意味着连接准备好发送和接收数据。那个。onmessage事件是接收到来自 WebSocket 服务器的消息。其他可用的事件侦听器有。onerror,它将接收发生的任何错误。onclose事件,当状态变为关闭时发出。
浏览器中的 WebSocket 对象也可以访问几个属性。这些包括用于传输信息的 URL 和协议,以及服务器提供的状态和任何扩展。WebSocket 连接还可以查看通过该连接传输的数据类型。这是通过.binaryType属性访问的,它可以根据传输的数据报告“blob”或“arraybuffer”。浏览器上 WebSocket 的最后一个属性是。bufferedAmount房产。这告诉您通过使用。send()法。
8-6.通过 WebSockets 通信服务器事件
问题
您已经看到了如何实现 WebSocket 框架和模块。现在,您想使用 WebSockets 发送服务器信息。
解决办法
当您构建 WebSocket 服务器时,这种双向信息高速公路的一个非常吸引人的用例是能够以低延迟的方式发送服务器状态的更新或来自服务器的事件。这与标准 web 服务器相反,在标准 web 服务器中,您需要轮询信息;相反,您可以简单地按需发送信息,并在消息到达时绑定到消息。
您可以想象一个类似于您在前面章节中看到的情况,但是您已经创建了一个 WebSocket 服务器,它正在向客户端传输数据,包括连接和客户端驱动的消息传递。在这里,您将处理 WebSocket 连接,并定期向所有连接的客户端发送更新。
清单 8-12 。发送服务器事件
/**
* server events
*/
var http = require('http'),
fs = require('fs'),
url = require('url'),
WebSocketServer = require('websocket').server;
var server = http.createServer(function(req, res) {
var urlParsed = url.parse(req.url,true, true);
fs.readFile(urlParsed.path.split('/')[1], function(err, data) {
if (err) {
res.statusCode = 404;
res.end(http.STATUS_CODES[404]);
}
res.statusCode = 200;
res.end(data);
});
}).listen(8080);
var serverConfig = {
httpServer: server,
autoAcceptConnections: false
};
var wsserver = new WebSocketServer();
wsserver.mount(serverConfig);
var conns = [];
wsserver.on('connect', function(connection) {
console.log('connected');
conns.push(connection);
setInterval(pingClients, 5e3);
});
wsserver.on('request', function(req) {
console.log('request');
var connection = req.accept('echo-protocol', req.origin);
connection.on('message', function(message) {
if (message.type === 'utf8') {
console.log(message.utf8Data);
}
else if (message.type === 'binary') {
console.log(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
console.log('connection closed', reasonCode, description);
});
});
wsserver.on('close', function(conn, reason, description) {
console.log('closing', reason, description);
for (var i = 0; i < conns.length; i++) {
if (conns[i] === conn) {
conns.splice(i, 1);
}
}
});
function pingClients() {
for (var i =0; i < conns.length; i++) {
conns[i].send('ping');
}
}它是如何工作的
在这个解决方案中,服务器本身的创建方式与您在 WebSocket-Node 中看到的许多其他方式相同。清单 8-11 突出显示了这种差异,显示了当建立连接时,它们被添加到一个到服务器的连接数组中。这样,您就不必为了向连接发送消息而留在连接内部或请求回调。在这个解决方案中,您创建了一个pingClients()方法,它将遍历数组中的所有连接,并每隔一段时间向它们发送一条消息;您可以想象这样一种情况,您有一个关键的服务器事件要传递,您能够以类似的方式将其分发到连接的套接字。
conns 数组包含对整个WebSocketConnection对象的引用。这意味着您能够从数组中挑选出一个连接并发送消息。您可以在pingClients函数中看到这一点,在这里您迭代数组并调用。send()法在每个单独的插座上。要在连接关闭后进行清理,只需在数组中找到关闭的连接,然后用splice()方法删除它。
8-7.与 WebSockets 的双向通信
问题
您需要能够利用 WebSockets 进行双向通信。
解决办法
这个解决方案将允许你用 WebSockets 创建一个简单的聊天室。您将使用套接字创建服务器。IO 并利用它在客户端与服务器和客户端之间传输数据。服务器如清单 8-13 所示,客户端网页如清单 8-14 所示。
清单 8-13 。插座。IO 聊天服务器
/**
* two-way communications
*/
var app = require('http').createServer(connectHandler),
io = require('socket.io').listen(app),
fs = require('fs');
app.listen(8080);
function connectHandler (req, res) {
fs.readFile(__dirname + '/8-7-1.html',
function (err, data) {
if (err) {
res.writeHead(500);
return res.end('Error loading 8-7-1.html');
}
res.writeHead(200);
res.end(data);
});
}
var members = [];
io.sockets.on('connection', function (socket) {
socket.on('joined', function(data) {
var mbr = data;
mbr.id = socket.id;
members.push(mbr);
socket.broadcast.emit('joined', data);
console.log(data.name, 'joined the room');
});
socket.on('message', function(data) {
// store chat now
socket.broadcast.emit('message', data);
});
socket.on('disconnect', function() {
for (var i = 0; i < members.length; i++) {
if (members[i].id === socket.id) {
socket.broadcast.emit('disconnected', { name: members[i].name });
}
}
});
});清单 8-14 。聊天客户端
<!doctype html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="messages">
</div>
<form id="newChat">
<textarea id="text"></textarea>
<input type="submit" id="sendMessage" value="Send" />
</form>
<script>
var socket = io.connect('http://localhost');
var who;
socket.on('connect', function() {
var chatter = prompt('Please enter your name');
chatter = (chatter === "" || chatter === null) ? "anon" : chatter;
addChatter("you", "Joined");
who = chatter;
socket.emit('joined', { name: chatter});
});
function addChatter(name, message) {
var chat = document.getElementById("messages");
chat.innerHTML += "<div>" + name + " - " + message + "</div>";
}
socket.on('joined', function(data) {
console.log(data);
addChatter(data.name, ' joined');
});
socket.on('disconnected', function(data) {
addChatter(data.name, 'disconnected');
});
socket.on('message', function(data) {
addChatter(data.name, data.message);
});
var chat = document.getElementById("newChat");
chat.onsubmit = function() {
var msg = document.getElementById("text").value;
socket.emit("message", { name: who, message: msg });
document.getElementById("text").value = "";
addChatter(who, msg);
return false;
}
</script>
</body>
</html>它是如何工作的
这个解决方案首先创建一个套接字。IO 服务器。该服务器将充当您连接的聊天客户端之间的中继,如果您要在生产环境中使用它,您可能希望添加一些持久层来将聊天存储在数据库中。
你的插座。IO server 为三个事件执行中继:加入聊天室、发送消息和断开套接字。
当你在客户端输入你的名字时,加入一个聊天室是被控制的。然后,客户端将通过socket.emit('joined', { name: <username> });发送一条消息,告诉服务器有一个加入事件,以及用户的名字。然后在服务器上接收,并立即向其他客户端发送广播事件。然后,这些客户端绑定到来自服务器的“joined”消息,该消息包含它们需要的数据,以便知道谁加入了房间。然后将其添加到网页的 HTML 中。
加入房间后,您可以向房间中的其他用户发送消息。这从客户端开始,您可以在文本区输入聊天消息,然后发送消息。这发生在socket.emit('message', {name: <user>, message: <text>});中,并且这再次被立即广播到文本所在的其他连接,并且用户被添加到 HTML 中。
最后,你想知道和你聊天的人是否已经离开了房间。为此,您绑定到套接字上的'disconnect'事件,并找到正在断开的套接字的用户名;这是通过将用户数据存储在服务器上的一个members[]数组中来实现的。然后,您向连接到服务器的其余客户端广播这一离开。
这是一个基本的聊天服务器,但它非常清楚地说明了如何使用 WebSockets 在客户端和服务器以及客户端之间进行低延迟的双向通信。在下一节中,您将看到如何使用类似的方法构建一个多用户白板,允许许多用户通过使用 WebSockets 以协作的方式共享绘制的坐标。
8-8.使用 WebSockets 构建多用户白板
问题
现在您已经理解了 WebSockets 的双向通信,您想要构建一个多用户白板应用,以便实时共享绘图。
解决办法
清单 8-15 展示了如何构建一个 WebSocket 服务器,作为 HTML 画布绘制客户端之间的媒介。这些客户端(HTML 如清单 8-16 所示)和(JavaScript 如清单 8-17 所示)将发送和接受 WebSocket 消息,该消息将提供跨客户端实例共享协作绘图程序的能力。
清单 8-15 。带有 WebSocket-Node 的绘图 WebSocket 服务器
var WebSocketServer = require('websocket').server,
http = require('http'),
sox = {},
idx = 0;
var server = http.createServer(function(request, response) {
response.writeHead(404);
response.end();
});
server.listen(8080, function() {
});
ws = new WebSocketServer({
httpServer: server,
autoAcceptConnections: false
});
function originIsAllowed(origin) {
//Check here to make sure we're on the right origin
return true;
}
var getNextId = (function() {
var idx = 0;
return function() { return ++idx; };
})();
ws.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
request.reject();
console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
return;
}
var connection = request.accept('draw-protocol', request.origin);
connection.socketid = getNextId();
connection.sendUTF("socketid_" + connection.socketid);
console.log(connection.socketid);
sox[connection.socketid] = connection;
connection.on('message', function(message) {
if (message.type === 'utf8') {
sendToAll(JSON.parse(message.utf8Data), 'utf8');
}
else if (message.type === 'binary') {
connection.sendBytes(message.binaryData);
}
});
connection.on('close', function(reasonCode, description) {
delete sox[connection.socketid];
});
});
function sendToAll(drawEvt, type) {
for (var socket in sox) {
if (type === 'utf8' &&drawEvt.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(drawEvt));
}
}
}清单 8-16 。绘图画布和 HTML 标记
<!doctype html>
<html>
<head>
<title>whiteboard</title>
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="jquery_1.10.2.js" type="text/javascript"></script>
<script src="drawings.js" type="text/javascript"></script>
</head>
<body>
<div id="wrapper">
<div class="menu">
<ul>
<li>
<a id="clear">Clear</a>
</li>
<li>
<li>
<a id="draw">Draw</a>
<ul id="colors">
<li style="background-color:white;">
<a>White</a>
</li>
<li style="background-color:red;">
<a>Red</a>
</li>
<li style="background-color:orange;">
<a>Orange</a>
</li>
<li style="background-color:yellow;">
<a>Yellow</a>
</li>
<li style="background-color:green;">
<a>Green</a>
</li>
<li style="background-color:blue;">
<a>Blue</a>
</li>
<li style="background-color:indigo;">
<a>Indigo</a>
</li>
<li style="background-color:violet;">
<a>Violet</a>
</li>
<li style="background-color:black;">
<a>Black</a>
</li>
</ul>
</li>
<label for="sizer">Line Size:</label>
<input name="sizer" id="sizer" type="number" min="5" max="100" step="5" />
</ul>
</div>
<canvas id="canvas" ></canvas>
<canvas id="remotecanvas"></canvas>
</div>
</body>
</html>清单 8-17 。绘图应用:WebSockets 和 Canvas
$(document).ready(function() {
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
remotecanvas = document.getElementById("remotecanvas"),
remotectx = remotecanvas.getContext("2d"),
$cvs = $("#canvas"),
top = $cvs.offset().top,
left = $cvs.offset().left,
wsc = new WebSocket("ws://localhost:8080", "draw-protocol"),
mySocketId = -1;
var resizeCvs = function() {
ctx.canvas.width = remotectx.canvas.width = $(window).width();
ctx.canvas.height = remotectx.canvas.height = $(window).height();
};
var initializeCvs = function () {
ctx.lineCap = remotectx.lineCap = "round";
resizeCvs();
ctx.save();
remotectx.save();
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
remotectx.clearRect(0,0, remotectx.canvas.width, remotectx.canvas.height);
ctx.restore();
remotectx.restore();
};
var draw = {
isDrawing: false,
mousedown: function(ctx, coordinates) {
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
mousemove: function(ctx, coordinates) {
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
mouseup: function(ctx, coordinates) {
this.isDrawing = false;
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
ctx.closePath();
},
touchstart: function(ctx, coordinates){
ctx.beginPath();
ctx.moveTo(coordinates.x, coordinates.y);
this.isDrawing = true;
},
touchmove: function(ctx, coordinates){
if (this.isDrawing) {
ctx.lineTo(coordinates.x, coordinates.y);
ctx.stroke();
}
},
touchend: function(ctx, coordinates){
if (this.isDrawing) {
this.touchmove(coordinates);
this.isDrawing = false;
}
}
};
// create a function to pass touch events and coordinates to drawer
function setupDraw(event, isRemote){
var coordinates = {};
var evt = {};
evt.type = event.type;
evt.socketid = mySocketId;
evt.lineWidth = ctx.lineWidth;
evt.strokeStyle = ctx.strokeStyle;
if (event.type.indexOf("touch") != -1 ){
evt.targetTouches = [{ pageX: 0, pageY: 0 }];
evt.targetTouches[0].pageX = event.targetTouches[0].pageX || 0;
evt.targetTouches[0].pageY = event.targetTouches[0].pageY || 0;
coordinates.x = event.targetTouches[0].pageX - left;
coordinates.y = event.targetTouches[0].pageY - top;
} else {
evt.pageX = event.pageX;
evt.pageY = event.pageY;
coordinates.x = event.pageX - left;
coordinates.y = event.pageY - top;
}
if (event.strokeStyle) {
remotectx.strokeStyle = event.strokeStyle;
remotectx.lineWidth = event.lineWidth;
}
if (!isRemote) {
wsc.send(JSON.stringify(evt));
draw[event.type](ctx, coordinates);
} else {
draw[event.type](remotectx, coordinates);
}
}
window.addEventListener("mousedown", setupDraw, false);
window.addEventListener("mousemove", setupDraw, false);
window.addEventListener("mouseup", setupDraw, false);
canvas.addEventListener('touchstart',setupDraw, false);
canvas.addEventListener('touchmove',setupDraw, false);
canvas.addEventListener('touchend',setupDraw, false);
document.body.addEventListener('touchmove',function(event){
event.preventDefault();
},false);
$('#clear').click(function (e) {
initializeCvs(true);
$("#sizer").val("");
});
$("#draw").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
});
$("#colors li").click(function (e) {
e.preventDefault();
$("label[for='sizer']").text("Line Size:");
ctx.strokeStyle = $(this).css("background-color");
});
$("#sizer").change(function (e) {
ctx.lineWidth = parseInt($(this).val(), 10);
});
initializeCvs();
window.onresize = function() {
resizeCvs();
};
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};
});它是如何工作的
这个解决方案再次从一个 WebSocket Node WebSocket 服务器的简单实现开始。该服务器将只接受“draw-protocol ”的连接。一旦建立了这些连接,就必须创建一个新的套接字标识符,以便稍后将消息传递给套接字。然后,您绑定到将从连接到达的消息事件。从这里开始,假设您将收到包含坐标的消息,这些坐标将从一个客户端向另一个客户端复制绘图。然后,通过遍历包含所有已连接套接字的对象,将这些消息发送给所有已连接的客户端。
function sendToAll(text, type) {
for (var socket in sox) {
if (type === 'utf8' && text.socketid !== socket) {
sox[socket].sendUTF(JSON.stringify(text));
}
}
}在客户端,您创建了一个具有一些功能的画布绘制应用,但是您可以通过某种方式对其进行扩展,以便能够模拟从一个客户端到另一个客户端的整套鼠标或触摸运动。当然,您首先要绑定到 WebSocket 服务器的 URL,并利用该服务器所需的“draw-protocol”。然后在 JavaScript 中构建一个setupDraw函数。这将解析发生在画布上的鼠标或触摸事件,并将它们发送到画布上进行实际绘制。如果实例化绘图的事件在客户端开始,那么您将把坐标、样式和事件发送到 WebSocket 服务器进行调度。
if (!isRemote) {
wsc.send(JSON.stringify(evt));
draw[event.type](ctx, coordinates);
} else {
draw[event.type](remotectx, coordinates);
}然后在客户端上接收发送的绘画事件。这将再次调用setupDraw函数;只是这次您告诉绘图工具您的数据来自远程,这意味着您不需要将 stringified 事件发送回 WebSocket 服务器。
wsc.onmessage = function(event) {
if (event.data.indexOf("socketid_") !== -1) {
mySocketId = event.data.split("_")[1];
} else {
var dt = JSON.parse(event.data);
setupDraw(dt, true);
}
};