news 2026/10/17 14:17:07

Cocos Creator 手搓 WebSocket 实时聊天:Node.js 服务端与客户端完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cocos Creator 手搓 WebSocket 实时聊天:Node.js 服务端与客户端完整实现

1. 为什么要在 Cocos Creator 里手搓一个 WebSocket 聊天

先把场景说清楚。你手上有一个 Cocos Creator 做的项目,可能是个小游戏、一个互动房间、或者一个带社交属性的轻应用,现在产品那边提了个需求:玩家之间要能实时说话。不是那种发一条刷新一次的留言板,而是消息发出去对面几乎同时就能看到。这时候你面前通常有三条路:轮询、长连接、或者直接用第三方即时通讯服务。

轮询最省事,但延迟高、请求量大,玩家发一句话要等一两秒才显示,体验很差。第三方服务接入快,但要么收费、要么有额外依赖、要么数据不在自己手里。所以大多数中小项目最后都会落到 WebSocket 上——它是浏览器和 Node.js 都原生支持的全双工通信协议,握手一次之后连接一直保持,双方随时可以推消息,延迟基本就是网络往返的时间。

这篇内容就是围绕“在 Cocos Creator 里用 WebSocket 做一个能跑起来的在线聊天”展开的。我会把服务端和客户端两边都写出来,服务端用 Node.js 加ws这个库,客户端用 Cocos Creator 的 TypeScript 脚本。整套东西不依赖任何第三方云服务,你自己一台能跑 Node 的机器就能验证。适合谁看?适合已经会用 Cocos Creator 做基本场景和脚本、但对网络通信还比较模糊的开发者;也适合想快速搭一个联机原型、验证玩法的人。读完你至少能拿到一套可以直接抄的代码骨架,知道每一步为什么这么写,以及哪些坑我替你踩过了。

需要提前说明的是,下面所有代码都是基于常见实践补全的完整实现,不是从某个现成项目里扒出来的,你可以直接拿去改。服务端监听地址、端口这些参数我会给出选择理由,不会甩一堆数字让你自己猜。

2. 整体架构与方案选型拆解

2.1 为什么是 WebSocket 而不是 HTTP 短连接

先把这个“为什么”讲透,不然你后面调参数的时候心里没底。

HTTP 是请求-响应模型。客户端不发请求,服务端就没法主动给你东西。聊天这个场景的本质是“别人说话的时候我要立刻知道”,这是典型的服务端主动推送。用 HTTP 硬做,只能让客户端每隔几百毫秒问一次“有新消息吗”,这就是轮询。轮询的问题有三个:一是延迟取决于轮询间隔,间隔短了请求量爆炸,间隔长了消息就慢;二是每次请求都要带一堆头部,有效数据占比很低;三是连接频繁建立断开,服务端压力大。

WebSocket 在握手阶段借用了 HTTP 的升级机制,一旦升级成功,这条 TCP 连接就变成全双工通道。之后双方发的每一帧数据都只有很小的协议开销,没有重复的头部。对于聊天这种“消息小而频繁”的场景,这是最合适的。你可以把它理解成:HTTP 是寄信,每封信都要写完整地址;WebSocket 是打电话,接通之后直接说话。

2.2 服务端为什么选 Node.js 加 ws

服务端语言选择很多,Java、Go、Python 都能做。我这里选 Node.js 加ws库,理由很实际:一是ws是目前 Node 生态里最成熟、最轻量的 WebSocket 实现,装完就能用,不需要额外框架;二是 Node 的事件驱动模型天然适合处理大量并发长连接,一个连接就是一个事件源,代码写起来很直观;三是前后端都用 JavaScript/TypeScript,心智负担小,你调试的时候不用在两套语言思维之间切换。

如果你项目后期要上生产,可以考虑在ws外面套一层更完整的框架,但对“简单在线聊天”这个目标来说,ws足够了,多引入一层反而增加理解成本。

2.3 客户端在 Cocos Creator 里怎么接

Cocos Creator 的脚本运行环境,在 Web 平台下就是浏览器环境,WebSocket是全局对象,直接用就行。在原生平台(比如打包成手机 App),Cocos 也提供了对应的 WebSocket 支持,API 基本一致。所以客户端这边我们不需要引入任何额外库,用引擎自带的WebSocket就行。

这里有个关键决策:消息格式用什么。我强烈建议用 JSON 字符串。原因很简单——可读、好调试、跨语言。你抓包或者打日志的时候,一眼就能看出消息内容。二进制协议虽然省流量,但对一个聊天原型来说,省的那点带宽远不如调试便利重要。等你的消息量大到 JSON 成为瓶颈了,再考虑换二进制也不迟。

2.4 消息协议怎么设计才不容易乱

聊天系统的消息类型会越来越多:登录、发消息、系统通知、用户列表更新、心跳……如果不提前约定好格式,代码很快就会变成一堆 if-else。

我的做法是定一个统一的消息信封,所有消息都长这样:

{ "type": "chat", "data": { "from": "用户A", "content": "大家好" } }

type字段决定这条消息是什么类型,data里放具体内容。服务端和客户端都根据type来分发处理。这样加新消息类型的时候,只需要加一个分支,不会影响已有逻辑。这个设计看起来简单,但它能帮你省掉后面大量的重构时间。

3. 服务端实现细节与关键代码

3.1 环境准备与依赖安装

先建一个空目录,初始化 Node 项目:

mkdir chat-server cd chat-server npm init -y npm install ws

ws装完之后,你的package.json里会多一条依赖。这里不需要装 express 之类的 Web 框架,因为 WebSocket 服务本身不依赖 HTTP 路由。如果你想让服务端同时提供 HTTP 接口(比如健康检查),可以再装,但聊天功能用不上。

提示:Node 版本建议 14 以上,ws的新版本对低版本 Node 支持不好。用node -v确认一下。

3.2 创建 WebSocket 服务并管理连接

服务端的核心任务是三件事:接受连接、维护连接列表、广播消息。先看基础骨架:

const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); // 用一个 Map 保存所有在线连接,key 是连接对象,value 是用户信息 const clients = new Map(); wss.on('connection', (ws) => { console.log('有新连接接入'); ws.on('message', (raw) => { // 处理消息 }); ws.on('close', () => { // 清理连接 }); }); console.log('WebSocket 服务已启动,监听 8080 端口');

端口选 8080 是个习惯问题,你也可以用 3000、9000,只要不和系统其他服务冲突就行。生产环境通常会在前面放一层反向代理,由代理来转发 WebSocket 请求,那时候服务端监听本地端口即可。

为什么用Map而不是数组来存连接?因为断开连接的时候,你需要快速找到并删除对应的记录。数组删除是 O(n),Map 是 O(1)。在线人数一多,这个差别就出来了。而且 Map 可以顺便存用户昵称、加入时间等信息,后面做用户列表很方便。

3.3 消息广播与用户上下线通知

广播的逻辑是遍历所有连接,逐个发送。但要注意,发送之前必须检查连接状态,因为可能有的连接正在关闭:

function broadcast(type, data, excludeWs = null) { const payload = JSON.stringify({ type, data }); for (const [ws, user] of clients) { if (ws === excludeWs) continue; if (ws.readyState === WebSocket.OPEN) { ws.send(payload); } } }

readyState这个检查非常关键。我见过太多新手直接ws.send(),结果连接已经断了,程序直接抛异常。WebSocket.OPEN是常量 1,表示连接正常。只有这个状态下发送才是安全的。

用户上线和下线都要广播,这样所有人的用户列表才能保持同步。上线的时候,先给新用户回一条“欢迎”消息,再广播“某某加入了”;下线的时候,从 Map 里删掉,然后广播“某某离开了”。

3.4 心跳检测:别让死连接占着位置

这是服务端最容易忽略、但上线后最容易出问题的地方。TCP 连接有时候会因为网络波动、客户端异常退出而变成“半开”状态——服务端以为还连着,实际上对面早就没了。这种连接不清理,会越积越多,最后内存爆掉。

解决办法是心跳。服务端定期给每个连接发一个 ping,客户端收到后回 pong。如果某个连接连续几次没回,就判定它死了,主动关掉。

const HEARTBEAT_INTERVAL = 30000; // 30 秒 const heartbeatTimer = setInterval(() => { for (const [ws] of clients) { if (ws.isAlive === false) { ws.terminate(); clients.delete(ws); continue; } ws.isAlive = false; ws.ping(); } }, HEARTBEAT_INTERVAL); wss.on('connection', (ws) => { ws.isAlive = true; ws.on('pong', () => { ws.isAlive = true; }); });

30 秒这个间隔是我实测下来比较稳的值。太短了浪费资源,太长了发现死连接不及时。ws.ping()发的是 WebSocket 协议层面的控制帧,不是业务消息,客户端不需要写代码处理,浏览器和 Cocos 底层会自动回 pong。这一点很多人不知道,以为要自己手动回,其实不用。

注意:ws.terminate()是强制断开,不发送关闭帧;ws.close()是优雅关闭。清理死连接用terminate()更干脆。

4. 客户端在 Cocos Creator 中的完整实现

4.1 建立连接与断线重连

客户端这边,我建议单独写一个网络管理脚本,不要把所有逻辑塞进某个 UI 组件里。这样后面加功能、换界面都不用动网络层。

import { _decorator, Component } from 'cc'; const { ccclass } = _decorator; @ccclass('ChatClient') export class ChatClient extends Component { private ws: WebSocket = null; private serverUrl: string = 'ws://127.0.0.1:8080'; private reconnectTimer: number = 0; private isManualClose: boolean = false; connect() { this.isManualClose = false; this.ws = new WebSocket(this.serverUrl); this.ws.onopen = () => { console.log('连接成功'); this.send('login', { name: '玩家' + Math.floor(Math.random() * 1000) }); }; this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); this.handleMessage(msg); }; this.ws.onclose = () => { console.log('连接断开'); if (!this.isManualClose) { this.scheduleReconnect(); } }; this.ws.onerror = (err) => { console.error('连接出错', err); }; } }

serverUrl这里写的是本机地址,你实际部署的时候换成服务器地址。注意协议是ws://,如果服务端上了 TLS,就换成wss://。

断线重连是必须做的。移动网络下,切后台、进电梯、信号弱,连接断掉太常见了。重连策略我用的是固定间隔加一点随机,避免所有客户端同时重连把服务端打崩:

private scheduleReconnect() { if (this.reconnectTimer) return; const delay = 3000 + Math.random() * 2000; this.reconnectTimer = setTimeout(() => { this.reconnectTimer = 0; this.connect(); }, delay); }

3 到 5 秒的随机延迟,实测能有效错开重连高峰。如果你想要更精细的策略,可以做指数退避,第一次 1 秒,第二次 2 秒,第三次 4 秒,但记得设一个上限,不然断网久了要等很久。

4.2 消息收发与类型分发

发送消息统一走一个方法,把 type 和 data 打包:

send(type: string, data: any) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { console.warn('连接未就绪,消息发送失败'); return; } this.ws.send(JSON.stringify({ type, data })); }

接收消息按 type 分发:

private handleMessage(msg: any) { switch (msg.type) { case 'chat': this.onChatMessage(msg.data); break; case 'userList': this.onUserListUpdate(msg.data); break; case 'system': this.onSystemMessage(msg.data); break; default: console.warn('未知消息类型', msg.type); } }

这个 switch 就是整个客户端的消息中枢。以后加新类型,在这里加一个 case 就行。我习惯把每个 case 的处理逻辑单独写成方法,这样 switch 本身保持清爽,一眼能看全所有消息类型。

4.3 把消息接到 UI 上

网络层写好了,接下来是界面。假设你场景里有一个ScrollView用来显示消息列表,一个EditBox输入内容,一个Button发送。

消息列表的更新要注意一点:不要每来一条消息就重建整个列表,那样消息一多会卡。正确做法是往列表里追加一个节点。如果你用的是ScrollView加Layout,追加完记得让滚动条自动滚到底部:

private appendMessage(text: string) { const item = instantiate(this.msgItemPrefab); item.getComponent(Label).string = text; this.contentNode.addChild(item); // 滚动到底部 this.scrollView.scrollToBottom(0.1); }

scrollToBottom的第二个参数是滚动时间,给个 0.1 秒有个平滑效果,比瞬间跳过去体验好。这个细节很多人不注意,但玩家能感觉到。

发送按钮的回调里,先做空内容校验,再调用send,最后清空输入框:

onSendClick() { const text = this.editBox.string.trim(); if (!text) return; this.send('chat', { content: text }); this.editBox.string = ''; }

提示:EditBox在移动端弹出键盘时会顶起界面,记得处理一下输入框的位置,不然键盘会挡住输入内容。这是移动端聊天界面的经典坑。

5. 实操中踩过的坑与排查技巧

5.1 连接建立失败怎么查

最常见的原因是地址写错或者服务端没起来。排查顺序是这样的:先在浏览器里开一个空白页,按 F12 打开控制台,手动执行new WebSocket('ws://127.0.0.1:8080'),看能不能连上。如果这里就报错,说明是服务端问题,去检查服务端进程是否在跑、端口是否被占用。

如果浏览器能连上,但 Cocos 里连不上,那多半是打包平台的问题。原生平台对ws://明文连接有安全限制,某些系统版本会直接拒绝。解决办法是服务端上 TLS,客户端用wss://。这个坑我在原生打包时踩过,Web 平台好好的,一打包到手机就连不上,查了半天才发现是明文连接被拦了。

5.2 消息发出去了对面收不到

先确认服务端的广播逻辑有没有把发送者自己排除掉。我上面给的broadcast有个excludeWs参数,如果你传了发送者自己,那发送者就看不到自己发的消息。聊天场景通常需要发送者也看到自己的消息(显示在右侧),所以广播时不要排除自己,或者客户端本地先追加一条。

另一个可能是消息格式不对。服务端JSON.parse失败会抛异常,如果你没做 try-catch,整个消息处理就中断了。建议在message事件里包一层:

ws.on('message', (raw) => { let msg; try { msg = JSON.parse(raw); } catch (e) { console.error('消息格式错误', raw); return; } // 正常处理 });

5.3 常见问题速查表

现象可能原因排查方向
连接一直转圈连不上地址错误或服务端未启动浏览器控制台手动测试连接
连上后立刻断开服务端异常或协议不匹配看服务端日志有无报错
消息延迟很高网络问题或服务端阻塞检查服务端是否有同步耗时操作
部分用户收不到消息连接状态未检查确认发送前判断 readyState
长时间挂机后失联缺少心跳机制补上 ping/pong 心跳
移动端切后台断连系统回收连接监听 onclose 做重连

5.4 几个提升稳定性的实操心得

第一,服务端所有对连接的遍历操作,都要考虑“遍历过程中连接被删除”的情况。JavaScript 的 Map 在遍历时删除元素是安全的,但如果你用的是数组,边遍历边 splice 就会漏掉元素。这是很多人写广播时容易犯的错。

第二,客户端重连成功后,要重新发送登录消息,让服务端重新登记用户信息。否则服务端那边这个连接是“匿名”的,用户列表会乱。我一般把登录逻辑放在onopen里,这样每次重连都会自动重新登录。

第三,消息内容要做长度限制。不加限制的话,有人发一条几兆的文本,服务端广播出去能把所有客户端卡死。服务端收到消息后先判断长度,超过阈值就丢弃或者截断。这个阈值我一般设 500 个字符,聊天场景足够了。

第四,开发阶段把收发的原始消息都打上日志,方便对照。上线前再把日志级别调高,避免影响性能。这个习惯能帮你省下大量调试时间。

6. 后续可以怎么扩展

这套骨架跑通之后,往上加东西就很顺了。想支持私聊,就在消息里加一个to字段,服务端根据to找到对应连接单独发送,而不是广播。想支持聊天房间,就在连接上挂一个roomId,广播时只发给同房间的连接。想存历史消息,就在服务端接一个轻量数据库,用户登录时把最近若干条推给他。

我个人在实际操作中的体会是,聊天功能真正的难点不在“怎么把消息发出去”,而在“怎么在各种异常情况下保持连接可用”。网络抖动、切后台、服务端重启,这些才是吃掉你大部分调试时间的地方。所以一开始就把重连、心跳、状态检查这些基础设施做扎实,后面加功能会轻松很多。反过来,如果一开始图省事跳过这些,等用户量上来再补,改动量会大得多。

最后分享一个小技巧:调试的时候,可以同时开两个浏览器标签页,各自连同一个服务端,互相发消息验证。这比在 Cocos 里反复打包测试快得多,因为浏览器改代码刷新就行,不用等引擎编译。等逻辑验证通了,再回到 Cocos 里接 UI,效率能翻倍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 14:16:26

Vibe Coding两天实测:用自然语言与Google AI Studio从零搭建小工具

最近我花了整整两天时间,把自己按进一个完全陌生的开发流程里:不直接写代码,全程靠对话,用自然语言把一个还算完整的小工具从零搭出来。工具用的是 Google AI Studio,玩法就是圈子里最近特别火的 Vibe Coding。两天下来…

作者头像 李华
网站建设 2026/10/17 14:16:17

大模型决策支持系统落地:从架构设计到防幻觉实战

1. 从“智能助手”到“决策伙伴”:语言模型在决策支持系统中的真实定位在给某制造业客户做数据中台改造的时候,我第一次认真思考LLM在决策支持系统里的角色。当时客户的业务负责人提了一个很有意思的需求:他们不缺数据,不缺报表&a…

作者头像 李华
网站建设 2026/10/17 14:16:13

电力杆塔智能巡检:无人机缺陷识别数据集与模型训练全复盘

远看一根好好的铁塔,无人机飞近绕到背面才发现绝缘子串上挂着一个鸟巢,再拉近一点,塔材的锈蚀和销钉缺失都藏在阴影和线缆交错里。这类场景我盯过太多次,真正的问题从来不是“看不看得到”,而是“人能不能在几千张图里…

作者头像 李华
网站建设 2026/10/17 14:16:06

STM32F103 CAN Bootloader量产级固件升级方案

简介:本资源是面向嵌入式开发工程师与STM32进阶学习者的CAN总线Bootloader实战项目,聚焦STM32F103系列MCU在工业通信场景下的固件在线升级能力构建。资源提供完整可编译的CAN Bootloader源码工程,涵盖CAN驱动配置、帧收发协议、Flash编程、CR…

作者头像 李华
网站建设 2026/10/17 14:15:53

VS2017下CLAPACK编译与集成实战:从源码到矩阵求逆

1. 为什么要在VS2017里折腾CLAPACK如果你正在用C或C做数值计算、信号处理、控制系统仿真,或者搞一些跟矩阵运算沾边的算法验证,大概率绕不开线性代数库。自己手写矩阵乘法、LU分解、特征值求解当然可以,但性能和稳定性跟经过几十年打磨的成熟…

作者头像 李华
网站建设 2026/10/17 14:13:05

编译五阶段实操指南:从C代码到机器指令的完整穿透

1. 这不是背概念的考试,而是考你“能不能让机器听懂人话”“软件设计师考试”里有一块内容,常年被考生称为“玄学章节”——程序设计语言与语言处理程序基础。很多人翻开教材,看到词法分析、语法树、中间代码、目标代码这些词,第一…

作者头像 李华