add tests for controllers and ActionCable channels; improve main backend docs

This commit is contained in:
2026-10-27 10:32:40 +03:00
parent b122cbb652
commit 50e8aacfa9
17 changed files with 1127 additions and 63 deletions
+135
View File
@@ -0,0 +1,135 @@
# Архитектура Main Server (Ruby on Rails)
Документ описывает архитектуру главного сервера проекта «Ruby Sea Battle», структуру компонентов Rails-приложения, модель безопасности и схему взаимодействия с клиентом и игровым сервером.
---
## 1. Роль Main Server в общей системе
Система «Ruby Sea Battle» состоит из трёх ключевых узлов:
1. **Frontend (React):** SPA-интерфейс в браузере.
2. **Main Server (Ruby on Rails):** центральный сервер бизнес-логики, аккаунтов и матчмейкинга.
3. **Game Server (Go):** сервер игровых комнат в реальном времени по WebSocket.
```mermaid
flowchart LR
Client["React Frontend"]
MainServer["Main Server (Rails 8)"]
RedisStore[("Redis (Очереди и Pub/Sub)")]
DB[(База данных SQLite/PostgreSQL)]
GameServer["Game Server (Go)"]
Client -- "REST API (Auth, Profile)" --> MainServer
Client -- "WebSocket /cable (Matchmaking)" --> MainServer
MainServer -- "State & Lua Scripts" --> RedisStore
MainServer -- "Пользователи и рейтинг" --> DB
MainServer -. "JWT с общим секретом" .-> GameServer
Client -- "WebSocket (Игровой процесс)" --> GameServer
```
### Основные обязанности Main Server:
- Регистрация и аутентификация пользователей с выдачей JWT.
- Хранение профилей и аватарок игроков (через Active Storage).
- Ведение базового рейтинга пользователей (`users.rating`).
- Очередь матчмейкинга и подбор соперников в реальном времени через Action Cable и Redis.
- Выпуск одноразовых билетов матча (`game_token`) для подключения к игровому серверу Go.
---
## 2. Структура слоёв приложения
Главный сервер построен на принципах Rails API-only приложения:
```
app/
├── channels/
│ ├── application_cable/
│ │ ├── channel.rb # Базовый класс каналов
│ │ └── connection.rb # Аутентификация WebSocket по JWT
│ └── matchmaking_channel.rb # Канал поиска соперника (start/stop/claim)
├── controllers/
│ ├── application_controller.rb # Базовый контроллер: проверка JWT и current_user
│ └── api/
│ ├── registrations_controller.rb # Регистрация и загрузка аватара
│ ├── sessions_controller.rb # Вход, выход, профиль (/api/me)
│ └── matchmaking_controller.rb # HTTP-отмена поиска
├── jobs/
│ ├── application_job.rb # Базовый класс фоновых задач
│ └── matchmaking_sweep_job.rb # Периодическая проверка очереди (Solid Queue)
├── models/
│ ├── application_record.rb # Базовый класс моделей
│ └── user.rb # Модель игрока (никнейм, пароль, рейтинг, аватар)
└── services/
├── jwt_service.rb # Кодирование и валидация JWT
└── matchmaking_service.rb # Логика очереди и вызовы Lua в Redis
```
---
## 3. Компоненты приложения
### 3.1. Контроллеры и фильтры аутентификации
Все защищённые маршруты наследуются от `ApplicationController`.
- **`authenticate_request!`:** выполняет проверку токена перед каждым действием.
- **Извлечение токена (`extract_token`):** поддерживает как заголовок `Authorization: Bearer <token>`, так и параметр строки запроса `params[:token]`.
- При отсутствии или невалидности токена клиенту возвращается стандартизированный ответ:
```json
{ "error": "Unauthorized" }
```
со статусом `401 Unauthorized`.
### 3.2. Модель пользователя (`User`)
- Использует механизм `has_secure_password` (BCrypt).
- **Нормализация никнейма:** автоматическая очистка крайних пробелов через `normalizes :nickname, with: ->(n) { n.strip }`.
- **Валидации:**
- Никнейм: обязателен, длина от 3 до 24 символов, регистронезависимая уникальность.
- Пароль: минимум 6 символов.
- Рейтинг: обязателен, значение по умолчанию в БД — `100`.
- Аватар (`has_one_attached :avatar`): проверка допустимых типов файлов (`image/png`, `image/jpeg`, `image/gif`, `image/webp`).
### 3.3. Action Cable и WebSocket
- **Подключение (`ApplicationCable::Connection`):** при коннекте к `/cable?token=...` сокет проверяет подпись токена. Если токен валиден и пользователь найден в базе, сокет идентифицируется как `current_user`. В противном случае вызывается `reject_unauthorized_connection`.
- **Канал `MatchmakingChannel`:**
- При подписке клиент слушает индивидуальный стрим `matchmaking:<user_id>`.
- Метод `claim` обеспечивает атомарную выдачу уже найденного матча, если клиент переподключился.
- Действие `start` запускает подбор.
- Действие `stop` или закрытие соединения (`unsubscribed`) отменяет поиск.
### 3.4. Сервисный слой
- **`JwtService`:**
- Алгоритм: HMAC-SHA256 (`HS256`).
- Секретный ключ: `Rails.application.secret_key_base`.
- Стандартный срок действия токена авторизации: 7 дней.
- Обработка исключений `JWT::DecodeError` и `JWT::ExpiredSignature` с возвратом `nil`.
- **`MatchmakingService`:**
- Инкапсулирует вызовы Redis Lua-скриптов (`PAIR_SCRIPT`, `FINALIZE_SCRIPT`, `CANCEL_SCRIPT`).
- Выполняет создание матчей, подписание игровых токенов и бродкаст уведомлений через `MatchmakingChannel`.
### 3.5. Фоновые задачи (Background Processing)
- В качестве движка фоновых задач используется `Solid Queue` (на базе БД) с периодическим расписанием `config/recurring.yml`.
- `MatchmakingSweepJob` запускается каждые 2 секунды (`MATCHMAKING_SWEEP_INTERVAL`) и вызывает `MatchmakingService.sweep` для постепенного расширения диапазона поиска и очистки таймаутов.
---
## 4. Модель токенов и безопасность
В системе используются два вида JWT:
| Параметр | User Auth Token | Game Match Token (`game_token`) |
|----------|-----------------|---------------------------------|
| **Назначение** | Авторизация запросов к REST API и Action Cable | Доступ к комнате матча на `game-server` |
| **Payload** | `{ "user_id": 1, "exp": ... }` | `{ "match_id": "...", "player1_id": 1, "player2_id": 2, "game_server": "...", "exp": ... }` |
| **Срок жизни** | 7 дней | `MATCHMAKING_TOKEN_TTL` (по умолчанию 120 сек) |
| **Секрет подписи** | `SECRET_KEY_BASE` | `SECRET_KEY_BASE` (общий для Rails и Go) |
| **Алгоритм** | HS256 | HS256 |
### Безопасность делегирования на Game Server:
1. Игровой сервер Go не имеет прямого доступа к базе данных пользователей Main Server.
2. При подключении к Game Server игрок предъявляет `game_token`.
3. Game Server валидирует подпись токена тем же общим секретом `SECRET_KEY_BASE`, сверяет `match_id` и идентификатор подключившегося игрока (`player1_id` или `player2_id`).
4. Токен действует всего 120 секунд, что исключает возможность повторного использования после начала игры.