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