Skip to content

Commit b94719e

Browse files
[1.4]新增超时fallback服务器
1 parent e91f5fe commit b94719e

5 files changed

Lines changed: 302 additions & 3 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>ict.minesunshineone</groupId>
77
<artifactId>simpletransfer</artifactId>
8-
<version>1.3</version>
8+
<version>1.4</version>
99
<packaging>jar</packaging>
1010

1111
<name>SimpleTransfer</name>

src/main/java/ict/minesunshineone/simpleTransfer/SimpleTransfer.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import com.velocitypowered.api.event.Subscribe;
55
import com.velocitypowered.api.event.connection.DisconnectEvent;
66
import com.velocitypowered.api.event.connection.PostLoginEvent;
7+
import com.velocitypowered.api.event.player.KickedFromServerEvent;
8+
import com.velocitypowered.api.event.player.ServerConnectedEvent;
79
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
810
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
911
import com.velocitypowered.api.plugin.Plugin;
1012
import com.velocitypowered.api.plugin.annotation.DataDirectory;
1113
import com.velocitypowered.api.proxy.Player;
1214
import com.velocitypowered.api.proxy.ProxyServer;
15+
import com.velocitypowered.api.proxy.server.RegisteredServer;
1316
import ict.minesunshineone.simpleTransfer.command.CommandRegistry;
1417
import ict.minesunshineone.simpleTransfer.config.ConfigManager;
1518
import ict.minesunshineone.simpleTransfer.connection.PlayerConnectionManager;
@@ -23,6 +26,7 @@
2326
import java.util.ArrayList;
2427
import java.util.Collection;
2528
import java.util.List;
29+
import java.util.UUID;
2630
import java.util.concurrent.TimeUnit;
2731

2832
@Plugin(
@@ -189,6 +193,90 @@ public void onPlayerDisconnect(DisconnectEvent event) {
189193

190194
// 日志关于玩家断开连接的信息已被移除(冗余),若需调试可在连接管理器中启用调试日志
191195
}
196+
197+
/**
198+
* 监听玩家连接到服务器的事件
199+
* 用于超时保护:记录成功连接的服务器,清理失败记录
200+
*/
201+
@Subscribe
202+
public void onServerConnected(ServerConnectedEvent event) {
203+
if (!configManager.isTransferTimeoutProtectionEnabled()) {
204+
return;
205+
}
206+
207+
Player player = event.getPlayer();
208+
RegisteredServer server = event.getServer();
209+
UUID playerUuid = player.getUniqueId();
210+
String serverName = server.getServerInfo().getName();
211+
212+
// 成功连接到服务器后,通知连接管理器清理该服务器的失败记录
213+
connectionManager.onServerConnected(playerUuid, serverName);
214+
215+
if (configManager.isDebugMode()) {
216+
logger.info("[超时保护] 玩家 {} 成功连接到服务器 {}", player.getUsername(), serverName);
217+
}
218+
}
219+
220+
/**
221+
* 监听玩家被服务器踢出的事件
222+
* 用于超时保护:检测连接超时并尝试转移到备用服务器
223+
*/
224+
@Subscribe
225+
public void onKickedFromServer(KickedFromServerEvent event) {
226+
if (!configManager.isTransferTimeoutProtectionEnabled()) {
227+
return;
228+
}
229+
230+
Player player = event.getPlayer();
231+
RegisteredServer kickedFrom = event.getServer();
232+
UUID playerUuid = player.getUniqueId();
233+
String serverName = kickedFrom.getServerInfo().getName();
234+
235+
// 检查是否是连接超时导致的踢出
236+
Component kickReason = event.getServerKickReason().orElse(null);
237+
if (kickReason != null) {
238+
String reasonText = kickReason.toString().toLowerCase();
239+
240+
// 检测常见的超时相关关键词
241+
boolean isTimeout = reasonText.contains("timeout") ||
242+
reasonText.contains("timed out") ||
243+
reasonText.contains("connection") ||
244+
reasonText.contains("disconnect");
245+
246+
if (isTimeout) {
247+
// 记录服务器连接失败
248+
connectionManager.recordServerFailure(playerUuid, serverName);
249+
250+
if (configManager.isDebugMode()) {
251+
logger.warn("[超时保护] 玩家 {} 连接服务器 {} 超时: {}",
252+
player.getUsername(), serverName, reasonText);
253+
}
254+
255+
// 尝试转移到安全服务器
256+
String safeServer = connectionManager.getSafeServer(playerUuid);
257+
if (safeServer != null && configManager.getTransferServers().containsKey(safeServer)) {
258+
ServerInfo serverInfo = configManager.getTransferServers().get(safeServer);
259+
260+
player.sendMessage(Component.text()
261+
.append(Component.text("[ST] ", NamedTextColor.GOLD))
262+
.append(Component.text("检测到连接超时,正在转移到备用服务器: ", NamedTextColor.YELLOW))
263+
.append(Component.text(safeServer, NamedTextColor.GREEN))
264+
.build());
265+
266+
// 使用 KickedFromServerEvent 的结果重定向功能
267+
server.getServer(serverInfo.getHost())
268+
.ifPresent(registeredServer -> {
269+
event.setResult(KickedFromServerEvent.RedirectPlayer.create(registeredServer));
270+
271+
if (configManager.isDebugMode()) {
272+
logger.info("[超时保护] 已将玩家 {} 重定向到安全服务器 {}",
273+
player.getUsername(), safeServer);
274+
}
275+
});
276+
}
277+
}
278+
}
279+
}
192280

193281
/**
194282
* 重载配置和命令(热重载功能)

src/main/java/ict/minesunshineone/simpleTransfer/config/ConfigManager.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ public class ConfigManager {
3333
private String rulesFile = "rules.conf";
3434
private boolean rulesEnabled = true;
3535

36+
// 转移超时保护配置
37+
private boolean transferTimeoutProtectionEnabled = true;
38+
private String fallbackServer = "bgp";
39+
private int transferTimeout = 4000;
40+
private int failureMemoryDuration = 300;
41+
3642
public ConfigManager(Logger logger, Path dataDirectory) {
3743
this.logger = logger;
3844
this.dataDirectory = dataDirectory;
@@ -104,6 +110,22 @@ public void loadConfig(Path configPath) {
104110
}
105111
}
106112

113+
// 读取转移超时保护配置
114+
ConfigurationNode timeoutProtection = settings.node("transfer_timeout_protection");
115+
if (!timeoutProtection.virtual()) {
116+
transferTimeoutProtectionEnabled = timeoutProtection.node("enabled").getBoolean(true);
117+
fallbackServer = timeoutProtection.node("fallback_server").getString("default.bgp");
118+
transferTimeout = timeoutProtection.node("timeout").getInt(4000);
119+
failureMemoryDuration = timeoutProtection.node("failure_memory_duration").getInt(300);
120+
121+
if (debugMode) {
122+
logger.debug("转移超时保护: {}", transferTimeoutProtectionEnabled ? "已启用" : "已禁用");
123+
logger.debug(" - 安全服务器: {}", fallbackServer);
124+
logger.debug(" - 超时时间: {}ms", transferTimeout);
125+
logger.debug(" - 失败记忆时长: {}秒", failureMemoryDuration);
126+
}
127+
}
128+
107129
// 在读取完调试模式后输出日志
108130
if (debugMode) {
109131
logger.debug("调试模式: 已启用");
@@ -363,4 +385,36 @@ public boolean isRulesEnabled() {
363385
public List<TransferRule> getTransferRules() {
364386
return new ArrayList<>(transferRules);
365387
}
388+
389+
// 转移超时保护相关getter方法
390+
391+
public boolean isTransferTimeoutProtectionEnabled() {
392+
return transferTimeoutProtectionEnabled;
393+
}
394+
395+
public String getFallbackServer() {
396+
return fallbackServer;
397+
}
398+
399+
/**
400+
* 根据 group.server 格式获取服务器信息
401+
* @param serverKey 格式: "groupName.serverName" 例如 "default.bgp"
402+
* @return 对应的 ServerInfo,如果未找到返回 null
403+
*/
404+
public ServerInfo getServerByKey(String serverKey) {
405+
if (serverKey == null) {
406+
return null;
407+
}
408+
409+
// 格式必须为: group.server
410+
return servers.get(serverKey);
411+
}
412+
413+
public int getTransferTimeout() {
414+
return transferTimeout;
415+
}
416+
417+
public int getFailureMemoryDuration() {
418+
return failureMemoryDuration;
419+
}
366420
}

src/main/java/ict/minesunshineone/simpleTransfer/connection/PlayerConnectionManager.java

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ public class PlayerConnectionManager {
5757
// 玩家最后一次收到延迟警告的时间 UUID -> 时间戳(毫秒)
5858
private final Map<UUID, Long> lastPingWarningTime = new ConcurrentHashMap<>();
5959

60+
// 服务器连接失败记录 玩家UUID -> (服务器名 -> 失败时间戳)
61+
private final Map<UUID, Map<String, Long>> serverFailureRecord = new ConcurrentHashMap<>();
62+
6063
// 玩家登录时间 UUID -> 时间戳(毫秒)
6164
private final Map<UUID, Long> playerLoginTime = new ConcurrentHashMap<>();
6265

@@ -190,6 +193,13 @@ private void setDefaultRouteForNewPlayer(Player player) {
190193
if (targetRoute != null) {
191194
// 检查目标线路是否存在
192195
if (configManager.getTransferServers().containsKey(targetRoute)) {
196+
// 检查是否最近连接失败过
197+
if (isServerRecentlyFailed(uuid, targetRoute)) {
198+
logger.warn("玩家 {} 匹配规则 [{}] 但目标线路 {} 最近连接失败,跳过此规则",
199+
player.getUsername(), rule.getName(), targetRoute);
200+
continue; // 尝试下一个规则
201+
}
202+
193203
lastRoutes.put(uuid, targetRoute);
194204
logDebug("玩家 {} 匹配规则 [{}],分配 {} 线路",
195205
player.getUsername(), rule.getName(), targetRoute.toUpperCase());
@@ -206,8 +216,15 @@ private void setDefaultRouteForNewPlayer(Player player) {
206216
}
207217
}
208218

209-
// 如果没有规则匹配,不自动分配线路(由玩家手动选择)
210-
logDebug("玩家 {} 未匹配任何规则,不自动分配线路", player.getUsername());
219+
// 如果没有规则匹配或所有匹配的服务器都失败过,使用安全服务器
220+
if (configManager.isTransferTimeoutProtectionEnabled()) {
221+
String safeServer = getSafeServer(uuid);
222+
logDebug("玩家 {} 未匹配任何有效规则,使用安全服务器: {}", player.getUsername(), safeServer);
223+
lastRoutes.put(uuid, safeServer);
224+
} else {
225+
// 如果没有规则匹配,不自动分配线路(由玩家手动选择)
226+
logDebug("玩家 {} 未匹配任何规则,不自动分配线路", player.getUsername());
227+
}
211228
}).delay(1, TimeUnit.SECONDS).schedule();
212229
}
213230

@@ -328,6 +345,9 @@ public void onPlayerDisconnect(UUID uuid) {
328345
playerLoginTime.remove(uuid);
329346
lastPingWarningTime.remove(uuid);
330347

348+
// 清理服务器失败记录
349+
cleanupFailureRecords(uuid);
350+
331351
// 注意:不清理 lastAutoTransferTime 和 pingMonitorEnabled,让设置在重连后依然生效
332352
}
333353

@@ -1165,4 +1185,113 @@ private static class IPInfo {
11651185
String isp;
11661186
boolean isForeign; // 是否为海外玩家(用于延迟监控豁免)
11671187
}
1188+
1189+
// ============================================================
1190+
// 转移超时保护相关方法
1191+
// ============================================================
1192+
1193+
/**
1194+
* 记录服务器连接失败
1195+
* @param playerUuid 玩家UUID
1196+
* @param serverName 服务器名称
1197+
*/
1198+
public void recordServerFailure(UUID playerUuid, String serverName) {
1199+
if (!configManager.isTransferTimeoutProtectionEnabled()) {
1200+
return;
1201+
}
1202+
1203+
Map<String, Long> failures = serverFailureRecord.computeIfAbsent(playerUuid, k -> new ConcurrentHashMap<>());
1204+
failures.put(serverName, System.currentTimeMillis());
1205+
1206+
logDebug("记录玩家 {} 连接服务器 {} 失败", playerUuid, serverName);
1207+
}
1208+
1209+
/**
1210+
* 玩家成功连接到服务器时调用
1211+
* 清除该服务器的失败记录
1212+
* @param playerUuid 玩家UUID
1213+
* @param serverName 服务器名称
1214+
*/
1215+
public void onServerConnected(UUID playerUuid, String serverName) {
1216+
if (!configManager.isTransferTimeoutProtectionEnabled()) {
1217+
return;
1218+
}
1219+
1220+
Map<String, Long> failures = serverFailureRecord.get(playerUuid);
1221+
if (failures != null) {
1222+
Long removed = failures.remove(serverName);
1223+
if (removed != null) {
1224+
logDebug("清除玩家 {} 对服务器 {} 的失败记录", playerUuid, serverName);
1225+
}
1226+
}
1227+
}
1228+
1229+
/**
1230+
* 检查服务器是否最近连接失败过
1231+
* @param playerUuid 玩家UUID
1232+
* @param serverName 服务器名称
1233+
* @return 是否在失败记忆时间内
1234+
*/
1235+
public boolean isServerRecentlyFailed(UUID playerUuid, String serverName) {
1236+
if (!configManager.isTransferTimeoutProtectionEnabled()) {
1237+
return false;
1238+
}
1239+
1240+
Map<String, Long> failures = serverFailureRecord.get(playerUuid);
1241+
if (failures == null) {
1242+
return false;
1243+
}
1244+
1245+
Long failureTime = failures.get(serverName);
1246+
if (failureTime == null) {
1247+
return false;
1248+
}
1249+
1250+
long memoryDuration = configManager.getFailureMemoryDuration() * 1000L;
1251+
long elapsed = System.currentTimeMillis() - failureTime;
1252+
1253+
if (elapsed > memoryDuration) {
1254+
// 超过记忆时间,清除记录
1255+
failures.remove(serverName);
1256+
return false;
1257+
}
1258+
1259+
return true;
1260+
}
1261+
1262+
/**
1263+
* 清理玩家的失败记录(玩家离开时调用)
1264+
* @param playerUuid 玩家UUID
1265+
*/
1266+
public void cleanupFailureRecords(UUID playerUuid) {
1267+
serverFailureRecord.remove(playerUuid);
1268+
}
1269+
1270+
/**
1271+
* 获取安全服务器(排除最近失败的服务器)
1272+
* @param playerUuid 玩家UUID
1273+
* @return 安全服务器名称,如果都失败则返回配置的fallback服务器
1274+
*/
1275+
public String getSafeServer(UUID playerUuid) {
1276+
String fallback = configManager.getFallbackServer();
1277+
1278+
// 如果fallback服务器没有失败过,直接返回
1279+
if (!isServerRecentlyFailed(playerUuid, fallback)) {
1280+
return fallback;
1281+
}
1282+
1283+
// 尝试找一个没有失败过的服务器
1284+
for (String serverName : configManager.getServers().keySet()) {
1285+
if (!isServerRecentlyFailed(playerUuid, serverName)) {
1286+
logDebug("玩家 {} 的fallback服务器 {} 最近失败过,使用备选服务器 {}",
1287+
playerUuid, fallback, serverName);
1288+
return serverName;
1289+
}
1290+
}
1291+
1292+
// 如果所有服务器都失败过,清空失败记录并返回fallback
1293+
logDebug("玩家 {} 所有服务器都失败过,清空失败记录", playerUuid);
1294+
cleanupFailureRecords(playerUuid);
1295+
return fallback;
1296+
}
11681297
}

src/main/resources/config/config.conf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,34 @@ settings {
2929
# 默认: rules.conf (与 config.conf 在同一目录)
3030
# 规则文件包含所有的自动转移规则,支持热重载
3131
rules_file = "rules.conf"
32+
33+
# 转移超时保护
34+
# 当玩家无法连接到转移目标服务器时的容错机制
35+
transfer_timeout_protection {
36+
# 是否启用转移超时保护
37+
# 启用后,如果玩家连接目标服务器失败,会自动转移到安全服务器
38+
enabled = true
39+
40+
# 安全服务器(兜底服务器)- 格式:权限组.线路名称
41+
# 当玩家无法连接到任何推荐线路时,将转移到此服务器
42+
# 示例:default.bgp 表示 permission_groups.default.servers.bgp
43+
# 格式说明:
44+
# - default.bgp → permission_groups 中 default 组的 bgp 服务器
45+
# - vip.vip → permission_groups 中 vip 组的 vip 服务器
46+
fallback_server = "default.bgp"
47+
48+
# 连接超时时间(毫秒)
49+
# 等待玩家连接到目标服务器的最长时间
50+
# 超过此时间未成功连接,将触发超时保护
51+
# 注意:此值应小于 Velocity 的 connection-timeout (默认 5000ms)
52+
timeout = 4000
53+
54+
# 失败记忆时间(秒)- 针对每个玩家
55+
# 记住某个玩家对某个服务器连接失败的时间
56+
# 在此时间内,该玩家不会再次尝试转移到这个失败的服务器
57+
# 避免玩家陷入"推荐服务器→连接失败→再次推荐→再次失败"的死循环
58+
failure_memory_duration = 300
59+
}
3260
}
3361

3462
# ============================================================

0 commit comments

Comments
 (0)