连接管理与会话
概述
长连接是游戏服务器的根本形态:玩家连接后可能在线数小时,期间经历登录、心跳、断线、重连、下线等多个阶段。服务器需要一个会话(Session)抽象,把底层 Channel 与"玩家身份、在线状态、连接时间"绑定起来统一管理。本文设计 Session 模型、心跳检测、断线重连与玩家上下线生命周期。
一、Session 抽象设计
1.1 为什么需要 Session
Channel 只代表一条 TCP 连接
业务需要知道:
这条连接是谁(playerId)
这条连接何时建立、何时活动
这条连接当前状态(在线/认证中/掉线)
Channel 之外还挂了哪些数据(房间号、会话参数)
→ 用 Session 把 Channel + 玩家信息 + 状态封装起来1.2 Session 字段设计
java
public class GameSession {
private Channel channel; // 底层连接
private long sessionId; // 会话唯一 ID
private long playerId; // 玩家 ID(登录后才有)
private int state; // 状态机:CONNECTED/AUTHED/OFFLINE
private long createTime; // 建立时间
private long lastActiveTime; // 最后活跃时间(心跳更新)
private Map<String, Object> attrs;// 会话附加属性
public void send(GameMessage msg) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(msg);
}
}
public void close() {
if (channel != null) {
channel.close();
}
}
}设计要点:
send 统一封装发送,业务不直接碰 Channel
attrs 用于会话级数据(当前房间号、认证信息)
状态字段支撑上下线状态机1.3 Session 与 Channel 的关系
两种组织方式:
方式一:Session 持有 Channel(本文方案)
session.send() 直接写
方式二:Channel 上挂 Session(Channel attr)
channel.attr(KEY_SESSION).set(session)
工程上常两者结合:
SessionManager 里 ConcurrentHashMap<Channel, GameSession>
同时 channel.attr 反向引用,双向查找二、会话生命周期
2.1 完整生命周期
建立连接 → 认证(登录) → 在线 → 断开/下线
每阶段触发的事件:
channelActive → 创建 Session(未认证状态)
收到登录消息 → 认证成功,绑定 playerId(已认证状态)
心跳保活 → 更新 lastActiveTime
channelInactive → 会话结束,清理资源2.2 状态机
| 状态 | 含义 | 允许操作 |
|---|---|---|
CONNECTED | 已连接未登录 | 仅登录类消息 |
AUTHED | 登录成功 | 全部业务消息 |
OFFLINE | 掉线待重连 | 仅重连恢复 |
CLOSED | 会话关闭 | 无 |
实现要点:
状态在 Session 内原子更新(volatile / AtomicInteger)
非 AUTHED 状态收到业务消息 → 直接拒绝或断开
防止未登录就发业务指令(协议层防线)三、心跳检测
3.1 为什么需要心跳
TCP 层的保活(SO_KEEPALIVE)周期长(默认 2 小时),不可靠
游戏需要快速发现死连接:
玩家 App 被杀、网络切换、运营商 NAT 超时
死连接占着资源不释放 → 连接数虚高
应用层心跳:定时发小包,超时未收到 → 判定死亡3.2 IdleStateHandler 使用
java
// pipeline 装配:读空闲 60s 触发
pipeline.addLast("idle", new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS));构造参数(读、写、全部空闲):
new IdleStateHandler(readerIdleTime, writerIdleTime, allIdleTime, unit)
只关心读空闲 → (60, 0, 0)
触发时机:超过指定时间没有读/写事件java
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
// 读超时:连续 60s 没收到任何数据
ctx.close(); // 断开,交给重连流程
}
} else {
ctx.fireUserEventTriggered(evt);
}
}
}3.3 心跳交互流程
服务器策略(服务端主动断开):
服务器读空闲 60s 未收到数据 → 断开连接
客户端心跳周期 30s → 服务器读到数据 → 刷新
两倍关系保证容错:断 1-2 个心跳才判定死亡
备选方案(服务器主动探测):
服务器周期发 Ping,客户端回 Pong
两次 Ping 未收到 Pong → 断开心跳消息设计:
复用协议:Opcode = 心跳命令
Body 尽量小(可空)
心跳消息不进入业务逻辑,在网关层处理3.4 心跳与 Session 联动
收到任何消息(不只心跳)都刷新 lastActiveTime:
玩家操作本身就是活跃信号
心跳只是兜底,防止长时间无操作被误杀
大房间/挂机场景:心跳保活即可四、断线重连机制
4.1 客户端重连策略
客户端检测到断线:
指数退避重连:1s → 2s → 4s → ... 上限 30s
保持用户可感知的"重连中"状态
重连成功后重新登录,携带原 playerId重连要点:
重连不是简单重新建连
需要恢复:房间位置、对局状态、未读消息
快速重连(几秒内)→ 直接恢复
慢速重连(超时)→ 判负/退出房间4.2 服务器端会话恢复
掉线瞬间(channelInactive):
保留玩家数据与房间席位(宽限期内)
标记 Session 为 OFFLINE
通知同房间玩家"xx 掉线"
宽限期内重连(通常 30-90 秒):
新连接登录时发现已有 OFFLINE 会话
复用原玩家数据,旧连接资源清理
恢复房间状态,通知房间"xx 回归"
超出宽限期:
清理 Session,房间移除,按规则判负/托管java
// 重连核心:旧会话数据迁移到新连接
public void onReconnect(GameSession oldSession, Channel newChannel) {
long playerId = oldSession.getPlayerId();
// 复用原会话对象,仅替换 channel 并重置状态
oldSession.setChannel(newChannel);
oldSession.setState(State.AUTHED);
oldSession.setLastActiveTime(System.currentTimeMillis());
// 通知房间玩家回归
}4.3 连接替换防顶号
同一 playerId 新连接登录(异地/顶号):
处理旧连接(发下线通知 → 关闭)
防止一账号多连接数据错乱
顶号策略:允许 / 禁止,按游戏运营需求定五、玩家上下线生命周期
5.1 上线流程
1. 建立连接 → 创建 Session(CONNECTED)
2. 客户端发登录请求
3. 鉴权(Token / 渠道验证)
4. 加载玩家数据(内存缓存)
5. 绑定 playerId,状态置 AUTHED
6. 恢复在线状态(好友可见、房间回归)
7. 推送登录成功 + 初始化数据5.2 下线流程
正常下线(客户端主动):
客户端发下线消息(先于 TCP 断开)
→ 服务端保存数据 → 清理会话 → 通知好友/房间
异常下线(断线/超时):
走"掉线"流程 → 宽限期等待重连
→ 超时后转为真正下线数据落库时机:
不是每次操作都写库
内存为主 + 定时落盘 + 下线时强制落库
掉线期间玩家数据仍在内存,重连后无缝衔接5.3 SessionManager 组件
java
public class SessionManager {
private final ConcurrentHashMap<Long, GameSession> sessions = new ConcurrentHashMap<>();
public void add(GameSession session) { ... } // 新连接
public GameSession get(long playerId) { ... } // 按玩家找
public void remove(long playerId) { ... } // 移除
public void broadcast(GameMessage msg) { ... } // 全服广播
public int onlineCount() { return sessions.size(); }
}Manager 关注点:
并发安全:ConcurrentHashMap
定时清理:配合心跳,清掉过期 Session
广播接口:全服/按房间广播的入口
统计接口:在线数、连接数(监控用)六、常见问题
| 问题 | 处理 |
|---|---|
| 连接数很多但在线数少 | 心跳超时清理不及时,调小读空闲时间 |
| 玩家掉线后数据丢失 | 掉线宽限期保护 + 下线强制落库 |
| 顶号后旧连接还活着 | 登录时按 playerId 关闭旧 Session |
| 心跳消息进入业务逻辑 | 心跳在网关层拦截,不进分发器 |
| 重连后状态错乱 | 状态机严格流转,重连只允许从 OFFLINE 恢复 |
七、小结
会话层是连接与业务之间的"粘合剂":GameSession 把 Channel、playerId、状态、附加属性封装成业务可用的对象,配合 SessionManager 统一管理在线集合。心跳用 IdleStateHandler 触发读超时事件,超时即断,保证死连接快速清理。断线重连依赖"掉线宽限期 + 状态机":宽限期内新连接复用旧会话数据,超时后按规则判负清理。加上上线加载、下线落库的生命周期管理,会话层就构成了长连接游戏的稳定底座。