那个实时头疼——不是 WebSockets 的错,而是你的框架

发布: (2025年12月28日 GMT+8 22:57)
8 min read
原文: Dev.to

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) { … }
}
  • UserResourceChatEndpoint 存在于两个平行的世界。
  • 每个都有各自的生命周期、注解和注入机制。
  • 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.getwss.on('connection') 完全是两套独立的逻辑。
  • 共享中间件相当繁琐:你必须在 WebSocket 升级请求时手动调用 Express 中间件。

状态共享变成噩梦

实时应用的核心是 状态

  • 哪个用户对应哪个 WebSocket 连接?
  • 用户订阅了哪些频道?

在分层架构中:

  1. REST API 处理登录并将会话数据存储在 HTTP 会话存储中。
  2. 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)

Back to Blog

相关文章

阅读更多 »

Redis Pub/Sub 与 Redis Streams

Redis 提供了多种在服务之间处理消息的方式——其中 Pub/Sub 和 Streams 是两个关键的内置选项。虽然它们看起来相似……