D1 phase 5+6: UDP unreliable channel (restores the 1995 reliable/unreliable split)
The ~60Hz entity update records now ride UDP in relay mode, so a lost/late
datagram is dropped (dead reckoning absorbs the gap) instead of head-of-line-
blocking the reliable TCP stream -- the fix for internet rubber-banding.
Reliable traffic (make/damage/death/control/egg/launch) stays on TCP.
This RESTORES the authentic 1995 NETNUB split, it doesn't invent one:
Receiver::Message defaults messageFlags=ReliableFlag; the update path clears
it (flags=0, ENTITY.cpp:590); Mode(UnreliableMode) fires at LoadingMission
(APP.cpp:704 -- the 2007 port's Mode() was a no-op, now it STORES the mode).
Routing gate = (mode==UnreliableMode && !(flags & ReliableFlag)) -- flag AND
mode, so the map-stream creation messages (also flags=0 but flowing during the
still-Reliable CreatingMission window) ride TCP for free.
Client (L4NET):
- Mode() stores currentNetworkMode (was ignored).
- ConnectRelayUdp: UDP socket connect()ed to the relay game port; HELLO
(outbound punch-through, relay learns our endpoint); udpUp on HELLO-ACK.
BT_RELAY_TCP_ONLY=1 disables the channel (UDP-blocked nets degrade to the
pure-TCP checkpoint automatically -- no ACK => everything stays on TCP).
- RelayUdpSendFrame: {route, fromHost, seq} envelope + frame, best-effort.
- RelayUdpKeepalive: ~15s NAT-hold + ~1s HELLO retry until acked.
- CheckRelayUdp: HELLO-ACK sets udpUp; per-sender seq gate drops stale/
out-of-order datagrams; frames handed up envelope-stripped like TCP.
- Send / ExclusiveBroadcast: route unreliable-window traffic to UDP, else TCP.
- CheckBuffers polls the UDP channel first, then TCP; keepalive tick.
(The relay-side UDP forwarder + endpoint learning + --udp-drop hook landed in
phase 1.)
Verified 2-node localhost relay: during a drive the update stream flows on UDP
(relay udp tx 2->190+, TCP frozen at 53) and the peer's replicant tracks the
master to sub-2u; 15% forced drop (--udp-drop 15) stays coherent (tracked to
0.1u); TCP-only fallback confirmed (udp-known drops the TCP-only pod); no
crashes; mesh mode un-regressed. KB: context/multiplayer.md gains the D1
RELAY MODE section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c029df6e6b
commit
dbb9af2dfa
+228
-5
@@ -226,7 +226,9 @@ L4NetworkManager::L4NetworkManager():
|
||||
relayPadTail = 0;
|
||||
relayConsoleDialedOnce = False;
|
||||
udpUp = False;
|
||||
relayLocalHostID = 0;
|
||||
lastUdpKeepaliveTick = 0;
|
||||
lastUdpHelloTick = 0;
|
||||
udpTxSeq = 0;
|
||||
memset(udpRxSeq, 0, sizeof(udpRxSeq));
|
||||
currentNetworkMode = NetworkManager::ReliableMode;
|
||||
@@ -1398,8 +1400,202 @@ void L4NetworkManager::ConnectRelayGame(HostID local_host_ID)
|
||||
{
|
||||
Fail("relay HELLO transmit failed\n");
|
||||
}
|
||||
relayLocalHostID = local_host_ID;
|
||||
DEBUG_STREAM << "[relay] game connection up, HELLO sent (hostID "
|
||||
<< (int)local_host_ID << ")\n" << std::flush;
|
||||
|
||||
// Bring up the UDP unreliable channel (a fully UDP-blocked network just
|
||||
// leaves udpUp False and everything rides TCP -- the fallback is automatic).
|
||||
ConnectRelayUdp(local_host_ID);
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ConnectRelayUdp -- create the UDP socket, connect() it to the relay game
|
||||
// port (so send/recv need no address), and send the first HELLO (the relay
|
||||
// learns our public endpoint from it -- outbound NAT punch-through). udpUp
|
||||
// flips True on the relay's HELLO-ACK (CheckRelayUdp). BT_RELAY_TCP_ONLY=1
|
||||
// disables the whole channel.
|
||||
//
|
||||
void L4NetworkManager::ConnectRelayUdp(HostID local_host_ID)
|
||||
{
|
||||
const char *tcp_only = getenv("BT_RELAY_TCP_ONLY");
|
||||
if (tcp_only != NULL && tcp_only[0] == '1')
|
||||
{
|
||||
DEBUG_STREAM << "[relay] BT_RELAY_TCP_ONLY: UDP channel disabled\n"
|
||||
<< std::flush;
|
||||
return;
|
||||
}
|
||||
relayUdpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (relayUdpSocket == INVALID_SOCKET)
|
||||
{
|
||||
DEBUG_STREAM << "[relay] UDP socket() failed (WSA "
|
||||
<< WSAGetLastError() << ") -- staying on TCP\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
if (connect(relayUdpSocket, (const sockaddr *)&relayGameAddress,
|
||||
sizeof(relayGameAddress)) != 0)
|
||||
{
|
||||
DEBUG_STREAM << "[relay] UDP connect() failed (WSA "
|
||||
<< WSAGetLastError() << ") -- staying on TCP\n" << std::flush;
|
||||
closesocket(relayUdpSocket);
|
||||
relayUdpSocket = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
unsigned long enable = 1;
|
||||
ioctlsocket(relayUdpSocket, FIONBIO, &enable);
|
||||
// First HELLO (retried by RelayUdpKeepalive until acked).
|
||||
RelayUdpEnvelope hello;
|
||||
hello.route = RELAY_ROUTE_HELLO;
|
||||
hello.fromHost = (int)local_host_ID;
|
||||
hello.sequence = 0;
|
||||
send(relayUdpSocket, (const char *)&hello, sizeof(hello), 0);
|
||||
lastUdpHelloTick = GetTickCount();
|
||||
DEBUG_STREAM << "[relay] UDP HELLO sent (hostID " << (int)local_host_ID
|
||||
<< "); awaiting ACK\n" << std::flush;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// RelayUdpSendFrame -- one datagram = one enveloped frame on the unreliable
|
||||
// channel. Best-effort: a failed sendto is simply dropped (the point of UDP).
|
||||
//
|
||||
Logical L4NetworkManager::RelayUdpSendFrame(
|
||||
int route,
|
||||
const NetworkPacket *packet,
|
||||
int packet_size)
|
||||
{
|
||||
if (relayUdpSocket == INVALID_SOCKET || !udpUp)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
char wire[sizeof(RelayUdpEnvelope) + NETWORKMANAGER_BUFFER_SIZE];
|
||||
if (packet_size > NETWORKMANAGER_BUFFER_SIZE)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
RelayUdpEnvelope *envelope = (RelayUdpEnvelope *)wire;
|
||||
envelope->route = route;
|
||||
envelope->fromHost = (int)relayLocalHostID;
|
||||
envelope->sequence = ++udpTxSeq;
|
||||
memcpy(wire + sizeof(RelayUdpEnvelope), packet, packet_size);
|
||||
send(relayUdpSocket, wire, (int)sizeof(RelayUdpEnvelope) + packet_size, 0);
|
||||
return True;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// RelayUdpKeepalive -- hold the NAT binding (~15s) and retry HELLO (~1s) until
|
||||
// acked. Called from CheckBuffers. During play the 60Hz stream IS the
|
||||
// keepalive; this covers idle/lobby time and the initial handshake.
|
||||
//
|
||||
void L4NetworkManager::RelayUdpKeepalive(HostID local_host_ID)
|
||||
{
|
||||
if (relayUdpSocket == INVALID_SOCKET)
|
||||
{
|
||||
return;
|
||||
}
|
||||
unsigned long now = GetTickCount();
|
||||
if (!udpUp)
|
||||
{
|
||||
if (now - lastUdpHelloTick >= 1000)
|
||||
{
|
||||
RelayUdpEnvelope hello;
|
||||
hello.route = RELAY_ROUTE_HELLO;
|
||||
hello.fromHost = (int)local_host_ID;
|
||||
hello.sequence = 0;
|
||||
send(relayUdpSocket, (const char *)&hello, sizeof(hello), 0);
|
||||
lastUdpHelloTick = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (now - lastUdpKeepaliveTick >= 15000)
|
||||
{
|
||||
RelayUdpEnvelope keepalive;
|
||||
keepalive.route = RELAY_ROUTE_HELLO; // HELLO doubles as keepalive
|
||||
keepalive.fromHost = (int)local_host_ID;
|
||||
keepalive.sequence = 0;
|
||||
send(relayUdpSocket, (const char *)&keepalive, sizeof(keepalive), 0);
|
||||
lastUdpKeepaliveTick = now;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// RelayShouldUseUdp -- the AUTHENTIC 1995 reliable/unreliable gate. Route to
|
||||
// UDP only in the unreliable window AND for a message the engine itself
|
||||
// flagged unreliable (messageFlags&ReliableFlag==0). This protects the
|
||||
// map-stream creation messages (which also lack ReliableFlag but flow during
|
||||
// the still-Reliable CreatingMission window) for free.
|
||||
//
|
||||
Logical L4NetworkManager::RelayShouldUseUdp(const Message *message) const
|
||||
{
|
||||
return (udpUp
|
||||
&& currentNetworkMode == NetworkManager::UnreliableMode
|
||||
&& (message->messageFlags & Message::ReliableFlag) == 0) ? True : False;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// CheckRelayUdp -- poll the UDP socket. HELLO-ACK sets udpUp; game datagrams
|
||||
// pass the per-sender sequence gate (drop stale/out-of-order -- dead reckoning
|
||||
// tolerates gaps), then hand the frame up envelope-stripped exactly like the
|
||||
// TCP path. Returns True with one game frame.
|
||||
//
|
||||
Logical L4NetworkManager::CheckRelayUdp(NetworkPacket *network_packet)
|
||||
{
|
||||
if (relayUdpSocket == INVALID_SOCKET)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
char datagram[sizeof(RelayUdpEnvelope) + NETWORKMANAGER_BUFFER_SIZE];
|
||||
int received = recv(relayUdpSocket, datagram, sizeof(datagram), 0);
|
||||
if (received == SOCKET_ERROR)
|
||||
{
|
||||
return False; // WSAEWOULDBLOCK etc: no more datagrams
|
||||
}
|
||||
if (received < (int)sizeof(RelayUdpEnvelope))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
RelayUdpEnvelope *envelope = (RelayUdpEnvelope *)datagram;
|
||||
if (envelope->route == RELAY_ROUTE_UDP_ACK)
|
||||
{
|
||||
if (!udpUp)
|
||||
{
|
||||
udpUp = True;
|
||||
lastUdpKeepaliveTick = GetTickCount();
|
||||
DEBUG_STREAM << "[relay] UDP channel UP (unreliable records now "
|
||||
"ride UDP)\n" << std::flush;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int frame_length = received - (int)sizeof(RelayUdpEnvelope);
|
||||
if (frame_length < (int)sizeof(NetworkPacketHeader))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Per-sender sequence gate: drop stale/duplicate/out-of-order.
|
||||
int from_host = envelope->fromHost;
|
||||
if (from_host >= 0 && from_host < 64)
|
||||
{
|
||||
if (envelope->sequence != 0
|
||||
&& envelope->sequence <= udpRxSeq[from_host])
|
||||
{
|
||||
continue; // stale -- newer state already applied
|
||||
}
|
||||
udpRxSeq[from_host] = envelope->sequence;
|
||||
}
|
||||
Mem_Copy(network_packet, datagram + sizeof(RelayUdpEnvelope),
|
||||
frame_length, NETWORKMANAGER_BUFFER_SIZE);
|
||||
if (getenv("BT_NET_TRACE"))
|
||||
{
|
||||
DEBUG_STREAM << "[net-rx] relay-udp client="
|
||||
<< (int)network_packet->clientID
|
||||
<< " msgID=" << (int)network_packet->messageData.messageID
|
||||
<< " len=" << (int)network_packet->messageData.messageLength
|
||||
<< " from host " << (int)network_packet->fromHost
|
||||
<< " seq=" << envelope->sequence << "\n" << std::flush;
|
||||
}
|
||||
return True;
|
||||
}
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1664,7 +1860,14 @@ void L4NetworkManager::Send(
|
||||
int relay_packet_size;
|
||||
NetworkPacket *relay_packet =
|
||||
BuildRelayPacket(message, client, &relay_packet_size);
|
||||
RelaySendFrame((int)host_ID, relay_packet, relay_packet_size);
|
||||
if (RelayShouldUseUdp(message))
|
||||
{
|
||||
RelayUdpSendFrame((int)host_ID, relay_packet, relay_packet_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
RelaySendFrame((int)host_ID, relay_packet, relay_packet_size);
|
||||
}
|
||||
free(relay_packet);
|
||||
CLEAR_SEND_PACKET();
|
||||
return;
|
||||
@@ -2092,7 +2295,18 @@ void
|
||||
int relay_packet_size;
|
||||
NetworkPacket *relay_packet =
|
||||
BuildRelayPacket(message, client, &relay_packet_size);
|
||||
RelaySendFrame(RELAY_ROUTE_BROADCAST, relay_packet, relay_packet_size);
|
||||
if (RelayShouldUseUdp(message))
|
||||
{
|
||||
// The 60Hz update-record fan-out -- the whole point of the UDP
|
||||
// channel (unreliable, latest-wins, no head-of-line blocking).
|
||||
RelayUdpSendFrame(RELAY_ROUTE_BROADCAST, relay_packet,
|
||||
relay_packet_size);
|
||||
}
|
||||
else
|
||||
{
|
||||
RelaySendFrame(RELAY_ROUTE_BROADCAST, relay_packet,
|
||||
relay_packet_size);
|
||||
}
|
||||
free(relay_packet);
|
||||
}
|
||||
return;
|
||||
@@ -2618,10 +2832,19 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
|
||||
// The classic per-host loop below still services the CONSOLE host; game
|
||||
// hosts are virtual (skipped via the relay gate in the OnLine recv block).
|
||||
//
|
||||
if (relayMode && CheckRelay(network_packet))
|
||||
if (relayMode)
|
||||
{
|
||||
CLEAR_CHECK_BUFFERS();
|
||||
return True;
|
||||
RelayUdpKeepalive(relayLocalHostID);
|
||||
if (CheckRelayUdp(network_packet)) // unreliable channel first
|
||||
{
|
||||
CLEAR_CHECK_BUFFERS();
|
||||
return True;
|
||||
}
|
||||
if (CheckRelay(network_packet)) // reliable + control
|
||||
{
|
||||
CLEAR_CHECK_BUFFERS();
|
||||
return True;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -350,6 +350,22 @@ private:
|
||||
RelayGameDown(const char *why);
|
||||
Logical
|
||||
CheckRelay(NetworkPacket *network_packet);
|
||||
//
|
||||
// UDP unreliable channel (restores the 1995 NETNUB reliable/unreliable
|
||||
// split): the ~60Hz update records ride UDP so a lost/late datagram is
|
||||
// dropped (dead reckoning absorbs the gap) instead of head-of-line-
|
||||
// blocking the reliable stream. Reliable traffic + control stay on TCP.
|
||||
//
|
||||
void
|
||||
ConnectRelayUdp(HostID local_host_ID);
|
||||
Logical
|
||||
RelayUdpSendFrame(int route, const NetworkPacket *packet, int packet_size);
|
||||
void
|
||||
RelayUdpKeepalive(HostID local_host_ID);
|
||||
Logical
|
||||
RelayShouldUseUdp(const Message *message) const;
|
||||
Logical
|
||||
CheckRelayUdp(NetworkPacket *network_packet);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Private Data
|
||||
@@ -390,7 +406,9 @@ private:
|
||||
int relayPadTail;
|
||||
Logical relayConsoleDialedOnce; // first dial gets the long timeout
|
||||
Logical udpUp; // UDP HELLO acked by the relay
|
||||
HostID relayLocalHostID; // our hostID (for UDP HELLO/keepalive/tx)
|
||||
unsigned long lastUdpKeepaliveTick;
|
||||
unsigned long lastUdpHelloTick; // HELLO retry until acked
|
||||
unsigned long udpTxSeq;
|
||||
unsigned long udpRxSeq[64]; // per-fromHost last-seen sequence
|
||||
NetworkMode currentNetworkMode; // Reliable/Unreliable (Mode() stores)
|
||||
|
||||
Reference in New Issue
Block a user