Create WebSocket Server
- Created a WebSocket server with connection handling and message broadcasting - Added frontend files including HTML, TypeScript, and CSS for a WebSocket test client - Configured package.json for both server and frontend with necessary scripts and dependencies - Introduced Prettier configuration for code formatting - Established a basic structure for handling WebSocket connections and messages
This commit is contained in:
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSameLine": true,
|
||||
"bracketSpacing": false,
|
||||
"printWidth": 180,
|
||||
"semi": true,
|
||||
"singleAttributePerLine": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
||||
13
frontend/web/index.html
Normal file
13
frontend/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
frontend/web/package.json
Normal file
15
frontend/web/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
13
frontend/web/src/main.ts
Normal file
13
frontend/web/src/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const webSocket = new WebSocket('http://localhost:3000');
|
||||
|
||||
webSocket.onerror = (e: Event) => {
|
||||
console.error(`${new Date().toISOString()} [WebSocket] error [%o]`, e);
|
||||
};
|
||||
|
||||
webSocket.onopen = () => {
|
||||
console.log(`${new Date().toISOString()} [WebSocket] open`);
|
||||
};
|
||||
|
||||
webSocket.onmessage = () => {
|
||||
console.log(`${new Date().toISOString()} [WebSocket] message!`);
|
||||
};
|
||||
4
frontend/web/src/style.css
Normal file
4
frontend/web/src/style.css
Normal file
@@ -0,0 +1,4 @@
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
1
frontend/web/src/vite-env.d.ts
vendored
Normal file
1
frontend/web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
25
frontend/web/tsconfig.json
Normal file
25
frontend/web/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
107
inlined.html
Normal file
107
inlined.html
Normal file
@@ -0,0 +1,107 @@
|
||||
|
||||
private getTestPage(): string {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket Test</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; margin: 10px 0; }
|
||||
#input { width: 70%; padding: 5px; }
|
||||
button { padding: 5px 10px; margin-left: 5px; }
|
||||
.status { padding: 5px; margin: 5px 0; border-radius: 3px; }
|
||||
.connected { background-color: #d4edda; color: #155724; }
|
||||
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Test Client</h1>
|
||||
<div id="status" class="status disconnected">Disconnected</div>
|
||||
<div id="messages"></div>
|
||||
<input type="text" id="input" placeholder="Type a message..." disabled>
|
||||
<button id="send" disabled>Send</button>
|
||||
<button id="connect">Connect</button>
|
||||
<button id="disconnect" disabled>Disconnect</button>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
const status = document.getElementById('status');
|
||||
const messages = document.getElementById('messages');
|
||||
const input = document.getElementById('input');
|
||||
const sendBtn = document.getElementById('send');
|
||||
const connectBtn = document.getElementById('connect');
|
||||
const disconnectBtn = document.getElementById('disconnect');
|
||||
|
||||
function addMessage(message) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = new Date().toLocaleTimeString() + ': ' + message;
|
||||
messages.appendChild(div);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(connected) {
|
||||
if (connected) {
|
||||
status.textContent = 'Connected';
|
||||
status.className = 'status connected';
|
||||
input.disabled = false;
|
||||
sendBtn.disabled = false;
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
} else {
|
||||
status.textContent = 'Disconnected';
|
||||
status.className = 'status disconnected';
|
||||
input.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.onclick = function() {
|
||||
ws = new WebSocket('ws://localhost:3000');
|
||||
|
||||
ws.onopen = function() {
|
||||
addMessage('Connected to WebSocket server');
|
||||
updateStatus(true);
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
addMessage('Received: ' + event.data);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
addMessage('Disconnected from WebSocket server');
|
||||
updateStatus(false);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
addMessage('Error: ' + error);
|
||||
};
|
||||
};
|
||||
|
||||
disconnectBtn.onclick = function() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
sendBtn.onclick = function() {
|
||||
if (ws && input.value) {
|
||||
ws.send(input.value);
|
||||
addMessage('Sent: ' + input.value);
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
input.onkeypress = function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendBtn.click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
1083
package-lock.json
generated
1083
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,14 @@
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:server": "npm run dev --workspace=server"
|
||||
"dev:web": "npm run dev --workspace=web",
|
||||
"dev:server": "npm run dev --workspace=server",
|
||||
"lint:server": "npm run lint --workspace=server",
|
||||
"lint:fix:server": "npm run lint:fix --workspace=server"
|
||||
},
|
||||
"workspaces": [
|
||||
"server"
|
||||
"server",
|
||||
"frontend/web"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "24.3.1",
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node-esm' src/index.ts"
|
||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node-esm' src/index.ts",
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"prelint:fix": "npm run format",
|
||||
"lint:fix": "eslint 'src/**/*.ts' --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techniker-me/logger": "0.0.15"
|
||||
@@ -19,6 +23,7 @@
|
||||
"globals": "14.0.0",
|
||||
"jiti": "2.5.1",
|
||||
"nodemon": "^3.1.0",
|
||||
"prettier": "3.6.2",
|
||||
"ts-node": "^10.9.0",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "8.42.0"
|
||||
|
||||
101
server/public/test-page.html
Normal file
101
server/public/test-page.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket Test</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; margin: 10px 0; }
|
||||
#input { width: 70%; padding: 5px; }
|
||||
button { padding: 5px 10px; margin-left: 5px; }
|
||||
.status { padding: 5px; margin: 5px 0; border-radius: 3px; }
|
||||
.connected { background-color: #d4edda; color: #155724; }
|
||||
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Test Client</h1>
|
||||
<div id="status" class="status disconnected">Disconnected</div>
|
||||
<div id="messages"></div>
|
||||
<input type="text" id="input" placeholder="Type a message..." disabled>
|
||||
<button id="send" disabled>Send</button>
|
||||
<button id="connect">Connect</button>
|
||||
<button id="disconnect" disabled>Disconnect</button>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
const status = document.getElementById('status');
|
||||
const messages = document.getElementById('messages');
|
||||
const input = document.getElementById('input');
|
||||
const sendBtn = document.getElementById('send');
|
||||
const connectBtn = document.getElementById('connect');
|
||||
const disconnectBtn = document.getElementById('disconnect');
|
||||
|
||||
function addMessage(message) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = new Date().toLocaleTimeString() + ': ' + message;
|
||||
messages.appendChild(div);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(connected) {
|
||||
if (connected) {
|
||||
status.textContent = 'Connected';
|
||||
status.className = 'status connected';
|
||||
input.disabled = false;
|
||||
sendBtn.disabled = false;
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
} else {
|
||||
status.textContent = 'Disconnected';
|
||||
status.className = 'status disconnected';
|
||||
input.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
connectBtn.onclick = function() {
|
||||
ws = new WebSocket('ws://localhost:3000');
|
||||
|
||||
ws.onopen = function() {
|
||||
addMessage('Connected to WebSocket server');
|
||||
updateStatus(true);
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
addMessage('Received: ' + event.data);
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
addMessage('Disconnected from WebSocket server');
|
||||
updateStatus(false);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
addMessage('Error: ' + error);
|
||||
};
|
||||
};
|
||||
|
||||
disconnectBtn.onclick = function() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
sendBtn.onclick = function() {
|
||||
if (ws && input.value) {
|
||||
ws.send(input.value);
|
||||
addMessage('Sent: ' + input.value);
|
||||
input.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
input.onkeypress = function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendBtn.click();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,29 +0,0 @@
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import {createServer as createHttpServer, type IncomingMessage, type ServerResponse, type Server as HTTPServer, type ServerOptions as HTTPServerOptions} from 'node:http';
|
||||
|
||||
export class Server {
|
||||
private readonly _logger = LoggerFactory.getLogger('Server');
|
||||
private readonly _http: HTTPServer;
|
||||
|
||||
constructor(serverOptions: HTTPServerOptions) {
|
||||
this._http = createHttpServer(serverOptions, this.httpRequestHandler.bind(this));
|
||||
}
|
||||
|
||||
public async listen(port: number, callback?: (port: number) => void) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this._http.listen(port, () => {
|
||||
resolve();
|
||||
});
|
||||
this._http.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
callback?.(port);
|
||||
}
|
||||
|
||||
private httpRequestHandler(req: IncomingMessage, res: ServerResponse) {
|
||||
this._logger.info(`Received request: ${req.url}`);
|
||||
res.end('Hello World');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {ServerFactory} from './ServerFactory.ts';
|
||||
import {ServerFactory} from './net/ServerFactory.ts';
|
||||
|
||||
const server = ServerFactory.createServer({
|
||||
keepAlive: true,
|
||||
keepAliveTimeout: 10000,
|
||||
keepAliveTimeout: 10000
|
||||
});
|
||||
|
||||
server.listen(3000, (port: number) => {
|
||||
|
||||
70
server/src/net/Server.ts
Normal file
70
server/src/net/Server.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {createServer as createHttpServer} from 'node:http';
|
||||
import type {IncomingMessage, ServerResponse, Server as HTTPServer, ServerOptions as HTTPServerOptions} from 'node:http';
|
||||
import type {IWebSocketServer} from './WebSockets/IWebSocketConnection.ts';
|
||||
import {WebSocketServerFactory} from './WebSockets/WebSocketServerFactory.ts';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export class Server {
|
||||
private readonly _logger = LoggerFactory.getLogger('Server');
|
||||
private readonly _http: HTTPServer;
|
||||
private readonly _webSocketServer: IWebSocketServer;
|
||||
|
||||
constructor(serverOptions: HTTPServerOptions) {
|
||||
this._http = createHttpServer(serverOptions, this.httpRequestHandler.bind(this));
|
||||
this._webSocketServer = WebSocketServerFactory.createWebSocketServer(this._http);
|
||||
this.setupWebSocketHandlers();
|
||||
}
|
||||
|
||||
public async listen(port: number, callback?: (port: number) => void) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this._http.listen(port, () => {
|
||||
resolve();
|
||||
});
|
||||
this._http.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
callback?.(port);
|
||||
}
|
||||
|
||||
private setupWebSocketHandlers(): void {
|
||||
this._webSocketServer.on('connection', connection => {
|
||||
this._logger.info(`WebSocket connection established: [${connection.id}]`);
|
||||
|
||||
connection.on('message', message => {
|
||||
this._logger.debug(`WebSocket message from [${connection.id}]: ${message}`);
|
||||
this._webSocketServer.broadcast(message, connection.id);
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
this._logger.info(`WebSocket connection closed: [${connection.id}]`);
|
||||
});
|
||||
|
||||
connection.on('error', error => {
|
||||
this._logger.error(`WebSocket connection error for [${connection.id}]:`, error);
|
||||
});
|
||||
});
|
||||
|
||||
this._webSocketServer.on('error', error => {
|
||||
this._logger.error('WebSocket server error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
private httpRequestHandler(req: IncomingMessage, res: ServerResponse) {
|
||||
this._logger.info(`Received request: ${req.url}`);
|
||||
|
||||
if (req.url === '/') {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end(fs.readFileSync(path.join(__dirname, '..', '..', 'public', 'test-page.html'), 'utf8'));
|
||||
} else {
|
||||
res.end('Hello World');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Server} from './Server.ts';
|
||||
import { type ServerOptions as HTTPServerOptions } from 'node:http';
|
||||
import {type ServerOptions as HTTPServerOptions} from 'node:http';
|
||||
|
||||
export class ServerFactory {
|
||||
public static createServer(serverOptions: HTTPServerOptions): Server {
|
||||
public static createServer(serverOptions: HTTPServerOptions): Server {
|
||||
return new Server(serverOptions);
|
||||
}
|
||||
|
||||
82
server/src/net/WebSockets/IWebSocketConnection.ts
Normal file
82
server/src/net/WebSockets/IWebSocketConnection.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {type IncomingMessage, type ServerResponse} from 'node:http';
|
||||
|
||||
export interface IWebSocketConnection {
|
||||
readonly id: string;
|
||||
readonly readyState: WebSocketReadyState;
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
on(event: 'close', listener: () => void): this;
|
||||
on(event: 'message', listener: (data: string | Buffer) => void): this;
|
||||
on(event: 'pong', listener: () => void): this;
|
||||
send(data: string | Buffer): void;
|
||||
close(code?: number, reason?: string): void;
|
||||
ping(): void;
|
||||
pong(): void;
|
||||
}
|
||||
|
||||
export interface IWebSocketServer {
|
||||
on(event: 'connection', listener: (connection: IWebSocketConnection) => void): this;
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
on(event: 'close', listener: () => void): this;
|
||||
emit(event: 'connection', connection: IWebSocketConnection): boolean;
|
||||
emit(event: 'error', error: Error): boolean;
|
||||
emit(event: 'close'): boolean;
|
||||
broadcast(message: string | Buffer, excludeConnectionId?: string): void;
|
||||
}
|
||||
|
||||
export interface IWebSocketHandshake {
|
||||
isValid(request: IncomingMessage): boolean;
|
||||
performHandshake(request: IncomingMessage, response: ServerResponse): boolean;
|
||||
}
|
||||
|
||||
export interface IWebSocketFrame {
|
||||
readonly opcode: WebSocketOpcode;
|
||||
readonly payloadLength: number;
|
||||
readonly payload: Buffer;
|
||||
readonly isMasked: boolean;
|
||||
readonly maskingKey: Buffer | undefined;
|
||||
readonly isFin: boolean;
|
||||
|
||||
toBuffer(): Buffer;
|
||||
}
|
||||
|
||||
export interface IWebSocketFrameParser {
|
||||
parse(buffer: Buffer): IWebSocketFrame | null;
|
||||
createFrame(opcode: WebSocketOpcode, payload: Buffer, isMasked?: boolean): IWebSocketFrame;
|
||||
}
|
||||
|
||||
export const WebSocketReadyState = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3
|
||||
} as const;
|
||||
|
||||
export const WebSocketOpcode = {
|
||||
CONTINUATION: 0x0,
|
||||
TEXT: 0x1,
|
||||
BINARY: 0x2,
|
||||
CLOSE: 0x8,
|
||||
PING: 0x9,
|
||||
PONG: 0xa
|
||||
} as const;
|
||||
|
||||
export type WebSocketReadyState = (typeof WebSocketReadyState)[keyof typeof WebSocketReadyState];
|
||||
export type WebSocketOpcode = (typeof WebSocketOpcode)[keyof typeof WebSocketOpcode];
|
||||
|
||||
export const WebSocketCloseCode = {
|
||||
NORMAL_CLOSURE: 1000,
|
||||
GOING_AWAY: 1001,
|
||||
PROTOCOL_ERROR: 1002,
|
||||
UNSUPPORTED_DATA: 1003,
|
||||
NO_STATUS_RECEIVED: 1005,
|
||||
ABNORMAL_CLOSURE: 1006,
|
||||
INVALID_FRAME_PAYLOAD_DATA: 1007,
|
||||
POLICY_VIOLATION: 1008,
|
||||
MESSAGE_TOO_BIG: 1009,
|
||||
MANDATORY_EXTENSION: 1010,
|
||||
INTERNAL_ERROR: 1011,
|
||||
SERVICE_RESTART: 1012,
|
||||
TRY_AGAIN_LATER: 1013,
|
||||
BAD_GATEWAY: 1014,
|
||||
TLS_HANDSHAKE: 1015
|
||||
} as const;
|
||||
212
server/src/net/WebSockets/WebSocketConnection.ts
Normal file
212
server/src/net/WebSockets/WebSocketConnection.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {type Socket} from 'node:net';
|
||||
import {type IWebSocketConnection, WebSocketReadyState, WebSocketOpcode, type IWebSocketFrameParser, type IWebSocketFrame} from './IWebSocketConnection.ts';
|
||||
import {WebSocketFrameParser} from './WebSocketFrame.ts';
|
||||
|
||||
export class WebSocketConnection extends EventEmitter implements IWebSocketConnection {
|
||||
public readonly id: string;
|
||||
public readyState: WebSocketReadyState = WebSocketReadyState.CONNECTING;
|
||||
private readonly socket: Socket;
|
||||
private readonly frameParser: IWebSocketFrameParser;
|
||||
private buffer: Buffer = Buffer.alloc(0);
|
||||
private pingInterval: NodeJS.Timeout | undefined = undefined;
|
||||
private readonly pingIntervalMs: number;
|
||||
|
||||
constructor(socket: Socket, pingIntervalMs: number = 30000) {
|
||||
super();
|
||||
this.id = randomUUID();
|
||||
this.socket = socket;
|
||||
this.frameParser = new WebSocketFrameParser();
|
||||
this.pingIntervalMs = pingIntervalMs;
|
||||
|
||||
this.setupSocketHandlers();
|
||||
this.startPingInterval();
|
||||
|
||||
// Set state to OPEN after handshake is complete
|
||||
this.readyState = WebSocketReadyState.OPEN;
|
||||
}
|
||||
|
||||
private setupSocketHandlers(): void {
|
||||
this.socket.on('data', (data: Buffer) => {
|
||||
this.handleData(data);
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
this.handleClose();
|
||||
});
|
||||
|
||||
this.socket.on('error', (error: Error) => {
|
||||
this.handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
private handleData(data: Buffer): void {
|
||||
// Append new data to buffer
|
||||
this.buffer = Buffer.concat([this.buffer, data]);
|
||||
|
||||
// Try to parse frames from buffer
|
||||
while (this.buffer.length > 0) {
|
||||
const frame = this.frameParser.parse(this.buffer);
|
||||
if (!frame) {
|
||||
break; // Not enough data for a complete frame
|
||||
}
|
||||
|
||||
// Remove parsed frame from buffer
|
||||
const frameLength = this.calculateFrameLength(frame);
|
||||
this.buffer = this.buffer.subarray(frameLength);
|
||||
|
||||
this.handleFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateFrameLength(frame: IWebSocketFrame): number {
|
||||
let length = 2; // Base header length
|
||||
|
||||
if (frame.payloadLength > 65535) {
|
||||
length += 8; // 64-bit length
|
||||
} else if (frame.payloadLength > 125) {
|
||||
length += 2; // 16-bit length
|
||||
}
|
||||
|
||||
if (frame.isMasked) {
|
||||
length += 4; // Masking key
|
||||
}
|
||||
|
||||
length += frame.payloadLength;
|
||||
return length;
|
||||
}
|
||||
|
||||
private handleFrame(frame: IWebSocketFrame): void {
|
||||
switch (frame.opcode) {
|
||||
case WebSocketOpcode.TEXT:
|
||||
this.emit('message', frame.payload.toString('utf8'));
|
||||
break;
|
||||
|
||||
case WebSocketOpcode.BINARY:
|
||||
this.emit('message', frame.payload);
|
||||
break;
|
||||
|
||||
case WebSocketOpcode.CLOSE:
|
||||
this.handleCloseFrame(frame);
|
||||
break;
|
||||
|
||||
case WebSocketOpcode.PING:
|
||||
this.pong();
|
||||
break;
|
||||
|
||||
case WebSocketOpcode.PONG:
|
||||
this.emit('pong');
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown opcode, close connection
|
||||
this.close(1002, 'Unknown opcode');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleCloseFrame(frame: IWebSocketFrame): void {
|
||||
let code = 1000; // Normal closure
|
||||
let reason = '';
|
||||
|
||||
if (frame.payloadLength >= 2) {
|
||||
code = frame.payload.readUInt16BE(0);
|
||||
if (frame.payloadLength > 2) {
|
||||
reason = frame.payload.subarray(2).toString('utf8');
|
||||
}
|
||||
}
|
||||
|
||||
this.close(code, reason);
|
||||
}
|
||||
|
||||
private handleClose(): void {
|
||||
if (this.readyState !== WebSocketReadyState.CLOSED) {
|
||||
this.readyState = WebSocketReadyState.CLOSED;
|
||||
this.stopPingInterval();
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: Error): void {
|
||||
this.emit('error', error);
|
||||
this.close(1011, 'Internal error');
|
||||
}
|
||||
|
||||
send(data: string | Buffer): void {
|
||||
if (this.readyState !== WebSocketReadyState.OPEN) {
|
||||
throw new Error('WebSocket connection is not open');
|
||||
}
|
||||
|
||||
const opcode = typeof data === 'string' ? WebSocketOpcode.TEXT : WebSocketOpcode.BINARY;
|
||||
const payload = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
||||
|
||||
const frame = this.frameParser.createFrame(opcode, payload);
|
||||
this.socket.write(frame.toBuffer());
|
||||
}
|
||||
|
||||
close(code: number = 1000, reason: string = ''): void {
|
||||
if (this.readyState === WebSocketReadyState.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.readyState === WebSocketReadyState.OPEN) {
|
||||
this.readyState = WebSocketReadyState.CLOSING;
|
||||
|
||||
// Send close frame
|
||||
const reasonBuffer = Buffer.from(reason, 'utf8');
|
||||
const payload = Buffer.alloc(2 + reasonBuffer.length);
|
||||
payload.writeUInt16BE(code, 0);
|
||||
reasonBuffer.copy(payload, 2);
|
||||
|
||||
const frame = this.frameParser.createFrame(WebSocketOpcode.CLOSE, payload);
|
||||
this.socket.write(frame.toBuffer());
|
||||
}
|
||||
|
||||
this.readyState = WebSocketReadyState.CLOSED;
|
||||
this.stopPingInterval();
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
ping(): void {
|
||||
if (this.readyState === WebSocketReadyState.OPEN) {
|
||||
const frame = this.frameParser.createFrame(WebSocketOpcode.PING, Buffer.alloc(0));
|
||||
this.socket.write(frame.toBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
pong(): void {
|
||||
if (this.readyState === WebSocketReadyState.OPEN) {
|
||||
const frame = this.frameParser.createFrame(WebSocketOpcode.PONG, Buffer.alloc(0));
|
||||
this.socket.write(frame.toBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
private startPingInterval(): void {
|
||||
this.pingInterval = setInterval(() => {
|
||||
if (this.readyState === WebSocketReadyState.OPEN) {
|
||||
this.ping();
|
||||
}
|
||||
}, this.pingIntervalMs);
|
||||
}
|
||||
|
||||
private stopPingInterval(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional methods for connection management
|
||||
getRemoteAddress(): string {
|
||||
return this.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
getRemotePort(): number {
|
||||
return this.socket.remotePort || 0;
|
||||
}
|
||||
|
||||
isAlive(): boolean {
|
||||
return this.readyState === WebSocketReadyState.OPEN && !this.socket.destroyed;
|
||||
}
|
||||
}
|
||||
152
server/src/net/WebSockets/WebSocketFrame.ts
Normal file
152
server/src/net/WebSockets/WebSocketFrame.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {type IWebSocketFrame, type IWebSocketFrameParser, WebSocketOpcode} from './IWebSocketConnection.ts';
|
||||
|
||||
export class WebSocketFrame implements IWebSocketFrame {
|
||||
public readonly opcode: WebSocketOpcode;
|
||||
public readonly payload: Buffer;
|
||||
public readonly isFin: boolean;
|
||||
public readonly isMasked: boolean;
|
||||
public readonly maskingKey: Buffer | undefined;
|
||||
|
||||
constructor(opcode: WebSocketOpcode, payload: Buffer, isFin: boolean = true, isMasked: boolean = false, maskingKey?: Buffer) {
|
||||
this.opcode = opcode;
|
||||
this.payload = payload;
|
||||
this.isFin = isFin;
|
||||
this.isMasked = isMasked;
|
||||
this.maskingKey = maskingKey;
|
||||
}
|
||||
|
||||
get payloadLength(): number {
|
||||
return this.payload.length;
|
||||
}
|
||||
|
||||
toBuffer(): Buffer {
|
||||
const payloadLength = this.payload.length;
|
||||
let headerLength = 2;
|
||||
|
||||
// Calculate header length based on payload size
|
||||
if (payloadLength > 65535) {
|
||||
headerLength += 8; // 64-bit length
|
||||
} else if (payloadLength > 125) {
|
||||
headerLength += 2; // 16-bit length
|
||||
}
|
||||
|
||||
// Add masking key length if masked
|
||||
if (this.isMasked) {
|
||||
headerLength += 4;
|
||||
}
|
||||
|
||||
const buffer = Buffer.alloc(headerLength + payloadLength);
|
||||
let offset = 0;
|
||||
|
||||
// First byte: FIN (1 bit) + RSV (3 bits) + Opcode (4 bits)
|
||||
const firstByte = (this.isFin ? 0x80 : 0x00) | this.opcode;
|
||||
buffer[offset++] = firstByte;
|
||||
|
||||
// Second byte: MASK (1 bit) + Payload length (7 bits)
|
||||
let secondByte = this.isMasked ? 0x80 : 0x00;
|
||||
|
||||
if (payloadLength <= 125) {
|
||||
secondByte |= payloadLength;
|
||||
buffer[offset++] = secondByte;
|
||||
} else if (payloadLength <= 65535) {
|
||||
secondByte |= 126;
|
||||
buffer[offset++] = secondByte;
|
||||
buffer.writeUInt16BE(payloadLength, offset);
|
||||
offset += 2;
|
||||
} else {
|
||||
secondByte |= 127;
|
||||
buffer[offset++] = secondByte;
|
||||
// Write 64-bit length (but we only use 32 bits for practical purposes)
|
||||
buffer.writeUInt32BE(0, offset);
|
||||
offset += 4;
|
||||
buffer.writeUInt32BE(payloadLength, offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// Masking key
|
||||
if (this.isMasked && this.maskingKey) {
|
||||
this.maskingKey.copy(buffer, offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// Payload
|
||||
if (this.isMasked && this.maskingKey) {
|
||||
// Apply masking
|
||||
const mask = this.maskingKey as Buffer;
|
||||
for (let i = 0; i < payloadLength; i++) {
|
||||
const srcByte = this.payload.readUInt8(i);
|
||||
const maskByte = mask.readUInt8(i % 4);
|
||||
buffer.writeUInt8(srcByte ^ maskByte, offset + i);
|
||||
}
|
||||
} else {
|
||||
this.payload.copy(buffer, offset);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSocketFrameParser implements IWebSocketFrameParser {
|
||||
parse(buffer: Buffer): IWebSocketFrame | null {
|
||||
if (buffer.length < 2) {
|
||||
return null; // Not enough data for a complete header
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// Parse first byte
|
||||
const firstByte = buffer[offset++]!;
|
||||
const isFin = (firstByte & 0x80) !== 0;
|
||||
const opcode = firstByte & 0x0f;
|
||||
|
||||
// Parse second byte
|
||||
const secondByte = buffer[offset++]!;
|
||||
const isMasked = (secondByte & 0x80) !== 0;
|
||||
let payloadLength = secondByte & 0x7f;
|
||||
|
||||
// Parse extended payload length
|
||||
if (payloadLength === 126) {
|
||||
if (buffer.length < offset + 2) return null;
|
||||
payloadLength = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
} else if (payloadLength === 127) {
|
||||
if (buffer.length < offset + 8) return null;
|
||||
// Skip first 4 bytes (should be 0 for practical purposes)
|
||||
offset += 4;
|
||||
payloadLength = buffer.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// Parse masking key
|
||||
let maskingKey: Buffer | undefined;
|
||||
if (isMasked) {
|
||||
if (buffer.length < offset + 4) return null;
|
||||
maskingKey = buffer.subarray(offset, offset + 4);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
// Check if we have enough data for the payload
|
||||
if (buffer.length < offset + payloadLength) {
|
||||
return null; // Not enough data for complete frame
|
||||
}
|
||||
|
||||
// Extract payload
|
||||
const payload = buffer.subarray(offset, offset + payloadLength);
|
||||
|
||||
// Unmask payload if necessary
|
||||
if (isMasked && maskingKey) {
|
||||
const mask = maskingKey as Buffer;
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const maskByte = mask.readUInt8(i % 4);
|
||||
const newVal = payload.readUInt8(i) ^ maskByte;
|
||||
payload.writeUInt8(newVal, i);
|
||||
}
|
||||
}
|
||||
|
||||
return new WebSocketFrame(opcode as WebSocketOpcode, payload, isFin, isMasked, maskingKey);
|
||||
}
|
||||
|
||||
createFrame(opcode: WebSocketOpcode, payload: Buffer, isMasked: boolean = false): IWebSocketFrame {
|
||||
return new WebSocketFrame(opcode, payload, true, isMasked);
|
||||
}
|
||||
}
|
||||
97
server/src/net/WebSockets/WebSocketHandshake.ts
Normal file
97
server/src/net/WebSockets/WebSocketHandshake.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {createHash} from 'node:crypto';
|
||||
import {type IncomingMessage, type ServerResponse} from 'node:http';
|
||||
import {type IWebSocketHandshake} from './IWebSocketConnection.ts';
|
||||
|
||||
export class WebSocketHandshake implements IWebSocketHandshake {
|
||||
private static readonly WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
private static readonly WEBSOCKET_VERSION = '13';
|
||||
|
||||
isValid(request: IncomingMessage): boolean {
|
||||
// Check HTTP method
|
||||
if (request.method !== 'GET') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check headers
|
||||
const upgrade = request.headers.upgrade;
|
||||
const connection = request.headers.connection;
|
||||
const secWebSocketKey = request.headers['sec-websocket-key'];
|
||||
const secWebSocketVersion = request.headers['sec-websocket-version'];
|
||||
|
||||
if (!upgrade || upgrade.toLowerCase() !== 'websocket') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!connection || !connection.toLowerCase().includes('upgrade')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!secWebSocketKey || typeof secWebSocketKey !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!secWebSocketVersion || secWebSocketVersion !== WebSocketHandshake.WEBSOCKET_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate Sec-WebSocket-Key format (should be 16 bytes base64 encoded)
|
||||
try {
|
||||
const keyBuffer = Buffer.from(secWebSocketKey, 'base64');
|
||||
if (keyBuffer.length !== 16) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
performHandshake(request: IncomingMessage, response: ServerResponse): boolean {
|
||||
if (!this.isValid(request)) {
|
||||
response.writeHead(400, {'Content-Type': 'text/plain'});
|
||||
response.end('Bad Request');
|
||||
return false;
|
||||
}
|
||||
|
||||
const secWebSocketKey = request.headers['sec-websocket-key'] as string;
|
||||
const secWebSocketProtocol = request.headers['sec-websocket-protocol'];
|
||||
const secWebSocketExtensions = request.headers['sec-websocket-extensions'];
|
||||
|
||||
// Generate Sec-WebSocket-Accept
|
||||
const acceptKey = this.generateAcceptKey(secWebSocketKey);
|
||||
|
||||
// Prepare response headers
|
||||
const headers: Record<string, string> = {
|
||||
Upgrade: 'websocket',
|
||||
Connection: 'Upgrade',
|
||||
'Sec-WebSocket-Accept': acceptKey
|
||||
};
|
||||
|
||||
// Add protocol if requested
|
||||
if (secWebSocketProtocol) {
|
||||
// For simplicity, we'll accept the first protocol
|
||||
const protocols = secWebSocketProtocol.split(',').map(p => p.trim());
|
||||
if (protocols.length > 0) {
|
||||
headers['Sec-WebSocket-Protocol'] = protocols[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Add extensions if requested (for simplicity, we'll echo back)
|
||||
if (secWebSocketExtensions) {
|
||||
headers['Sec-WebSocket-Extensions'] = secWebSocketExtensions as string;
|
||||
}
|
||||
|
||||
// Send the handshake response
|
||||
response.writeHead(101, headers);
|
||||
response.end();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private generateAcceptKey(secWebSocketKey: string): string {
|
||||
const combined = secWebSocketKey + WebSocketHandshake.WEBSOCKET_GUID;
|
||||
const hash = createHash('sha1').update(combined).digest('base64');
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
156
server/src/net/WebSockets/WebSocketServer.ts
Normal file
156
server/src/net/WebSockets/WebSocketServer.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {LoggerFactory} from '@techniker-me/logger';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {createHash} from 'node:crypto';
|
||||
import type {Socket} from 'node:net';
|
||||
import {type IncomingMessage, type Server as HTTPServer} from 'node:http';
|
||||
import {type IWebSocketServer, type IWebSocketHandshake} from './IWebSocketConnection.ts';
|
||||
import {WebSocketHandshake} from './WebSocketHandshake.ts';
|
||||
import {WebSocketConnection} from './WebSocketConnection.ts';
|
||||
|
||||
export class WebSocketServer extends EventEmitter implements IWebSocketServer {
|
||||
private readonly _logger = LoggerFactory.getLogger('WebSocketServer');
|
||||
private readonly httpServer: HTTPServer;
|
||||
private readonly handshake: IWebSocketHandshake;
|
||||
private readonly connections: Map<string, WebSocketConnection> = new Map();
|
||||
private readonly pingIntervalMs: number;
|
||||
|
||||
constructor(httpServer: HTTPServer, pingIntervalMs: number = 30000) {
|
||||
super();
|
||||
this.httpServer = httpServer;
|
||||
this.handshake = new WebSocketHandshake();
|
||||
this.pingIntervalMs = pingIntervalMs;
|
||||
|
||||
this.setupHttpServer();
|
||||
}
|
||||
|
||||
private setupHttpServer(): void {
|
||||
this.httpServer.on('upgrade', this.handleUpgrade.bind(this));
|
||||
}
|
||||
|
||||
private handleUpgrade(request: IncomingMessage, socket: Socket, _head: Buffer): void {
|
||||
this._logger.debug(`WebSocket upgrade request: [${request.url}]`);
|
||||
|
||||
if (!this.handshake.isValid(request)) {
|
||||
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
socket.destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const secWebSocketKey = request.headers['sec-websocket-key'] as string;
|
||||
const acceptKey = this.generateAcceptKey(secWebSocketKey);
|
||||
const response = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${acceptKey}`, '', ''].join('\r\n');
|
||||
const connection = new WebSocketConnection(socket, this.pingIntervalMs);
|
||||
|
||||
this.setupConnectionHandlers(connection);
|
||||
this.connections.set(connection.id, connection);
|
||||
this.emit('connection', connection);
|
||||
socket.write(response);
|
||||
}
|
||||
|
||||
private generateAcceptKey(secWebSocketKey: string): string {
|
||||
const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
const combined = secWebSocketKey + WEBSOCKET_GUID;
|
||||
return createHash('sha1').update(combined).digest('base64');
|
||||
}
|
||||
|
||||
private setupConnectionHandlers(connection: WebSocketConnection): void {
|
||||
connection.on('close', () => {
|
||||
this.connections.delete(connection.id);
|
||||
});
|
||||
|
||||
connection.on('error', (error: Error) => {
|
||||
this.emit('error', error);
|
||||
this.connections.delete(connection.id);
|
||||
});
|
||||
}
|
||||
|
||||
public getConnection(id: string): WebSocketConnection | undefined {
|
||||
return this.connections.get(id);
|
||||
}
|
||||
|
||||
public getAllConnections(): WebSocketConnection[] {
|
||||
return Array.from(this.connections.values());
|
||||
}
|
||||
|
||||
public getConnectionCount(): number {
|
||||
return this.connections.size;
|
||||
}
|
||||
|
||||
public broadcast(message: string | Buffer, excludeConnectionId?: string): void {
|
||||
const connections = this.getAllConnections();
|
||||
|
||||
for (const connection of connections) {
|
||||
if (connection.id !== excludeConnectionId && connection.readyState === 1) {
|
||||
// OPEN
|
||||
try {
|
||||
connection.send(message);
|
||||
} catch (error) {
|
||||
console.error(`Error broadcasting to connection ${connection.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public closeConnection(id: string, code?: number, reason?: string): boolean {
|
||||
const connection = this.connections.get(id);
|
||||
if (connection) {
|
||||
connection.close(code, reason);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public closeAllConnections(code?: number, reason?: string): void {
|
||||
const connections = this.getAllConnections();
|
||||
for (const connection of connections) {
|
||||
connection.close(code, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public getAliveConnections(): WebSocketConnection[] {
|
||||
return this.getAllConnections().filter(conn => conn.readyState === 1 && (conn as WebSocketConnection).isAlive?.() !== false);
|
||||
}
|
||||
|
||||
public getConnectionStats(): {
|
||||
total: number;
|
||||
alive: number;
|
||||
connecting: number;
|
||||
closing: number;
|
||||
closed: number;
|
||||
} {
|
||||
const connections = this.getAllConnections();
|
||||
const stats = {
|
||||
total: connections.length,
|
||||
alive: 0,
|
||||
connecting: 0,
|
||||
closing: 0,
|
||||
closed: 0
|
||||
};
|
||||
|
||||
for (const connection of connections) {
|
||||
switch (connection.readyState) {
|
||||
case 0: // CONNECTING
|
||||
stats.connecting++;
|
||||
break;
|
||||
case 1: // OPEN
|
||||
stats.alive++;
|
||||
break;
|
||||
case 2: // CLOSING
|
||||
stats.closing++;
|
||||
break;
|
||||
case 3: // CLOSED
|
||||
stats.closed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.closeAllConnections(1001, 'Server shutting down');
|
||||
this.connections.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
13
server/src/net/WebSockets/WebSocketServerFactory.ts
Normal file
13
server/src/net/WebSockets/WebSocketServerFactory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {type Server as HTTPServer} from 'node:http';
|
||||
import {type IWebSocketServer} from './IWebSocketConnection.ts';
|
||||
import {WebSocketServer} from './WebSocketServer.ts';
|
||||
|
||||
export class WebSocketServerFactory {
|
||||
public static createWebSocketServer(httpServer: HTTPServer, pingIntervalMs: number = 30000): IWebSocketServer {
|
||||
return new WebSocketServer(httpServer, pingIntervalMs);
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('WebSocketServerFactory is a static class and cannot be instantiated');
|
||||
}
|
||||
}
|
||||
7
server/src/net/WebSockets/index.ts
Normal file
7
server/src/net/WebSockets/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Export all WebSocket-related classes and interfaces
|
||||
export * from './IWebSocketConnection.ts';
|
||||
export * from './WebSocketFrame.ts';
|
||||
export * from './WebSocketHandshake.ts';
|
||||
export * from './WebSocketConnection.ts';
|
||||
export * from './WebSocketServer.ts';
|
||||
export * from './WebSocketServerFactory.ts';
|
||||
Reference in New Issue
Block a user