Added working example.

This commit is contained in:
Andrew Lalis 2025-05-22 18:40:19 -04:00
parent fe5c31cd2f
commit 40e0bd152f
8 changed files with 168 additions and 14 deletions

View File

@ -4,11 +4,11 @@
], ],
"copyright": "Copyright © 2025, Andrew Lalis", "copyright": "Copyright © 2025, Andrew Lalis",
"dependencies": { "dependencies": {
"handy-http-primitives": "~>1.5", "handy-http-primitives": "~>1.6",
"slf4d": "~>3", "slf4d": "~>3",
"photon": "~>0.10" "photon": "~>0.10"
}, },
"description": "mplementati", "description": "Websocket implementation for Handy-Http.",
"license": "CC0", "license": "CC0",
"name": "handy-http-websockets" "name": "handy-http-websockets"
} }

View File

@ -1,7 +1,7 @@
{ {
"fileVersion": 1, "fileVersion": 1,
"versions": { "versions": {
"handy-http-primitives": "1.5.0", "handy-http-primitives": "1.6.0",
"photon": "0.10.2", "photon": "0.10.2",
"sharded-map": "2.7.0", "sharded-map": "2.7.0",
"slf4d": "3.0.1", "slf4d": "3.0.1",

View File

@ -4,6 +4,12 @@
dependency "handy-http-websockets" path="../" 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; module examples.simple_example;
import handy_http_transport; import handy_http_transport;
@ -15,7 +21,7 @@ import core.thread;
class MyMessageHandler : WebSocketMessageHandler { class MyMessageHandler : WebSocketMessageHandler {
private bool closed = false; private bool closed = false;
override void onConnectionEstablished(WebSocketConnection conn) { override void onConnectionEstablished(WebSocketConnection conn, in ServerHttpRequest req) {
info("Connection established."); info("Connection established.");
import photon : go; import photon : go;
go(() { go(() {
@ -47,6 +53,28 @@ class MyMessageHandler : WebSocketMessageHandler {
} }
void main() { 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(); new Http1Transport(handler).start();
} }

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Simple Websocket Example</title>
<style>
#output {
width: 100%;
height: 500px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>Simple Websocket Example</h1>
<textarea id="output"></textarea>
<script>
const output = document.getElementById('output');
window.onload = () => {
output.value = '';
}
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = () => {
output.value += 'WebSocket connection opened.\n';
ws.send('Hello, server!');
};
ws.onmessage = (event) => {
output.value += `Message from server: ${event.data}\n`;
};
ws.onclose = () => {
output.value += 'WebSocket connection closed.\n';
};
ws.onerror = (error) => {
output.value += `WebSocket error: ${error.message}\n`;
};
</script>
</body>
</html>

View File

@ -1,6 +1,7 @@
module handy_http_websockets.components; module handy_http_websockets.components;
import handy_http_websockets.connection; import handy_http_websockets.connection;
import handy_http_primitives.request : ServerHttpRequest;
/** /**
* An exception that's thrown if an unexpected situation arises while dealing * An exception that's thrown if an unexpected situation arises while dealing
@ -8,6 +9,7 @@ import handy_http_websockets.connection;
*/ */
class WebSocketException : Exception { class WebSocketException : Exception {
import std.exception : basicExceptionCtors; import std.exception : basicExceptionCtors;
import handy_http_primitives.request;
mixin basicExceptionCtors; mixin basicExceptionCtors;
} }
@ -47,8 +49,9 @@ abstract class WebSocketMessageHandler {
* Called when a new websocket connection is established. * Called when a new websocket connection is established.
* Params: * Params:
* conn = The new connection. * 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. * Called when a text message is received.

View File

@ -36,6 +36,10 @@ class WebSocketConnection {
this.id = randomUUID(); this.id = randomUUID();
} }
/**
* Gets the message handler that handles events for this connection.
* Returns: The message handler.
*/
WebSocketMessageHandler getMessageHandler() { WebSocketMessageHandler getMessageHandler() {
return this.messageHandler; return this.messageHandler;
} }

View File

@ -17,10 +17,25 @@ import handy_http_websockets.manager : webSocketManager;
class WebSocketRequestHandler : HttpRequestHandler { class WebSocketRequestHandler : HttpRequestHandler {
private WebSocketMessageHandler messageHandler; 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(WebSocketMessageHandler messageHandler) {
this.messageHandler = 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) { void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) {
auto verification = verifyWebSocketRequest(request); auto verification = verifyWebSocketRequest(request);
if (verification == RequestVerificationResponse.INVALID_HTTP_METHOD) { if (verification == RequestVerificationResponse.INVALID_HTTP_METHOD) {
@ -36,7 +51,7 @@ class WebSocketRequestHandler : HttpRequestHandler {
messageHandler, messageHandler,
request.inputStream, request.inputStream,
response.outputStream response.outputStream
)); ), request);
} }
} }

View File

@ -6,17 +6,28 @@ import std.uuid;
import streams; import streams;
import slf4d; import slf4d;
import photon : go; import photon : go;
import handy_http_primitives.request : ServerHttpRequest;
import handy_http_websockets.connection; import handy_http_websockets.connection;
import handy_http_websockets.components; import handy_http_websockets.components;
import handy_http_websockets.frame; 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; __gshared WebSocketManager webSocketManager;
static this() { static this() {
webSocketManager = new WebSocketManager(); webSocketManager = new WebSocketManager();
} }
/**
* The websocket manager is responsible for managing all websocket connections.
*/
class WebSocketManager { class WebSocketManager {
private WebSocketConnection[UUID] connections; private WebSocketConnection[UUID] connections;
private ReadWriteMutex connectionsMutex; private ReadWriteMutex connectionsMutex;
@ -25,36 +36,82 @@ class WebSocketManager {
connectionsMutex = new ReadWriteMutex(); 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) { synchronized(connectionsMutex.writer) {
connections[conn.id] = conn; connections[conn.id] = conn;
} }
go(() => connectionHandler(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) { void removeConnection(WebSocketConnection conn) {
synchronized(connectionsMutex.writer) { synchronized(connectionsMutex.writer) {
connections.remove(conn.id); connections.remove(conn.id);
} }
conn.close(); 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) { void broadcast(string text) {
debugF!"Broadcasting %d-length text message to all clients."(text.length);
synchronized(connectionsMutex.reader) { synchronized(connectionsMutex.reader) {
foreach (id, conn; connections) { foreach (id, conn; connections) {
try { try {
conn.sendTextMessage(text); conn.sendTextMessage(text);
} catch (WebSocketException e) { } 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; bool running = true;
while (running) { while (running) {
try { try {
@ -82,10 +139,16 @@ void connectionHandler(WebSocketConnection conn) {
running = false; 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; bool isText = f.opcode == WebSocketFrameOpcode.TEXT_FRAME;
ubyte[] payload = f.payload.dup; ubyte[] payload = f.payload.dup;
while (!f.finalFragment) { while (!f.finalFragment) {