Login:登录服
对应代码目录:
login/
一、Login 是什么?
Login 是 Wukong 框架的登录入口服务,通过 HTTP API 对外提供账号认证、角色创建、进入游戏等功能。它是整个游戏流程的起点。
打个比方:Login 就像游乐园的售票处------买票(登录)、选项目(创角)、检票入园(进入游戏),之后你就在游乐园里自由活动了。
为什么用 HTTP 而不是 TCP?
- 登录流程是短连接,适合 HTTP
- 方便接入第三方支付/认证(都是HTTP)
- CDN/负载均衡容易(nginx反代即可)
- 登录服不需要维持长连接状态
二、核心设计
2.1 核心类
| 类 | 文件 | 职责 |
|---|---|---|
LoginServer |
login_server.h |
单例,初始化HTTP/MySQL/Redis/Nexus |
LoginHandlerMgr |
login_handler_mgr.h |
单例,HTTP请求处理器 |
LoginConfig |
login_config.h |
配置解析 |
2.2 四个 HTTP API
| 接口 | 方法 | 功能 |
|---|---|---|
/login |
POST | 账号登录,返回token和角色列表 |
/getProfile |
POST | 获取角色画像数据 |
/createRole |
POST | 创建角色 |
/enterGame |
POST | 进入游戏,返回Gateway地址 |
2.3 Login 服的特殊性
- 不启动 RPC Server:只通过 HTTP 对外服务
- 持有 GatewayAgent:用于 kick 旧连接和获取 Gateway 地址
- 连接 MySQL:用户和角色数据持久化
- 连接 Redis:token/passport/session 管理
三、完整登录流程
3.1 流程图
客户端 Login服(HTTP) Redis MySQL
│ │ │ │
│── POST /login ───────────────→│ │ │
│ {openid} │ │ │
│ │── LoadOrCreateUser ───────────────────────────────→│
│ │←── {userId, roles} ────────────────────────────────│
│ │── SetLoginToken ──────────→│ │
│←── {userId, token, roles} ────│ │ │
│ │ │ │
│── POST /createRole ──────────→│ │ │
│ {userId, token, serverId} │── checkToken ─────────────→│ │
│ │←── ok ────────────────────│ │
│ │── CreateRole ─────────────────────────────────────→│
│ │── SaveRole ───────────────→│ │
│←── {role} ────────────────────│ │ │
│ │ │ │
│── POST /enterGame ───────────→│ │ │
│ {userId, token, roleId} │── checkToken ─────────────→│ │
│ │── CheckRole ──────────────────────────────────────→│
│ │── GetSession(旧) ─────────→│ │
│ │ 若有旧Session: │ │
│ │── GatewayAgent.kick ──→ Gateway(踢旧连接) │
│ │── SetPassport ────────────→│ │
│ │ {gToken, gateId, roleId} │ │
│←── {gateId, host, port, gToken}│ │ │
│ │ │ │
│── TCP连接Gateway ──────────────────────────────────────→ Gateway │
│── AUTH(userId, gateId, gToken)──────────────────────────→ Gateway │
3.2 阶段一:/login
1. 客户端提交 openid
2. 登录频率限制(LoginLock,防止重复登录)
3. 身份校验 checkLogin()(当前默认返回true,需业务实现)
4. MySQL LoadOrCreateUser:根据openid查找或创建用户
→ 返回 userId 和已有角色列表
5. 生成临时 token,存入 Redis(SetLoginToken)
6. 返回:{userId, token, 服务器列表, 角色列表}
3.3 阶段二:/createRole
1. 校验 userId + token + serverId
2. 判断服务器状态(NORMAL/FULL/CLOSED)
3. checkToken:从Redis读取token比对
4. 创角锁(CreateRoleLock):防止1分钟内重复创角
5. 校验角色数量限制(跨服总数 + 单服数量)
6. DemoRoleBuilder 构建角色初始数据
7. MySQL写库:CreateRole + UpdateRoleIds
8. Redis缓存:SaveRole + SaveProfile
9. 返回角色信息
3.4 阶段三:/enterGame(核心)
这是最复杂的阶段,负责生成进入游戏的"门票":
1. 校验 userId + token + roleId + serverId
2. checkToken:校验token有效性
3. MySQL CheckRole:校验角色归属(防止A用户操作B用户的角色)
4. 顶号处理:
├── Redis GetSession(roleId) 查询旧Session
├── 若存在旧Session:
│ ├── 获取旧的 gateId 和 gToken
│ ├── 向旧Gateway发送 kick RPC
│ └── kick失败则返回错误(不允许新登录)
5. 生成 gToken(随机数,游戏期间的身份令牌)
6. 分配 Gateway:
├── GatewayAgent.randomServer(gatewayId) 随机选择
└── 获取Gateway的 host:port
7. 写入 Passport(关键!):
├── Redis SetPassport(gToken, gateId, roleId)
├── Lua脚本 set_pass 保证原子性
├── 超时60秒
└── 只能被使用一次(Gateway认证时校验后不删除,但超时自动过期)
8. 返回:{gateId, host, port, gToken}
→ 客户端用这些信息连接Gateway
四、Passport 机制
Passport 是 Login 服签发的一次性进入游戏凭证,类似游乐园的门票。
4.1 为什么需要 Passport?
如果没有 Passport,Gateway 怎么知道客户端是合法登录的?
- 客户端直接告诉 Gateway "我是用户xxx" → 不安全,谁都能冒充
- Login 直接告诉 Gateway "用户xxx要来了" → 可以,但 Login 和 Gateway 耦合
- Passport 方案:Login 把凭证存Redis,Gateway从Redis验证,解耦且安全
4.2 工作流程
Login服 Redis Gateway
│ │ │
│── SetPassport ───────────────→│ │
│ key: passport:<roleId> │ │
│ value: {gToken, gateId, │ │
│ roleId} │ │
│ TTL: 60秒 │ │
│ │ │
│ 返回 gToken 给客户端 │ │
│ │ │
│ │ 客户端连接Gateway │
│ │ 发送AuthRequest(gToken) │
│ │←── CheckPassport ──────────│
│ │ Lua脚本 chk_pass │
│ │ 校验 gateId + gToken │
│ │ 返回 roleId │
│ │──→ {roleId} ───────────────→│
│ │ │
│ │ 创建Session │
4.3 Lua 脚本保证原子性
lua
-- set_pass:设置Passport(原子操作)
-- 只在不存在时设置,防止覆盖
local key = KEYS[1]
local gToken = ARGV[1]
local gateId = ARGV[2]
local roleId = ARGV[3]
local expire = ARGV[4]
if redis.call('exists', key) == 0 then
redis.call('hset', key, 'gtoken', gToken, 'gateid', gateId, 'roleid', roleId)
redis.call('expire', key, expire)
return 1
else
return 0 -- 已存在,设置失败
end
lua
-- chk_pass:校验Passport
-- 校验 gateId 和 gToken 是否匹配
local key = KEYS[1]
local gateId = ARGV[1]
local gToken = ARGV[2]
if redis.call('exists', key) == 0 then
return '' -- 不存在
end
local storedGateId = redis.call('hget', key, 'gateid')
local storedGToken = redis.call('hget', key, 'gtoken')
if storedGateId == gateId and storedGToken == gToken then
return redis.call('hget', key, 'roleid') -- 返回roleId
else
return '' -- 校验失败
end
五、分区分服与合服
5.1 服务器组概念
Login 服通过 serverGroupData_ 管理服务器组:
大区(物理划分)
└── 分区/区服(逻辑划分)
├── serverId 1
├── serverId 2
└── serverId 3(合服后可能共享角色)
- 不同区服可共用同一组物理机器,仅逻辑隔离
- 支持大区、分区、滚服、合服
- 合服后,同一group中的多个serverId共享角色
5.2 实现
1. 从 Redis CorePersist 库读取服务器组数据
2. 解析为:
- groupStatusMap_(组ID → 状态:NORMAL/FULL/CLOSED)
- serverId2groupIdMap_(服ID → 组ID)
3. 通过 PubSub 订阅 "WK_ServerGroups" 主题
→ 服务器组变更时自动更新
5.3 服务器状态
| 状态 | 含义 | createRole | enterGame |
|---|---|---|---|
| NORMAL | 正常 | 允许 | 允许 |
| FULL | 满员 | 不允许 | 允许 |
| CLOSED | 关闭 | 不允许 | 不允许 |
六、Token 体系
Login 服管理着整个 Token 体系的一部分:
| Token | 生成方 | 存储位置 | 用途 | 生命周期 |
|---|---|---|---|---|
| loginToken | Login(/login) | Redis Cache | 校验createRole/enterGame | 临时 |
| gToken | Login(/enterGame) | Redis Passport→Session | Gateway身份校验、断线重连 | 游戏期间 |
| lToken | Lobby | Redis Location | 游戏对象唯一标识 | 游戏期间 |
| rToken | Record | Redis Record | 记录对象唯一标识 | 游戏期间+缓存期 |
| sToken | Scene | Redis SceneLocation | 场景唯一标识 | 场景存活期间 |
loginToken → gToken 的转换:
/login生成 loginToken,存Redis/enterGame校验 loginToken,生成 gToken- 客户端用 gToken 连接 Gateway,之后不再需要 loginToken
七、数据库设计
7.1 user 表
sql
CREATE TABLE `user` (
`userid` int(11) unsigned NOT NULL AUTO_INCREMENT,
`account` varchar(40) NOT NULL, -- 账号(openid)
`roles` varchar(1023) NOT NULL DEFAULT '', -- 角色列表JSON
`rolenum` int(5) NOT NULL DEFAULT 0, -- 角色数量
PRIMARY KEY (`userid`),
UNIQUE KEY `account` (`account`)
) ENGINE=InnoDB AUTO_INCREMENT=100000000;
7.2 role 表
sql
CREATE TABLE `role` (
`roleid` int(11) unsigned NOT NULL AUTO_INCREMENT,
`userid` int(11) unsigned NOT NULL,
`serverid` int(10) unsigned NOT NULL,
`data` mediumblob, -- 角色数据(protobuf序列化)
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` tinyint(1) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`roleid`)
) ENGINE=InnoDB AUTO_INCREMENT=100000000;
7.3 分片设计
AUTO_INCREMENT初始值为 100000000- 约定每个分片表最多存 99999999 条数据
- 第二个分片设为 200000000,以此类推
- 通过分片路由实现水平扩展
7.4 存储过程
sql
-- 幂等创建用户
CREATE PROCEDURE loadOrCreateUser(IN account VARCHAR(40))
BEGIN
INSERT IGNORE INTO user(account) VALUES(account);
SELECT userid, roles FROM user WHERE account=account;
END
-- 创建角色
CREATE PROCEDURE createRole(IN userid INT, IN serverid INT, IN data BLOB)
BEGIN
INSERT INTO role(userid, serverid, data) VALUES(userid, serverid, data);
SELECT LAST_INSERT_ID();
END
八、解决的问题
| 问题 | Login 的解决方案 |
|---|---|
| 账号认证 | HTTP API + MySQL用户表 |
| 防止重复登录 | LoginLock(Redis锁) |
| 防止重复创角 | CreateRoleLock(1分钟内) |
| 角色数量限制 | 跨服总数 + 单服数量校验 |
| 顶号 | GetSession + kick RPC |
| 安全进入游戏 | Passport一次性令牌 + Lua脚本原子性 |
| 分区分服 | 服务器组配置 + PubSub动态更新 |
| 数据库分片 | AUTO_INCREMENT初始值策略 |