Added working example.
This commit is contained in:
parent
fe5c31cd2f
commit
40e0bd152f
4
dub.json
4
dub.json
|
@ -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"
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue