本教程演示如何在Node.js中使用WebSocket实现浏览器与服务器之间的双向、交互式通信。这一技术对于快速、实时的应用程序至关重要,如仪表板、聊天应用和多人游戏。
网络基于请求-响应的HTTP消息。浏览器发起URL请求,服务器响应数据。这可能导致浏览器进一步请求图片、CSS、JavaScript等,以及服务器的相应响应,但服务器不能随意向浏览器发送数据。
长轮询Ajax技术可以使Web应用看似实时更新,但对于真正的实时应用来说,这一过程过于局限。每秒轮询在某些时候效率低下,在其他时候又太慢。
在浏览器发起初始连接后,服务器发送事件是一种标准的(流式)HTTP响应,可以随时向服务器发送消息。然而,通道是单向的,浏览器无法发送消息回服务器。对于真正的快速双向通信,你需要WebSocket。
WebSocket概述
术语WebSocket指的是通过ws://
或安全加密的wss://
运行的TCP通信协议。它不同于HTTP,尽管它可以运行在80或443端口上,以确保在阻止非Web流量的地方也能工作。自2012年以来发布的大多数浏览器支持WebSocket协议。
在典型的实时Web应用中,你至少需要一个Web服务器来提供Web内容(HTML、CSS、JavaScript、图片等)和一个WebSocket服务器来处理双向通信。
浏览器仍需向服务器发起初始的WebSocket请求,以开启通信通道。此后,无论是浏览器还是服务器,均可通过该通道发送消息,从而在对方设备上触发相应事件。
与其他连接的浏览器通信
初始请求完成后,浏览器即可与WebSocket服务器进行消息的收发。WebSocket服务器同样能与任意已连接的客户端浏览器进行消息交换。
点对点通信并不可行。即便BrowserA与BrowserB运行于同一设备且连接至同一WebSocket服务器,BrowserA也无法直接向BrowserB发送消息。它只能将消息发给服务器,期待服务器按需转发给其他浏览器。
WebSocket服务器支持
尽管有传闻称即将到来,但Node.js目前尚未原生支持WebSocket。本文使用第三方ws模块,市面上还存在众多其他选择。
内置WebSocket支持已在Deno与Bun这两个JavaScript运行时中实现。
WebSocket库适用于多种运行时环境,包括PHP、Python和Ruby。此外,第三方SaaS解决方案如Pusher和PubNub也提供托管的WebSocket服务。
WebSocket演示快速入门
聊天应用是WebSocket演示的Hello, World!
,因此我为以下几点道歉:
-
缺乏原创性。尽管如此,聊天应用是解释这些概念的绝佳方式。
-
无法提供完全托管的在线解决方案。我宁愿不监控和审核匿名消息流!
克隆或下载GitHub上的node-wschat仓库:
git clone https://github.com/craigbuckler/node-wschat
安装Node.js依赖:
cd node-wschat
npm install
启动聊天应用:
npm start
在多个浏览器或标签页中打开http://localhost:3000/(你也可以在查询字符串中定义你的聊天名称,例如http://localhost:3000/?Craig)。在其中任意一个窗口输入内容并点击发送或按下回车键,你将看到该内容在所有已连接的浏览器中出现。
Node.js代码概览
该Node.js应用程序的入口文件index.js
启动了两个服务器:
-
一个运行在http://localhost:3000/的Express应用,使用EJS模板提供包含客户端HTML、CSS和JavaScript的单页服务。浏览器JavaScript利用WebSocket API建立初始连接并发送及接收消息。
-
A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:
// WebSocket 服务器 import WebSocket, { WebSocketServer } from 'ws'; const ws = new WebSocketServer({ port: cfg.wsPort }); // 客户端连接 ws.on('connection', (socket, req) => { console.log(`connection from ${ req.socket.remoteAddress }`); // 接收消息 socket.on('message', (msg, binary) => { // 向所有客户端广播 ws.clients.forEach(client => { client.readyState === WebSocket.OPEN && client.send(msg, { binary }); }); }); // 关闭 socket.on('close', () => { console.log(`disconnection from ${ req.socket.remoteAddress }`); }); });
使用Node.js ws 库:
-
当浏览器尝试连接时,触发
"connection"
事件。处理函数接收一个socket
对象,用于与该设备进行通信。此对象在整个连接期间必须保持。 -
当浏览器发送消息时,触发
socket "message"
事件。处理函数将消息广播回每个已连接的浏览器(包括发送消息的浏览器)。 -
当浏览器断开连接时,通常是关闭或刷新标签页时,触发
socket "close"
事件。
客户端JavaScript代码概览
应用程序的static/main.js
文件运行一个wsInit()
函数,并将WebSocket服务器的地址(页面域名加上在HTML页面模板中定义的端口值)传递给它:
wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);
// 处理WebSocket通信
function wsInit(wsServer) {
const ws = new WebSocket(wsServer);
// 连接到服务器
ws.addEventListener('open', () => {
sendMessage('entered the chat room');
});
当浏览器连接到WebSocket服务器时,open
事件被触发。处理函数通过调用sendMessage()
发送一条已进入聊天室
消息:
// 发送消息
function sendMessage(setMsg) {
let
name = dom.name.value.trim(),
msg = setMsg || dom.message.value.trim();
name && msg && ws.send( JSON.stringify({ name, msg }) );
}
sendMessage()
函数从HTML表单中获取用户名和消息,尽管消息可以通过任何传递的setMsg
参数覆盖。这些值被转换为JSON对象,并使用ws.send()
方法发送到WebSocket服务器。
WebSocket服务器接收传入的消息,触发"message"
处理程序(见上文),并将其广播回所有浏览器。这会在每个客户端触发"message"
事件:
// 接收消息
ws.addEventListener('message', e => {
try {
const
chat = JSON.parse(e.data),
name = document.createElement('div'),
msg = document.createElement('div');
name.className = 'name';
name.textContent = (chat.name || 'unknown');
dom.chat.appendChild(name);
msg.className = 'msg';
msg.textContent = (chat.msg || 'said nothing');
dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });
}
catch(err) {
console.log('invalid JSON', err);
}
});
处理程序在事件对象的.data
属性上接收传输的JSON数据。该函数将其解析为JavaScript对象并更新聊天窗口。
最后,每当表单的"submit"
处理程序触发时,都会使用sendMessage()
函数发送新消息:
// 表单提交
dom.form.addEventListener('submit', e => {
e.preventDefault();
sendMessage();
dom.message.value = '';
dom.message.focus();
}, false);
处理错误
当WebSocket通信失败时,会触发"error"
事件。这可以在服务器上处理:
// 在服务器上处理WebSocket错误
socket.on('error', e => {
console.log('WebSocket error:', e);
});
和/或在客户端上:
// 在客户端上处理WebSocket错误
ws.addEventListener('error', e => {
console.log('WebSocket error:', e);
})
只有客户端能通过再次运行new WebSocket()
构造函数来重新建立连接。
关闭连接
任何一方都可以随时使用连接的.close()
方法关闭WebSocket。你可以选择性地提供code
整数和reason
字符串(最多123字节)参数,这些参数在断开连接前会传输到另一方。
高级WebSocket
在Node.js中管理WebSocket很容易:一台设备使用.send()
方法发送消息,这会在另一台设备上触发"message"
事件。每台设备如何创建和响应这些消息可能会更具挑战性。以下部分描述了你可能需要考虑的问题。
WebSocket安全
WebSocket协议不处理授权或认证。你不能保证传入的通信请求来自浏览器或登录到你的Web应用程序的用户——特别是当Web服务器和WebSocket服务器可能在不同的设备上时。初始连接接收包含Cookie和服务器Origin
的HTTP头,但有可能伪造这些值。
以下技术确保将WebSocket通信限制为授权用户:
-
在进行初始WebSocket请求之前,浏览器会与HTTP Web服务器联系(可能使用Ajax)。
-
服务器检查用户的凭据并返回一个新的授权票据。该票据通常会引用包含用户ID、IP地址、请求时间、会话过期时间以及其他所需数据的数据库记录。
-
浏览器在初始握手时将票据传递给WebSocket服务器。
-
WebSocket服务器验证票据并在允许连接前检查IP地址、过期时间等因素。当票据无效时,它执行WebSocket
.close()
方法。 -
WebSocket服务器可能需要不时地重新检查数据库记录,以确保用户会话保持有效。
重要的是,始终验证传入数据:
-
与HTTP类似,WebSocket服务器容易受到SQL注入和其他攻击。
-
客户端绝不应将原始值注入DOM或执行JavaScript代码。
单个与多个WebSocket服务器实例
考虑一个在线多人游戏。该游戏有多个宇宙在独立的游戏实例中进行:universeA
、universeB
和universeC
。玩家连接到一个特定的宇宙:
universeA
:由player1
、player2
和player3
加入universeB
:由player99
加入
你可以实施以下方案:
-
A separate WebSocket server for each universe.
A player action in
universeA
would never be seen by those inuniverseB
. However, launching and managing separate server instances could be difficult. Would you stopuniverseC
because it has no players, or continue to manage that resource? -
为所有游戏宇宙使用一个WebSocket服务器。
这使用较少的资源且更易于管理,但WebSocket服务器必须记录每个玩家加入的宇宙。当
player1
执行一个动作时,它必须广播给player2
和player3
,而不是player99
。
多WebSocket服务器
示例聊天应用能处理数百并发用户,但一旦受欢迎程度和内存使用超过临界点,应用将崩溃。最终需通过增加更多服务器进行水平扩展。
每台WebSocket服务器仅能管理其连接的客户端。浏览器向serverX
发送的消息无法广播给连接到serverY
的用户。可能需要实施后端发布者-订阅者(pub-sub)消息系统,例如:
-
WebSocket
serverX
希望向所有客户端发送消息,它在pub-sub系统上发布该消息。 -
所有订阅pub-sub系统的WebSocket服务器接收到新消息事件(包括
serverX
),各自处理消息并广播给其连接的客户端。
WebSocket消息传输效率
WebSocket通信快速,但服务器需管理所有连接的客户端。构建多人动作游戏时,必须考虑消息机制与效率,尤其是:
-
如何将玩家动作同步至所有客户端设备?
-
若
玩家1
与玩家2
位置不同,是否需向玩家2
发送其无法观察到的动作信息? -
如何应对网络延迟或通信滞后?拥有高速设备与连接的玩家是否会享有不公平优势?
快节奏游戏需做出权衡。设想在本地设备上游戏,但某些对象受其他玩家活动影响。游戏通常不持续发送每个对象的确切位置,而是发送更简单、频率较低的消息,例如:
物体X
出现在点X物体Y
获得新方向与速度物体Z
已被摧毁。
每个客户端游戏都在填补空白。当objectZ
爆炸时,无论在每个设备上爆炸效果看起来是否不同,都无关紧要。
结论
Node.js使得处理WebSockets变得简单。它不一定使实时应用的设计或编码变得更容易,但这项技术不会成为你的阻碍!
主要缺点:
-
WebSockets需要自己的独立服务器实例。Ajax
Fetch()
请求和服务器推送事件可以由你已经在运行的网络服务器处理。 -
WebSocket服务器需要自己的安全和授权检查。
-
断开的WebSocket连接必须手动重新建立。
但不要因此而放弃!
关于使用WebSockets和Server-Sent Events的实时应用的常见问题(FAQs)
WebSockets在性能和功能上与HTTP有何不同?
WebSockets提供了一个基于单一TCP连接的全双工通信通道,意味着数据可以同时双向发送和接收。这相较于HTTP有了显著改进,因为HTTP每发起一次请求都需要建立新的连接。WebSockets还支持实时数据传输,非常适合需要即时更新的应用,例如聊天应用或实时体育更新。相比之下,HTTP是无状态的,每个请求-响应对都是独立的,这更适合不需要实时更新的应用场景。
能否解释一下WebSocket连接的生命周期?
WebSocket连接的生命周期始于握手阶段,这一阶段将HTTP连接升级为WebSocket连接。一旦连接建立,客户端和服务器之间就可以来回传输数据,直到任何一方决定关闭连接。连接可以通过客户端或服务器发送关闭帧来终止,随后另一方确认该关闭帧。
如何在Android应用中实现WebSockets?
在Android应用中实现WebSockets涉及创建一个能连接到WebSocket服务器的WebSocket客户端。这可以通过使用OkHttp或Scarlet等库来完成。设置好客户端后,你可以打开与服务器的连接,发送和接收消息,并处理诸如连接开启、消息接收和连接关闭等不同事件。
什么是服务器发送事件(Server-Sent Events),它们与WebSockets有何不同?
服务器发送事件(SSE)是一种标准,允许服务器通过HTTP向客户端推送更新。与WebSocket不同,SSE是单向的,意味着它们仅允许数据从服务器发送到客户端。这使得它们不太适合需要双向通信的应用程序,但对于仅需要服务器更新的应用程序来说,它们可以是一种更简单、更高效的解决方案。
WebSocket和Server-Sent Events的常见用例有哪些?
WebSocket常用于需要实时双向通信的应用,如聊天应用、多人在线游戏和协作工具。而Server-Sent Events则常用于需要从服务器接收实时更新的应用,例如实时新闻更新、股票价格更新或长时间运行任务的进度报告。
如何在Spring Boot应用中处理WebSocket连接?
Spring Boot通过Spring WebSocket模块支持WebSocket通信。您可以使用@EnableWebSocket注解来启用WebSocket支持,并定义一个WebSocketHandler来处理连接生命周期和消息处理。您还可以使用SimpMessagingTemplate向已连接的客户端发送消息。
使用WebSocket时有哪些安全考虑?
与任何网络技术一样,WebSockets可能面临多种安全威胁,例如跨站WebSocket劫持(CSWSH)和拒绝服务(DoS)攻击。为降低这些风险,应始终使用安全的WebSocket连接(wss://),并对所有传入数据进行验证和净化。同时,考虑采用认证和授权机制以控制对WebSocket服务器的访问。
我能否将WebSockets与REST API结合使用?
可以,您可以将WebSockets与REST API协同使用。虽然REST API非常适合无状态的请求-响应通信,但WebSockets适用于实时双向通信。这在需要即时更新的应用中特别有用,如聊天应用或实时体育更新。
如何测试WebSocket服务器?
有多种工具可用于测试WebSocket服务器,如WebSocket.org的回显测试或Postman。这些工具允许您打开到服务器的WebSocket连接,发送消息并接收响应。您还可以使用Jest或Mocha等库为WebSocket服务器编写自动化测试。
WebSockets和服务器发送事件(Server-Sent Events)有哪些限制?
尽管WebSockets和服务器发送事件为实时通信提供了强大的功能,但它们也有其局限性。例如,并非所有浏览器和网络都支持这些技术,如果不妥善管理,它们可能会消耗大量资源。此外,与传统HTTP通信相比,它们的实现和管理可能更为复杂。
Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/