【C++ 在线五子棋对战】 - 在线用户管理模块实现

发布时间:2026/7/13 19:43:19

【C++ 在线五子棋对战】 - 在线用户管理模块实现 大家好我是Halcyon.平安欢迎文末添加好友交流共同进步一、模块概述二、完整源码 — online.hpp三、类结构总览 — online_manager3.1 成员变量3.2 方法总览四、方法逐个解析4.1 加入在线 — enter_game_hall / enter_game_room4.2 移除在线 — exit_game_hall / exit_game_room4.3 判断在线 — is_in_hall / is_in_room4.4 获取连接 — get_conn_from_hall / get_conn_from_room五、实际使用场景5.1 用户在线状态完整流转5.2 上层模块调用示例六、为什么用 unordered_map 而不是 map七、线程安全设计八、设计总结一、模块概述在线用户管理模块负责记录当前谁在线、在哪里、通过哪条连接通信。在五子棋对战中服务器需要时刻知道每个在线用户的大致位置——是在游戏大厅等待匹配还是在某个游戏房间中对战——以及他们对应的 WebSocket 连接对象。只有拿到连接对象服务器才能向客户端推送消息匹配结果、对手棋步、聊天内容等。本模块用一个online_manager类管理两张哈希表分别对应大厅和房间两类场景提供加入、移除、判断在线、获取连接四组操作。类功能说明online_manager在线用户管理器 — 维护大厅/房间两组用户-连接映射提供增删查操作模块依赖关系util.hppwsserver_t 类型定义 ↓ online.hpp本模块在线用户管理 ↓ room.hpp房间模块通过 online_manager 查找对手连接广播棋步/聊天 ↓ matcher.hpp / server.hpp上层模块调用进入/退出大厅和房间二、完整源码 — online.hpp//在线用户管理模块#ifndef__M_ONLINE_H__#define__M_ONLINE_H__#includeutil.hpp#includemutex#includeunordered_mapclassonline_manager{private:std::mutex _mutex;//使用哈希分别为key-idval-服务器保存客户端的连接对象std::unordered_mapuint64_t,wsserver_t::connection_ptr_hall_user;//建立大厅用户与通信连接关系std::unordered_mapuint64_t,wsserver_t::connection_ptr_room_user;//建立房间用户与通信连接关系public://websoket连接建立时才会 加入 游戏大厅 游戏房间在线用户管理boolenter_game_hall(uint64_tuid,wsserver_t::connection_ptrconn){std::unique_lockstd::mutexlocal(_mutex);_hall_user.insert(std::make_pair(uid,conn));returntrue;}boolenter_game_room(uint64_tuid,wsserver_t::connection_ptrconn){std::unique_lockstd::mutexlocal(_mutex);_room_user.insert(std::make_pair(uid,conn));returntrue;}//websoket连接断开时才会 移除 游戏大厅 游戏房间在线用户管理boolexit_game_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);_hall_user.erase(uid);returntrue;}boolexit_game_room(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);_room_user.erase(uid);returntrue;}//判断当前制定用户是否在 大厅/房间boolis_in_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_hall_user.find(uid);if(it_hall_user.end()){returnfalse;}returntrue;}boolis_in_room(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_room_user.find(uid);if(it_room_user.end()){returnfalse;}returntrue;}//通过用户ID在 大厅/房间 用户管理中获取对应的通信连接wsserver_t::connection_ptrget_conn_from_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_hall_user.find(uid);if(it_hall_user.end()){returnwsserver_t::connection_ptr();}returnit-second;}wsserver_t::connection_ptrget_conn_from_room(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_room_user.find(uid);if(it_room_user.end()){returnwsserver_t::connection_ptr();}returnit-second;}};#endif三、类结构总览 — online_manager3.1 成员变量classonline_manager{private:std::mutex _mutex;std::unordered_mapuint64_t,wsserver_t::connection_ptr_hall_user;std::unordered_mapuint64_t,wsserver_t::connection_ptr_room_user;};字段类型说明_mutexstd::mutex互斥锁保护两个哈希表的并发访问_hall_userunordered_mapuint64_t, connection_ptr大厅在线用户表uid → WebSocket 连接_room_userunordered_mapuint64_t, connection_ptr房间在线用户表uid → WebSocket 连接为什么分两张表而不是一张表加一个场景字段一个用户在同一时刻只可能处于大厅或房间中的一个。如果只用一张哈希表加一个enum { HALL, ROOM }字段来区分场景看起来也能工作但会带来两个问题操作耦合大厅模块和房间模块都要操作同一张表任何一方的修改都可能影响另一方。分表后大厅模块只操作_hall_user房间模块只操作_room_user各场景互不干扰逻辑清晰。状态转换复杂用户从大厅进入房间时需要先删除旧条目再插入新条目或者原地修改场景字段这在并发环境下需要额外加锁保护。而分表后exit_game_hall操作_hall_user、enter_game_room操作_room_user两张表互不影响不需要额外的原子性保证。键值对为什么是uid → connection_ptr键用 uid上层模块server、room、matcher都以用户 ID 来标识一个玩家。比如匹配成功后matcher 拿到的就是两个用户的 uid需要通过 uid 找到对应的连接来通知匹配结果。值用 connection_ptrwsserver_t::connection_ptr是 WebSocket 的连接智能指针封装了与客户端的 TCP 长连接。拿到它就能调用conn-send(msg)向客户端发送数据。3.2 方法总览本模块共 8 个方法按功能分为四组每组针对大厅和房间各提供一个功能大厅方法房间方法说明加入enter_game_hallenter_game_room用户上线插入 uid-conn 映射移除exit_game_hallexit_game_room用户下线删除 uid 对应条目判断在线is_in_hallis_in_room查询 uid 是否在哈希表中获取连接get_conn_from_hallget_conn_from_room通过 uid 获取 WebSocket 连接四、方法逐个解析4.1 加入在线 — enter_game_hall / enter_game_roomboolenter_game_hall(uint64_tuid,wsserver_t::connection_ptrconn){std::unique_lockstd::mutexlocal(_mutex);_hall_user.insert(std::make_pair(uid,conn));returntrue;}参数说明参数类型说明uiduint64_t用户 ID由数据库自增分配connwsserver_t::connection_ptrWebSocket 连接的智能指针引用解析std::unique_lockstd::mutex local(_mutex)— RAII 方式加锁local变量生命周期结束时自动解锁。因为服务器可能同时处理多个 WebSocket 连接建立/断开事件多个线程可能同时操作_hall_user哈希表必须加锁保护。std::make_pair(uid, conn)— 构造键值对uid 是uint64_tconn 是智能指针的拷贝引用计数 1。插入哈希表后即使外部的 conn 被释放哈希表中的副本仍然有效。insert— 如果 uid 已存在insert不会覆盖与operator[]不同。在本项目中正常流程不会出现重复加入的情况如果出现说明逻辑有问题应该在上层排查而不是在这里静默覆盖。enter_game_room的实现完全相同只是操作_room_user哈希表。调用时机WebSocket 连接建立时。在 server.hpp 中当客户端通过 WebSocket 连接到游戏大厅的端点时调用enter_game_hall匹配成功进入房间后调用enter_game_room。4.2 移除在线 — exit_game_hall / exit_game_roomboolexit_game_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);_hall_user.erase(uid);returntrue;}解析只需要uid参数不需要连接对象——因为移除操作只需要找到哈希表中的条目并删除连接对象已经存在于哈希表的 value 中。unordered_map::erase(key)的行为如果 key 存在则删除并返回 1如果 key 不存在也不会报错返回 0。这个特性很实用——用户断开连接时不需要先判断他在不在表中直接 erase 即可。删除条目后哈希表中存储的connection_ptr智能指针被销毁引用计数 -1。如果这是最后一个持有该连接的智能指针连接对象会被自动释放。调用时机WebSocket 连接断开时server.hpp 的wsclose回调中调用用户从大厅进入房间时需要先exit_game_hall再enter_game_room完成大厅→房间的场景切换用户从房间退出回到大厅时需要先exit_game_room再enter_game_hall实际代码中通常直接断开重连4.3 判断在线 — is_in_hall / is_in_roomboolis_in_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_hall_user.find(uid);if(it_hall_user.end()){returnfalse;}returntrue;}解析auto it _hall_user.find(uid)— 在哈希表中查找 uid。find返回一个迭代器指向找到的元素如果没找到返回end()迭代器。auto关键字让编译器自动推导迭代器类型避免写出冗长的std::unordered_mapuint64_t, wsserver_t::connection_ptr::iterator。返回true表示用户在线false表示不在线。为什么需要这个方法上层模块在很多场景下需要先判断用户是否在线再决定后续操作匹配模块匹配线程从队列中取出两个用户但在匹配等待期间用户可能已经断开连接了。匹配成功后必须先检查两个用户是否都还在大厅如果有人已经离线就不能创建房间。房间模块用户下棋时需要检查对手是否还在房间中如果对手已断开则判定对手认输。消息推送服务器向用户推送消息前需要确认用户在线避免向已断开的连接发送数据。4.4 获取连接 — get_conn_from_hall / get_conn_from_roomwsserver_t::connection_ptrget_conn_from_hall(uint64_tuid){std::unique_lockstd::mutexlocal(_mutex);autoit_hall_user.find(uid);if(it_hall_user.end()){returnwsserver_t::connection_ptr();// 返回空智能指针}returnit-second;}解析这是本模块最核心、使用频率最高的方法。整个项目的实时通信都依赖它——服务器需要拿到用户的连接对象才能通过conn-send()推送数据。it-second— 哈希表中的 value即wsserver_t::connection_ptr智能指针。返回它相当于拷贝一份智能指针引用计数 1调用者持有期间连接不会被释放。wsserver_t::connection_ptr()— 构造一个空的智能指针等价于nullptr。上层模块通过判断返回值是否为空来确定用户是否在线autoconn_online_mgr-get_conn_from_room(uid);if(conn){conn-send(json_msg);// 用户在线发送消息}// conn 为空 用户已断开不发送为什么不直接用is_in_hallget_conn_from_hall两个调用看起来先判断在线、再获取连接是更安全的做法但这样存在竞态条件时间线 t1: is_in_hall(uid) 返回 true — 线程A检查用户在线 t2: 用户断开连接exit_game_hall(uid) — 线程B删除了用户 t3: get_conn_from_hall(uid) 返回空 — 线程A获取连接失败两次调用之间用户可能已经断开连接被移除了。所以正确的做法是直接调用get_conn_from_hall根据返回值判空处理一次调用完成判断和获取不存在时间窗口问题。is_in_hall更多用于只关心在不在线、不需要拿连接的场景如匹配前的校验。五、实际使用场景5.1 用户在线状态完整流转用户注册/登录HTTP 短连接不涉及本模块 │ │ 用户建立 WebSocket 连接到游戏大厅 │ server.hpp: wsopen_game_hall 回调 │ → enter_game_hall(uid, conn) │ ▼ 在大厅等待匹配 │ 此时 _hall_user[uid] conn │ is_in_hall(uid) true │ 匹配模块可通过 get_conn_from_hall(uid) 推送正在匹配等消息 │ │ 匹配成功 │ matcher.hpp: 匹配线程处理完毕 │ → exit_game_hall(uid) // 移出大厅 │ → enter_game_room(uid, conn) // 进入房间 │ ▼ 在房间中对战 │ 此时 _room_user[uid] conn │ is_in_room(uid) true │ 对手下棋后room.hpp 通过 get_conn_from_room(uid) 获取连接 │ → conn-send(棋步JSON) // 推送对手棋步给该用户 │ │ 游戏结束 / 用户关闭页面 │ server.hpp: wsclose 回调 │ → exit_game_room(uid) // 移出房间 │ ▼ 用户断开 WebSocket 连接离线 │ _hall_user 和 _room_user 中均无该用户 │ is_in_hall(uid) false │ is_in_room(uid) false5.2 上层模块调用示例场景一WebSocket 连接建立用户进入大厅server.hpp 的 wsopen 回调voidwsopen_game_hall(websocketpp::connection_hdl hdl){// 1. 从连接中获取 Cookie解析出 ssid// 2. 通过 ssid 找到 session获取 uid// 3. 将用户加入大厅在线表_online_mgr-enter_game_hall(uid,conn);}场景二匹配成功用户从大厅移到房间room_manager 创建房间时// matcher 匹配成功后由 room_manager 创建房间room_ptr rm_room_mgr-create_room(uid1,uid2);// 用户从大厅移出进入房间autoconn1_online_mgr-get_conn_from_hall(uid1);autoconn2_online_mgr-get_conn_from_hall(uid2);_online_mgr-exit_game_hall(uid1);_online_mgr-exit_game_hall(uid2);_online_mgr-enter_game_room(uid1,conn1);_online_mgr-enter_game_room(uid2,conn2);注意这里的顺序先通过get_conn_from_hall拿到连接对象智能指针拷贝引用计数 1再exit_game_hall删除哈希表条目。因为conn1/conn2已经持有了智能指针的副本即使哈希表中的条目被删除连接对象也不会被释放后续enter_game_room可以正常使用。场景三房间中向对手推送棋步room.hpp 的 handle_chess 方法voidhandle_chess(uint64_tuid,Json::Valuereq){// 解析棋步坐标...// 判定胜负...// 广播给对手uint64_topponent_id(uid_white_id)?_black_id:_white_id;autoopponent_conn_online_mgr-get_conn_from_room(opponent_id);if(opponent_conn){opponent_conn-send(json_resp.toStyledString());}}get_conn_from_room返回对手的连接如果对手已断开则返回空指针跳过发送。场景四WebSocket 断开用户下线server.hpp 的 wsclose 回调voidwsclose_game_hall(websocketpp::connection_hdl hdl){// 1. 获取 uid// 2. 如果用户在匹配队列中移出队列// 3. 移出大厅在线表_online_mgr-exit_game_hall(uid);}voidwsclose_game_room(websocketpp::connection_hdl hdl){// 1. 获取 uid// 2. 通知房间内对手该玩家已断开可能判负// 3. 移出房间在线表_online_mgr-exit_game_room(uid);}六、为什么用unordered_map而不是map本模块选择unordered_map而不是map这个选择值得说明特性unordered_mapmap底层结构哈希表红黑树查找复杂度平均 O(1)最坏 O(n)O(log n)插入复杂度平均 O(1)O(log n)是否有序无序按 key 有序内存开销哈希表有额外桶开销每个节点有红黑树指针开销选择unordered_map的理由不需要遍历也不需要有序本模块的所有操作都是通过 uid 精确查找一个用户从不需要按顺序遍历所有在线用户或找到分数最高/最低的用户。unordered_map的 O(1) 查找正合适。key 是uint64_t天然适合哈希整数类型做哈希键效率极高几乎不会有哈希冲突。如果 key 是字符串比如用户名哈希冲突的概率会稍高但 uid 是整数哈希函数直接返回整数本身或简单变换分布非常均匀。在线用户数量有限一个五子棋服务器的同时在线人数通常在几百到几千unordered_map的桶数量远大于元素数量查找就是一次哈希计算 一次指针解引用比红黑树的多次比较要快。七、线程安全设计本模块使用一把互斥锁_mutex保护所有对两张哈希表的操作线程A: enter_game_hall() → lock → insert → unlock 线程B: exit_game_room() → 等待... → lock → erase → unlock 线程C: get_conn_from_hall() → 等待... → lock → find → unlock每个方法都加锁不会出现只读不加锁的情况。这与db.hpp不同——db.hpp中win()/lose()等 UPDATE 操作不需要加锁而本模块的所有操作包括is_in_hall这种只读操作都加锁。原因有两个读也可能读到脏数据unordered_map在插入/删除过程中如果发生 rehash扩容所有迭代器都会失效。如果is_in_hall正在遍历哈希表的同时另一个线程触发了insert导致 rehashfind返回的迭代器就是野指针程序会崩溃。操作频率不高is_in_hall和get_conn虽然是只读操作但在线用户数量有限几百人级别加锁的开销可以忽略不计不值得为了这点性能引入更复杂的读写锁。八、设计总结设计点实现方式数据结构unordered_map哈希表uid 为键O(1) 查找、插入、删除大厅/房间分离两张独立哈希表各自管理逻辑清晰互不干扰线程安全std::mutexunique_lock所有操作含只读都加锁保护连接管理wsserver_t::connection_ptr智能指针引用计数自动管理连接生命周期查询失败处理返回空智能指针connection_ptr()上层通过if(conn)判空处理接口设计四组对称方法enter/exit/is_in/get_conn大厅/房间各一份简单直观键值选择uid 做键上层模块都通过 uid 标识用户connection_ptr 做值直接可发送数据

相关新闻