Tutorial 166: Online sessions, invitations and host migration

CNA Tutorials  ·  CNA snapshot b0e97bb1

ℹ

What you’ll learn: how online sessions differ from SystemLink, how to host, find and join through the server, accept invitations, survive host migration, add local players and use voice, and which limits to plan for.

ℹ

Status: PlayerMatch and Ranked sessions need a CNA Gamer Services server (Tutorial 164) and signed-in server accounts, and run in native builds only. SystemLink needs neither a server nor the Internet; it is covered in depth by Tutorial 97. The code follows CNA’s headers and its own online-session test client at snapshot b0e97bb1; it was not compiled or run for this page.

Session types at a glance

TypeWithout a serverWith a serverTransport
LocalYes, in one processThe sameNone (one machine); game data sent with SendData is not delivered (CNA-BUG-250)
LocalWithLeaderboardsYes; scores go to the local storeAccounts’ scores are committed to the server when the host ends the gameNone
SystemLinkYes, with signed-in local profiles; works offlineThe sameDirect ENet over UDP on the LAN, broadcast discovery on UDP port 61190
PlayerMatch, RankedRefused: GamerServicesNotAvailableExceptionCreate, Find, Join and JoinInvited through the server’s session directory; accounts onlyEvery datagram, game data and voice, through the server’s authenticated WebSocket relay

There is no peer-to-peer path and no NAT traversal for online sessions: every packet goes from a machine to the relay and on to the host or a client, over TLS on TCP. That works behind any NAT and keeps the server’s authority over who is in a session, at the cost of TCP head-of-line latency and jitter when packets are lost. A turn-based game will not notice; a fast action game should test it under real conditions.

1. Host an online session

Online Begin calls do not complete inline: their result completes during GamerServicesDispatcher::Update, which the GamerServicesComponent runs from Game::Update. Keep the result, which the caller owns, and finish the call once it has completed:

#include "Microsoft/Xna/Framework/Net/HostChangedEventArgs.hpp"
#include "Microsoft/Xna/Framework/Net/NetworkSession.hpp"
#include "Microsoft/Xna/Framework/Net/NetworkSessionEndedEventArgs.hpp"
#include <memory>

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Net;

std::unique_ptr<System::IAsyncResult> pending_;   // caller-owned Begin result
NetworkSession* session_ = nullptr;               // caller-owned: Dispose(), then delete
bool sessionEnded_ = false;

void MyGame::HostOnline()
{
    // up to two signed-in accounts from this machine, eight gamers in all
    pending_.reset(NetworkSession::BeginCreate(NetworkSessionType::PlayerMatch, 2, 8, {}, {}));
}

void MyGame::Update(GameTime& gameTime)
{
    Game::Update(gameTime);   // completes online work
    if (pending_ && pending_->getIsCompletedProperty())
    {
        session_ = NetworkSession::EndCreate(pending_.get());   // throws if the server refused
        pending_.reset();
        session_->setAllowHostMigrationProperty(true);          // only the host may set it
        session_->HostChanged += [](System::Object*, const HostChangedEventArgs& e) {
            // e.getNewHostProperty() now owns StartGame/EndGame and the session settings
        };
        session_->SessionEnded += [this](System::Object*, const NetworkSessionEndedEventArgs& e) {
            sessionEnded_ = true;   // e.getEndReasonProperty(): HostEndedSession, Disconnected, ...
        };
    }
    if (session_) session_->Update();
    if (session_ && sessionEnded_)
    {
        session_->Dispose();   // releases the transport and every gamer object; does not delete
        delete session_;
        session_ = nullptr;
        sessionEnded_ = false;
    }
}

Without a list of gamers, CNA takes the signed-in, non-guest accounts on this machine up to maxLocalGamers (1–4); maxGamers is 2–31. Every gamer must be a server account allowed online sessions; a local profile or a guest makes the call throw GamerServicesNotAvailableException (guests: CNA-GAP-074). A process runs one session at a time, with at most one Create, Find or Join pending. The synchronous Create, Find and Join also work online; they wait by pumping the dispatcher.

2. Find and join

#include "Microsoft/Xna/Framework/Net/AvailableNetworkSessionCollection.hpp"
#include "Microsoft/Xna/Framework/Net/NetworkSessionProperties.hpp"

std::unique_ptr<System::IAsyncResult> search_;

// start a search
search_.reset(NetworkSession::BeginFind(NetworkSessionType::PlayerMatch, 1, NetworkSessionProperties{}, {}, {}));

// in Update, once search_ has completed
AvailableNetworkSessionCollection found = NetworkSession::EndFind(search_.get());
search_.reset();
const AvailableNetworkSessionCollection& list = found;   // index the collection through a const reference
if (list.getCountProperty() > 0)
    pending_.reset(NetworkSession::BeginJoin(&list[0], {}, {}));   // finish with EndJoin as with EndCreate

Search properties filter on the eight XNA session properties the host advertised. A join completes only after the server has admitted the gamers, the relay is connected and the host has welcomed the new machine. Ranked sessions do not allow joining a game in progress (setAllowJoinInProgressProperty(true) throws NotSupportedException).

3. Invitations

Players send invitations from the Guide — the Home key, a friend’s gamer card, Invite to game — so a game does not have to call anything; Guide::ShowGameInvite(player, recipients) opens the same page from code. The invited player accepts in their Guide, and their game hears XNA’s InviteAccepted:

#include "Microsoft/Xna/Framework/GamerServices/InviteAcceptedEventArgs.hpp"

System::EventHandler<GamerServices::InviteAcceptedEventArgs>::Token invite_{};

// in the constructor
invite_ = NetworkSession::InviteAccepted.Add(
    [this](System::Object*, const GamerServices::InviteAcceptedEventArgs& e) {
        if (e.getIsCurrentSessionProperty()) return;   // already in that session
        if (session_) { session_->Dispose(); delete session_; session_ = nullptr; }
        session_ = NetworkSession::JoinInvited(1);     // waits until the join completes
    });

// in the destructor
NetworkSession::InviteAccepted.Remove(invite_);

An acceptance that arrives before any handler exists is delivered once to the first handler added, but only while the invitation can still be used. Invitations are CNA server IDs, not Xbox tokens; they last 15 minutes and only while the session exists. Joining a friend’s or party member’s game from their gamer card raises InviteAccepted too.

4. Host migration

Only the host sets AllowHostMigration; every machine reads the host’s value. Online, when the host leaves or its process stops (its relay connection closes and does not come back within 20 seconds), the server makes the machine holding the lowest remaining gamer ID the new host. On every machine the old host’s gamers leave (GamerLeft), then HostChanged is raised; gamer IDs, the roster and session properties carry over, and the new host takes over settings, StartGame/EndGame and machine removal. A client waits up to 30 seconds for the decision; if the session is gone or migration is off, it ends with HostEndedSession. How the new host is chosen is CNA’s policy — XNA only reports that a migration happened. SystemLink sessions migrate too, promoting the machine with the lowest wire ID.

5. More players, voice and test conditions

  • Extra local gamers. session_->AddLocalGamer(gamer) adds another signed-in account from this machine. It arrives at a later Update, with GamerJoined on every machine; a gamer without an account throws GamerPrivilegeException.
  • Voice is routed automatically in SystemLink and online sessions, as XNA did: the machine’s microphone belongs to Player One’s gamer, speech is detected and encoded with Opus (16 kHz, 20 ms frames) and sent unreliably to each machine allowed to hear it, relayed by the host. NetworkGamer::getHasVoiceProperty(), getIsTalkingProperty() and getIsMutedByLocalUserProperty() follow it, and LocalNetworkGamer::EnableSendVoice switches one direction. One microphone per machine means a second local player cannot talk. Voice needs libopus 1.3 or newer when CNA is configured: CNA_ENABLE_VOICE=AUTO (the default) uses it when pkg-config finds it, ON fails without it, OFF builds none; browser and Android builds never carry voice. CNA_VOICE=0 turns it off at run time. Without voice, sessions work unchanged and HasVoice is false.
  • Simulated conditions. setSimulatedLatencyProperty(System::TimeSpan::FromMilliseconds(150)) and setSimulatedPacketLossProperty(0.05f) delay or drop game data arriving for this session’s local gamers (not session-management traffic). By reading the source, they act in the direct ENet transport that SystemLink uses; the online relay path has no such hook.
  • Leaderboards in online sessions are written while the game is playing and committed when the host calls EndGame; Ranked rows count only when a strict majority of reporting machines agree, and CNA computes no TrueSkill. Tutorial 123 has the details.

Limits to plan for

  • Online search results do not measure connection quality: QualityOfService of a PlayerMatch or Ranked listing reports IsAvailable false (CNA-GAP-053). SystemLink results measure round trip and both bandwidths, and joined sessions report round-trip times and traffic on every transport.
  • SendDataOptions is a plain enum, so XNA’s combined Chat | Reliable style flags cannot be expressed, and chat packets are always sent reliably (CNA-GAP-072).
  • Guests cannot take part in PlayerMatch or Ranked (CNA-GAP-074).
  • Browser builds have no relay and no service, and SystemLink discovery finds nothing there (CNA-PLAT-012).
  • The host relays game data for its clients and is trusted to do so: the server checks membership and sender identity, not game content. There is no anti-cheat.

Evidence and qualification

  • CNA’s final audit run of this work (2 October 2026, Linux): the GamerServices suite 649 passed and the Net suite 524 passed, with no failures; the server’s 40 registered tests: 33 passed, 7 network-isolation tests skipped for lack of a test tool, none failed. The passing server tests run two real CNA processes through the TLS relay for PlayerMatch and Ranked, invitations, a server restart mid-session and host migration; the skipped seven are the NAT-isolated variants, the host-crash test and the AddLocalGamer test. These are CNA-recorded runs; this site did not re-run them.
  • CNA’s earlier NAT-isolation runs put each process in its own network namespace on one Linux host: shared-host evidence, not a measurement of the public Internet.
  • Not qualified: Windows and macOS, a real wide-area network with NAT (latency, loss and failover are unmeasured), voice on physical audio devices (tested in software only), and avatar pixels on Direct3D and Metal.
  • Browser builds have no service, relay or voice.

Next

Tutorial 167 draws the players’ avatars, which a game can also share over any session. Back to Tutorial 165 for friends and presence, or to the Gamer Services overview.