Wat een game-backend anders maakt dan al het andere
Als je ooit een backend voor een webapplicatie hebt ontwikkeld, weet je hoe het werkt: een client stuurt een verzoek HTTP, de server verwerkt het, vraagt een database op en retourneert een JSON-antwoord. De cyclus herhaalt zich elke interactie. Een paar milliseconden meer latentie? De gebruiker merkt het niet eens.
Un backend voor multiplayer-games hij leeft in een heel ander universum. We hebben het hier niet over verzoeken en antwoorden: laten we het hebben over a continue stroom van bidirectionele gegevens, waarbij elke milliseconde zaken, waar honderden spelers een gemeenschappelijke toestand delen die tientallen keren per seconde verandert, en waar een vertraging van 100 ms kan het verschil betekenen tussen overwinning en nederlaag.
In dit eerste artikel van de serie Game-backend-techniek, zullen we de anatomie verkennen compleet met een multiplayer-game-backend: van de netwerkarchitectuur tot de communicatieprotocollen, van de game server-side loops tot serialisatie van berichten, tot schaalstrategieën en platforms Backend-as-a-Service. Aan het einde heb je een volledige mentale kaart van elk onderdeel en de keuzes bouwkundige vraagstukken waarmee u te maken krijgt.
Serieoverzicht
| # | Item | Focus |
|---|---|---|
| 1 | Je bent hier - Anatomie van een game-backend | Architectuur, protocollen, componenten |
| 2 | Statussynchronisatie | Netcode, interpolatie, voorspelling |
| 3 | Matchmaking-engine | ELO-algoritmen, wachtrijen, lobby's |
| 4 | Speciale gameservers | Infrastructuur en orkestratie |
| 5 | Anti-Cheat-architectuur | Validatie en detectie aan de serverzijde |
| 6 | LiveOps en het genereren van inkomsten | In-game economie, live evenementen |
| 7 | Telemetrie en analyse | Statistieken, datapijplijnen, dashboards |
| 8 | Waarneembaarheid | Loggen, traceren, alarmeren |
| 9 | Cloud gaming-infrastructuur | Streaming, edge computing, latentie |
| 10 | Open source-gamestapels | Nakama, Colyseus, Agones |
Wat je gaat leren
- De fundamentele verschillen tussen een webbackend en een gamebackend
- Netwerkmodellen: client-server, peer-to-peer en relay
- Hoe de gameloop aan de serverzijde werkt (gezaghebbende server)
- Communicatieprotocollen: TCP versus UDP versus WebSocket versus WebRTC
- Berichtserialisatie: protocolbuffers, FlatBuffers, MessagePack
- Verbindingsbeheer: sessies, herverbindingen, hartslagen
- Databasekeuzes: Redis, PostgreSQL, tijdreeksen
- Schaalpatroon: lobbyserver, wereldserver, geïnstantieerde zones
- BaaS-platforms: PlayFab, GameLift, Nakama, Colyseus
1. Webbackend versus gamebackend: twee verschillende werelden
Om te begrijpen waarom een game-backend een radicaal andere aanpak vereist, gaan we de vereisten vergelijken fundamenteel met die van een traditionele webapplicatie.
Vergelijking van vereisten: web versus game
| Ik wacht | Web-backend | Game Backend-multiplayer |
|---|---|---|
| Communicatiemodel | Verzoek-antwoord (HTTP) | Continue bidirectionele streaming |
| Acceptabele latentie | 200-500 ms | 16-50 ms (één frame bij 60 fps = 16,6 ms) |
| Frequentie bijwerken | Op aanvraag (klikken, verzenden) | 20-128 keer per seconde (tikfrequentie) |
| Staat | Staatloos (elk onafhankelijk verzoek) | Stateful (gedeelde status in geheugen) |
| Samenhang | Elke aanvaardbare consistentie | Sterke consistentie in realtime |
| Schaalbaarheid | Horizontaal (load balancer + replica's) | Verticaal per sessie + horizontaal per sessie |
| Tolerantie voor gegevensverlies | Nul (elke transactie telt) | Selectief (verloren posities zijn oké, aankopen niet) |
| Sessieduur | Minuten (navigatie) | Uren (spelsessie) |
| Bandbreedte per gebruiker | Sporadische KB | 5-50 KB/s continu |
Het cruciale punt is de natuur statelijk van de game-backend. Een webserver kan dat zijn op elk moment vervangen - leid verkeer gewoon naar een ander exemplaar. Een gameserver houdt de status van een actief spel in het geheugen: de posities van de spelers, de kogels tijdens de vlucht, de actieve effecten, de score. Als die server crasht, is het spel verloren.
Het probleem van latentie bij gaming
Bij een competitieve FPS met een snelheid van 128 tick/s verwerkt de server elke update één update 7,8 ms. Als een speler een netwerklatentie (RTT) van 50 ms heeft, arriveert zijn invoer 25 ms te laat bij de server en het antwoord arriveert met nog eens 25 ms. De speler ziet de wereld 50 ms achter zich naar de realiteit van de server. Bij 200 ms wordt het spel onspeelbaar. Dit is de reden waarom technieken zoals de Voorspelling aan de klantzijde en de vertragingscompensatie ze zijn fundamenteel.
2. Netwerkmodellen: hoe spelers verbinding maken
De eerste architecturale beslissing betreft het netwerkmodel: hoe klanten met elkaar communiceren en met de server? Er zijn drie hoofdbenaderingen, elk met specifieke afwegingen.
2.1 Client-server (autoritaire server)
Het dominante model in moderne gaming. Eén centrale server is de enige autoriteit op staat van het spel. Clients sturen hun invoer (toetsaanslagen, muisbewegingen) naar de server, die valideert ze, werkt de toestand van de wereld bij en stuurt het resultaat naar alle klanten. Geen enkele cliënt kan dat rechtstreeks de status van het spel veranderen.
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] |
Voordelen: maximale veiligheid (de server controleert alles), geïntegreerde anti-cheat, Consistente status voor alle spelers, eenvoudig te debuggen. Nadelen: hoge infrastructuurkosten (één server voor elke game), latentie extra (elke ingang moet heen en weer gaan), single point of Failure.
2.2 Peer-to-peer (P2P)
Elke klant communiceert rechtstreeks met alle anderen. Er is geen centrale server - elke speler en zowel client als "server" voor zichzelf. Dit model is populair in vechtspellen en in RTS, waar het aantal spelers beperkt is (2-8).
Voordelen: geen serverkosten, minimale latentie tussen peers (directe verbinding), overleeft de dood van elk knooppunt. Nadelen: onmogelijk om bedrog te voorkomen (elke klant heeft zeggenschap over zijn eigen staat), exponentiële complexiteit met het aantal spelers (N*(N-1)/2 verbindingen), NAT-traversal-problemen.
2.3 Relayserver (server als proxy)
Een compromis tussen de twee modellen. Een centrale server fungeert als relais: ontvangt berichten van elk client en stuurt ze door naar alle anderen, maar verwerkt de spellogica niet. De simulatie vindt plaats op clients, de server en gewoon een "postbode".
Vergelijking van netwerkmodellen
| Kenmerkend | Client-server | P2P | Relais |
|---|---|---|---|
| Veiligheid | Hoog (autoriteitserver) | Laag (geen autoriteit) | Gemiddeld (afhankelijk van klant) |
| Serverkosten | Hoog | Nul | Bas |
| Schaalbaarheid | Honderden spelers | 2-8 spelers | Tientallen spelers |
| Latentie | Gemiddeld (retour) | Laag (direct) | Media (via server) |
| Typisch gebruik | FPS, MMO, Battle Royale | Vechten, klassieke RTS | Coöp, casual, mobiel |
| Voorbeelden | Valorant, Fortnite, CS2 | Straatvechter, StarCraft | Onder ons, herfstjongens |
3. De gamelus aan de serverzijde
Het hart van een gezaghebbende game-backend is de spel lus: een zich herhalende cyclus a constante frequentie (de tiksnelheid), invoer verwerken, status bijwerken en verzenden de resultaten aan de opdrachtgevers. De tick rate-frequentie bepaalt de "tijdresolutie" van de simulatie.
Vink Beoordeel op genre aan
| Type | Vink Tarief aan | Interval | Voorbeeld |
|---|---|---|---|
| Competitieve FPS | 128 Hz | 7,8 ms | Valorant, CS2 |
| Standaard FPS | 64 Hz | 15,6 ms | Overwatch 2 |
| Battle Royale | 20-30 Hz | 33-50 ms | Fortnite, PUBG |
| MMO | 10-20 Hz | 50-100 ms | Wereld van Warcraft |
| Strategisch / bochten | 1-10 Hz | 100 ms-1s | Beschaving, 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
);
}
}
Vink Budget en Overschrijding aan
Bij 64 tick/s heeft elke tick een budget van 15,6 ms. Als de vinklogica (natuurkunde, botsingen, AI, netwerken) duurt langer, de server stapelt vertraging op en de spelers zij ervaren vertraging. Het monitoren van het tekenbudget is van fundamenteel belang: in de productie volg je de p99 van de tekenduur, niet het gemiddelde.
4. Staatsbeheer: de drie niveaus van de staat
De staat van een multiplayergame is geen monolithische klodder: het is verdeeld in niveaus met kenmerken en verschillende eisen. Elke laag vereist opslag-, synchronisatie- en persistentiestrategieën anders.
De drie niveaus van spelstatus
| Niveau | Wat het bevat | Frequentie bijwerken | Vasthoudendheid | Opslag |
|---|---|---|---|---|
| Kaderstaat | Posities, snelheden, rotaties, projectielen | Elke tik (20-128 Hz) | Alleen ter herinnering | RAM van de spelserver |
| Sessiestatus | HP, inventaris, score, buff/debuff | Op evenement (schade, incasso) | Voor de duur van de wedstrijd | RAM + Redis (back-up) |
| Aanhoudende staat | Profiel, statistieken, aankopen, ranking | Aan het einde van het spel of bij een transactie | Permanent | 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;
}
De scheiding in niveaus is van fundamenteel belang voor de prestaties. De frame staat het moet zo zijn zo compact mogelijk omdat het geserialiseerd is en bij elke tik naar alle klanten wordt verzonden. De sessie staat wordt alleen verzonden bij wisselgeld. De aanhoudende toestand het wordt nooit uitgezonden: alleen de eigenaar kan het opvragen, en het wordt opgeslagen asynchroon op de database.
5. Communicatieprotocollen: TCP, UDP, WebSocket, WebRTC
De keuze van het transportprotocol is een van de meest impactvolle beslissingen in de architectuur van een backend van het spel. Elk protocol biedt verschillende afwegingen tussen betrouwbaarheid, latentie en complexiteit.
5.1 TCP (Transmissiecontroleprotocol)
TCP zorgt voor een ordelijke en betrouwbare bezorging van elk pakket. Als een pakket verloren gaat, wordt TCP het verzendt het opnieuw en blokkeert de bezorging van volgende pakketten totdat het verlorene arriveert. Dit fenomeen heet blokkering aan het hoofd van de lijn en het is de doodsvijand van realtime gamen.
5.2 UDP (Gebruikersdatagramprotocol)
UDP en "fire and vergeet": verzend pakketten zonder garanties op levering, bestelling of integriteit. Als een pakket verloren gaat, wordt het niet opnieuw verzonden. Dit klinkt verschrikkelijk, maar voor een real-time game en precies wat nodig is: de positie van een speler 100 ms geleden en niet relevant als je die hebt van 16 ms geleden.
5.3 WebSockets
WebSocket werkt bovenop TCP, maar biedt een full-duplex bidirectionele verbinding. En het protocol standaard voor browsergebaseerde en mobiele games, waarbij native UDP niet beschikbaar is. Latentie e groter dan pure UDP, maar implementatiegemak en universele compatibiliteit maken het de pragmatische keuze voor veel genres.
5.4 WebRTC (datakanaal)
WebRTC DataChannel biedt peer-to-peer (of client-server) communicatie op basis van SCTP via UDP. Ondersteunt zowel betrouwbare als onbetrouwbare modi, configureerbaar per kanaal. En de enige optie voor ontvang UDP-achtige communicatie in uw browser.
Vergelijking van netwerkprotocollen voor gaming
| Ik wacht | TCP | UDP | WebSockets | WebRTC DC |
|---|---|---|---|---|
| Vervoer | TCP | UDP | TCP | SCTP/UDP |
| Gegarandeerde levering | Si | No | Si | Configureerbaar |
| Gegarandeerde bestelling | Si | No | Si | Configureerbaar |
| Blokkering van de hoofdlijn | Si | No | Si | Nee (onbetrouwbaar) |
| Typische latentie | Hoog (opnieuw verzonden) | Minimaal | Gemiddeld | Laag |
| Browser-ondersteuning | Nee (direct) | No | Si | Si |
| NAT-traversatie | Niet nodig | Problematisch | Niet nodig | Geïntegreerd (ICE) |
| Complexiteit | Laag | Hoog (betrouwbaarheid op maat) | Laag | Hoog (signalering) |
| Ideaal voor | Chatten, diensten, lobbyen | FPS, racen, simulaties | Browsergames, mobiel | FPS-browser, VoIP |
WebTransport: de toekomst?
WebTransport en een nieuwe standaard gebaseerd op HTTP/3 (QUIC) die belooft te combineren het beste van alle werelden: gemultiplexte bidirectionele streams, betrouwbare en onbetrouwbare modi, geen head-of-line blokkering en native browsertoegang. In 2026 is browserondersteuning beschikbaar volwassen wordt (Chrome en Edge ondersteunen dit), maar ondersteuning aan de serverzijde is nog steeds beperkt. En de protocol om te kijken naar de volgende generatie browsergebaseerde games.
5.5 Hybride patroon: meerkanaals
Moderne game-backends gebruiken zelden slechts één protocol. Het meest voorkomende patroon is de meerkanaals: Verschillende kanalen voor verschillende gegevenstypen.
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. Serialisatie van berichten: elke byte telt
Wanneer je 64 updates per seconde naar 100 spelers stuurt, zal elke extra byte in het bericht dat ook doen vermenigvuldigen met 6.400 keer per seconde. De keuze van het serialisatieformaat heeft een impact rechtstreeks op bandbreedte, latentie en infrastructuurkosten.
Vergelijking van serialisatieformaten
| Formaat | Type | Grootte (relatief) | Coderingssnelheid | Decodeer snelheid | Schema |
|---|---|---|---|---|---|
| JSON | Tekst | 100% (basislijn) | Langzaam | Langzaam | No |
| Berichtpakket | Sporen | ~60-70% | Snel | Snel | No |
| Protocolbuffers | Sporen | ~30-40% | Zeer snel | Zeer snel | Ja (.proto) |
| Platte buffers | Sporen | ~35-45% | Nul-kopie | Nul-kopie | Ja (.fbs) |
| Kapitein Proto | Sporen | ~35-45% | Nul-kopie | Nul-kopie | 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
Wanneer wat te gebruiken
- JSON: Prototyping, turn-by-turn games, communicatie met externe webservices. Gemakkelijk te debuggen, universeel
- Berichtpakket: "Binaire JSON" - wanneer u de grootte wilt verkleinen zonder de codelogica te wijzigen. Drop-in vervanging voor JSON
- Protocolbuffers: De de facto standaard voor krachtige gameservers. Getypt schema, meertalige codegen, achterwaartse compatibiliteit
- Platte buffers: Wanneer zelfs de kosten van deserialisatie te hoog zijn. Zero-copy-toegang: lees velden rechtstreeks uit de buffer zonder geheugen toe te wijzen
7. Verbindingsbeheer: sessies, hartslag, herverbinding
In een multiplayergame is de netwerkverbinding nooit 100% betrouwbaar. De spelers wel de verbinding wordt verbroken vanwege onstabiele wifi, netwerkverandering (4G/WiFi), clientcrash of gewoonweg omdat ze het spel afsluiten. Een robuuste game-backend moet dit allemaal transparant afhandelen.
7.1 Sessielevenscyclus
[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 Hartslag- en verbindingsdetectie
Il hartslag en een periodiek bericht dat de client naar de server stuurt (of omgekeerd) om aan te geven dat de verbinding actief is. Als de server gedurende een bepaalde periode geen hartslagen ontvangt configureerbaar, beschouwt de client als niet verbonden. De hartslag dient ook om de hartslag te meten latentie (RTT) en de rilling.
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. Database: de persistentiestapel
Een game-backend gebruikt niet slechts één database: het gebruikt er één stapel van opslagmotoren, elk geoptimaliseerd voor een specifiek gegevenstype en toegangspatroon.
Stapeldatabase voor game-backend
| Lagen | Technologie | Wat het opslaat | Latentie |
|---|---|---|---|
| L1 - In het geheugen | Datastructuren in RAM | Framestatus, invoerbuffer | < 0,001 ms |
| L2 - Gedistribueerde cache | Redis/Libel | Sessiestatus, klassement, matchmaking-wachtrij | 0,1-1 ms |
| L3 - Relationele database | PostgreSQL / KakkerlakDB | Spelersprofielen, inventaris, aankopen, ranglijsten | 1-10 ms |
| L4 - Tijdreeksen | TijdschaalDB / InfluxDB | Telemetrie, prestatiestatistieken, analyses | 1-5 ms |
| L5 - Objectopslag | S3 / GCS | Herhalingen, screenshots, assets, back-ups | 50-200 ms |
8.1 Redis voor realtime status
Redis is het meest kritische onderdeel van de databasestack van een game-backend. Zijn structuren inheemse gegevens (gesorteerde sets, hashes, lijsten, pub/sub) worden rechtstreeks gekoppeld aan spelpatronen:
- Gesorteerde sets voor klassement en ranking (ZADD, ZRANK, ZRANGE)
- Hasj voor sessiestatus (HSET, HGETALL)
- Lijsten voor matchmaking-wachtrijen (LPUSH, RPOP)
- Pub/Sub voor communicatie tussen gameservers en services
- Stromen voor gebeurtenissourcing en auditlogboeken
8.2 PostgreSQL voor persistentie
Voor alle gegevens die een herstart van de server moeten overleven: spelersprofielen, voortgang,
inventaris, economische transacties, batchgeschiedenis. PostgreSQL biedt ACID-transacties,
JSON(B) voor semi-gestructureerde data, en met extensies zoals pgvector ondersteunt
ook zoeken naar gelijkenissen voor op vaardigheden gebaseerde matchmaking.
9. Infrastructuur: speciale servers, containers, orkestratie
In tegenstelling tot staatloze webservers die schalen door replica's achter een load balancer toe te voegen, spelservers zijn statelijk: elke instantie beheert een of meer actieve games met status in het geheugen. Dit maakt schaling en orkestratie veel complexer.
9.1 Toegewijde spelservers
Un speciale gameserver en een proces dat de simulatie van een enkele uitvoert spel (of een deel van de spelwereld). In tegenstelling tot "listenerservers" (waarbij een speler fungeert ook als server), de dedicated server draait op speciale hardware in de cloud en garandeert consistente prestaties en eerlijkheid.
[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 Containerisatie met Docker
Elke gameserver draait in een Docker-container, waardoor isolatie, reproduceerbaarheid en implementatie worden gegarandeerd snel. De container bevat het binaire bestand van de server, configuraties en afhankelijkheden. Dankzij de lichtheid van containers kunt u binnen enkele seconden nieuwe exemplaren lanceren.
9.3 Orkestratie met Kubernetes en Agones
Agones en een open-sourceproject van Google dat Kubernetes uitbreidt om te beheren speciale gameservers. Biedt aangepaste resourcedefinities (CRD) om GameServer te definiëren, Vloot en FleetAutoscaler. De wagenparkbeheerder bewaakt de batchvraag en schaalt automatisch het aantal beschikbare servers.
Vergelijking van orkestratieplatforms
| Platform | Type | Wolk | Sterke punten |
|---|---|---|---|
| Agones | Open source (K8s) | Elke + op locatie | Totale flexibiliteit, geen leverancierslock-in |
| Amazon GameLift | Beheerd (AWS) | AWS | AWS-integratie, FlexMatch-matchmaking |
| Azure PlayFab | Beheerd (Azure) | Azuur | Compleet ecosysteem (LiveOps, analytics, economie) |
| Google Cloud-gameservers | Beheerd (GCP) | GCP | Mogelijk gemaakt door Agones, wereldwijde schaalvergroting |
10. Schaalpatroon: lobby, wereld, zone
Niet alle games schalen hetzelfde. Het schaalpatroon is afhankelijk van geslacht en spelstructuur. Hier zijn de drie belangrijkste patronen.
10.1 Lobby/Matchserver
Gebruikt door FPS, Battle Royale, MOBA. Elk spel is een geïsoleerd exemplaar met een nummer vast aantal spelers (10-100). Aan het einde van de wedstrijd wordt de server en de bronnen vernietigd gerecycled. Schalen = het aantal exemplaren vergroten.
10.2 Wereldserver (persistente wereld)
Gebruikt door MMO's. Een wereld volhardend en verdeeld in gebieden, elk beheerd vanaf een speciale server. Spelers bewegen tussen zones met een transparante overdracht. Zoneservers communiceren met elkaar om grenzen te beheren.
10.3 Geïnstalleerde zones
Een hybride: de wereld is volhardend, maar specifieke gebieden (kerkers, raids, arena's) komen geïnstantieerd dynamisch. Elke groep spelers krijgt een eigen exemplaar van het gebied. Hierdoor kunt u gebieden met een hoge dichtheid schalen zonder de omgeving te overbelasten wereldserver.
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-as-a-Service voor games: de platforms
Niet iedereen hoeft (of wil) een game-backend helemaal opnieuw bouwen. Er zijn platforms Backend-as-a-Service (BaaS) met kant-en-klare componenten: matchmaking, leaderboard, authenticatie, opslag, in-game economie. De keuze hangt af van het budget, de controle gewenste en het spelgenre.
Vergelijking van BaaS-platforms voor gaming
| Platform | Servertaal | Open bron | Zelf gehost | Sterke punten | Ideaal voor |
|---|---|---|---|---|---|
| Nakama | Ga, TS, Lua | Si | Si | Realtime, matchmaking, opslag | Indie, middelgroot, mobiel |
| Colyseus | Typescript | Si | Si | Gezaghebbende, automatische statussynchronisatie | Browserspellen, prototypes |
| SpeelFab | C# (Azure-functies) | No | No | LiveOps, economie, analyse | AAA, mobiele F2P |
| Amazon GameLift | C++, C# | No | No | Vlootbeheer, FlexMatch | AAA-multiplayer |
| Foton | C# | No | Gedeeltelijk | Eenheidsintegratie, relais | Unity-spellen, mobiel |
| Spiegel | C# | Si | Si | Unity-netwerken, HLAPI-vervanging | Unity-indiegames |
Bouwen versus kopen: keuzecriteria
- Gebruik een BaaS indien: beperkt budget, prioriteit time-to-market, standaardgenre (casual, puzzel, kaartspel), klein team
- Bouw op maat se: extreme latentievereisten (<20 ms), unieke spellogica, behoefte aan totale controle over netcode en anti-cheat, schaal> 100K CCU
- Hybride aanpak: gebruik een BaaS voor authenticatie, leaderboard en social, maar bouw de aangepaste gameserver voor de gamelogica
12. Prestatiestatistieken: wat te monitoren
Een game-backend zonder waarneembaarheid en een tijdbom. Dit zijn de statistieken fundamentele zaken die elk team tijdens de productie in de gaten moet houden.
Belangrijkste backend-statistieken van games
| Metrisch | Beschrijving | Doel | Alarm |
|---|---|---|---|
| CCU | Gelijktijdige verbonden gebruikers | Het hangt af van het genre | > 90% capaciteit |
| Vink Duur aan (p99) | Vink verwerkingstijd aan | < 80% van het tekenbudget | > 90% van het budget |
| RTT (p50 / p95 / p99) | Round-Trip Time-client-server | p50 < 50 ms, p99 < 150 ms | p99 > 200 ms |
| Pakketverlies | Percentage verloren pakket | < 1% | > 3% |
| Jitter | Variatie in latentie | < 10 ms | > 30 ms |
| Berichten/sec | Berichten verwerkt per seconde per server | > 10K bericht/s | < 5K bericht/s |
| Bandbreedte per speler | KB/s-uitvoer per speler | 5-30KB/s | > 50 KB/s |
| Herverbindingssnelheid | Percentage succesvolle herverbindingen | > 90% | < 70% |
| Opstarttijd van server | Tijd om een nieuw exemplaar te starten | < 10s | > jaren 30 |
// 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. Alles samenbrengen: de complete architectuur
Hier is een overzicht van alle componenten van een moderne multiplayer-gamebackend en hoe ze interacteren met elkaar.
+==============================================================+
| 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 |
+============================================================+
Volgende stappen
In dit artikel heb je een volledig inzicht gekregen in de anatomie van een game-backend multiplayer: van netwerkarchitectuur tot transportprotocollen, van de gezaghebbende gameloop tot berichtserialisatie, van verbindingsbeheer tot schaalpatronen. Je hebt gezien hoe elke component samenwerkt met de andere en de architecturale afwegingen ontwerpkeuzes begeleiden.
In de volgend artikel we gaan in op de kern van multiplayer-netcode: de synchronisatie van de status. We gaan dieper in op de klantkant voorspelling, serverafstemming, interpolatie, vertragingscompensatie en rollback netcode. We zullen zien hoe moderne games ondanks dat de illusie van een soepele ervaring creëren netwerklatentie.
Aanvullende bronnen
- Gabriël Gambetta: "Fast-Paced Multiplayer" - De serie referentieartikelen over client-serverarchitectuur voor gaming
- Klepontwikkelaar Wiki: "Source Multiplayer Networking" - Technische documentatie over Source Engine-netcode
- Glenn Fiedler: "Game Networking" - Complete serie over protocollen, statussynchronisatie en beveiliging
- Agones-documentatie: Volledige gids voor het orkestreren van gameservers op Kubernetes
- Colyseus.io: Open-source Node.js-gameserverframework met automatische statussynchronisatie
- Nakama van Heroic Labs: Open-source gameserver met matchmaking, opslag en realtime multiplayer







