傳輸協議(自行實現客戶端)
你用的引擎或語言還沒有官方 SDK(如 Godot 的 GDScript、Unreal、原生 iOS / Android)時,可以按本頁用 WebSocket 自己實現遊戲客戶端:先用 HTTPS 加入房間、拿到遊戲節點的地址和連線票據,再用 WebSocket 連線節點收發二進位制訊息。有官方 SDK 的平臺請直接使用 SDK:Web、Unity。
只公開 WebSocket 通道
遊戲節點還有一條 UDP(KCP + 加密)通道,延遲更低,只由官方 SDK 實現,本頁不描述。自己實現時用 WebSocket:加密由 TLS 負責,幾乎所有引擎都自帶 WebSocket,一般一兩天能完成。
一次完整的連線:
1. HTTPS POST /client/v1/sessions 登录 IM,拿到 access_token(已登录的跳过)
2. HTTPS POST /client/v1/game/rooms/{room_id}/join 带上业务给的票据,拿到 connect.ws_url 和 connect.connect_ticket
3. WSS 连接 connect.ws_url(子协议 im.game.v1)
4. WSS → 第一条二进制消息:握手(HELLO + 连接票据)
5. WSS ← WELCOME:座位、成员、当前帧号、resume_token
6. WSS ⇄ INPUT / FRAME / CUSTOM / PING / PONG … 每条二进制消息就是一条协议消息| 要做什麼 | 看哪裡 |
|---|---|
| 加入、離開房間,找回當前房間 | HTTPS 介面 |
| 連線節點、握手 | 連線與握手 |
| 把訊息編碼成位元組、從位元組解出訊息 | 訊息格式 |
| 傳送操作、按順序處理幀、補幀 | 幀同步 |
| 心跳、斷線後恢復 | 心跳與斷線恢復 |
| 處理節點返回的錯誤 | 錯誤碼 |
| 用測試資料核對自己的實現 | 測試資料 |
你需要實現的部分
| 部分 | 說明 | 工作量 |
|---|---|---|
| HTTPS 呼叫 | 登入、加入(C1)、離開(C2)、找回(C3),JSON | 小 |
| 訊息編解碼 | 18 種訊息,大端整數、帶長度字首的位元組串 | 小,幾百行 |
| 收幀整理 | 按幀號排序、去重、缺幀時請求補幀 | 小 |
| 心跳和斷線恢復 | 每秒 PING;斷線後用 resume_token 恢復,失敗時重新調 C1 | 中 |
HTTPS 介面
介面都在 $IM_API 下,帶登入得到的 User Token:Authorization: Bearer <access_token>。登入、Token 續期見客戶端登入。錯誤的格式同服務端錯誤碼,原因在 details.reason。
加入房間(C1)
POST /client/v1/game/rooms/{room_id}/join
{"client_session_id": "join-6f2a01b7-468e", "ticket": "gtk_9f3KqZ1mXb2…", "transport": "ws"}| 欄位 | 說明 |
|---|---|
client_session_id | 本次加入的 ID,8–64 位可見 ASCII(0x21–0x7E),由你生成。同一次加入流程中重試、斷線後重新取連線票據時必須保持不變;App 重啟後用新的 |
ticket | 你的服務端簽發給這個玩家的加入票據。open 房間不傳 |
transport | 傳 ws |
返回 201(首次加入)或 200(同一登入會話重複呼叫,座位不變):
{
"room": {"room_id": "rank-8812", "status": "open", "sync_mode": "frame", "tick_rate": 30, "max_players": 4, "reconnect_window_seconds": 60},
"member": {"seat": 0, "username": "alice", "status": "joined"},
"connect": {
"node_id": "game-e1",
"ws_url": "wss://game-e1.game.example.com:7001/game/v1/ws",
"connect_ticket": "AQ3k…",
"protocol_version": 1,
"expires_at": "2026-11-10T08:01:00.000Z"
}
}| 欄位 | 說明 |
|---|---|
connect.ws_url | 遊戲節點的 WebSocket 地址 |
connect.connect_ticket | 連線票據,握手時原樣發給節點。60 秒內有效、只能用一次;過期或用過後重新調 C1 取新的 |
connect.protocol_version | 協議版本,當前為 1 |
room.reconnect_window_seconds | 斷線後保留座位的時間,決定你最多重連多久 |
響應中還有 connect.udp、connect.session_key、connect.key_id、connect.transport,只用於 UDP 通道,用 WebSocket 時忽略。session_key 是敏感資料,不要寫進日誌。
拿到座位後 60 秒內沒有連上節點,座位會被釋放(connect_timeout)。
| 返回 | 原因 | 處理 |
|---|---|---|
403 permission_denied(沒有 reason) | 加入票據無效、過期或已用過 | 讓你的服務端重新簽發 |
403 permission_denied + game_disabled / plan_feature_disabled | 平臺沒有開放,或套餐不含遊戲房間 | 提示暫時無法進入 |
404 not_found、409 conflict + room_not_open | 房間不存在或已結束 | 提示對局已結束 |
409 conflict + room_full / seat_taken | 房間已滿、指定的座位被佔 | 提示房間已滿 |
409 conflict + joined_on_other_device | 同一賬號已在其他裝置加入 | 提示在其他裝置進入了對局 |
503 unavailable + game_node_unavailable | 房間所在的遊戲節點暫時不可用 | 稍後重試 |
401 unauthenticated | User Token 過期 | 續期後重試一次 |
離開房間(C2)
POST /client/v1/game/rooms/{room_id}/leave,沒有請求體。可以重複呼叫;從沒加入過返回 {"member": null}。離開時先在 WebSocket 上發 LEAVE,再調這個介面。
找回當前房間(C3)
GET /client/v1/game/current 返回本登入會話還沒離開的房間 {"room": {"room_id", "status"}, "member": {...}},沒有時都為 null。App 重啟後用它找回對局,再用同一個 room_id 調 C1(不需要新票據,用新的 client_session_id)。
這要求沿用原來的登入會話:儲存好登入得到的 token,重啟後繼續使用。重新登入會成為新的登入會話,C3 查不到原來的房間,C1 也會返回 joined_on_other_device。
連線與握手
- 用
connect.ws_url建立 WebSocket 連線,子協議傳im.game.v1。 - 連上後傳送第一條二進位制訊息(握手):
| 偏移 | 長度 | 內容 |
|---|---|---|
| 0 | 1 | kind:1 = HELLO(用連線票據加入),2 = RESUME(斷線恢復,見斷線恢復) |
| 1 | 2 | token 的位元組數,大端 |
| 3 | n | token:HELLO 時為 connect_ticket 字串的 UTF-8 位元組;RESUME 時為 resume_token 原樣的位元組 |
| 3+n | … | 一條 HELLO 或 RESUME 訊息(按訊息格式編碼) |
- 節點回復的第一條訊息是
WELCOME(成功)或ERROR(失敗,見錯誤碼),之後節點不會再回復握手。
例:connect_ticket 为 "AQ3k",HELLO 的 client_nonce = 0x0123456789ABCDEF,sdk_version = "godot/0.1"
01 kind = HELLO
00 04 token 长度 = 4
41 51 33 6B "AQ3k"
01 消息类型 HELLO
01 23 45 67 89 AB CD EF client_nonce(u64,大端)
09 sdk_version 长度(uvarint)
67 6F 64 6F 74 2F 30 2E 31 "godot/0.1"- 節點在 10 秒內收不到握手就斷開。
client_nonce填一個隨機數;sdk_version填平台/版本,不超過 64 位元組,用於排查問題。- 握手之後,每個二進位制幀就是一條訊息,節點忽略文本幀。不要把幾條訊息拼在一個幀裡。
訊息格式
每條訊息的第一個位元組是型別,後面的欄位按表中的順序緊挨著編碼,沒有分隔和填充:
| 型別 | 編碼 |
|---|---|
u8、u16、u32、u64 | 無符號整數,大端,1、2、4、8 位元組 |
i8 | 有符號 8 位整數(補碼),只用於 snapshot_seat |
bool | 1 位元組,0 或 1,其他值視為錯誤 |
uvarint | 無符號變長整數(每位元組低 7 位,最高位為 1 表示後面還有,小端順序,與 Protocol Buffers 的 varint 相同) |
bytes | uvarint 長度 + 位元組 |
string | 同 bytes,內容為 UTF-8 |
frames | uvarint 幀數,每幀:u32 幀號 + u8 運算元 + 每個操作(u8 座位 + u8 長度 + 位元組) |
解碼時,越界、長度超過上限、bool 不是 0 或 1、末尾有多餘位元組,都應當作訊息無效丟棄。節點連續收到 3 條無效訊息會以 protocol_unsupported 斷開。
訊息一覽
| 型別 | 名稱 | 方向 | 欄位(按順序) |
|---|---|---|---|
0x01 | HELLO | 客戶端 → 節點(只在握手中) | client_nonce: u64,sdk_version: string(≤ 64) |
0x02 | WELCOME | 節點 → 客戶端 | conv: u32,seat: u8,sync_mode: u8,tick_rate: u8,room_state: u8,start_frame: u32,frame: u32,members: uvarint 个数 + 每个(seat: u8,username: string,online: bool),resume_token: bytes(≤ 256),server_time_ms: u64,snapshot_seat: i8,max_players: u8 |
0x03 | RESUME | 客戶端 → 節點(只在握手中) | client_nonce: u64,last_frame: u32 |
0x10 | INPUT | 客戶端 → 節點 | ack_frame: u32,data: bytes(≤ 128) |
0x11 | FRAME | 節點 → 客戶端 | frames(≤ 3 幀:新幀和前兩幀冗餘) |
0x12 | FRAME_REQ | 客戶端 → 節點 | from: u32,to: u32 |
0x13 | FRAME_BATCH | 節點 → 客戶端 | frames(≤ 300 幀) |
0x20 | CUSTOM | 雙向 | to: u16,from: u8,reliable: bool,data: bytes(≤ 1024) |
0x30 | READY | 客戶端 → 節點 | 無 |
0x31 | START | 客戶端 → 節點 | 無 |
0x32 | ROOM_STATE | 節點 → 客戶端 | state: u8,start_frame: u32,reason: string,result: bytes |
0x33 | MEMBER | 節點 → 客戶端 | seat: u8,username: string,kind: u8,reason: string |
0x40 | SNAPSHOT_PUT | 客戶端 → 節點 | frame: u32,data: bytes(≤ 65536) |
0x41 | SNAPSHOT | 節點 → 客戶端 | frame: u32,data: bytes(≤ 65536) |
0x50 | PING | 客戶端 → 節點 | client_ms: u64,rtt_ms: u32 |
0x51 | PONG | 節點 → 客戶端 | client_ms: u64,server_ms: u64 |
0x60 | LEAVE | 客戶端 → 節點 | 無 |
0x7F | ERROR | 節點 → 客戶端 | code: u8,message: string,reason: string,result: bytes |
欄位的取值:
| 欄位 | 取值 |
|---|---|
sync_mode | 1 幀同步(frame),2 即時轉發(relay) |
room_state、ROOM_STATE.state | 1 等待開局,2 對局中,3 正在結束 |
MEMBER.kind | 1 上線,2 斷線(reason 為 network 或 timeout,可能回來),3 離開(座位釋放) |
WELCOME.snapshot_seat | 需要提供快照的座位;-1 表示房間沒有開啟快照 |
WELCOME.conv | 只用於 UDP,WebSocket 忽略 |
CUSTOM.to | 收件座位的位掩碼(座位 n 對應第 n 位),0 表示除自己以外的所有人;from 由節點填寫,傳送時填 0 |
CUSTOM.reliable | WebSocket 上總是可靠送達,這個欄位原樣轉給收件人 |
ROOM_STATE.result、ERROR.result | 結束房間時你的服務端給的結算結果,JSON 的 UTF-8 位元組;沒有時長度為 0 |
幀同步
開局:房間的 start_mode 為 all_ready 時,每個玩家發 READY,所有已連上的玩家都準備後開局;auto 在第一個玩家連上時開局;manual 由你的服務端調開局介面。客戶端的 START 暫不開放。開局時節點廣播 ROOM_STATE(state=2, start_frame),第一幀的幀號為 start_frame。WELCOME 的 room_state 已經是 2 時,說明你加入時對局已經開始。
傳送操作:每幀發一條 INPUT,data 是你自己編碼的本幀操作(≤ 128 位元組),ack_frame 填你已經按順序處理到的幀號。節點把一個幀週期內收到的最後一條 INPUT 放進下一幀。每個座位每秒不要超過幀率的 2 倍條,超出的被丟棄,連續 10 秒超出會被移出房間。
接收幀:節點每幀發一條 FRAME,裡面最多 3 幀:最新的一幀加前兩幀的冗餘。按下面的規則整理後再交給遊戲邏輯,保證不亂序、不重複、不跳號:
- 記下期望的下一幀
next,初始為 WELCOME 的frame + 1(frame為 0 時從 1 開始)。 - 收到
FRAME或FRAME_BATCH時,丟棄幀號小於next的,其餘放進快取。 - 只要快取裡有
next,就交給遊戲邏輯並next + 1,重複。 - 快取裡最新的幀號大於等於
next + 3(冗餘也補不上)時,發FRAME_REQ(from=next, to=最新帧号-1);同一個next只請求一次。節點用FRAME_BATCH回覆(一次最多 300 幀)。
每一幀的 inputs 只包含這一幀有操作的座位。sync_mode 為 relay 的房間沒有幀,只用 CUSTOM。
快照:WELCOME 的 snapshot_seat 等於你的座位時,你是快照提供者,請按房間設定的間隔發 SNAPSHOT_PUT(frame, 游戏状态)。斷線超過 60 秒(節點只保留 60 秒的幀)後恢復時,節點會先發 SNAPSHOT,你用它恢復遊戲狀態,把 next 設為 SNAPSHOT.frame + 1,再接收之後的幀。
心跳與斷線恢復
心跳:連上後每 1 秒發一條 PING(client_ms=本地毫秒时间, rtt_ms=最近的往返延迟,没有时为 0)。節點立即回 PONG,原樣帶回 client_ms 並附上 server_ms。往返延遲 = 收到 PONG 的時間 − client_ms,建議取最近 10 次的中位數;伺服器時間 ≈ server_ms + 往返延遲 / 2。節點 10 秒沒收到任何訊息就斷開這個連線,所以心跳不能停。
判定斷線:5 秒沒有收到任何訊息,或 WebSocket 關閉,按斷線處理。
斷線恢復
- 立即重連:新建 WebSocket,握手的
kind = 2,token 為 WELCOME 裡的resume_token,訊息為RESUME(client_nonce=随机数, last_frame=已处理的最后一帧)。 - 成功時節點回
WELCOME,然後用FRAME_BATCH(必要時先發SNAPSHOT)補發last_frame之後的幀。用新 WELCOME 裡的resume_token替換舊的。 - 節點回
ERROR(11 resume_rejected),或連線票據相關的錯誤時:用同一個client_session_id重新調 C1,拿到新的connect_ticket,再用kind = 1握手。這種情況下節點不會主動補幀,請按收到的 WELCOME 的frame發FRAME_REQ補齊。 - 連不上時按 0、1、2、4、4… 秒退避重試,直到超過
room.reconnect_window_seconds;超過後座位已被釋放,結束對局。 - 收到
kicked、room_closed、node_draining、seat_taken時不要重連,見錯誤碼。
離開:玩家主動退出時發 LEAVE,關閉 WebSocket,再調 C2。
對局結束:你的服務端結束房間時,節點先廣播 ROOM_STATE(state=3, reason, result),再發 ERROR(9 room_closed, reason, result) 並在 1 秒後斷開。reason 為結束原因(如 closed_by_server、expired),result 為結算結果。
錯誤碼
節點用 ERROR 訊息報告錯誤。握手階段回 ERROR 後關閉連線;連上以後收到 ERROR,按表處理:
code | 名稱 | 什麼時候 | 處理 |
|---|---|---|---|
| 1 | ticket_invalid | 握手:連線票據無效 | 重新調 C1 取票後再握手一次;仍失敗讓業務重新簽發加入票據 |
| 2 | ticket_expired | 握手:連線票據過期或已用過 | 同上 |
| 3 | room_not_found | 握手:房間不在這個節點上或已結束 | 結束 |
| 4 | room_full | 握手:座位不可用 | 結束,提示房間已滿 |
| 5 | seat_taken | 握手:座位已有更新的加入;連上後:同一座位在別處連上(reason 為 replaced) | 結束,提示在其他裝置進入了對局 |
| 6 | protocol_unsupported | 握手格式不對、連續傳送無效訊息 | 檢查實現,結束 |
| 7 | rate_limited | 連上後:連續 10 秒超出傳送頻率 | 按斷線恢復處理;這時座位通常已被釋放,恢復和重新加入都會失敗,結束對局 |
| 8 | kicked | 連上後:被你的服務端移出 | 結束,不要重連 |
| 9 | room_closed | 對局結束(reason、result 見上文) | 結束,顯示結算 |
| 10 | node_draining | 遊戲節點下線維護 | 結束 |
| 11 | resume_rejected | 握手:resume_token 無效或座位已不存在 | 用同一 client_session_id 重新調 C1,再用 HELLO 握手 |
測試資料
官方 SDK 用同一份測試資料保證實現一致,你可以用它核對自己的編解碼和收幀整理:下載測試資料(協議版本 1) · SHA256 校驗檔案。
| 檔案 | 內容 | 怎麼用 |
|---|---|---|
messages.json | 全部 18 種訊息的欄位和對應的十六進位制編碼,以及必須拒絕的輸入 | valid 的每一項:按 fields 編碼應等於 hex,解碼 hex 應等於 fields;invalid 的每一項都應解碼失敗 |
ws_handshake.json | 握手(HELLO、RESUME)的完整位元組 | 用 kind、token_hex、message_hex 拼出的結果應等於 hex |
frames.json | 收幀整理的場景 | 按 steps 依次送入幀號,每步交付的幀和補幀請求應與 deliver、request 相同 |
behavior.json | 超時、心跳、重連退避等常量,加入介面和節點錯誤碼的處理 | 對照本頁的規則 |
測試資料中 64 位整數寫成十進位制字串,位元組串寫成十六進位制。
規則與細節
- 一個座位同一時刻只有一個連線。同一賬號用同一座位再握手,舊連線會被節點斷開。
- 幀號是
u32,從開局幀開始每幀加一;對局時長受房間的最長時長限制,不會溢位。 - 節點不保證
PONG、FRAME一定送到你這裡(連線斷開時會丟);靠幀號和補幀保證完整。 - 協議的新版本會改變 C1 返回的
protocol_version;收到不認識的版本號時請提示使用者升級。解碼時遇到不認識的訊息型別直接忽略,以便節點以後增加新訊息。不要傳送本頁沒有列出的訊息。 - 本頁描述的是協議版本 1。限制和計費見控制台、計費與限制。
