Admission and Queues¶
A server often has to decide whether a client may join before it lets the client in: check a whitelist or a ban list, validate a one-time ticket against a web service, or hold the client in a queue until a slot frees up. MafiaNet lets the server make that decision before either side reports a connection, with the waiting clients kept out of the player slots entirely.
This page explains how the pieces fit together and walks through a complete queue. The primitives are the interactive session handshake (Connecting) plus four additions built for exactly this:
API |
Role |
|---|---|
|
Waiting clients live in a pool of their own instead of in the player slots. |
|
How long a request may wait for its answer, on either end. |
|
Tell a waiting client what is happening; restarts the timeout on both ends. |
|
What the client receives from |
|
Tells the server a request it was holding went away unanswered. |
Why not decide after connecting?¶
The obvious approach – accept the connection, then check the client and kick it if it fails – has two problems.
The client already got in. By the time it is kicked it has been told it is connected, has received whatever the server sends on connect, and may have started loading. A refused player sees the world flash up and then get torn down.
Waiting costs a slot. A connected client holds one of the
SetMaximumIncomingConnections()slots and appears inNumberOfConnections()for as long as the check takes. A slow database lookup, a queue, or an attacker who connects and never finishes, all hold slots that real players need, and inflate the player count a server list shows.
Deciding inside the session handshake fixes the first: nothing is reported, and nothing but handshake traffic crosses the connection, until the server answers. The pending pool fixes the second.
How the pending pool works¶
Every remote peer that asked to connect but is not yet CONNECTED is pending – from its first
open-connection request, through the transport handshake, to the session handshake waiting on your
answer. Without a pool, pending peers in the session handshake count against
SetMaximumIncomingConnections() alongside connected ones, which is the historic behaviour and
stays the default.
With a pool:
Pending peers count against the pool,
totalof them at once, at mostperAddressfrom any one IP address. A newcomer that would exceed either is refused at the transport withID_NO_FREE_INCOMING_CONNECTIONS, exactly as a full server always refused one.The incoming limit counts connected peers only.
NumberOfConnections()andGetSystemList()never include a pending peer, so neither does anything built on them – a status command, a server browser’s player count.A handshake may start while the server is full. That is what makes a queue possible: the client waits in the pool while every player slot is taken.
Acceptance is bounded by the incoming limit. An
AcceptSession()that would take the connected count pastSetMaximumIncomingConnections()refuses the peer instead (the client seesID_CONNECTION_ATTEMPT_FAILEDwith no reason). CheckNumberOfConnections()yourself first and refuse with a reason of your own; the library check is the backstop for two accepts racing one free slot.
A flood of clients that connect and stall – or just a long queue – fills the pool and nothing else. Players already connected keep playing, and free player slots stay free.
Startup() allocates one connection structure per slot, so it must leave room for both:
const unsigned short maxPlayers = 64;
const unsigned short maxWaiting = 32;
server->SetSessionConfigInteractive(true);
server->SetMaximumPendingSessions(maxWaiting, 4); // at most 4 waiting from one IP
server->SetSessionTimeout(45000); // how long an answer may take
MafiaNet::SocketDescriptor sd(60000, 0);
server->Startup(maxPlayers + maxWaiting, &sd, 1); // room for players AND waiting clients
server->SetMaximumIncomingConnections(maxPlayers); // players only
Note
The per-address share is per IP, not per player. Players behind one NAT – a household, a LAN
party, some mobile carriers – share an address. Keep perAddress above 1 unless you mean to
allow one waiting client per network.
The life of a waiting request¶
client server
| -- open connection (transport) ------------->| counted in the pending pool
| -- ID_SESSION_CONFIG_REQUEST (payload) ----->| surfaced to the application
|<-- ID_SESSION_CONFIG_STATUS (0..n) -----------| SendSessionStatus(); timeout restarts
| |
|<-- ID_SESSION_CONFIG --------------------------| AcceptSession(): now CONNECTED
| ID_CONNECTION_REQUEST_ACCEPTED | ID_NEW_INCOMING_CONNECTION
| |
| or |
|<-- ID_SESSION_CONFIG_REJECTED (reason) --------| RejectSession(): never connected
| ID_CONNECTION_ATTEMPT_FAILED + reason |
| |
| or the client leaves / the timeout passes |
| ID_CONNECTION_ATTEMPT_FAILED | ID_SESSION_CONFIG_ABANDONED
While a request waits:
GetConnectionState(guid)reportsIS_CONNECTINGon both ends.Send()to the waiting guid returns 0, and broadcasts skip it: the application has not been told this connection exists. Handshake messages, status included, go out regardless.Application data the waiting client pushes at the server is dropped, not delivered.
Every request ends exactly one way on the server: you answer it, or you receive
ID_SESSION_CONFIG_ABANDONED for it – once, and never for a request you already answered.
Answering after that does nothing.
Timeouts¶
SetSessionTimeout() bounds how long a request may go without an answer. It replaces the
connection timeout for the session handshake only; 0, the default, keeps using the connection
timeout. Every SendSessionStatus() restarts it, on the server when it is sent and on the
client when it arrives, so a queue that updates its clients regularly is never timed out however long
they wait.
Set it on both ends. Whichever end runs out first ends the attempt: a server prepared to wait 45 seconds does not help a client that gives up after the 10-second default. Clients should be generous – the server decides, and a dead server is caught by the ordinary connection timeout anyway:
client->SetSessionTimeout(120000);
client->Connect("game.example.com", 60000, nullptr, 0);
When the server’s timeout passes, the client gets ID_CONNECTION_ATTEMPT_FAILED with no reason and
the server gets ID_SESSION_CONFIG_ABANDONED. If you want the client to read why, refuse it
yourself before the library’s timeout does, and keep the library’s value as the backstop.
Status messages¶
SendSessionStatus() sends an opaque payload – a UTF-8 line, a JSON object, a queue position as
bytes – to a client whose request is still waiting. The client receives it as
ID_SESSION_CONFIG_STATUS with the payload at packet->data + 1:
// client
case ID_SESSION_CONFIG_STATUS: {
const std::string status((const char *)packet->data + 1, packet->length - 1);
ShowConnectingScreen(status); // "You are 3rd in the queue"
break;
}
Status is sent reliably and in order. It is ignored for a request that is no longer waiting, so a
status racing an accept is simply dropped. It is bounded by MAXIMUM_SESSION_CONFIG_SIZE. A client
cannot send one to a server: the server ignores it.
A complete queue¶
The pattern: every request goes into a FIFO; each tick the server lets in as many as there are free slots, tells everyone still waiting where they stand, and forgets anyone who left.
struct Waiting {
MafiaNet::RakNetGUID guid;
MafiaNet::TimeMS lastStatus = 0;
};
std::deque<Waiting> queue;
unsigned int pendingAccepts = 0; // accepted, not yet reported as ID_NEW_INCOMING_CONNECTION
void OnPacket(MafiaNet::Packet *packet) {
switch (packet->data[0]) {
case ID_SESSION_CONFIG_REQUEST: {
// The client's session payload rides the request: who it is, a ticket, a build id.
if (!IsAllowed(packet->data + 1, packet->length - 1)) {
server->RejectSession(packet->guid, "You are not on the whitelist.");
break;
}
queue.push_back({packet->guid});
break;
}
case ID_SESSION_CONFIG_ABANDONED:
// Gave up or timed out. Drop it; everyone behind moves up.
queue.erase(std::remove_if(queue.begin(), queue.end(), [&](const Waiting &w) { return w.guid == packet->guid; }), queue.end());
break;
case ID_NEW_INCOMING_CONNECTION:
// An accepted request became a connection: it is counted by NumberOfConnections() now.
--pendingAccepts;
break;
}
}
void Tick() {
// Let in whoever fits.
while (!queue.empty() && server->NumberOfConnections() + pendingAccepts < maxPlayers) {
server->AcceptSession(queue.front().guid, sessionConfig.data(), (unsigned int)sessionConfig.size());
++pendingAccepts;
queue.pop_front();
}
// Tell the rest where they stand, well inside the session timeout.
const MafiaNet::TimeMS now = MafiaNet::GetTimeMS();
for (size_t i = 0; i < queue.size(); ++i) {
if (now - queue[i].lastStatus < 5000)
continue;
const std::string status = "You are " + std::to_string(i + 1) + " in the queue.";
server->SendSessionStatus(queue[i].guid, status.data(), (unsigned int)status.size());
queue[i].lastStatus = now;
}
}
Things to note in that loop:
Count accepts in flight.
AcceptSession()is queued to the network thread, soNumberOfConnections()does not include a peer you accepted a moment ago. WithoutpendingAcceptstwo ticks can accept into the same free slot; the library refuses the second, but that player is then turned away instead of kept waiting.Status doubles as the keep-alive. Each one restarts both timeouts, so the interval only has to be comfortably shorter than
SetSessionTimeout(). A client that stops getting status is timed out and shows up asID_SESSION_CONFIG_ABANDONED.The pool bounds the queue. Once
totalclients wait, the next one is refused withID_NO_FREE_INCOMING_CONNECTIONSbefore it ever reachesID_SESSION_CONFIG_REQUEST. Size the pool for the longest queue you are willing to hold.Never block the receive loop. A decision that needs a database or an HTTP call is started when the request arrives and answered when the result comes back; the request waits in the pool meanwhile, with status to keep it alive if the call is slow.
Security notes¶
The client’s session payload, and anything in it, is untrusted input. Validate it before use; see Session Handshake Security.
The pool is a bound, not a filter: anyone who can reach the port can occupy a pending slot until it is answered or times out. Keep the timeout proportionate, keep the per-address share small, and refuse early – a request refused on arrival frees its slot at once.
Status is visible to the waiting client only. Do not put anything in it you would not show a player who is about to be refused.
See Also¶
Connecting - The session handshake and interactive mode
Session Handshake Security - Trust model and denial-of-service limits
Network Messages -
ID_SESSION_CONFIG_*message referenceClient-Server Architecture - Client-server setup