Mobil veya web tabanlı çok oyunculu oyunlarda (örneğin PvP kelime yarışması oyunumuz Find & Win gibi sistemlerde), oyuncuların hamlelerinin diğer oyuncuya 50 milisaniyenin altında aktarılması gerekir. Klasik HTTP istek-cevap döngüsü (REST API veya polling), her istekte TCP handshake ve HTTP başlık (header) yükü getirdiği için gerçek zamanlı etkileşimlerde yetersiz kalır.
WebSocket protokolü (RFC 6455), tek bir TCP bağlantısı üzerinden istemci (client) ile sunucu (server) arasında iki yönlü (full-duplex) ve sürekli açık bir iletişim kanalı kurarak bu sorunu kökten çözer. Bu makalede, Node.js ve hafif ws kütüphanesi kullanarak 1v1 rekabetçi bir eşleşme (matchmaking) ve oda (room) senkronizasyon motoru inşa edeceğiz.
HTTP Polling vs WebSocket: Mimari Karşılaştırması
- HTTP Polling: İstemci her 1 saniyede bir "Rakip hamle yaptı mı?" diye sunucuya sorar. Sunucu binlerce gereksiz boş istek işler; ağ trafiği ve sunucu CPU tüketimi fırlar.
- WebSocket: Bağlantı açılışta bir kez el sıkışır (HTTP Upgrade). Sonrasında sunucu veya istemci tek bir baytlık ek başlık yüküyle dilediği an anlık paket iletir. Gecikme doğrudan ağın fiziksel ping süresine iner.
Node.js Çok Oyunculu Oda ve Eşleşme Motoru (Server Kodu)
Aşağıdaki kod, boşta bekleyen oyuncuları kuyruğa (queue) alan, iki oyuncu bulduğunda izole bir oyun odası (room) oluşturan ve kopan bağlantıları temizleyen tam teşekküllü bir WebSocket sunucusudur:
// server.js - Node.js WebSocket Çok Oyunculu Eşleşme Motoru
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const wss = new WebSocket.Server({ port: 8080 });
let waitingPlayer = null; // Eşleşme bekleyen oyuncu
const activeRooms = new Map(); // Oda Id -> { player1, player2, gameState }
console.log("WebSocket Eşleşme Sunucusu 8080 Portunda Hazır!");
wss.on('connection', (ws) => {
ws.id = uuidv4();
ws.isAlive = true;
// Heartbeat: Bağlantının canlı olduğunu doğrula
ws.on('pong', () => { ws.isAlive = true; });
console.log(`Yeni oyuncu bağlandı: ${ws.id}`);
// Eşleşme Mantığı (Matchmaking)
if (!waitingPlayer) {
waitingPlayer = ws;
ws.send(JSON.stringify({ type: 'STATUS', message: 'Rakip bekleniyor...' }));
} else {
const roomId = uuidv4();
const opponent = waitingPlayer;
waitingPlayer = null;
const room = {
id: roomId,
p1: opponent,
p2: ws,
scores: { [opponent.id]: 0, [ws.id]: 0 }
};
activeRooms.set(roomId, room);
opponent.roomId = roomId;
ws.roomId = roomId;
// Her iki oyuncuya oyunun başladığını bildir
opponent.send(JSON.stringify({ type: 'GAME_START', roomId, role: 'P1' }));
ws.send(JSON.stringify({ type: 'GAME_START', roomId, role: 'P2' }));
console.log(`Oda açıldı: ${roomId} (P1: ${opponent.id} vs P2: ${ws.id})`);
}
// İstemciden Gelen Mesajlar (Hamle, Kelime Tahmini vb.)
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
const room = activeRooms.get(ws.roomId);
if (!room) return;
if (data.type === 'MOVE') {
// Hamleyi rakip oyuncuya ilet
const target = (ws === room.p1) ? room.p2 : room.p1;
if (target && target.readyState === WebSocket.OPEN) {
target.send(JSON.stringify({
type: 'OPPONENT_MOVE',
payload: data.payload
}));
}
}
} catch (err) {
console.error("Geçersiz JSON mesajı:", err);
}
});
// Oyuncu Bağlantısı Koptuğunda
ws.on('close', () => {
console.log(`Oyuncu ayrıldı: ${ws.id}`);
if (waitingPlayer === ws) {
waitingPlayer = null;
}
if (ws.roomId && activeRooms.has(ws.roomId)) {
const room = activeRooms.get(ws.roomId);
const survivor = (ws === room.p1) ? room.p2 : room.p1;
if (survivor && survivor.readyState === WebSocket.OPEN) {
survivor.send(JSON.stringify({ type: 'OPPONENT_LEFT', message: 'Rakip oyundan ayrıldı. Zafer!' }));
}
activeRooms.delete(ws.roomId);
}
});
});
// Zombi Bağlantı Temizleme (Heartbeat Interval)
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
İstemci Tarafı (JavaScript WebSocket İstemcisi)
// client.js - Tarayıcı veya React Native İstemcisi
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', () => {
console.log('Sunucuya bağlanıldı, eşleşme aranıyor...');
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch(data.type) {
case 'STATUS':
console.log(data.message);
break;
case 'GAME_START':
console.log(`Oyun Başladı! Oda: ${data.roomId} - Rol: ${data.role}`);
break;
case 'OPPONENT_MOVE':
console.log("Rakip hamlesi geldi:", data.payload);
break;
case 'OPPONENT_LEFT':
alert(data.message);
break;
}
});
// Hamle göndermek için:
function sendMove(letter, score) {
socket.send(JSON.stringify({
type: 'MOVE',
payload: { letter, score }
}));
}
Yatay Ölçekleme: Milyonlarca Oyuncuya Nasıl Ulaşılır?
Tek bir Node.js süreci genellikle 10.000 ile 50.000 aktif soket bağlantısını yönetebilir. Ancak sunucu çökmelerine karşı dayanıklılık (high availability) ve yatay ölçekleme (horizontal scaling) gerektiğinde şu araçlar devreye girer:
- Redis Pub/Sub & Redis Adapter: Farklı sunucu makinelerinde (Node.js Cluster veya Kubernetes podları) bağlı oyuncuların birbirine mesaj atabilmesi için odalar arası mesaj kuyruğu olarak Redis kullanılır.
- Nginx Reverse Proxy: İstemci trafiğini SSL sertifikası (WSS: WebSockets Secure) ile karşılar ve arka plandaki Node.js sunucularına
ip_hashveya round-robin ile dağıtır.