Cyd asked for an analysis of the networking stack and what would make the simulation feel better over the internet. The analysis found something more urgent than latency: the transport has been losing data silently since the arcade, and nothing in the game could see it happen. Every send result was discarded - L4NET, the console, all of it. On the 1ms arcade LAN the socket buffer never filled, so it never mattered. Over the internet it matters twice. A peer stalled in its own 10-30 second mission load stops reading, its window closes, and our nonblocking send starts answering would-block, which threw the message away; or worse, answering a PARTIAL count, and since framing on that stream is recovered purely from each message's length prefix, the bytes that never followed sheared it for good. Both are reachable in an ordinary race, because every race has a load in it. So sends go through a bounded per-connection queue now. What the wire will not take is kept, byte-exact, and retried at three flush points - before the render (the present blocks on vsync, and this frame's state should be travelling while it does), at the top of the receive pump, and before a connect sequence. Nothing is ever dropped from the middle: these are reliable ordered messages carrying entity creation, damage and race control, so a queue that overflows its 256K declares the connection dead and lets the disconnect path run rather than quietly desyncing the stream. RP412NETSENDQ=0 restores the old behaviour and still logs what it would have lost, which is the honest way to A/B it. On Steam the same queue finally surfaces k_EResultLimitExceeded, which the old code collapsed into -1 and discarded - that was backpressure, unlogged. The receive side gained the check the release build never had. The length prefix is untrusted input; Verify() compiles away in release, so a corrupt one went to memmove as a negative, or copied 4096 bytes of assembled packet into a 1600-byte stack buffer, or named a size the pad could never complete and wedged the connection forever. It is now validated against the same bounds the sender works to, and a stream that fails them is dropped like any other lost peer. And a fry that never ends: drop zones are map entities dealt round-robin at load, ownership transfer is not implemented, so a leaver's pads stay in the DropZones group. The respawn request dispatched to one goes to a host that is gone - dropped at the send, the 'no host N in the table' path - and the two-second retry re-dispatches to the same dead owner forever. The pad scan now skips zones whose owner has left, and re-validates one assigned earlier before reusing it. The rest is measurement, because the symptoms this work exists to chase are all reported in prose and none of them are in any log. Sixteen logs from the six-player night contain zero player-facing latency lines. A race now ends with a NetLog summary: per remote pod, how many updates arrived and how evenly (median and p95 out of a log2 histogram), the widest gap, how many gaps were long enough to mean a quiet sender versus short enough to mean OUR loop stalled, how often its motion snapped instead of blending, and how far arriving updates moved it. Per peer, whether the clock alignment ever had to step mid-race - which is the input for deciding if it needs slewing, rather than guessing. The mission t0 tick goes in the log too, alongside the console's per-pod RunMission send ticks, because nothing has ever measured how far apart the machines actually start; the clockwork doors inherit that skew directly. RP412NETSTATS adds the transport's own view - per connection: messages, bytes, wire writes, partials, refusals, how much sat queued - and on Steam the first read this codebase has ever taken of GetConnectionRealTime Status. Ping, quality, pending and unacked bytes, and one route description per connection at teardown. The API was vendored and never called; there was no RTT number anywhere in the game. Finally, rpl4opt -spoolstats reads any recording offline. The data was already in every spool ever made and nothing read it that way: the recorder restamps each packet with local arrival time while the update records inside keep the sender's sim-grid stamp, so the difference is clock offset plus one-way delay, and the same running-minimum estimator the game runs live separates them. It prints delay above the per-host minimum, and decomposes each entity's gaps into sender pacing versus delivery jitter - which no live counter can do. It lives in the game exe rather than RPL4TOOL because the tool is deliberately not /Zp1 and would misread every struct in the file. Verified on the two-pod loopback harness: mesh up, egg fed, 60s raced, stopped on command, scores collected, and both summaries reading exactly what a pair of PARKED pods should read - heartbeat cadence, one snap per heartbeat, sub-quarter-metre corrections, no clock steps. The t0 ticks and the netclock offsets agree with each other to the two seconds the pods launched apart. The latency tier is deliberately NOT here. TCP_NODELAY, the Steam NoNagle flag, per-frame coalescing and the pre-sim receive drain are all scoped and all wait on this build's numbers, because the point of shipping measurement first is to find out whether the thing we would fix is the thing that hurts. Nagle is still on. Interest management is still inert. The wire format is untouched, so this build and the last one still race each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
934 lines
23 KiB
C++
934 lines
23 KiB
C++
#include "mungal4.h"
|
|
#pragma hdrstop
|
|
|
|
#include "l4nettransport.h"
|
|
#include "..\munga\appmgr.h"
|
|
#include <Ws2tcpip.h>
|
|
|
|
//########################################################################
|
|
// Waiting for a peer without hanging the window
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
Logical gInWait = False; // re-entrancy guard
|
|
Logical gWaitCancelled = False;
|
|
char gOriginalTitle[256] = "";
|
|
Logical gTitleSaved = False;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// How long Connect keeps redialling.
|
|
//
|
|
// It was two minutes, which was right for what it was written for: an
|
|
// arcade console redialling a pod that is still booting and WILL answer,
|
|
// on a LAN with nothing else to go wrong. Over Steam the same two minutes
|
|
// is a host staring at a dead window, because a peer that has not answered
|
|
// in twenty seconds is not coming - it never launched, or it is behind
|
|
// something the relay cannot cross.
|
|
//
|
|
int
|
|
NetTransport_ConnectWaitSeconds()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412CONNECTWAIT");
|
|
|
|
cached = (setting != NULL) ? atoi(setting) : 20;
|
|
if (cached < 2) { cached = 2; }
|
|
if (cached > 300) { cached = 300; }
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
void
|
|
NetTransport_SetWaitProgress(const char *text)
|
|
{
|
|
if (ghWnd == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!gTitleSaved)
|
|
{
|
|
GetWindowTextA(ghWnd, gOriginalTitle, sizeof(gOriginalTitle) - 1);
|
|
gTitleSaved = True;
|
|
}
|
|
|
|
//
|
|
// The title bar because it is the one surface guaranteed to exist
|
|
// here: this runs before the engine block, so there is no renderer to
|
|
// draw a progress screen with yet.
|
|
//
|
|
SetWindowTextA(ghWnd, (text != NULL) ? text : gOriginalTitle);
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
NetWaitResult
|
|
NetTransport_PumpAndSleep(int milliseconds)
|
|
{
|
|
//
|
|
// Dispatching a message can run application code, and that code can
|
|
// reach a connect of its own. Nested pumping would then deliver the
|
|
// same messages twice and let an inner wait consume the escape meant
|
|
// for the outer one, so an inner call just sleeps.
|
|
//
|
|
if (gInWait)
|
|
{
|
|
Sleep(milliseconds);
|
|
return NetWaitContinue;
|
|
}
|
|
|
|
gInWait = True;
|
|
|
|
NetWaitResult result = NetWaitContinue;
|
|
MSG message;
|
|
|
|
while (PeekMessage(&message, NULL, 0, 0, PM_REMOVE))
|
|
{
|
|
if (message.message == WM_QUIT)
|
|
{
|
|
//
|
|
// Put it back: the message loop that owns the shutdown has
|
|
// to see this, not us.
|
|
//
|
|
PostQuitMessage((int) message.wParam);
|
|
result = NetWaitQuit;
|
|
break;
|
|
}
|
|
if (message.message == WM_KEYDOWN && message.wParam == VK_ESCAPE)
|
|
{
|
|
gWaitCancelled = True;
|
|
}
|
|
TranslateMessage(&message);
|
|
DispatchMessage(&message);
|
|
}
|
|
|
|
if (result == NetWaitContinue && gWaitCancelled)
|
|
{
|
|
result = NetWaitCancelled;
|
|
}
|
|
if (result == NetWaitContinue)
|
|
{
|
|
Sleep(milliseconds);
|
|
}
|
|
|
|
gInWait = False;
|
|
return result;
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
//
|
|
// Cleared when a connect sequence starts, so an escape pressed during one
|
|
// race does not cancel the next one.
|
|
//
|
|
void
|
|
NetTransport_ClearWaitCancel()
|
|
{
|
|
gWaitCancelled = False;
|
|
}
|
|
|
|
//########################################################################
|
|
// Outbound send queue - shared machinery for both transports.
|
|
// See the block comment in l4nettransport.h for why this exists.
|
|
//########################################################################
|
|
|
|
Logical
|
|
NetTransport_SendQueueEnabled()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412NETSENDQ");
|
|
|
|
cached = (setting != NULL && atoi(setting) == 0) ? 0 : 1;
|
|
}
|
|
return cached ? True : False;
|
|
}
|
|
|
|
int
|
|
NetTransport_SendQueueCapBytes()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412NETQUEUEKB");
|
|
int kilobytes = (setting != NULL) ? atoi(setting) : 256;
|
|
|
|
if (kilobytes < 16) { kilobytes = 16; }
|
|
if (kilobytes > 4096) { kilobytes = 4096; }
|
|
cached = kilobytes * 1024;
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
Logical
|
|
NetTransport_StatsEnabled()
|
|
{
|
|
static int cached = -1;
|
|
|
|
if (cached < 0)
|
|
{
|
|
const char *setting = getenv("RP412NETSTATS");
|
|
cached = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
|
}
|
|
return cached ? True : False;
|
|
}
|
|
|
|
NetSendQueueTable::NetSendQueueTable()
|
|
{
|
|
for (int i = 0; i < MaxQueues; ++i)
|
|
{
|
|
queues[i].connection = NetTransport::InvalidConnection;
|
|
queues[i].buffer = NULL;
|
|
queues[i].head = 0;
|
|
queues[i].tail = 0;
|
|
queues[i].dead = False;
|
|
queues[i].sentMessages = 0;
|
|
queues[i].sentBytes = 0;
|
|
queues[i].wireWrites = 0;
|
|
queues[i].partialWrites = 0;
|
|
queues[i].wouldBlocks = 0;
|
|
queues[i].queuedHighWater = 0;
|
|
}
|
|
}
|
|
|
|
NetSendQueueTable::~NetSendQueueTable()
|
|
{
|
|
ReleaseAll();
|
|
}
|
|
|
|
NetSendQueue *
|
|
NetSendQueueTable::Find(NetTransport::Connection connection)
|
|
{
|
|
for (int i = 0; i < MaxQueues; ++i)
|
|
{
|
|
if (queues[i].connection == connection)
|
|
{
|
|
return &queues[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
NetSendQueue *
|
|
NetSendQueueTable::FindOrAdopt(NetTransport::Connection connection)
|
|
{
|
|
NetSendQueue *queue = Find(connection);
|
|
|
|
if (queue != NULL)
|
|
{
|
|
return queue;
|
|
}
|
|
for (int i = 0; i < MaxQueues; ++i)
|
|
{
|
|
if (queues[i].connection == NetTransport::InvalidConnection)
|
|
{
|
|
queue = &queues[i];
|
|
queue->connection = connection;
|
|
queue->head = 0;
|
|
queue->tail = 0;
|
|
queue->dead = False;
|
|
queue->sentMessages = 0;
|
|
queue->sentBytes = 0;
|
|
queue->wireWrites = 0;
|
|
queue->partialWrites = 0;
|
|
queue->wouldBlocks = 0;
|
|
queue->queuedHighWater = 0;
|
|
return queue;
|
|
}
|
|
}
|
|
DEBUG_STREAM << "NetSendQueue: table full - connection "
|
|
<< (unsigned long) connection << " gets no queue\n" << std::flush;
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
NetSendQueueTable::Release(NetTransport::Connection connection)
|
|
{
|
|
NetSendQueue *queue = Find(connection);
|
|
|
|
if (queue == NULL)
|
|
{
|
|
return;
|
|
}
|
|
if (queue->buffer != NULL)
|
|
{
|
|
free(queue->buffer);
|
|
queue->buffer = NULL;
|
|
}
|
|
queue->connection = NetTransport::InvalidConnection;
|
|
queue->head = 0;
|
|
queue->tail = 0;
|
|
queue->dead = False;
|
|
}
|
|
|
|
void
|
|
NetSendQueueTable::ReleaseAll()
|
|
{
|
|
for (int i = 0; i < MaxQueues; ++i)
|
|
{
|
|
if (queues[i].connection != NetTransport::InvalidConnection)
|
|
{
|
|
Release(queues[i].connection);
|
|
}
|
|
}
|
|
}
|
|
|
|
Logical
|
|
NetSendQueueTable::Append(
|
|
NetSendQueue *queue,
|
|
const void *data,
|
|
int size)
|
|
{
|
|
int capacity = NetTransport_SendQueueCapBytes();
|
|
int pending = queue->PendingBytes();
|
|
|
|
if (pending + size > capacity)
|
|
{
|
|
//
|
|
// Overflow. These are reliable ordered messages - dropping any of
|
|
// them desyncs the stream - so the only honest answer is to give
|
|
// up on the connection and let the disconnect path run.
|
|
//
|
|
DEBUG_STREAM << "NetSendQueue: connection "
|
|
<< (unsigned long) queue->connection << " overflowed ("
|
|
<< pending << " pending + " << size << " > " << capacity
|
|
<< ") - declaring it dead\n" << std::flush;
|
|
if (queue->buffer != NULL)
|
|
{
|
|
free(queue->buffer);
|
|
queue->buffer = NULL;
|
|
}
|
|
queue->head = 0;
|
|
queue->tail = 0;
|
|
queue->dead = True;
|
|
return False;
|
|
}
|
|
if (queue->buffer == NULL)
|
|
{
|
|
queue->buffer = (unsigned char *) malloc(capacity);
|
|
if (queue->buffer == NULL)
|
|
{
|
|
queue->dead = True;
|
|
return False;
|
|
}
|
|
}
|
|
//
|
|
// Rejustify before appending when the tail would run off the end -
|
|
// the bytes ahead of head are already on the wire.
|
|
//
|
|
if (queue->tail + size > capacity && queue->head > 0)
|
|
{
|
|
memmove(queue->buffer, &queue->buffer[queue->head], pending);
|
|
queue->head = 0;
|
|
queue->tail = pending;
|
|
}
|
|
memcpy(&queue->buffer[queue->tail], data, size);
|
|
queue->tail += size;
|
|
if ((unsigned long) queue->PendingBytes() > queue->queuedHighWater)
|
|
{
|
|
queue->queuedHighWater = (unsigned long) queue->PendingBytes();
|
|
}
|
|
return True;
|
|
}
|
|
|
|
//########################################################################
|
|
// WinsockNetTransport - the TCP wire the arcade always used, moved
|
|
// verbatim out of L4NET.CPP behind the NetTransport seam. Behavior
|
|
// notes preserved from the original:
|
|
// - Connect retries while the remote refuses (WSAECONNREFUSED): the
|
|
// console feeds eggs in [pilots] order with ACKs, so earlier pods
|
|
// are listening before later pods open - the retry covers the race.
|
|
// - Sockets go nonblocking once connected; listeners are nonblocking
|
|
// from creation.
|
|
//########################################################################
|
|
|
|
namespace
|
|
{
|
|
class WinsockNetTransport:
|
|
public NetTransport
|
|
{
|
|
public:
|
|
WinsockNetTransport():
|
|
started(False),
|
|
statsNextTick(0)
|
|
{
|
|
}
|
|
|
|
Logical
|
|
Startup()
|
|
{
|
|
if (started)
|
|
{
|
|
return True;
|
|
}
|
|
int result = WSAStartup(MAKEWORD(2, 2), &winsockData);
|
|
if (result != NO_ERROR)
|
|
{
|
|
DEBUG_STREAM << "ERROR: WSAStartup() failed with " << result
|
|
<< "!\n" << std::flush;
|
|
return False;
|
|
}
|
|
started = True;
|
|
return True;
|
|
}
|
|
|
|
void
|
|
Cleanup()
|
|
{
|
|
if (started)
|
|
{
|
|
FlushSends();
|
|
sendQueues.ReleaseAll();
|
|
WSACleanup();
|
|
started = False;
|
|
}
|
|
}
|
|
|
|
int
|
|
GetLocalAddresses(
|
|
unsigned long *addresses,
|
|
int max_count
|
|
)
|
|
{
|
|
char name[255];
|
|
PHOSTENT hostinfo;
|
|
|
|
if (gethostname(name, sizeof(name)) != 0)
|
|
{
|
|
DEBUG_STREAM << "ERROR: gethostname() failed!" << std::endl << std::flush;
|
|
return 0;
|
|
}
|
|
if ((hostinfo = gethostbyname(name)) == NULL)
|
|
{
|
|
DEBUG_STREAM << "ERROR: gethostbyname() failed!" << std::endl << std::flush;
|
|
return 0;
|
|
}
|
|
|
|
int count = 0;
|
|
for (int i = 0; hostinfo->h_addr_list[i] != NULL && count < max_count; ++i)
|
|
{
|
|
addresses[count++] = *((unsigned long *) hostinfo->h_addr_list[i]);
|
|
}
|
|
|
|
// loopback rounds out the list (single-machine testing)
|
|
if (count < max_count)
|
|
{
|
|
addresses[count++] = 0x0100007F;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
Logical
|
|
Resolve(
|
|
const char *host_name,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
// numeric ip[:port] only - the egg carries addresses, not
|
|
// names; port stays 0 when absent (caller applies default)
|
|
int buffer_size = sizeof(SOCKADDR_IN);
|
|
return WSAStringToAddressA(
|
|
(LPSTR) host_name, AF_INET, NULL,
|
|
(LPSOCKADDR) address, &buffer_size) == 0;
|
|
}
|
|
|
|
Connection
|
|
Connect(
|
|
const SOCKADDR_IN *remote,
|
|
int local_port
|
|
)
|
|
{
|
|
DEBUG_STREAM << "Opening connection to "
|
|
<< inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port)
|
|
<< "...\n" << std::flush;
|
|
|
|
//
|
|
// Retry-while-refused, bounded: the egg-ACK ordering means
|
|
// the peer may not be listening yet. A refused TCP socket
|
|
// is dead - every attempt needs a fresh one.
|
|
//
|
|
//
|
|
// NOTE the connect() below is still BLOCKING - the socket only
|
|
// goes nonblocking once it has succeeded - so an unreachable
|
|
// host (filtered rather than refused) sits in the OS SYN retry
|
|
// for around twenty seconds with nothing we can do about it.
|
|
// Pumping between redials fixes the refused case, which is the
|
|
// common one; the unreachable case needs the socket made
|
|
// nonblocking before connect() and a select() on our own
|
|
// timeout. Not done here because this is the LAN/direct path -
|
|
// a Steam host goes through SteamNetTransport::Connect.
|
|
//
|
|
DWORD deadline =
|
|
GetTickCount() + (DWORD) NetTransport_ConnectWaitSeconds() * 1000;
|
|
char progress[256];
|
|
|
|
sprintf(
|
|
progress,
|
|
"Red Planet - connecting to %s ...",
|
|
inet_ntoa(remote->sin_addr)
|
|
);
|
|
NetTransport_SetWaitProgress(progress);
|
|
|
|
for (;;)
|
|
{
|
|
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (sock == INVALID_SOCKET)
|
|
{
|
|
DEBUG_STREAM << "ERROR: socket() failed with "
|
|
<< WSAGetLastError() << "!\n";
|
|
return InvalidConnection;
|
|
}
|
|
|
|
bool reuse_address = true;
|
|
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
|
|
(char *) &reuse_address, sizeof(bool)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
// bind the game port: the mesh identifies peers by
|
|
// address AND port, so the source port must be ours
|
|
// (port 0 = ephemeral, for console channels)
|
|
sockaddr_in local_endpoint;
|
|
memset(&local_endpoint, 0, sizeof(local_endpoint));
|
|
local_endpoint.sin_family = AF_INET;
|
|
local_endpoint.sin_port = htons((unsigned short) local_port);
|
|
local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
|
|
if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
if (connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)) == 0)
|
|
{
|
|
unsigned long enable = 1;
|
|
if (ioctlsocket(sock, FIONBIO, &enable))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(sock);
|
|
return InvalidConnection;
|
|
}
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return (Connection) sock;
|
|
}
|
|
|
|
int wsa_error = WSAGetLastError();
|
|
closesocket(sock);
|
|
if (wsa_error != WSAECONNREFUSED ||
|
|
(LONG)(GetTickCount() - deadline) >= 0)
|
|
{
|
|
DEBUG_STREAM << "ERROR: connect() failed with "
|
|
<< wsa_error << "!\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
//
|
|
// Peer not up yet - redial, answering the window meanwhile.
|
|
//
|
|
DWORD left = (DWORD)((LONG)(deadline - GetTickCount()) / 1000);
|
|
|
|
sprintf(
|
|
progress,
|
|
"Red Planet - connecting to %s ... %us left, ESC to cancel",
|
|
inet_ntoa(remote->sin_addr),
|
|
(unsigned) left
|
|
);
|
|
NetTransport_SetWaitProgress(progress);
|
|
|
|
for (int slept = 0; slept < 250; slept += 50)
|
|
{
|
|
NetWaitResult wait = NetTransport_PumpAndSleep(50);
|
|
if (wait != NetWaitContinue)
|
|
{
|
|
DEBUG_STREAM << "Connect "
|
|
<< ((wait == NetWaitCancelled) ? "cancelled" : "abandoned, quitting")
|
|
<< " by the user\n" << std::flush;
|
|
NetTransport_SetWaitProgress(NULL);
|
|
return InvalidConnection;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connection
|
|
Listen(
|
|
int local_port,
|
|
int backlog
|
|
)
|
|
{
|
|
DEBUG_STREAM << "Starting to listen on port " << local_port
|
|
<< "...\n" << std::flush;
|
|
|
|
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (listener == INVALID_SOCKET)
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not create listener socket; socket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
return InvalidConnection;
|
|
}
|
|
|
|
bool reuse_address = true;
|
|
if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR,
|
|
(char *) &reuse_address, sizeof(bool)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on listener socket; setsockopt() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
sockaddr_in local_endpoint;
|
|
memset(&local_endpoint, 0, sizeof(local_endpoint));
|
|
local_endpoint.sin_family = AF_INET;
|
|
local_endpoint.sin_port = htons((unsigned short) local_port);
|
|
local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY;
|
|
if (bind(listener, (sockaddr *) &local_endpoint, sizeof(local_endpoint)))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not bind listener socket; bind() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
if (listen(listener, backlog))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not listen on listener socket; listen() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
|
|
unsigned long enable = 1;
|
|
if (ioctlsocket(listener, FIONBIO, &enable))
|
|
{
|
|
DEBUG_STREAM << "ERROR: Could not set listener socket to nonblocking; ioctlsocket() failed with "
|
|
<< WSAGetLastError() << "!\n" << std::flush;
|
|
closesocket(listener);
|
|
return InvalidConnection;
|
|
}
|
|
return (Connection) listener;
|
|
}
|
|
|
|
Connection
|
|
Accept(Connection listener)
|
|
{
|
|
SOCKET accepted = accept((SOCKET) listener, NULL, 0);
|
|
if (accepted == INVALID_SOCKET)
|
|
{
|
|
return InvalidConnection;
|
|
}
|
|
return (Connection) accepted;
|
|
}
|
|
|
|
void
|
|
Close(Connection connection)
|
|
{
|
|
//
|
|
// Best-effort last drain, so an orderly close does not strand
|
|
// bytes a slow connection still owes the wire.
|
|
//
|
|
NetSendQueue *queue = sendQueues.Find(connection);
|
|
if (queue != NULL)
|
|
{
|
|
if (!queue->dead && queue->PendingBytes() > 0)
|
|
{
|
|
DrainQueue(queue);
|
|
}
|
|
sendQueues.Release(connection);
|
|
}
|
|
shutdown((SOCKET) connection, SD_BOTH);
|
|
closesocket((SOCKET) connection);
|
|
}
|
|
|
|
int
|
|
Send(
|
|
Connection connection,
|
|
const void *data,
|
|
int size
|
|
)
|
|
{
|
|
NetSendQueue *queue = sendQueues.Find(connection);
|
|
|
|
if (queue != NULL && queue->dead)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (!NetTransport_SendQueueEnabled())
|
|
{
|
|
//
|
|
// Legacy direct send (RP412NETSENDQ=0) - but count and
|
|
// name the losses the queue would have prevented.
|
|
//
|
|
int sent = send((SOCKET) connection, (const char *) data, size, 0);
|
|
if (sent != size)
|
|
{
|
|
static int said = 0;
|
|
if (said < 8)
|
|
{
|
|
++said;
|
|
DEBUG_STREAM << "WinsockNetTransport::Send: legacy mode "
|
|
<< ((sent < 0) ? "lost a message of " : "sheared the stream at ")
|
|
<< size << " bytes (send returned " << sent
|
|
<< ", error " << WSAGetLastError() << ")\n" << std::flush;
|
|
}
|
|
}
|
|
return sent;
|
|
}
|
|
|
|
//
|
|
// Every live connection gets a table slot (the buffer itself
|
|
// stays unallocated until something actually queues), so the
|
|
// RP412NETSTATS counters exist for healthy connections too.
|
|
//
|
|
if (queue == NULL)
|
|
{
|
|
queue = sendQueues.FindOrAdopt(connection);
|
|
if (queue == NULL)
|
|
{
|
|
// table full: behave exactly like the legacy send
|
|
return send((SOCKET) connection, (const char *) data, size, 0);
|
|
}
|
|
}
|
|
queue->sentMessages++;
|
|
queue->sentBytes += size;
|
|
|
|
//
|
|
// FIFO discipline: bytes already queued must go first, so a
|
|
// queued connection only ever appends.
|
|
//
|
|
if (queue->PendingBytes() > 0)
|
|
{
|
|
if (!sendQueues.Append(queue, data, size))
|
|
{
|
|
return -1;
|
|
}
|
|
DrainQueue(queue);
|
|
return size;
|
|
}
|
|
|
|
int sent = send((SOCKET) connection, (const char *) data, size, 0);
|
|
if (sent == size)
|
|
{
|
|
queue->wireWrites++;
|
|
return size;
|
|
}
|
|
|
|
//
|
|
// The wire would not take all of it. Keep the byte-exact
|
|
// remainder - a partial send left unhealed is a sheared
|
|
// stream, the failure this queue exists to close.
|
|
//
|
|
if (sent < 0)
|
|
{
|
|
DWORD error = WSAGetLastError();
|
|
if (error != WSAEWOULDBLOCK)
|
|
{
|
|
DEBUG_STREAM << "WinsockNetTransport::Send: hard error "
|
|
<< error << " - declaring the connection dead\n" << std::flush;
|
|
queue->dead = True;
|
|
return -1;
|
|
}
|
|
queue->wouldBlocks++;
|
|
sent = 0;
|
|
}
|
|
else
|
|
{
|
|
queue->wireWrites++;
|
|
queue->partialWrites++;
|
|
}
|
|
if (!sendQueues.Append(queue, (const char *) data + sent, size - sent))
|
|
{
|
|
return -1;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
void
|
|
FlushSends()
|
|
{
|
|
for (int i = 0; i < NetSendQueueTable::MaxQueues; ++i)
|
|
{
|
|
NetSendQueue *queue = &sendQueues.queues[i];
|
|
|
|
if (queue->connection != InvalidConnection &&
|
|
!queue->dead &&
|
|
queue->PendingBytes() > 0)
|
|
{
|
|
DrainQueue(queue);
|
|
}
|
|
}
|
|
|
|
//
|
|
// RP412NETSTATS: 5 s per-connection rollup, from here because
|
|
// FlushSends is the one transport entry the engine calls on a
|
|
// steady cadence.
|
|
//
|
|
if (NetTransport_StatsEnabled())
|
|
{
|
|
DWORD now = GetTickCount();
|
|
if ((LONG) (now - statsNextTick) >= 0)
|
|
{
|
|
statsNextTick = now + 5000;
|
|
for (int j = 0; j < NetSendQueueTable::MaxQueues; ++j)
|
|
{
|
|
NetSendQueue *queue = &sendQueues.queues[j];
|
|
if (queue->connection == InvalidConnection ||
|
|
queue->sentMessages == 0)
|
|
{
|
|
continue;
|
|
}
|
|
DEBUG_STREAM << "NetStats: tcp conn "
|
|
<< (unsigned long) queue->connection << ": "
|
|
<< queue->sentMessages << " msgs "
|
|
<< queue->sentBytes << " B, "
|
|
<< queue->wireWrites << " writes ("
|
|
<< queue->partialWrites << " partial, "
|
|
<< queue->wouldBlocks << " would-block), queued high "
|
|
<< queue->queuedHighWater << " B, pending "
|
|
<< queue->PendingBytes()
|
|
<< (queue->dead ? " B, DEAD" : " B")
|
|
<< "\n" << std::flush;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int
|
|
Receive(
|
|
Connection connection,
|
|
void *buffer,
|
|
int size
|
|
)
|
|
{
|
|
//
|
|
// A connection the send side declared dead reports as a
|
|
// disconnect here, so the one disconnect path upstairs runs.
|
|
//
|
|
NetSendQueue *queue = sendQueues.Find(connection);
|
|
if (queue != NULL && queue->dead)
|
|
{
|
|
return ReceiveDisconnected;
|
|
}
|
|
|
|
int received = recv((SOCKET) connection, (char *) buffer, size, 0);
|
|
if (received > 0)
|
|
{
|
|
return received;
|
|
}
|
|
if (received == 0)
|
|
{
|
|
return ReceiveDisconnected;
|
|
}
|
|
|
|
DWORD error = WSAGetLastError();
|
|
switch (error)
|
|
{
|
|
case WSAECONNRESET:
|
|
// hard drop reads the same as an orderly close upstairs
|
|
return ReceiveDisconnected;
|
|
case WSAEWOULDBLOCK:
|
|
return ReceiveNoData;
|
|
default:
|
|
DEBUG_STREAM << "WinsockNetTransport::Receive: recv returned an unexpected error: WSAGetLastError = "
|
|
<< error << std::endl << std::flush;
|
|
return ReceiveNoData;
|
|
}
|
|
}
|
|
|
|
Logical
|
|
GetRemoteAddress(
|
|
Connection connection,
|
|
SOCKADDR_IN *address
|
|
)
|
|
{
|
|
int size = sizeof(SOCKADDR_IN);
|
|
memset(address, 0, size);
|
|
return getpeername((SOCKET) connection, (sockaddr *) address, &size) == 0;
|
|
}
|
|
|
|
private:
|
|
//
|
|
// Push a queue's pending bytes at the wire until it is empty or
|
|
// the wire refuses. Partial progress advances head byte-exactly.
|
|
//
|
|
void
|
|
DrainQueue(NetSendQueue *queue)
|
|
{
|
|
while (queue->PendingBytes() > 0 && !queue->dead)
|
|
{
|
|
int sent = send(
|
|
(SOCKET) queue->connection,
|
|
(const char *) &queue->buffer[queue->head],
|
|
queue->PendingBytes(), 0);
|
|
|
|
if (sent > 0)
|
|
{
|
|
queue->wireWrites++;
|
|
queue->head += sent;
|
|
if (queue->PendingBytes() == 0)
|
|
{
|
|
queue->head = 0;
|
|
queue->tail = 0;
|
|
}
|
|
else
|
|
{
|
|
queue->partialWrites++;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
DWORD error = WSAGetLastError();
|
|
if (sent < 0 && error == WSAEWOULDBLOCK)
|
|
{
|
|
queue->wouldBlocks++;
|
|
break;
|
|
}
|
|
DEBUG_STREAM << "WinsockNetTransport::DrainQueue: hard error "
|
|
<< error << " with " << queue->PendingBytes()
|
|
<< " bytes pending - declaring the connection dead\n" << std::flush;
|
|
queue->dead = True;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Logical started;
|
|
WSADATA winsockData;
|
|
NetSendQueueTable sendQueues;
|
|
DWORD statsNextTick;
|
|
};
|
|
|
|
WinsockNetTransport gWinsockTransport;
|
|
NetTransport *gTransport = &gWinsockTransport;
|
|
}
|
|
|
|
NetTransport *
|
|
NetTransport_Get()
|
|
{
|
|
return gTransport;
|
|
}
|
|
|
|
void
|
|
NetTransport_Set(NetTransport *transport)
|
|
{
|
|
gTransport = (transport != NULL) ? transport : &gWinsockTransport;
|
|
}
|