Co sprawia, że backend gry różni się od wszystkiego innego
Jeśli kiedykolwiek tworzyłeś backend dla aplikacji internetowej, wiesz, jak to działa: klient wysyła żądanie HTTP, serwer przetwarza go, wysyła zapytanie do bazy danych i zwraca odpowiedź JSON. Cykl się powtarza każdą interakcję. Kilka milisekund większe opóźnienie? Użytkownik nawet tego nie zauważa.
Un Zaplecze gry wieloosobowej żyje w zupełnie innym wszechświecie. Nie o tym tu mówimy prośby i odpowiedzi: porozmawiajmy o ciągły przepływ danych dwukierunkowych, gdzie co milisekundę ma znaczenie, gdzie setki graczy mają wspólny stan, który zmienia się dziesiątki razy na sekundę i gdzie opóźnienie 100 ms może oznaczać różnicę między zwycięstwem a porażką.
W pierwszym artykule z tej serii Inżynieria backendu gier, będziemy badać anatomię wraz z backendem gry wieloosobowej: od architektury sieci po protokoły komunikacyjne, od gry pętle po stronie serwera do serializacji wiadomości, aż do strategii i platform skalowania Backend jako usługa. Na koniec będziesz mieć pełną mapę mentalną każdego komponentu i wyborów problemy architektoniczne, z którymi będziesz musiał sobie poradzić.
Przegląd serii
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | Jesteś tutaj - Anatomia backendu gry | Architektura, protokoły, komponenty |
| 2 | Synchronizacja stanu | Kod sieciowy, interpolacja, predykcja |
| 3 | Silnik kojarzeń | Algorytmy ELO, kolejki, lobby |
| 4 | Dedykowane serwery gier | Infrastruktura i orkiestracja |
| 5 | Architektura zapobiegająca oszustwom | Walidacja po stronie serwera, wykrywanie |
| 6 | LiveOps i monetyzacja | Ekonomia w grze, wydarzenia na żywo |
| 7 | Telemetria i analityka | Metryki, potoki danych, dashboardy |
| 8 | Obserwowalność | Rejestrowanie, śledzenie, alarmowanie |
| 9 | Infrastruktura gier w chmurze | Przesyłanie strumieniowe, przetwarzanie brzegowe, opóźnienia |
| 10 | Stosy gier Open Source | Nakama, Kolyzeusz, Agones |
Czego się nauczysz
- Podstawowe różnice pomiędzy backendem internetowym a backendem gry
- Modele sieciowe: klient-serwer, peer-to-peer i przekaźnikowe
- Jak działa pętla gry po stronie serwera (serwer autorytatywny)
- Protokoły komunikacyjne: TCP vs UDP vs WebSocket vs WebRTC
- Serializacja wiadomości: Bufory protokołów, FlatBuffers, MessagePack
- Zarządzanie połączeniami: sesje, ponowne połączenia, pulsy
- Wybór baz danych: Redis, PostgreSQL, szeregi czasowe
- Wzorzec skalowania: serwer lobby, serwer światowy, strefy utworzone
- Platformy BaaS: PlayFab, GameLift, Nakama, Colyseus
1. Backend sieciowy a backend gry: dwa różne światy
Aby zrozumieć, dlaczego backend gry wymaga radykalnie innego podejścia, porównajmy wymagania fundamentalne w porównaniu z tradycyjną aplikacją internetową.
Porównanie wymagań: Internet vs gra
| Czekam | Zaplecze sieciowe | Zaplecze gry wieloosobowej |
|---|---|---|
| Model komunikacji | Żądanie-odpowiedź (HTTP) | Ciągłe dwukierunkowe przesyłanie strumieniowe |
| Akceptowalne opóźnienie | 200-500 ms | 16-50 ms (jedna klatka przy 60 kl./s = 16,6 ms) |
| Częstotliwość aktualizacji | Na żądanie (kliknij, prześlij) | 20-128 razy na sekundę (częstotliwość taktowania) |
| Państwo | Bezstanowy (każde niezależne żądanie) | Stanowy (stan współdzielony w pamięci) |
| Konsystencja | Dowolna akceptowalna konsystencja | Silna spójność w czasie rzeczywistym |
| Skalowalność | Poziomo (równoważenie obciążenia + repliki) | Pionowo na sesję + poziomo na sesję |
| Tolerancja utraty danych | Zero (liczy się każda transakcja) | Selektywne (utracone pozycje są w porządku, zakupy nie) |
| Czas trwania sesji | Minuty (nawigacja) | Godziny (sesja gry) |
| Przepustowość na użytkownika | Sporadyczne KB | 5-50 KB/s ciągła |
Kluczowym punktem jest natura stanowy backendu gry. Serwer WWW może być zastąpiony w dowolnym momencie - wystarczy skierować ruch do innej instancji. Serwer gry zapamiętuje stan aktywnej gry: pozycje graczy, lecące kule, aktywne efekty, wynik. Jeśli ten serwer ulegnie awarii, gra zostanie przegrana.
Problem opóźnień w grach
Przy konkurencyjnej liczbie klatek na sekundę wynoszącej 128 taktów/s serwer przetwarza co jedną aktualizację 7,8 ms. Jeśli gracz ma opóźnienie sieciowe (RTT) wynoszące 50 ms, jego dane wejściowe docierają do serwera z opóźnieniem 25 ms a odpowiedź nadchodzi po kolejnych 25 ms. Gracz widzi świat 50 ms do tyłu do rzeczywistości serwerowej. Po 200 ms gra staje się niegrywalna. Właśnie dlatego techniki takie jak przewidywanie po stronie klienta e il kompensacja opóźnienia są fundamentalne.
2. Modele sieciowe: jak gracze się łączą
Pierwsza decyzja dotycząca architektury dotyczy modelu sieci: sposobu, w jaki klienci komunikują się ze sobą i z serwerem? Istnieją trzy główne podejścia, każde z określonymi kompromisami.
2.1 Klient-Serwer (Serwer Autorytatywny)
Dominujący model we współczesnym gamingu. Jeden serwer centralny jest jedyny władza na stan gry. Klienci wysyłają swoje dane wejściowe (naciśnięcia klawiszy, ruchy myszą) do serwera, który sprawdza je, aktualizuje stan świata i wysyła wynik do wszystkich klientów. Żaden klient nie może bezpośrednio zmienić stan gry.
Client A Server Client B
| | |
|--- Input (W,A) -->| |
| |--- Input (S,D) ---|<
| | |
| [Valida input] |
| [Aggiorna stato] |
| [Rileva collisioni] |
| [Calcola risultato] |
| | |
|<-- Stato mondo ---|--- Stato mondo -->|
| | |
| [Interpola/ | [Interpola/ |
| Predici] | Predici] |
Zalety: maksymalne bezpieczeństwo (serwer kontroluje wszystko), zintegrowany moduł anty-cheat, Spójny stan dla wszystkich graczy, łatwy do debugowania. Wady: wysoki koszt infrastruktury (jeden serwer na każdą grę), opóźnienia dodatkowy (każde wejście musi działać w obie strony), pojedynczy punkt awarii.
2.2 Peer-to-Peer (P2P)
Każdy klient komunikuje się bezpośrednio ze wszystkimi pozostałymi. Nie ma centralnego serwera - każdy gracz oraz zarówno klient, jak i „serwer” dla siebie. Model ten cieszy się dużą popularnością w grach walki oraz w RTS-ie, gdzie liczba graczy jest ograniczona (2-8).
Zalety: brak kosztów serwera, minimalne opóźnienia pomiędzy urządzeniami równorzędnymi (połączenie bezpośrednie), przetrwa śmierć dowolnego węzła. Wady: nie da się zapobiec oszustwom (każdy klient ma władzę nad swoim stanem), wykładnicza złożoność z liczbą graczy (N*(N-1)/2 połączeń), problemy z przejściem NAT.
2.3 Serwer przekazujący (serwer jako serwer proxy)
Kompromis pomiędzy obydwoma modelami. Serwer centralny pełni funkcję przekaźnik: odbiera wiadomości od każdego z nich klienta i przekazuje je wszystkim innym, ale nie przetwarza logiki gry. Symulacja się dzieje na klientach, serwerze i po prostu „listonosz”.
Porównanie modeli sieciowych
| Charakterystyczny | Klient-Serwer | P2P | Przekaźnik |
|---|---|---|---|
| Bezpieczeństwo | Wysoki (serwer autoryzacyjny) | Niski (brak uprawnień) | Średnia (zależy od klienta) |
| Koszt serwera | Wysoki | Nieważny | Bas |
| Skalowalność | Setki graczy | 2-8 graczy | Dziesiątki graczy |
| Utajenie | Średnia (w obie strony) | Niski (bezpośredni) | Media (przez serwer) |
| Typowe zastosowanie | FPS, MMO, Battle Royale | Bijatyka, klasyczny RTS | Kooperacja, swobodna, mobilna |
| Przykłady | Valorant, Fortnite, CS2 | Uliczny wojownik w StarCrafcie | Wśród nas, jesienni chłopcy |
3. Pętla gry po stronie serwera
Sercem wiarygodnego backendu gry jest pętla gry: powtarzający się cykl a stała częstotliwość (tzw współczynnik zaznaczenia), przetwarzanie danych wejściowych, aktualizowanie stanu i wysyłanie wyniki klientom. Częstotliwość taktowania określa „rozdzielczość czasową” sygnału symulacja.
Zaznacz Oceń według gatunku
| Typ | Zaznacz stawkę | Interwał | Przykład |
|---|---|---|---|
| Konkurencyjny FPS | 128 Hz | 7,8 ms | Valorant, CS2 |
| Standardowy FPS | 64 Hz | 15,6 ms | Overwatch 2 |
| Bitwa królewska | 20-30 Hz | 33-50 ms | Fortnite, PUBG |
| MMO | 10-20 Hz | 50-100 ms | Świat Warcrafta |
| Strategiczne/Tury | 1-10 Hz | 100 ms-1 s | Cywilizacja, Hearthstone |
interface PlayerInput {
readonly playerId: string;
readonly tick: number;
readonly keys: ReadonlyArray<string>;
readonly mouseX: number;
readonly mouseY: number;
readonly timestamp: number;
}
interface GameState {
readonly tick: number;
readonly players: ReadonlyMap<string, PlayerState>;
readonly projectiles: ReadonlyArray<Projectile>;
readonly timestamp: number;
}
class AuthoritativeGameServer {
private readonly TICK_RATE = 64; // 64 aggiornamenti/secondo
private readonly TICK_INTERVAL = 1000 / 64; // ~15.625ms per tick
private currentTick = 0;
private gameState: GameState;
private readonly inputBuffer: Map<string, PlayerInput[]> = new Map();
start(): void {
console.log(`Server avviato a ${this.TICK_RATE} tick/s`);
setInterval(() => this.tick(), this.TICK_INTERVAL);
}
// Riceve input dal client (chiamato dalla rete)
onPlayerInput(input: PlayerInput): void {
const buffer = this.inputBuffer.get(input.playerId) ?? [];
// Crea nuovo array invece di mutare
this.inputBuffer.set(input.playerId, [...buffer, input]);
}
private tick(): void {
const tickStart = performance.now();
this.currentTick++;
// 1. Processa tutti gli input ricevuti
const processedInputs = this.processInputs();
// 2. Aggiorna la simulazione di gioco
const updatedState = this.updateSimulation(processedInputs);
// 3. Rileva collisioni
const stateAfterCollisions = this.detectCollisions(updatedState);
// 4. Aggiorna lo stato di gioco (immutabile)
this.gameState = {
...stateAfterCollisions,
tick: this.currentTick,
timestamp: Date.now(),
};
// 5. Invia lo stato aggiornato a tutti i client
this.broadcastState(this.gameState);
// 6. Monitora le performance del tick
const tickDuration = performance.now() - tickStart;
if (tickDuration > this.TICK_INTERVAL) {
console.warn(
`Tick ${this.currentTick} overrun: ${tickDuration.toFixed(2)}ms ` +
`(budget: ${this.TICK_INTERVAL.toFixed(2)}ms)`
);
}
}
private processInputs(): Map<string, PlayerInput> {
const latest = new Map<string, PlayerInput>();
for (const [playerId, inputs] of this.inputBuffer) {
if (inputs.length > 0) {
// Prendi l'ultimo input valido
const lastInput = inputs[inputs.length - 1];
if (this.validateInput(lastInput)) {
latest.set(playerId, lastInput);
}
}
}
// Svuota il buffer (nuovo Map, non mutare)
this.inputBuffer.clear();
return latest;
}
private validateInput(input: PlayerInput): boolean {
// Anti-cheat: verifica che i valori siano plausibili
const maxSpeed = 10;
return (
Math.abs(input.mouseX) <= 360 &&
Math.abs(input.mouseY) <= 90 &&
input.keys.length <= 6
);
}
}
Zaznacz Budżet i Przekroczenie
Przy 64 taktach/s każdy tik ma budżet 15,6 ms. Jeśli logika tykania (fizyka, kolizje, sztuczna inteligencja, sieć) trwa dłużej, serwer kumuluje opóźnienia i gracze dostrzegają opóźnienie. Monitorowanie budżetu tickowego ma fundamentalne znaczenie: na produkcji śledzisz str. 99 czasu trwania ticku, a nie średniej.
4. Zarządzanie państwem: trzy poziomy państwa
Stan gry wieloosobowej nie jest monolityczną plamą: jest podzielony na poziomy z charakterystycznymi cechami i różne wymagania. Każda warstwa wymaga strategii przechowywania, synchronizacji i trwałości inny.
Trzy poziomy stanu gry
| Poziom | Co zawiera | Częstotliwość aktualizacji | Trwałość | Składowanie |
|---|---|---|---|---|
| Stan ramki | Pozycje, prędkości, obroty, pociski | Każdy takt (20-128 Hz) | Tylko w pamięci | Pamięć RAM serwera gry |
| Stan sesji | HP, ekwipunek, wynik, wzmocnienie/debuff | W przypadku zdarzenia (szkoda, windykacja) | Na czas trwania meczu | RAM + Redis (kopia zapasowa) |
| Stan trwały | Profil, statystyki, zakupy, ranking | Na koniec gry lub podczas transakcji | Stały | PostgreSQL/MongoDB |
// === FRAME STATE: aggiornato ogni tick ===
interface FrameState {
readonly tick: number;
readonly entities: ReadonlyMap<string, EntityTransform>;
readonly projectiles: ReadonlyArray<ProjectileState>;
readonly effects: ReadonlyArray<ActiveEffect>;
}
interface EntityTransform {
readonly posX: number;
readonly posY: number;
readonly posZ: number;
readonly rotYaw: number;
readonly rotPitch: number;
readonly velocityX: number;
readonly velocityY: number;
readonly velocityZ: number;
}
// === SESSION STATE: aggiornato su eventi ===
interface SessionState {
readonly playerId: string;
readonly health: number;
readonly maxHealth: number;
readonly armor: number;
readonly inventory: ReadonlyArray<InventoryItem>;
readonly activeWeapon: string;
readonly kills: number;
readonly deaths: number;
readonly score: number;
readonly buffs: ReadonlyArray<BuffEffect>;
readonly team: string;
}
// === PERSISTENT STATE: salvato su database ===
interface PersistentPlayerData {
readonly odlayerId: string;
readonly username: string;
readonly elo: number;
readonly totalMatches: number;
readonly totalWins: number;
readonly totalKills: number;
readonly unlockedItems: ReadonlyArray<string>;
readonly purchaseHistory: ReadonlyArray<Purchase>;
readonly createdAt: Date;
readonly lastLoginAt: Date;
}
Podział na poziomy ma fundamentalne znaczenie dla wydajności. The stan ramki to musi być tak kompaktowy, jak to możliwe, ponieważ jest serializowany i wysyłany do wszystkich klientów przy każdym takcie. The stan sesji jest wysyłany tylko w przypadku zmiany. The stan trwały nigdy nie jest transmitowany: tylko właściciel może o to poprosić i zostaje zapisany asynchronicznie w bazie danych.
5. Protokoły komunikacyjne: TCP, UDP, WebSocket, WebRTC
Wybór protokołu transportowego jest jedną z najbardziej wpływowych decyzji w architekturze aplikacji zaplecze gry. Każdy protokół oferuje różne kompromisy między niezawodnością, opóźnieniem i złożonością.
5.1 TCP (protokół kontroli transmisji)
TCP zapewnia uporządkowaną i niezawodną dostawę każdego pakietu. Jeśli pakiet zostanie utracony, protokół TCP retransmituje go i blokuje dostarczanie kolejnych pakietów do czasu dotarcia utraconego. To zjawisko tzw blokowanie nagłówka linii i jest to śmiertelny wróg gier czasu rzeczywistego.
5.2 UDP (protokół datagramów użytkownika)
UDP i „odpal i zapomnij”: wysyłaj pakiety bez gwarancji dostarczenia, porządku i integralności. Jeśli pakiet został utracony, nie jest retransmitowany. Brzmi to okropnie, ale w przypadku gry w czasie rzeczywistym i dokładnie to, co jest potrzebne: pozycja gracza 100 ms temu i nieistotna, jeśli ją masz od 16 ms temu.
5.3 WebSockety
WebSocket działa w oparciu o protokół TCP, ale zapewnia dwukierunkowe połączenie pełnodupleksowe. I protokół standard dla gier przeglądarkowych i mobilnych, gdzie natywny protokół UDP nie jest dostępny. Opóźnienie tj większy niż czysty UDP, ale łatwość wdrożenia i uniwersalna kompatybilność czynią go pragmatycznym wyborem dla wielu gatunków.
5.4 WebRTC (kanał danych)
WebRTC DataChannel oferuje komunikację typu peer-to-peer (lub klient-serwer) w oparciu o protokół SCTP przez UDP. Obsługuje zarówno tryby niezawodne, jak i zawodne, konfigurowalne dla każdego kanału. I jedyną opcją dla uzyskaj komunikację w stylu UDP w swojej przeglądarce.
Porównanie protokołów sieciowych do gier
| Czekam | TCP | UDP | WebSockety | WebRTC DC |
|---|---|---|---|---|
| Transport | TCP | UDP | TCP | SCTP/UDP |
| Gwarantowana dostawa | Si | No | Si | Konfigurowalne |
| Gwarantowane zamówienie | Si | No | Si | Konfigurowalne |
| Blokowanie nagłówka linii | Si | No | Si | Nie (niewiarygodne) |
| Typowe opóźnienie | Wysoka (retransmisja) | Minimalny | Przeciętny | Niski |
| Obsługa przeglądarki | Nie (bezpośrednio) | No | Si | Si |
| Przejście NAT | Nie jest to konieczne | Problematyczny | Nie jest to konieczne | Zintegrowany (ICE) |
| Złożoność | Niski | Wysoka (niestandardowa niezawodność) | Niski | Wysoki (sygnalizacja) |
| Idealny dla | Czat, zmiany, lobby | FPS, wyścigi, symulacje | Gry przeglądarkowe, mobilne | Przeglądarka FPS, VoIP |
WebTransport: przyszłość?
InternetTransport oraz nowy standard oparty na HTTP/3 (QUIC), który obiecuje połączyć to, co najlepsze: multipleksowane strumienie dwukierunkowe, tryby niezawodne i zawodne, brak blokowania nagłówka linii i natywny dostęp do przeglądarki. W 2026 r. obsługa przeglądarek jest dojrzewa (Chrome i Edge to obsługują), ale obsługa po stronie serwera jest nadal ograniczona. I protokół do oglądania pod kątem następnej generacji gier przeglądarkowych.
5.5 Wzór hybrydowy: wielokanałowy
Współczesne backendy gier rzadko używają tylko jednego protokołu. Najpopularniejszym wzorem jest tzw wielokanałowy: Różne kanały dla różnych typów danych.
Canale Unreliable (UDP / WebRTC unreliable):
- Posizioni dei giocatori (ogni tick)
- Rotazioni e animazioni
- Effetti particellari
- Dati audio posizionale
Canale Reliable (TCP / WebSocket / WebRTC reliable):
- Danno inflitto/subito
- Cambio arma/inventario
- Chat
- Eventi di gioco (kill, obiettivo)
- Transazioni economiche
- Comandi di matchmaking
6. Serializacja wiadomości: liczy się każdy bajt
Jeśli wyślesz 64 aktualizacje na sekundę do 100 graczy, każdy dodatkowy bajt wiadomości będzie pomnóż przez 6400 razy na sekundę. Wybór formatu serializacji ma znaczenie bezpośrednio na przepustowość, opóźnienia i koszt infrastruktury.
Porównanie formatów serializacji
| Format | Typ | Rozmiar (względny) | Szybkość kodowania | Szybkość dekodowania | Schemat |
|---|---|---|---|---|---|
| JSON | Tekst | 100% (wartość bazowa) | Powolny | Powolny | No |
| Pakiet wiadomości | Utwory | ~60-70% | Szybko | Szybko | No |
| Bufory protokołu | Utwory | ~30-40% | Bardzo szybko | Bardzo szybko | Tak (.proto) |
| Płaskie Bufory | Utwory | ~35-45% | Kopia zerowa | Kopia zerowa | Tak (.fbs) |
| Kapitanie Proto | Utwory | ~35-45% | Kopia zerowa | Kopia zerowa | Si |
// === JSON: ~180 bytes ===
const jsonMsg = JSON.stringify({
type: "player_state",
playerId: "p_abc123",
posX: 123.456,
posY: 78.901,
posZ: 45.678,
rotYaw: 180.5,
rotPitch: -12.3,
health: 85,
weapon: "rifle",
tick: 15042,
});
// === Protocol Buffers: ~42 bytes ===
// Definizione .proto:
// message PlayerState {
// uint32 player_id = 1;
// float pos_x = 2;
// float pos_y = 3;
// float pos_z = 4;
// float rot_yaw = 5;
// float rot_pitch = 6;
// uint32 health = 7;
// WeaponType weapon = 8;
// uint32 tick = 9;
// }
// Risparmio: ~77% meno bandwidth
// A 64 tick/s, 100 giocatori:
// JSON: 180 * 100 * 64 = 1.15 MB/s
// Protobuf: 42 * 100 * 64 = 0.27 MB/s
// Risparmio: 0.88 MB/s = 76% in meno
Kiedy używać czego
- JSON: Prototypowanie, gry krok po kroku, komunikacja z zewnętrznymi serwisami internetowymi. Łatwy do debugowania, uniwersalny
- Pakiet wiadomości: „Binary JSON” - gdy chcesz zmniejszyć rozmiar bez zmiany logiki kodu. Zamiennik typu drop-in dla JSON
- Bufory protokołu: De facto standard dla wysokowydajnych serwerów gier. Schemat wpisany, wielojęzyczny generator kodów, kompatybilność wsteczna
- Płaskie Bufory: Kiedy nawet koszt deserializacji jest zbyt duży. Dostęp typu zero-copy: Odczyt pól bezpośrednio z bufora bez przydzielania pamięci
7. Zarządzanie połączeniami: sesje, puls, ponowne połączenie
W grze wieloosobowej połączenie sieciowe nigdy nie jest w 100% niezawodne. Gracze tak rozłączyć się z powodu niestabilnej sieci Wi-Fi, zmiany sieci (4G/WiFi), awarii klienta lub po prostu ponieważ zamykają grę. Solidny backend gry musi obsługiwać to wszystko w przejrzysty sposób.
7.1 Cykl życia sesji
[Client avvia connessione]
|
v
[Handshake + Autenticazione] -- Token JWT o session ticket
|
v
[Session creata sul server] -- SessionID, PlayerID, Timestamp
|
v
[Heartbeat loop avviato] -- Ping ogni 1-5 secondi
|
v
[Gioco attivo] -- Input/State loop
|
v
[Disconnessione rilevata] -- Timeout heartbeat (10-30s)
|
+--- [Reconnection window] -- 30-120s per riconnettersi
| |
| +--> Riconnesso: ripristina sessione, invia stato corrente
| |
| +--> Timeout: sessione distrutta, giocatore rimosso
|
v
[Sessione terminata] -- Salva statistiche, libera risorse
7.2 Wykrywanie pulsu i rozłączenia
Il bicie serca oraz okresowy komunikat, który klient wysyła do serwera (lub odwrotnie) aby wskazać, że połączenie jest aktywne. Jeśli serwer nie odbiera pulsu przez pewien okres konfigurowalny, uważa klienta za odłączonego. Bicie serca służy również do pomiaru opóźnienie (RTT) e il drganie.
interface ConnectionMetrics {
readonly lastHeartbeat: number;
readonly rttMs: number;
readonly jitterMs: number;
readonly packetLoss: number;
readonly rttHistory: ReadonlyArray<number>;
}
class ConnectionManager {
private readonly HEARTBEAT_INTERVAL = 2000; // 2 secondi
private readonly DISCONNECT_TIMEOUT = 15000; // 15 secondi
private readonly RECONNECT_WINDOW = 60000; // 60 secondi
private readonly sessions: Map<string, SessionData> = new Map();
handleHeartbeat(sessionId: string, clientTimestamp: number): void {
const session = this.sessions.get(sessionId);
if (!session) return;
const now = Date.now();
const rtt = now - clientTimestamp;
// Calcola jitter (variazione del RTT)
const prevRtt = session.metrics.rttMs;
const jitter = Math.abs(rtt - prevRtt);
// Crea nuovo oggetto metrics (immutabile)
const updatedMetrics: ConnectionMetrics = {
lastHeartbeat: now,
rttMs: rtt,
jitterMs: jitter,
packetLoss: session.metrics.packetLoss,
rttHistory: [...session.metrics.rttHistory.slice(-19), rtt],
};
// Aggiorna sessione con nuove metriche
this.sessions.set(sessionId, {
...session,
metrics: updatedMetrics,
});
}
checkDisconnections(): void {
const now = Date.now();
for (const [sessionId, session] of this.sessions) {
const elapsed = now - session.metrics.lastHeartbeat;
if (elapsed > this.DISCONNECT_TIMEOUT && session.status === 'connected') {
// Passa a stato "disconnesso" ma mantiene la sessione
this.sessions.set(sessionId, {
...session,
status: 'disconnected',
disconnectedAt: now,
});
console.log(`Player ${session.playerId} disconnesso. Reconnect window: 60s`);
}
if (session.status === 'disconnected') {
const disconnectElapsed = now - (session.disconnectedAt ?? now);
if (disconnectElapsed > this.RECONNECT_WINDOW) {
// Finestra di reconnection scaduta
this.destroySession(sessionId);
}
}
}
}
handleReconnect(playerId: string, newSocket: WebSocket): boolean {
// Cerca sessione disconnessa per questo player
for (const [sessionId, session] of this.sessions) {
if (session.playerId === playerId && session.status === 'disconnected') {
// Ripristina la sessione con la nuova connessione
this.sessions.set(sessionId, {
...session,
status: 'connected',
socket: newSocket,
metrics: { ...session.metrics, lastHeartbeat: Date.now() },
});
// Invia lo stato corrente del gioco al client riconnesso
this.sendFullStateSync(sessionId);
return true;
}
}
return false;
}
private destroySession(sessionId: string): void {
const session = this.sessions.get(sessionId);
if (session) {
console.log(`Sessione ${sessionId} distrutta per player ${session.playerId}`);
this.sessions.delete(sessionId);
}
}
}
8. Baza danych: stos trwałości
Backend gry nie używa tylko jednej bazy danych: używa jednej stos silników magazynujących, każdy zoptymalizowany pod kątem określonego typu danych i wzorca dostępu.
Baza danych stosu dla zaplecza gry
| Warstwy | Technologia | Co przechowuje | Utajenie |
|---|---|---|---|
| L1 - W pamięci | Struktury danych w pamięci RAM | Stan ramki, bufor wejściowy | < 0,001 ms |
| L2 — rozproszona pamięć podręczna | Redis/Ważka | Stan sesji, tabela wyników, kolejka dobierania graczy | 0,1-1 ms |
| L3 – Relacyjna baza danych | PostgreSQL / KaraluchDB | Profile graczy, ekwipunek, zakupy, rankingi | 1-10ms |
| L4 - Szereg czasowy | Skala czasowaDB / InfluxDB | Telemetria, wskaźniki wydajności, analityka | 1-5 ms |
| L5 - Magazyn obiektów | S3 / GCS | Powtórki, zrzuty ekranu, zasoby, kopie zapasowe | 50-200ms |
8.1 Redis dla stanu w czasie rzeczywistym
Redis jest najważniejszym składnikiem stosu bazy danych zaplecza gry. Jego struktury dane natywne (posortowane zestawy, skróty, listy, pub/sub) mapują się bezpośrednio na wzorce gier:
- Posortowane zestawy dla tabeli liderów i rankingu (ZADD, ZRANK, ZRANGE)
- Hasze dla stanu sesji (HSET, HGETALL)
- Listy dla kolejek matchmakingowych (LPUSH, RPOP)
- Pub/sub do komunikacji pomiędzy serwerami gier i usługami
- Strumienie do pozyskiwania zdarzeń i dzienników audytu
8.2 PostgreSQL dla trwałości
Dla wszystkich danych, które muszą przetrwać restart serwera: profile graczy, postępy,
zapasy, transakcje gospodarcze, historia partii. PostgreSQL oferuje transakcje ACID,
JSON(B) dla danych częściowo ustrukturyzowanych i z rozszerzeniami takimi jak pgvector obsługuje
również wyszukiwanie podobieństw w przypadku kojarzeń opartych na umiejętnościach.
9. Infrastruktura: Serwery dedykowane, Kontenery, Orkiestracja
W przeciwieństwie do bezstanowych serwerów WWW, które skalują się poprzez dodanie replik za modułem równoważenia obciążenia, serwery gier są stanowy: każda instancja zarządza jedną lub większą liczbą aktywnych gier ze stanem w pamięci. To sprawia, że skalowanie i orkiestracja są znacznie bardziej złożone.
9.1 Dedykowane serwery gier
Un dedykowany serwer gier oraz proces, który uruchamia symulację pojedynczego gra (lub obszar świata gry). W przeciwieństwie do „serwerów nasłuchujących” (gdzie plik player pełni również funkcję serwera), serwer dedykowany działa na dedykowanym sprzęcie w chmurze, gwarantując konsekwentne działanie i uczciwość.
[Internet]
|
[CDN / Edge]
|
[Load Balancer (L7)]
/ \
[Gateway API] [Gateway API]
| |
+--------+--------+-------+--------+
| | | | |
[Matchmaker] [Auth] [Social] [Shop] [Leaderboard]
| |
v v
[Fleet Manager / Orchestrator] [Redis Cluster]
| |
+---+---+---+---+ |
| | | | | |
[GS] [GS] [GS] [GS] [GS] <---> [PostgreSQL]
Game Server Instances
Legenda:
GS = Game Server (una partita ciascuno)
Ogni GS e un container/pod con CPU e RAM dedicate
Il Fleet Manager scala il numero di GS in base alla domanda
9.2 Konteneryzacja za pomocą Dockera
Każdy serwer gier działa w kontenerze Docker, zapewniając izolację, odtwarzalność i wdrożenie szybko. Kontener zawiera plik binarny serwera, konfiguracje i zależności. Lekkość kontenerów pozwala na uruchomienie nowych instancji w ciągu kilku sekund.
9.3 Orkiestracja za pomocą Kubernetes i Agones
Agony oraz projekt open source od Google, który rozszerza Kubernetes do zarządzania dedykowane serwery gier. Zapewnia niestandardowe definicje zasobów (CRD) w celu zdefiniowania serwera gier, Flota i FleetAutoscaler. Menedżer floty monitoruje zapotrzebowanie partii i automatycznie skaluje liczba dostępnych serwerów.
Porównanie platform orkiestracyjnych
| Platforma | Typ | Chmura | Mocne strony |
|---|---|---|---|
| Agony | Otwarte oprogramowanie (K8s) | Dowolne + na miejscu | Całkowita elastyczność, brak uzależnienia od dostawcy |
| Amazon GameLift | Zarządzane (AWS) | AWS | Integracja z AWS, kojarzenie FlexMatch |
| Azure PlayFab | Zarządzane (Lazur) | Lazur | Kompletny ekosystem (LiveOps, analityka, ekonomia) |
| Serwery gier Google Cloud | Zarządzane (GCP) | GCP | Obsługiwane przez Agones, globalne skalowanie |
10. Wzorzec skalowania: Lobby, Świat, Strefa
Nie wszystkie gry skalują się tak samo. Schemat skalowania zależy od płci i struktura gry. Oto trzy główne wzorce.
10.1 Serwer poczekalni/meczowy
Używany przez FPS, Battle Royale, MOBA. Każda gra jest izolowaną instancją z numerem stała liczba graczy (10-100). Pod koniec meczu serwer zostaje zniszczony wraz z zasobami poddane recyklingowi. Skalowanie = zwiększenie liczby instancji.
10.2 Serwer Światowy (Świat Trwały)
Używany w grach MMO. Świat trwały i podzielony na obszary, każdy zarządzał z serwera dedykowanego. Gracze poruszają się pomiędzy strefami z przejrzystym przekazaniem. Serwery stref komunikują się ze sobą w celu zarządzania granicami.
10.3 Strefy instancji
Hybryda: świat jest trwały, ale pojawiają się określone obszary (lochy, rajdy, areny). instancja dynamicznie. Każda grupa graczy otrzymuje swój własny egzemplarz obszaru. Umożliwia to skalowanie obszarów o dużej gęstości bez przeciążania serwer światowy.
LOBBY/MATCH SERVER (FPS, BR):
[Matchmaker] --> [Server Pool]
|
+---------+---------+
| | |
[Match 1] [Match 2] [Match 3]
10 players 10 players 10 players
(30 min) (25 min) (nuovo)
WORLD SERVER (MMO):
[World Manager]
|
+---+---+---+---+
| | | | |
[Zona A][Zona B][Zona C][Zona D]
Citta Foresta Dungeon PvP
200 p. 50 p. 30 p. 80 p.
^ ^
|--- Handoff zone ---|
INSTANCED ZONES (MMO + Dungeon):
[Zona Citta] --> [Ingresso Dungeon]
|
+---------+---------+
| | |
[Dungeon [Dungeon [Dungeon
Copia 1] Copia 2] Copia 3]
5 players 5 players 5 players
11. Backend jako usługa dla gier: platformy
Nie każdy musi (lub chce) budować backend gry od zera. Są platformy Backend-as-a-Service (BaaS) oferujący gotowe do użycia komponenty: matchmaking, ranking, uwierzytelnianie, przechowywanie, ekonomia w grze. Wybór zależy od budżetu, kontroli pożądane i gatunek gry.
Porównanie platform BaaS do gier
| Platforma | Język serwera | Otwarte źródło | Hostowane samodzielnie | Mocne strony | Idealny dla |
|---|---|---|---|---|---|
| Nakama | Idź, TS, Lua | Si | Si | W czasie rzeczywistym, kojarzenie, przechowywanie | Niezależne, średniej wielkości, mobilne |
| Kolyzeusz | Maszynopis | Si | Si | Autorytatywna, automatyczna synchronizacja stanu | Gry przeglądarkowe, prototypy |
| PlayFab | C# (funkcje platformy Azure) | No | No | LiveOps, ekonomia, analityka | AAA, mobilny F2P |
| Amazon GameLift | C++, C# | No | No | Zarządzanie flotą, FlexMatch | AAA dla wielu graczy |
| Foton | C# | No | Częściowy | Integracja jedności, przekaźnik | Gry Unity na urządzenia mobilne |
| Lustro | C# | Si | Si | Sieć Unity, wymiana HLAPI | Gry niezależne od Unity |
Budowanie a kupowanie: kryteria wyboru
- Użyj BaaS jeśli: ograniczony budżet, priorytetowy czas wprowadzenia produktu na rynek, standardowy gatunek (casual, puzzle, gra karciana), mały zespół
- Buduj na zamówienie se: ekstremalne wymagania dotyczące opóźnień (<20ms), unikalna logika gry, potrzeba całkowitej kontroli nad kodem sieciowym i zabezpieczeniem przed oszustwami, skala >100 tys. CCU
- Podejście hybrydowe: użyj BaaS do uwierzytelniania, tworzenia tabel wyników i społeczności, ale zbuduj niestandardowy serwer gier dla logiki gry
12. Metryki wydajności: co monitorować
Backend gry bez obserwowalności i bomby zegarowej. To są metryki podstawy, które każdy zespół musi monitorować podczas produkcji.
Kluczowe wskaźniki backendu gry
| Metryczny | Opis | Cel | Alarm |
|---|---|---|---|
| CCU | Jednocześnie połączeni użytkownicy | To zależy od gatunku | > 90% pojemności |
| Zaznacz czas trwania (p99) | Zaznacz czas przetwarzania | < 80% budżetu kleszczy | > 90% budżetu |
| RTT (p50 / p95 / p99) | Klient-serwer w czasie podróży w obie strony | p50 < 50 ms, p99 < 150 ms | p99 > 200 ms |
| Utrata pakietów | Procent utraconych pakietów | <1% | > 3% |
| Drganie | Zmienność opóźnienia | < 10 ms | > 30 ms |
| Wiadomości/sek | Wiadomości przetwarzane na sekundę na serwer | > 10 tys. wiadomości/s | < 5 tys. wiadomości/s |
| Przepustowość na gracza | Wydajność KB/s na gracza | 5-30KB/s | > 50 KB/s |
| Szybkość ponownego połączenia | Procent udanych ponownych połączeń | > 90% | < 70% |
| Czas uruchamiania serwera | Czas rozpocząć nową instancję | < 10 s | > 30-te |
// Struttura per le metriche di un game server in Go
type TickMetrics struct {
TickNumber uint64
Duration time.Duration
PlayersActive int
InputsProcessed int
MessagesOut int
BytesOut int64
}
type ServerMetrics struct {
mu sync.RWMutex
tickDurations []time.Duration // ring buffer ultimi 1000 tick
totalTicks uint64
overrunCount uint64
ccu int32
}
func (m *ServerMetrics) RecordTick(metrics TickMetrics) {
m.mu.Lock()
defer m.mu.Unlock()
idx := m.totalTicks % 1000
m.tickDurations[idx] = metrics.Duration
m.totalTicks++
if metrics.Duration > tickBudget {
m.overrunCount++
log.Printf(
"TICK OVERRUN #%d: tick=%d duration=%v budget=%v players=%d",
m.overrunCount,
metrics.TickNumber,
metrics.Duration,
tickBudget,
metrics.PlayersActive,
)
}
}
// Calcola il percentile p99 della durata dei tick
func (m *ServerMetrics) P99TickDuration() time.Duration {
m.mu.RLock()
defer m.mu.RUnlock()
sorted := make([]time.Duration, len(m.tickDurations))
copy(sorted, m.tickDurations)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i] < sorted[j]
})
idx := int(float64(len(sorted)) * 0.99)
return sorted[idx]
}
13. Składanie tego wszystkiego w całość: kompletna architektura
Oto przegląd wszystkich komponentów nowoczesnego zaplecza gier wieloosobowych i sposobu ich tworzenia wchodzą ze sobą w interakcję.
+==============================================================+
| CLIENT LAYER |
+==============================================================+
| [Game Client] [Game Client] [Game Client] [Game Client] |
| | | | | |
| WebSocket/UDP WebSocket/UDP WebSocket/UDP WebSocket/UDP |
+======|==============|==============|==============|==========+
| | | |
+======|==============|==============|==============|==========+
| EDGE LAYER |
+==============================================================+
| [CDN] [DDoS Protection] [Load Balancer] [SSL Termination]|
+==========================|===================================+
|
+==========================|===================================+
| GATEWAY LAYER |
+==============================================================+
| [API Gateway] [WebSocket Gateway] |
| REST per login, Routing verso il game server |
| shop, profili corretto per la partita |
+=========|========================|===========================+
| |
+---------+----------+ +--------+--------+
| SERVICE LAYER | | GAME SERVER |
+====================+ | LAYER |
| [Auth Service] | +=================+
| [Matchmaker] | | [GS Instance 1] |
| [Leaderboard] | | [GS Instance 2] |
| [Shop/Economy] | | [GS Instance 3] |
| [Social/Chat] | | [GS Instance N] |
| [Analytics] | +=================+
+========|===========+ |
| |
+========|======================|===========================+
| DATA LAYER |
+============================================================+
| [Redis] [PostgreSQL] [TimescaleDB] [S3/GCS] |
| Sessions, Profili, Telemetria, Replay, |
| Leaderboard, Inventario, Metriche, Assets, |
| Match Queue Transazioni Analytics Backup |
+============================================================+
| |
| [Agones / GameLift] - Fleet Management & Autoscaling |
| [Prometheus + Grafana] - Monitoring & Alerting |
| [ELK / Loki] - Logging centralizzato |
+============================================================+
Następne kroki
W tym artykule uzyskałeś pełną wiedzę na temat anatomii backendu gry tryb wieloosobowy: od architektury sieci po protokoły transportowe, od autorytatywnej pętli gry po serializację wiadomości, od zarządzania połączeniami po wzorce skalowania. Widziałeś, jak każdy komponent współdziała z innymi i jakie są kompromisy architektoniczne kierować wyborami projektowymi.
Nel następny artykuł zagłębimy się w sedno kodu sieciowego dla wielu graczy: plik synchronizacja stanu. Zbadamy dogłębnie stronę klienta przewidywanie, uzgadnianie serwerów, interpolacja, kompensacja opóźnień i wycofywanie zmian kod sieciowy. Zobaczymy, jak współczesne gry mimo wszystko tworzą iluzję płynnego działania opóźnienie sieci.
Dodatkowe zasoby
- Gabriel Gambetta: „Szybki tryb wieloosobowy” — seria artykułów referencyjnych na temat architektury klient-serwer w grach
- Wiki dla twórców Valve: „Source Multiplayer Networking” – dokumentacja techniczna dotycząca kodu sieciowego Source Engine
- Glenna Fiedlera: „Game Networking” — kompletna seria poświęcona protokołom, synchronizacji stanu i bezpieczeństwu
- Dokumentacja Agonu: Kompletny przewodnik po organizowaniu serwerów gier w Kubernetes
- Colyseus.io: Struktura serwera gier Node.js typu open source z automatyczną synchronizacją stanu
- Nakama od Heroic Labs: Serwer gier typu open source z funkcją dobierania graczy, pamięcią masową i trybem wieloosobowym w czasie rzeczywistym







