跳到主要内容

RhythMC-Reborn WebSocket 协议文档

目录

  1. 协议概述
  2. 连接管理
  3. 消息格式
  4. 消息类型
  5. 实时同步流程
  6. 数据包详解
  7. 状态管理
  8. 错误处理

协议概述

基本信息

项目
协议WebSocket over TLS (WSS)
数据格式JSON
字符编码UTF-8
心跳间隔30秒 (由HTTP客户端配置)

实现说明:插件打包时不再 relocate fastjson2,保留其原始包名,避免 fastjson2 的 ASM 反序列化 reader 在运行时回查原始 com.alibaba.fastjson2.* 类名时触发 NoClassDefFoundError

架构位置

类型层次

恢复语义

  • 服务端在 serverSession WebSocket 意外断开后,会保留玩家队列、房间和待处理对战请求一段恢复窗口,而不是立刻把玩家视为主动退出。
  • 客户端在 WebSocket 重新连上后,必须继续为每个在线玩家发送一次 resume_session,服务端才会把这些运行态重新绑定到新的连接。
  • 如果恢复窗口内没有重新连回,后端的 serverSession 清理器才会执行真正的强制下线与房间清理。

连接管理

连接生命周期

连接参数

// 配置参数
maxRetry = 5 // 退避上限阶段,不再代表最终停止次数
reconnectDelayMs = 1000 // 初始重连延迟(毫秒)

// 指数退避算法
delay = reconnectDelayMs * 2^min(retryCount, maxRetry - 1)

握手认证

  • WebSocket 握手统一使用 Authorization: Bearer <serverAccessToken>
  • 客户端不再在连接 URL query 中附带 serverSessionId
  • 后端在握手成功后从 Bearer token 反查当前 serverSession,再将后续 resume_session、聊天、对战等玩家动作绑定到该服务器会话。

重连时序图

恢复语义

  • 服务端在 serverSession WebSocket 意外断开后,会保留玩家队列、房间和待处理对战请求一段恢复窗口,而不是立刻把玩家视为主动退出。
  • 客户端在 WebSocket 重新连上后,必须继续为每个在线玩家发送一次 resume_session,服务端才会把这些运行态重新绑定到新的连接。
  • 如果恢复窗口内没有重新连回,后端的 serverSession 清理器才会执行真正的强制下线与房间清理。

消息格式

通用结构

所有 WebSocket 消息都包含 type 字段用于类型识别:

{
"type": "message_type",
// ... 其他字段
}

序列化/反序列化

使用 JsonUtils 工具类处理:

// 序列化
String json = JsonUtils.toJson(wsPayload);

// 反序列化 (根据type字段自动识别)
WsPayload payload = JsonUtils.fromWsJson(json);

类型路由

switch (type) {
case "ping" -> Ping.class
case "pong" -> Pong.class
default -> throw IllegalArgumentException
}

消息类型

1. Ping (心跳请求)

方向: Server → Client

用途: 检测客户端连接状态

{
"type": "ping"
}

字段:

字段类型描述
typeString固定值 "ping"

2. Pong (心跳响应)

方向: Client → Server

用途: 响应心跳请求

{
"type": "pong"
}

字段:

字段类型描述
typeString固定值 "pong"

实时同步流程

心跳保活机制


数据包详解

WsPayload 接口

public sealed interface WsPayload
permits Ping, Pong {
String type();
}

设计特点:

  • Sealed Interface: 限制实现类型,确保类型安全
  • 统一type方法: 所有消息必须提供类型标识

完整类型定义

// Ping - 心跳请求
public record Ping(String type) implements WsPayload {
public static final String TYPE = "ping";
public static Ping create() {
return new Ping(TYPE);
}
}

// Pong - 心跳响应
public record Pong(String type) implements WsPayload {
public static final String TYPE = "pong";
public static Pong create() {
return new Pong(TYPE);
}
}

消息类型常量表

类型常量方向用途
Ping.TYPE"ping"S→C心跳请求
Pong.TYPE"pong"C→S心跳响应

状态管理

NetworkStatus 枚举

public enum NetworkStatus {
OFFLINE("&4📱"), // 离线
CONNECTING("&e📱"), // 连接中
ONLINE("&2📱"), // 在线
LAGGING("&6📱"); // 延迟
}

状态转换图:

状态转换规则:

当前状态事件新状态
OFFLINEconnect()CONNECTING
CONNECTINGonOpenONLINE
CONNECTINGonFailureCONNECTING (持续重试)
ONLINEonClosed/onFailureCONNECTING (自动重连)
ONLINE心跳超时LAGGING
LAGGING心跳恢复ONLINE
任意disconnect()OFFLINE

错误处理

错误类型

错误处理方式
连接失败指数退避重连
消息解析失败忽略该消息,记录日志
监听器异常捕获异常,不影响其他监听器
握手 401/403尝试 /server/refresh,失败后才重建服务器认证

日志级别

级别场景
INFO连接成功、重连调度、正常断开
WARNING连接失败、消息解析错误
SEVERE初始化失败等不可恢复错误

2026-03-18 Update: kick_player

Packet

Direction: Server -> Client

{
"type": "kick_player",
"playerId": 12345,
"message": "&cYou have been banned.\n&7Reason: &fabusive chat"
}

Fields:

FieldTypeDescription
typeStringfixed value kick_player
playerIdinttarget RhythMC player uid on the current game server
messageStringfinal kick text shown to the player

Client behavior

  • Resolve playerId to the active RhyPlayer.
  • If that player is online, immediately kick the Bukkit player with message.
  • Intended for admin ban and other forced disconnect scenarios.

Binding requirement

  • kick_player depends on the player already being bound to the current server WebSocket.
  • The client therefore now sends resume_session after successful login sync, not only after reconnect.
  • resume_session 使用玩家短期 access token;服务端会同时校验该玩家 access token 属于当前 WebSocket 已绑定的 server session。
  • 客户端还会按 config.yml -> net.player-session-refresh-interval-seconds 周期性重发 resume_session 相关玩家状态同步,用于在玩家空闲时持续续期短期 access token;默认 240 秒一次。

2026-03-18 Update: Cross-Server Chat And Private Messaging

Client entry points

  • Global shout uses configurable aliases from config.yml -> chat.global-command-aliases.
  • The default alias is hh, so /hh <message> sends a cross-server global chat message.
  • Cross-server private messaging uses /pm <player> <message>.
  • These chat commands are registered through ACF, so the client can see them as valid commands and request tab completion for /pm targets.
  • Chat delivery is routed to currently bound server WebSocket sessions; PM falls back to TARGET_OFFLINE if the target session is no longer websocket-bound.

Packet: global_chat_request

Direction: Client -> Server

{
"type": "global_chat_request",
"playerId": 12345,
"accessToken": "player-access-token",
"message": "大家晚上好"
}

Fields:

FieldTypeDescription
typeStringfixed value global_chat_request
playerIdintsender RhythMC uid
accessTokenStringcurrent player access token, used for auth and server-session binding
messageStringfinal chat content after local trimming

Packet: private_message_request

Direction: Client -> Server

{
"type": "private_message_request",
"playerId": 12345,
"accessToken": "player-access-token",
"target": "Frkovo",
"message": "来一把吗?"
}

Fields:

FieldTypeDescription
typeStringfixed value private_message_request
playerIdintsender RhythMC uid
accessTokenStringcurrent player access token, used for auth
targetStringtarget player username or numeric uid
messageStringPM content after local trimming

Packet: chat_message

Direction: Server -> Client

{
"type": "chat_message",
"channel": "global",
"outgoing": false,
"sender": {
"uid": 12345,
"username": "Frkovo",
"displayName": "&bFrkovo",
"aptitude": 12840,
"wattHour": 952700,
"currentServerName": "CN-East-01",
"prefix": {
"key": "rhythmc:title.beta",
"text": "&6[Beta]",
"description": [
"参与封测的玩家",
"感谢你的陪伴"
],
"rarity": "Legendary",
"position": true
},
"suffix": null
},
"target": null,
"message": "大家晚上好",
"timestamp": 1760000000000
}

Fields:

FieldTypeDescription
typeStringfixed value chat_message
channelStringglobal or private
outgoingbooleanonly meaningful for private messages; true for the local sender copy
senderChatPlayerInfosender profile used for rendering name / hover
targetChatPlayerInfo?target profile for PM packets
messageStringplain chat content
timestamplongserver-generated unix millis

Packet: chat_error

Direction: Server -> Client

{
"type": "chat_error",
"playerId": 12345,
"channel": "global",
"code": "RATE_LIMITED",
"target": null,
"retryAfterSeconds": 7
}

Fields:

FieldTypeDescription
typeStringfixed value chat_error
playerIdintlocal player that should receive the error
channelStringglobal or private
codeStringone of AUTH_REQUIRED, INVALID_MESSAGE, RATE_LIMITED, SELF_TARGET, TARGET_NOT_FOUND, TARGET_OFFLINE
targetString?target ref when the error is PM-related
retryAfterSecondsintretry delay for rate-limit responses

Rendering rules on the client

  • The client renders incoming chat through Adventure components.
  • Title/prefix/suffix blocks show hover text built from collection description and rarity.
  • The player-name block shows hover text with display name, username, uid, aptitude, watt-hour and current server.
  • Clicking the player-name block suggests /pm <username> .

Server-side behavior

  • Global chat is rate-limited on the backend only.
  • The limit is fixed: one global message per player every 10 seconds.
  • This limit is intentionally not configurable.
  • Delivery depends on the player already being rebound to the current WebSocket via resume_session.