那个实时头疼——不是 WebSockets 的错,而是你的框架
Source: Dev.to
介绍
我记得几年前,我带领一个团队开发实时股票行情仪表盘。 📈
最初,大家的热情异常高涨。我们都很兴奋能亲手打造一个 实时 的应用。
但很快我们就陷入了泥潭。我们选择的技术栈对普通的 REST API 还能勉强跑得不错,但一旦涉及到 WebSocket,整个局面就变得面目全非。
我们的代码库被划分成了两个世界:
- 主应用 – 处理 HTTP 请求。
- 独立模块 – 处理 WebSocket 连接。
在这两个世界之间共享状态(例如用户的登录信息)成了一场噩梦。我们不得不求助于诸如 Redis 或消息队列之类的“聪明”(更确切地说是丑陋)方法来同步数据。 🐛 代码变得越来越复杂,bug 也成倍增长。最终我们交付了产品,但开发过程却像一次漫长而痛苦的拔牙。 🦷
这段经历给了我深刻的教训:对于需要实时交互的现代 Web 应用,框架如何直接处理 WebSocket 决定了开发体验以及项目最终的成败。许多框架声称“支持” WebSocket,但它们大多只是把一个 WebSocket 模块“焊接”到主框架上。这种“嫁接”方案往往是我们所有头疼问题的根源。
“嫁接”解决方案的问题
Java
// REST endpoint
@Path("/users")
public class UserResource {
@GET
public Response getUser(@Context SecurityContext sc) { … }
}
// WebSocket endpoint
@ServerEndpoint("/chat")
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session) { … }
}
UserResource和ChatEndpoint存在于两个平行的世界。- 每个都有各自的生命周期、注解和注入机制。
- 在
ChatEndpoint中获取当前用户的认证信息通常需要深入底层的 HTTP 会话——框架很少让你轻松做到这一点。 😫
Node.js
// Express HTTP routes
const app = require('express')();
app.get('/profile', authMiddleware, (req, res) => { … });
// WebSocket server (ws)
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', (ws, request) => {
// No direct access to Express middleware here
});
app.get和wss.on('connection')完全是两套独立的逻辑。- 共享中间件相当繁琐:你必须在 WebSocket 升级请求时手动调用 Express 中间件。
状态共享变成噩梦
实时应用的核心是 状态:
- 哪个用户对应哪个 WebSocket 连接?
- 用户订阅了哪些频道?
在分层架构中:
- REST API 处理登录并将会话数据存储在 HTTP 会话存储中。
- WebSocket 模块无法直接访问该会话。
结果:你被迫引入外部依赖(例如 Redis)作为 “状态中介”。这会增加运维成本并带来新的故障点。 💔
Source: …
原生集成的 WebSocket 框架:Hyperlane
Hyperlane 将 WebSocket 处理器 视作任何其他 HTTP 路由处理器——一个普通的 async 函数,接收一个 Context 对象。它们是自然的“兄弟”,而不是远亲。
一致的 API
- 中间件 – 编写一次,即可用于 HTTP 和 WebSocket 路由。
- Context – 保存请求特定的数据(例如,已认证的用户)。
- 发送数据 – 对于 HTTP 响应体、SSE 事件以及 WebSocket 消息,使用相同的方法:
ctx.send_body().await;
框架抽象了 WebSocket 协议的细节(消息帧、掩码等),你只需要关心业务负载(Vec)。
示例:带认证的 WebSocket 路由
// auth_middleware.rs
pub async fn auth_middleware(mut ctx: Context, next: Next) -> Result {
let token = ctx.request.headers().get("Authorization");
// …validate token…
ctx.state.insert("user", user);
next.run(ctx).await
}
// secure_websocket_route.rs
pub async fn secure_websocket_route(mut ctx: Context) -> Result {
// The user was injected by `auth_middleware`
let user = ctx.state.get::("user").unwrap();
// Upgrade to WebSocket
let mut ws = ctx.upgrade().await?;
// Bind the connection to the user
ws.on_message(move |msg| {
// handle incoming messages, knowing `user`
}).await;
Ok(())
}
- HTTP 升级请求首先会经过
auth_middleware。 - 若令牌有效,用户信息会被存入
ctx.state。 - 在
secure_websocket_route中,我们 无需任何胶水代码 就能获取用户信息。 😎
向聊天室广播
Hyperlane 的文档展示了如何使用辅助 crate(例如 hyperlane-broadcast)进行广播。模式如下:
use hyperlane_broadcast::Broadcaster;
pub async fn chat_handler(mut ctx: Context) -> Result {
let mut ws = ctx.upgrade().await?;
let broadcaster = Broadcaster::new();
ws.on_message(move |msg| {
// Forward the message to all connected clients
broadcaster.broadcast(msg);
}).await;
Ok(())
}
Technical tip: 在应用启动时 只注册一次 广播器,并通过
Context共享,以避免创建多个实例。这类“老手”建议帮助开发者规避常见陷阱。 👍
结论
实时功能不应再是 Web 开发中的“特殊问题”;它是现代应用的核心组成部分。如果你的框架仍然迫使你以完全独立的方式处理 WebSockets,你必然会遭遇上文所述的状态共享、middleware 集成以及运维复杂性等头痛问题。
一个把 WebSockets 视为 一等公民 的框架——提供统一的 API、共享的 middleware,以及单一的 Context 对象——可以显著减少样板代码,消除外部状态中介,让你专注于业务逻辑,而不是协议的花活。
Hyperlane 展示了如何干净高效地实现这一点。试一试,你会看到实时开发可以变得多么顺畅。🚀
…如果采用碎片化的方式,那么它可能已经不适合这个时代。
一个真正现代的框架应当将实时通信无缝集成到其核心模型中。它应提供一致的 API、可共享的 middleware 生态系统以及统一的状态管理机制。Hyperlane 向我们展示了这种可能性。
所以,下次在开发实时功能时感到头疼,请考虑问题可能不在于 WebSockets 本身,而在于你仍在使用的、仍抱有 “嫁接”思维 的过时框架。是时候改变了!🚀
GitHub Home (link to the repository)