14 KiB
Sea Battle — Main Server API
Центральный сервер для браузерной онлайн-игры «Морской бой». Предоставляет REST API для регистрации, аутентификации и матчмейкинга.
Дополнительная документация
- docs/matchmaking.md — детальное описание алгоритма матчмейкинга, структур данных Redis, формулы допуска рейтинга, Lua-скриптов и гарантий доставки.
- docs/architecture.md — архитектура главного сервера, стек компонентов Rails, безопасность и взаимодействие с Game Server.
Базовый URL
http://localhost:3000/api
Аутентификация
Все endpoints (кроме /register и /login) требуют JWT-токен. Токен может передаваться двумя способами:
- В HTTP-заголовке (рекомендуемый способ для REST API):
Authorization: Bearer <token>
- В параметрах запроса query string (используется для WebSocket-подключений и поддерживается REST API):
?token=<token>
Токен выдаётся при успешной регистрации или входе и действителен 7 дней. Токен содержит идентификатор пользователя (user_id) и время истечения (exp). При обращении к защищённым endpoints сервер проверяет подпись токена, срок действия и существование пользователя в базе данных.
Endpoints
Регистрация
Создаёт нового пользователя и возвращает токен.
URL: POST /api/register
Auth: не требуется
Content-Type: application/json или multipart/form-data (при загрузке аватара)
Параметры (JSON):
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
user.nickname |
string | ✅ | Уникальный никнейм (3–24 символа) |
user.password |
string | ✅ | Пароль (минимум 6 символов) |
user.password_confirmation |
string | ✅ | Подтверждение пароля |
user.avatar |
file | — | Аватар (PNG, JPEG, GIF, WebP) |
Пример запроса (JSON):
curl -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{
"user": {
"nickname": "CaptainNemo",
"password": "secret123",
"password_confirmation": "secret123"
}
}'
Пример запроса (с аватаром):
curl -X POST http://localhost:3000/api/register \
-F "user[nickname]=CaptainNemo" \
-F "user[password]=secret123" \
-F "user[password_confirmation]=secret123" \
-F "user[avatar]=@avatar.png"
Успешный ответ (201 Created):
{
"user": {
"id": 1,
"nickname": "CaptainNemo",
"avatar_url": "http://localhost:3000/rails/active_storage/blobs/...",
"created_at": "2026-09-21T10:00:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjox..."
}
Ошибки (422 Unprocessable Entity):
{
"errors": [
"Nickname has already been taken",
"Password is too short (minimum is 6 characters)"
]
}
Вход
Аутентифицирует пользователя и возвращает токен.
URL: POST /api/login
Auth: не требуется
Content-Type: application/json
Параметры:
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
nickname |
string | ✅ | Никнейм |
password |
string | ✅ | Пароль |
Пример запроса:
curl -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{
"nickname": "CaptainNemo",
"password": "secret123"
}'
Успешный ответ (200 OK):
{
"user": {
"id": 1,
"nickname": "CaptainNemo",
"avatar_url": "http://localhost:3000/rails/active_storage/blobs/...",
"created_at": "2026-09-21T10:00:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjox..."
}
Ошибка (401 Unauthorized):
{
"error": "Invalid nickname or password"
}
Текущий пользователь
Возвращает данные авторизованного пользователя.
URL: GET /api/me
Auth: требуется
Пример запроса:
curl -X GET http://localhost:3000/api/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Успешный ответ (200 OK):
{
"user": {
"id": 1,
"nickname": "CaptainNemo",
"avatar_url": "http://localhost:3000/rails/active_storage/blobs/...",
"created_at": "2026-09-21T10:00:00.000Z"
}
}
Ошибка (401 Unauthorized):
{
"error": "Unauthorized"
}
Выход
Завершает сессию. JWT является stateless — клиент должен удалить токен.
URL: DELETE /api/logout
Auth: требуется
Пример запроса:
curl -X DELETE http://localhost:3000/api/logout \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
Успешный ответ (200 OK):
{
"message": "Signed out successfully"
}
Поиск соперника (Матчмейкинг)
Поиск соперника происходит через WebSocket на базе протокола Action Cable. Это устраняет необходимость long-polling: сокет не блокирует HTTP-потоки, а результат поиска доставляется сервером асинхронно push-уведомлением.
Подробное описание структур данных в Redis, формулы допуска рейтинга, двухфазного поиска и Lua-скриптов вынесено в отдельный документ: docs/matchmaking.md.
URL подключения: ws://localhost:3000/cable?token=<JWT>
Auth: токен передаётся параметром token в query string (или в заголовке Authorization: Bearer <JWT> при поддержке клиентом). Если токен отсутствует, просрочен или пользователь не найден, сервер немедленно закрывает соединение (reject_unauthorized_connection).
Канал: MatchmakingChannel
Протокол взаимодействия Action Cable
Action Cable использует формат JSON-сообщений через WebSocket:
-
Подключение и рукопожатие: При открытии сокета сервер отправляет приветственное сообщение:
{ "type": "welcome" }В процессе работы сервер регулярно отправляет heartbeat-сообщения (пинг):
{ "type": "ping", "message": 1729864800 } -
Подписка на канал: Клиент отправляет команду подписки на канал
MatchmakingChannel:{ "command": "subscribe", "identifier": "{\"channel\":\"MatchmakingChannel\"}" }При успешной подписке сервер отвечает подтверждением:
{ "type": "confirm_subscription", "identifier": "{\"channel\":\"MatchmakingChannel\"}" }Примечание: Если матч был сформирован в момент временного разрыва соединения (кратковременный реконнект), сервер в момент подписки выполняет операцию
claimи немедленно передаёт клиенту сообщениеmatched(гарантия exactly-once доставки). -
Запуск поиска соперника: Для входа в очередь матчмейкинга клиент отправляет action
start:{ "command": "message", "identifier": "{\"channel\":\"MatchmakingChannel\"}", "data": "{\"action\":\"start\"}" }Рейтинг для подбора считывается из БД (
users.rating, по умолчанию100). Игрок помещается в Redis (mm:queue). Сервер сразу выполняет попытку мгновенного подбора (Phase 1). Если подходящий соперник не найден сразу, фоновый воркерMatchmakingSweepJobкаждые 2 секунды проверяет очередь (Phase 2), плавно расширяя допустимый диапазон рейтинга по мере ожидания. -
Получение результатов поиска:
Матч найден:
{ "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(действителенMATCHMAKING_TOKEN_TTLсек)game_serverWebSocket URL игрового сервера Go, на который должны подключиться оба игрока playersМассив идентификаторов игроков матча [player1_id, player2_id]Таймаут поиска (соперник не найден за
MATCHMAKING_QUEUE_TTLсекунд, по умолчанию 60 сек):{ "identifier": "{\"channel\":\"MatchmakingChannel\"}", "message": { "type": "timeout" } }После таймаута игрок удаляется из очереди поиска. При необходимости поиск можно начать заново командой
start. -
Отмена поиска: Отменить поиск можно двумя способами:
- Отправить команду
stop:{ "command": "message", "identifier": "{\"channel\":\"MatchmakingChannel\"}", "data": "{\"action\":\"stop\"}" } - Отписаться от канала или закрыть WebSocket-соединение:
{ "command": "unsubscribe", "identifier": "{\"channel\":\"MatchmakingChannel\"}" }
Сервер атомарно удаляет игрока из всех очередей и хешей Redis.
- Отправить команду
Health Check
Проверка доступности сервера.
URL: GET /up
Auth: не требуется
Успешный ответ (200 OK):
<!DOCTYPE html><html><body style="background-color: green"></body></html>
Коды ошибок
| Код | Описание |
|---|---|
200 |
Успешный запрос |
201 |
Ресурс создан (регистрация) |
401 |
Неавторизован (отсутствует/невалиден токен) |
422 |
Ошибка валидации |
500 |
Внутренняя ошибка сервера |
Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
REDIS_URL |
redis://localhost:6380/0 |
Подключение к Redis (матчмейкинг и pub/sub Action Cable) |
GAME_SERVER_URL |
ws://localhost:8080 |
Адрес игрового сервера для матчмейкинга |
MATCHMAKING_QUEUE_TTL |
60 |
Время ожидания соперника до таймаута, сек |
MATCHMAKING_PENDING_TTL |
15 |
Время ожидания финализации пары, сек |
MATCHMAKING_MATCH_TTL |
120 |
Время жизни данных матча, сек |
MATCHMAKING_TOKEN_TTL |
120 |
Время жизни game_token, сек |
MATCHMAKING_BASE_RANGE |
100 |
Начальный допуск по рейтингу |
MATCHMAKING_RANGE_PER_SECOND |
50 |
Рост допуска за секунду ожидания |
MATCHMAKING_MAX_RANGE |
1000 |
Максимальный допуск по рейтингу |
MATCHMAKING_SWEEP_INTERVAL |
2 |
Периодичность фоновой сверки очереди, сек |
CORS_ORIGINS |
* |
Разрешённые origins для CORS (через запятую) |
SECRET_KEY_BASE |
из credentials.yml.enc |
Секретный ключ для JWT |
Пример workflow: от регистрации до матча
# 1. Регистрация
TOKEN=$(curl -s -X POST http://localhost:3000/api/register \
-H "Content-Type: application/json" \
-d '{"user":{"nickname":"Player1","password":"secret123","password_confirmation":"secret123"}}' \
| jq -r '.token')
# 2. Проверка профиля
curl -s http://localhost:3000/api/me \
-H "Authorization: Bearer $TOKEN"
# 3. Поиск матча — WebSocket (например, через wscat):
# wscat -c "ws://localhost:3000/cable?token=$TOKEN"
# > {"command":"subscribe","identifier":"{\"channel\":\"MatchmakingChannel\"}"}
# > {"command":"message","identifier":"{\"channel\":\"MatchmakingChannel\"}","data":"{\"action\":\"start\"}"}