尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

前端实时数据通信:短轮询、长轮询/SSE与WebSocket选型指南

前端实时数据通信:短轮询、长轮询/SSE与WebSocket选型指南 1. 从“刷新”到“推送”实时数据的前世今生干了这么多年前端我见过太多项目在“实时数据”这个需求上栽跟头。老板说“我要看实时销量”产品经理说“用户消息要秒达”听起来理所当然但真做起来你会发现从“定时刷新”到“真正实时”中间隔着一整个技术选型的鸿沟。很多新手甚至一些工作两三年的朋友一提到实时数据脑子里可能就只剩下“WebSocket”这一个选项或者更糟还在用setInterval暴力轮询把服务器拖得半死不活。今天我们就用十分钟把前端获取实时数据的三种主流方式——短轮询、长轮询及SSE、WebSocket——彻底掰扯清楚。这不仅仅是面试八股文更是你日常开发中做技术选型时必须权衡的决策点。我会结合真实的踩坑经历告诉你每种方案在什么场景下是“神器”在什么场景下又是“毒药”。比如你正在做一个后台数据监控面板每秒要更新几十个图表该用哪种做一个在线聊天室又该用哪种这里面门道可多了。2. 方案一短轮询——简单粗暴的“刷新”按钮短轮询Short Polling可能是最古老、最直观的“实时”方案了。它的逻辑简单到令人发指前端就像个不耐烦的用户每隔一段时间比如2秒就向后端发一次请求“嘿有数据更新吗”不管后端有没有新数据都会立即响应。没有新数据就返回空或者状态码。2.1 工作原理与代码实现它的本质是利用浏览器的定时器周期性地发起普通的HTTP请求通常是GET。我们用最基础的fetchAPI就能实现。// 一个简单的短轮询示例 function shortPolling(url, interval 2000) { let isPolling true; const poll async () { if (!isPolling) return; try { const response await fetch(url); const data await response.json(); console.log(轮询到数据:, data); // 处理数据例如更新UI updateUI(data); } catch (error) { console.error(轮询请求失败:, error); // 错误处理可以增加重试逻辑或拉长轮询间隔 } finally { // 无论成功失败都计划下一次轮询 if (isPolling) { setTimeout(poll, interval); } } }; // 启动轮询 poll(); // 返回一个停止轮询的函数 return () { isPolling false; console.log(短轮询已停止); }; } // 使用示例 const stopPolling shortPolling(/api/data, 2000); // 在组件卸载或需要停止时调用 // stopPolling();为什么这样设计这里有几个关键点第一用isPolling标志位控制轮询生命周期防止组件卸载后请求还在继续这是内存泄漏的常见坑。第二错误处理在catch中并且finally块确保无论成功失败都会安排下一次请求保证了轮询的持续性。第三返回一个清理函数这是符合React Hooks或现代前端开发模式的好习惯。2.2 适用场景与致命缺陷短轮询并非一无是处它在特定场景下依然有价值数据变化不频繁且对实时性要求不高例如每隔30秒或1分钟检查一次系统公告、版本更新信息。客户端环境受限在一些老旧的浏览器或特殊环境如某些嵌入式设备的Web界面中WebSocket或SSE可能不被支持短轮询是保底方案。实现极其简单快速原型当你需要快速验证一个想法不想在通信协议上花费时间时。但是它的缺陷在实时场景下是致命的高延迟Latency数据的“新鲜度”最多等于轮询间隔。你设2秒数据就可能延迟2秒才到。这不是真正的实时。高开销Overhead即使没有数据更新也会产生大量无效的HTTP请求。每个请求都有完整的Header、TCP握手HTTP/1.1下可能还有队头阻塞、SSL握手如果是HTTPS开销。这对服务器和网络都是巨大的浪费。想象一下1000个在线用户每2秒请求一次服务器每秒就要处理500个请求其中绝大部分是“空跑”。服务器压力这些无用的请求会毫无意义地占用服务器的连接数、CPU和I/O资源在高并发下极易成为性能瓶颈。实操心得我曾维护过一个老旧的报表系统用的就是5秒一次的短轮询。平时相安无事一到业务高峰服务器负载直接飙红。后来我们做的第一件事不是优化数据库而是先把轮询间隔改成10秒并增加了“无变化时跳过UI渲染”的逻辑负载瞬间下降一半。教训是如果你不得不用短轮询一定要把间隔时间设得尽可能长并在客户端做去重和节流渲染。3. 方案二长轮询与SSE—— “待命”与“单工广播”为了克服短轮询的无效请求问题更聪明的“长轮询”出现了。而SSE则是HTML5标准中为这种“服务器推送”模式量身定制的协议。3.1 长轮询挂起的请求长轮询Long Polling可以理解为“聪明的短轮询”。前端发起一个请求但这个请求会被后端“挂起”Hold直到后端有数据更新或者等待超时比如30秒。一旦有数据或超时后端立即响应前端收到响应后马上再发起下一个新的长轮询请求如此循环。function longPolling(url, timeout 30000) { let isActive true; const poll async () { if (!isActive) return; try { // 可以添加超时控制参数但注意服务器端也需要配置相应的超时 const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), timeout); const response await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); const data await response.json(); console.log(长轮询到数据:, data); updateUI(data); // 关键立即发起下一次轮询 poll(); } catch (error) { if (error.name AbortError) { console.log(长轮询请求超时重新连接); } else { console.error(长轮询请求失败:, error); } // 错误或超时后延迟一段时间再重试避免疯狂重连 setTimeout(poll, 2000); } }; poll(); return () { isActive false; }; }为什么这样设计这里使用了AbortController来实现客户端超时控制这是一个比传统setTimeout更优雅的方式能真正取消网络请求。超时后我们延迟2秒再重连这是为了避免在服务器临时故障时客户端立即发起大量重连请求形成“重连风暴”。长轮询减少了大量无用的请求数据延迟可以做到很低一旦服务器有数据就返回。但它仍有缺点每个连接在大部分时间是空闲的但仍占用服务器资源如一个线程或一个协程连接不断建立和断开仍有开销并且实现复杂度比短轮询高需要服务器端配合保持连接。3.2 SSE长轮询的“标准升级版”SSEServer-Sent Events让长轮询模式标准化、更高效。它基于HTTP协议允许服务器主动向客户端推送数据但连接方向是单向的服务器到客户端。一旦连接建立这个连接会一直保持服务器可以随时通过这个连接发送数据片段。// 前端代码使用 EventSource API (非常简单) function setupSSE(url) { const eventSource new EventSource(url); // 监听默认事件服务器未指定事件名时发送的数据 eventSource.onmessage (event) { const data JSON.parse(event.data); console.log(收到SSE数据:, data); updateUI(data); }; // 监听自定义事件服务器可以发送不同类型的事件 eventSource.addEventListener(stock-update, (event) { console.log(股票更新:, event.data); }); eventSource.addEventListener(news-alert, (event) { console.log(新闻提醒:, event.data); }); // 错误处理连接出错时会触发并自动尝试重连 eventSource.onerror (error) { console.error(SSE连接错误:, error); // 你可以在这里决定是否手动关闭 eventSource // eventSource.close(); }; // 返回关闭函数 return () { eventSource.close(); }; } // 使用 const closeSSE setupSSE(/api/sse-stream);为什么SSE更优首先浏览器原生支持EventSourceAPI使用极其简单。其次连接是真正的持久化省去了长轮询中反复建立连接的开销。第三它自动支持断线重连。第四服务器推送的数据格式是定义好的data:、event:、id:等字段更规范。SSE的局限性最大的限制就是单向通信。客户端无法通过这个连接向服务器发送数据。如果需要交互你得额外使用普通的HTTP请求如Fetch。另外它只支持文本数据通常用JSON不支持二进制流。对于不支持EventSource的旧浏览器主要是IE需要polyfill。实操心得SSE是实现“实时通知”、“实时报表”、“股票价格推送”这类场景的绝佳选择。我曾经用SSE重构了一个实时监控大屏替换掉之前的短轮询。改造后服务器连接数从每秒上千个骤降到与客户端数持平每个用户一个持久连接网络流量也大幅下降。但务必注意SSE连接默认有重试机制如果服务器崩溃重启所有客户端会疯狂重连。生产环境一定要在服务端做平滑重启和连接迁移或者在客户端实现带指数退避的智能重连。3.3 长轮询与SSE的服务端实现要点无论是长轮询还是SSE都对服务端有要求必须能够保持住HTTP连接并异步地返回数据。这在不同的后端框架中实现方式不同。以Node.js (Express) 为例实现一个简单的SSE端点// 服务器端 SSE 示例 (Express.js) app.get(/api/sse-stream, (req, res) { // 1. 设置SSE必需的响应头 res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, // 重要允许跨域 if needed Access-Control-Allow-Origin: * }); // 2. 发送一个初始注释可选帮助建立连接 res.write(: 连接已建立\n\n); // 3. 模拟定期发送数据 const intervalId setInterval(() { const data { timestamp: new Date().toISOString(), value: Math.random() * 100 }; // 格式必须遵守 data: JSON字符串 \n\n res.write(data: ${JSON.stringify(data)}\n\n); // 注意必须调用 res.flush() 以确保数据立即发送取决于服务器和中间件 }, 1000); // 4. 客户端断开连接时清理 req.on(close, () { console.log(客户端断开连接); clearInterval(intervalId); res.end(); }); });服务端关键点头部HeadersContent-Type: text/event-stream是必须的。Cache-Control: no-cache防止缓存。Connection: keep-alive保持连接。数据格式每条消息以data:开头以两个换行符\n\n结束。可以发送event:定义事件类型id:设置消息ID用于断线重连。连接管理必须监听请求的close事件及时清理定时器或释放资源防止内存泄漏。性能一个连接就是一个常驻的请求句柄。对于高并发需要使用异步I/O和非阻塞框架如Node.js、Go、Java NIO等避免一个连接阻塞一个线程。4. 方案三WebSocket——全双工实时通信的王者当你的应用需要真正的、双向的、低延迟的对话时WebSocket是唯一的选择。它不是在HTTP上打补丁而是在TCP之上建立了一个独立的、全双工Full-Duplex的通信通道。连接一旦通过HTTP握手升级建立双方就可以在任何时刻互相发送数据没有请求/响应模型的概念。4.1 为什么是WebSocket核心优势解析想象一下在线游戏、协同编辑文档如Google Docs、实时聊天、股票交易终端。这些场景中客户端和服务器需要频繁地、低延迟地互相“说话”。用轮询或SSE会非常别扭且低效。真正的低延迟数据准备好后立即发送没有轮询间隔延迟通常在毫秒级。极低的协议开销建立连接后数据传输的头部开销极小通常只有2-10字节远小于HTTP头部。双向通信服务器可以主动推客户端也可以主动发沟通自如。支持二进制和文本数据可以传输Blob、ArrayBuffer等二进制数据适合传输文件、音频、视频流片段。4.2 客户端实现与核心API浏览器通过WebSocket对象提供支持。function setupWebSocket(url) { // 1. 创建WebSocket连接协议是 ws:// 或 wss:// (加密) const socket new WebSocket(url); // 2. 监听连接打开事件 socket.onopen (event) { console.log(WebSocket连接已打开); // 连接建立后可以发送一些初始化数据 socket.send(JSON.stringify({ type: auth, token: user-token-123 })); }; // 3. 监听收到消息事件 socket.onmessage (event) { // event.data 可能是字符串文本或 Blob/ArrayBuffer二进制 let data; if (typeof event.data string) { data JSON.parse(event.data); console.log(收到文本消息:, data); handleMessage(data); } else { // 处理二进制数据例如图片 console.log(收到二进制数据大小:, event.data.size); handleBinaryData(event.data); } }; // 4. 监听连接错误事件 socket.onerror (error) { console.error(WebSocket错误:, error); }; // 5. 监听连接关闭事件 socket.onclose (event) { console.log(连接关闭代码: ${event.code}, 原因: ${event.reason}); // 可以根据关闭码决定是否重连 if (event.code ! 1000) { // 1000是正常关闭 console.log(连接异常断开5秒后尝试重连...); setTimeout(() setupWebSocket(url), 5000); } }; // 发送消息的函数 const sendMessage (payload) { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify(payload)); } else { console.warn(WebSocket未连接消息被丢弃:, payload); // 可以加入消息队列待连接恢复后发送 } }; // 返回关闭和发送方法 return { close: () socket.close(1000, 用户主动关闭), send: sendMessage }; } // 使用示例 const wsManager setupWebSocket(wss://api.example.com/ws); // 发送消息 wsManager.send({ type: chat, content: Hello WebSocket! }); // 关闭连接 // wsManager.close();客户端关键点状态readyStateCONNECTING (0),OPEN (1),CLOSING (2),CLOSED (3)。发送消息前一定要检查状态是否为OPEN。重连策略WebSocket连接可能因网络波动、服务器重启而断开。一个健壮的重连策略如指数退避1秒、2秒、4秒、8秒...是必须的。心跳机制为了防止中间网络设备如代理、防火墙断开空闲连接需要定期如每30秒从客户端向服务器发送一个心跳包ping服务器回应pong。浏览器WebSocket API有ping/pong帧但JavaScript层不能直接发送通常用发送一个特定含义的文本消息来模拟。消息序列化WebSocket只负责传输消息格式需要自己定义。JSON是最常见的但对于高频小消息可以考虑更高效的序列化方式如MessagePack或Protobuf。4.3 服务端实现与连接管理挑战服务端实现WebSocket比HTTP复杂得多因为它要管理大量的持久连接。以Node.js的ws库为例const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); // 存储所有连接的客户端 const clients new Set(); wss.on(connection, (ws, request) { console.log(新的客户端连接); clients.add(ws); // 你可以从请求头或URL中解析用户信息 // const userId getUserIdFromRequest(request); // ws.userId userId; ws.on(message, (message) { console.log(收到客户端消息:, message.toString()); // 处理消息例如广播给其他客户端 clients.forEach(client { if (client ! ws client.readyState WebSocket.OPEN) { client.send(有人说了: ${message}); } }); }); ws.on(close, () { console.log(客户端断开连接); clients.delete(ws); // 清理与该客户端相关的资源 }); ws.on(error, (error) { console.error(WebSocket错误:, error); }); // 发送欢迎消息 ws.send(JSON.stringify({ type: welcome, message: 连接成功 })); });服务端核心挑战连接状态管理你需要知道哪些用户在线他们的连接对象是什么。这通常需要借助Redis等外部存储来在多个服务器实例间共享连接状态。广播与群发如何高效地将消息发给特定房间、群组或所有人需要设计良好的数据结构如MaproomId, Setws来管理订阅关系。横向扩展单台服务器有连接数上限。要支持百万连接必须使用多台服务器。这就引入了新问题如何将消息路由到正确服务器上的正确连接通常需要引入消息队列如Redis Pub/Sub, Kafka和连接网关。安全与认证WebSocket握手阶段是HTTP可以在此进行认证如校验Token。连接建立后也需要防止未授权的消息注入。资源清理连接断开后必须确保从内存中的客户端集合、订阅关系等数据结构中移除防止内存泄漏。踩坑实录我曾负责一个在线客服系统初期用单Node.js实例当在线客服和用户数达到几千时内存飙升广播消息延迟明显。后来我们引入了Redis来存储连接映射关系和发布广播消息。每个Node实例只管理自己的连接当需要跨实例广播时就将消息发布到Redis的特定频道其他实例订阅该频道并转发给自己管理的相关连接。这个架构升级的关键教训是WebSocket应用一旦需要扩展你的挑战就从“处理连接”变成了“管理状态和路由消息”。5. 三种方案的综合对比与选型指南光知道怎么用还不够关键是知道什么时候该用哪个。下面这个表格从多个维度进行了对比特性短轮询长轮询 / SSEWebSocket通信方向客户端单向请求主要是服务器向客户端推送 (SSE是单向)全双工双向实时通信协议基础HTTPHTTP (长轮询)/ HTTP (SSE)独立的WebSocket协议(基于TCP)实时性差 (延迟轮询间隔)好 (数据产生即推送)极好(毫秒级延迟)服务器开销极高(大量无效请求)中等 (持久连接占资源)低 (一个连接双向通信)客户端开销低 (简单)低 (SSE原生简单)中等 (需管理连接、心跳、重连)数据格式任意 (HTTP支持均可)文本 (SSE标准为文本)文本与二进制浏览器兼容完美好 (IE需polyfill for SSE)好 (IE10)典型场景不频繁的更新检查、兼容性要求极高实时通知、股票行情、新闻推送、监控数据流在线聊天、协同编辑、多人在线游戏、实时交易系统5.1 决策流程图我到底该选哪个面对一个实时数据需求你可以遵循以下思路决策是否需要客户端主动、频繁地向服务器发送数据是- 基本锁定WebSocket。例如聊天发消息、游戏发送操作指令。否- 进入第2步。数据更新是否非常频繁秒级或更短且要求延迟极低是- 选择WebSocket或SSE。如果只是服务器向客户端推送SSE更简单。例如实时GPS位置追踪、高频股票报价。否- 进入第3步。数据更新频率中等几秒到几分钟且连接主要是服务器向客户端推送是-SSE是最佳选择。它实现简单自动重连资源消耗低于WebSocket。例如新邮件提醒、后台任务进度更新、体育比赛比分。否- 进入第4步。数据更新不频繁几分钟以上或对实时性要求极低或需要兼容极度老旧的客户端是- 可以考虑短轮询。但务必设置合理的、尽量长的轮询间隔并在客户端做好防抖和渲染优化。一个简单的口诀“你问我答定时刷新”- 短轮询迫不得已时用。“你有消息就喊我我只听不说”- SSE推送通知、数据流首选。“我们随时畅聊你来我往”- WebSocket交互式实时应用核心。5.2 混合使用与降级策略在实际复杂项目中经常需要混合使用这些技术。主通道控制通道一个在线文档编辑器可能用WebSocket传输实时光标位置和文本增量高频、双向而用SSE或普通HTTP来接收系统公告或文档历史版本信息低频、单向。降级策略为了兼容性你的应用可以优先尝试建立WebSocket连接如果失败比如在严苛的企业防火墙后则自动降级到SSE再失败则降级到长轮询。许多成熟的库如Socket.IO内部就实现了这种优雅降级机制。6. 进阶考量生产环境必须面对的坑掌握了基本用法只是走出了第一步。要把实时功能稳定地上到生产环境以下几个问题你必须心中有数。6.1 连接稳定性与重连设计网络是不稳定的。移动端切换WiFi/4G、服务器滚动更新、负载均衡器超时都会导致连接中断。心跳保活即使没有业务数据也要定期如25-30秒通过连接发送“心跳”包告诉中间网络设备这个连接是活跃的。对于WebSocket可以用ping/pong帧服务端发起或发送特定消息模拟。智能重连连接断开后不要立即重连。应采用“指数退避”策略第一次断开等1秒重连第二次等2秒第三次等4秒……直到一个最大值如30秒。这可以避免服务器故障恢复瞬间被海量重连请求击垮。连接状态同步重连成功后客户端需要告诉服务器“我回来了我的最后一条消息ID是XXX”以便服务器同步丢失期间的消息。SSE的last-event-id头和WebSocket的自定义握手协议可以用于此目的。6.2 消息的可靠性与顺序性TCP保证数据包不丢失、不重复、按序到达但这是在传输层。在你的应用层依然可能因为网络断开、程序崩溃导致消息丢失。消息确认ACK机制对于关键消息如支付成功通知服务器发送后需要等待客户端的确认回执。如果超时未收到则重发。这需要为消息分配唯一ID。序列号与去重为消息附加递增的序列号客户端可以判断消息是否连续并对重复消息因重传导致进行去重。离线消息存储对于聊天应用用户离线时的消息需要存储在服务器如Redis或数据库待其重连后按序推送。6.3 安全与认证一个开放的WebSocket或SSE端点是非常危险的。握手阶段认证在WebSocket的HTTP Upgrade请求或SSE的初始请求中必须携带身份认证信息如JWT Token、Cookie。服务器在建立连接前进行验证失败则返回HTTP 401。消息级授权即使连接建立每次处理客户端发来的消息时也要验证该客户端是否有权限执行此操作例如是否在发送消息的聊天室内。防止DoS限制单个IP的连接数、设置合理的消息速率限制rate limiting防止恶意客户端耗尽服务器资源。WSS与HTTPS生产环境务必使用wss://和https://对传输内容进行加密防止中间人攻击和窃听。6.4 性能与扩展性当用户量上来后单机瓶颈很快就会出现。连接分布使用负载均衡器如Nginx将WebSocket连接分散到多个后端服务器实例。Nginx需要配置Upgrade和Connection头部的转发。会话共享用户可能连接到任意一个后端实例。你需要一个中心化的存储如Redis来记录“用户A在哪个服务器实例上”以便实现跨实例的消息路由。发布/订阅模式这是扩展的关键。每个服务器实例订阅一个全局的消息总线如Redis Pub/Sub, Kafka。当实例A需要发送消息给在实例B上的用户时它只需将消息发布到总线实例B收到后转发给本地连接。这样服务器实例之间就解耦了。7. 实战从零搭建一个简单的实时应用理论说再多不如动手做一遍。我们用一个超简化的“简易股票价格看板”来串联SSE和WebSocket的使用。7.1 场景定义与架构功能一个页面显示几只虚拟股票的价格价格每秒随机波动。所有连接的客户端看到的价格实时同步。架构后端Node.js Express。提供一个SSE端点/sse推送股票价格模拟广播。同时提供一个WebSocket服务器/ws允许客户端发送“订阅/取消订阅某只股票”的指令。前端一个HTML页面通过SSE接收所有股票价格更新并通过WebSocket发送控制指令。7.2 后端代码实现// server.js const express require(express); const WebSocket require(ws); const path require(path); const app express(); const PORT 3000; // 静态文件服务 app.use(express.static(path.join(__dirname, public))); // 模拟股票数据 let stocks { AAPL: { name: Apple, price: 175.50 }, GOOGL: { name: Alphabet, price: 145.80 }, MSFT: { name: Microsoft, price: 330.20 } }; // 1. SSE 端点 - 广播所有股票数据 app.get(/sse, (req, res) { res.writeHead(200, { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, Access-Control-Allow-Origin: * }); // 每秒推送一次 const intervalId setInterval(() { // 模拟价格波动 for (const symbol in stocks) { stocks[symbol].price (Math.random() - 0.5) * 2; // 随机涨跌 stocks[symbol].price Math.max(stocks[symbol].price, 0.01); // 保持为正数 } const data JSON.stringify({ type: update, data: stocks, timestamp: Date.now() }); res.write(data: ${data}\n\n); }, 1000); req.on(close, () { clearInterval(intervalId); console.log(SSE客户端断开); }); }); // 2. WebSocket 服务器 const wss new WebSocket.Server({ noServer: true }); const subscribedClients new Map(); // symbol - Set of WebSocket connections wss.on(connection, (ws) { console.log(WebSocket客户端连接); ws.subscriptions new Set(); // 记录此客户端订阅的股票代码 ws.on(message, (message) { try { const { action, symbol } JSON.parse(message); if (action subscribe symbol) { if (!ws.subscriptions.has(symbol)) { ws.subscriptions.add(symbol); if (!subscribedClients.has(symbol)) { subscribedClients.set(symbol, new Set()); } subscribedClients.get(symbol).add(ws); console.log(客户端订阅了 ${symbol}); ws.send(JSON.stringify({ type: info, message: 已订阅 ${symbol} })); } } else if (action unsubscribe symbol) { if (ws.subscriptions.has(symbol)) { ws.subscriptions.delete(symbol); const clients subscribedClients.get(symbol); if (clients) { clients.delete(ws); if (clients.size 0) { subscribedClients.delete(symbol); } } console.log(客户端取消订阅 ${symbol}); ws.send(JSON.stringify({ type: info, message: 已取消订阅 ${symbol} })); } } } catch (e) { console.error(解析WebSocket消息失败:, e); } }); ws.on(close, () { // 清理该客户端的订阅 for (const symbol of ws.subscriptions) { const clients subscribedClients.get(symbol); if (clients) { clients.delete(ws); if (clients.size 0) { subscribedClients.delete(symbol); } } } console.log(WebSocket客户端断开); }); }); // 将Express HTTP服务器与WebSocket服务器关联 const server app.listen(PORT, () { console.log(服务器运行在 http://localhost:${PORT}); }); server.on(upgrade, (request, socket, head) { // 将所有 /ws 路径的HTTP升级请求交给WebSocket服务器处理 if (request.url /ws) { wss.handleUpgrade(request, socket, head, (ws) { wss.emit(connection, ws, request); }); } else { socket.destroy(); } });7.3 前端代码实现!-- public/index.html -- !DOCTYPE html html head title简易股票看板/title style table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } .up { color: green; } .down { color: red; } button { margin: 5px; } /style /head body h1实时股票价格看板/h1 div label订阅股票代码: /label input idsymbolInput typetext placeholder例如: AAPL / button onclicksubscribe()订阅/button button onclickunsubscribe()取消订阅/button p idwsStatusWebSocket状态: 未连接/p /div table idstockTable theadtrth代码/thth名称/thth价格/thth趋势/th/tr/thead tbody/tbody /table script const stockTableBody document.querySelector(#stockTable tbody); const wsStatusEl document.getElementById(wsStatus); let ws; let lastPrices {}; // 1. 建立SSE连接接收广播数据 const eventSource new EventSource(/sse); eventSource.onmessage (event) { const { type, data: stocks } JSON.parse(event.data); if (type update) { updateStockTable(stocks); } }; eventSource.onerror (err) { console.error(SSE错误:, err); }; // 2. 建立WebSocket连接用于发送控制指令 function connectWebSocket() { ws new WebSocket(ws://${window.location.host}/ws); ws.onopen () { wsStatusEl.textContent WebSocket状态: 已连接; console.log(WebSocket已连接); }; ws.onmessage (event) { const msg JSON.parse(event.data); console.log(收到WebSocket控制消息:, msg); alert(服务器消息: ${msg.message}); }; ws.onclose () { wsStatusEl.textContent WebSocket状态: 已断开5秒后重连...; console.log(WebSocket断开); setTimeout(connectWebSocket, 5000); }; ws.onerror (error) { console.error(WebSocket错误:, error); }; } connectWebSocket(); // 更新表格的函数 function updateStockTable(stocks) { for (const [symbol, info] of Object.entries(stocks)) { let row document.getElementById(row-${symbol}); if (!row) { row document.createElement(tr); row.id row-${symbol}; row.innerHTML td${symbol}/td td${info.name}/td td classprice${info.price.toFixed(2)}/td td classtrend-/td ; stockTableBody.appendChild(row); } else { const priceCell row.querySelector(.price); const trendCell row.querySelector(.trend); const oldPrice lastPrices[symbol] || info.price; const newPrice info.price; priceCell.textContent newPrice.toFixed(2); if (newPrice oldPrice) { trendCell.textContent ↑; trendCell.className trend up; } else if (newPrice oldPrice) { trendCell.textContent ↓; trendCell.className trend down; } else { trendCell.textContent -; trendCell.className trend; } } lastPrices[symbol] info.price; } } // 通过WebSocket发送订阅/取消订阅指令 function subscribe() { const symbol document.getElementById(symbolInput).value.trim().toUpperCase(); if (symbol ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ action: subscribe, symbol })); } } function unsubscribe() { const symbol document.getElementById(symbolInput).value.trim().toUpperCase(); if (symbol ws ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ action: unsubscribe, symbol })); } } /script /body /html7.4 运行与观察将后端代码保存为server.js前端HTML保存为public/index.html。在终端运行node server.js。打开浏览器访问http://localhost:3000。你会看到股票价格每秒更新。打开多个浏览器标签所有标签的价格变化是同步的因为SSE广播。在输入框输入TSLA一个模拟代码点击“订阅”。后端会收到WebSocket指令并在控制台打印日志。这里只是一个演示实际订阅逻辑可以更复杂例如只向订阅了某股票的客户端推送该股票的详细变化。这个例子虽然简单但它清晰地展示了两种技术的分工SSE负责高效、单向的广播数据流WebSocket负责双向的控制指令交互。在实际大型应用中这种混合架构非常普遍。
返回列表