基于PHP的在线五子棋游戏开发笔记
条评论在线五子棋开发
准备工作
使用命令创建 WebSocket Server 项目:composer create-project imiphp/project-websocket:~1.0 ./
在config/config.php文件里配置连接池
新建目录Module用于存放模块
User用户模块
Model用于指定模型类
Enum枚举类
将tb_user表指定到刚刚生成的命名空间里,config/config.php文件下
1 | 'tools' => [ |
使用命令生成模型类:vendor/bin/imi generate/model -namespace "ImiApp" -prefix tb_
:::alert-danger
必须先建立数据库和表才能使用命令生成模型
连接池配置不正确可导致模型生成错误
:::
修改默认的WebSocketServer名称为Mainserver,一键替换各个类以及主服务器配置里的名称
异常类和接口状态码定义
接口状态码
- 新建一个目录用于存放状态码的枚举类
Enum - 新建一个枚举类用于定义接口状态码
Enum/MessageCode.php - 配置扫描目录,在
config/config.php下面配置以让Enum目录被框架扫描到
异常类
- 新建一个异常类目录
Exception - 定义一个统一的业务异常类,以便捕获异常,将异常格式化输出到前台
Exception/BusinessException.php - 定义一个未查询到记录的异常类
Exception/NotFoundException.php
接口格式化
- 实现所有接口默认自动带上
code和message参数返回通过AOP机制注入到所有带有Action注解的方法中
AOP(Aspect Oriented Programming)面向切面编程:是在不修改源代码的情况下给程序动态统一添加某种特定功能的一种技术
- 新建一个Aop目录用于存储Aop类
MainServer/Aop - 新建一个Aop注入类,服务于接口
MainServer/Aop/ApiControllerAspect.php - 在
MainServer/config/config.php下配置扫描目录以让Aop目录被框架扫描到
接口异常时处理code,message返回
- 利用自定义错误捕获处理器,捕获错误、处理错误。
- 新建一个错误处理器目录
MainServer/ErrorHandler - 新建一个接口异常处理类
MainServer/ErrorHandler/HttpErrorHandler.php - 配置
HttpErrorHandler,在MainServer/config/config.php里的beans下
HTTP有关接口开发
规范
- 路由、参数采用驼峰命名
- 请求参数支持
Form、Json两种格式 - 读取操作使用GET请求,反之使用POST请求
- 服务端返回数据格式为Json
- 返回数据必须携带
code、message参数,code为0则为成功,非0失败,失败时message应有信息 - 用户会话采用Session方式存储
注册
POST /user/register
- 参数
username用户名password密码
- 返回
1
2
3
4
5{
"token":"token用于传入请求头 x-Session-Id中",
"code":0,
"message":""
}
- 新建一个
ApiController目录用于存储api控制器Module/User/ApiController - 编写一个User控制器类
Module/User/ApiController/UserController.php - 新建一个
Service目录用于存储Service层的类Module/User/Service - 编写一个User服务类
Module/User/Service/UserService.php - 在User控制器内将User服务注入到
protected $userService变量中@Inject("UserService") - 定义
register方法,实现登录注册功能 - 配置
Module扫描目录MainServer/config/config.php
登录
POST /user/login
- 参数
username用户名password密码
- 返回
1
2
3
4
5{
"token":"token用于传入请求头 x-Session-Id中",
"code":0,
"message":""
}
- 配置自定义Session中间件
MainServer/config/config.php下的beans里 - 配置禁用SeeionCookie
MainServer/config/config.php下的beans里 - 编写
login方法,实现登录功能
登录状态
GET /user/status
验证用户是否登录,并获取用户信息(注入注解的方式实现)
- 返回
1
2
3
4
5
6
7
8
9
10{
"data":{
"id":1,
"username":"yang",
"registerTime":"2022-04-12 00:00:00",
"lastLoginTime":"2022-04-12 00:00:00",
},
"message":"",
"code":0
}
- 新建一个注解目录
Module/User/Annotation用于存放注解类 - 编写一个
LoginRequired注解类Module/User/Annotation/LoginRequired.php - 新建一个Aop注入目录
Module/User/Aop用于存放注入类 - 编写一个
LoginRequiredInject注入类Module/User/Aop/LoginRequiredInject.php处理注入,使用@LoginRequired注入 - 编写一个UserSession服务类
Module/User/Service/UserSessionService.php,验证用户Session以及获取用户信息 - 编写
get方法通过id获取用户记录,在Module/User/Service/UserService.php下 - 新建一个
Module/User/Exception目录用于存放异常类- 编写一个
用户登录状态异常类Module/User/Exception/UserNoLoginException.php
- 编写一个
- 过滤模型字段,在
Module/User/Model/User.php里添加@Serializables(mode="deny", fields={"password"})
webSocket
WebSocket配置
WebSocketDispatcherWebSocket 调度器,用于配置中间件ServerGroup配置服务端的逻辑分组ConnectContextStore连接上下文存储,WebSocket握手登录后,存储当前连接的相关数据,连接断开后销毁数据ConnectionBinder连接绑定器,把用户的标识绑定到一个连接,让其产生逻辑上的必要联系
- 配置
MainServer/config/config.phpWebSocket和Redis相关项
WebSocket接口开发
规范
- 路由、参数采用驼峰命名
- 返回参数为
JSON - 请求、响应都会有一个
action参数表示动作 - 返回数据必须携带
code、message参数,code为0则为成功,非0失败,失败时message应有信息 - 客户端提交的数据必须带上
messageId,服务器响应则会原样返回messageId的值 - 由于JS无法自定义WebSocket的请求头,所以
sessionId使用GET参数方式传递,参数名为_sessionId
WebSocket握手
- 编写类
MainServer/HttpController/HandShakeController.php - 指定websocket通信数据的解析系
@WSConfig(parserClass=\Imi\Server\DataParser\JsonArrayParser::class),在HandShakeController.php - 编写方法
ws()实现登录检测、将用户ID写入连接上下文,在MainServer/HttpController/HandShakeController.php中
统一返回数据处理
- 配置
ReturnMessageMiddleware,在MainServer/config/config.php下WebSocketDispatcher中的middlewares - 编写中间件
MainServer/Middleware/ReturnMessageMiddleware.php - 配置
'ImiApp\MainServer\Middleware'扫描目录,在MainServer/config/config.php下
实现Ping通信
客户端主动发送的Ping
JS中无法手动发送WebSocket协议自带的Ping帧,所以需要自己写一个Ping请求
客户端发送数据
1
2
3
4{
"action":"ping",
"messageId":1
}服务器响应数据
1
2
3
4
5
6{
"action":"pong",
"messageId":1,
"code":0,
"message":""
}
- 新建一个websocket控制器目录
MainServer/WebSocketController用于存储WebSocket控制器 - 配置
WebSocketController扫描目录MainServer/config/config.php - 编写一个控制器类
MainServer/WebSocketController/PublicController.php
游戏房间
设计游戏房间的Redis模型
- 新建一个目录
Module/Gobang用于存储五子棋游戏相关的文件 - 新建一个目录
Module/Gobang/Model用于存放五子棋相关模型类 - 编写一个模型类
Module/Gobang/Model/RoomModel.php用于构建游戏房间的Redis模型 - 新建一个目录
Module/Gobang/Enum用于存放枚举类 - 编写一个枚举类
Module/Gobang/Enum/GobangStatus.php用于列出房间的所有状态
创建房间
规范
客户端发送数据
1
2
3
4
5{
"action":"room.create",
"title":"房间标题",
"messageId":1
}服务器响应数据
1
2
3
4
5
6{
"action":"pong",
"messageId":1,
"code":0,
"message":""
}用户在某个房间内不可创建房间
创建房间包括:创建房间、创建房间分组、将创建者加入房间、重新推送房间列表
- 新建一个目录
WebSocketController用于存放WebSocket控制器类 - 编写一个控制器类
Module/Gobang/WebSocketController/RoomController.php用于控制房间的相关业务 - 新建一个目录
Module/Gobang/Service用于存放服务类 - 编写一个服务类
Module/Gobang/Service/RoomService.php用于服务房间相关业务 - 编写一个
create方法,在Module/Gobang/Service/RoomService.php里,实现创建房间(服务层) - 新建一个目录
Module/Gobang/Logic,用于存放业务逻辑类 - 编写一个业务逻辑类
Module/Gobang/Logic/RoomLogic.php用于实现房间相关业务逻辑 - 编写一个
create方法,在Module/Gobang/Logic/RoomLogic.php里,实现创建房间(逻辑层) - 编写一个
getList方法,在Module/Gobang/Logic/RoomLogic.php里,实现获取房间列表(逻辑层) - 编写一个
pushRooms方法,在Module/Gobang/Logic/RoomLogic.php里,实现推送房间列表(逻辑层) - 编写一个枚举类
Module/Gobangnum/MessageActions.php用于列出所有的消息动作(如:加入房间、退出房间、准备……)
房间列表
规范
客户端发送数据
1
2
3
4{
"action":"room.list",
"messageId":1
}服务器响应数据
1
2
3
4
5
6{
"action":"pong",
"messageId":1,
"code":0,
"message":""
}
- 编写一个方法
list,在Module/Gobang/WebSocketController/RoomController.php,用于获取房间列表
项目启动时清除Redis数据
- 新建一个目录
Module/Gobang/Listener用于存放监听器类 - 编写一个监听器
Module/Gobang/Listener/AppInit.php,应用初始化时删除Redis数据
获取房间信息
规范
客户端发送数据
1
2
3
4
5{
"action":"room.info",
"roomId":1,
"messageId":1
}服务器响应数据
1
2
3
4
5
6
7{
"action":"room.info",
"messageId":1,
"roomInfo":{}
"code":0,
"message":""
}
- 编写一个
fetInfo方法在Module/Gobang/Service/RoomService.php里
加入游戏房间
1 | { |
- 编写一个
jonin方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现加入房间(控制器) - 编写一个
join方法,在Module\Gobang\Logic\RoomLogic.php里(逻辑) - 配置room锁的超时自动解锁,在
config/config.php下 - 编写一个
lock方法,在Module/Gobang/Service/RoomService.php下,用于为房间数据操作加锁 - 编写一个
join方法,在Module/Gobang/Service/RoomService.php里,用于实现加入房间,操作Redis数据库的部分(服务) - 编写一个
pushMessage方法Module/Gobang/Logic/RoomLogic.php里,用于向房间组内推送玩家加入消息
离开游戏房间
1 | { |
- 编写一个
leave方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现离开房间(控制器) - 编写一个
leave方法,在Module\Gobang\Logic\RoomLogic.php里,用于实现离开房间,业务逻辑部分(逻辑) - 编写一个
leave方法,在Module/Gobang/Service/RoomService.php里,用于实现离开房间,操作Redis数据库的部分(服务)
断线后自动离开房间
- 新建一个监听器
Module/Gobang/Listener/OnClose.php监听连接的断开,并在编写断开之后要执行的操作 - 编写一个
onUserClose方法,在Module\Gobang\Logic\RoomLogic.php里,用于连接断开后要进行的业务逻辑
战局
战局数据模型设计
- 编写一个枚举类
Module/Gobang/Enum/GobangCell.php,用于列出棋格的所有状态 - 编写一个模型类
Module/Gobang/Model/GobangGameModel.php,用于构建五子棋战局数据模型和游戏结束判定
准备
- 编写一个
ready方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现玩家准备(控制器) - 编写一个
ready方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现玩家准备(逻辑) - 编写一个
ready方法,在Module/Gobang/Service/RoomService.php里,用于实现玩家准备,操作Redis数据库部分(服务)
取消准备
- 编写一个
cancelReady方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现玩家准备(控制器) - 编写一个
cancelReady方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现玩家准备(逻辑) - 编写一个
cancelReady方法,在Module/Gobang/Service/RoomService.php里,用于实现玩家准备,操作Redis数据库部分(服务)
创建战局
- 编写一个服务类
Module/Gobang/Service/GobangService.php,用于服务游戏对局
观战
- 编写一个
watch方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现用户进入房间观战(控制器) - 编写一个
watch方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现用户进入房间观战(逻辑) - 编写一个
watch方法,在Module/Gobang/Service/RoomService.php里,用于实现用户进入房间观战(服务)
$memberIds = &$room->getWatchMemberIds();
这里通过引用返回,修改$memberIds会改动模型中的$memberIds属性,而不必使用setter
游戏过程
下棋落子后的处理
- 新建一个控制器类
Module\Gobang\WebSocketController\GobangController.php,用于实现落子处理(控制器类) - 编写一个
go方法,在Module\Gobang\WebSocketController\GobangController.php里,用于实现落子处理(控制器) - 新建一个逻辑类
Module/Gobang/Logic/GobangLogic.php,用于实现落子处理(逻辑类) - 编写一个
go方法,在Module/Gobang/Logic/GobangLogic.php里,用于实现落子处理(逻辑) - 编写一个
go方法,在Module/Gobang/Service/GobangService.php里,用于实现落子处理(服务)
GobangLogic中,注解
@Bean("GobangLogic")是为了,可以使用@Inject("RoomService")将RoomService注入到变量
im通讯
- 客户端发送数据
- 服务器响应数据
1
2
3
4
5"action":"im.send",
"roomId":1,
"messageId":1,
"code":0,
"message":"" - 服务器推送数据
1
2
3
4
5
6
7
8"action":"im.receive",
"type":1,
"sender":"发送者",
"content":"内容",
"time":"2022-04-15",
"messageId":1,
"code":0,
"message":""握手
- 新建一个
Module/IM目录,用于存放im通讯模块相关文件 - 新建一个
Module/IM/HttpController目录,用于存放im通讯http控制器 - 编写一个控制器类
Module/IM/HttpController/HandShakeController.php,用于Im通讯的握手
加入im房间
1 | { |
- 新建一个
Module/IM/WebSocketController目录,用于存放im通讯WebSocket控制器 - 编写一个控制器类
Module/IM/WebSocketController/IMController.php,用于im通讯(控制器) - 编写一个方法
joinRoom在Module/IM/WebSocketController/IMController.php里,用于加入im通讯房间(控制器)
发送消息
1 | { |
- 新建一个
Module/Enum目录,用于存放枚举类 - 编写一个枚举类
Module/Enum/MessageActions.php,用于列出所有im消息动作(加入房间、发送内容、接收内容) - 编写一个枚举类
Module\IM\Enum\MessageType.php,用于列出所有im消息类型(系统消息和聊天) - 编写一个
send方法,在Module/IM/WebSocketController/IMController.php里,用于发送消息(控制器)
用户离开
1 | { |
- 新建一个目录
Module/IM/Listener用于存放监听器 - 编写一个监听器类
Module/IM/Listener/OnLeave.php用于监听用户离开
