- ProviderWithOnline 和 Online 使用说明文档
ProviderWithOnline 和 Online 使用说明文档
文档目标
分析 ProviderWithOnline.java 和 Online.java 的设计意图、用法、核心接口,并输出使用说明文档。
分析结果
一、ProviderWithOnline.java - 在线服务提供者基类
1.1 设计意图
ProviderWithOnline 是一个继承自 ProviderImplement 的服务提供者基类,专门用于管理在线玩家状态的服务。它的核心设计目标是:
- Online实例管理:管理一个默认的Online实例和多个命名的OnlineSet(在线集合)
- 负载监控:集成ProviderLoadWithOnline实现负载监控和报告
- 断线处理:统一处理连接断开事件,维护玩家在线状态
1.2 使用场景
场景1:MMORPG游戏服务器
- 需求:一个大型MMORPG需要支持数万玩家同时在线,需要实时管理玩家状态,处理登录、登出、断线重连
- 为什么使用:提供了完整的在线状态管理框架,自动处理玩家会话,不需要开发者自己维护复杂的连接状态
- 实现方式:继承ProviderWithOnline,创建一个默认Online管理游戏主流程
场景2:多客户端/多平台同时在线管理
- 需求:游戏支持多个客户端同时在线(PC端、移动端、网页端),或者同一账号使用不同的应用(游戏App、独立聊天App、社区App),每个客户端需要独立的在线状态管理
- 为什么使用:支持创建多个OnlineSet,每个OnlineSet独立管理不同客户端的在线状态,实现跨客户端通信和状态同步
- 实现方式:
// 创建多个OnlineSet对应不同的客户端平台
provider.create(app, "PC", "Mobile", "Web", "ChatApp");
// 示例:玩家同时在PC端和移动端登录
// - PC OnlineSet:管理PC端的在线状态、配置、数据
// - Mobile OnlineSet:管理移动端的在线状态、配置、数据
// - 两个OnlineSet独立管理,互不干扰
// - 可以互相通信(如PC端通知移动端上线)
// - 可以数据同步(如从PC端同步进度到移动端)
// 示例:游戏App和独立聊天App同时在线
// - GameApp OnlineSet:管理游戏内的在线状态
// - ChatApp OnlineSet:管理独立聊天App的在线状态
// - 玩家可以在游戏内,聊天App离线
// - 或者聊天App在线,游戏App离线(独立聊天客户端)
// - 两者可以互相通信(游戏内聊天同步到聊天App)
场景3:服务器负载均衡
- 需求:需要根据在线人数动态调整负载,实现跨服务器的负载调度
- 为什么使用:集成了ProviderLoadWithOnline,自动报告和监控每个服务器的在线人数和负载
- 实现方式:通过getLoad()获取负载信息,配合负载均衡策略使用
场景4:分布式游戏世界
- 需求:游戏世界分布在多个服务器上,需要统一管理玩家的全局在线状态
- 为什么使用:OnlineShared数据在全局共享,可以追踪玩家在任何服务器的状态
- 实现方式:通过OnlineShared查询玩家全局状态,实现跨服功能
1.3 核心成员
protected Online online; // 默认的Online实例
private ProviderLoadWithOnline load; // 负载监控
protected final HashMap<String, Online> onlineSetMap; // 所有Online集合
1.3 核心接口
获取Online实例
getOnline(): 获取默认Online实例getOnline(String name): 获取指定名称的Online实例foreachOnline(Consumer<Online> consumer): 遍历所有Online实例
生命周期管理
create(AppBase app, String... names): 创建默认Online和指定名称的OnlineSetstart(): 启动Online和负载监控stop(): 停止所有Online服务
事件处理
ProcessLinkBroken(LinkBroken p): 处理连接断开事件
1.4 使用方式
// 1. 继承ProviderWithOnline
public class GameProvider extends ProviderWithOnline {
// 自定义实现
}
// 2. 在App.Start流程中创建
provider.create(app, "OnlineSetName1", "OnlineSetName2");
// 3. 启动服务
provider.start();
// 4. 获取Online实例
Online defaultOnline = provider.getOnline();
Online namedOnline = provider.getOnline("OnlineSetName1");
二、Online.java - 在线状态管理核心类
2.1 设计意图
Online 是整个在线状态管理的核心类,负责:
- 玩家在线状态管理:登录、登出、断线重连、超时处理
- 本地数据管理:管理角色在当前服务器进程的本地数据(Local)
- 协议发送:提供单发、群发、广播、可靠通知等多种协议发送方式
- 多OnlineSet支持:支持创建多个独立的在线集合,实现业务隔离
- 热更新支持:支持热更新场景下的数据迁移和恢复
- 负载均衡转发:支持跨服务器的请求转发(Transmit)
2.2 使用场景
场景1:玩家登录登出管理
- 需求:玩家登录游戏时需要初始化数据,登出时需要保存数据并清理资源
- 为什么使用:提供完整的登录登出流程管理,自动处理会话状态、版本控制、断线重连
- 实现方式:
// 注册登录事件
online.getLoginEvents().add((sender, arg) -> {
// 初始化玩家数据
initPlayerData(arg.roleId);
// 加载玩家背包
loadInventory(arg.roleId);
// 通知好友玩家上线
notifyFriendsOnline(arg.roleId);
return 0;
});
// 注册登出事件
online.getLogoutEvents().add((sender, arg) -> {
// 保存玩家数据
savePlayerData(arg.roleId);
// 清理缓存
clearCache(arg.roleId);
// 通知好友玩家下线
notifyFriendsOffline(arg.roleId);
return 0;
});
场景2:断线重连与延迟登出
- 需求:玩家网络波动导致短暂断线,需要自动恢复会话,避免数据丢失和游戏体验中断
- 为什么使用:提供延迟登出机制,断线后等待一段时间才真正登出,期间重连可恢复会话
- 实现方式:
// 配置延迟登出时间(如30秒)
// 在Zeze配置中:OnlineLogoutDelay=30000
// 注册断线事件
online.getLinkBrokenEvents().add((sender, arg) -> {
// 保存关键数据(保险起见)
saveCriticalData(arg.roleId);
// 通知客户端连接断开(显示重连按钮)
notifyClientLinkBroken(arg.roleId);
// 不清理数据,等待可能的重连
return 0;
});
// 注册重连事件(ReLogin)
online.getReloginEvents().add((sender, arg) -> {
// 恢复会话
restoreSession(arg.roleId);
// 同步断线期间的数据变更
syncDataDuringDisconnect(arg.roleId);
return 0;
});
场景3:向玩家发送协议
- 需求:需要向玩家发送各种游戏通知、奖励、更新等
- 为什么使用:提供多种发送方式(单发、群发、广播),支持事务中发送,支持可靠通知
- 实现方式:
// 场景3.1:发送奖励通知(普通发送)
public void giveReward(long roleId, Reward reward) {
// 发放奖励
addRewardToInventory(roleId, reward);
// 事务提交后发送通知
Transaction.whileCommit(() -> {
online.send(roleId, new RewardNotify(reward));
});
}
// 场景3.2:发送重要通知(可靠通知,必须送达)
public void sendImportantAnnouncement(long roleId, String announcement) {
// 添加可靠通知标记
online.addReliableNotifyMark(roleId, "SystemAnnouncement");
// 发送可靠通知
online.sendReliableNotify(roleId, "SystemAnnouncement",
new AnnouncementProtocol(announcement));
}
// 场景3.3:广播服务器公告(所有在线玩家)
public void broadcastServerNotice(String notice) {
online.broadcast(new ServerNotice(notice));
}
// 场景3.4:群发给公会成员(多个玩家)
public void sendToGuildMembers(long guildId, GuildMessage msg) {
List<Long> members = getGuildMembers(guildId);
online.send(members, new GuildMessageNotify(msg));
}
场景4:本地数据管理
- 需求:某些数据只需要在单个服务器进程中使用,不需要全局共享,用于提高性能
- 为什么使用:提供Local数据存储,仅本进程可见,减少网络开销和数据库压力
- 实现方式:
// 场景4.1:缓存战斗数据(仅战斗服务器需要)
public void startBattle(long roleId, BattleInstance battle) {
// 保存战斗数据到本地(快,不跨进程)
online.setLocalBean(roleId, "CurrentBattle", battle);
// 战斗过程中频繁访问,不需要全局同步
// ...
}
public BattleInstance getBattle(long roleId) {
// 从本地获取(快速)
return online.getLocalBean(roleId, "CurrentBattle", BattleInstance.class);
}
// 场景4.2:临时会话数据(聊天、交易等)
public void startTrade(long roleId1, long roleId2, TradeSession session) {
// 保存交易会话数据
online.setLocalBean(roleId1, "TradeSession", session);
online.setLocalBean(roleId2, "TradeSession", session);
}
// 场景4.3:玩家登出时清理
online.getLocalRemoveEvents().add((sender, arg) -> {
// 清理本地数据
var battle = online.getLocalBean(arg.roleId, "CurrentBattle", BattleInstance.class);
if (battle != null) {
endBattle(arg.roleId, battle);
}
return 0;
});
场景5:跨服务器请求转发
- 需求:需要查询或操作在另一个服务器上的玩家数据
- 为什么使用:提供Transmit机制,自动找到目标玩家所在服务器并转发请求
- 实现方式:
// 场景5.1:跨服好友查询
public class FriendService {
public void init() {
// 注册跨服查询处理器
online.getTransmitActions().put("QueryFriendProfile", (sender, target, param) -> {
// 查询目标玩家的资料
var profile = getProfile(target);
// 返回给查询发起者
online.send(sender, new FriendProfileResponse(target, profile));
return 0;
});
}
// 查询好友资料(好友可能在其他服务器)
public void queryFriendProfile(long requester, long friendRoleId) {
// Transmit会自动找到friendRoleId所在的服务器并转发请求
online.transmit(requester, "QueryFriendProfile", friendRoleId);
}
}
// 场景5.2:跨服邮件发送
online.getTransmitActions().put("SendMail", (sender, target, param) -> {
var mail = decodeMail(param);
deliverMail(target, mail);
return 0;
});
// 发送邮件给离线玩家(无论他在哪台服务器)
public void sendMail(long sender, long targetRoleId, Mail mail) {
online.transmit(sender, "SendMail", targetRoleId, encodeMail(mail));
}
场景6:多客户端/多平台同时在线
- 需求:玩家可能同时在多个客户端登录(PC端、移动端、网页端),或者在同一账号下使用不同的应用(游戏App、聊天App、社区App),需要独立的在线状态管理
- 为什么使用:支持创建多个OnlineSet,每个OnlineSet独立管理不同客户端的在线状态,互不干扰
- 实现方式:
// 创建多个OnlineSet对应不同的客户端
provider.create(app, "PC", "Mobile", "Web", "ChatApp");
// 场景6.1:PC端和移动端同时在线
public class MultiPlatformGame {
private Online pcOnline;
private Online mobileOnline;
public void init() {
pcOnline = provider.getOnline("PC");
mobileOnline = provider.getOnline("Mobile");
// PC端登录事件
pcOnline.getLoginEvents().add((sender, arg) -> {
// 加载PC端配置
loadPCSettings(arg.roleId);
// 同步数据到PC端(从移动端同步过来)
syncDataFromMobile(arg.roleId);
// 通知移动端:PC端上线了
notifyCrossPlatform(arg.roleId, "PC", "Online");
return 0;
});
// 移动端登录事件
mobileOnline.getLoginEvents().add((sender, arg) -> {
// 加载移动端配置
loadMobileSettings(arg.roleId);
// 同步数据到移动端(从PC端同步过来)
syncDataFromPC(arg.roleId);
// 通知PC端:移动端上线了
notifyCrossPlatform(arg.roleId, "Mobile", "Online");
return 0;
});
// PC端登出事件
pcOnline.getLogoutEvents().add((sender, arg) -> {
// 保存PC端数据到服务器
savePCData(arg.roleId);
// 通知移动端:PC端下线了
notifyCrossPlatform(arg.roleId, "PC", "Offline");
return 0;
});
}
// 跨平台数据同步
private void syncDataFromMobile(long roleId) {
var mobileOnline = provider.getOnline("Mobile");
if (mobileOnline.isLogin(roleId)) {
// 从移动端获取最新数据
var mobileData = mobileOnline.getUserData(roleId, GameData.class);
// 同步到PC端
pcOnline.setUserData(roleId, mobileData);
}
}
}
// 场景6.2:游戏App和独立聊天App同时在线
public class GameAndChatApps {
private Online gameOnline;
private Online chatOnline;
public void init() {
gameOnline = provider.getOnline("GameApp");
chatOnline = provider.getOnline("ChatApp");
// 游戏App登录
gameOnline.getLoginEvents().add((sender, arg) -> {
// 初始化游戏数据
initGameData(arg.roleId);
// 如果聊天App也在线,通知游戏内好友状态
if (chatOnline.isLogin(arg.roleId)) {
syncChatStatusToGame(arg.roleId);
}
return 0;
});
// 聊天App登录(独立App)
chatOnline.getLoginEvents().add((sender, arg) -> {
// 加载聊天记录和好友列表
loadChatData(arg.roleId);
// 如果游戏App也在线,显示游戏内状态
if (gameOnline.isLogin(arg.roleId)) {
showInGameStatus(arg.roleId);
}
return 0;
});
// 跨App消息通知
chatOnline.getLinkBrokenEvents().add((sender, arg) -> {
// 聊天App断线,在游戏内显示通知
if (gameOnline.isLogin(arg.roleId)) {
gameOnline.send(arg.roleId, new ChatAppDisconnectedNotice());
}
return 0;
});
}
// 从游戏App发送聊天消息到聊天App
public void sendGameChatMessage(long roleId, String message) {
var chatOnline = provider.getOnline("ChatApp");
if (chatOnline.isLogin(roleId)) {
// 在聊天App中显示游戏内聊天
chatOnline.send(roleId, new GameChatMessage(message));
}
}
}
// 场景6.3:网页端临时查看,不影响PC端/移动端
public class WebViewer {
private Online webOnline;
private Online pcOnline;
private Online mobileOnline;
public void handleWebLogin(long roleId) {
webOnline = provider.getOnline("Web");
// 网页端登录(只读模式)
webOnline.getLoginEvents().add((sender, arg) -> {
// 仅加载只读数据
loadReadOnlyData(arg.roleId);
// 查询主设备状态
String primaryDevice = null;
if (pcOnline.isLogin(arg.roleId)) {
primaryDevice = "PC";
} else if (mobileOnline.isLogin(arg.roleId)) {
primaryDevice = "Mobile";
}
// 通知网页端:这是临时查看会话
webOnline.send(arg.roleId, new WebViewerNotice(primaryDevice));
return 0;
});
// 网页端登出(不影响主设备)
webOnline.getLogoutEvents().add((sender, arg) -> {
// 清理临时数据即可
clearTempData(arg.roleId);
return 0;
});
}
}
// 场景6.4:跨设备踢人逻辑
public class MultiDeviceKick {
private Online pcOnline;
private Online mobileOnline;
// PC端登录时,如果移动端已登录,询问是否踢掉移动端
public void onPCLogin(long roleId) {
if (mobileOnline.isLogin(roleId)) {
// 通知移动端:PC端登录了
mobileOnline.send(roleId, new AnotherDeviceLogin("PC"));
// 可选:自动踢掉移动端
// mobileOnline.kick(roleId, BKick.ErrorDuplicateLogin, "PC端登录");
}
}
// 强制登出所有设备
public void kickAllDevices(long roleId, String reason) {
pcOnline.kick(roleId, BKick.ErrorCustom, reason);
mobileOnline.kick(roleId, BKick.ErrorCustom, reason);
}
}
场景7:热更新数据迁移
- 需求:游戏运行时需要热更新Bean类,需要迁移现有数据到新的Bean结构
- 为什么使用:提供热更新支持,自动迁移Local数据中的Bean
- 实现方式:
// 注册自定义Bean类型
Online.register(PlayerDataV1.class);
Online.register(PlayerDataV2.class);
// 热更新时执行数据迁移
online.upgrade(oldBean -> {
if (oldBean instanceof PlayerDataV1) {
var v1 = (PlayerDataV1)oldBean;
// 迁移到V2
var v2 = new PlayerDataV2();
v2.setLevel(v1.getLevel());
v2.setExp(v1.getExp());
// V2新增字段
v2.setNewField(defaultValue);
return v2;
}
return null; // 不迁移
});
场景8:负载均衡与服务器选择
- 需求:根据服务器在线人数和负载,动态分配玩家到不同服务器
- 为什么使用:提供负载查询接口,配合ProviderLoad实现负载均衡
- 实现方式:
// 选择负载最低的服务器
public int selectBestServer() {
int bestServerId = -1;
int minLoad = Integer.MAX_VALUE;
for (var server : providerApp.providerDirectService.providerByServerId.values()) {
var load = provider.getLoad().getOnlineLoad(server.getServerId());
if (load < minLoad) {
minLoad = load;
bestServerId = server.getServerId();
}
}
return bestServerId;
}
// 玩家登录时分配到负载低的服务器
public int assignServerForLogin(long roleId) {
int serverId = selectBestServer();
// 通知玩家连接到指定服务器
return serverId;
}
2.3 核心数据结构
Online系统使用5个核心数据表来管理玩家的在线状态和数据,每个表都有特定的设计意图和使用场景。
2.3.1 _tOnline(角色在线信息表)
设计意图 存储角色在当前服务器进程的在线信息,每个服务器进程有独立的副本,不与其他服务器共享。
为什么需要
- ServerId追踪:记录角色当前登录在哪个服务器进程上
- 可靠通知支持:管理可靠通知的标记和索引
- 本地数据隔离:每个服务器进程可以存储不同的本地数据
- 性能优化:不需要跨进程同步,访问速度快
存储内容
public class BOnline extends Bean {
private int serverId; // 当前所在服务器ID
private HashSet<String> reliableNotifyMark; // 可靠通知标记集合
private long reliableNotifyIndex; // 可靠通知索引
private long reliableNotifyConfirmIndex; // 已确认的可靠通知索引
private Bean userData; // 用户自定义数据(全局可见)
}
使用场景
场景1:查询角色所在服务器
// 查询角色在哪个服务器上
public int locateRoleServer(long roleId) {
var online = online.getOnline(roleId);
if (online != null) {
return online.getServerId();
}
return -1; // 不在线
}
// 用于跨服功能:如跨服组队时找到角色所在服务器
场景2:可靠通知管理
// 添加可靠通知标记(确保重要通知送达)
online.addReliableNotifyMark(roleId, "ImportantAnnouncement");
online.sendReliableNotify(roleId, "ImportantAnnouncement",
new MaintenanceNotice(server, time));
// 客户端确认后,自动删除已送达的通知
场景3:跨服务器请求定位
// 发送跨服请求前,先定位目标角色
public void sendCrossServerRequest(long requester, long target) {
var targetOnline = online.getOnline(target);
if (targetOnline == null) {
// 目标不在线
online.send(requester, new TargetOfflineResponse());
return;
}
if (targetOnline.getServerId() != getCurrentServerId()) {
// 目标在其他服务器,使用Transmit转发
online.transmit(requester, "QueryInfo", target);
} else {
// 目标在本地服务器,直接处理
processLocalRequest(requester, target);
}
}
数据库表名
- 表名:
Zeze.Game.Online.tOnline_[OnlineSetName] - 默认OnlineSet:
Zeze.Game.Online.tOnline_ - 命名OnlineSet:
Zeze.Game.Online.tOnline_Mobile - 特点:每个服务器进程有独立的本地缓存,不与其他服务器共享
2.3.2 _tOnlineShared(角色共享在线信息表)
设计意图 存储角色的全局共享在线信息,所有服务器进程都能访问,用于维护角色的全局在线状态和会话信息。
为什么需要
- 全局在线状态:所有服务器都能查询角色的在线状态
- 会话管理:管理角色的登录链接信息(LinkName、LinkSid)
- 版本控制:通过LoginVersion/LogoutVersion防止并发冲突
- 账号绑定:维护角色与账号的绑定关系
- 跨服协作:多个服务器需要协同管理同一个角色
存储内容
public class BOnlineShared extends Bean {
private String account; // 绑定的账号
private BLink link; // 登录链接信息(LinkName、LinkSid、State)
private long loginVersion; // 登录版本号(每次登录递增)
private long logoutVersion; // 登出版本号
private Bean userData; // 用户自定义数据(全局可见)
}
public class BLink extends Bean {
private String linkName; // 连接名称(如"Link-GameServer1")
private long linkSid; // 连接会话ID
private int state; // 状态:eOffline/eLogined/eLinkBroken
}
使用场景
场景1:全局在线状态查询
// 查询角色是否在线(全局)
public boolean isOnline(long roleId) {
var onlineShared = online.getOnlineShared(roleId);
return onlineShared != null
&& onlineShared.getLink().getState() == eLogined;
}
// 用于好友列表、公会成员列表等显示在线状态
场景2:防止重复登录
// Login流程中检查是否已登录
public long ProcessLoginRequest(Login rpc) {
var onlineShared = online.getOrAddOnlineShared(roleId);
var link = onlineShared.getLink();
// 如果已登录,踢掉旧连接
if (link.getState() == eLogined) {
providerApp.providerService.kick(
link.getLinkName(),
link.getLinkSid(),
BKick.ErrorDuplicateLogin,
"duplicate role login"
);
}
// 创建新登录
// ...
}
场景3:版本控制防止并发冲突
// Local数据带版本号,过期自动删除
public void processLoginVersion(long roleId) {
var onlineShared = online.getOnlineShared(roleId);
var local = online._tlocal.get(roleId);
// 版本不一致,说明是过期数据,删除
if (local != null
&& local.getLoginVersion() != onlineShared.getLoginVersion()) {
online._tlocal.remove(roleId);
}
}
场景4:断线重连恢复
// ReLogin时检查版本号,确保恢复正确的会话
public long ProcessReLoginRequest(ReLogin rpc) {
var onlineShared = online.getOrAddOnlineShared(roleId);
// 比对版本号,防止重连到错误的会话
long expectedVersion = rpc.Argument.getLoginVersion();
if (onlineShared.getLoginVersion() != expectedVersion) {
// 版本不匹配,拒绝重连
return errorCode(ResultCodeVersionMismatch);
}
// 恢复会话...
}
数据库表名
- 表名:
Zeze.Game.Online.tOnlineShared_[OnlineSetName] - 默认OnlineSet:
Zeze.Game.Online.tOnlineShared_ - 命名OnlineSet:
Zeze.Game.Online.tOnlineShared_Mobile - 特点:全局共享,所有服务器进程都能访问,通过数据库同步
2.3.3 _tlocal(角色本地数据表)
设计意图 存储角色在当前服务器进程的本地数据,仅本进程可见,不与其他服务器共享,用于缓存和性能优化。
为什么需要
- 性能优化:频繁访问的数据不需要跨进程通信
- 存储临时数据:战斗数据、临时会话等不需要全局共享的数据
- 减少网络开销:本地数据读写速度快,不占用网络带宽
- 版本控制:带LoginVersion,过期自动清理
- 超时清理:长时间不活跃的数据自动删除
存储内容
public class BLocal extends Bean {
private long loginVersion; // 登录版本号(与OnlineShared一致)
private BLink link; // 本地链接信息副本
private HashMap<String, BAny> datas; // 本地数据集合(Key-Value)
}
public class BAny extends Bean {
private Any any; // 存储任意Bean类型
}
使用场景
场景1:战斗数据缓存
// 开始战斗,保存战斗数据到本地(快)
public void startBattle(long roleId, BattleInstance battle) {
online.setLocalBean(roleId, "CurrentBattle", battle);
// 战斗过程中频繁访问,不需要跨进程通信
}
// 战斗结束,清理数据
public void endBattle(long roleId) {
online.removeLocalBean(roleId, "CurrentBattle");
}
场景2:临时会话数据
// 开始交易,保存交易会话
public void startTrade(long roleId1, long roleId2, TradeSession session) {
online.setLocalBean(roleId1, "TradeSession", session);
online.setLocalBean(roleId2, "TradeSession", session);
}
// 玩家登出时自动清理(超时或版本过期)
online.getLocalRemoveEvents().add((sender, arg) -> {
var trade = online.getLocalBean(arg.roleId, "TradeSession", TradeSession.class);
if (trade != null) {
cancelTrade(arg.roleId, trade);
}
return 0;
});
场景3:模块加载缓存
// 热更新模块缓存
public class GameModule {
public void loadModuleData(long roleId) {
// 加载模块数据到本地缓存
var data = new ModuleData();
// ... 加载数据
online.setLocalBean(roleId, "ModuleData", data);
}
public ModuleData getModuleData(long roleId) {
// 优先从本地缓存获取
return online.getLocalBean(roleId, "ModuleData", ModuleData.class);
}
}
场景4:超时自动清理
// 配置超时时间(如10分钟不活跃就清理)
online.setLocalActiveTimeout(600 * 1000);
online.setLocalCheckPeriod(600 * 1000);
// 活跃时更新时间
online.setLocalActiveTimeIfPresent(roleId);
// 超时后自动删除,释放内存
数据库表名
- 表名:
Zeze.Game.Online.tlocal_[OnlineSetName] - 默认OnlineSet:
Zeze.Game.Online.tlocal_ - 命名OnlineSet:
Zeze.Game.Online.tlocal_Mobile - 特点:仅本进程可见,不与其他服务器共享,带版本控制和超时清理
2.3.4 _tRoleTimers(角色定时器表)
设计意图 管理角色的在线定时器,角色在线时触发的定时任务存储在这个表中,角色登出后自动清理。
为什么需要
- 在线期间定时任务:如心跳检查、定时保存、定时刷新等
- 自动清理:角色登出后,定时器自动删除,不会占用资源
- 角色级别隔离:每个角色的定时器独立管理,互不干扰
- 支持暂停/恢复:角色登出时暂停,重新登录时恢复
使用场景
场景1:定时保存数据
// 每5分钟自动保存角色数据
public void startAutoSave(long roleId) {
online.getTimerRole().schedule(roleId, 5 * 60 * 1000, (context) -> {
// 保存角色数据
saveRoleData(roleId);
return 0; // 返回0表示继续定时
});
}
场景2:定时刷新状态
// 每1秒刷新战斗状态
public void startBattleTick(long roleId) {
online.getTimerRole().schedule(roleId, 1000, (context) -> {
var battle = online.getLocalBean(roleId, "Battle", BattleInstance.class);
if (battle != null) {
battle.tick(); // 战斗逻辑帧
}
return 0;
});
}
场景3:定时检查
// 每30秒检查玩家是否在安全区
public void startSafetyZoneCheck(long roleId) {
online.getTimerRole().schedule(roleId, 30 * 1000, (context) -> {
if (isInSafetyZone(roleId)) {
// 在安全区,恢复血量
recoverHealth(roleId);
}
return 0;
});
}
数据库表名
- 表名:
Zeze.Game.Online.tRoleTimers_[OnlineSetName] - 默认OnlineSet:
Zeze.Game.Online.tRoleTimers_ - 命名OnlineSet:
Zeze.Game.Online.tRoleTimers_Mobile - 特点:角色登出后自动清理,仅存储在线期间的定时器
2.3.5 _tRoleOfflineTimers(角色离线定时器表)
设计意图 管理角色的离线定时器,角色登出后仍需要执行的定时任务存储在这个表中,即使角色离线也能触发。
为什么需要
- 离线期间定时任务:如离线经验增长、建筑升级、资源产出等
- 持久化存储:角色登出后定时器仍然存在,服务器重启后继续执行
- 跨服务器生效:无论角色在哪个服务器登录,离线定时器都有效
- 一次性触发:触发后自动删除,不会重复执行
使用场景
场景1:离线经验增长
// 玩家离线后,每10分钟获得离线经验
public void startOfflineExp(long roleId) {
var timer = new OfflineExpTimer();
timer.setRoleId(roleId);
timer.setExpPerMinute(100);
// 登出10分钟后开始计算离线经验
online._tRoleOfflineTimers.getOrAdd(roleId).setTimer(timer);
}
// 触发离线经验
public void onOfflineExpTimer(long roleId) {
var timer = online._tRoleOfflineTimers.get(roleId);
if (timer != null) {
long offlineMinutes = calculateOfflineTime(roleId);
long exp = offlineMinutes * timer.getExpPerMinute();
// 玩家重新登录时发放离线经验
addOfflineExp(roleId, exp);
// 删除定时器(一次性)
online._tRoleOfflineTimers.remove(roleId);
}
}
场景2:建筑升级
// 玩家开始建造建筑,需要1小时
public void startBuilding(long roleId, int buildingId) {
var timer = new BuildingTimer();
timer.setRoleId(roleId);
timer.setBuildingId(buildingId);
timer.setCompleteTime(System.currentTimeMillis() + 60 * 60 * 1000);
online._tRoleOfflineTimers.getOrAdd(roleId).setTimer(timer);
}
// 建筑完成(玩家可能在线,也可能离线)
public void onBuildingComplete(long roleId) {
var timer = online._tRoleOfflineTimers.get(roleId);
if (timer != null && timer instanceof BuildingTimer) {
var building = (BuildingTimer)timer;
// 建筑完成,发放奖励
completeBuilding(roleId, building.getBuildingId());
// 删除定时器
online._tRoleOfflineTimers.remove(roleId);
}
}
场景3:邮件/通知延迟发送
// 1小时后发送提醒邮件
public void scheduleReminderMail(long roleId, String content) {
var timer = new ReminderMailTimer();
timer.setRoleId(roleId);
timer.setContent(content);
timer.setSendTime(System.currentTimeMillis() + 60 * 60 * 1000);
online._tRoleOfflineTimers.getOrAdd(roleId).setTimer(timer);
}
数据库表名
- 表名:
Zeze.Game.Online.tRoleOfflineTimers_[OnlineSetName] - 默认OnlineSet:
Zeze.Game.Online.tRoleOfflineTimers_ - 命名OnlineSet:
Zeze.Game.Online.tRoleOfflineTimers_Mobile - 特点:持久化存储,角色离线后仍然存在,触发后自动删除
2.4 数据表对比总结
| 表名 | 作用域 | 生命周期 | 用途 | 数据库共享 |
|---|---|---|---|---|
| _tOnline | 本进程 | 角色在当前进程在线时 | ServerId、可靠通知标记、本地UserData | 否(各进程独立) |
| _tOnlineShared | 全局 | 角色在线期间 | 全局在线状态、链接信息、版本号、账号 | 是(全局共享) |
| _tlocal | 本进程 | 角色在当前进程在线时 | 临时缓存、战斗数据、会话数据 | 否(各进程独立) |
| _tRoleTimers | 全局 | 角色在线期间 | 在线定时任务(心跳、刷新、保存) | 是(跟随角色) |
| _tRoleOfflineTimers | 全局 | 离线定时任务触发前 | 离线定时任务(离线经验、建筑升级) | 是(跟随角色) |
状态枚举
eOffline = 0; // 离线
eLogined = 1; // 已登录
eLinkBroken = 2; // 断线(延迟登出中)
2.3 核心接口分类
2.3.1 Online状态查询
// 查询Online状态
Online getOnline(long roleId);
boolean isOnline(long roleId);
boolean isLogin(long roleId);
// 获取共享状态
BOnlineShared getOnlineShared(long roleId);
BOnlineShared getLoginOnlineShared(long roleId); // 仅登录状态
// 获取状态值
int getState(long roleId);
Long getLoginVersion(long roleId);
Long getLogoutVersion(long roleId);
2.3.2 用户数据管理
// UserData - 跟随OnlineShared,所有服务器可见
<T extends Bean> T getUserData(long roleId);
void setUserData(long roleId, T data);
// LocalBean - 仅本进程可见
<T extends Bean> T getLocalBean(long roleId, String key);
void setLocalBean(long roleId, String key, T bean);
void removeLocalBean(long roleId, String key);
<T extends Bean> T getOrAddLocalBean(long roleId, String key, T defaultHint);
2.3.3 协议发送接口
单发协议
// 发送给单个角色
void send(long roleId, Protocol<?> p);
void sendOnline(long roleId, Protocol<?> p);
// 发送RPC
void sendRpc(long roleId, Rpc<A, R> rpc, ProtocolHandle<Rpc<A, R>> handle);
TaskCompletionSource<R> sendOnlineRpcForWait(long roleId, Rpc<A, R> rpc);
群发协议
// 发送给多个角色
void send(Collection<Long> roleIds, Protocol<?> p);
void sendOnline(Collection<Long> roleIds, Protocol<?> p);
// 发送给所有OnlineSet
void sendAllOnlines(long roleId, Protocol<?> p);
void sendAllOnlines(Collection<Long> roleIds, Protocol<?> p);
可靠通知(保证送达,需要确认)
void addReliableNotifyMark(long roleId, String listenerName);
void removeReliableNotifyMark(long roleId, String listenerName);
void sendReliableNotify(long roleId, String listenerName, Protocol<?> p);
void sendReliableNotifyWhileCommit(long roleId, String listenerName, Protocol<?> p);
广播协议(发送给所有连接的Link)
int broadcast(Protocol<?> p);
int broadcast(Protocol<?> p, int timeout);
int broadcast(Protocol<?> p, boolean onlySameVersion);
直接发送(通过LinkName+LinkSid发送,不经过Online查询)
boolean send(String linkName, long linkSid, Protocol<?> p);
boolean send(Long roleId, String linkName, long linkSid, Protocol<?> p);
事务相关发送
void sendWhileCommit(long roleId, Protocol<?> p); // 事务提交时发送
void sendWhileRollback(long roleId, Protocol<?> p); // 事务回滚时发送
2.3.4 事件系统
// 登录事件
EventDispatcher getLoginEvents();
// 断线重连事件
EventDispatcher getReloginEvents();
// 登出事件
EventDispatcher getLogoutEvents();
// 本地数据删除事件
EventDispatcher getLocalRemoveEvents();
// 断线事件
EventDispatcher getLinkBrokenEvents();
2.3.5 请求转发(Transmit)
// 注册转发处理器
ConcurrentHashMap<String, TransmitAction> getTransmitActions();
// 转发请求
void transmit(long sender, String actionName, long roleId);
void transmit(long sender, String actionName, long roleId, Serializable parameter);
void transmit(long sender, String actionName, Iterable<Long> roleIds);
// 事务相关转发
void transmitWhileCommit(long sender, String actionName, long roleId);
void transmitWhileRollback(long sender, String actionName, long roleId);
TransmitAction接口:
public interface TransmitAction {
long call(long sender, long target, Binary parameter) throws Exception;
}
2.3.6 踢人功能
void kick(long roleId, int code, String desc);
2.3.7 OnlineSet管理
String getOnlineSetName();
Online getOnline(String name);
Online getOnlineByContext(); // 获取上下文中的Online
2.4 登录登出流程
Login流程
- 创建Local记录(如不存在)
- 验证账号绑定
- 如果已登录,触发Logout并重试
- 更新LoginVersion
- 设置Link状态为eLogined
- 设置Link的UserState(OnlineSetName + Context=roleId)
- 清空可靠通知标记和队列
- 触发Login事件
ReLogin流程(断线重连)
- 类似Login流程
- 保留ServerId
- 同步可靠通知队列(ReliableNotifySync)
Logout流程
- 设置LogoutVersion
- 设置Link状态为eOffline
- 删除Local记录
- 触发Logout事件
- 清空UserState
LinkBroken流程(断线)
- 设置Link状态为eLinkBroken
- 触发LinkBroken事件
- 延迟登出(配置OnlineLogoutDelay时间后执行Logout)
- 如果在延迟期间重连成功,取消Logout
2.5 本地数据超时清理
Online会定期检查Local数据的活跃度,超时未活跃的数据会被清理:
// 配置超时时间和检查间隔
online.setLocalActiveTimeout(600 * 1000); // 活跃超时时间(默认10分钟)
online.setLocalCheckPeriod(600 * 1000); // 检查间隔(默认10分钟)
// 更新活跃时间
online.setLocalActiveTimeIfPresent(roleId);
2.6 多OnlineSet机制
Online支持创建多个独立的在线集合,实现业务隔离:
// 创建多个OnlineSet
provider.create(app, "Game", "Chat", "Match");
// 每个OnlineSet独立管理角色在线状态
Online gameOnline = provider.getOnline("Game");
Online chatOnline = provider.getOnline("Chat");
// 角色可以同时在多个OnlineSet中登录
// 例如:角色在Game中登录,但不在Chat中登录
使用场景:
- 不同业务模块隔离(游戏逻辑、聊天、匹配等)
- 不同服务类型隔离(实时战斗服务、挂机服务等)
- 不同玩家群体隔离
2.7 热更新支持
Online支持热更新场景下的Bean类型迁移:
// 注册自定义Bean类型
Online.register(MyCustomBean.class);
// 热更新时的数据迁移
online.upgrade(oldBean -> {
// 返回新的Bean实例
return newBean;
});
三、何时应该使用这套系统
3.1 判断标准
应该使用 ProviderWithOnline + Online 的情况:
- 需要管理玩家在线状态
- 玩家需要登录、登出
- 需要追踪玩家是否在线
- 需要处理断线重连
- 示例:MMORPG、卡牌对战、MOBA等所有需要持久连接的游戏
- 需要向玩家推送数据
- 服务器主动发送通知给玩家
- 需要实时更新游戏状态
- 需要广播服务器公告
- 示例:奖励通知、系统公告、好友上线通知
- 需要跨服务器交互
- 玩家分布在多个游戏服务器
- 需要跨服查询、跨服聊天、跨服交易
- 需要全局玩家状态
- 示例:大型MMO、分区游戏、跨服活动
- 需要本地缓存优化性能
- 某些数据访问频繁,不需要全局同步
- 需要减少网络开销和数据库压力
- 示例:战斗数据、临时会话数据
- 需要业务隔离
- 不同模块需要独立的在线状态
- 需要独立的资源管理
- 示例:独立聊天系统、独立匹配系统
不应该使用的情况:
- 无状态服务
- HTTP REST API服务
- 纯请求-响应模式
- 不需要维护会话状态
- 单机小游戏
- 所有逻辑在客户端
- 服务器仅提供简单的数据存储
- 实时性要求极高的场景
- FPS游戏(需要专用游戏服务器架构)
- 需要微秒级延迟同步(Online系统的延迟可能过高)
3.2 架构对比
传统架构 vs Online系统架构
| 特性 | 传统架构 | Online系统架构 |
|---|---|---|
| 会话管理 | 需要自己维护 | 自动管理,带版本控制 |
| 断线重连 | 需要自己实现 | 延迟登出,自动恢复 |
| 跨服通信 | 需要自己实现 | Transmit自动转发 |
| 协议发送 | 需要自己实现连接管理 | 提供多种发送方式 |
| 负载监控 | 需要自己实现 | 集成ProviderLoad |
| 本地缓存 | 需要自己实现 | Local数据自动管理 |
| 可靠通知 | 需要自己实现 | 内置可靠通知机制 |
| 热更新 | 需要自己处理数据迁移 | 提供upgrade接口 |
什么时候使用Online系统的价值最大:
- 团队规模小,需要快速开发
- 需要支持复杂的在线状态管理
- 需要跨服务器功能
- 需要稳定的断线重连机制
- 需要负载均衡和动态扩展
四、典型使用场景
public class GameApp {
private ProviderWithOnline provider;
public void start() {
// 1. 创建Provider并配置OnlineSet
provider.create(app, "Game", "Chat", "Match");
// 2. 注册事件处理器
var online = provider.getOnline("Game");
online.getLoginEvents().add((sender, arg) -> {
// 处理登录事件
System.out.println("Role login: " + arg.roleId);
return 0;
});
online.getLogoutEvents().add((sender, arg) -> {
// 处理登出事件
System.out.println("Role logout: " + arg.roleId);
return 0;
});
// 3. 注册Transmit处理器
online.getTransmitActions().put("QueryRoleInfo", (sender, target, param) -> {
// 查询角色信息并返回给sender
var roleInfo = getRoleInfo(target);
online.send(sender, new RoleInfoResponse(roleInfo));
return 0;
});
// 4. 启动服务
provider.start();
}
}
3.2 发送协议给玩家
public void sendReward(long roleId, Reward reward) {
var online = provider.getOnline();
// 在事务中发送
Transaction.whileCommit(() -> {
// 发放奖励后发送通知
online.send(roleId, new RewardNotify(reward));
});
}
// 可靠通知(必须送达)
public void sendImportantNotice(long roleId, String notice) {
var online = provider.getOnline();
// 添加标记
online.addReliableNotifyMark(roleId, "ImportantNotice");
// 发送可靠通知
online.sendReliableNotify(roleId, "ImportantNotice",
new NoticeProtocol(notice));
}
// 广播给所有在线玩家
public void broadcastAnnouncement(String announcement) {
var online = provider.getOnline();
online.broadcast(new AnnouncementProtocol(announcement));
}
3.3 管理玩家本地数据
public void savePlayerBattleData(long roleId, BattleData data) {
var online = provider.getOnline();
// 保存本地数据(仅本进程可见)
online.setLocalBean(roleId, "BattleData", data);
// 保存共享数据(所有服务器可见)
online.setUserData(roleId, new UserData(data));
}
public BattleData getPlayerBattleData(long roleId) {
var online = provider.getOnline();
// 优先从本地获取
var localData = online.getLocalBean(roleId, "BattleData", BattleData.class);
if (localData != null) {
return localData;
}
// 从共享数据获取
return online.getUserData(roleId, BattleData.class);
}
3.4 跨服务器请求转发
public class QueryService {
public void init() {
var online = provider.getOnline();
// 注册转发处理器
online.getTransmitActions().put("QueryFriendList", (sender, target, param) -> {
// 查询目标玩家的好友列表
var friendList = getFriendList(target);
// 返回给查询发起者
online.send(sender, new FriendListResponse(target, friendList));
return 0;
});
}
public void queryFriendList(long requester, long target) {
var online = provider.getOnline();
// 转发请求到目标角色所在的服务器
online.transmit(requester, "QueryFriendList", target);
}
}
四、关键设计模式
4.1 版本号机制
LoginVersion: 每次登录递增,用于标识登录会话LogoutVersion: 登出时设置为当前LoginVersion- Local数据带有LoginVersion,过期自动删除
4.2 三层数据隔离
tOnline: 每个服务器进程独立存储tOnlineShared: 全局共享(通过数据库同步)tlocal: 本地进程数据(用于缓存和优化)
4.3 延迟登出机制
- 断线后不立即登出,等待配置的超时时间
- 超时期间重连成功可恢复会话
- 避免短暂网络抖动导致的数据丢失
4.4 可靠通知机制
- 服务端维护通知队列
- 客户端按序确认接收
- 重连时自动同步未确认的通知
五、配置项
在Zeze配置中可设置:
<!-- Online登出延迟时间(毫秒) -->
<property name="OnlineLogoutDelay" value="30000" />
<!-- 本地数据活跃超时时间(毫秒) -->
<!-- 通过代码设置:online.setLocalActiveTimeout(600000) -->
<!-- 本地数据检查间隔(毫秒) -->
<!-- 通过代码设置:online.setLocalCheckPeriod(600000) -->
六、注意事项
- OnlineSet创建时机:必须在App.Start流程中、Application.start()之前创建
- 默认Online:空字符串名称的Online为默认Online,负责管理所有OnlineSet
- 线程安全:所有接口都是线程安全的,可在多线程环境中调用
- 事务使用:建议在事务中使用Online接口,保证数据一致性
- Local数据限制:Local数据仅本进程可见,不要存储需要跨进程访问的数据
- 可靠通知限制:使用可靠通知前需要先添加标记