Overview
The outgoing WebSocket server on port 5200 broadcasts processed match data to frontend applications.
- No authentication required: Simple logon with
groupCode
- Room-based broadcasting: Clients join rooms and receive data for their match
- Filtered data: Internal fields and secrets removed before emission
- Real-time updates: Instant
match_data events as game state changes
Server Initialization
The server is created in src/connector/websocketOutgoing.ts:19-85:
let serverInstance;
if (process.env.INSECURE == "true") {
serverInstance = createInsecureServer();
} else {
if (!process.env.SERVER_KEY || !process.env.SERVER_CERT) {
Log.error(
`Missing TLS key or certificate! Please provide the paths to the key and certificate in the .env file. (SERVER_KEY and SERVER_CERT)`,
);
}
const options = {
key: readFileSync(process.env.SERVER_KEY!),
cert: readFileSync(process.env.SERVER_CERT!),
};
serverInstance = createServer(options);
}
this.wss = new Server(serverInstance, {
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 * 1024,
},
threshold: 1024,
},
cors: { origin: "*" },
});
serverInstance.listen(5200);
The outgoing server uses identical Socket.IO configuration as the incoming server, including perMessageDeflate compression and CORS wildcard.
Connection Flow
Frontend clients connect and join rooms based on their groupCode.
Initial Connection
From websocketOutgoing.ts:54-76:
this.wss.on(`connection`, (ws) => {
ws.on("error", (e) => {
Log.error(`Someone in ${ws.rooms} encountered a Websocket error: ${e}`);
});
ws.once("logon", (msg: string) => {
try {
const json = JSON.parse(msg);
ws.join(json.groupCode);
ws.emit(
"logon_success",
JSON.stringify({
groupCode: json.groupCode,
msg: `Logon succeeded for group code ${json.groupCode}`,
}),
);
Log.info(`Received output logon using Group Code ${json.groupCode}`);
MatchController.getInstance().sendMatchDataForLogon(json.groupCode);
} catch (e) {
Log.error(`Error parsing outgoing logon request: ${e}`);
}
});
});
Logon Event
Clients must emit a logon event with their groupCode to join the room:
socket.emit("logon", JSON.stringify({ groupCode: "MATCH123" }));
Logon payload:
{
groupCode: string; // The match identifier
}
Logon Success Response
On successful logon, the server:
- Joins the client to the
groupCode room
- Emits
logon_success confirmation
- Sends current match data via
MatchController.sendMatchDataForLogon()
logon_success payload:
{
groupCode: string;
msg: string; // "Logon succeeded for group code {groupCode}"
}
After logon_success, the client immediately receives the current match state via a match_data event.
Room-Based Broadcasting
Socket.IO rooms enable efficient broadcasting to all clients watching the same match.
Joining Rooms
Each groupCode becomes a Socket.IO room. Clients in the same room receive identical match_data events.
Broadcasting to Rooms
From websocketOutgoing.ts:87-116:
sendMatchData(groupCode: string, data: any) {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
replayLog,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
eventNumber,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
groupSecret,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
playercamUrl,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
timeoutEndTimeout,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
timeoutRemainingLoop,
...formattedData
} = data;
const deepMod: any = structuredClone(formattedData);
if (
deepMod.tools &&
deepMod.tools.playercamsInfo &&
typeof deepMod.tools.playercamsInfo === "object"
) {
delete deepMod.tools.playercamsInfo.secret;
delete deepMod.tools.playercamsInfo.endTime;
}
this.wss.to(groupCode).emit("match_data", JSON.stringify(deepMod));
}
Data Filtering
Before emitting match_data, the server removes internal fields to prevent exposing sensitive information:
Removed Fields
- replayLog: Internal replay buffer
- eventNumber: Internal event counter
- groupSecret: Secret token for match validation
- playercamUrl: Internal player camera URL
- timeoutEndTimeout: Internal timeout handler reference
- timeoutRemainingLoop: Internal interval reference
- tools.playercamsInfo.secret: Player camera authentication secret
- tools.playercamsInfo.endTime: Internal timeout reference
Destructuring and Cloning
The server uses object destructuring to remove fields:
const {
replayLog,
eventNumber,
groupSecret,
playercamUrl,
timeoutEndTimeout,
timeoutRemainingLoop,
...formattedData // All remaining fields
} = data;
Then creates a deep clone to safely delete nested properties:
const deepMod: any = structuredClone(formattedData);
if (
deepMod.tools &&
deepMod.tools.playercamsInfo &&
typeof deepMod.tools.playercamsInfo === "object"
) {
delete deepMod.tools.playercamsInfo.secret;
delete deepMod.tools.playercamsInfo.endTime;
}
structuredClone creates a deep copy of the object, preventing mutations to the original match data stored by the server.
match_data Event
The primary event emitted to frontend clients is match_data.
Emission
this.wss.to(groupCode).emit("match_data", JSON.stringify(deepMod));
- Event name:
match_data
- Payload: JSON string of filtered match data
- Target: All clients in the
groupCode room
Event Frequency
The server emits match_data whenever:
- A client joins (via
sendMatchDataForLogon)
- Observer/auxiliary clients send new data to port 5100
- Match state changes (scoreboard, killfeed, round info, etc.)
Payload Structure
The match_data payload contains the complete match state:
{
matchId: string;
groupCode: string;
scoreboard: IFormattedScoreboard[];
killfeed: IFormattedKillfeed[];
roster: IFormattedRoster[];
roundInfo: IFormattedRoundInfo;
score: IFormattedScore;
map: string;
gameMode: string;
observing: string;
teamIsAttacker: boolean;
spikeDetonated: boolean;
spikeDefused: boolean;
tools: ToolsData;
// ... and more fields
}
See the Data Events page for complete data schemas.
Singleton Pattern
The WebsocketOutgoing class uses a singleton pattern:
private static instance: WebsocketOutgoing;
public static getInstance(): WebsocketOutgoing {
if (WebsocketOutgoing.instance == null) WebsocketOutgoing.instance = new WebsocketOutgoing();
return WebsocketOutgoing.instance;
}
The MatchController calls WebsocketOutgoing.getInstance().sendMatchData() to broadcast updates.
Error Handling
Connection Errors
ws.on("error", (e) => {
Log.error(`Someone in ${ws.rooms} encountered a Websocket error: ${e}`);
});
Engine Errors
this.wss.engine.on("connection_error", (err) => {
Log.error("Socket.IO error: " + err);
});
Errors are logged but do not disconnect clients or crash the server.
Connection Example
import { io } from "socket.io-client";
const socket = io("wss://server:5200");
socket.on("connect", () => {
console.log("Connected to port 5200");
// Join room for match
socket.emit("logon", JSON.stringify({ groupCode: "MATCH123" }));
});
socket.on("logon_success", (msg) => {
const response = JSON.parse(msg);
console.log(response.msg); // "Logon succeeded for group code MATCH123"
});
socket.on("match_data", (msg) => {
const matchData = JSON.parse(msg);
console.log("Received match data:", matchData);
// Update UI with scoreboard, killfeed, etc.
updateScoreboard(matchData.scoreboard);
updateKillfeed(matchData.killfeed);
updateRoundInfo(matchData.roundInfo);
});
socket.on("disconnect", () => {
console.log("Disconnected from server");
});
Compression
The server compresses messages over 1KB using perMessageDeflate, reducing bandwidth usage for large match data payloads.
Room Broadcasting
Socket.IO rooms use efficient internal data structures to broadcast to multiple clients without serializing the payload multiple times.
Deep Cloning
The server uses structuredClone instead of JSON.parse(JSON.stringify()) for better performance when filtering data.
Next Steps
Data Events
Explore match_data payload structure and schemas
Incoming Connection
Learn how data arrives at port 5100