게임 백엔드가 다른 모든 것과 다른 점
웹 애플리케이션용 백엔드를 개발해 본 적이 있다면 그것이 어떻게 작동하는지 알 것입니다. 클라이언트가 요청을 보냅니다. HTTP는 서버가 이를 처리하고 데이터베이스를 쿼리하고 JSON 응답을 반환합니다. 주기가 반복됩니다 모든 상호작용. 대기 시간이 몇 밀리초 더 늘어나나요? 사용자는 이를 인지하지도 못합니다.
Un 멀티플레이어 게임 백엔드 그는 완전히 다른 우주에 살고 있습니다. 우리는 여기서 말하는 것이 아닙니다 요청과 답변: 지속적인 흐름 밀리초마다 양방향 데이터 수백 명의 플레이어가 초당 수십 번 변경되는 공통 상태를 공유하는 경우 100ms 지연은 승리와 패배의 차이를 의미할 수 있습니다.
이 시리즈의 첫 번째 기사에서는 게임 백엔드 엔지니어링, 우리는 해부학을 탐구할 것입니다 멀티플레이어 게임 백엔드로 완성: 네트워크 아키텍처부터 통신 프로토콜, 게임까지 메시지 직렬화에 대한 서버 측 루프, 최대 확장 전략 및 플랫폼까지 서비스로서의 백엔드. 마지막에는 각 구성 요소와 선택 항목에 대한 완전한 정신 지도를 갖게 됩니다. 당신이 처리해야 할 건축 문제.
시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 현재 위치 - 게임 백엔드 분석 | 아키텍처, 프로토콜, 구성 요소 |
| 2 | 상태 동기화 | 넷코드, 보간, 예측 |
| 3 | 매치메이킹 엔진 | ELO 알고리즘, 대기열, 로비 |
| 4 | 전용 게임 서버 | 인프라 및 오케스트레이션 |
| 5 | 치트 방지 아키텍처 | 서버 측 검증, 탐지 |
| 6 | LiveOps 및 수익 창출 | 게임 내 경제, 라이브 이벤트 |
| 7 | 원격 측정 및 분석 | 측정항목, 데이터 파이프라인, 대시보드 |
| 8 | 관찰 가능성 | 로깅, 추적, 경고 |
| 9 | 클라우드 게임 인프라 | 스트리밍, 엣지 컴퓨팅, 대기 시간 |
| 10 | 오픈 소스 게임 스택 | 나카마, 콜리세우스, 아고네스 |
무엇을 배울 것인가
- 웹 백엔드와 게임 백엔드의 근본적인 차이점
- 네트워크 모델: 클라이언트-서버, P2P 및 릴레이
- 서버측 게임 루프 작동 방식(권한 있는 서버)
- 통신 프로토콜: TCP vs UDP vs WebSocket vs WebRTC
- 메시지 직렬화: 프로토콜 버퍼, FlatBuffer, MessagePack
- 연결 관리: 세션, 재연결, 하트비트
- 데이터베이스 선택: Redis, PostgreSQL, 시계열
- 확장 패턴: 로비 서버, 월드 서버, 인스턴스화된 영역
- BaaS 플랫폼: PlayFab, GameLift, Nakama, Colyseus
1. 웹 백엔드와 게임 백엔드: 서로 다른 두 세계
게임 백엔드에 근본적으로 다른 접근 방식이 필요한 이유를 이해하기 위해 요구 사항을 비교해 보겠습니다. 전통적인 웹 애플리케이션의 기본입니다.
요구 사항 비교: 웹과 게임
| 나는 기다린다 | 웹 백엔드 | 게임 백엔드 멀티플레이어 |
|---|---|---|
| 커뮤니케이션 모델 | 요청-응답(HTTP) | 지속적인 양방향 스트리밍 |
| 허용 가능한 지연 시간 | 200-500ms | 16-50ms(60fps에서 한 프레임 = 16.6ms) |
| 업데이트 빈도 | 주문형(클릭, 제출) | 초당 20~128회(틱 속도) |
| 상태 | 무상태(각 독립적 요청) | 상태 저장(메모리의 공유 상태) |
| 일관성 | 허용 가능한 일관성 | 실시간의 강력한 일관성 |
| 확장성 | 수평(로드 밸런서 + 복제본) | 세션당 수직 + 세션당 수평 |
| 데이터 손실 허용 | 0(모든 거래가 중요함) | 선택적(상실 포지션은 괜찮지만 구매는 안 됨) |
| 세션 기간 | 분(탐색) | 시간(게임 세션) |
| 사용자당 대역폭 | 산발적인 KB | 5~50KB/s 연속 |
중요한 점은 자연이다. 상태 저장 게임 백엔드의 웹 서버는 다음과 같습니다. 언제든지 교체됩니다. 트래픽을 다른 인스턴스로 직접 전달하면 됩니다. 게임 서버 활성 게임의 상태(플레이어의 위치, 비행 중인 총알, 활성 효과, 점수. 해당 서버가 충돌하면 게임이 손실됩니다.
게임의 지연 문제
128틱/초의 경쟁 FPS에서 서버는 매 1회 업데이트를 처리합니다. 7.8ms. 플레이어의 RTT(네트워크 대기 시간)가 50ms인 경우 해당 플레이어의 입력은 25ms 늦게 서버에 도착합니다. 응답은 25ms 후에 도착합니다. 플레이어는 50ms 뒤의 세계를 봅니다. 서버의 현실. 200ms에서는 게임을 플레이할 수 없게 됩니다. 그렇기 때문에 다음과 같은 기술을 사용합니다. 클라이언트 측 예측 그리고 지연 보상 그것들은 근본적입니다.
2. 네트워크 모델: 플레이어가 연결되는 방식
첫 번째 아키텍처 결정은 네트워크 모델, 즉 클라이언트가 서로 통신하는 방법에 관한 것입니다. 그리고 서버랑? 세 가지 주요 접근 방식이 있으며 각각 특정 장단점이 있습니다.
2.1 클라이언트-서버(신뢰할 수 있는 서버)
현대 게임의 지배적인 모델. 하나의 중앙 서버가 유일한 서버입니다. 권한 에 게임의 상태. 클라이언트는 입력(키 누르기, 마우스 움직임)을 서버로 보냅니다. 이를 검증하고 세계 상태를 업데이트한 후 결과를 모든 클라이언트에 보냅니다. 어떤 클라이언트도 할 수 없습니다 게임 상태를 직접 변경합니다.
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] |
장점: 최대 보안(서버가 모든 것을 제어), 통합 안티 치트, 모든 플레이어에 대한 일관된 상태, 디버깅이 쉽습니다. 단점: 높은 인프라 비용(게임당 하나의 서버), 대기 시간 추가(각 입력은 왕복해야 함), 단일 실패 지점.
2.2 피어 투 피어(P2P)
각 클라이언트는 다른 모든 클라이언트와 직접 통신합니다. 중앙 서버가 없습니다 - 모든 플레이어 클라이언트와 "서버" 모두를 포함합니다. 이 모델은 격투 게임에서 인기가 많았습니다 RTS에서는 플레이어 수가 제한되어 있습니다(2~8명).
장점: 서버 비용 없음, 피어 간 대기 시간 최소화(직접 연결), 어떤 노드가 죽어도 살아남습니다. 단점: 치트 방지가 불가능함(각 클라이언트는 자신의 상태에 대한 권한을 가짐) 플레이어 수(N*(N-1)/2 연결)에 따른 기하급수적인 복잡성, NAT 통과 문제.
2.3 릴레이 서버(프록시 서버)
두 모델 간의 절충안입니다. 중앙 서버는 다음과 같은 역할을 합니다. 계전기: 각 사용자로부터 메시지를 받습니다. 클라이언트에 전달하여 다른 모든 사람에게 전달하지만 게임 로직을 처리하지는 않습니다. 시뮬레이션이 발생합니다 클라이언트, 서버 및 "우체부"에만 있습니다.
네트워크 모델 비교
| 특성 | 클라이언트-서버 | P2P | 계전기 |
|---|---|---|---|
| 안전 | 높음(권한 서버) | 낮음(권한 없음) | 평균(클라이언트에 따라 다름) |
| 서버 비용 | 높은 | Nullo | 베이스 |
| 확장성 | 수백 명의 플레이어 | 2-8명의 선수 | 수십 명의 플레이어 |
| 숨어 있음 | 평균(왕복) | 낮음(직접) | 미디어(서버를 통해) |
| 일반적인 사용 | FPS, MMO, 배틀로얄 | 격투, 클래식 RTS | 협동, 캐주얼, 모바일 |
| Esempi | 용기의, 포트나이트, CS2 | 스트리트 파이터, 스타크래프트 | 우리 가운데, Fall Guys |
3. 서버측 게임 루프
권위 있는 게임 백엔드의 핵심은 게임 루프: 반복되는 사이클 a 일정한 주파수( 틱율), 입력 처리, 상태 업데이트 및 전송 그 결과를 고객에게. 틱 속도 빈도는 "시간 분해능"을 결정합니다. 시뮬레이션.
장르별 틱율
| 유형 | 틱율 | 간격 | Esempio |
|---|---|---|---|
| 경쟁력 있는 FPS | 128Hz | 7.8ms | 용감한, CS2 |
| 표준 FPS | 64Hz | 15.6ms | 오버워치 2 |
| 배틀로얄 | 20~30Hz | 33-50ms | 포트나이트, PUBG |
| MMO | 10~20Hz | 50-100ms | 월드 오브 워크래프트 |
| 전략/전환 | 1-10Hz | 100ms-1s | 문명, 하스스톤 |
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
);
}
}
예산 및 초과 실행
64틱/초에서 각 틱에는 15.6ms의 예산. 틱 로직(물리, 충돌, AI, 네트워킹) 시간이 더 오래 걸리고 서버에 지연이 누적되고 플레이어가 그들은 지연을 인식합니다. 틱 예산을 모니터링하는 것은 기본입니다. 프로덕션에서는 p99 평균이 아닌 틱 기간입니다.
4. 국가 관리: 국가의 세 가지 수준
멀티플레이어 게임의 상태는 단일체 덩어리가 아닙니다. 특성이 있는 레벨로 나누어져 있습니다. 그리고 다른 요구 사항. 각 계층에는 저장, 동기화 및 지속성 전략이 필요합니다. 다르다.
게임 상태의 세 가지 수준
| 수준 | 포함된 내용 | 업데이트 빈도 | 고집 | 저장 |
|---|---|---|---|---|
| 프레임 상태 | 위치, 속도, 회전, 발사체 | 모든 틱(20-128Hz) | 기억 속에만 | 게임 서버 RAM |
| 세션 상태 | HP, 인벤토리, 점수, 버프/디버프 | 이벤트 발생시(파손, 회수) | 경기가 진행되는 동안 | RAM + Redis(백업) |
| 지속 상태 | 프로필, 통계, 구매, 순위 | 게임 종료 시 또는 거래 시 | 영구적인 | PostgreSQL/몽고DB |
// === 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;
}
레벨로 분리하는 것은 성능을 위한 기본입니다. 그만큼 프레임 상태 그래야만 해 매 틱마다 직렬화되어 모든 클라이언트에 전송되기 때문에 가능한 한 컴팩트합니다. 그만큼 세션 상태 변경된 경우에만 전송됩니다. 그만큼 지속적인 상태 절대 방송되지 않습니다. 소유자만 요청할 수 있으며 저장됩니다. 데이터베이스에서 비동기적으로.
5. 통신 프로토콜: TCP, UDP, WebSocket, WebRTC
전송 프로토콜의 선택은 아키텍처에서 가장 영향력 있는 결정 중 하나입니다. 게임 백엔드. 각 프로토콜은 안정성, 대기 시간 및 복잡성 간에 서로 다른 균형을 제공합니다.
5.1 TCP(전송 제어 프로토콜)
TCP는 모든 패킷의 질서 있고 안정적인 전달을 보장합니다. 패킷이 손실되면 TCP 이를 재전송하고 손실된 패킷이 도착할 때까지 후속 패킷의 전달을 차단합니다. 이 현상이라고 한다 헤드 오브 라인 차단 그리고 그것은 실시간 게임의 치명적인 적입니다.
5.2 UDP(사용자 데이터그램 프로토콜)
UDP 및 "실행 후 잊어버리기": 배달, 순서 또는 무결성을 보장하지 않고 패킷을 보냅니다. 만약 패킷이 손실되어 재전송되지 않습니다. 끔찍하게 들리겠지만 실시간 게임에서는 그리고 정확히 필요한 것: 100ms 전 플레이어의 위치, 그것이 있는 경우에는 관련이 없습니다. 16ms 전부터요.
5.3 웹소켓
WebSocket은 TCP 위에서 작동하지만 전이중 양방향 연결을 제공합니다. 그리고 프로토콜 기본 UDP를 사용할 수 없는 브라우저 기반 및 모바일 게임의 표준입니다. 대기 시간 e 순수 UDP보다 크지만 구현이 쉽고 보편적인 호환성이 있음 다양한 장르에 실용적인 선택이 될 수 있습니다.
5.4 WebRTC(데이터채널)
WebRTC DataChannel은 UDP를 통한 SCTP를 기반으로 하는 P2P(또는 클라이언트-서버) 통신을 제공합니다. 신뢰할 수 있는 모드와 신뢰할 수 없는 모드를 모두 지원하며 채널별로 구성할 수 있습니다. 그리고 유일한 옵션은 브라우저에서 UDP와 유사한 통신을 받으세요.
게임용 네트워크 프로토콜 비교
| 나는 기다린다 | TCP | UDP | 웹소켓 | WebRTC DC |
|---|---|---|---|---|
| 수송 | TCP | UDP | TCP | SCTP/UDP |
| 배송 보장 | Si | No | Si | 구성 가능 |
| 보장된 주문 | Si | No | Si | 구성 가능 |
| 헤드 오브 라인 차단 | Si | No | Si | 아니요(신뢰할 수 없음) |
| 일반적인 대기 시간 | 높음(재전송) | 최소한의 | 평균 | 낮은 |
| 브라우저 지원 | 아니요(직접) | No | Si | Si |
| NAT 통과 | 필요하지 않음 | 문제가 있는 | 필요하지 않음 | 통합(ICE) |
| 복잡성 | 낮은 | 높음(맞춤형 신뢰성) | 낮은 | 높음(신호) |
| 다음에 이상적입니다. | 채팅, 교대근무, 로비 | FPS, 레이싱, 시뮬레이션 | 브라우저 게임, 모바일 | FPS 브라우저, VoIP |
WebTransport: 미래?
웹운송 그리고 결합을 약속하는 HTTP/3(QUIC) 기반의 새로운 표준 세계 최고: 다중화된 양방향 스트림, 신뢰할 수 있는 모드와 신뢰할 수 없는 모드, 헤드 오브 라인 차단 및 기본 브라우저 액세스가 없습니다. 2026년에는 브라우저 지원이 성숙해지고 있지만(Chrome 및 Edge는 이를 지원함) 서버측 지원은 여전히 제한적입니다. 그리고 차세대 브라우저 기반 게임을 감시하기 위한 프로토콜입니다.
5.5 하이브리드 패턴: 다중 채널
최신 게임 백엔드는 하나의 프로토콜만 사용하는 경우가 거의 없습니다. 가장 일반적인 패턴은 다중 채널: 다양한 데이터 유형에 대한 다양한 채널.
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. 메시지 직렬화: 모든 바이트가 계산됩니다.
100명의 플레이어에게 초당 64개의 업데이트를 보내면 메시지의 모든 추가 바이트는 초당 6,400번 곱합니다. 직렬화 형식의 선택이 영향을 미칩니다. 대역폭, 대기 시간 및 인프라 비용에 직접적으로 영향을 미칩니다.
직렬화 형식 비교
| 체재 | 유형 | 크기(상대적) | 인코딩 속도 | 디코드 속도 | 계획 |
|---|---|---|---|---|---|
| JSON | 텍스트 | 100%(기준선) | 느린 | 느린 | No |
| 메시지팩 | 트랙 | ~60-70% | 빠른 | 빠른 | No |
| 프로토콜 버퍼 | 트랙 | ~30-40% | 매우 빠르다 | 매우 빠르다 | 예(.proto) |
| FlatBuffer | 트랙 | ~35-45% | 제로 카피 | 제로 카피 | 예(.fbs) |
| 캡틴 프로토 | 트랙 | ~35-45% | 제로 카피 | 제로 카피 | 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
언제 무엇을 사용해야 하는가?
- JSON: 프로토타이핑, 턴바이턴 게임, 외부 웹 서비스와의 통신. 디버깅이 쉽고 보편적입니다.
- 메시지팩: "Binary JSON" - 코드 로직을 변경하지 않고 크기를 줄이려는 경우. JSON에 대한 드롭인 대체
- 프로토콜 버퍼: 고성능 게임 서버의 사실상의 표준입니다. 형식화된 스키마, 다중 언어 코드 생성, 이전 버전과의 호환성
- FlatBuffer: 역직렬화 비용조차 너무 많은 경우. 제로 복사 액세스: 메모리를 할당하지 않고 버퍼에서 직접 필드를 읽습니다.
7. 연결 관리: 세션, 하트비트, 재연결
멀티플레이어 게임에서는 네트워크 연결이 100% 안정적일 수 없습니다. 선수들이 그러는데 불안정한 WiFi, 네트워크 변경(4G/WiFi), 클라이언트 충돌 또는 단순히 연결 끊김 게임을 종료하기 때문입니다. 강력한 게임 백엔드는 이 모든 것을 투명하게 처리해야 합니다.
7.1 세션 수명주기
[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 하트비트 및 연결 끊김 감지
Il 심장 박동 클라이언트가 서버에 보내는 주기적 메시지(또는 그 반대) 연결이 활성화되었음을 나타냅니다. 서버가 일정 기간 동안 하트비트를 수신하지 못하는 경우 구성 가능하며 클라이언트 연결이 끊어진 것으로 간주됩니다. 심장박동은 또한 대기 시간(RTT) 그리고 지터.
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. 데이터베이스: 지속성 스택
게임 백엔드는 하나의 데이터베이스만 사용하는 것이 아니라 하나의 데이터베이스를 사용합니다. 스택 스토리지 엔진의 각각은 특정 데이터 유형 및 액세스 패턴에 최적화되어 있습니다.
게임 백엔드용 스택 데이터베이스
| 레이어 | 기술 | 무엇을 저장하는가 | 숨어 있음 |
|---|---|---|---|
| L1 - 메모리 내 | RAM의 데이터 구조 | 프레임 상태, 입력 버퍼 | < 0.001ms |
| L2 - 분산 캐시 | 레디스/잠자리 | 세션 상태, 리더보드, 매치메이킹 대기열 | 0.1-1ms |
| L3 - 관계형 데이터베이스 | PostgreSQL / 바퀴벌레DB | 플레이어 프로필, 인벤토리, 구매, 순위 | 1-10ms |
| L4 - 시계열 | TimescaleDB / InfluxDB | 원격 측정, 성능 지표, 분석 | 1-5ms |
| L5 - 개체 저장소 | S3 / GCS | 리플레이, 스크린샷, 자산, 백업 | 50-200ms |
8.1 실시간 상태를 위한 Redis
Redis는 게임 백엔드 데이터베이스 스택의 가장 중요한 구성 요소입니다. 그 구조 기본 데이터(정렬된 세트, 해시, 목록, 게시/구독)는 게임 패턴에 직접 매핑됩니다.
- 정렬된 세트 리더보드 및 순위(ZADD, ZRANK, ZRANGE)
- 해시 세션 상태(HSET, HGETALL)
- 기울기 매치메이킹 대기열용(LPUSH, RPOP)
- 게시/구독 게임 서버와 서비스 간의 통신을 위해
- 스트림 이벤트 소싱 및 감사 로그용
8.2 지속성을 위한 PostgreSQL
서버를 다시 시작해도 유지되어야 하는 모든 데이터: 플레이어 프로필, 진행 상황,
재고, 경제 거래, 배치 내역. PostgreSQL은 ACID 트랜잭션을 제공합니다.
반구조화된 데이터용 JSON(B) 및 다음과 같은 확장 기능 포함 pgvector 지원하다
또한 기술 기반 매치메이킹을 위한 유사성 검색도 가능합니다.
9. 인프라: 전용 서버, 컨테이너, 오케스트레이션
로드 밸런서 뒤에 복제본을 추가하여 확장하는 상태 비저장 웹 서버와 달리 게임 서버는 상태 저장: 각 인스턴스는 하나 이상의 활성 게임을 관리합니다. 상태가 메모리에 있습니다. 이로 인해 확장 및 조정이 훨씬 더 복잡해졌습니다.
9.1 전용 게임 서버
Un 전용 게임 서버 단일 시뮬레이션을 실행하는 프로세스 게임(또는 게임 세계의 영역). "리스너 서버"(플레이어가 서버 역할도 함) 전용 서버는 클라우드의 전용 하드웨어에서 실행되므로 일관된 성과와 공정성.
[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 Docker를 사용한 컨테이너화
각 게임 서버는 Docker 컨테이너에서 실행되어 격리, 재현성 및 배포를 보장합니다. 빨리. 컨테이너에는 서버 바이너리, 구성 및 종속성이 포함됩니다. 컨테이너가 가벼우면 몇 초 안에 새 인스턴스를 시작할 수 있습니다.
9.3 쿠버네티스와 Agones를 사용한 오케스트레이션
아고네스 Kubernetes를 확장하여 관리하는 Google의 오픈소스 프로젝트 전용 게임 서버. GameServer를 정의하기 위한 CRD(Custom Resource Definition)를 제공합니다. Fleet 및 FleetAutoscaler. 차량 관리자는 배치 수요를 모니터링하고 자동으로 확장합니다. 사용 가능한 서버 수.
오케스트레이션 플랫폼 비교
| 플랫폼 | 유형 | 구름 | 강점 |
|---|---|---|---|
| 아고네스 | 오픈소스(K8s) | 모든 + 온프레미스 | 완전한 유연성, 공급업체 종속 없음 |
| 아마존 게임리프트 | 관리형(AWS) | AWS | AWS 통합, FlexMatch 매치메이킹 |
| Azure PlayFab | 관리형(Azure) | 하늘빛 | 완전한 생태계(LiveOps, 분석, 경제) |
| Google 클라우드 게임 서버 | 관리형(GCP) | GCP | Agones 기반, 글로벌 확장 |
10. 확장 패턴: 로비, 월드, 구역
모든 게임의 규모가 동일하지는 않습니다. 스케일링 패턴은 성별과 게임 구조. 세 가지 주요 패턴은 다음과 같습니다.
10.1 로비/매치 서버
FPS, 배틀로얄, MOBA에서 사용됩니다. 각 게임은 숫자가 포함된 격리된 인스턴스입니다. 고정된 플레이어 수(10-100). 경기가 끝나면 서버가 파괴되고 자원이 재활용. 확장 = 인스턴스 수를 늘립니다.
10.2 월드 서버(영구 월드)
MMO에서 사용됩니다. 지속적이고 분할된 세계 지역, 각각 관리됨 전용 서버에서. 플레이어는 투명한 핸드오프를 통해 영역 사이를 이동합니다. Zone 서버는 서로 통신하여 경계를 관리합니다.
10.3 인스턴스화된 영역
하이브리드: 세계는 지속되지만 특정 영역(던전, 공격대, 경기장)이 옵니다. 인스턴스화 동적으로. 각 플레이어 그룹은 자신의 복사본을 받습니다. 지역의. 이를 통해 과부하 없이 고밀도 영역을 확장할 수 있습니다. 월드 서버.
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. 게임을 위한 서비스로서의 백엔드: 플랫폼
모든 사람이 처음부터 게임 백엔드를 구축해야 하거나 구축하기를 원하지는 않습니다. 플랫폼이 있습니다 즉시 사용 가능한 구성 요소를 제공하는 서비스형 백엔드(BaaS): 매치메이킹, 리더보드, 인증, 저장, 게임 내 경제. 선택은 예산, 통제에 달려 있습니다. 원하는 게임 장르와
게임용 BaaS 플랫폼 비교
| 플랫폼 | 서버 언어 | 오픈 소스 | 자체 호스팅 | 강점 | 이상적인 대상 |
|---|---|---|---|---|---|
| 나카마 | 가, TS, 루아 | Si | Si | 실시간, 매치메이킹, 저장 | 인디, 중형, 모바일 |
| 콜리세우스 | 타입스크립트 | Si | Si | 신뢰할 수 있는 자동 상태 동기화 | 브라우저 게임, 프로토타입 |
| 플레이팹 | C#(애저 함수) | No | No | LiveOps, 경제, 분석 | AAA, 모바일 F2P |
| 아마존 게임리프트 | C++, C# | No | No | 차량 관리, FlexMatch | AAA 멀티플레이어 |
| 광자 | C# | No | 부분 | Unity 통합, 릴레이 | 유니티 게임, 모바일 |
| 거울 | C# | Si | Si | Unity 네트워킹, HLAPI 대체 | 유니티 인디 게임 |
구축 vs 구매: 선택 기준
- BaaS 사용 if: 제한된 예산, 출시 기간 우선, 표준 장르(캐주얼, 퍼즐, 카드 게임), 소규모 팀
- 맞춤 제작 se: 극도의 대기 시간 요구 사항(<20ms), 고유한 게임 논리, 넷코드 및 치트 방지에 대한 전체 제어 필요, 규모 >100K CCU
- 하이브리드 접근 방식: 인증, 리더보드 및 소셜에는 BaaS를 사용하지만 게임 로직에는 사용자 정의 게임 서버를 구축합니다.
12. 성능 지표: 모니터링 대상
관찰 가능성과 시한폭탄이 없는 게임 백엔드입니다. 다음은 측정항목입니다. 모든 팀이 프로덕션에서 모니터링해야 하는 기본 사항입니다.
주요 게임 백엔드 지표
| 미터법 | 설명 | 목표 | 경보 |
|---|---|---|---|
| CCU | 동시 접속 사용자 | 장르에 따라 다르죠 | > 90% 용량 |
| 틱 지속시간 (p99) | 진드기 처리 시간 | < 틱 예산의 80% | > 예산의 90% |
| RTT(p50/p95/p99) | 왕복 시간 클라이언트-서버 | p50 < 50ms, p99 < 150ms | p99 > 200ms |
| 패킷 손실 | 손실된 패킷 비율 | < 1% | > 3% |
| 지터 | 지연 시간 변화 | < 10ms | > 30ms |
| 메시지/초 | 서버당 초당 처리되는 메시지 수 | > 10K 메시지/초 | < 5K 메시지/초 |
| 플레이어당 대역폭 | 플레이어당 KB/s 출력 | 5~30KB/초 | > 50KB/초 |
| 재접속률 | 성공적인 재연결 비율 | > 90% | < 70% |
| 서버 시작 시간 | 새 인스턴스를 시작할 시간입니다. | 10초 미만 | > 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. 모든 것을 하나로 묶기: 완전한 아키텍처
다음은 최신 멀티플레이어 게임 백엔드의 모든 구성 요소와 방법에 대한 개요입니다. 그들은 서로 상호 작용합니다.
+==============================================================+
| 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 |
+============================================================+
다음 단계
이 기사에서는 게임 백엔드의 구조를 완전히 이해했습니다. 멀티플레이어: 네트워크 아키텍처부터 전송 프로토콜까지, 권위 있는 게임 루프까지 메시지 직렬화, 연결 관리부터 확장 패턴까지. 각 구성 요소가 다른 구성 요소와 상호 작용하는 방식과 아키텍처의 장단점을 살펴보았습니다. 디자인 선택을 안내합니다.
에서 다음 기사 우리는 멀티플레이어 넷코드의 핵심인 상태 동기화. 우리는 클라이언트 측을 심층적으로 탐구할 것입니다. 예측, 서버 조정, 보간, 지연 보상 및 롤백 넷코드. 우리는 현대 게임이 어떻게 매끄러운 경험의 환상을 만들어내는지 살펴보겠습니다. 네트워크 대기 시간.
추가 리소스
- 가브리엘 강베타: "Fast-Paced Multiplayer" - 게임용 클라이언트-서버 아키텍처에 대한 참조 기사 시리즈
- 밸브 개발자 위키: "소스 멀티플레이어 네트워킹" - 소스 엔진 넷코드에 대한 기술 문서
- 글렌 피들러: "게임 네트워킹" - 프로토콜, 상태 동기화 및 보안에 대한 전체 시리즈
- 아고네스 문서: Kubernetes에서 게임 서버를 조정하는 방법에 대한 전체 가이드
- Colyseus.io: 자동 상태 동기화 기능을 갖춘 오픈 소스 Node.js 게임 서버 프레임워크
- Heroic Labs의 나카마: 매치메이킹, 스토리지, 실시간 멀티플레이어 기능을 갖춘 오픈 소스 게임 서버







