深入浅出:彻底搞懂 WebSocket、SSE 与心跳机制

0 阅读8分钟

在当今的大前端与全栈开发中,实时通信技术已经成为了不可或缺的技能。特别是随着 ChatGPT 等大语言模型(LLM)的爆火,实时流式输出技术再次被推上风口浪尖。

很多前端同学对传统的 HTTP 协议了如指掌,但在面对多协议开发(如 WebSocket、SSE)时,往往会感到一丝陌生。在面试中,面试官也常常借此切入,考察候选人对 408 计算机网络底层协议的理解,以及浏览器(B/S架构)与传统客户端(C/S架构)在通信上的差异。

今天,我们将从业务场景选型出发,带你深入剖析 HTTP、SSE 与 WebSocket 的核心差异,并手把手带你用 Node.js (Koa) 实现一个支持多人在线的 Chat App,最后深入探讨长连接中必不可少的“心跳机制”。

一、 业务场景选型:为什么聊天应用必须用 WebSocket?

假设我们要开发一个多人在线聊天室(Chat App),面对这个需求,我们通常有三种技术栈选择。让我们来看看它们的优劣:

1. HTTP 协议(轮询方案)

HTTP 协议是基于传统的“请求-响应”模型的短连接通信。

  • 机制: 客户端发起请求,服务端返回响应。如果要获取最新消息,前端必须使用 setInterval 等方式不断发起 Fetch 或 Ajax 请求(这种技术称为短轮询)。
  • 痛点: 这种方式性能极差且十分复杂。即使 HTTP 协议可以通过 Connection: keep-alive 复用底层的 TCP/IP 通道,但其应用层依然是单向的“一问一答”模式。这会导致大量无效请求,极大地浪费服务器带宽和性能。

2. SSE (Server-Sent Events)

SSE 是一种轻量级的长连接持久化单向通道技术。

  • 机制: 建立连接后,服务器可以单向、持续地向客户端推送文本数据。
  • 适用场景: 它是 LLM(大语言模型)流式打字机输出的当下业务热点。非常适合“用户 Prompt 一次,LLM 流式输出”的场景。
  • 痛点: 聊天是双向的(既要收消息,又要发消息)。SSE 只能做到服务端持续推送,无法做到用户端向服务端的持续推送,因此不适合全双工的聊天应用。

3. WebSocket 协议(终极方案)

WebSocket 是 HTML5 提供的新特性,用于在 Web 端实现即时通讯。

  • 机制: 它是一种在浏览器和服务器之间建立“长连接”的协议,可以实现真正的双向实时全双工通信。
  • 优势: 一次连接,持续通信。服务器端和用户端都可以随时主动向对方推送数据,完美契合聊天应用实时收发、多人同步的需求。

📊 核心特性全方位对比

为了更直观地理解,我们可以通过下表快速对比这三种通信方式:

对比维度HTTPSSE (Server-Sent Events)WebSocket
通信方式单向(客户端发起,服务端响应)单向(仅服务端向客户端推送)双向 / 全双工(双方均可主动发送)
连接持久性基于请求与响应,默认短连接(可 Keep-Alive)长连接(持久化单向通道)长连接(持久化双向通道)
数据格式无限制(文本、二进制、JSON 等)仅限文本(通常为 JSON 或纯文本)文本帧或二进制帧
协议类型HTTP/1.1, HTTP/2, HTTP/3HTTP 协议 (Content-Type: text/event-stream)独立协议:ws://wss://
浏览器兼容完美支持所有浏览器不支持 IE,现代浏览器支持良好支持所有现代浏览器

二、 WebSocket 核心原理解析

1. 什么是 WebSocket?

简单来说,WebSocket = Web + Socket。

传统的 Socket 是基于 TCP/IP 的实时通讯双工协议,常用于 QQ、微信、端游等 C/S(客户端/服务器)架构中。而 HTML5 提供的 WebSocket 特性,成功将这种底层的双向通信能力带入了 B/S(浏览器/服务器)架构中。

2. 核心考点:101 状态码与协议升级

很多初学者会有疑问:既然 WebSocket 叫 ws://,那它和 http:// 还有关系吗?

答案是:WebSocket 的第一次握手,依然使用的是 HTTP 协议。

当我们在前端执行 new WebSocket('ws://localhost:3000/ws') 时,浏览器会先发送一个标准的 HTTP 请求,并在请求头中带上特殊标记(Upgrade: websocket)。

服务器收到请求后,如果同意升级,会返回 HTTP 101 Switching Protocols 状态码。在这之后,双方的通信通道正式切换为 WebSocket 协议,不再使用臃肿的 HTTP 请求头。

三、 实战:从零手写基于 Koa 的聊天室

接下来,我们将使用 Node.js 结合 Koa 框架,亲手实现一个极简但五脏俱全的聊天应用。

1. 环境准备

我们需要安装 Koa 以及使 Koa 支持 WebSocket 的中间件:

pnpm i koa koa-websocket

提示:Koa 原生只支持 HTTP 请求,koa-websocket 库的作用是劫持和升级 HTTP 协议,让 Koa 能够处理 WebSocket 通信。

2. 服务端代码解析 (server.js)

以下是完整的服务端代码与深度解析:

// 引入 Koa 框架与 koa-websocket 库
const Koa = require('koa');
const WebSocket = require('koa-websocket');

// 初始化 Koa 实例,并立即用 WebSocket() 将其包裹
const app = WebSocket(new Koa());

// 创建一个 Set 集合,用来保存所有当前连接到服务器的客户端
// 使用 Set 可以天然保证元素的唯一性,防止同一客户端被重复添加
const clients = new Set();

// ==========================================
// 1. HTTP 路由部分:给浏览器下发前端页面
// ==========================================
app.use(async (ctx) => {
    // 第一次与服务器通信使用 HTTP 协议,拿到前端 HTML 页面
    // 采用简单的服务端渲染 (SSR) 做法
    ctx.body = `
    <!DOCTYPE html>
    <html>
    <body>
        <div id="messages" style="height:300px;overflow-y:scroll;"></div>
        <input type="text" id="messageInput"/>
        <button onclick="sendMessage()">发送</button>
        <script>
        // 利用 HTML5 原生的 WebSocket API 发起连接
        // 协议变为 ws://
        const ws = new WebSocket('ws://localhost:3000/ws')

        // 监听来自服务端的推送消息
        ws.onmessage = function(event) {
            const messages = document.getElementById('messages');
            messages.innerHTML += '<div>' + event.data + '</div>';
        }

        // 发送消息函数
        function sendMessage() {
            const input = document.getElementById('messageInput');
            ws.send(input.value); // 通过 WebSocket 通道发给服务端
            input.value = '';
        }
        </script>
    </body>
    </html>
    `;
})

// ==========================================
// 2. WebSocket 路由部分:处理实时双向通信
// ==========================================
app.ws.use(async (ctx, next) => {
    // 客户端连接成功,将当前专属的 websocket 实例存入 Set 集合
    clients.add(ctx.websocket);

    // 监听客户端发来的 'message' 事件
    ctx.websocket.on('message', (message) => {
        // 【核心逻辑:广播 Broadcast】
        for(const client of clients) {
            // 遍历所有连接的人,将消息发给所有人(包括发送者自己)
            client.send(message.toString());
        }
    })

    // 监听断开连接事件(如用户关闭 Tab)
    ctx.websocket.on('close', () => {
        // 必须从集合中移除失效连接,防止广播报错与内存泄漏
        clients.delete(ctx.websocket);
    })
})

// ==========================================
// 3. 启动服务
// ==========================================
app.listen(3000, () => {
    console.log('Server is running on port 3000');
})

通过这段代码,我们清晰地看到了 HTTP 到 WebSocket 的演进:用户首先通过传统的 HTTP 请求获取页面,页面加载后,脚本中的 new WebSocket() 发起协议升级请求,彻底切换到高效的 ws 协议进行实时通信。

四、 进阶:深入理解心跳机制(Heartbeat)与断线重连

做完了基本的聊天功能,我们就算是掌握 WebSocket 了吗?还不够!在真实的生产环境中,网络情况极其复杂。WebSocket 和 SSE 都是长连接,既然是长连接,就必须面对一个致命问题:连接假死

1. 为什么需要心跳机制?

心跳机制是指客户端和服务端定期互相报平安,用来检测连接是否还活着的一种技术。我们需要它的主要原因包括:

  • 网络断开与掉线: 当用户突然断网(如走进电梯)、拔掉网线等,底层的 TCP 可能无法正常发出挥手包。服务端以为连接还在,客户端却已经掉线了。
  • 主动监测: 必须主动监测连接状态,通过发送 ping/pong 来测试链路是否通畅。

💡 延伸思考:TCP 不是自带 Keep-Alive 吗?

很多同学知道 HTTP Connection: keep-alive 底层复用 TCP 通道。TCP 协议确实也有自己的 Keep-Alive 探活机制,但它的默认探测周期极长(通常以小时计),且只能探测网络层面的死活,无法判断应用层进程是否卡死。因此,在业务代码中实现应用层的心跳机制是业界标准做法。

2. 心跳机制的核心实现思路

一个健壮的心跳机制通常包含以下经典的“三步曲”:

  • 第一步:定时发送 Ping

    客户端通过 setInterval 定期(例如每 30 秒)向服务端发送探测包。

    setInterval(() => {
        ws.send(JSON.stringify({type: 'ping'}));
    }, 30000);
    
  • 第二步:接收并响应 Pong

    服务器端收到消息后,解析判断如果 type === 'ping',则立即回传一个类型为 pong 的消息。

    if(msg.type === 'ping') {
        ws.send(JSON.stringify({type: 'pong'}));
    }
    
  • 第三步:超时判断 + 重连机制

    客户端在发送 Ping 之后,会启动一个超时定时器。如果在规定时间内没有收到服务端的 Pong 响应,客户端就可以判定当前连接已断开,进而触发前端的 UI 提示,并执行重连逻辑(Reconnection)。

五、 总结

回顾整篇文章,我们从业务痛点出发,明白了为什么在即时通讯场景下,轮询太慢、SSE 不适用,而 WebSocket 是最终解。我们剖析了 WebSocket 101 状态码 的底层升级原理,并用极简的代码手撸了一个基于 Koa 的全双工广播聊天室。最后,我们补齐了长连接应用走向生产环境的最后一块拼图——心跳机制。