WebSocket 与 Socket.IO
Source: Dev.to
此帖子包含一个闪烁的 GIF。
HTTP 请求已经帮了我很多,但我开始遇到它们的局限性。如何告诉客户端服务器在午夜更新了,并且它需要获取最新的数据?当另一个用户发布内容时,如何通知某个用户?简而言之,如何在客户端不发起请求的情况下向其推送信息?
Websocket
一种可能的解决方案是使用 WebSockets,它在客户端和服务器之间建立持久连接。这使我们能够在需要时向客户端发送数据,而无需等待客户端的下一次请求。WebSocket 有自己的协议(虽然连接是通过 HTTP 请求发起的),并且与语言无关。
如果我们愿意,可以从头实现一个 WebSocket 客户端及其对应的 服务器,或者使用 Deno…;也可以使用已经为我们完成了繁重工作的库。我在之前的项目中使用过 Socket.IO,所以我们就选它吧。我之前使用它时感觉很好,而且它还有在 WebSocket 失败时的回退机制。
Colorsocket
为了实现即时的视觉反馈,我们将制作一个小演示,任意一个客户端都可以影响所有客户端显示的颜色。每个位于 /color 端点的客户端都有一个滑块来控制一种主色,以及一个按钮可以反转所有其他 /color 客户端的颜色。
- 当客户端连接时,服务器会按顺序为其分配一种颜色,所以你只需刷新几次即可获得全部三种颜色。
- 重复的颜色会同步工作。
/admin用户可以打开或关闭主颜色。
下面是该应用的实际运行效果:
客户端并不是一直在向服务器发送请求。它们是如何知道需要更新的?
建立连接
当每个客户端执行其 io() 调用时,会创建一个新 socket,从而打开到服务器的连接。
// color.html
const socket = io('/color'); // 我们稍后会回到这个参数
脚本随后在该新 socket 上为我们期望从服务器接收的各种事件分配处理函数:
// color.html
socket.on('assign-color', (color, colorSettings, activeSettings) => {
document.getElementById('color-name').innerText = color;
controllingColor = color;
currentBackground = colorSettings;
active = activeSettings;
colorSlider.disabled = !active[controllingColor];
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
colorSlider.value = colorSettings[controllingColor];
updateBackground();
});
socket.on('set-color', (color, value) => {
currentBackground[color] = value;
if (controllingColor === color) {
colorSlider.value = value;
}
updateBackground();
});
socket.on('invert', () => {
inverted = !inverted;
document.getElementById('inverted').innerText = inverted ? '' : 'not ';
updateBackground();
});
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
与此同时,服务器检测到新的连接,为客户端分配颜色,将该颜色以及当前的应用状态发送给客户端,并为通过 socket 接收到的事件设置自己的处理函数:
// index.js
colorNamespace.on('connection', (socket) => {
const color = colors[colorCount % 3]; // 从列表中挑选下一个颜色,然后循环
colorCount++;
socket.emit('assign-color', color, colorSettings, activeSettings); // 将客户端与应用状态同步
socket.data.color = color; // 你可以将信息保存到 socket 的 data 键中
socket.on('set-color', (color, value) => {
colorSettings[color] = value;
colorNamespace.emit('set-color', color, value);
});
socket.on('invert', () => {
socket.broadcast.emit('invert');
});
});
/admin 页面遵循类似的设置。
向客户端发送信息
让我们来看看在一个页面上的用户交互是如何影响所有其他页面的。
当 蓝色 页面上的用户移动滑块时,滑块会触发一个 change 事件,该事件被滑块的事件监听器捕获:
// color.html
colorSlider.addEventListener('change', (event) => {
socket.emit('set-color', controllingColor, event.target.value);
});
该监听器会发送一个带有颜色和新数值的 set-color 事件。服务器接收到客户端的 set-color 后,再次发送该事件,将数据传递给 所有 客户端。每个客户端收到消息后相应地更新其蓝色值。
向其他套接字广播
点击 “Invert others” 按钮会影响其他 /color 用户,但不会影响实际点击按钮的用户。关键在于 broadcast 标志:
socket.on('invert', () => {
socket.broadcast.emit('invert'); // sends to everyone except the sender
});
使用 socket.broadcast.emit 可以确保发起请求的客户端不会收到自己的广播,而所有其他已连接的套接字都会收到。这一模式可用于任何需要更新其他客户端而不把更改回传给发起者的事件。
广播事件 – “向除发送者之外的所有已连接客户端”
Socket.IO documentation – Broadcasting events
当服务器接收到并重新发送 invert 消息时:
// server.js
socket.on('invert', () => {
socket.broadcast.emit('invert'); // broadcast
});
该标志表示服务器会向除调用它的那个套接字之外的每个套接字发送事件。
在这个简单示例中它只是一个巧妙的小技巧,但在实际使用中,它可以避免向最初创建该事件的用户再次发送信息,因为他们的客户端已经拥有该信息。
Source:
命名空间
你可能已经注意到 admin 选项卡的颜色没有随另外三个页面一起变化。为简化起见,我没有为 admin 页面设置任何处理程序。即使我设置了,它们也不会起作用,因为 admin 套接字根本没有接收到这些事件。这是因为 admin 选项卡位于不同的 命名空间 中。
// color.html
const socket = io('/color');
// admin.html
const socket = io('/admin');
// index.js (server)
const colorNamespace = io.of('/color');
const adminNamespace = io.of('/admin');
…
只有位于 /color 命名空间的套接字会收到以下事件:
colorNamespace.emit('set-color', color, value);
(为清晰起见,我把两个命名空间的名称设为页面所在的两个端点的同名,但这并非必须。只要客户端与服务器匹配,命名空间可以使用任意名称,功能不受影响。)
命名空间提供了一种方便的方式来定位一部分套接字,但它们 可以 相互通信:
// admin.html
const toggleFunction = (color) => {
socket.emit('toggle-active', color);
};
// index.js (server)
// 在 admin 页面点击按钮会触发 color 页面上的变化
adminNamespace.on('connection', (socket) => {
socket.on('toggle-active', (color) => {
activeSettings[color] = !activeSettings[color];
colorNamespace.emit('toggle-active', color);
});
});
// color.html (client)
socket.on('toggle-active', (color) => {
active[color] = !active[color];
if (controllingColor === color) {
colorSlider.disabled = !active[color];
}
document.getElementById('active').innerText = active[controllingColor] ? 'active' : 'inactive';
updateBackground();
});
在所有示例中,事件都是由某个客户端的交互触发的。事件先发送到服务器,随后服务器再向相应的客户端发送第二条消息。这只是可能性的一小部分。例如,服务器可以使用 WebSocket 定期更新所有客户端,或从 API 获取信息并推送给客户端。这个演示仅展示了我所学到的内容,并希望在今后的项目中继续应用。
参考文献与进一步阅读
- Socket.IO – 尤其是该tutorial,让我能够快速上手。
- WebSockets on MDN – API reference 和 glossary。
- 有关编写自己的client和server实现的文章。
- Deno version of a WebSocket server。
