From 40e0bd152f3d85e7a49812550bb5c3fb29c4c1c1 Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 22 May 2025 18:40:19 -0400 Subject: [PATCH] Added working example. --- dub.json | 4 +- dub.selections.json | 2 +- examples/simple-example.d | 32 +++++++++- examples/simple-example.html | 41 ++++++++++++ source/handy_http_websockets/components.d | 5 +- source/handy_http_websockets/connection.d | 4 ++ source/handy_http_websockets/handler.d | 17 ++++- source/handy_http_websockets/manager.d | 77 ++++++++++++++++++++--- 8 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 examples/simple-example.html diff --git a/dub.json b/dub.json index 6204828..61a7955 100644 --- a/dub.json +++ b/dub.json @@ -4,11 +4,11 @@ ], "copyright": "Copyright © 2025, Andrew Lalis", "dependencies": { - "handy-http-primitives": "~>1.5", + "handy-http-primitives": "~>1.6", "slf4d": "~>3", "photon": "~>0.10" }, - "description": "mplementati", + "description": "Websocket implementation for Handy-Http.", "license": "CC0", "name": "handy-http-websockets" } \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json index 0098d19..c4a496e 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -1,7 +1,7 @@ { "fileVersion": 1, "versions": { - "handy-http-primitives": "1.5.0", + "handy-http-primitives": "1.6.0", "photon": "0.10.2", "sharded-map": "2.7.0", "slf4d": "3.0.1", diff --git a/examples/simple-example.d b/examples/simple-example.d index 20d4d66..adaa2b2 100755 --- a/examples/simple-example.d +++ b/examples/simple-example.d @@ -4,6 +4,12 @@ dependency "handy-http-websockets" path="../" +/ +/** + * This example demonstrates a simple websocket server that broadcasts a + * message to all connected clients every 5 seconds. It also responds to + * incoming text messages. See the `simple-example.html` file which is served + * when you open your browser to http://localhost:8080. + */ module examples.simple_example; import handy_http_transport; @@ -15,7 +21,7 @@ import core.thread; class MyMessageHandler : WebSocketMessageHandler { private bool closed = false; - override void onConnectionEstablished(WebSocketConnection conn) { + override void onConnectionEstablished(WebSocketConnection conn, in ServerHttpRequest req) { info("Connection established."); import photon : go; go(() { @@ -47,6 +53,28 @@ class MyMessageHandler : WebSocketMessageHandler { } void main() { - HttpRequestHandler handler = new WebSocketRequestHandler(new MyMessageHandler()); + // Create a websocket request handler that will accept incoming websocket + // connections, and use the given message handler to handle any events. + HttpRequestHandler wsHandler = new WebSocketRequestHandler(new MyMessageHandler()); + + // Create the main HTTP request handler that will determine whether to + // open a websocket connection or serve the HTML file, depending on the + // request URL. + HttpRequestHandler handler = HttpRequestHandler.of((ref ServerHttpRequest req, ref ServerHttpResponse resp) { + if (req.url == "/ws") { + // Handle websocket requests. + wsHandler.handle(req, resp); + } else { + // Serve the HTML file. + import std.conv : to; + import std.file : readText; + const html = readText("simple-example.html"); + resp.headers.add("Content-Type", "text/html"); + resp.headers.add("Content-Length", html.length.to!string); + resp.outputStream.writeToStream(cast(ubyte[]) html); + } + }); + + // Start the server with all default settings. new Http1Transport(handler).start(); } diff --git a/examples/simple-example.html b/examples/simple-example.html new file mode 100644 index 0000000..b8c3a44 --- /dev/null +++ b/examples/simple-example.html @@ -0,0 +1,41 @@ + + + + + Simple Websocket Example + + + + +

Simple Websocket Example

+ + + + + \ No newline at end of file diff --git a/source/handy_http_websockets/components.d b/source/handy_http_websockets/components.d index b0a4d7e..79d1556 100644 --- a/source/handy_http_websockets/components.d +++ b/source/handy_http_websockets/components.d @@ -1,6 +1,7 @@ module handy_http_websockets.components; import handy_http_websockets.connection; +import handy_http_primitives.request : ServerHttpRequest; /** * An exception that's thrown if an unexpected situation arises while dealing @@ -8,6 +9,7 @@ import handy_http_websockets.connection; */ class WebSocketException : Exception { import std.exception : basicExceptionCtors; + import handy_http_primitives.request; mixin basicExceptionCtors; } @@ -47,8 +49,9 @@ abstract class WebSocketMessageHandler { * Called when a new websocket connection is established. * Params: * conn = The new connection. + * request = The HTTP request that initiated the connection. */ - void onConnectionEstablished(WebSocketConnection conn) {} + void onConnectionEstablished(WebSocketConnection conn, in ServerHttpRequest request) {} /** * Called when a text message is received. diff --git a/source/handy_http_websockets/connection.d b/source/handy_http_websockets/connection.d index d0b3523..b2d3a3d 100644 --- a/source/handy_http_websockets/connection.d +++ b/source/handy_http_websockets/connection.d @@ -36,6 +36,10 @@ class WebSocketConnection { this.id = randomUUID(); } + /** + * Gets the message handler that handles events for this connection. + * Returns: The message handler. + */ WebSocketMessageHandler getMessageHandler() { return this.messageHandler; } diff --git a/source/handy_http_websockets/handler.d b/source/handy_http_websockets/handler.d index c257fa2..3b738ff 100644 --- a/source/handy_http_websockets/handler.d +++ b/source/handy_http_websockets/handler.d @@ -17,10 +17,25 @@ import handy_http_websockets.manager : webSocketManager; class WebSocketRequestHandler : HttpRequestHandler { private WebSocketMessageHandler messageHandler; + /** + * Constructs a request handler that will use the given message handler to + * deal with events from any websocket connections that are established. + * Params: + * messageHandler = The message handler to use. + */ this(WebSocketMessageHandler messageHandler) { this.messageHandler = messageHandler; } + /** + * Handles an incoming HTTP request and tries to establish a websocket + * connection by first verifying the request, then sending a switching- + * protocols response, and finally registering the new connection with the + * websocket manager. + * Params: + * request = The request to read from. + * response = The response to write to. + */ void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) { auto verification = verifyWebSocketRequest(request); if (verification == RequestVerificationResponse.INVALID_HTTP_METHOD) { @@ -36,7 +51,7 @@ class WebSocketRequestHandler : HttpRequestHandler { messageHandler, request.inputStream, response.outputStream - )); + ), request); } } diff --git a/source/handy_http_websockets/manager.d b/source/handy_http_websockets/manager.d index c441ccc..e6486e4 100644 --- a/source/handy_http_websockets/manager.d +++ b/source/handy_http_websockets/manager.d @@ -6,17 +6,28 @@ import std.uuid; import streams; import slf4d; import photon : go; +import handy_http_primitives.request : ServerHttpRequest; import handy_http_websockets.connection; import handy_http_websockets.components; import handy_http_websockets.frame; +/** + * Global singleton websocket manager that handles all websocket connections. + * Generally, the `addConnection` method will be called by a `WebSocketRequestHandler` + * that you've registered in your server, so users will most often use the + * manager to access the set of connected clients, and broadcast messages to + * them. + */ __gshared WebSocketManager webSocketManager; static this() { webSocketManager = new WebSocketManager(); } +/** + * The websocket manager is responsible for managing all websocket connections. + */ class WebSocketManager { private WebSocketConnection[UUID] connections; private ReadWriteMutex connectionsMutex; @@ -25,36 +36,82 @@ class WebSocketManager { connectionsMutex = new ReadWriteMutex(); } - void addConnection(WebSocketConnection conn) { + /** + * Adds a connection to the manager and starts listening for messages. + * Usually only called by a `WebSocketRequestHandler`. + * Params: + * conn = The connection to add. + * request = The HTTP request that initiated the connection. + */ + void addConnection(WebSocketConnection conn, in ServerHttpRequest request) { synchronized(connectionsMutex.writer) { connections[conn.id] = conn; } go(() => connectionHandler(conn)); - conn.getMessageHandler().onConnectionEstablished(conn); + conn.getMessageHandler().onConnectionEstablished(conn, request); + debugF!"Added websocket connection: %s"(conn.id.toString()); } + /** + * Removes a websocket connection from the manager and closes it. This is + * called automatically if the client sends a CLOSE frame, but you can also + * call it yourself. + * Params: + * conn = + */ void removeConnection(WebSocketConnection conn) { synchronized(connectionsMutex.writer) { connections.remove(conn.id); } conn.close(); + debugF!"Removed websocket connection: %s"(conn.id.toString()); } + /** + * Broadcasts a message to all connected clients. + * Params: + * text = The text to send to all clients. + */ void broadcast(string text) { + debugF!"Broadcasting %d-length text message to all clients."(text.length); synchronized(connectionsMutex.reader) { foreach (id, conn; connections) { try { conn.sendTextMessage(text); } catch (WebSocketException e) { - warnF!"Failed to broadcast to client %s."(id.toString()); + warnF!"Failed to broadcast to client %s: %s"(id.toString(), e.msg); + } + } + } + } + + /** + * Broadcasts a binary message to all connected clients. + * Params: + * data = The binary data to send to all clients. + */ + void broadcast(ubyte[] data) { + debugF!"Broadcasting %d bytes of binary data to all clients."(data.length); + synchronized(connectionsMutex.reader) { + foreach (id, conn; connections) { + try { + conn.sendBinaryMessage(data); + } catch (WebSocketException e) { + warnF!"Failed to broadcast to client %s: %s"(id.toString(), e.msg); } } } } } -void connectionHandler(WebSocketConnection conn) { - infoF!"Started routine to monitor websocket connection %s."(conn.id.toString()); +/** + * Internal routine that runs in a fiber, and handles an individual websocket + * connection by listening for messages. + * Params: + * conn = The connection to handle. + */ +private void connectionHandler(WebSocketConnection conn) { + traceF!"Started routine to monitor websocket connection %s."(conn.id.toString()); bool running = true; while (running) { try { @@ -82,10 +139,16 @@ void connectionHandler(WebSocketConnection conn) { running = false; } } - infoF!"Routine to monitor websocket connection %s has ended."(conn.id.toString()); + traceF!"Routine to monitor websocket connection %s has ended."(conn.id.toString()); } -void handleClientDataFrame(WebSocketConnection conn, WebSocketFrame f) { +/** + * Handles a websocket data frame (text or binary). + * Params: + * conn = The connection from which the frame was received. + * f = The frame that was received. + */ +private void handleClientDataFrame(WebSocketConnection conn, WebSocketFrame f) { bool isText = f.opcode == WebSocketFrameOpcode.TEXT_FRAME; ubyte[] payload = f.payload.dup; while (!f.finalFragment) {