diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 2cb10a8..a643c90 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -29,4 +29,27 @@ jobs: run: | docker compose -f docker-compose.prod.yml build docker compose -f docker-compose.prod.yml up -d - docker image prune -f + + # Если сборка/подъём упали — шаг упадёт, а старые контейнеры продолжат + # работать (up -d не выполнился). Проверяем, что новое поднялось по + # healthcheck'ам обоих сервисов, и что nginx реально отдаёт главную. + - name: Smoke check + working-directory: /data/docker/compose/cactoz + run: | + n=starting + g=starting + for i in $(seq 1 30); do + n=$(docker inspect -f '{{.State.Health.Status}}' cactoz-nginx 2>/dev/null || echo starting) + g_id=$(docker compose -f docker-compose.prod.yml ps -q gameserver) + g=$([ -n "$g_id" ] && docker inspect -f '{{.State.Health.Status}}' "$g_id" 2>/dev/null || echo starting) + if [ "$n" = "healthy" ] && [ "$g" = "healthy" ]; then break; fi + sleep 3 + done + docker compose -f docker-compose.prod.yml ps + [ "$n" = "healthy" ] + [ "$g" = "healthy" ] + docker exec cactoz-nginx wget -qO /dev/null http://127.0.0.1/ + + - name: Prune dangling images + working-directory: /data/docker/compose/cactoz + run: docker image prune -f diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d5622d --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# cactoz.su + +Личный сайт-визитка на `cactoz.su` в Matrix-стилистике: резюме, проекты и +браузерные игры. Три страницы статики + мультиплеерный сервер для `./tanks`. +Исходный замысел и игровой протокол — в [TZ.md](TZ.md) (⚠️ местами устарел). + +## Архитектура + +``` + ┌────────── homelab (cacto-homelab) ──────────┐ + push в main │ │ + в Gitea ────────────► │ gitea-actions runner (docker.sock хоста) │ + git.cactoz.su │ │ клонирует, синкает, собирает │ + │ ▼ │ + │ /data/docker/compose/cactoz │ + │ docker compose -f docker-compose.prod.yml │ + │ ├── nginx (статика + прокси /ws) │ + │ └── gameserver (C++/uWebSockets, :9001) │ + │ │ │ + │ ▼ (external docker-сеть proxy) │ + │ Caddy-стек (TLS-терминация 80/443) │ + └──────────────────────────────────────────────┘ +``` + +- **Деплой**: push в `main` → workflow `.gitea/workflows/deploy.yml` → build + + `up -d` + smoke-проверка по healthcheck'ам. Отката нет: если сборка/подъём + падают, шлётся уведомление, старые контейнеры продолжают работать. +- **TLS** терминирует отдельный Caddy-стек, сеть `proxy` создаётся там + (`external: true`) — на этом хосте только подключаемся. +- Gameserver изнутри доступен только по docker-сети `internal`; наружу + смотрит только nginx (через Caddy). + +## Локальный запуск + +```bash +docker compose up --build # http://localhost, /ws на ws://localhost/ws +``` + +Сборка gameserver тянет зависимости через CMake FetchContent (uWebSockets, +uSockets, nlohmann/json) — первый билд долгий, дальше кэшируется слоями Docker. + +## Структура + +``` +frontend/ чистый HTML/CSS/JS, без сборки + vendor/fonts/ self-hosted JetBrains Mono (OFL), 4 сабсета × 4 насыщенности + js/game/ клиент ./tanks: network (WS + ping 25с), lobby, battle + js/blocks.js, js/invaders.js одиночные мини-игры (canvas, без сервера) +gameserver/ + src/main.cpp uWS::App, /ws, лимиты фреймов/таймауты + src/Lobby.* лобби: комнаты, broadcast, ping/pong, события в stdout + src/Game.* авторитетная симуляция (20 Гц), pvp и coop с AI +nginx/ статика + проксирование /ws (rate-limit на соединения) +``` + +## Runbook + +```bash +# на homelab: +cd /data/docker/compose/cactoz +docker compose -f docker-compose.prod.yml ps # статус + healthcheck'и +docker logs -f cactoz-gameserver-... 2>&1 | less # события лобби/игр +docker compose -f docker-compose.prod.yml up -d --build # ручной перезапуск +``` + +- Логи ротируются (`max-size 10m × 3`), лимиты ресурсов: nginx 128M/0.5cpu, + gameserver 256M/1cpu. +- Gameserver не умеет graceful shutdown — при рестарте активные матчи обрываются + (комнаты сами поднимаются заново). +- Статика без хешей в именах: js/css отдаются с `no-cache`, `/assets/` — час. + +## Доступ и безопасность + +- SSH на homelab: ключи, вход только с доверенных сетей; sudo — по необходимости. +- Gitea (`git.cactoz.su`) — источник истины для кода; секретов в репо нет. +- За пределами compose на хосте живут Caddy-стек, Gitea и её runner — их + конфиги не входят в этот репозиторий и бэкапятся отдельно. diff --git a/TZ.md b/TZ.md index ad6aca6..299a5ca 100644 --- a/TZ.md +++ b/TZ.md @@ -1,5 +1,10 @@ # ТЗ: cactoz.su +> ⚠️ Это ТЗ исходного замысла (MVP). Реальность разошлась: появились мини-игры +> `./blocks` и `./invaders`, самостоятельная страница `./tanks` со своим JS, +> шрифты self-hosted, healthcheck'и в compose, структура `gameserver/src` компактнее +> описанной. Актуальная архитектура деплоя и runbook — в [README.md](README.md). + ## 1. Общее Личный сайт-визитка на домене `cactoz.su`. Три страницы, единая Matrix-стилистика. diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 43233e8..78739fe 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -10,12 +10,54 @@ services: depends_on: - gameserver restart: unless-stopped + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + healthcheck: + test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 5s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 128M + cpus: "0.5" gameserver: build: ./gameserver networks: - internal restart: unless-stopped + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/127.0.0.1/9001'"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 256M + cpus: "1.0" networks: internal: diff --git a/frontend/blocks.html b/frontend/blocks.html index 0cb894c..6088782 100644 --- a/frontend/blocks.html +++ b/frontend/blocks.html @@ -5,9 +5,7 @@ ./blocks — cacto - - - + diff --git a/frontend/css/fonts.css b/frontend/css/fonts.css new file mode 100644 index 0000000..70fb486 --- /dev/null +++ b/frontend/css/fonts.css @@ -0,0 +1,149 @@ +/* Self-hosted JetBrains Mono (сабсеты cyrillic/cyrillic-ext/latin/latin-ext). + + Источник: Google Fonts, OFL-лицензия. Заменяет fonts.googleapis.com — + + убирает внешнюю зависимость и лишние хосты из CSP. */ + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/vendor/fonts/jbmono-400-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/vendor/fonts/jbmono-400-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/vendor/fonts/jbmono-400-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/vendor/fonts/jbmono-400-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/vendor/fonts/jbmono-500-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/vendor/fonts/jbmono-500-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/vendor/fonts/jbmono-500-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/vendor/fonts/jbmono-500-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/vendor/fonts/jbmono-600-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/vendor/fonts/jbmono-600-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/vendor/fonts/jbmono-600-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('/vendor/fonts/jbmono-600-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/vendor/fonts/jbmono-700-cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/vendor/fonts/jbmono-700-cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/vendor/fonts/jbmono-700-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('/vendor/fonts/jbmono-700-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/frontend/game.html b/frontend/game.html index ad5fb35..6565937 100644 --- a/frontend/game.html +++ b/frontend/game.html @@ -5,9 +5,7 @@ ./tanks — cacto - - - + diff --git a/frontend/index.html b/frontend/index.html index 75298e7..71f85ba 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,9 +13,7 @@ - - - + diff --git a/frontend/invaders.html b/frontend/invaders.html index 9e88467..e834ed3 100644 --- a/frontend/invaders.html +++ b/frontend/invaders.html @@ -5,9 +5,7 @@ ./invaders — cacto - - - + diff --git a/frontend/js/game/network.js b/frontend/js/game/network.js index fe6784e..7109c92 100644 --- a/frontend/js/game/network.js +++ b/frontend/js/game/network.js @@ -12,6 +12,15 @@ export class Network { const msg = JSON.parse(event.data); this._emit(msg.type, msg.payload); }); + + // NAT-таблицы на промежуточных роутерах гаснут раньше, чем серверный + // idleTimeout (300с) — application-ping раз в 25с держит соединение живым + // и на случай обрыва держим интервал заметно меньше таймаута. + setInterval(() => { + if (this.socket.readyState === WebSocket.OPEN) { + this.send(ClientMessage.PING); + } + }, 25000); } on(type, handler) { diff --git a/frontend/js/game/protocol.js b/frontend/js/game/protocol.js index bc7ebcf..51c180f 100644 --- a/frontend/js/game/protocol.js +++ b/frontend/js/game/protocol.js @@ -1,6 +1,7 @@ // Типы сообщений из TZ.md, раздел 5. export const ClientMessage = { HELLO: 'hello', + PING: 'ping', LOBBY_LIST_ROOMS: 'lobby.list_rooms', LOBBY_CREATE_ROOM: 'lobby.create_room', LOBBY_JOIN_ROOM: 'lobby.join_room', @@ -11,6 +12,7 @@ export const ClientMessage = { export const ServerMessage = { HELLO_ACK: 'hello.ack', + PONG: 'pong', ERROR: 'error', LOBBY_ROOMS: 'lobby.rooms', LOBBY_ROOM_JOINED: 'lobby.room_joined', diff --git a/frontend/projects.html b/frontend/projects.html index 4619bbc..bfc59a1 100644 --- a/frontend/projects.html +++ b/frontend/projects.html @@ -7,9 +7,7 @@ - - - + diff --git a/frontend/vendor/fonts/jbmono-400-cyrillic-ext.woff2 b/frontend/vendor/fonts/jbmono-400-cyrillic-ext.woff2 new file mode 100644 index 0000000..7f62749 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-400-cyrillic-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-400-cyrillic.woff2 b/frontend/vendor/fonts/jbmono-400-cyrillic.woff2 new file mode 100644 index 0000000..885d480 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-400-cyrillic.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-400-latin-ext.woff2 b/frontend/vendor/fonts/jbmono-400-latin-ext.woff2 new file mode 100644 index 0000000..11da8b4 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-400-latin-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-400-latin.woff2 b/frontend/vendor/fonts/jbmono-400-latin.woff2 new file mode 100644 index 0000000..5b9b02f Binary files /dev/null and b/frontend/vendor/fonts/jbmono-400-latin.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-500-cyrillic-ext.woff2 b/frontend/vendor/fonts/jbmono-500-cyrillic-ext.woff2 new file mode 100644 index 0000000..cef2dc4 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-500-cyrillic-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-500-cyrillic.woff2 b/frontend/vendor/fonts/jbmono-500-cyrillic.woff2 new file mode 100644 index 0000000..2a1f220 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-500-cyrillic.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-500-latin-ext.woff2 b/frontend/vendor/fonts/jbmono-500-latin-ext.woff2 new file mode 100644 index 0000000..87940cd Binary files /dev/null and b/frontend/vendor/fonts/jbmono-500-latin-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-500-latin.woff2 b/frontend/vendor/fonts/jbmono-500-latin.woff2 new file mode 100644 index 0000000..a43f1c3 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-500-latin.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-600-cyrillic-ext.woff2 b/frontend/vendor/fonts/jbmono-600-cyrillic-ext.woff2 new file mode 100644 index 0000000..f9b3e90 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-600-cyrillic-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-600-cyrillic.woff2 b/frontend/vendor/fonts/jbmono-600-cyrillic.woff2 new file mode 100644 index 0000000..b015629 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-600-cyrillic.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-600-latin-ext.woff2 b/frontend/vendor/fonts/jbmono-600-latin-ext.woff2 new file mode 100644 index 0000000..d34ede9 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-600-latin-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-600-latin.woff2 b/frontend/vendor/fonts/jbmono-600-latin.woff2 new file mode 100644 index 0000000..71ab3e6 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-600-latin.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-700-cyrillic-ext.woff2 b/frontend/vendor/fonts/jbmono-700-cyrillic-ext.woff2 new file mode 100644 index 0000000..77ddfd7 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-700-cyrillic-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-700-cyrillic.woff2 b/frontend/vendor/fonts/jbmono-700-cyrillic.woff2 new file mode 100644 index 0000000..499bf4b Binary files /dev/null and b/frontend/vendor/fonts/jbmono-700-cyrillic.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-700-latin-ext.woff2 b/frontend/vendor/fonts/jbmono-700-latin-ext.woff2 new file mode 100644 index 0000000..c1e5a07 Binary files /dev/null and b/frontend/vendor/fonts/jbmono-700-latin-ext.woff2 differ diff --git a/frontend/vendor/fonts/jbmono-700-latin.woff2 b/frontend/vendor/fonts/jbmono-700-latin.woff2 new file mode 100644 index 0000000..3728c4b Binary files /dev/null and b/frontend/vendor/fonts/jbmono-700-latin.woff2 differ diff --git a/gameserver/src/Lobby.cpp b/gameserver/src/Lobby.cpp index 07307bd..affbd7c 100644 --- a/gameserver/src/Lobby.cpp +++ b/gameserver/src/Lobby.cpp @@ -1,5 +1,6 @@ #include "Lobby.hpp" #include "Game.hpp" +#include "Log.hpp" #include #include @@ -74,6 +75,10 @@ void Lobby::onOpen(WS *ws) { } void Lobby::onClose(WS *ws) { + if (!ws->getUserData()->room_id.empty()) { + logLine("connection closed in-room: " + ws->getUserData()->room_id + " " + + ws->getUserData()->nickname); + } endActiveGameIfAny(ws->getUserData()->room_id, ws); removePlayerFromRoom(ws); sockets_.erase(ws); @@ -107,6 +112,8 @@ void Lobby::onMessage(WS *ws, std::string_view message) { handleGameReady(ws); } else if (type == "game.input") { handleGameInput(ws, payload); + } else if (type == "ping") { + send(ws, "pong", {}); } else { sendError(ws, "unknown_type", "unknown message type: " + type); } @@ -186,6 +193,8 @@ void Lobby::handleCreateRoom(WS *ws, const json &payload) { std::string room_id = room.id; rooms_[room_id] = std::move(room); + logLine("room created: " + room_id + " mode=" + mode + " by=" + data->nickname); + send(ws, "lobby.room_joined", { {"room_id", room_id}, {"name", room_id}, @@ -227,6 +236,9 @@ void Lobby::handleJoinRoom(WS *ws, const json &payload) { room.players.push_back({data->player_id, data->nickname, ws}); data->room_id = room_id; + logLine("player joined: " + room_id + " " + data->nickname + " (" + + std::to_string(room.players.size()) + "/" + std::to_string(kMaxPlayersPerRoom) + ")"); + send(ws, "lobby.room_joined", { {"room_id", room_id}, {"name", room_id}, @@ -243,6 +255,7 @@ void Lobby::handleLeaveRoom(WS *ws) { sendError(ws, "not_in_room", "you are not in a room"); return; } + logLine("player left: " + ws->getUserData()->room_id + " " + ws->getUserData()->nickname); endActiveGameIfAny(ws->getUserData()->room_id, ws); removePlayerFromRoom(ws); broadcastRoomsToLobby(); @@ -301,7 +314,11 @@ void Lobby::maybeStartGame(const std::string &room_id) { room.state = "playing"; + logLine("game started: " + room_id + " mode=" + room.mode + " players=" + + std::to_string(room.players.size())); + auto game = std::make_unique(room, [this, room_id]() { + logLine("game finished: " + room_id); auto rit = rooms_.find(room_id); if (rit != rooms_.end()) { rit->second.state = "waiting"; @@ -358,6 +375,7 @@ void Lobby::removePlayerFromRoom(WS *ws) { room.players.end()); if (room.players.empty()) { + logLine("room removed: " + it->first + " (empty)"); rooms_.erase(it); } else { broadcastRoomUpdated(room); diff --git a/gameserver/src/Log.hpp b/gameserver/src/Log.hpp new file mode 100644 index 0000000..07aa30d --- /dev/null +++ b/gameserver/src/Log.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +// События лобби/игр пишутся в stdout — их забирает `docker logs`, это единственный +// способ понять в проде, что происходило с комнатами, не заходя в контейнер. +inline void logLine(const std::string &message) { + std::time_t now = std::time(nullptr); + std::tm tm{}; + localtime_r(&now, &tm); + char ts[20]; + std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm); + // endl, а не "\n": stdout в контейнере идёт в пайп и полностью + // буферизуется — без сброса docker logs видит строки только на exit + std::cout << "[" << ts << "] " << message << std::endl; +} diff --git a/gameserver/src/main.cpp b/gameserver/src/main.cpp index ea65b6b..3ed59c3 100644 --- a/gameserver/src/main.cpp +++ b/gameserver/src/main.cpp @@ -4,12 +4,23 @@ #include #include "Lobby.hpp" +#include "Log.hpp" int main() { Lobby lobby; uWS::App() .ws("/ws", { + // сообщения у нас мелкие (input/lobby) — 16 МиБ дефолта не нужен, + // лимит режет попытки завалить сервер гигантскими фреймами + .compression = uWS::DISABLED, + .maxPayloadLength = 8 * 1024, + // сервер сам шлёт WS-ping в пределах idleTimeout; клиент дополнительно + // шлёт application-ping раз в 25с — соединение переживает NAT-таймауты + .idleTimeout = 300, + .maxBackpressure = 16 * 1024, + .closeOnBackpressureLimit = true, + .sendPingsAutomatically = true, .open = [&lobby](auto *ws) { lobby.onOpen(ws); }, @@ -22,7 +33,7 @@ int main() { }) .listen(9001, [](auto *token) { if (token) { - std::cout << "gameserver listening on :9001\n"; + logLine("gameserver listening on :9001"); } else { std::cerr << "failed to listen on :9001\n"; } diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 42b6b8d..f0384ee 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -13,10 +13,16 @@ server { # не светим точную версию nginx в заголовке Server и в error-страницах server_tokens off; + gzip on; + gzip_vary on; + gzip_min_length 256; + gzip_comp_level 5; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; - add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; base-uri 'none'; frame-ancestors 'none'" always; + add_header Content-Security-Policy "default-src 'self'; connect-src 'self' wss://cactoz.su wss://www.cactoz.su; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; base-uri 'none'; frame-ancestors 'none'" always; location / { try_files $uri $uri.html $uri/ =404; @@ -28,6 +34,13 @@ server { try_files $uri =404; } + # статичные ассеты меняются редко (лого, og-картинка, CV) — час кэша + # компромисс между скоростью и актуальностью без хешей в именах + location /assets/ { + add_header Cache-Control "public, max-age=3600"; + try_files $uri =404; + } + location /ws { limit_req zone=ws_connect burst=10 nodelay; limit_conn ws_conn 20;