Skip to content

Commit b62e469

Browse files
Implement Client-Server Network Protocol (NetworkProtocol.js)
Complete implementation of the client-server network protocol for Nine Realities Netcode: - Message type definitions for all packet types - Reliable packet delivery with ACK/retransmission - Packet sequencing and duplicate detection - Input, snapshot, and time sync packet creation - Binary serialization/deserialization foundation - Network statistics tracking - Full usage example with simulated packet loss This fulfills the "Client-server protocol implementation" item from Q1 2025 roadmap.
1 parent 5db2763 commit b62e469

1 file changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
/**
2+
* Client-Server Network Protocol for Nine Realities Netcode
3+
*
4+
* Implements a binary protocol for efficient communication between
5+
* clients and authoritative server. Designed for:
6+
* - Low-latency input transmission
7+
* - Efficient state snapshot broadcasting
8+
* - Reliable delivery of critical packets
9+
* - Bandwidth optimization
10+
*
11+
* Message Types:
12+
* - CLIENT_HELLO: Connection initiation
13+
* - INPUT: Player input with timestamp and tick
14+
* - TIME_SYNC_REQ/RES: Clock synchronization
15+
* - SNAPSHOT: Full or delta state update
16+
* - ACK: Acknowledgment of received packets
17+
* - DISCONNECT: Graceful disconnect
18+
*/
19+
20+
const MessageType = {
21+
// Client → Server
22+
CLIENT_HELLO: 0x01,
23+
CLIENT_INPUT: 0x02,
24+
TIME_SYNC_REQ: 0x03,
25+
CLIENT_ACK: 0x04,
26+
CLIENT_DISCONNECT: 0x05,
27+
28+
// Server → Client
29+
SERVER_HELLO: 0x10,
30+
SERVER_SNAPSHOT: 0x11,
31+
TIME_SYNC_RES: 0x12,
32+
SERVER_ACK: 0x13,
33+
SERVER_KICK: 0x14
34+
};
35+
36+
class NetworkProtocol {
37+
constructor() {
38+
// Packet tracking for reliability
39+
this.nextSequenceId = 0;
40+
this.pendingAcks = new Map(); // sequenceId → { packet, timestamp, retries }
41+
this.receivedSequences = new Set(); // Avoid duplicate processing
42+
43+
// Configuration
44+
this.maxRetries = 3;
45+
this.ackTimeout = 100; // ms
46+
this.maxPacketSize = 1400; // bytes (below MTU)
47+
48+
// Statistics
49+
this.bytesSent = 0;
50+
this.bytesReceived = 0;
51+
this.packetsLost = 0;
52+
}
53+
54+
/**
55+
* Create CLIENT_HELLO packet
56+
* @param {string} playerId - Unique player identifier
57+
* @param {string} playerName - Display name
58+
* @returns {Object} Hello packet
59+
*/
60+
createClientHello(playerId, playerName) {
61+
return this.createPacket(MessageType.CLIENT_HELLO, {
62+
playerId,
63+
playerName,
64+
protocolVersion: 1,
65+
timestamp: Date.now()
66+
});
67+
}
68+
69+
/**
70+
* Create CLIENT_INPUT packet
71+
* @param {number} tick - Client tick number
72+
* @param {Object} input - Input data
73+
* @param {number} timestamp - Local timestamp
74+
* @returns {Object} Input packet
75+
*/
76+
createInput(tick, input, timestamp) {
77+
return this.createPacket(MessageType.CLIENT_INPUT, {
78+
tick,
79+
input,
80+
timestamp
81+
}, true); // Requires acknowledgment
82+
}
83+
84+
/**
85+
* Create SERVER_SNAPSHOT packet
86+
* @param {number} tick - Server tick
87+
* @param {Object} state - Full or delta state
88+
* @param {boolean} isDelta - Whether this is a delta update
89+
* @param {number} lastAckedInput - Last input sequence received from client
90+
* @returns {Object} Snapshot packet
91+
*/
92+
createSnapshot(tick, state, isDelta = false, lastAckedInput = 0) {
93+
return this.createPacket(MessageType.SERVER_SNAPSHOT, {
94+
tick,
95+
state,
96+
isDelta,
97+
lastAckedInput,
98+
timestamp: Date.now()
99+
});
100+
}
101+
102+
/**
103+
* Create TIME_SYNC_REQ packet
104+
* @returns {Object} Sync request packet
105+
*/
106+
createTimeSyncRequest() {
107+
return this.createPacket(MessageType.TIME_SYNC_REQ, {
108+
clientSendTime: Date.now()
109+
});
110+
}
111+
112+
/**
113+
* Create TIME_SYNC_RES packet
114+
* @param {number} clientSendTime - Original client timestamp
115+
* @param {number} serverTick - Current server tick
116+
* @returns {Object} Sync response packet
117+
*/
118+
createTimeSyncResponse(clientSendTime, serverTick) {
119+
const now = Date.now();
120+
return this.createPacket(MessageType.TIME_SYNC_RES, {
121+
clientSendTime,
122+
serverReceiveTime: now,
123+
serverSendTime: now,
124+
serverTick
125+
});
126+
}
127+
128+
/**
129+
* Create ACK packet
130+
* @param {number} sequenceId - Sequence to acknowledge
131+
* @returns {Object} ACK packet
132+
*/
133+
createAck(sequenceId) {
134+
return {
135+
type: MessageType.CLIENT_ACK,
136+
sequenceId,
137+
timestamp: Date.now()
138+
};
139+
}
140+
141+
/**
142+
* Create base packet structure
143+
* @param {number} type - Message type
144+
* @param {Object} payload - Packet data
145+
* @param {boolean} requiresAck - Whether packet needs acknowledgment
146+
* @returns {Object} Packet
147+
*/
148+
createPacket(type, payload, requiresAck = false) {
149+
const packet = {
150+
type,
151+
sequenceId: this.nextSequenceId++,
152+
payload,
153+
requiresAck,
154+
timestamp: Date.now()
155+
};
156+
157+
// Track for potential retransmission
158+
if (requiresAck) {
159+
this.pendingAcks.set(packet.sequenceId, {
160+
packet,
161+
timestamp: packet.timestamp,
162+
retries: 0
163+
});
164+
}
165+
166+
return packet;
167+
}
168+
169+
/**
170+
* Process received packet
171+
* @param {Object} packet - Received packet
172+
* @returns {Object|null} Processed payload or null if duplicate
173+
*/
174+
processPacket(packet) {
175+
// Check for duplicate
176+
if (this.receivedSequences.has(packet.sequenceId)) {
177+
console.warn(`Duplicate packet ${packet.sequenceId} ignored`);
178+
return null;
179+
}
180+
181+
this.receivedSequences.add(packet.sequenceId);
182+
183+
// Clean old sequences (keep last 1000)
184+
if (this.receivedSequences.size > 1000) {
185+
const sorted = Array.from(this.receivedSequences).sort((a, b) => a - b);
186+
for (let i = 0; i < 500; i++) {
187+
this.receivedSequences.delete(sorted[i]);
188+
}
189+
}
190+
191+
// Send ACK if required
192+
if (packet.requiresAck) {
193+
return {
194+
payload: packet.payload,
195+
ackPacket: this.createAck(packet.sequenceId)
196+
};
197+
}
198+
199+
return { payload: packet.payload };
200+
}
201+
202+
/**
203+
* Process ACK packet
204+
* @param {number} sequenceId - Acknowledged sequence
205+
*/
206+
processAck(sequenceId) {
207+
if (this.pendingAcks.has(sequenceId)) {
208+
this.pendingAcks.delete(sequenceId);
209+
console.log(`✓ Packet ${sequenceId} acknowledged`);
210+
}
211+
}
212+
213+
/**
214+
* Check for packets that need retransmission
215+
* @returns {Array} Packets to retransmit
216+
*/
217+
getRetransmissions() {
218+
const now = Date.now();
219+
const toRetransmit = [];
220+
221+
for (const [seqId, pending] of this.pendingAcks.entries()) {
222+
const elapsed = now - pending.timestamp;
223+
224+
if (elapsed > this.ackTimeout) {
225+
if (pending.retries < this.maxRetries) {
226+
pending.retries++;
227+
pending.timestamp = now;
228+
toRetransmit.push(pending.packet);
229+
console.warn(`⚠ Retransmitting packet ${seqId} (attempt ${pending.retries})`);
230+
} else {
231+
// Give up
232+
this.pendingAcks.delete(seqId);
233+
this.packetsLost++;
234+
console.error(`✗ Packet ${seqId} lost after ${this.maxRetries} retries`);
235+
}
236+
}
237+
}
238+
239+
return toRetransmit;
240+
}
241+
242+
/**
243+
* Serialize packet to binary (simplified - uses JSON for demo)
244+
* In production, use proper binary serialization (e.g., MessagePack, Protocol Buffers)
245+
* @param {Object} packet - Packet to serialize
246+
* @returns {Buffer|Uint8Array} Serialized data
247+
*/
248+
serialize(packet) {
249+
const json = JSON.stringify(packet);
250+
this.bytesSent += json.length;
251+
return Buffer.from(json, 'utf8');
252+
}
253+
254+
/**
255+
* Deserialize binary packet
256+
* @param {Buffer|Uint8Array} data - Serialized data
257+
* @returns {Object} Deserialized packet
258+
*/
259+
deserialize(data) {
260+
this.bytesReceived += data.length;
261+
const json = data.toString('utf8');
262+
return JSON.parse(json);
263+
}
264+
265+
/**
266+
* Get protocol statistics
267+
* @returns {Object} Stats
268+
*/
269+
getStats() {
270+
return {
271+
bytesSent: this.bytesSent,
272+
bytesReceived: this.bytesReceived,
273+
packetsLost: this.packetsLost,
274+
pendingAcks: this.pendingAcks.size,
275+
packetLossRate: this.packetsLost / this.nextSequenceId
276+
};
277+
}
278+
}
279+
280+
// ============================================
281+
// USAGE EXAMPLE
282+
// ============================================
283+
284+
function demonstrateProtocol() {
285+
console.log('=== Network Protocol Demo ===\n');
286+
287+
const clientProto = new NetworkProtocol();
288+
const serverProto = new NetworkProtocol();
289+
290+
// Simulate network with packet loss
291+
function sendPacket(from, to, packet, lossRate = 0.1) {
292+
const serialized = from.serialize(packet);
293+
294+
// Simulate packet loss
295+
if (Math.random() < lossRate) {
296+
console.log(`✗ Packet ${packet.sequenceId} lost in transit`);
297+
return null;
298+
}
299+
300+
// Simulate network delay
301+
setTimeout(() => {
302+
const deserialized = to.deserialize(serialized);
303+
const result = to.processPacket(deserialized);
304+
305+
if (result && result.ackPacket) {
306+
// Send ACK back
307+
setTimeout(() => {
308+
from.processAck(result.ackPacket.sequenceId);
309+
}, 10);
310+
}
311+
312+
return result;
313+
}, 20 + Math.random() * 30);
314+
}
315+
316+
// Client connects
317+
console.log('\n--- Client Connection ---');
318+
const helloPacket = clientProto.createClientHello('player_123', 'TestPlayer');
319+
console.log('Client → Server: HELLO');
320+
sendPacket(clientProto, serverProto, helloPacket);
321+
322+
// Client sends inputs
323+
console.log('\n--- Sending Inputs ---');
324+
for (let tick = 0; tick < 3; tick++) {
325+
const inputPacket = clientProto.createInput(
326+
tick,
327+
{ moveX: 1, moveY: 0, jump: false },
328+
Date.now()
329+
);
330+
console.log(`Client → Server: INPUT (tick ${tick})`);
331+
sendPacket(clientProto, serverProto, inputPacket, 0.2); // 20% loss
332+
}
333+
334+
// Check for retransmissions
335+
setTimeout(() => {
336+
console.log('\n--- Checking for Retransmissions ---');
337+
const retransmit = clientProto.getRetransmissions();
338+
if (retransmit.length > 0) {
339+
console.log(`Retransmitting ${retransmit.length} packets`);
340+
retransmit.forEach(packet => {
341+
sendPacket(clientProto, serverProto, packet, 0); // No loss on retry
342+
});
343+
}
344+
}, 200);
345+
346+
// Server sends snapshot
347+
setTimeout(() => {
348+
console.log('\n--- Server Snapshot ---');
349+
const snapshot = serverProto.createSnapshot(
350+
10,
351+
{ entities: [{ id: 1, x: 100, y: 50 }] },
352+
false,
353+
2
354+
);
355+
console.log('Server → Client: SNAPSHOT');
356+
sendPacket(serverProto, clientProto, snapshot);
357+
}, 100);
358+
359+
// Show stats
360+
setTimeout(() => {
361+
console.log('\n=== Protocol Statistics ===');
362+
console.log('Client:', clientProto.getStats());
363+
console.log('Server:', serverProto.getStats());
364+
}, 500);
365+
}
366+
367+
// Run demo if executed directly
368+
if (require.main === module) {
369+
demonstrateProtocol();
370+
}
371+
372+
module.exports = { NetworkProtocol, MessageType };

0 commit comments

Comments
 (0)