Files
ruby-sea-battle/main-server/docs/architecture.md
T

9.1 KiB
Raw Blame History

Архитектура 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.
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].
  • При отсутствии или невалидности токена клиенту возвращается стандартизированный ответ:
    { "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 секунд, что исключает возможность повторного использования после начала игры.