How DD Poker Handles Network Latency and Synchronization in Online Play
DD Poker implements a custom UDP transport layer with periodic heartbeat pings, adaptive retransmission timers, and session-based MTU discovery to maintain tight synchronization across distributed poker tables despite variable network conditions.
The open-source DD Poker application (repository dougdonohoe/ddpoker) replaces TCP with a lightweight UDP networking stack to minimize protocol overhead, then rebuilds reliability and network latency and synchronization guarantees directly in application code. This design provides millisecond-level control over packet recovery, congestion adaptation, and session continuity required for real-time card games.
The Three-Pillar Architecture for Low-Latency Sync
The networking subsystem centers on three complementary mechanisms implemented in code/udp/src/main/java/com/donohoedigital/udp/UDPLink.java:
-
Periodic Ping/Ack Heartbeat: Every approximately 333 ms,
UDPManagerschedules aSendAckTaskthat transmits either a normal ACK batch or a "ping-ack" packet (SENDALLversusSENDACK). The round-trip time of these acknowledgments is recorded inUDPLink.UDPStatsviarecordRoundTripTime, producing a moving-average latency estimate exposed throughstats.getAverage(). -
Adaptive Retransmission: When a queued
UDPDatafragment remains unacknowledged longer than a dynamic threshold—calculated asminAck = max(1000, 1.25 × averageRTT)milliseconds but never exceeding 7 seconds—the link automatically resends it. The system permits up to 25 retry attempts for normal game data and 3 attempts for MTU discovery probes, ensuring eventual delivery without flooding the connection. -
Session and MTU Discovery: Each connection maintains unique
localSessionID_andremoteSessionID_values. When a new session begins, all queues clear and the link executes aHELLO→ACKhandshake. Concurrently,mtuPathDiscovery()(lines 580‑617) probes the network path to find the largest unfragmented payload size, preventing hidden latency spikes caused by IP fragmentation.
Core Implementation: From Link Creation to Data Flow
The synchronization logic follows a strict lifecycle coordinated by UDPManager.java and executed within UDPLink.java.
Link Establishment and Hello Handshake
When a client joins a table, UDPManager.getLink() (lines 28‑45) either returns an existing UDPLink or instantiates a fresh instance and adds it to the internal links_ collection. The link immediately enters a handshake phase:
// Acquire a link to the remote host (IP + port)
UDPLink link = manager.getLink("203.0.113.42", 7777);
// Kick‑off the hello handshake; this also starts MTU discovery
link.connect(); // calls hello() → mtuPathDiscovery() → send()
The hello() method (lines 514‑523) transmits a HELLO message; upon receiving the corresponding ACK processed at lines 1490‑1498, the link fires a UDPLinkEvent.Type.ESTABLISHED event and begins accepting MESSAGE packets.
Continuous Latency Monitoring
Once established, the link maintains a bi-directional heartbeat via SendAckTask. Every third tick (~1 second) the task queues SENDALL to flush pending data; on other ticks it queues SENDACK. The sendAcksPing() method (lines 698‑706) first verifies the connection is alive via aliveCheck() before transmitting.
When the remote peer returns an ACK, processAcks invokes stats_.recordRoundTripTime(qData) (lines 1114‑1117). The UDPStats class maintains a moving average accessible via getAverage() (lines 727‑734), providing a live millisecond-accurate latency reading.
Intelligent Packet Recovery
The sendAll() method (lines 221‑260) iterates over the outbound queue and resends any UDPData whose age exceeds the adaptive minAck threshold. The resend loop (lines 369‑390) increments attempt counters and discards fragments that exceed the maximum retry limit (25 for data, 3 for MTU tests). This balances responsiveness against network congestion.
Timeout Handling and Session Reset
If aliveCheck() (lines 666‑689) detects no traffic—including pings—for longer than the configurable TIMEOUT_MILLIS, the link emits a TIMEOUT event, invokes resetSession() (lines 1068‑1074), and forces a new HELLO handshake. This rapidly recovers from prolonged latency spikes or temporary network partitions.
Session Management and MTU Discovery
Session identifiers prevent stale packets from corrupting game state after a reconnect. When resetSession() clears the localSessionID_, the next outbound HELLO establishes a fresh logical connection, and the remote peer updates its remoteSessionID_ accordingly.
Before normal gameplay resumes, mtuPathDiscovery() transmits a series of payloads with increasing sizes. Successful MTU_ACK responses (handled at lines 1025‑1035) raise the nMTU_ value until the path limit is found. By avoiding IP fragmentation, the system prevents "phantom" latency caused by fragmented packet reassembly or loss.
Measuring and Displaying Network Health
The UI layer consumes the same statistics used by the transport layer. In code/pokerwicket/src/main/java/com/donohoedigital/games/poker/wicket/pages/support/OnlineSupplement.html, the application displays latency and packet loss to the player.
Retrieve the current latency programmatically:
// Periodically (e.g., every second) read the moving‑average RTT
long latencyMs = link.getStats().getAverage(); // in milliseconds
System.out.println("Current network latency: " + latencyMs + " ms");
Event listeners monitor connection state changes to trigger visual warnings:
link.addMonitor(event -> {
switch (event.getType()) {
case TIMEOUT:
System.out.println("Link timed out – will re‑handshake");
// UI can show a “re‑connecting…” banner
break;
case SESSION_CHANGED:
System.out.println("Remote session changed – resetting state");
// Flush any pending game actions that depend on the old session
break;
default:
// handle other events (ESTABLISHED, RESEND_FAILURE, etc.)
}
});
Display the value in a Wicket component:
add(new Label("latency", new LoadableDetachableModel<String>() {
@Override
protected String load() {
UDPLink link = getSessionLink(); // application‑specific lookup
return link != null ? link.getStats().getAverage() + " ms" : "—";
}
}).setOutputMarkupId(true));
Summary
- Custom UDP Transport: DD Poker bypasses TCP overhead with a lightweight UDP layer implemented in
UDPManager.javaandUDPLink.java, rebuilding reliability in user space. - Adaptive Timing: Latency-aware retransmission uses a moving-average RTT multiplied by 1.25 (minimum 1 s, maximum 7 s) to determine when to resend lost packets.
- Session Isolation: Unique session IDs ensure that delayed packets from previous connections cannot corrupt the current game state after a reconnect.
- Path MTU Discovery: Proactive MTU testing prevents fragmentation-related latency spikes by locking the payload size to the network path's actual capacity.
- Observability: Real-time latency statistics flow from
UDPStatsdirectly to the Wicket-based UI, giving players visibility into connection health.
Frequently Asked Questions
How does DD Poker measure network latency during gameplay?
The transport layer records round-trip times for every acknowledgment packet. In UDPLink.processAcks(), the code invokes stats_.recordRoundTripTime() to update a moving-average statistic. The UI retrieves this value via link.getStats().getAverage() and displays it in the Online Support page, giving players a live latency reading updated approximately every second.
What happens when a packet is lost or delayed?
If a UDPData fragment remains unacknowledged beyond the adaptive threshold—calculated as max(1000ms, 1.25 × averageRTT) capped at 7 seconds—the sendAll() method automatically retransmits it. The system allows up to 25 retry attempts for game messages before logging a RESEND_FAILURE event, ensuring critical state changes eventually reach all players.
Why does DD Poker use UDP instead of TCP for online play?
TCP's congestion control and head-of-line blocking can introduce unpredictable latency spikes that disrupt real-time card game synchronization. By implementing a custom UDP-based protocol in code/udp/src/main/java/com/donohoedigital/udp/, DD Poker gains fine-grained control over retransmission timing, session resets, and MTU sizing, allowing it to maintain tight synchronization even on lossy or high-latency internet connections.
How does the game recover from a complete network disconnection?
When no packets arrive for the duration specified by TIMEOUT_MILLIS, the aliveCheck() method triggers a TIMEOUT event and calls resetSession(). This clears all pending queues, generates new session IDs, and initiates a fresh HELLO handshake. Consequently, the client can rapidly rejoin the game table without requiring a full application restart, preserving the player's seat and chip stack.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →