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) {