Files
ruby-sea-battle/main-server/API.md
T

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-токен. Токен может передаваться двумя способами:

  1. В HTTP-заголовке (рекомендуемый способ для REST API):
Authorization: Bearer <token>
  1. В параметрах запроса 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:

  1. Подключение и рукопожатие: При открытии сокета сервер отправляет приветственное сообщение:

    { "type": "welcome" }
    

    В процессе работы сервер регулярно отправляет heartbeat-сообщения (пинг):

    { "type": "ping", "message": 1729864800 }
    
  2. Подписка на канал: Клиент отправляет команду подписки на канал MatchmakingChannel:

    {
      "command": "subscribe",
      "identifier": "{\"channel\":\"MatchmakingChannel\"}"
    }
    

    При успешной подписке сервер отвечает подтверждением:

    {
      "type": "confirm_subscription",
      "identifier": "{\"channel\":\"MatchmakingChannel\"}"
    }
    

    Примечание: Если матч был сформирован в момент временного разрыва соединения (кратковременный реконнект), сервер в момент подписки выполняет операцию claim и немедленно передаёт клиенту сообщение matched (гарантия exactly-once доставки).

  3. Запуск поиска соперника: Для входа в очередь матчмейкинга клиент отправляет action start:

    {
      "command": "message",
      "identifier": "{\"channel\":\"MatchmakingChannel\"}",
      "data": "{\"action\":\"start\"}"
    }
    

    Рейтинг для подбора считывается из БД (users.rating, по умолчанию 100). Игрок помещается в Redis (mm:queue). Сервер сразу выполняет попытку мгновенного подбора (Phase 1). Если подходящий соперник не найден сразу, фоновый воркер MatchmakingSweepJob каждые 2 секунды проверяет очередь (Phase 2), плавно расширяя допустимый диапазон рейтинга по мере ожидания.

  4. Получение результатов поиска:

    Матч найден:

    {
      "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_server WebSocket URL игрового сервера Go, на который должны подключиться оба игрока
    players Массив идентификаторов игроков матча [player1_id, player2_id]

    Таймаут поиска (соперник не найден за MATCHMAKING_QUEUE_TTL секунд, по умолчанию 60 сек):

    {
      "identifier": "{\"channel\":\"MatchmakingChannel\"}",
      "message": {
        "type": "timeout"
      }
    }
    

    После таймаута игрок удаляется из очереди поиска. При необходимости поиск можно начать заново командой start.

  5. Отмена поиска: Отменить поиск можно двумя способами:

    • Отправить команду 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\"}"}