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

136 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура 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 секунд, что исключает возможность повторного использования после начала игры.