实战指南:使用Socket.IO和Redis构建实时多人在线PK系统
近期开发了一个知识PK系统,主要包括创建房间、加入房间、开始游戏、答题等功能。对此我选用了SocketIO
,实现客户端和服务器之间低延迟,双向和基于事件的通信。
技术方案
- Hyperf框架
一个基于Swoole的PHP协程框架
- SocketIO组件
Socket.IO是一款非常流行的应用层实时通讯协议和框架,可以轻松实现应答、分组、广播
组件基于WebSocket
实现,房间适配器基于Redis
驱动,可以适应多进程乃至分布式场景
- Redis
存储房间和玩家状态
- Postman
一个强大的客户端工具,支持Socket.IO连接,用它来做功能测试十分方便
状态存储
Redis存储
数据结构 | Key | Value | 说明 | |
---|---|---|---|---|
房间信息 | Hash | rooms:{room_no} | {"id": 1, "state": "created", "game_number": 1, "sid":"6626326272d7b#1"} | 存储房间ID、状态等基本信息 |
玩家集合 | Set | rooms:{room_no}:playerIds | player1,player2 | 存储房间内所有玩家ID |
玩家状态 | Hash | rooms:{room_no}:players:{player_id} | {"id": 1, "nickname": "卡夕洛", "avatar": "", "state":"ready", "score":0, "answer_count":0,"time":1713779921} | 存储玩家的ID、昵称、状态、得分等信息 |
暂存房间 | String | staging_room:{user_id} | room_no_xx | 房主断线后保留房间,X秒过期,重连后直接使用该房间 |
Context-连接上下文
实例化后的Roomer/Player
服务类可以存储在连接上下文
事件
基本流程
房主:创建房间 - 即将开始 - 开始游戏 - 答题 - 对局完成 - 返回房间/解散房间
玩家:加入房间 - 准备/取消准备/离开房间 - 答题 - 对局完成 - 返回房间/离开房间
事件 | 说明 |
---|---|
create-room | 房主:创建房间并加入,响应玩家列表和房间信息 |
join-room | 玩家:加入房间,响应玩家列表和房间信息,向房间内广播player-join |
leave-room | 玩家:离开房间,向房间内广播player-leave |
player-ready | 玩家:准备,向房间内广播player-ready |
player-cancel | 玩家:取消准备,向房间内广播player-cancel |
game-ready | 房主:游戏即将开始,向房间内广播game-ready |
game-start | 房主:游戏开始,向房间内广播game-start ,消息为PK的题目 |
answer | 答题,向房间内广播分数等信息 |
return-room | 返回房间,响应为玩家列表和房间信息 |
room-dissolve | 房主:销毁房间,向房间内广播room-dissolve |
掉线情况
心跳机制来检测玩家是否掉线,如果客户端在一段时间内没有发送心跳包,则认为客户端已掉线
Socket.IO服务器
2.x
版本,客户端与服务器建立连接时,服务器向客户端发送pingInterval
和pingTimeout
参数,用来控制客户端发送ping消息的频率和超时时间
-
玩家掉线后,直接向房间内广播
player-leave
-
房主掉线后,在某些情况下(如APP切换到后台被系统杀掉),保留房间一段时间
启动一个定时器,X秒后执行销毁房间和广播事件。如果客户端重新连接并创建房间,优先判断是否有保留房间,有则直接加入房间并清除定时器
对局记录持久化
每场PK结束后需成绩排名,并把对局记录存储到数据库,即结算
- 所有玩家答题完毕
当玩家答题到最后一道时,判断是否所有玩家都已答完,若是则结算
这里可能会有多人同时触发结算,我们使用Redis分布式锁
避免并发问题,只有加锁成功的玩家才能进入结算逻辑
- 所有玩家掉线
当玩家掉线时,如果房间状态为PK中,判断该玩家是否为房间最后一人,若是则结算
- 定时任务
定时巡查结算超时的房间
部署
经过测算,每名玩家约占用1kb
Redis内存
Docker + 蓝绿部署
蓝绿部署是一种发布新版本软件的策略,它将新版本软件部署在一个与生产环境完全隔离的环境中,称为“蓝环境”。经过测试验证后,再将流量从“绿环境”切换到“蓝环境”,完成新版本软件的发布。
每次发版切换流量后,客户端与旧版本之间建立的长连接不受影响,待等到旧版本上没有连接后再关闭服务即可。