實(shí)戰(zhàn)與源碼解析)
后端游戲開發(fā)【免費(fèi)下載鏈接】colyseus? Multiplayer Framework for Node.js項(xiàng)目地址https://gitcode.com/gh_mirrors/co/colyseus點(diǎn)擊查看免費(fèi)下載本指南圍繞colyseus/bun-websockets傳輸包的最新版本演進(jìn)展開介紹如何用 Bun 原生 WebSocket 承載 Colyseus 游戲服務(wù)器從beforeUpgrade握手?jǐn)r截、raw()二進(jìn)制發(fā)送的正確姿勢到斷線重連關(guān)閉碼的選擇與simulateLatency延遲模擬并深入對應(yīng)源碼與測試讓你理解該傳輸層每個(gè)版本更新背后的設(shè)計(jì)取舍能在真實(shí)項(xiàng)目中直接落地。包概況與版本脈絡(luò)colyseus/bun-websockets位于倉庫 packages/transport/bun-websockets是 Colyseus 的 WebSocket 傳輸層實(shí)現(xiàn)之一。它不依賴 Node.js 的ws庫而是直接調(diào)用 Bun 運(yùn)行時(shí)內(nèi)建的Bun.serve()ServerWebSocketAPI因此只能在 Bun 運(yùn)行時(shí)下運(yùn)行源碼第一行即聲明reference typesbun-types /并在 BunWebSockets.ts 中標(biāo)注 bun-types 與 ws 類型存在沖突故以// ts-ignore引入 Bun 類型。包的導(dǎo)出面非常精簡src/index.ts 只導(dǎo)出兩樣?xùn)|西WebSocketClient實(shí)現(xiàn)colyseus/core的Client接口的客戶端封裝BunWebSockets與TransportOptions傳輸層主體及其配置類型。從 package.json 可以看到它依賴colyseus/core同倉 workspace與bun-serve-express用于在 Bun 中兼容 Express 中間件并以type: module同時(shí)發(fā)布importESMbuild/index.mjs與requireCJSbuild/index.cjs兩種產(chǎn)物。當(dāng)前倉庫中該包最新版本為 0.18.3其 CHANGELOG.md 記錄的演進(jìn)脈絡(luò)如下版本核心變化0.17.6首個(gè) changelog 條目0.17.8修復(fù) HTTP 頭獲取0.17.9升級bun-serve-express修復(fù)靜態(tài)文件與 Buffer 響應(yīng)0.17.10onReconnect()期間的消息改為入隊(duì)確保重連握手完成后才送達(dá)0.17.11對已 join 客戶端繼續(xù)入隊(duì)消息增加防御性檢查0.17.12修復(fù)sendBinary requires an ArrayBufferView報(bào)錯(cuò)修復(fù)shutdown()未完全釋放端口0.17.13devMode 下改用MAY_TRY_RECONNECT關(guān)閉碼給 HMR 重載窗口內(nèi)的 SDK 重試機(jī)會(huì)0.18.1enqueueRaw()委托給colyseus/core的enqueueClientRaw()統(tǒng)一 join 期消息緩沖與afterNextPatch路由0.18.2新增beforeUpgrade選項(xiàng)握手前攔截可返回Response拒絕升級0.18.3require()與import解析到同一 ESM 構(gòu)建產(chǎn)物避免雙份加載下面按主題逐個(gè)展開這些變更背后的實(shí)現(xiàn)細(xì)節(jié)。從 defineServer 到 Bun.serve傳輸層的整體接線BunWebSockets是一個(gè)標(biāo)準(zhǔn)的 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構(gòu)造時(shí)做了兩件事見 BunWebSockets.tsmaxPayloadLength未顯式提供時(shí)默認(rèn)設(shè)為4 * 10244KB把beforeUpgrade從選項(xiàng)里單獨(dú)拆出存為_beforeUpgrade其余選項(xiàng)原樣保留——因?yàn)樗皇?BunWebSocketHandler的合法字段不能隨websocket配置一并展開傳給Bun.serve()。listen()BunWebSockets.ts是核心接線點(diǎn)它在Bun.serve()的fetch處理器里完成了三條分支WebSocket 升級優(yōu)先嘗試server.upgrade(req)成功則進(jìn)入websocket回調(diào)族HTTP 路由升級失敗后先寫 CORS 頭、處理OPTIONS預(yù)檢返回 204再交給_routerColyseus 內(nèi)部 router處理Express 兼容router 沒有命中時(shí)通過bun-serve-express構(gòu)造IncomingMessage/ServerResponse包裝回退到 Express 應(yīng)用非 GET/HEAD 請求會(huì)先readBody()最終用eres.getBunResponse()拿回 Bun 響應(yīng)。對應(yīng)的集成測試Bun.test.ts同時(shí)斷言了GET /dummy內(nèi)部 router與GET /expressExpress 路由兩條 HTTP 鏈路都可用。WebSocketClientWebSocketClient.ts負(fù)責(zé)把 Bun 的ServerWebSocket包裝成 Colyseus 的ClientWebSocketWrapper繼承EventEmitter并持有原始wsWebSocketClient在此基礎(chǔ)上實(shí)現(xiàn)send、sendBytes、raw、error、leave等接口。值得注意的是close()已標(biāo)記廢棄并引導(dǎo)使用leave()且會(huì)打印調(diào)用棧警告見 WebSocketClient.ts。beforeUpgrade握手前的最后一扇門beforeUpgrade是 0.18.2 引入的能力在 WebSocket 握手真正發(fā)生之前用與onAuth()完全相同的只讀AuthContext對入站Request做校驗(yàn)返回一個(gè)Response就表示拒絕升級、直接應(yīng)答該請求。這通常用于封禁、限流、自定義鑒權(quán)或邊緣節(jié)點(diǎn)轉(zhuǎn)發(fā)比如測試?yán)锓祷豧ly-replay頭把請求彈給另一實(shí)例。它的類型定義與執(zhí)行規(guī)范位于核心包 Transport.tsexport type BeforeUpgradeHandler ( request: Request, context: ReadonlyAuthContext, ) Response | void | PromiseResponse | void;處理器返回Response→ 直接應(yīng)答不再升級返回undefined/void→ 繼續(xù)升級流程處理器拋異常 →runBeforeUpgrade捕獲并返回 500Host頭無法解析為合法 URL → 返回 400。所有傳輸層uWebSockets.js、Node、Bun都經(jīng)由同一個(gè)runBeforeUpgrade所以一套校驗(yàn)邏輯可以跨傳輸復(fù)用。在 Bun 側(cè)只有當(dāng)請求帶upgrade: websocket頭時(shí)才會(huì)構(gòu)造AuthContext并調(diào)用處理器BunWebSockets.tscontext惰性掛到WebSocketData上只有確實(shí)需要時(shí)才構(gòu)建。AuthContext由createAuthContext統(tǒng)一構(gòu)造Transport.ts字段包括tokenURL 查詢參數(shù)_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一并塞進(jìn)server.upgrade()的data供后續(xù)open回調(diào)使用。測試 Bun.test.ts 完整演示了兩種行為正常加入時(shí)斷言request.url包含 roomId、context.ip可取到客戶端地址隨后開啟intercept標(biāo)志用手工構(gòu)造的 Upgrade 請求驗(yàn)證返回的Response攜帶了自定義fly-replay響應(yīng)頭。raw() 與 ArrayBufferViewBun 二進(jìn)制發(fā)送的坑與修復(fù)0.17.12 修復(fù)的sendBinary requires an ArrayBufferView是一個(gè)很典型的 Bun 專屬問題。Bun 的ServerWebSocket.sendBinary()只接受ArrayBufferView如Uint8Array而 Colyseus 內(nèi)部getMessageBytes在編碼某些協(xié)議幀例如Protocol.ROOM_STATE時(shí)返回的是普通number[]直接傳給sendBinary會(huì)拋錯(cuò)。修復(fù)落在 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[] 數(shù)組 this.ref.ws.sendBinary(ArrayBuffer.isView(data) ? data : new Uint8Array(data)); }核心思路一句話ArrayBuffer.isView(data)為真就直接發(fā)送否則用new Uint8Array(data)包裝成視圖再發(fā)。與之配套的還有readyState檢查——連接未打開時(shí)靜默跳過避免在錯(cuò)誤狀態(tài)上調(diào)用發(fā)送。測試 Bun.test.ts 精確復(fù)現(xiàn)了這個(gè)場景先用Bun.serve起一個(gè)裸 WebSocket 服務(wù)器拿到服務(wù)端ws句柄構(gòu)造一個(gè)state JOINED的WebSocketClient然后斷言getMessageBytes[Protocol.ROOM_STATE]返回的是普通數(shù)組Array.isArray為真、ArrayBuffer.isView為假最后直接調(diào)用client.raw(data)驗(yàn)證不再拋錯(cuò)。enqueueRaw 的統(tǒng)一化join 期緩沖與 afterNextPatch 路由0.18.1 起enqueueRaw()不再在本包內(nèi)實(shí)現(xiàn)緩沖邏輯而是直接委托給colyseus/core的enqueueClientRaw()WebSocketClient.ts每個(gè)傳輸層只保留最底層的raw()接線。這也消除了WebSocketClient上的_afterNextPatchQueue字段。核心實(shí)現(xiàn)見 Transport.ts它是一個(gè)框架級的統(tǒng)一發(fā)送路徑按消息去向分三條路afterNextPatch消息推入客戶端的_pendingFrames緩沖作為下一幀狀態(tài)補(bǔ)丁之后單獨(dú)送達(dá)的幀同周期內(nèi)首幀推送時(shí)把客戶端登記進(jìn)_pendingFrameClients供Room在補(bǔ)丁后統(tǒng)一派發(fā)見 Room.ts 與_pendingFrames相關(guān)聲明 Room.ts。這是一個(gè)復(fù)用數(shù)組、零分配的路徑。尚未 JOINED在onJoin/onReconnect期間客戶端還不能注冊onMessage處理器消息先入_enqueuedMessages等JOIN_ROOM握手完成時(shí)統(tǒng)一沖刷——這正是 0.17.10 與 0.17.11 兩項(xiàng)修復(fù)的語義歸屬重連握手期間發(fā)送的消息必須排隊(duì)且對已 join 客戶端做防御性檢查。已 JOINED直接走client.raw(data, options)即發(fā)。理解這條路徑就能解釋 changelog 里 0.17.10/0.17.11 兩個(gè)條目的價(jià)值沒有緩沖onReconnect()里發(fā)出的消息會(huì)先于重連握手到達(dá)客戶端端onMessage尚未就緒消息就會(huì)丟失。重連關(guān)閉碼的選擇MAY_TRY_RECONNECT 與 HMR 窗口0.17.13 的語義變更非常貼近開發(fā)體驗(yàn)。在onConnection出錯(cuò)處理里BunWebSockets.ts如果連接失敗會(huì)根據(jù)場景選擇關(guān)閉碼client.error(e.code, e.message, () rawClient.close(reconnectionToken ? (isDevMode) ? CloseCode.MAY_TRY_RECONNECT // 4010 : CloseCode.FAILED_TO_RECONNECT // 4003 : CloseCode.WITH_ERROR)); // 4002關(guān)閉碼定義在 shared-types/src/Protocol.tsWITH_ERROR 4002、FAILED_TO_RECONNECT 4003、MAY_TRY_RECONNECT 4010。關(guān)鍵點(diǎn)在于攜帶了 reconnectionToken 但座位尚未保留成功時(shí)典型的 HMR 熱重載場景——舊進(jìn)程被 Vite 重載新進(jìn)程還沒接管座位devMode 下返回MAY_TRY_RECONNECT而非FAILED_TO_RECONNECT。SDK 看到 4010 會(huì)理解為可以再試一次從而在短暫的重載窗口內(nèi)自動(dòng)重連而 4003 表示放棄重連。生產(chǎn)模式下仍用FAILED_TO_RECONNECT避免無限重試打空轉(zhuǎn)。同樣的邏輯也貫穿核心層Room在重連超時(shí)等場景同樣使用這兩個(gè)關(guān)閉碼區(qū)分可以再試與徹底失敗見 Room.ts 與 MatchMaker.ts。端口釋放與雙模塊加載兩個(gè)容易被忽視的生命周期問題0.17.12 的第二個(gè)修復(fù)是shutdown()必須用stop(true)強(qiáng)制關(guān)閉監(jiān)聽器。Bun 的Server.stop(force)會(huì)同步強(qiáng)制關(guān)閉底層 socket否則端口可能殘留占用導(dǎo)致同一端口上新建的Bun.serve()拿到的是舊 handler。當(dāng)前實(shí)現(xiàn)BunWebSockets.ts正是public shutdown() { if (this._server) { this._server.stop(true); } }0.18.3 的修復(fù)則關(guān)乎包的雙模塊加載問題此前require()與import可能各加載一份構(gòu)建產(chǎn)物進(jìn)程里出現(xiàn)兩份傳輸層實(shí)例兩份Client類、兩份內(nèi)部狀態(tài)。修復(fù)后require()解析到與import相同的 ESM 構(gòu)建見 package.json 中exports的require: ./build/index.cjs與module-sync: ./build/index.mjs配置從而消除重復(fù)加載。simulateLatencyBun 側(cè)的延遲模擬實(shí)現(xiàn)simulateLatency(ms)是 Colyseus 的調(diào)試?yán)饔糜谀M往返延遲。Bun 傳輸層采用替換原型方法的方式實(shí)現(xiàn)BunWebSockets.tspublic simulateLatency(milliseconds: number) { if (this._originalRawSend null) { this._originalRawSend WebSocketClient.prototype.raw; // 緩存原始實(shí)現(xiàn) } const originalRawSend this._originalRawSend; WebSocketClient.prototype.raw milliseconds Number.EPSILON ? originalRawSend // 歸零即還原 : function (...args: any[]) { let [buf, ...rest] args; buf Buffer.from(buf); // 先拷貝避免共享緩沖被后續(xù)修改 setTimeout(() originalRawSend.apply(this, [buf, ...rest]), milliseconds); }; }要點(diǎn)有三先Buffer.from(buf)拷貝原始緩沖可能在延遲期間被復(fù)用或改寫必須先拷貝再延遲發(fā)送毫秒數(shù) Number.EPSILON即還原原始實(shí)現(xiàn)保證關(guān)閉模擬后零開銷只延遲出站方向入站方向由核心層applySimulatedLatency對Room.prototype._onMessage做對稱延遲Server.ts因此server.simulateLatency(ms)與COLYSEUS_LATENCY環(huán)境變量走的是同一套機(jī)制。測試 Bun.test.ts 在啟用 1ms 模擬后完成一次完整的joinOrCreate等待延遲消息沖刷Bun.sleep(100)再離開驗(yàn)證延遲路徑不會(huì)破壞sendBinary調(diào)用鏈??焖偕鲜忠粋€(gè)最小可用示例綜合以上機(jī)制一個(gè)帶beforeUpgrade鑒權(quán)的最小服務(wù)器長這樣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, // 默認(rèn)值可覆蓋 beforeUpgrade: async (request, context) { // 與 onAuth() 相同的只讀上下文token / ip / headers if (context.ip undefined) { return new Response(null, { status: 403 }); // 拒絕升級 } // 返回 undefined 則繼續(xù)升級 }, }), rooms: { game: defineRoom(GameRoom) }, }); await server.listen(2567); console.log(Colyseus Bun WebSocket server listening on ws://localhost:2567);注意事項(xiàng)匯總必須在 Bun 運(yùn)行時(shí)執(zhí)行bun run server.tsbun-types與ws類型沖突時(shí)用// ts-ignore處理默認(rèn)maxPayloadLength為 4KB需要更大幀如自定義二進(jìn)制協(xié)議請顯式調(diào)大重連場景下devMode 用MAY_TRY_RECONNECT給 SDK 重試空間生產(chǎn)環(huán)境保持FAILED_TO_RECONNECT發(fā)送二進(jìn)制務(wù)必走raw()/sendBytes()避免把number[]直接塞給 Bun 的sendBinary()調(diào)試網(wǎng)絡(luò)時(shí)用server.simulateLatency(ms)并記得用0或Number.EPSILON關(guān)閉。結(jié)語colyseus/bun-websockets的版本歷史本身就是一份 Bun 適配踩坑實(shí)錄從二進(jìn)制視圖類型、端口強(qiáng)關(guān)、握手?jǐn)r截到統(tǒng)一消息緩沖與重連關(guān)閉碼語義每個(gè)條目都能在 BunWebSockets.ts、WebSocketClient.ts 與核心 Transport.ts 里找到對應(yīng)的實(shí)現(xiàn)與測試佐證。若要在 Bun 上部署 Colyseus可直接對照 Bun.test.ts 中的場景驗(yàn)證你的部署并按上述注意事項(xiàng)規(guī)避已知的兼容性陷阱。贊分享后端游戲開發(fā)【免費(fèi)下載鏈接】colyseus? Multiplayer Framework for Node.js項(xiàng)目地址https://gitcode.com/gh_mirrors/co/colyseus點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Colyseus 傳輸層終極指南WebSocket、TCP 和 uWebSockets 的實(shí)戰(zhàn)應(yīng)用Colyseus 傳輸層終極指南WebSocket、TCP 和 uWebSockets 的實(shí)戰(zhàn)應(yīng)用 Colyseus 是一個(gè)強(qiáng)大的 Node.js 多人游戲框后端游戲開發(fā)Colyseus傳輸層終極指南WebSocket、TCP與uWebSockets性能對比Colyseus傳輸層終極指南WebSocket、TCP與uWebSockets性能對比 Colyseus是一款專為多人游戲和實(shí)時(shí)應(yīng)用設(shè)計(jì)的開源框架其傳輸層后端游戲開發(fā)Colyseus擴(kuò)展架構(gòu)驅(qū)動(dòng)與傳輸層深度解析Colyseus擴(kuò)展架構(gòu)驅(qū)動(dòng)與傳輸層深度解析 還在為Node.js多人在線游戲框架的擴(kuò)展性發(fā)愁Colyseus的模塊化架構(gòu)設(shè)計(jì)讓你輕松應(yīng)對各種場景需求本文后端游戲開發(fā)上一篇小愛音箱AI化實(shí)戰(zhàn)MiGPT讓普通音箱變身智能助手的完整指南下一篇Puerts重構(gòu)游戲開發(fā)的跨語言交互范式創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考