Websockets
Live on this serverA chat that pushes every message to all browsers in the same room.
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.
Open the chat
Every visitor gets a room of their own - open it a second time to chat with yourself.