add tests for controllers and ActionCable channels; improve main backend docs
This commit is contained in:
+101
-47
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user