加入房间要处理两件事
- 建立用户和房间的映射
- 建立用户和 fd 的映射
其实建立这两个映射很简单,比如很自然的就能想到数据库,不管是 mysql 还是 redis,但是这样会增大系统的复杂度,原因如下
- 用户的 fd 只在链接可用时有效,用户弱网环境应当考虑为一个常见场景,这必然会造成过多的 IO
- 链接只在服务可用的时候有意义,也就是 fd 只在服务可用的时候有意义,在服务挂掉 / 重启的时候所有用户和 fd 的绑定都应该失去意义
综上考虑,我们决定将用户和 fd 的映射关系存储在内存中,用户和房间的关系也一并存储在内存中并在适当的时候落盘
决定了如何建立映射关系,就还剩下一个问题悬而未决,就是系统如何描述房间和用户
让我们这么想,房间有什么东西?房间 id, 房间内用户等等,房间可以做什么?加入 / 踢出用户,同样的,用户有用户 id, 昵称,可以修改昵称等等
这么想就很清楚了,这俩就是个对象嘛,房间有什么,就是一个类的属性,能做什么,就是类的方法
好的,那就新建两个类,Room 和 User
-
Room 类暂时构建如下
namespace App\Entity; class Room { protected int $room_id = 0; protected array $players = []; protected array $user_fds = []; public function getRoomId() { return $this->room_id; } public function setRoomId(int $roomId) { $this->room_id = $roomId; } public function setPlayer(int $userId) { $this->players[] = $userId; } public function removePlayer(int $userId) { foreach ($this->players as $index => $player) { if ($userId == $player) { array_splice($this->players, $index, 1); } } } public function setUserFd(int $userId, int $fd) { $this->user_fds[$userId] = $fd; } public function getFds() { return array_values($this->user_fds); } } -
同理,User 类暂时定义如下
namespace App\Entity; class User { protected int $user_id = 0; protected int $room_id = 0; protected int $fd = 0; protected string $nickname = ''; public function getUserId() { return $this->user_id; } public function setUserId(int $userId) { $this->user_id = $userId; } public function getRoomId(int $roomId) { $this->room_id = $roomId; } public function setRoomId(int $roomId) { $this->room_id = $roomId; } public function setFd(int $fd) { $this->fd = $fd; } public function getFd() { return $this->fd; } public function setNickname(string $nickname) { $this->nickname = $nickname; } public function getNickname() { return $this->nickname; } }
描述了用户和房间这两个实体,下一步就是建立 RoomManager 和 UserManager 来管理这两个东西,代码如下
-
RoomManager
namespace App\Provider; use App\Entity\Room; use Hyperf\Logger\LoggerFactory; use Psr\Log\LoggerInterface; class RoomManager { protected LoggerInterface $logger; protected array $rooms; public function __construct(LoggerFactory $loggerFactory) { $this->logger = $loggerFactory->get('room_manager'); } public function hasRoom(int $roomId): bool { return isset($this->rooms[$roomId]); } public function getRoom(int $roomId): Room { if (! $this->hasRoom($roomId)) { throw new \Exception('room not found: ' . $roomId); } return $this->rooms[$roomId]; } public function addRoom(Room $room) { $this->rooms[$room->getRoomId()] = $room; } public function fetchRooms(): array { return $this->rooms; } } -
UserManager
namespace App\Provider; use App\Entity\User; class UserManager { protected array $users = []; protected array $fdUser = []; public function setUser(User $user) { $this->users[$user->getUserId()] = $user; $this->fdUser[$user->getFd()] = $user; } public function getUser(int $uid): User { return $this->users[$uid]; } public function hasUser(int $uid) { return isset($this->users[$uid]); } public function getUserFromFd(int $fd): User { return $this->fdUser[$fd]; } public function removeFd(int $fd) { unset($this->fdUser[$fd]); } public function removeUser(int $uid) { unset($this->users[$uid]); } } -
那么做完了这些工作,用户加入房间这个操作就变得非常简单了,只需要设置房间,用户,fd 三者的绑定关系即可,我们可以新建一个 JoinRoomService 来完成这件事,代码如下
namespace App\Service; use App\Entity\Room; use App\Entity\User; use App\Provider\RoomManager; use App\Provider\UserManager; use Hyperf\Logger\LoggerFactory; use Psr\Log\LoggerInterface; use Room\JoinRoomRequest; class JoinRoomService { protected LoggerInterface $logger; protected RoomManager $roomManager; protected UserManager $userManager; public function __construct(LoggerFactory $loggerFactory, RoomManager $roomManager, UserManager $userManager) { $this->logger = $loggerFactory->get('join_room'); $this->roomManager = $roomManager; $this->userManager = $userManager; } public function joinRoom(JoinRoomRequest $joinRoomRequest, int $fd) { $roomId = $joinRoomRequest->getRid(); $userId = $joinRoomRequest->getUserId(); if ($this->roomManager->hasRoom($roomId)) { $room = $this->roomManager->getRoom($roomId); } else { $room = new Room(); $room->setRoomId($roomId); $this->roomManager->addRoom($room); } $room->setUserFd($userId, $fd); if ($this->userManager->hasUser($userId)) { $user = $this->userManager->getUser($userId); } else { $user = new User(); $user->setUserId($userId); } $user->setFd($fd); $this->userManager->setUser($user); } } -
剩下的工作就只是将这些业务代码组装起来了,修改 DispatchMessage::dispatch ,增加一个 case
namespace App\Provider; use App\Service\JoinRoomService; use Base\BaseData; use Hyperf\Utils\ApplicationContext; class DispatchMessage { public function dispatch(BaseData $baseData, int $fd) { $container = ApplicationContext::getContainer(); switch ($baseData->getBody()) { case 'join_room_request': return $container->get(JoinRoomService::class)->joinRoom($baseData->getJoinRoomRequest(), $fd); default: throw new \Exception('Invalid body', 400); } } }