ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Colyseus Bun WebSocket 传输层(@colyseus/bun-websockets)实战与源码解析

Colyseus Bun WebSocket 传输层(@colyseus/bun-websockets)实战与源码解析 后端游戏开发【免费下载链接】colyseus⚔ Multiplayer Framework for Node.js项目地址https://gitcode.com/gh_mirrors/co/colyseus点击查看免费下载本指南围绕colyseus/bun-websockets传输包的最新版本演进展开介绍如何用 Bun 原生 WebSocket 承载 Colyseus 游戏服务器从beforeUpgrade握手拦截、raw()二进制发送的正确姿势到断线重连关闭码的选择与simulateLatency延迟模拟并深入对应源码与测试让你理解该传输层每个版本更新背后的设计取舍能在真实项目中直接落地。包概况与版本脉络colyseus/bun-websockets位于仓库 packages/transport/bun-websockets是 Colyseus 的 WebSocket 传输层实现之一。它不依赖 Node.js 的ws库而是直接调用 Bun 运行时内建的Bun.serve()ServerWebSocketAPI因此只能在 Bun 运行时下运行源码第一行即声明reference typesbun-types /并在 BunWebSockets.ts 中标注 bun-types 与 ws 类型存在冲突故以// ts-ignore引入 Bun 类型。包的导出面非常精简src/index.ts 只导出两样东西WebSocketClient实现colyseus/core的Client接口的客户端封装BunWebSockets与TransportOptions传输层主体及其配置类型。从 package.json 可以看到它依赖colyseus/core同仓 workspace与bun-serve-express用于在 Bun 中兼容 Express 中间件并以type: module同时发布importESMbuild/index.mjs与requireCJSbuild/index.cjs两种产物。当前仓库中该包最新版本为 0.18.3其 CHANGELOG.md 记录的演进脉络如下版本核心变化0.17.6首个 changelog 条目0.17.8修复 HTTP 头获取0.17.9升级bun-serve-express修复静态文件与 Buffer 响应0.17.10onReconnect()期间的消息改为入队确保重连握手完成后才送达0.17.11对已 join 客户端继续入队消息增加防御性检查0.17.12修复sendBinary requires an ArrayBufferView报错修复shutdown()未完全释放端口0.17.13devMode 下改用MAY_TRY_RECONNECT关闭码给 HMR 重载窗口内的 SDK 重试机会0.18.1enqueueRaw()委托给colyseus/core的enqueueClientRaw()统一 join 期消息缓冲与afterNextPatch路由0.18.2新增beforeUpgrade选项握手前拦截可返回Response拒绝升级0.18.3require()与import解析到同一 ESM 构建产物避免双份加载下面按主题逐个展开这些变更背后的实现细节。从 defineServer 到 Bun.serve传输层的整体接线BunWebSockets是一个标准的 ColyseusTransport子类通过defineServer注入使用。测试 Bun.test.ts 展示了最小接线方式import { defineServer, defineRoom, Room } from colyseus/core; import { BunWebSockets } from colyseus/bun-websockets; class DummyRoom extends Room { /* onCreate / onJoin / ... */ } const server defineServer({ transport: new BunWebSockets(), rooms: { dummy: defineRoom(DummyRoom) }, routes: createRouter({ ... }), }); await server.listen(8567);BunWebSockets构造时做了两件事见 BunWebSockets.tsmaxPayloadLength未显式提供时默认设为4 * 10244KB把beforeUpgrade从选项里单独拆出存为_beforeUpgrade其余选项原样保留——因为它不是 BunWebSocketHandler的合法字段不能随websocket配置一并展开传给Bun.serve()。listen()BunWebSockets.ts是核心接线点它在Bun.serve()的fetch处理器里完成了三条分支WebSocket 升级优先尝试server.upgrade(req)成功则进入websocket回调族HTTP 路由升级失败后先写 CORS 头、处理OPTIONS预检返回 204再交给_routerColyseus 内部 router处理Express 兼容router 没有命中时通过bun-serve-express构造IncomingMessage/ServerResponse包装回退到 Express 应用非 GET/HEAD 请求会先readBody()最终用eres.getBunResponse()拿回 Bun 响应。对应的集成测试Bun.test.ts同时断言了GET /dummy内部 router与GET /expressExpress 路由两条 HTTP 链路都可用。WebSocketClientWebSocketClient.ts负责把 Bun 的ServerWebSocket包装成 Colyseus 的ClientWebSocketWrapper继承EventEmitter并持有原始wsWebSocketClient在此基础上实现send、sendBytes、raw、error、leave等接口。值得注意的是close()已标记废弃并引导使用leave()且会打印调用栈警告见 WebSocketClient.ts。beforeUpgrade握手前的最后一扇门beforeUpgrade是 0.18.2 引入的能力在 WebSocket 握手真正发生之前用与onAuth()完全相同的只读AuthContext对入站Request做校验返回一个Response就表示拒绝升级、直接应答该请求。这通常用于封禁、限流、自定义鉴权或边缘节点转发比如测试里返回fly-replay头把请求弹给另一实例。它的类型定义与执行规范位于核心包 Transport.tsexport type BeforeUpgradeHandler ( request: Request, context: ReadonlyAuthContext, ) Response | void | PromiseResponse | void;处理器返回Response→ 直接应答不再升级返回undefined/void→ 继续升级流程处理器抛异常 →runBeforeUpgrade捕获并返回 500Host头无法解析为合法 URL → 返回 400。所有传输层uWebSockets.js、Node、Bun都经由同一个runBeforeUpgrade所以一套校验逻辑可以跨传输复用。在 Bun 侧只有当请求带upgrade: websocket头时才会构造AuthContext并调用处理器BunWebSockets.tscontext惰性挂到WebSocketData上只有确实需要时才构建。AuthContext由createAuthContext统一构造Transport.ts字段包括tokenURL 查询参数_authToken或Authorization: Bearer头ip按x-real-ip→x-forwarded-for取第一跳→x-client-ip→ 传输层remoteAddress的顺序解析headers惰性物化的Headers对象req仅 HTTP 匹配请求阶段存在。Bun 传输层用server.requestIP(req)?.address || unknown取对端地址BunWebSockets.ts并把url、searchParams、headers、remoteAddress与可选的context一并塞进server.upgrade()的data供后续open回调使用。测试 Bun.test.ts 完整演示了两种行为正常加入时断言request.url包含 roomId、context.ip可取到客户端地址随后开启intercept标志用手工构造的 Upgrade 请求验证返回的Response携带了自定义fly-replay响应头。raw() 与 ArrayBufferViewBun 二进制发送的坑与修复0.17.12 修复的sendBinary requires an ArrayBufferView是一个很典型的 Bun 专属问题。Bun 的ServerWebSocket.sendBinary()只接受ArrayBufferView如Uint8Array而 Colyseus 内部getMessageBytes在编码某些协议帧例如Protocol.ROOM_STATE时返回的是普通number[]直接传给sendBinary会抛错。修复落在 WebSocketClient.ts 的raw()方法public raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) void) { // WebSocket is globally available on Bun runtime if (this.ref.ws.readyState ! WebSocket.OPEN) { return; // 客户端未打开则跳过 } // Bun 的 sendBinary 要求 ArrayBufferViewUint8Array 等 // 确保不传入纯 number[] 数组 this.ref.ws.sendBinary(ArrayBuffer.isView(data) ? data : new Uint8Array(data)); }核心思路一句话ArrayBuffer.isView(data)为真就直接发送否则用new Uint8Array(data)包装成视图再发。与之配套的还有readyState检查——连接未打开时静默跳过避免在错误状态上调用发送。测试 Bun.test.ts 精确复现了这个场景先用Bun.serve起一个裸 WebSocket 服务器拿到服务端ws句柄构造一个state JOINED的WebSocketClient然后断言getMessageBytes[Protocol.ROOM_STATE]返回的是普通数组Array.isArray为真、ArrayBuffer.isView为假最后直接调用client.raw(data)验证不再抛错。enqueueRaw 的统一化join 期缓冲与 afterNextPatch 路由0.18.1 起enqueueRaw()不再在本包内实现缓冲逻辑而是直接委托给colyseus/core的enqueueClientRaw()WebSocketClient.ts每个传输层只保留最底层的raw()接线。这也消除了WebSocketClient上的_afterNextPatchQueue字段。核心实现见 Transport.ts它是一个框架级的统一发送路径按消息去向分三条路afterNextPatch消息推入客户端的_pendingFrames缓冲作为下一帧状态补丁之后单独送达的帧同周期内首帧推送时把客户端登记进_pendingFrameClients供Room在补丁后统一派发见 Room.ts 与_pendingFrames相关声明 Room.ts。这是一个复用数组、零分配的路径。尚未 JOINED在onJoin/onReconnect期间客户端还不能注册onMessage处理器消息先入_enqueuedMessages等JOIN_ROOM握手完成时统一冲刷——这正是 0.17.10 与 0.17.11 两项修复的语义归属重连握手期间发送的消息必须排队且对已 join 客户端做防御性检查。已 JOINED直接走client.raw(data, options)即发。理解这条路径就能解释 changelog 里 0.17.10/0.17.11 两个条目的价值没有缓冲onReconnect()里发出的消息会先于重连握手到达客户端端onMessage尚未就绪消息就会丢失。重连关闭码的选择MAY_TRY_RECONNECT 与 HMR 窗口0.17.13 的语义变更非常贴近开发体验。在onConnection出错处理里BunWebSockets.ts如果连接失败会根据场景选择关闭码client.error(e.code, e.message, () rawClient.close(reconnectionToken ? (isDevMode) ? CloseCode.MAY_TRY_RECONNECT // 4010 : CloseCode.FAILED_TO_RECONNECT // 4003 : CloseCode.WITH_ERROR)); // 4002关闭码定义在 shared-types/src/Protocol.tsWITH_ERROR 4002、FAILED_TO_RECONNECT 4003、MAY_TRY_RECONNECT 4010。关键点在于携带了 reconnectionToken 但座位尚未保留成功时典型的 HMR 热重载场景——旧进程被 Vite 重载新进程还没接管座位devMode 下返回MAY_TRY_RECONNECT而非FAILED_TO_RECONNECT。SDK 看到 4010 会理解为可以再试一次从而在短暂的重载窗口内自动重连而 4003 表示放弃重连。生产模式下仍用FAILED_TO_RECONNECT避免无限重试打空转。同样的逻辑也贯穿核心层Room在重连超时等场景同样使用这两个关闭码区分可以再试与彻底失败见 Room.ts 与 MatchMaker.ts。端口释放与双模块加载两个容易被忽视的生命周期问题0.17.12 的第二个修复是shutdown()必须用stop(true)强制关闭监听器。Bun 的Server.stop(force)会同步强制关闭底层 socket否则端口可能残留占用导致同一端口上新建的Bun.serve()拿到的是旧 handler。当前实现BunWebSockets.ts正是public shutdown() { if (this._server) { this._server.stop(true); } }0.18.3 的修复则关乎包的双模块加载问题此前require()与import可能各加载一份构建产物进程里出现两份传输层实例两份Client类、两份内部状态。修复后require()解析到与import相同的 ESM 构建见 package.json 中exports的require: ./build/index.cjs与module-sync: ./build/index.mjs配置从而消除重复加载。simulateLatencyBun 侧的延迟模拟实现simulateLatency(ms)是 Colyseus 的调试利器用于模拟往返延迟。Bun 传输层采用替换原型方法的方式实现BunWebSockets.tspublic simulateLatency(milliseconds: number) { if (this._originalRawSend null) { this._originalRawSend WebSocketClient.prototype.raw; // 缓存原始实现 } const originalRawSend this._originalRawSend; WebSocketClient.prototype.raw milliseconds Number.EPSILON ? originalRawSend // 归零即还原 : function (...args: any[]) { let [buf, ...rest] args; buf Buffer.from(buf); // 先拷贝避免共享缓冲被后续修改 setTimeout(() originalRawSend.apply(this, [buf, ...rest]), milliseconds); }; }要点有三先Buffer.from(buf)拷贝原始缓冲可能在延迟期间被复用或改写必须先拷贝再延迟发送毫秒数 Number.EPSILON即还原原始实现保证关闭模拟后零开销只延迟出站方向入站方向由核心层applySimulatedLatency对Room.prototype._onMessage做对称延迟Server.ts因此server.simulateLatency(ms)与COLYSEUS_LATENCY环境变量走的是同一套机制。测试 Bun.test.ts 在启用 1ms 模拟后完成一次完整的joinOrCreate等待延迟消息冲刷Bun.sleep(100)再离开验证延迟路径不会破坏sendBinary调用链。快速上手一个最小可用示例综合以上机制一个带beforeUpgrade鉴权的最小服务器长这样import { defineServer, defineRoom, Room } from colyseus/core; import { BunWebSockets } from colyseus/bun-websockets; class GameRoom extends Room { onCreate() { this.setState({ players: 0 }); } onJoin() { /* ... */ } } const server defineServer({ transport: new BunWebSockets({ maxPayloadLength: 4 * 1024, // 默认值可覆盖 beforeUpgrade: async (request, context) { // 与 onAuth() 相同的只读上下文token / ip / headers if (context.ip undefined) { return new Response(null, { status: 403 }); // 拒绝升级 } // 返回 undefined 则继续升级 }, }), rooms: { game: defineRoom(GameRoom) }, }); await server.listen(2567); console.log(Colyseus Bun WebSocket server listening on ws://localhost:2567);注意事项汇总必须在 Bun 运行时执行bun run server.tsbun-types与ws类型冲突时用// ts-ignore处理默认maxPayloadLength为 4KB需要更大帧如自定义二进制协议请显式调大重连场景下devMode 用MAY_TRY_RECONNECT给 SDK 重试空间生产环境保持FAILED_TO_RECONNECT发送二进制务必走raw()/sendBytes()避免把number[]直接塞给 Bun 的sendBinary()调试网络时用server.simulateLatency(ms)并记得用0或Number.EPSILON关闭。结语colyseus/bun-websockets的版本历史本身就是一份 Bun 适配踩坑实录从二进制视图类型、端口强关、握手拦截到统一消息缓冲与重连关闭码语义每个条目都能在 BunWebSockets.ts、WebSocketClient.ts 与核心 Transport.ts 里找到对应的实现与测试佐证。若要在 Bun 上部署 Colyseus可直接对照 Bun.test.ts 中的场景验证你的部署并按上述注意事项规避已知的兼容性陷阱。赞分享后端游戏开发【免费下载链接】colyseus⚔ Multiplayer Framework for Node.js项目地址https://gitcode.com/gh_mirrors/co/colyseus点击查看免费下载相关推荐Colyseus 传输层终极指南WebSocket、TCP 和 uWebSockets 的实战应用Colyseus 传输层终极指南WebSocket、TCP 和 uWebSockets 的实战应用 Colyseus 是一个强大的 Node.js 多人游戏框后端游戏开发Colyseus传输层终极指南WebSocket、TCP与uWebSockets性能对比Colyseus传输层终极指南WebSocket、TCP与uWebSockets性能对比 Colyseus是一款专为多人游戏和实时应用设计的开源框架其传输层后端游戏开发Colyseus扩展架构驱动与传输层深度解析Colyseus扩展架构驱动与传输层深度解析 还在为Node.js多人在线游戏框架的扩展性发愁Colyseus的模块化架构设计让你轻松应对各种场景需求本文后端游戏开发上一篇小爱音箱AI化实战MiGPT让普通音箱变身智能助手的完整指南下一篇Puerts重构游戏开发的跨语言交互范式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表