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
+101 -47
View File
@@ -2,6 +2,11 @@
Центральный сервер для браузерной онлайн-игры «Морской бой». Предоставляет REST API для регистрации, аутентификации и матчмейкинга.
## Дополнительная документация
- [docs/matchmaking.md](docs/matchmaking.md) — детальное описание алгоритма матчмейкинга, структур данных Redis, формулы допуска рейтинга, Lua-скриптов и гарантий доставки.
- [docs/architecture.md](docs/architecture.md) — архитектура главного сервера, стек компонентов Rails, безопасность и взаимодействие с Game Server.
## Базовый URL
```
@@ -10,13 +15,17 @@ http://localhost:3000/api
## Аутентификация
Все endpoints (кроме `/register` и `/login`) требуют JWT-токен в заголовке:
Все endpoints (кроме `/register` и `/login`) требуют JWT-токен. Токен может передаваться двумя способами:
1. В HTTP-заголовке (рекомендуемый способ для REST API):
```
Authorization: Bearer <token>
```
2. В параметрах запроса query string (используется для WebSocket-подключений и поддерживается REST API):
```
?token=<token>
```
Токен выдаётся при регистрации или входе и действителен **7 дней**.
Токен выдаётся при успешной регистрации или входе и действителен **7 дней**. Токен содержит идентификатор пользователя (`user_id`) и время истечения (`exp`). При обращении к защищённым endpoints сервер проверяет подпись токена, срок действия и существование пользователя в базе данных.
---
@@ -209,65 +218,110 @@ curl -X DELETE http://localhost:3000/api/logout \
### Поиск соперника (Матчмейкинг)
Поиск соперника происходит через **WebSocket** (`Action Cable`), а не через long-polling: HTTP-поток не блокируется, а результат доставляется сервером push-уведомлением, как только найден соперник.
Поиск соперника происходит через **WebSocket** на базе протокола `Action Cable`. Это устраняет необходимость long-polling: сокет не блокирует HTTP-потоки, а результат поиска доставляется сервером асинхронно push-уведомлением.
> Подробное описание структур данных в Redis, формулы допуска рейтинга, двухфазного поиска и Lua-скриптов вынесено в отдельный документ: [docs/matchmaking.md](docs/matchmaking.md).
**URL подключения:** `ws://localhost:3000/cable?token=<JWT>`
**Auth:** токен передаётся параметром `token` в query string (тот же JWT, что и для HTTP API).
**Auth:** токен передаётся параметром `token` в query string (или в заголовке `Authorization: Bearer <JWT>` при поддержке клиентом). Если токен отсутствует, просрочен или пользователь не найден, сервер немедленно закрывает соединение (`reject_unauthorized_connection`).
**Канал:** `MatchmakingChannel`
#### Поток взаимодействия
#### Протокол взаимодействия Action Cable
1. Подключиться к `/cable?token=...` и подписаться на канал `MatchmakingChannel`.
2. Отправить команду начала поиска:
Action Cable использует формат JSON-сообщений через WebSocket:
```json
{ "action": "start" }
```
1. **Подключение и рукопожатие:**
При открытии сокета сервер отправляет приветственное сообщение:
```json
{ "type": "welcome" }
```
В процессе работы сервер регулярно отправляет heartbeat-сообщения (пинг):
```json
{ "type": "ping", "message": 1729864800 }
```
Рейтинг для подбора берётся из базы данных (`users.rating`). У новых пользователей он инициализируется нейтральным значением `100` (значение задано в БД); в будущем рейтинг будет пересчитываться по результатам матчей. Игроки подбираются по близости рейтинга; допуск расширяется с ростом времени ожидания.
2. **Подписка на канал:**
Клиент отправляет команду подписки на канал `MatchmakingChannel`:
```json
{
"command": "subscribe",
"identifier": "{\"channel\":\"MatchmakingChannel\"}"
}
```
При успешной подписке сервер отвечает подтверждением:
```json
{
"type": "confirm_subscription",
"identifier": "{\"channel\":\"MatchmakingChannel\"}"
}
```
*Примечание:* Если матч был сформирован в момент временного разрыва соединения (кратковременный реконнект), сервер в момент подписки выполняет операцию `claim` и немедленно передаёт клиенту сообщение `matched` (гарантия exactly-once доставки).
3. Дождаться одного из сообщений:
3. **Запуск поиска соперника:**
Для входа в очередь матчмейкинга клиент отправляет action `start`:
```json
{
"command": "message",
"identifier": "{\"channel\":\"MatchmakingChannel\"}",
"data": "{\"action\":\"start\"}"
}
```
Рейтинг для подбора считывается из БД (`users.rating`, по умолчанию `100`). Игрок помещается в Redis (`mm:queue`). Сервер сразу выполняет попытку мгновенного подбора (Phase 1). Если подходящий соперник не найден сразу, фоновый воркер `MatchmakingSweepJob` каждые 2 секунды проверяет очередь (Phase 2), плавно расширяя допустимый диапазон рейтинга по мере ожидания.
**Матч найден:**
4. **Получение результатов поиска:**
```json
{
"type": "matched",
"match_id": "145575d6-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
"game_token": "eyJhbGciOiJIUzI1NiJ9.eyJtYXRjaF9pZCI6IjE...",
"game_server": "ws://localhost:8080",
"players": [1, 2]
}
```
**Матч найден:**
```json
{
"identifier": "{\"channel\":\"MatchmakingChannel\"}",
"message": {
"type": "matched",
"match_id": "145575d6-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
"game_token": "eyJhbGciOiJIUzI1NiJ9.eyJtYXRjaF9pZCI6IjE...",
"game_server": "ws://localhost:8080",
"players": [1, 2]
}
}
```
| Поле | Описание |
|------|----------|
| `match_id` | Уникальный идентификатор матча (UUID) |
| `game_token` | JWT-токен для аутентификации на игровом сервере |
| `game_server` | WebSocket-адрес игрового сервера |
| `players` | ID обоих игроков `[player1_id, player2_id]` |
| Поле | Описание |
|------|----------|
| `match_id` | Уникальный UUID матча, сгенерированный сервером |
| `game_token` | Подписанный JWT-токен для авторизации на `game-server` (действителен `MATCHMAKING_TOKEN_TTL` сек) |
| `game_server` | WebSocket URL игрового сервера Go, на который должны подключиться оба игрока |
| `players` | Массив идентификаторов игроков матча `[player1_id, player2_id]` |
**Таймаут поиска** (соперник не найден за `MATCHMAKING_QUEUE_TTL` секунд):
**Таймаут поиска** (соперник не найден за `MATCHMAKING_QUEUE_TTL` секунд, по умолчанию 60 сек):
```json
{
"identifier": "{\"channel\":\"MatchmakingChannel\"}",
"message": {
"type": "timeout"
}
}
```
После таймаута игрок удаляется из очереди поиска. При необходимости поиск можно начать заново командой `start`.
```json
{ "type": "timeout" }
```
4. Поиск отменяется автоматически при закрытии WebSocket-подписки. Также можно явно отправить:
```json
{ "action": "stop" }
```
**Примечание:** `game_token` содержит `match_id`, `player1_id`, `player2_id`, `game_server` и действителен `MATCHMAKING_TOKEN_TTL` секунд.
---
### Отмена поиска
Отмена поиска происходит через WebSocket: достаточно отключиться от `MatchmakingChannel` либо отправить `{ "action": "stop" }`.
5. **Отмена поиска:**
Отменить поиск можно двумя способами:
- Отправить команду `stop`:
```json
{
"command": "message",
"identifier": "{\"channel\":\"MatchmakingChannel\"}",
"data": "{\"action\":\"stop\"}"
}
```
- Отписаться от канала или закрыть WebSocket-соединение:
```json
{
"command": "unsubscribe",
"identifier": "{\"channel\":\"MatchmakingChannel\"}"
}
```
Сервер атомарно удаляет игрока из всех очередей и хешей Redis.
---