Got websockets working with http1transport.
This commit is contained in:
parent
6961a310a4
commit
6faf8ca8e9
5
dub.json
5
dub.json
|
@ -4,9 +4,8 @@
|
||||||
],
|
],
|
||||||
"copyright": "Copyright © 2025, Andrew Lalis",
|
"copyright": "Copyright © 2025, Andrew Lalis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handy-http-primitives": "~>1.6",
|
"handy-http-primitives": "~>1.7",
|
||||||
"slf4d": "~>4",
|
"slf4d": "~>4"
|
||||||
"photon": "~>0.10"
|
|
||||||
},
|
},
|
||||||
"description": "Websocket implementation for Handy-Http.",
|
"description": "Websocket implementation for Handy-Http.",
|
||||||
"license": "CC0",
|
"license": "CC0",
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
{
|
{
|
||||||
"fileVersion": 1,
|
"fileVersion": 1,
|
||||||
"versions": {
|
"versions": {
|
||||||
"handy-http-primitives": "1.6.0",
|
"handy-http-primitives": "1.7.0",
|
||||||
"photon": "0.11.0",
|
|
||||||
"sharded-map": "2.7.0",
|
|
||||||
"slf4d": "4.1.1",
|
"slf4d": "4.1.1",
|
||||||
"streams": "3.6.0"
|
"streams": "3.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
simple-example
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env dub
|
#!/usr/bin/env dub
|
||||||
/+ dub.sdl:
|
/+ dub.sdl:
|
||||||
dependency "handy-http-transport" version="~>1.1"
|
dependency "handy-http-transport" path="../../transport"
|
||||||
dependency "handy-http-websockets" path="../"
|
dependency "handy-http-websockets" path="../"
|
||||||
+/
|
+/
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import handy_http_transport;
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import handy_http_websockets;
|
import handy_http_websockets;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
import slf4d.default_provider;
|
||||||
import core.thread;
|
import core.thread;
|
||||||
|
|
||||||
class MyMessageHandler : WebSocketMessageHandler {
|
class MyMessageHandler : WebSocketMessageHandler {
|
||||||
|
@ -23,18 +24,18 @@ class MyMessageHandler : WebSocketMessageHandler {
|
||||||
|
|
||||||
override void onConnectionEstablished(WebSocketConnection conn, in ServerHttpRequest req) {
|
override void onConnectionEstablished(WebSocketConnection conn, in ServerHttpRequest req) {
|
||||||
info("Connection established.");
|
info("Connection established.");
|
||||||
import photon : go;
|
this.closed = false;
|
||||||
go(() {
|
new Thread(() {
|
||||||
while (!closed) {
|
while (!this.closed) {
|
||||||
info("Broadcasting...");
|
info("Broadcasting...");
|
||||||
webSocketManager.broadcast("BROADCAST TEST!");
|
conn.getRequestHandler().broadcast("BROADCAST TEST!");
|
||||||
Thread.sleep(seconds(5));
|
Thread.sleep(seconds(5));
|
||||||
}
|
}
|
||||||
});
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
override void onTextMessage(WebSocketTextMessage msg) {
|
override void onTextMessage(WebSocketTextMessage msg) {
|
||||||
infoF!"Got a text message: %s"(msg.payload);
|
infoF!"Got a text message from connection %s: %s"(msg.conn.id, msg.payload);
|
||||||
msg.conn.sendTextMessage("test");
|
msg.conn.sendTextMessage("test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,5 +77,5 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the server with all default settings.
|
// Start the server with all default settings.
|
||||||
new Http1Transport(handler).start();
|
new TaskPoolHttp1Transport(handler).start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import std.socket;
|
||||||
|
|
||||||
import handy_http_websockets.components;
|
import handy_http_websockets.components;
|
||||||
import handy_http_websockets.frame;
|
import handy_http_websockets.frame;
|
||||||
|
import handy_http_websockets.handler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All the data that represents a WebSocket connection tracked by the
|
* All the data that represents a WebSocket connection tracked by the
|
||||||
|
@ -29,8 +30,19 @@ class WebSocketConnection {
|
||||||
*/
|
*/
|
||||||
private WebSocketMessageHandler messageHandler;
|
private WebSocketMessageHandler messageHandler;
|
||||||
|
|
||||||
this(WebSocketMessageHandler messageHandler, InputStream!ubyte inputStream, OutputStream!ubyte outputStream) {
|
/**
|
||||||
|
* The HTTP request handler that created and owns this connection.
|
||||||
|
*/
|
||||||
|
private WebSocketRequestHandler requestHandler;
|
||||||
|
|
||||||
|
this(
|
||||||
|
WebSocketMessageHandler messageHandler,
|
||||||
|
WebSocketRequestHandler requestHandler,
|
||||||
|
InputStream!ubyte inputStream,
|
||||||
|
OutputStream!ubyte outputStream
|
||||||
|
) {
|
||||||
this.messageHandler = messageHandler;
|
this.messageHandler = messageHandler;
|
||||||
|
this.requestHandler = requestHandler;
|
||||||
this.inputStream = inputStream;
|
this.inputStream = inputStream;
|
||||||
this.outputStream = outputStream;
|
this.outputStream = outputStream;
|
||||||
this.id = randomUUID();
|
this.id = randomUUID();
|
||||||
|
@ -44,6 +56,15 @@ class WebSocketConnection {
|
||||||
return this.messageHandler;
|
return this.messageHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the `WebSocketRequestHandler` that owns this connection. Use this
|
||||||
|
* to interact with the set of connections managed by that handler.
|
||||||
|
* Returns: The request handler.
|
||||||
|
*/
|
||||||
|
WebSocketRequestHandler getRequestHandler() {
|
||||||
|
return this.requestHandler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a text message to the connected client.
|
* Sends a text message to the connected client.
|
||||||
* Params:
|
* Params:
|
||||||
|
|
|
@ -106,6 +106,11 @@ void sendWebSocketPongFrame(S)(S stream, ubyte[] pingPayload) if (isByteOutputSt
|
||||||
* frame = The frame to write.
|
* frame = The frame to write.
|
||||||
*/
|
*/
|
||||||
void sendWebSocketFrame(S)(S stream, WebSocketFrame frame) if (isByteOutputStream!S) {
|
void sendWebSocketFrame(S)(S stream, WebSocketFrame frame) if (isByteOutputStream!S) {
|
||||||
|
traceF!"Sending websocket frame: opcode=%s, final=%s, payload.length=%d"(
|
||||||
|
frame.opcode,
|
||||||
|
frame.finalFragment,
|
||||||
|
frame.payload.length
|
||||||
|
);
|
||||||
static if (isPointerToStream!S) {
|
static if (isPointerToStream!S) {
|
||||||
S ptr = stream;
|
S ptr = stream;
|
||||||
} else {
|
} else {
|
||||||
|
@ -160,7 +165,7 @@ WebSocketFrame receiveWebSocketFrame(S)(S stream) if (isByteInputStream!S) {
|
||||||
immutable ubyte maskAndLength = readDataOrThrow!(ubyte)(ptr);
|
immutable ubyte maskAndLength = readDataOrThrow!(ubyte)(ptr);
|
||||||
immutable bool payloadMasked = (maskAndLength & 128) > 0;
|
immutable bool payloadMasked = (maskAndLength & 128) > 0;
|
||||||
immutable ubyte initialPayloadLength = maskAndLength & 127;
|
immutable ubyte initialPayloadLength = maskAndLength & 127;
|
||||||
debugF!"Websocket data frame Mask bit = %s, Initial payload length = %d"(payloadMasked, initialPayloadLength);
|
traceF!"Websocket data frame Mask bit = %s, Initial payload length = %d"(payloadMasked, initialPayloadLength);
|
||||||
size_t payloadLength = readPayloadLength(initialPayloadLength, ptr);
|
size_t payloadLength = readPayloadLength(initialPayloadLength, ptr);
|
||||||
if (isControlFrame && payloadLength > 125) {
|
if (isControlFrame && payloadLength > 125) {
|
||||||
throw new WebSocketException("Control frame payload is too large.");
|
throw new WebSocketException("Control frame payload is too large.");
|
||||||
|
@ -168,7 +173,7 @@ WebSocketFrame receiveWebSocketFrame(S)(S stream) if (isByteInputStream!S) {
|
||||||
|
|
||||||
ubyte[4] maskingKey;
|
ubyte[4] maskingKey;
|
||||||
if (payloadMasked) maskingKey = readDataOrThrow!(ubyte[4])(ptr);
|
if (payloadMasked) maskingKey = readDataOrThrow!(ubyte[4])(ptr);
|
||||||
debugF!"Receiving websocket frame: (FIN=%s,OP=%d,MASK=%s,LENGTH=%d)"(
|
traceF!"Receiving websocket frame: (FIN=%s,OP=%d,MASK=%s,LENGTH=%d)"(
|
||||||
finalFragment,
|
finalFragment,
|
||||||
opcode,
|
opcode,
|
||||||
payloadMasked,
|
payloadMasked,
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
module handy_http_websockets.handler;
|
module handy_http_websockets.handler;
|
||||||
|
|
||||||
|
import core.sync.rwmutex : ReadWriteMutex;
|
||||||
|
import std.uuid;
|
||||||
|
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import streams;
|
import streams;
|
||||||
|
|
||||||
import handy_http_websockets.components;
|
import handy_http_websockets.components;
|
||||||
import handy_http_websockets.connection;
|
import handy_http_websockets.connection;
|
||||||
import handy_http_websockets.manager : webSocketManager;
|
import handy_http_websockets.frame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HTTP request handler implementation that's used as the entrypoint for
|
* An HTTP request handler implementation that's used as the entrypoint for
|
||||||
|
@ -17,6 +20,9 @@ import handy_http_websockets.manager : webSocketManager;
|
||||||
class WebSocketRequestHandler : HttpRequestHandler {
|
class WebSocketRequestHandler : HttpRequestHandler {
|
||||||
private WebSocketMessageHandler messageHandler;
|
private WebSocketMessageHandler messageHandler;
|
||||||
|
|
||||||
|
private shared WebSocketConnection[UUID] connections;
|
||||||
|
private shared ReadWriteMutex connectionsMutex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a request handler that will use the given message handler to
|
* Constructs a request handler that will use the given message handler to
|
||||||
* deal with events from any websocket connections that are established.
|
* deal with events from any websocket connections that are established.
|
||||||
|
@ -25,6 +31,7 @@ class WebSocketRequestHandler : HttpRequestHandler {
|
||||||
*/
|
*/
|
||||||
this(WebSocketMessageHandler messageHandler) {
|
this(WebSocketMessageHandler messageHandler) {
|
||||||
this.messageHandler = messageHandler;
|
this.messageHandler = messageHandler;
|
||||||
|
this.connectionsMutex = new shared ReadWriteMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,20 +46,168 @@ class WebSocketRequestHandler : HttpRequestHandler {
|
||||||
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) {
|
||||||
|
warnF!"Received a %s request to a websocket request handler. Only GET requests are expected."(
|
||||||
|
request.method
|
||||||
|
);
|
||||||
sendErrorResponse(response, HttpStatus.METHOD_NOT_ALLOWED, "Only GET requests are allowed.");
|
sendErrorResponse(response, HttpStatus.METHOD_NOT_ALLOWED, "Only GET requests are allowed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (verification == RequestVerificationResponse.MISSING_KEY) {
|
if (verification == RequestVerificationResponse.MISSING_KEY) {
|
||||||
|
warn("Received a request to a websocket request handler, but is " ~
|
||||||
|
"missing the required \"Sec-WebSocket-Key\" header.");
|
||||||
sendErrorResponse(response, HttpStatus.BAD_REQUEST, "Missing Sec-WebSocket-Key header.");
|
sendErrorResponse(response, HttpStatus.BAD_REQUEST, "Missing Sec-WebSocket-Key header.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendSwitchingProtocolsResponse(request, response);
|
sendSwitchingProtocolsResponse(request, response);
|
||||||
webSocketManager.addConnection(new WebSocketConnection(
|
addConnection(new WebSocketConnection(
|
||||||
messageHandler,
|
messageHandler,
|
||||||
|
this,
|
||||||
request.inputStream,
|
request.inputStream,
|
||||||
response.outputStream
|
response.outputStream
|
||||||
), request);
|
), request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of active connections.
|
||||||
|
* Returns: The number of active connections.
|
||||||
|
*/
|
||||||
|
size_t connectionCount() {
|
||||||
|
synchronized(connectionsMutex.reader) {
|
||||||
|
return connections.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a connection to the manager and starts listening for messages.
|
||||||
|
* Params:
|
||||||
|
* conn = The connection to add.
|
||||||
|
* request = The HTTP request that initiated the connection.
|
||||||
|
*/
|
||||||
|
void addConnection(WebSocketConnection conn, in ServerHttpRequest request) {
|
||||||
|
import core.thread;
|
||||||
|
synchronized(connectionsMutex.writer) {
|
||||||
|
connections[conn.id] = cast(shared(WebSocketConnection)) conn;
|
||||||
|
}
|
||||||
|
new Thread(() => connectionHandler(conn)).start();
|
||||||
|
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 = The connection to remove.
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
synchronized(connectionsMutex.reader) {
|
||||||
|
debugF!"Broadcasting %d-length text message to %d clients."(text.length, connections.length);
|
||||||
|
foreach (id, conn; connections) {
|
||||||
|
try {
|
||||||
|
(cast(WebSocketConnection) conn).sendTextMessage(text);
|
||||||
|
} catch (WebSocketException e) {
|
||||||
|
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) {
|
||||||
|
synchronized(connectionsMutex.reader) {
|
||||||
|
debugF!"Broadcasting %d bytes of binary data to %d clients."(data.length, connections.length);
|
||||||
|
foreach (id, conn; connections) {
|
||||||
|
try {
|
||||||
|
(cast(WebSocketConnection) conn).sendBinaryMessage(data);
|
||||||
|
} catch (WebSocketException e) {
|
||||||
|
warnF!"Failed to broadcast to client %s: %s"(id.toString(), e.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
debugF!"Started routine to monitor websocket connection %s."(conn.id.toString());
|
||||||
|
bool running = true;
|
||||||
|
while (running) {
|
||||||
|
try {
|
||||||
|
WebSocketFrame f = receiveWebSocketFrame(conn.inputStream);
|
||||||
|
switch (f.opcode) {
|
||||||
|
case WebSocketFrameOpcode.CONNECTION_CLOSE:
|
||||||
|
removeConnection(conn);
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
case WebSocketFrameOpcode.PING:
|
||||||
|
sendWebSocketPongFrame(conn.outputStream, f.payload);
|
||||||
|
break;
|
||||||
|
case WebSocketFrameOpcode.TEXT_FRAME:
|
||||||
|
case WebSocketFrameOpcode.BINARY_FRAME:
|
||||||
|
handleClientDataFrame(conn, f);
|
||||||
|
break;
|
||||||
|
case WebSocketFrameOpcode.CONTINUATION:
|
||||||
|
warn("Got websocket CONTINUATION frame when not expecting one. Ignoring.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
error(e);
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugF!"Routine to monitor websocket connection %s has ended."(conn.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
f = receiveWebSocketFrame(conn.inputStream);
|
||||||
|
if (f.opcode != WebSocketFrameOpcode.CONTINUATION) {
|
||||||
|
removeConnection(conn);
|
||||||
|
warnF!"Received invalid websocket frame opcode %s when expecting a CONTINUATION frame."(
|
||||||
|
f.opcode
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload ~= f.payload;
|
||||||
|
}
|
||||||
|
if (isText) {
|
||||||
|
conn.getMessageHandler().onTextMessage(WebSocketTextMessage(conn, cast(string) payload));
|
||||||
|
} else {
|
||||||
|
conn.getMessageHandler().onBinaryMessage(WebSocketBinaryMessage(conn, payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum RequestVerificationResponse {
|
private enum RequestVerificationResponse {
|
||||||
|
@ -73,15 +228,7 @@ private RequestVerificationResponse verifyWebSocketRequest(in ServerHttpRequest
|
||||||
|
|
||||||
private void sendErrorResponse(ref ServerHttpResponse response, HttpStatus status, string msg) {
|
private void sendErrorResponse(ref ServerHttpResponse response, HttpStatus status, string msg) {
|
||||||
response.status = status;
|
response.status = status;
|
||||||
ubyte[] data = cast(ubyte[]) msg;
|
response.writeBodyString(msg);
|
||||||
import std.conv : to;
|
|
||||||
response.headers.add("Content-Type", "text/plain");
|
|
||||||
response.headers.add("Content-Length", data.length.to!string);
|
|
||||||
auto result = response.outputStream.writeToStream(data);
|
|
||||||
if (result.hasError) {
|
|
||||||
StreamError e = result.error;
|
|
||||||
warnF!"Failed to send HTTP error response: %s"(e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSwitchingProtocolsResponse(in ServerHttpRequest request, ref ServerHttpResponse response) {
|
private void sendSwitchingProtocolsResponse(in ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
module handy_http_websockets.manager;
|
|
||||||
|
|
||||||
import core.thread.osthread : Thread;
|
|
||||||
import core.sync.rwmutex : ReadWriteMutex;
|
|
||||||
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;
|
|
||||||
|
|
||||||
this() {
|
|
||||||
connectionsMutex = new ReadWriteMutex();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, 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: %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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
WebSocketFrame f = receiveWebSocketFrame(conn.inputStream);
|
|
||||||
switch (f.opcode) {
|
|
||||||
case WebSocketFrameOpcode.CONNECTION_CLOSE:
|
|
||||||
webSocketManager.removeConnection(conn);
|
|
||||||
running = false;
|
|
||||||
break;
|
|
||||||
case WebSocketFrameOpcode.PING:
|
|
||||||
sendWebSocketPongFrame(conn.outputStream, f.payload);
|
|
||||||
break;
|
|
||||||
case WebSocketFrameOpcode.TEXT_FRAME:
|
|
||||||
case WebSocketFrameOpcode.BINARY_FRAME:
|
|
||||||
handleClientDataFrame(conn, f);
|
|
||||||
break;
|
|
||||||
case WebSocketFrameOpcode.CONTINUATION:
|
|
||||||
warn("Got websocket CONTINUATION frame when not expecting one. Ignoring.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
error(e);
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
traceF!"Routine to monitor websocket connection %s has ended."(conn.id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
f = receiveWebSocketFrame(conn.inputStream);
|
|
||||||
if (f.opcode != WebSocketFrameOpcode.CONTINUATION) {
|
|
||||||
webSocketManager.removeConnection(conn);
|
|
||||||
warnF!"Received invalid websocket frame opcode %s when expecting a CONTINUATION frame."(
|
|
||||||
f.opcode
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
payload ~= f.payload;
|
|
||||||
}
|
|
||||||
if (isText) {
|
|
||||||
conn.getMessageHandler().onTextMessage(WebSocketTextMessage(conn, cast(string) payload));
|
|
||||||
} else {
|
|
||||||
conn.getMessageHandler().onBinaryMessage(WebSocketBinaryMessage(conn, payload));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,5 +3,4 @@ module handy_http_websockets;
|
||||||
public import handy_http_websockets.components;
|
public import handy_http_websockets.components;
|
||||||
public import handy_http_websockets.connection;
|
public import handy_http_websockets.connection;
|
||||||
public import handy_http_websockets.handler;
|
public import handy_http_websockets.handler;
|
||||||
public import handy_http_websockets.manager;
|
|
||||||
public import handy_http_websockets.frame : WebSocketCloseStatusCode;
|
public import handy_http_websockets.frame : WebSocketCloseStatusCode;
|
||||||
|
|
Loading…
Reference in New Issue