☰
WebSocket实战:用原生JS+Node.js从零搭建实时聊天室
2026/10/19 3:18:55 网站建设 项目流程

上一次聊 WebSocket 的时候,我们还在理论层打转——握手怎么完成、连接状态怎么切换、帧格式里掩码和长度字段是怎么回事。这次换个务实角度,直接把它写出来跑一遍。还是那个最经典的需求:一个浏览器页面,一个服务端,用户打开页面就能进聊天室,别人发消息我立刻能看到,不用刷新页面,也不用几秒钟轮询一次。适合刚看完理论想动手的人,也适合总觉得“好像懂了但一写就卡壳”的同学。第二篇的核心不是堆功能,而是把一条 WebSocket 链路完整走通:服务端负责接入和广播,H5 页面负责建立连接和渲染消息,中间再夹着几个不踩不舒服的坑。

先说结论:整个项目没有用到任何框架。前端是原生 HTML + CSS + JavaScript,服务端用 Node.js 加一个ws库。之所以这么选,是因为 WebSocket 本身就是浏览器和服务器之间一条保活的双向通道,框架在这个场景里反而会掩盖底层细节——连接什么时候真正建立、消息以什么格式到达、断线了是什么表现,这些只有用原生代码才能看得清楚。等这条链路跑通了,再去套 Vue、React 或者 Socket.IO 也就顺理成章,因为底层原理已经在手里了。

1. 第二篇的破题思路:不走全栈框架,就抓一条双向通道

很多教程一上来就给你一个聊天室项目,前后端几十个文件,看起来功能很多,但跑起来之后你根本不知道哪里是核心。我这里刻意做减法。要实现的目标就三条:

  1. 多个浏览器标签页同时打开,任意一个页面发送的消息,其他页面都能收到;
  2. 页面顶部显示当前在线人数,有人进入或退出时这个数字要自动变化;
  3. 断线之后页面要有明确提示,不能静默失败,也不能让用户一直对着一个假聊天界面。

这三件事听着简单,但它们恰好覆盖了 WebSocket 的四个关键能力:建立连接(握手)、双向消息传递、服务端主动推送、连接状态感知。浏览器端的new WebSocket()只负责把通道建起来,剩下的事情——怎么解析消息、怎么区分用户、怎么处理服务端主动下发的系统通知——全都要靠我们在代码里自己约定。

1.1 为什么不用 Socket.IO 这类封装库

ws库只是实现了 WebSocket 协议的服务器端细节,本质上和浏览器内置的 WebSocket API 是对应的:浏览器发ws://握手请求,服务端返回 101 状态码,然后这条 TCP 连接就升级成了 WebSocket 连接。ws库帮你处理了协议层的握手、帧收发和底层 socket 管理,但业务逻辑——比如消息怎么广播、连接断掉之后集合怎么清理——还得自己写。

不用 Socket.IO 的原因更直接:Socket.IO 在最上层封装了自动重连、事件广播、房间管理,这些东西确实好用,但它自己的握手机制和协议格式跟原生 WebSocket 完全不同。如果你用 Socket.IO 练手,学到的其实是 Socket.IO 的 API,不是 WebSocket 本身。我这篇想让你看到的,是浏览器ws://地址背后的原生链路。等这条链路理解透了,再回头看 Socket.IO 也就一层窗户纸。

1.2 项目结构和验收标准

项目只有两个文件,硬性要求是它们能在同一个目录下直接跑起来:

chat-demo/ ├── server.js // WebSocket 服务端 └── index.html // 浏览器端页面

验收标准很简单:终端执行node server.js启动服务,然后用浏览器打开index.html,开三个标签页,在任意一个输入框发一句话,另外两个页面要在 1 秒之内显示出来。如果有人关掉了一个标签页,剩下的页面顶部在线人数要自动减一。就这么简单。能跑通这三条,WebSocket 的日常核心用法就已经拿下了。

2. 先搭服务端:连接管理、广播推送与断线清理

WebSocket 服务端是整个链路的数据中枢。所有客户端都直连这个服务端,消息发送方向是“A 客户端 → 服务端 → 所有其他客户端”。如果服务端不维护一份“当前有哪些连接还活着”的名单,广播就无从谈起。

2.1 服务端要做的事,拆开就三件

第一件事:维护连接集合。每来一个客户端,WebSocketServer 就会触发一次connection事件,我们把这个连接的实例存进一个Set。用Set而不是数组,是因为删除操作方便——客户端断开时,直接从集合里移除对应连接即可。

第二件事:消息广播。收到某个客户端的消息,要把这条消息转发给集合里的所有其他客户端。这里有两个细节容易翻车:一是不能原样转发二进制帧就完事,得在业务层定义一个 JSON 结构,把消息类型、发送者、内容、时间都装进去;二是遍历集合时,每个连接在发送前都要判断readyState,因为集合里可能有已经断开但还没来得及清理的僵尸连接。

第三件事:断线感知与动态上下线通知。浏览器标签页被关掉,连接会触发close事件。在这个事件里把连接从集合中移除,同时广播一条“xxx 离开聊天室”的系统消息和最新在线人数。

2.2 服务端代码:基于 ws 库的完整实现

// server.js const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 8080 }); // 用一个 Set 保存所有存活连接 const clients = new Set(); wss.on('connection', (ws, request) => { // 给连接打一个临时编号,方便日志排查 const id = `${request.socket.remoteAddress}:${Date.now()}`; ws.id = id; clients.add(ws); // 1. 新用户进入,给当前连接回一条欢迎消息 ws.send(JSON.stringify({ type: 'system', content: '欢迎进入聊天室', time: Date.now() })); // 2. 广播给所有其他客户端:有人进来了 + 最新人数 broadcast({ type: 'system', content: `新用户加入,当前在线 ${clients.size} 人`, time: Date.now() }, ws); // 3. 处理收到的消息 ws.on('message', (data) => { let parsed; try { parsed = JSON.parse(data); } catch (e) { // 非 JSON 数据直接丢弃,不能让脏数据拖垮整条链路 return; } if (parsed.type === 'chat') { broadcast({ type: 'chat', name: parsed.name || '匿名用户', content: parsed.content, time: Date.now() }, null); } }); // 4. 连接断开时,从 Set 里移除并广播离开消息 ws.on('close', () => { clients.delete(ws); broadcast({ type: 'system', content: `有用户离开,当前在线 ${clients.size} 人`, time: Date.now() }, null); }); // 5. 异常兜底:出现 error 时打印日志,避免进程直接挂掉 ws.on('error', (err) => { console.error('连接异常:', err.message); }); }); function broadcast(data, excludeWs) { const payload = JSON.stringify(data); for (const client of clients) { // 排除消息来源,不需要把用户自己的消息转一圈再发回来 if (client === excludeWs) continue; // 只给就绪状态的连接发消息 if (client.readyState === client.OPEN) { client.send(payload); } } } console.log('WebSocket 服务已启动,监听端口 8080');

2.3 启动验证:用浏览器直接看服务端是不是活的

终端里运行node server.js,看到“WebSocket 服务已启动,监听端口 8080”后,服务端就已经在跑。验证服务端有没有起来,可以打开 Chrome 开发者工具的 Console,直接执行一段前端代码去连接:

const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => console.log('连接成功'); ws.onmessage = (e) => console.log('收到消息:', e.data);

如果能打印出“连接成功”和那条“欢迎进入聊天室”的 JSON,说明握手正常、服务端消息下发也正常。这一步通过了,再往下写页面才不会两眼一抹黑。

2.4 服务端端口和监听地址的细节

端口我选了 8080,没有特殊原因,避开常见端口而已。需要注意的是,new WebSocketServer({ port: 8080 })默认监听在所有网络接口上,如果你只想本机调试,建议改成host: '127.0.0.1'。还有一个小坑:如果前端页面用http://打开,服务端连接地址写成ws://是没问题的;但如果你把前端页面部署到了 HTTPS 站点,WebSocket 地址就必须是wss://,否则浏览器会直接拦截混合内容。这个后面踩坑部分还会展开。

3. H5 页面:从连接建立到消息渲染的完整实现

服务端通了,接下来是前端的活。这里标题里写的“H5+CSS+JS”就体现出来了:一个 HTML 文件,里面塞了页面结构和样式,再写一段原生 JavaScript 管理 WebSocket 生命周期。

3.1 页面骨架和样式

页面结构不复杂:一个头部栏显示标题和在线人数,中间是消息滚动区域,底部是输入框加发送按钮。CSS 部分我只做两件关键事:让消息区域可以滚动,以及区分系统消息和聊天消息的视觉样式。

<!-- index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebSocket 聊天 Demo</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, -apple-system, sans-serif; background: #f5f6fa; } .chat-app { max-width: 720px; margin: 30px auto; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); overflow: hidden; } .chat-header { padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; } .chat-header .title { font-size: 18px; font-weight: 600; color: #2c3e50; } .chat-header .status { font-size: 13px; color: #95a5a6; } .chat-header .status.online { color: #27ae60; } #messages { height: 420px; overflow-y: auto; padding: 16px 20px; background: #fafbfc; } .msg { margin-bottom: 12px; } .msg .meta { font-size: 12px; color: #aaa; margin-bottom: 3px; } .msg .bubble { display: inline-block; padding: 8px 12px; border-radius: 8px; background: #e2e8f0; max-width: 70%; line-height: 1.5; word-break: break-word; } .msg.mine .bubble { background: #3498db; color: #fff; } .msg.system .bubble { background: transparent; color: #7f8c8d; font-size: 13px; text-align: center; width: 100%; } .chat-input { display: flex; gap: 10px; padding: 14px 20px; border-top: 1px solid #eee; } .chat-input input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; outline: none; font-size: 14px; } .chat-input button { padding: 10px 20px; border: none; background: #3498db; color: #fff; border-radius: 8px; cursor: pointer; font-size: 14px; } .chat-input button:hover { background: #2980b9; } </style> </head> <body> <div class="chat-app"> <div class="chat-header"> <span class="title">WebSocket 即时聊天</span> <span id="onlineCount" class="status">连接中...</span> </div> <div id="messages"></div> <div class="chat-input"> <input id="content" type="text" placeholder="输入消息,回车发送"> <button id="sendBtn">发送</button> </div> </div> <script> // 见下方 JavaScript 部分 </script> </body> </html>

3.2 原生 JavaScript:连接生命周期管理

前端 JavaScript 部分拆成四个环节:创建连接、监听事件、解析数据、渲染消息。状态管理用readyState就够了——浏览器 WebSocket 对象自带这个属性,它只有四个状态:CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3)。

const onlineCountEl = document.getElementById('onlineCount'); const messagesEl = document.getElementById('messages'); const contentInput = document.getElementById('content'); const sendBtn = document.getElementById('sendBtn'); let ws = null; let userName = '用户' + Math.floor(Math.random() * 1000); function connect() { // 连接地址要和服务端保持一致 ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { onlineCountEl.textContent = '已连接'; onlineCountEl.classList.add('online'); }; ws.onmessage = (event) => { let data; try { data = JSON.parse(event.data); } catch (e) { // 服务端发来了一段无法解析的文本,这里直接忽略 return; } if (data.type === 'system') { appendMessage({ name: '系统', content: data.content, time: data.time, isSystem: true }); } else if (data.type === 'chat') { appendMessage({ name: data.name, content: data.content, time: data.time, isMine: data.name === userName }); } }; ws.onclose = () => { onlineCountEl.textContent = '连接断开'; onlineCountEl.classList.remove('online'); }; ws.onerror = () => { onlineCountEl.textContent = '连接异常'; appendMessage({ name: '系统', content: '与服务端的连接出现异常,请检查服务端是否启动', isSystem: true }); }; } function appendMessage({ name, content, time = Date.now(), isMine = false, isSystem = false }) { const item = document.createElement('div'); item.className = 'msg' + (isMine ? ' mine' : '') + (isSystem ? ' system' : ''); if (!isSystem) { const meta = document.createElement('div'); meta.className = 'meta'; meta.textContent = `${name} · ${formatTime(time)}`; item.appendChild(meta); } const bubble = document.createElement('div'); bubble.className = 'bubble'; bubble.textContent = content; item.appendChild(bubble); messagesEl.appendChild(item); messagesEl.scrollTop = messagesEl.scrollHeight; } function formatTime(t) { const d = new Date(t); const pad = (n) => String(n).padStart(2, '0'); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function sendMessage() { const content = contentInput.value.trim(); if (!content) return; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'chat', name: userName, content })); contentInput.value = ''; } else { appendMessage({ name: '系统', content: '连接已断开,无法发送消息', isSystem: true }); } } sendBtn.addEventListener('click', sendMessage); contentInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); sendMessage(); } }); // 页面加载即建立连接 connect();

3.3 消息渲染的三个核心细节

第一个细节是isMine标记。用户自己发的消息显示成蓝色靠左,别人的消息显示成灰色靠左。很多人都想做靠左靠右区分,但那个前提是服务端替每条消息标了唯一 ID,前端要靠对比 ID 才能准确判断。这个 Demo 里我用data.name === userName这种弱判断,消息少够用,但坑也很明显——如果两个人起了同一个名字,消息归属就乱了。这个后面讲协议设计时再展开。

第二个细节是messagesEl.scrollTop = messagesEl.scrollHeight。每次渲染完消息都自动把滚动条拉到底,否则新消息会一直在视线之外。这个操作要在appendChild之后执行,因为scrollHeight是拿到渲染完成后真实内容的高度。

第三个细节是WebSocket.OPEN的判断。Mozilla 的标准写法里,可以用WebSocket.OPEN这个常量,也可以用数字1,两者等价。发送前判断连接状态,是为了避免用户在连接没建立好或者已经断开时点击发送按钮,向一个不存在的通道发送数据。

3.4 页面首次连接失败的表现

如果服务端没启动,浏览器拿到ws://localhost:8080会发现端口不通。此时onerror和onclose都会触发,页面上会显示“连接异常”“连接断开”。很多人第一次跑这种项目,看到状态栏一行红字就慌了,其实这是预期内的:WebSocket 连接失败不会让页面崩溃,只会触发错误事件和关闭事件,代码里把这两个事件的 UI 反馈做好就可以了。

4. 踩坑实录:本地调试 WebSocket 最容易翻车的四个细节

这部分算是我实际调试时印象最深的地方。理论上链路很清晰,但真跑起来,问题全出在一些很不起眼的地方。

4.1 用 http 协议去连 ws 地址

最基础也最容易犯的错误,是把前端的连接地址写成http://localhost:8080而不是ws://localhost:8080。浏览器会直接抛一个SyntaxError: Failed to construct 'WebSocket': The URL's scheme must be 'ws' or 'wss'. 这个错误你在 Console 里一眼能看到。解决方式就是前端地址用ws://,服务端监听用端口,两者协议一致即可。

4.2 服务端广播时没判断 readyState

服务端broadcast函数里如果少了client.readyState === 1这个判断,你会间歇性遇到一个诡异问题:某个标签页已经关了,但服务端向它发送消息时明显变慢,甚至进程直接报错。原因很简单:close事件从触发到真正把连接从Set里移出去是有时间窗口的,这个窗口内连接可能已经半死。所以在写广播逻辑时,每个连接发送前先检查readyState,这是服务端代码的基本素养。

下面是这个问题的完整排查链路,值得看一眼:

现象可能的根因排查方法解决办法
消息时灵时不灵僵尸连接还在集合里在broadcast里打印每个client.readyState发送前判断OPEN
其他标签页收不到消息消息体 JSON 解析失败被丢弃服务端打印data原文前端用JSON.stringify严格封装
页面显示连接失败服务端没启动或端口被占用终端运行node server.js看报错检查端口冲突,换 8080
关闭一个标签页,其他人看到人数没减close事件没绑定或集合没删干净服务端日志观察connection与close是否成对出现确认clients.delete(ws)

4.3 连接事件、消息事件的顺序问题

有读者可能觉得,代码里ws.onopen写在new WebSocket()之后,会不会太晚了?实际上不会。new WebSocket()是异步握手,监听器的绑定发生在握手完成之前是来得及的。但这个顺序有一个需要注意的场景:如果服务端特别快,在connection事件里立刻send()了一条欢迎消息,而前端还没来得及绑定onmessage监听,那这条欢迎消息可能丢。理论上 TCP 层会缓存,不会丢,但要做到稳妥,我习惯在onopen里主动向服务端要一次最新状态。

4.4 直接把服务端代码复制到生产环境

这也是新手特别容易踩的坑。本地是ws://localhost:8080,一旦把前端代码部署到https://域名下,控台会报一堆混合内容警告,浏览器会拦截ws://的请求。这个没有捷径,生产环境必须让服务端走 TLS,前端连接地址改成wss://。如果你的服务端前面套了 Nginx,Nginx 里要加proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection "upgrade";,这两行缺失,服务端握手永远过不去,浏览器只会反复重连然后失败。

4.5 用同一个端口跑 HTTP 和 WebSocket 的问题

在这个 Demo 里,index.html是用file://协议直接打开的,没有走 HTTP 服务,所以不存在同源冲突。但如果你想用http://localhost:8080直接打开页面,而 8080 又被 WebSocket 占用了,就会撞端口。解决办法有几种:一是把静态页面丢给一个独立 HTTP 服务跑,WebSocket 换端口;二是用同一个 Node 服务既托管静态页面又挂 WebSocket server。第二种方式写起来也不复杂:

const http = require('http'); const { WebSocketServer } = require('ws'); const server = http.createServer((req, res) => { // 放一个最简单的静态文件响应 res.end('hello'); }); const wss = new WebSocketServer({ server }); server.listen(8080);

这种方式的好处是:同一个握手端口,浏览器的ws://localhost:8080和http://localhost:8080不冲突,因为 WS 升级请求和普通 HTTP 请求在 TCP 层面靠协议头区分。条件允许的话,建议直接采用这种方案,省掉静态服务器和 WS 服务端之间的端口协调工作。

5. 消息协议设计:客户端和服务端要提前约法三章

这个章节看起来不起眼,实际上是整个 Demo 能不能进化成真正聊天室的关卡。前面代码里频繁出现type、name、content、time这些字段,这就是协议。前后端对一条消息的“长相”达成一致,代码才能各干各的活。

5.1 为什么一定要 JSON 结构

WebSocket 原生消息可以是文本也可以是二进制帧。如果客户端直接发一段裸文本大家好,服务端原样广播出去,前端收到的也是大家好,初跑能通,但实现不了任何逻辑——你分不清这是聊天内容、系统通知还是心跳包。定义 JSON 结构的目的,是给消息加上“信封”:type字段决定这条消息干什么用,其他字段决定消息内容是什么。就像快递箱上写着这是“文件”还是“生鲜”,派件的人才知道怎么处理。

5.2 消息类型设计

这个 Demo 里定义了四种type:

type方向用途载荷字段
system服务端 → 客户端上下线通知、欢迎语、系统状态content、time
chat客户端 → 服务端 → 其他客户端聊天消息name、content、time
heartbeat双向保活检测time
userList服务端 → 客户端在线人数/用户列表count、users

system和其它类型的区别在于没有name字段,因为系统消息不是某个用户发的。前端渲染时,看到type === 'system'就走居中灰色小字的分支,看到type === 'chat'就走气泡分支。这个分支判断写得越清晰,后面加新消息类型就越容易。

5.3 心跳保活与断线检测

WebSocket 本身有 TCP 层面的保活,但浏览器端杀掉标签页、笔记本电脑合盖休眠,都可能让连接处于“看似存在实则死了”的状态。这时候服务端往这条连接发消息,TCP 层会超时或者收到 RST,但这个过程有时候长达几十秒甚至几分钟。要缩短这个感知时间,需要心跳机制。

ws库自带一套基于 ping/pong 的心跳示例:服务端每隔 30 秒给所有客户端发 ping,客户端在协议层自动回 pong。服务端用isAlive标记记录每个连接是否在最近一个周期内回应过 pong,没回应的就terminate()掉。这套机制不用改前端代码,因为 ping/pong 是 WebSocket 协议内置的控制帧,浏览器会自动回复。

前端也可以自己做应用层心跳:每隔 20 秒发一条{ type: 'heartbeat', time: Date.now() },服务端收到后回一条 pong。这样做的意义是:当服务端挂了、网络闪断时,前端能在较短周期内感知连接失效,从而触发重连逻辑。注意,前端onclose在纯网络闪断场景下不一定立刻触发,有时候要等很久,所以我一般会挂一个setInterval定时检查readyState,超过 N 秒没有任何消息到达就主动close()再重连。

5.4 服务端广播范围的两种模型

有一种常见误解:WebSocket 天然就是“全房间广播”。其实不是。wss收到的每条消息只有一个来源,往哪里发完全由业务代码说了算。当前的broadcast函数把所有客户端都发一遍,这叫全局广播。

后面如果要做群聊、私聊,有两个扩展方向:

  1. 房间模型:客户端发送{ type: 'join', room: '前端交流' },服务端维护Map<房间名, Set<连接>>,广播时只遍历这个房间的连接集合;
  2. 私聊模型:每条消息加to字段(目标用户 ID),服务端遍历连接集合找到目标用户那条连接,单独send(),其他连接不理会。

从“全局广播”到“房间广播”,代码层面的改动量非常小,但设计上要提前留好type和载荷格式,否则后面改协议时会牵一发动全身。

5.5 JSON 解析的防御性处理

前端onmessage里我写了一段try...catch包住JSON.parse,看起来多余,实则是防止服务端或其他中间层发送非 JSON 数据造成页面报错。WebSocket 的message事件不关心内容是不是合法 JSON,只要是一个完整帧就会触发。如果服务端某个时刻手滑发了一段普通文本,JSON.parse会抛异常,而onmessage里的异常一旦不捕获,整段脚本会中断。后面所有消息都渲染不了,那种故障排查起来非常痛苦。只要是从网络上来的数据,解析前一律防御性包裹。

6. 实测一轮:二十个连接同时在线,服务端表现如何

跑通基本链路之后,我顺手做了个压力测试。也不太严格,就是开了 20 个标签页同时连这个服务端,然后每页每秒发一条消息,持续五分钟,看服务端进程的内存变化和消息延迟表现。

6.1 测试数据与观察结果

指标结果
最大在线连接数20(标签页多开)
每 5 秒消息总量约 100 条
消息平均延迟小于 50ms(本地回环网络)
服务端 Node 进程内存从约 35MB 涨到约 58MB,趋于平稳
断线标签页数3 个(手动关闭),在线人数正确递减

这个结果说明,在消息量不大的场景下,一套原生 WebSocket 服务完全没有性能压力。瓶颈大概率会出现在两个地方:一是单条消息广播时遍历的集合规模,二是消息体 JSON 序列化和反序列化消耗的 CPU。对于日常小应用,几千个连接以内,这套结构都是够用的。

6.2 断线重连:把“用户自动掉线”变成“悄然恢复”

测试里我故意关掉 Wi-Fi 再打开,然后观察页面表现。很遗憾,页面在断网期间没有任何反应——onclose没有触发,readyState一直停留在OPEN。这就是前文说过的“死连接”问题。加一段自动重连逻辑才是更完整的方案:

let retryCount = 0; function connect() { ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { retryCount = 0; onlineCountEl.textContent = '已连接'; }; ws.onclose = () => { onlineCountEl.textContent = '连接断开,重试中...'; const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); retryCount++; setTimeout(connect, delay); }; }

这里用了指数退避策略:第一次断开 1 秒后重连,第二次 2 秒,第三次 4 秒,封顶 10 秒。如果不封顶,退避时间会越来越长,用户等很久都回不来。这个策略看起来简单,但它能避免服务端短暂重启时,几十个客户端同时猛冲上来造成连接风暴——错峰重连要好得多。

6.3 云服务器上跑的额外注意点

如果这个 Demo 要丢到云服务器上跑,要注意云平台的安全组/防火墙是否放行了 8080 端口。很多人本地跑得好好的,一上服务器别人就连不上,八成是安全组只开了 22 和 80,没有开 WebSocket 用的端口。有些人图省事直接关防火墙,这个做法我不太推荐——开最小权限端口就好。

6.4 这个 Demo 后续可以怎么扩展

代码写到这里,已经具备了聊天的核心链路。再往前走,有几个很自然的扩展方向:

  1. 用户身份:聊天消息里加一个随机昵称,这个我现在已经做了,但更完整的是做一个简单的登录页,把昵称和头像颜色存到localStorage;
  2. 历史消息补发:服务端用一个数组暂存最近 100 条聊天记录,新用户连接后先把历史记录发给他;
  3. 消息已读回执:消息体里加msgId,接受方收到后回一条 ACK,发送方可以做“已送达”标记;
  4. 文件/图片发送:二进制帧可以把图片 base64 编码后放进 JSON 编码传输,或者把content扩展成一个包含 MIME 类型的结构。

我在实际测试这套代码时,最大的感触是:WebSocket 本身不难,难的是把“连接”当成一个需要全程呵护的对象来对待——什么时候建立,什么时候销毁,什么时候重试,什么时候广播,每一步都得有明确逻辑。把这些边界条件处理干净,代码自然就稳了。下一篇文章如果继续写,我打算顺着“房间+私聊”这个方向做一版更接近真实产品的聊天室,把现在的一些弱判断换成更严谨的身份系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询