Websockets

Live on this server

A chat that pushes every message to all browsers in the same room.

WebsocketSample.cs161 lines
using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.IO;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Websockets;

public static class WebsocketSample
{

    public static IHandlerBuilder Create()
    {
        /*
         *
         * A chat: every message a client sends over the websocket is pushed
         * to all clients connected to the same room.
         *
         * Open /samples/websockets/ in two browser windows (with the same room) and start chatting.
         *
         * See https://genhttp.org/documentation/content/frameworks/websockets/
         *
         */

        var chat = Websocket.Reactive()
                            .Handler(new ChatHandler());

        var client = Content.From(Resource.FromString(ClientPage).Type(ContentType.TextHtml));

        return Layout.Create()
                     .Index(client) // GET /samples/websockets/
                     .Add("chat", chat); // ws://.../samples/websockets/chat?room=...
    }

    public record ChatMessage(string Author, string Text, DateTime? Sent = null);

    public class ChatHandler : IReactiveHandler
    {
        // the room of every connected client, as requested via "?room=..."
        private readonly Dictionary<IReactiveConnection, string> _clients = [];

        // connections are not thread-safe, so we never write to them concurrently
        private readonly SemaphoreSlim _sync = new(1, 1);

        public async ValueTask OnConnected(IReactiveConnection connection)
        {
            var room = connection.Request.Header.Query.GetEntry("room") ?? "lobby";

            await _sync.WaitAsync();

            try
            {
                _clients[connection] = room;
            }
            finally
            {
                _sync.Release();
            }
        }

        public async ValueTask OnMessage(IReactiveConnection connection, IWebsocketFrame message)
        {
            // JSON sent by the client is deserialized into our record
            var received = await message.ReadPayloadAsync<ChatMessage>();

            if (received == null || received.Text.Length > 500)
            {
                return;
            }

            var toSend = received with { Sent = DateTime.UtcNow };

            await _sync.WaitAsync();

            try
            {
                var room = _clients[connection];

                foreach (var (client, clientRoom) in _clients)
                {
                    if (clientRoom == room)
                    {
                        // ... and serialized to JSON again
                        await client.WritePayloadAsync(toSend);
                    }
                }
            }
            finally
            {
                _sync.Release();
            }
        }

        public async ValueTask OnClose(IReactiveConnection connection, IWebsocketFrame message)
        {
            await _sync.WaitAsync();

            try
            {
                _clients.Remove(connection);
            }
            finally
            {
                _sync.Release();
            }

            await connection.CloseAsync();
        }

    }

    private const string ClientPage =
        """
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="utf-8">
            <title>Chat</title>
        </head>
        <body>
            <p>Room <strong id="room"></strong> - <a id="invite" target="_blank">open it in another window</a></p>
            <form id="form">
                <input id="author" placeholder="Your name" maxlength="50" required>
                <input id="text" placeholder="Your message" maxlength="500" required autofocus>
                <button>Send</button>
            </form>
            <ul id="messages"></ul>
            <script>
                // every visitor gets a room of their own, unless invited to another one
                const params = new URLSearchParams(location.search);
                const room = params.get('room') || Math.random().toString(36).substring(2, 8);

                document.getElementById('room').textContent = room;
                document.getElementById('invite').href = `?room=${room}`;

                const url = new URL(`chat?room=${room}`, location.href.replace(/^http/, 'ws'));
                const socket = new WebSocket(url);

                socket.onmessage = (event) => {
                    const message = JSON.parse(event.data);

                    const item = document.createElement('li');
                    item.textContent = `[${new Date(message.sent).toLocaleTimeString()}] ${message.author}: ${message.text}`;

                    document.getElementById('messages').prepend(item);
                };

                document.getElementById('form').onsubmit = (event) => {
                    event.preventDefault();

                    const text = document.getElementById('text');

                    socket.send(JSON.stringify({ author: document.getElementById('author').value, text: text.value }));

                    text.value = '';
                };
            </script>
        </body>
        </html>
        """;

}

Try it

Send the requests to the sample running on this server - change the path, headers or body as you like, or copy them as curl commands.

  1. Open the chat

    Every visitor gets a room of their own - open it a second time to chat with yourself.

    PAGE /samples/websockets/ Open