Build a Chat Room
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
- Securing Your Server — add encryption and support UDP/WebSocket alongside TCP
- Implementing Packet Handlers — more on writing handler logic