Chapter 51 Networking and Session Semantics
CNA’s Microsoft::Xna::Framework::Net surface combines one functional LAN route with several compatibility lifecycles. SystemLink uses the vendored ENet transport; Local, LocalWithLeaderboards, PlayerMatch, and Ranked do not. Code that only observes session objects can miss that distinction, so this chapter starts with it.
51.1 Session types and ownership
| NetworkSessionType | Pinned-revision behavior |
|---|---|
| SystemLink | ENet host/join, LAN discovery, handshake, application-data relay, disconnect handling, simulated latency/loss, and host migration |
| Local | Synthetic state and events; no transport or local data delivery |
| LocalWithLeaderboards | Same synthetic lifecycle plus Chapter 49’s local persistence |
| PlayerMatch, Ranked | Stubs; no matchmaking service |
Create, Find, Join, and JoinInvited return a caller-owned NetworkSession*. Dispose() releases transport and owned gamer resources, but it does not delete the session object. The caller must perform both operations, normally through an owning smart pointer with explicit disposal where required.
The game loop must call Update() each frame. Initial GamerJoined events are different: subscribing replays the handler synchronously for gamers already in AllGamers. Later joins arrive through the update queue.
Limitation. The maxGamers compatibility quirk The create path does not forward the caller’s maxGamers. The constructed session reports the literal 69, preserving the FNA/XNA behavior recorded in the implementation. A game that needs an eight-player limit in the example above must enforce eight itself.
51.2 SystemLink transport
The internal ENetBackend uses a star topology. Clients send application data to the host; the host relays it to the target. NetworkGamer::Id and IsHost are consistent across machines, unlike FNA’s fixed zero/true placeholders. RoundtripTime reads ENet’s peer measurement.
LAN discovery uses UDP port 61190, protocol version 1, and a 150-ms search window. A finder sends broadcast and loopback queries, then deduplicates replies by connect port. The game session uses an OS-assigned ENet port; Emscripten reserves 61191 but cannot perform raw UDP browser discovery. QualityOfService’s network-backed construction reports discovery round-trip time; bandwidth remains unmeasured.
The protocol opcodes are ClientHello=0x01, ServerWelcome=0x02, gamer join/leave 0x03/0x04, reserved host change 0x05, state change 0x06, and application data 0x10. There are two ENet channels and at most 31 peers. Up to 64 application sends may wait for handshake completion; the oldest is evicted on the 65th. Disconnect, gamer removal, and migration purge affected queued sends. An internal dropped-packet counter supports tests and diagnostics but is not part of NetworkSession’s public API.
Discovery packets are version-checked. Property indices must be in , one-byte list counts reject values above 255, and completed peers that repeat a hello are disconnected. Host-only broadcasts from a non-host peer are also rejected. These checks assume that an untrusted client can reproduce the documented wire format.
51.3 Host migration
When AllowHostMigration=false, loss of the host ends the session. When it is true, all survivors choose the lowest remaining wire ID from their shared roster. No election packet is needed. The peer owning that ID promotes itself; the others rediscover its cached gamertag and perform a normal hello/welcome reconnect. Discovery is retried for three 150-ms windows.
Migration clears remote gamers and wire-ID state first. Consequently, reconnecting peers see leave and join events and receive new NetworkGamer objects; sockets and object identity are not preserved. If the new host cannot be discovered, the session ends.
SimulatedLatency and SimulatedPacketLoss affect received application data. They do not delay session-management packets or the host’s relay hop. FNA stores these values without applying them; CNA uses them as transport-test controls.
51.4 Delivery guarantees
ENet can express three behaviors for XNA’s five SendDataOptions values:
| XNA option | ENet flag | Observed guarantee |
|---|---|---|
| None | UNSEQUENCED | best effort, unordered |
| InOrder | none | best effort, sequenced |
| Reliable | RELIABLE | reliable and ordered |
| ReliableInOrder | RELIABLE | reliable and ordered |
| Chat | RELIABLE | reliable and ordered |
ENet’s reliable channel is ordered, so CNA cannot reproduce “reliable but unordered” without adding another protocol. Reliable therefore receives the stronger ordered guarantee. Chat uses the same route by CNA policy.
51.5 Compatibility limits and asymmetries
Even in a SystemLink session, voice transport and invitations are absent. EnableSendVoice() and SendPartyInvites() do nothing; voice and private-slot properties retain false defaults. NetworkMachine::RemoveFromSession() throws NotImplementedException. Invite and TrueSkill/arbitration events are never raised. JoinInvited() creates a synthetic PlayerMatch lifecycle rather than joining a service-backed session.
Two inherited data-shape problems are visible to applications:
-
•
PacketWriter writes Color as four bytes, while PacketReader::ReadColor() expects four floats. They are not inverses.
-
•
ReceiveData(PacketReader&, NetworkGamer*&) fills and rewinds the reader but returns zero because its local length is never updated. Gate it with IsDataAvailable, or use the byte-vector overload for an accurate count.
The mutable NetworkSessionProperties indexer also grows the collection on an out-of-range access because C++ cannot distinguish read and write through the translated mutable reference. Use a const reference for bounds-checked reads.
51.6 Asynchronous lifecycle constraint
All session Begin*/End* pairs share one static NetworkSessionAction*. Only one create, find, or join operation can be outstanding in the process, even across different sessions. A second Begin* throws until the matching End* deletes and clears the shared action. The operations are positioned to complete synchronously because their work occurs in the matching End* path.
This design once depended on GamerServicesDispatcher::UpdateAsync() returning false, which it does not after initialization. The resulting busy loop and its isolated regression harness are described in Section 49.7.
51.7 Lifetime hardening
Historical note. Defects found under integration and sanitizers The network module has corrected the following distinct failures:
-
•
Dispose() could run twice, traverse gamer pointers freed by the first call, and trigger a sanitizer-detected use-after-free;
-
•
one disposal left raw pointers visible through gamer collections and Host;
-
•
session-created gamer objects leaked because the session did not own their destruction;
-
•
asynchronous callbacks were stored but not invoked, including reentrant replacement;
-
•
End* cleared the shared action without deleting it;
-
•
local IDs derived from collection size collided after remove/add;
-
•
pre-handshake application data was dropped, then a first queue fix failed to purge entries when peers left;
-
•
a disposed GamerCollection enumerator’s MoveNext() dereferenced a null collection even though Current already guarded the same state.
The current Dispose() is idempotent, clears all gamer collections and Host, and releases owned gamer objects. Focused tests cover double disposal, post-disposal exposure, action counts, callbacks, ID allocation, and queue eviction. AddressSanitizer was the observation point for the original double-dispose failure; ordinary unit success alone had not exposed it.
51.8 Evidence status
Codec, hostile-input, policy, ownership, simulated loss/latency, and compatibility behaviors have focused tests. An out-of-process, two-instance ENet harness establishes cross-process SystemLink delivery on the recorded host. It does not establish Internet matchmaking, voice, browser UDP discovery, or delivery on an untested platform. Host migration is runtime-observed through the local discovery/reconnect route; it preserves neither sockets nor gamer-object identity.