Cocos Creator WebSocket 实战:从连接到优化的完整指南
·
背景与痛点
把实时对战做进 Cocos Creator 时,最痛的往往不是逻辑,而是“网”。
官方给的 WebSocket 只是浏览器接口的搬运工,真到项目里,掉线、重连、乱序、卡顿一样不少。下面这些问题,我踩了不止一次:
- 连接管理:原生
WebWebSocket没有“断线重连”概念,弱网一抖,玩家就被踢出房间。 - 数据序列化:JSON 图方便,但一个
JSON.stringify就把 60 FPS 降到 30;二进制不会用,又容易写出一堆readUInt8。 - 性能优化:每帧广播 20 个角色,每个角色 30 字段,峰值 500 msg/s,CPU 直接飙红。
- 安全:明文
ws://一抓一个准,外挂直接发{"cmd":"killAll"}。
一句话:WebSocket 不等于“实时”,能跑起来的那套“框架”才是核心竞争力。
技术选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HTTP 轮询 | 实现简单、兼容老后端 | 延迟高、流量大 | 弱交互、回合制 |
| Socket.IO | 自动降级、房间 API 丰富 | 包体 +120 KB、需要专用服务端 | H5 轻对战 |
| 原生 WebSocket | 零依赖、可细粒度优化 | 什么都得自己写 | 中重度实时、帧同步 |
结论:Cocos Creator 做多人同屏、帧同步,原生 WebSocket 仍是性价比最高的“毛坯房”,自己装修即可入住。
核心实现
下面代码基于 Cocos Creator 3.8,TypeScript 9,跑在浏览器与原生双端。
设计目标:
- 单例
NetManager全局复用 - 自动重连 + 心跳
- 支持
protobuf二进制协议(可选 JSON) - 事件派发,业务层只关心
cmd和body
1. 建立连接与事件监听
// NetManager.ts
import { EventTarget } from 'cc';
enum NetEvent {
OPEN = 'open',
ERROR = 'error',
CLOSE = 'close',
MESSAGE = 'message',
}
export class NetManager extends EventTarget {
private static._inst: NetManager;
public static get inst() {
return this._inst ?? (this._inst = new NetManager());
}
private _ws: WebSocket | null = null;
private _url: string = '';
private _reconnectTimer: number = 0;
private _heartTimer: number = 0;
private _missedPong: number = 0;
/** 外部调用:长连接入口 */
public connect(url: string) {
if (this._ws?.readyState === WebSocket.OPEN) return;
this._url = url;
this._doConnect();
}
private _doConnect() {
this._ws = new WebSocket(this._url);
this._ws.binaryType = 'arraybuffer'; // 重点:二进制传输
this._ws.onopen = () => this._onOpen();
this._ws.onclose = () => this._onClose();
this._ws.onerror = () => this._onError();
this._ws.onmessage = (e) => this._onMessage(e);
}
private _onOpen() {
this._missedPong = 0;
this._startHeartbeat();
this.emit(NetEvent.OPEN);
}
private _onClose() {
this._stopHeartbeat();
this.emit(NetEvent.CLOSE);
// 断线 3s 后自动重连
this._reconnectTimer = window.setTimeout(() => this._doConnect(), 3000);
}
private _onError() {
this.emit(NetEvent.ERROR);
this._ws?.close();
}
private _onMessage(e: MessageEvent) {
// 统一解包
const buf = new Uint8Array(e.data);
const cmd = buf[0]; // 第 1 字节命令号
const body = buf.slice(1);
this.emit(NetEvent.MESSAGE, cmd, body);
}
/** 发送二进制帧 */
public send(cmd: number, body: Uint8Array) {
if (this._ws?.readyState !== WebSocket.OPEN) return;
const pack = new Uint8Array(1 + body.length);
pack[0] = cmd;
pack.set(body, 1);
this._ws.send(pack.buffer);
}
/* ===== 心跳 ===== */
private _startHeartbeat() {
this._heartTimer = window.setInterval(() => {
if (this._missedPong > 2) { // 丢 3 次判死亡
this._ws?.close();
return;
}
this.send(0xFF, new Uint8Array(0)); // 0xFF 为心跳
this._missedPong++;
}, 3000);
}
private _stopHeartbeat() {
window.clearInterval(this._heartTimer);
}
}
2. 业务层监听示例
// GameCtrl.ts
import { NetManager } from './NetManager';
import { proto } from './proto/bundle'; // protobufjs 生成
NetManager.inst.connect('wss://game.example.com/ws');
NetManager.inst.on('message', (cmd: number, buf: ArrayBuffer) => {
if (cmd === 0x01) { // 角色同步
const data = proto.RoleSync.decode(new Uint8Array(buf));
// 更新渲染层
RoleLayer.inst.sync(data);
}
});
3. 打包与发送
const role = { id: 1001, x: 1234, y: 5678 };
const body = proto.RoleSync.encode(role).finish();
NetManager.inst.send(0x01, body);
性能优化
- 二进制优先:JSON 文本 186 B → protobuf 38 B,带宽降 80%,解析耗时从 1.2 ms 降到 0.15 ms。
- 合并发送:每 50 ms 批量合并 20 条消息,减少
send调用次数,降低内核切换开销。 - 对象池:角色同步包复用
Uint8Array,避免new造成的 GC 抖动。 - 优先级队列:语音、帧同步走高优通道;聊天、道具走普通通道,防止大头包堵队。
- 压缩:对 >1 KB 的包头启用
pako.deflate,实测再省 45% 流量,代价 0.3 ms CPU,可接受。
避坑指南
- 心跳包必须带“回包”校验,否则 NAT 设备会悄悄把连接踢掉。
- 断线重连要清队列,旧消息再发会乱序;给每条消息加 2 字节
seq,服务端按序重放。 - 安卓息屏后 45 s 会断 Wi-Fi,回到游戏要先检测
navigator.onLine,再决定是否重连。 - iOS 切后台 3 min 进程就被挂起,重连后需要重新登录并拉全量快照,防止“幽灵帧”。
- 并发登录:玩家快速点两次“开始游戏”,可能创建双
WebSocket;单例模式里加_connecting标记,保证同时只连一次。
安全考量
- 一律
wss://,防止明文被抓包。 - 每条逻辑包再做
AES-128-GCM加密,密钥通过 HTTPS 登录接口下发,一次一密。 - 签名:包头附加 4 字节
HMAC-SHA256截断,服务端拒收伪造包。 - 指令白名单:服务端校验
cmd是否在账号权限表,避免客户端发高级 GM 指令。 - 限速:同一 IP 60 包/s 触发阈值,直接拉黑 5 min,防止脚本爆破。
完整流程图

结语
把 WebSocket 塞进 Cocos Creator 只是起点:帧同步、预测回滚、LOD 网络、服务器帧率……多人实时每往下走一步,都会冒出更细的挑战。
你可以试着把这套 NetManager 移植到微信小游戏的小程序环境,或者把 protobuf 换成 FlatBuffers 做零拷贝,甚至用 WebWorker 把解码线程独立出来。
当延迟 <16 ms、掉线率 <1% 时,玩家才会真正觉得“这游戏不卡”。希望本文的代码与思路,能让你少踩几个坑,把更多时间花在“好玩”本身。欢迎交流你的踩坑记录,一起把实时对战做得更丝滑。
更多推荐





所有评论(0)