c++游戏后端开源框架学习——wukong(五、登录)

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 的转换

  1. /login 生成 loginToken,存Redis
  2. /enterGame 校验 loginToken,生成 gToken
  3. 客户端用 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初始值策略
相关推荐
先跑起来再说1 小时前
Qavor:一个能跑、能观测、能扩展的开源 AI Agent 工作台
人工智能·开源
敢敢是只喵i2 小时前
给现有业务系统接 AI 助手,入口怎么选?网页聊天、自动化任务与本地 Agent 对比
运维·人工智能·ai·开源·自动化·安全架构
冬奇Lab3 小时前
一天一个开源项目(第206篇):T3 Code - AI 编程 Agent 的统一控制台
人工智能·开源·资讯
IT古董3 小时前
AI 资讯日报 | 2026年9月1日 :混元 Hy4 开源、DeepSeek 多模态登顶、可灵获国家队 14 亿注资
人工智能·开源
程序员-Benothing3 小时前
OpenAI断供Cursor:当AI巨头开始“清理门户“,开源生态的中立性还能撑多久?
人工智能·开源·大模型
码匠许师傅4 小时前
【设计模式精讲】14.外观模式(Facade)
c++·设计模式·uml·外观模式
葡萄城技术团队4 小时前
ERP 内置 BI(下):四种技术路线与自研 / 采购 / 开源的 ROI 权衡
开源
阿里技术4 小时前
zg 正式开源:本地检索,不止于关键词
开源·本地检索
xxwxx__5 小时前
C++ list 深度解析:从使用原理到模拟实现
开发语言·c++·list
Chester_19995 小时前
CSP202303C.LDAP
开发语言·c++·蓝桥杯·stl