Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

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.

1 std::unique_ptr<NetworkSession> session(NetworkSession::Create(
2 NetworkSessionType::SystemLink, 1, 8));
3
4 session->GamerJoined.Add(
5 [](System::Object*, const GamerJoinedEventArgs& args) {
6 // Existing gamers replay now; later joins arrive from Update().
7 });
8
9 // Once per game update:
10 session->Update();

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 [0,256), 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.

1 PacketWriter writer;
2 writer.Write(playerTintColor); // four bytes
3 localGamer->SendData(writer, SendDataOptions::Reliable);
4
5 std::vector<SharpRuntime::bytecs> rgba(4);
6 NetworkGamer* sender = nullptr;
7 while (localGamer->getIsDataAvailableProperty()) {
8 int count = localGamer->ReceiveData(rgba, sender);
9 if (count == 4) {
10 Color tint(rgba[0], rgba[1], rgba[2], rgba[3]);
11 }
12 }

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.

Type at least three characters. Results are ranked by how often and where the words occur.