Skip to content

Client Session Guide

Learning Signals

  • Level: Intermediate
  • Time: 15 minutes
  • Prerequisites: Architecture

This guide walks through creating, configuring, and connecting a client session using Nalix.SDK. It focuses on the actual SDK surface in the current source tree: TransportOptions, TcpSession, UdpSession, and the transport extension methods built on top of them.

Security Warning

Never expose the Secret key or the SessionToken snowflakes in clear-text logs or debug UIs. These are sensitive cryptographic materials.

Common Configuration Pitfall (Handshake Errors)

If you plan to use session.HandshakeAsync(), DO NOT manually configure TransportOptions.EncryptionEnabled = true or set a hardcoded Secret beforehand! The initial handshake phase runs in plaintext to securely negotiate a dynamic key using X25519. If you manually enable encryption out of the gate, the SDK will encrypt the CLIENT_HELLO packet, the server will fail to decrypt it, and the connection will immediately drop. HandshakeAsync() safely and automatically enables encryption and assigns the Secret for you upon success.

1. Create a Shared Packet Catalog

The client and server must use the exact same packet contracts. The IPacketRegistry (catalog) maps packet types to their structural metadata and deserialize logic.

using Nalix.Abstractions.Networking.Packets;
using Nalix.Codec.DataFrames;

// 1. Initialize the shared registry.
// This factory scans for packets and binds high-performance deserialize pointers.
IPacketRegistry catalog = new PacketRegistryFactory()
    .RegisterPacket<MyRequestPacket>()
    .RegisterPacket<MyResponsePacket>()
    .CreateCatalog();

2. Configure Transport Options

The TransportOptions govern connection backoff patterns, socket buffer sizing, event queue capacities, and encryption. You can fetch these directly from the environment using ConfigurationManager or manually construct them.

using Nalix.SDK.Options;
using Nalix.Environment.Configuration;

// OPTION A: Load automatically from the environment's configuration files (Recommended)
// This binds values from your INI definitions and natively supports hot-reloading.
TransportOptions options = ConfigurationManager.Instance.Get<TransportOptions>();

// OPTION B: Manual Initialization
TransportOptions manualOptions = new()
{
    // Connectivity
    Address = "127.0.0.1",
    Port = 57206,
    ConnectTimeoutMillis = 5000,

    // Auto-reconnect configuration
    ReconnectEnabled = true,
    ReconnectBaseDelayMillis = 500,
    ReconnectMaxDelayMillis = 30000,
    ReconnectMaxAttempts = 0, // 0 = infinite retries

    // Performance tuning
    NoDelay = true,           // Disable Nagle's algorithm (highly recommended for Games/Real-time)
    BufferSize = 8192,        // Socket send/receive buffer size
    AsyncQueueCapacity = 1024,// Queue capacity for OnMessageAsync to prevent memory exhaustion
    MaxUdpDatagramSize = 1400,// UDP limit including the 8-byte session token

    // We typically let the Handshake process set Encryption/Algorithms automatically.
    // However, if bypassing Handshake on an unencrypted server, ensure EncryptionEnabled = false.
};

3. Create and Connect a TCP Session

A TcpSession uses the configured TransportOptions to establish TCP streams. In practice, you will usually register events before calling ConnectAsync() so you do not miss early connection or error signals.

using System;
using System.Threading.Tasks;
using Nalix.SDK.Transport;
using Nalix.SDK.Transport.Extensions;
using Nalix.Framework.Memory.Buffers;

async Task ConnectTcpStandardAsync()
{
    // The 'using' declaration ensures sockets and background loops are safely tracked
    using TcpSession session = new(options, catalog);

    // 1. Bind Lifecycle Events
    session.OnConnected += (_, _) => 
    {
        Console.WriteLine("TCP Connected successfully.");
    };

    session.OnDisconnected += (_, ex) => 
    {
        // ex contains underlying SocketExceptions or NetworkExceptions
        Console.WriteLine($"TCP Disconnected. Reason: {ex.Message}");
    };

    session.OnError += (_, ex) => 
    {
        Console.WriteLine($"TCP Error observed: {ex.Message}");
    };

    // 2. Bind Message Handlers (Strongly-Typed)
    // The framework will automatically read the byte stream, deserialize the packet,
    // and invoke your callback strictly when the matching type is observed.
    session.On<MyResponsePacket>(packet => 
    {
        Console.WriteLine($"Received strongly-typed data: {packet.StatusMessage}");
    });

    // 3. Connect, try resume, then fall back to handshake when needed
    // `src/Nalix.SDK/Transport/Extensions/ResumeExtensions.cs` connects first,
    // attempts resume only when ResumeEnabled + SessionToken + Secret are present,
    // and reconnects before falling back to HandshakeAsync when configured to do so.
    Console.WriteLine("Connecting to server...");
    bool resumed = await session.ConnectWithResumeAsync();
    Console.WriteLine(resumed ? "Session resumed." : "Fresh handshake completed.");

    // At this point, the shared TransportOptions object contains the negotiated
    // encryption state and the current session token assigned by the server.
    // ---------------------------------------------------------

    // 4. Ship Payloads
    // session.SendAsync(...) handles packing, compression, encryption, and dispatch over the wire.
    // await session.SendAsync(new MyCustomPacket());

    // Prevent context from tearing down
    await Task.Delay(-1); 
}

Performance Edge-case Options

The On<T> extension is designed for highest developer velocity. If you need lower-level control, see our Session APIs Guide for raw OnMessageReceived / OnMessageAsync flows.

Performance Recommendation

While the SDK is mostly platform-agnostic, specialized socket options like TCP_NODELAY are enabled by default for gaming workloads.

4. Create and Connect a UDP Session (Auxiliary Channel)

TCP is Primary, UDP is Auxiliary

In the Nalix architecture, TCP is typically the primary, stateful connection. The UdpSession acts as an auxiliary channel for high-frequency, unreliable data. In the common secured flow, the UDP session reuses the SessionToken established by the TCP handshake or resume path.

UdpSession follows the same base transport contract, but handles datagrams instead of framed TCP streams. Because the primary TCP session already completed handshake or resume, the shared options object already holds the required SessionToken.

using System;
using System.Threading.Tasks;
using Nalix.SDK.Transport;

async Task ConnectUdpStandardAsync()
{
    // The shared `options` object already has `options.SessionToken` populated
    // by the TCP handshake or resume flow above.
    using UdpSession udp = new(options, catalog);

    udp.OnConnected += (_, _) => Console.WriteLine("UDP socket bound and active.");
    udp.OnDisconnected += (_, ex) => Console.WriteLine($"UDP Disconnected: {ex.Message}");
    udp.OnError += (_, ex) => Console.WriteLine($"UDP Error: {ex.Message}");

    // Connect to setup targeted UDP Socket (binding against Server IP and Port)
    await udp.ConnectAsync();

    // UDP internally prefixes the current SessionToken to all outbound datagrams.
    // await udp.SendAsync(new PlayerMovementPacket { X = 1, Y = 2, Z = 5 });

    await Task.Delay(-1);
}

UDP Fragmented Size Constraint

UDP payloads plus the 8-byte SessionToken prefix cannot exceed TransportOptions.MaxUdpDatagramSize (Default 1400 bytes). Large chunks of data must be routed through TCP protocols. Violating the MTU triggers local NetworkException halts.

Info

TransportOptions.Secret uses the Bytes32 primitive in the current SDK source.

5. Common SDK Extensions

To help you get started quickly and handle advanced networking tasks gracefully, here are some built-in extensions that can be executed directly on any session instance:

using Nalix.SDK.Options;
using Nalix.SDK.Transport.Extensions;

// 1. Network Latency Check (Ping)
// Measures round-trip time (RTT) safely across the protocol
double rttMs = await session.PingAsync();
Console.WriteLine($"Current Latency: {rttMs}ms");

// 2. Time Synchronization
// Re-aligns the client's internal clock offsets against the core server's clock
var sync = await session.SyncTimeAsync();
Console.WriteLine($"RTT={sync.RttMs}ms Adjusted={sync.AdjustedMs}ms");

// 3. Request-Response Mechanics (RPC via Packets)
// Send a request packet and cleanly await a strongly-typed response 
var response = await session.RequestAsync<MyResponsePacket>(
    requestPacket: new MyRequestPacket { UserId = 123 },
    options: RequestOptions.Default.WithTimeout(3000), // timeout within 3s
    predicate: p => p.IsSuccess // Optional: Filter exactly which response to accept
);

6. Automatic Keep-Alive (Heartbeat)

Most Nalix servers employ strict idle-timeout rules and will aggressively drop socket connections if no data is transmitted for a sustained period. To prevent your client from being disconnected while idle, you should boot a background fire-and-forget task that strictly calls PingAsync() at the interval defined in your options.

// Boot a background worker to continuously ping the server.
// `options.KeepAliveIntervalMillis` defaults to 20_000 (20 seconds).
_ = Task.Run(async () => 
{
    while (session.IsConnected)
    {
        try 
        {
            await Task.Delay(options.KeepAliveIntervalMillis);
            if (!session.IsConnected) break;

            // PingAsync uses the highest-priority Control packet lane
            double rtt = await session.PingAsync();
            Console.WriteLine($"KeepAlive Ping: {rtt}ms");
        }
        catch (Exception ex)
        {
            // A TimeoutException or NetworkException here indicates the server has 
            // likely died or the network cable was pulled.
            Console.WriteLine("KeepAlive Failed. Reconnection loop should trigger.");
            break; 
        }
    }
});

7. Graceful Cleanup

Always enforce rigorous teardowns if destroying connection lifetimes. This collapses the asynchronous looping structures safely and reclaims memory leases instantly.

// Inform sockets to shut down and unmanaged handlers to cease executing
await session.DisconnectAsync();

// Reclaim buffers and class instances
session.Dispose();