Build a Chat Room

Read time 3 minutes
On this page

This builds on Your First Server by adding server-to-client push: the server broadcasts every message to all connected clients.

Full source: samples/ChatRoom

What you’ll build

A server that receives a chat message from one client and rebroadcasts it to everyone connected — including multiple clients typing at once.

The packet

Unlike HelloWorld’s fixed-size packet, this one carries variable-length strings.

// samples/ChatRoom/ChatRoom.Contracts/ChatMessagePacket.cs
[GenerateFormatter]
[SerializePackable(SerializeLayout.Explicit)]
public sealed partial class ChatMessagePacket : PacketBase<ChatMessagePacket>, IPacketStaticOpcode
{
    public static ushort StaticOpCode => 0x7101;

    [SerializeOrder(0)]
    public string Username { get; set; } = string.Empty;

    [SerializeOrder(1)]
    public string Message { get; set; } = string.Empty;

    public ChatMessagePacket() { }
}

Full source: samples/ChatRoom/ChatRoom.Contracts/ChatMessagePacket.cs

The handler broadcasts instead of replying

// samples/ChatRoom/ChatRoom.Server/ChatHandlers.cs
[PacketController("ChatRoom.Chat")]
public static class ChatHandlers
{
    [PacketOpcode(0x7101)]
    public static async ValueTask HandleChatAsync(IPacketContext<ChatMessagePacket> context)
    {
        ArgumentNullException.ThrowIfNull(context);

        IConnectionBroadcaster? hub = InstanceManager.Instance
            .GetExistingInstance<IConnectionBroadcaster>();

        if (hub is null)
        {
            return;
        }

        await hub.BroadcastAsync(context.Packet).ConfigureAwait(false);
    }
}

IConnectionBroadcaster is registered automatically by the hosting builder. BroadcastAsync serializes the packet once and sends it to every connected client, applying each connection’s own compression and encryption settings.

Full source: samples/ChatRoom/ChatRoom.Server/ChatHandlers.cs

The server setup is the same shape as HelloWorld

The bootstrap is identical to Quick Start §3 — just swap in ChatHandlers and the chat port. No broadcast-specific wiring is needed on the builder; the broadcaster is resolved inside the handler.

Full source: samples/ChatRoom/ChatRoom.Server/Program.cs

The client subscribes to pushed messages

Unlike a request/response call, the client doesn’t ask for chat messages — it subscribes once and gets called back whenever one arrives.

// samples/ChatRoom/ChatRoom.Client/Program.cs
TcpSession session = new(options);

IDisposable subscription = session.On<ChatMessagePacket>(packet =>
{
    Console.WriteLine($"{packet.Username}: {packet.Message}");
});

await session.ConnectAsync(Host, Port);

// later, to send a message:
ChatMessagePacket packet = new() { Username = username, Message = line.Trim() };
await session.SendAsync(packet, CancellationToken.None).ConfigureAwait(false);

Subscribe with On<TPacket>() before connecting, so you never miss a message that arrives right after the connection opens. Dispose the subscription and disconnect the session when you’re done.

Full source: samples/ChatRoom/ChatRoom.Client/Program.cs

Run it

dotnet build samples/ChatRoom/ChatRoom.sln

Open three terminals.

Terminal 1 — start the server:

dotnet run --project samples/ChatRoom/ChatRoom.Server

Terminal 2 and 3 — start two clients:

dotnet run --project samples/ChatRoom/ChatRoom.Client

Each will prompt for a username.

What you should see

Type a message in one client’s terminal:

hello everyone

Both client terminals will show:

alice: hello everyone

Type /exit in a client to disconnect it. Press Ctrl+C in the server terminal to stop it.

Next steps