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初始值策略
相关推荐
小小龙学IT42 分钟前
simdjson:利用 SIMD 指令实现 GB/s 级 JSON 解析的 C++ 开源库
开发语言·c++·json
Marco_Marco13544 分钟前
开源地图API vs 商业地图API:选型全对比
经验分享·开源
不正经学生1 小时前
C语言字符串函数进阶:安全版函数 + 错误处理 + 子串查找
c语言·开发语言·c++·算法·面试
ClouGence1 小时前
数据库管理工具 CloudDM 4.1.0 发布,支持达梦、Cloudberry
数据库·开源
三克的油2 小时前
算法基础-1
数据结构·c++·算法
云卷云舒___________2 小时前
【加急快讯】DeepSeek Harness 深夜开源!主打「一切皆插件」,由 Cordis 驱动
开源·aiagent·deepseek·ai日报·智能体框架·deepseekharness·cordis
zlinear数据采集卡2 小时前
ZLinear产品线全景对比:D223 vs DABL7606 vs DABL-G511选型指南
arm开发·嵌入式硬件·算法·fpga开发·开源
依然鸣2 小时前
PTA团体程序设计天梯赛L2真题讲解L2-005-008
开发语言·数据结构·c++·算法·深度优先·pat考试·图论
欧特克_Glodon4 小时前
OpenCV计算机视觉开发入门与实践<七>:OpenCV 界面编程之窗口
c++·人工智能·opencv·计算机视觉
郝学胜_神的一滴4 小时前
C++20 高级编程 002:夯实现代C++底层基本功
c++·编程语言