在线五子棋开发

准备工作

使用命令创建 WebSocket Server 项目:composer create-project imiphp/project-websocket:~1.0 ./
config/config.php文件里配置连接池
新建目录Module用于存放模块
User用户模块
Model用于指定模型类
Enum枚举类

tb_user表指定到刚刚生成的命名空间里,config/config.php文件下

1
2
3
4
5
6
7
8
9
10
11
'tools'  =>  [
'generate/model' => [
'namespace' => [
'ImiApp\Module\User\Model' => [
'tables' => [
'tb_user',
],
],
]
],
],

使用命令生成模型类:vendor/bin/imi generate/model -namespace "ImiApp" -prefix tb_
:::alert-danger
必须先建立数据库和表才能使用命令生成模型
连接池配置不正确可导致模型生成错误
:::
修改默认的WebSocketServer名称为Mainserver,一键替换各个类以及主服务器配置里的名称

异常类和接口状态码定义

接口状态码

  1. 新建一个目录用于存放状态码的枚举类Enum
  2. 新建一个枚举类用于定义接口状态码Enum/MessageCode.php
  3. 配置扫描目录,在config/config.php下面配置以让Enum目录被框架扫描到

异常类

  1. 新建一个异常类目录Exception
  2. 定义一个统一的业务异常类,以便捕获异常,将异常格式化输出到前台Exception/BusinessException.php
  3. 定义一个未查询到记录的异常类Exception/NotFoundException.php

接口格式化

  • 实现所有接口默认自动带上codemessage参数返回

    通过AOP机制注入到所有带有Action注解的方法中
    AOP(Aspect Oriented Programming)面向切面编程:是在不修改源代码的情况下给程序动态统一添加某种特定功能的一种技术

  1. 新建一个Aop目录用于存储Aop类MainServer/Aop
  2. 新建一个Aop注入类,服务于接口MainServer/Aop/ApiControllerAspect.php
  3. MainServer/config/config.php下配置扫描目录以让Aop目录被框架扫描到

接口异常时处理code,message返回

  • 利用自定义错误捕获处理器,捕获错误、处理错误。
  1. 新建一个错误处理器目录MainServer/ErrorHandler
  2. 新建一个接口异常处理类MainServer/ErrorHandler/HttpErrorHandler.php
  3. 配置HttpErrorHandler,在MainServer/config/config.php里的beans

HTTP有关接口开发

规范

  • 路由、参数采用驼峰命名
  • 请求参数支持FormJson两种格式
  • 读取操作使用GET请求,反之使用POST请求
  • 服务端返回数据格式为Json
  • 返回数据必须携带codemessage参数,code为0则为成功,非0失败,失败时message应有信息
  • 用户会话采用Session方式存储

注册

POST /user/register

  • 参数
    • username用户名
    • password密码
  • 返回
    1
    2
    3
    4
    5
    {
    "token":"token用于传入请求头 x-Session-Id中"
    "code":0,
    "message":""
    }

  1. 新建一个ApiController目录用于存储api控制器Module/User/ApiController
  2. 编写一个User控制器类Module/User/ApiController/UserController.php
  3. 新建一个Service目录用于存储Service层的类Module/User/Service
  4. 编写一个User服务类Module/User/Service/UserService.php
  5. 在User控制器内将User服务注入到protected $userService变量中@Inject("UserService")
  6. 定义register方法,实现登录注册功能
  7. 配置Module扫描目录MainServer/config/config.php

登录

POST /user/login

  • 参数
    • username用户名
    • password密码
  • 返回
    1
    2
    3
    4
    5
    {
    "token":"token用于传入请求头 x-Session-Id中"
    "code":0,
    "message":""
    }
  1. 配置自定义Session中间件MainServer/config/config.php下的beans
  2. 配置禁用SeeionCookieMainServer/config/config.php下的beans
  3. 编写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
    }
  1. 新建一个注解目录Module/User/Annotation用于存放注解类
  2. 编写一个LoginRequired注解类Module/User/Annotation/LoginRequired.php
  3. 新建一个Aop注入目录Module/User/Aop用于存放注入类
  4. 编写一个LoginRequiredInject注入类Module/User/Aop/LoginRequiredInject.php处理注入,使用@LoginRequired注入
  5. 编写一个UserSession服务类Module/User/Service/UserSessionService.php,验证用户Session以及获取用户信息
  6. 编写get方法通过id获取用户记录,在Module/User/Service/UserService.php
  7. 新建一个Module/User/Exception目录用于存放异常类
    1. 编写一个用户登录状态异常类Module/User/Exception/UserNoLoginException.php
  8. 过滤模型字段,在Module/User/Model/User.php里添加@Serializables(mode="deny", fields={"password"})

webSocket

WebSocket配置

  • WebSocketDispatcherWebSocket 调度器,用于配置中间件
  • ServerGroup 配置服务端的逻辑分组
  • ConnectContextStore连接上下文存储,WebSocket握手登录后,存储当前连接的相关数据,连接断开后销毁数据
  • ConnectionBinder连接绑定器,把用户的标识绑定到一个连接,让其产生逻辑上的必要联系
  1. 配置MainServer/config/config.phpWebSocket和Redis相关项

WebSocket接口开发

规范

  • 路由、参数采用驼峰命名
  • 返回参数为JSON
  • 请求、响应都会有一个action参数表示动作
  • 返回数据必须携带codemessage参数,code为0则为成功,非0失败,失败时message应有信息
  • 客户端提交的数据必须带上messageId,服务器响应则会原样返回messageId的值
  • 由于JS无法自定义WebSocket的请求头,所以sessionId使用GET参数方式传递,参数名为_sessionId

WebSocket握手

  1. 编写类MainServer/HttpController/HandShakeController.php
  2. 指定websocket通信数据的解析系@WSConfig(parserClass=\Imi\Server\DataParser\JsonArrayParser::class),在HandShakeController.php
  3. 编写方法ws()实现登录检测、将用户ID写入连接上下文,在MainServer/HttpController/HandShakeController.php

统一返回数据处理

  1. 配置ReturnMessageMiddleware,在MainServer/config/config.phpWebSocketDispatcher中的middlewares
  2. 编写中间件MainServer/Middleware/ReturnMessageMiddleware.php
  3. 配置'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":""
    }

  1. 新建一个websocket控制器目录MainServer/WebSocketController用于存储WebSocket控制器
  2. 配置WebSocketController扫描目录MainServer/config/config.php
  3. 编写一个控制器类MainServer/WebSocketController/PublicController.php

游戏房间

设计游戏房间的Redis模型

  1. 新建一个目录Module/Gobang用于存储五子棋游戏相关的文件
  2. 新建一个目录Module/Gobang/Model用于存放五子棋相关模型类
  3. 编写一个模型类Module/Gobang/Model/RoomModel.php用于构建游戏房间的Redis模型
  4. 新建一个目录Module/Gobang/Enum用于存放枚举类
  5. 编写一个枚举类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":""
    }

    用户在某个房间内不可创建房间
    创建房间包括:创建房间、创建房间分组、将创建者加入房间、重新推送房间列表


  1. 新建一个目录WebSocketController用于存放WebSocket控制器类
  2. 编写一个控制器类Module/Gobang/WebSocketController/RoomController.php用于控制房间的相关业务
  3. 新建一个目录Module/Gobang/Service用于存放服务类
  4. 编写一个服务类Module/Gobang/Service/RoomService.php用于服务房间相关业务
  5. 编写一个create方法,在Module/Gobang/Service/RoomService.php里,实现创建房间(服务层)
  6. 新建一个目录Module/Gobang/Logic,用于存放业务逻辑类
  7. 编写一个业务逻辑类Module/Gobang/Logic/RoomLogic.php用于实现房间相关业务逻辑
  8. 编写一个create方法,在Module/Gobang/Logic/RoomLogic.php里,实现创建房间(逻辑层)
  9. 编写一个getList 方法,在Module/Gobang/Logic/RoomLogic.php里,实现获取房间列表(逻辑层)
  10. 编写一个pushRooms方法,在Module/Gobang/Logic/RoomLogic.php里,实现推送房间列表(逻辑层)
  11. 编写一个枚举类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":""
    }

  1. 编写一个方法list,在Module/Gobang/WebSocketController/RoomController.php,用于获取房间列表

项目启动时清除Redis数据

  1. 新建一个目录Module/Gobang/Listener用于存放监听器类
  2. 编写一个监听器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":""
    }
  1. 编写一个fetInfo方法在Module/Gobang/Service/RoomService.php

加入游戏房间

1
2
3
4
5
{
"action":"room.join",
"roomId":1,
"messageId":1
}
  1. 编写一个jonin方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现加入房间(控制器)
  2. 编写一个join方法,在Module\Gobang\Logic\RoomLogic.php里(逻辑)
  3. 配置room锁的超时自动解锁,在config/config.php
  4. 编写一个lock方法,在Module/Gobang/Service/RoomService.php下,用于为房间数据操作加锁
  5. 编写一个join方法,在Module/Gobang/Service/RoomService.php里,用于实现加入房间,操作Redis数据库的部分(服务)
  6. 编写一个pushMessage方法Module/Gobang/Logic/RoomLogic.php里,用于向房间组内推送玩家加入消息

离开游戏房间

1
2
3
4
5
{
"action":"room.leave",
"roomId":1,
"messageId":1
}
  1. 编写一个leave方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现离开房间(控制器)
  2. 编写一个leave方法,在Module\Gobang\Logic\RoomLogic.php里,用于实现离开房间,业务逻辑部分(逻辑)
  3. 编写一个leave方法,在Module/Gobang/Service/RoomService.php里,用于实现离开房间,操作Redis数据库的部分(服务)

断线后自动离开房间

  1. 新建一个监听器Module/Gobang/Listener/OnClose.php监听连接的断开,并在编写断开之后要执行的操作
  2. 编写一个onUserClose方法,在Module\Gobang\Logic\RoomLogic.php里,用于连接断开后要进行的业务逻辑

战局

战局数据模型设计

  1. 编写一个枚举类Module/Gobang/Enum/GobangCell.php,用于列出棋格的所有状态
  2. 编写一个模型类Module/Gobang/Model/GobangGameModel.php,用于构建五子棋战局数据模型和游戏结束判定

准备

  1. 编写一个ready方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现玩家准备(控制器)
  2. 编写一个ready方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现玩家准备(逻辑)
  3. 编写一个ready方法,在Module/Gobang/Service/RoomService.php里,用于实现玩家准备,操作Redis数据库部分(服务)

取消准备

  1. 编写一个cancelReady方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现玩家准备(控制器)
  2. 编写一个cancelReady方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现玩家准备(逻辑)
  3. 编写一个cancelReady方法,在Module/Gobang/Service/RoomService.php里,用于实现玩家准备,操作Redis数据库部分(服务)

创建战局

  1. 编写一个服务类Module/Gobang/Service/GobangService.php,用于服务游戏对局

观战

  1. 编写一个watch方法,在Module/Gobang/WebSocketController/RoomController.php里,用于实现用户进入房间观战(控制器)
  2. 编写一个watch方法,在Module/Gobang/Logic/RoomLogic.php里,用于实现用户进入房间观战(逻辑)
  3. 编写一个watch方法,在Module/Gobang/Service/RoomService.php里,用于实现用户进入房间观战(服务)

$memberIds = &$room->getWatchMemberIds();
这里通过引用返回,修改$memberIds会改动模型中的$memberIds属性,而不必使用setter

游戏过程

下棋落子后的处理

  1. 新建一个控制器类Module\Gobang\WebSocketController\GobangController.php,用于实现落子处理(控制器类)
  2. 编写一个go方法,在Module\Gobang\WebSocketController\GobangController.php里,用于实现落子处理(控制器)
  3. 新建一个逻辑类Module/Gobang/Logic/GobangLogic.php,用于实现落子处理(逻辑类)
  4. 编写一个go方法,在Module/Gobang/Logic/GobangLogic.php里,用于实现落子处理(逻辑)
  5. 编写一个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":""

    握手

  1. 新建一个Module/IM目录,用于存放im通讯模块相关文件
  2. 新建一个Module/IM/HttpController目录,用于存放im通讯http控制器
  3. 编写一个控制器类Module/IM/HttpController/HandShakeController.php,用于Im通讯的握手

加入im房间

1
2
3
4
5
{
"action":"im.joinRoom",
"roomId":1,
"messageId":1
}
  1. 新建一个Module/IM/WebSocketController目录,用于存放im通讯WebSocket控制器
  2. 编写一个控制器类Module/IM/WebSocketController/IMController.php,用于im通讯(控制器)
  3. 编写一个方法joinRoomModule/IM/WebSocketController/IMController.php里,用于加入im通讯房间(控制器)

发送消息

1
2
3
4
5
6
{
"action":"im.send",
"content":"内容",
"roomId":1,
"messageId":1
}
  1. 新建一个Module/Enum目录,用于存放枚举类
  2. 编写一个枚举类Module/Enum/MessageActions.php,用于列出所有im消息动作(加入房间、发送内容、接收内容)
  3. 编写一个枚举类Module\IM\Enum\MessageType.php,用于列出所有im消息类型(系统消息和聊天)
  4. 编写一个send方法,在Module/IM/WebSocketController/IMController.php里,用于发送消息(控制器)

用户离开

1
2
3
4
5
6
{
"action":"im.leave",
"content":"内容",
"roomId":1,
"messageId":1
}
  1. 新建一个目录Module/IM/Listener用于存放监听器
  2. 编写一个监听器类Module/IM/Listener/OnLeave.php用于监听用户离开