# Архитектура 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 `, так и параметр строки запроса `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:`. - Метод `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 секунд, что исключает возможность повторного использования после начала игры.