Socket.IO Real-time Application Development
Comprehensive guide for building real-time applications with Socket.IO, including chat systems, live updates, gaming, and collaboration features.
# Socket.IO Real-time Application Development
## 1. Basic Socket.IO Setup and Configuration
### Server Setup with Express
```typescript
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import cors from 'cors';
interface ServerToClientEvents {
message: (data: MessageData) => void;
userJoined: (data: UserJoinedData) => void;
userLeft: (data: UserLeftData) => void;
typing: (data: TypingData) => void;
notification: (data: NotificationData) => void;
}
interface ClientToServerEvents {
joinRoom: (data: JoinRoomData) => void;
sendMessage: (data: SendMessageData) => void;
startTyping: () => void;
stopTyping: () => void;
disconnect: () => void;
}
interface InterServerEvents {
ping: () => void;
}
interface SocketData {
userId: string;
username: string;
rooms: string[];
}
class SocketServer {
private app: express.Application;
private server: any;
private io: Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>;
private connectedUsers: Map<string, ConnectedUser> = new Map();
constructor(port: number = 3000) {
this.app = express();
this.server = createServer(this.app);
this.io = new Server(this.server, {
cors: {
origin: process.env.CLIENT_URL || "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true
},
transports: ['websocket', 'polling'],
allowEIO3: true
});
this.setupMiddleware();
this.setupSocketHandlers();
this.startServer(port);
}
private setupMiddleware(): void {
this.app.use(cors());
this.app.use(express.json());
// Socket.IO authentication middleware
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const user = await this.authenticateUser(token);
socket.data.userId = user.id;
socket.data.username = user.username;
socket.data.rooms = [];
next();
} catch (error) {
next(new Error('Authentication failed'));
}
});
// Rate limiting middleware
this.io.use(this.rateLimitMiddleware);
}
private rateLimitMiddleware = (socket: any, next: any) => {
const userId = socket.data.userId;
const now = Date.now();
if (!this.rateLimits.has(userId)) {
this.rateLimits.set(userId, { count: 1, resetTime: now + 60000 });
return next();
}
const limit = this.rateLimits.get(userId)!;
if (now > limit.resetTime) {
limit.count = 1;
limit.resetTime = now + 60000;
return next();
}
if (limit.count >= 100) { // 100 requests per minute
return next(new Error('Rate limit exceeded'));
}
limit.count++;
next();
};
private setupSocketHandlers(): void {
this.io.on('connection', (socket) => {
console.log(`User connected: ${socket.data.username} (${socket.data.userId})`);
this.handleUserConnection(socket);
this.handleRoomEvents(socket);
this.handleMessageEvents(socket);
this.handleTypingEvents(socket);
this.handleDisconnection(socket);
});
}
private handleUserConnection(socket: any): void {
const user: ConnectedUser = {
id: socket.data.userId,
username: socket.data.username,
socketId: socket.id,
connectedAt: new Date(),
isActive: true
};
this.connectedUsers.set(socket.data.userId, user);
// Send user list to client
socket.emit('connectedUsers', Array.from(this.connectedUsers.values()));
}
private startServer(port: number): void {
this.server.listen(port, () => {
console.log(`Socket.IO server running on port ${port}`);
});
}
}
```
### Client-Side Setup
```typescript
import { io, Socket } from 'socket.io-client';
interface ServerToClientEvents {
message: (data: MessageData) => void;
userJoined: (data: UserJoinedData) => void;
userLeft: (data: UserLeftData) => void;
typing: (data: TypingData) => void;
notification: (data: NotificationData) => void;
connectedUsers: (users: ConnectedUser[]) => void;
}
interface ClientToServerEvents {
joinRoom: (data: JoinRoomData) => void;
sendMessage: (data: SendMessageData) => void;
startTyping: () => void;
stopTyping: () => void;
}
class SocketClient {
private socket: Socket<ServerToClientEvents, ClientToServerEvents>;
private reconnectionAttempts: number = 0;
private maxReconnectionAttempts: number = 5;
constructor(serverUrl: string, token: string) {
this.socket = io(serverUrl, {
auth: { token },
transports: ['websocket', 'polling'],
timeout: 20000,
forceNew: true
});
this.setupEventHandlers();
this.setupConnectionHandlers();
}
private setupConnectionHandlers(): void {
this.socket.on('connect', () => {
console.log('Connected to server');
this.reconnectionAttempts = 0;
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected from server:', reason);
if (reason === 'io server disconnect') {
// Server forcefully disconnected, try to reconnect
this.handleReconnection();
}
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.handleReconnection();
});
}
private handleReconnection(): void {
if (this.reconnectionAttempts < this.maxReconnectionAttempts) {
this.reconnectionAttempts++;
const delay = Math.pow(2, this.reconnectionAttempts) * 1000; // Exponential backoff
setTimeout(() => {
console.log(`Attempting to reconnect (${this.reconnectionAttempts}/${this.maxReconnectionAttempts})`);
this.socket.connect();
}, delay);
} else {
console.error('Max reconnection attempts reached');
}
}
private setupEventHandlers(): void {
this.socket.on('message', this.handleMessage.bind(this));
this.socket.on('userJoined', this.handleUserJoined.bind(this));
this.socket.on('userLeft', this.handleUserLeft.bind(this));
this.socket.on('typing', this.handleTyping.bind(this));
this.socket.on('notification', this.handleNotification.bind(this));
}
// Public methods for interacting with the socket
joinRoom(roomId: string): void {
this.socket.emit('joinRoom', { roomId });
}
sendMessage(roomId: string, content: string): void {
this.socket.emit('sendMessage', {
roomId,
content,
timestamp: new Date()
});
}
startTyping(roomId: string): void {
this.socket.emit('startTyping', { roomId });
}
stopTyping(roomId: string): void {
this.socket.emit('stopTyping', { roomId });
}
disconnect(): void {
this.socket.disconnect();
}
}
```
## 2. Real-time Chat Application
### Chat Room Management
```typescript
interface ChatRoom {
id: string;
name: string;
type: 'public' | 'private' | 'direct';
participants: string[];
createdBy: string;
createdAt: Date;
settings: {
maxParticipants?: number;
isModerated: boolean;
allowFileSharing: boolean;
};
}
interface Message {
id: string;
roomId: string;
senderId: string;
senderName: string;
content: string;
type: 'text' | 'image' | 'file' | 'system';
timestamp: Date;
edited?: boolean;
editedAt?: Date;
replyTo?: string;
reactions: Map<string, string[]>; // emoji -> user IDs
}
class ChatManager {
private rooms: Map<string, ChatRoom> = new Map();
private messages: Map<string, Message[]> = new Map();
private typingUsers: Map<string, Set<string>> = new Map();
constructor(private io: Server) {
this.setupChatHandlers();
}
private setupChatHandlers(): void {
this.io.on('connection', (socket) => {
socket.on('joinRoom', this.handleJoinRoom.bind(this, socket));
socket.on('leaveRoom', this.handleLeaveRoom.bind(this, socket));
socket.on('sendMessage', this.handleSendMessage.bind(this, socket));
socket.on('editMessage', this.handleEditMessage.bind(this, socket));
socket.on('deleteMessage', this.handleDeleteMessage.bind(this, socket));
socket.on('reactToMessage', this.handleMessageReaction.bind(this, socket));
socket.on('startTyping', this.handleStartTyping.bind(this, socket));
socket.on('stopTyping', this.handleStopTyping.bind(this, socket));
});
}
private async handleJoinRoom(socket: any, data: { roomId: string }): Promise<void> {
const { roomId } = data;
const userId = socket.data.userId;
const username = socket.data.username;
try {
// Validate user can join room
await this.validateRoomAccess(roomId, userId);
// Add user to room
socket.join(roomId);
socket.data.rooms.push(roomId);
// Update room participants
const room = this.rooms.get(roomId);
if (room && !room.participants.includes(userId)) {
room.participants.push(userId);
}
// Send recent messages to newly joined user
const recentMessages = this.messages.get(roomId)?.slice(-50) || [];
socket.emit('roomHistory', {
roomId,
messages: recentMessages
});
// Notify other users in room
socket.to(roomId).emit('userJoined', {
roomId,
userId,
username,
timestamp: new Date()
});
// Send updated participant list
this.io.to(roomId).emit('participantsUpdated', {
roomId,
participants: room?.participants || []
});
} catch (error) {
socket.emit('error', {
type: 'JOIN_ROOM_FAILED',
message: error.message
});
}
}
private async handleSendMessage(socket: any, data: SendMessageData): Promise<void> {
const { roomId, content, type = 'text', replyTo } = data;
const userId = socket.data.userId;
const username = socket.data.username;
try {
// Validate message
await this.validateMessage(roomId, userId, content);
const message: Message = {
id: this.generateMessageId(),
roomId,
senderId: userId,
senderName: username,
content: this.sanitizeContent(content),
type,
timestamp: new Date(),
replyTo,
reactions: new Map()
};
// Store message
if (!this.messages.has(roomId)) {
this.messages.set(roomId, []);
}
this.messages.get(roomId)!.push(message);
// Persist to database
await this.persistMessage(message);
// Broadcast to room
this.io.to(roomId).emit('message', message);
// Clear typing status for sender
this.handleStopTyping(socket, { roomId });
// Send push notifications to offline users
await this.sendPushNotifications(roomId, message, userId);
} catch (error) {
socket.emit('error', {
type: 'SEND_MESSAGE_FAILED',
message: error.message
});
}
}
private handleStartTyping(socket: any, data: { roomId: string }): void {
const { roomId } = data;
const userId = socket.data.userId;
const username = socket.data.username;
if (!this.typingUsers.has(roomId)) {
this.typingUsers.set(roomId, new Set());
}
this.typingUsers.get(roomId)!.add(userId);
socket.to(roomId).emit('typing', {
roomId,
userId,
username,
isTyping: true
});
// Auto-stop typing after 3 seconds
setTimeout(() => {
this.handleStopTyping(socket, { roomId });
}, 3000);
}
private handleStopTyping(socket: any, data: { roomId: string }): void {
const { roomId } = data;
const userId = socket.data.userId;
const username = socket.data.username;
if (this.typingUsers.has(roomId)) {
this.typingUsers.get(roomId)!.delete(userId);
socket.to(roomId).emit('typing', {
roomId,
userId,
username,
isTyping: false
});
}
}
private async handleMessageReaction(socket: any, data: {
roomId: string;
messageId: string;
emoji: string;
}): Promise<void> {
const { roomId, messageId, emoji } = data;
const userId = socket.data.userId;
const roomMessages = this.messages.get(roomId);
const message = roomMessages?.find(m => m.id === messageId);
if (message) {
if (!message.reactions.has(emoji)) {
message.reactions.set(emoji, []);
}
const users = message.reactions.get(emoji)!;
const userIndex = users.indexOf(userId);
if (userIndex === -1) {
users.push(userId); // Add reaction
} else {
users.splice(userIndex, 1); // Remove reaction
}
// Remove emoji if no users
if (users.length === 0) {
message.reactions.delete(emoji);
}
// Broadcast reaction update
this.io.to(roomId).emit('messageReactionUpdated', {
roomId,
messageId,
reactions: Object.fromEntries(message.reactions)
});
// Persist reaction
await this.persistMessageReaction(messageId, emoji, userId);
}
}
}
```
## 3. Live Collaboration Features
### Real-time Document Editing
```typescript
interface DocumentOperation {
type: 'insert' | 'delete' | 'retain';
position: number;
content?: string;
length?: number;
userId: string;
timestamp: Date;
}
interface DocumentState {
id: string;
content: string;
version: number;
lastModified: Date;
collaborators: Map<string, CollaboratorInfo>;
}
interface CollaboratorInfo {
userId: string;
username: string;
cursor: {
position: number;
selection?: { start: number; end: number };
};
color: string;
isActive: boolean;
}
class CollaborativeEditor {
private documents: Map<string, DocumentState> = new Map();
private operationQueue: Map<string, DocumentOperation[]> = new Map();
constructor(private io: Server) {
this.setupCollaborationHandlers();
}
private setupCollaborationHandlers(): void {
this.io.on('connection', (socket) => {
socket.on('joinDocument', this.handleJoinDocument.bind(this, socket));
socket.on('leaveDocument', this.handleLeaveDocument.bind(this, socket));
socket.on('documentOperation', this.handleDocumentOperation.bind(this, socket));
socket.on('cursorUpdate', this.handleCursorUpdate.bind(this, socket));
socket.on('textSelection', this.handleTextSelection.bind(this, socket));
});
}
private handleJoinDocument(socket: any, data: { documentId: string }): void {
const { documentId } = data;
const userId = socket.data.userId;
const username = socket.data.username;
socket.join(`doc:${documentId}`);
// Get or create document
if (!this.documents.has(documentId)) {
this.documents.set(documentId, {
id: documentId,
content: '',
version: 0,
lastModified: new Date(),
collaborators: new Map()
});
}
const doc = this.documents.get(documentId)!;
// Add collaborator
const collaborator: CollaboratorInfo = {
userId,
username,
cursor: { position: 0 },
color: this.generateUserColor(userId),
isActive: true
};
doc.collaborators.set(userId, collaborator);
// Send document state to new collaborator
socket.emit('documentState', {
documentId,
content: doc.content,
version: doc.version,
collaborators: Array.from(doc.collaborators.values())
});
// Notify other collaborators
socket.to(`doc:${documentId}`).emit('collaboratorJoined', collaborator);
}
private async handleDocumentOperation(socket: any, data: {
documentId: string;
operations: DocumentOperation[];
}): Promise<void> {
const { documentId, operations } = data;
const userId = socket.data.userId;
const doc = this.documents.get(documentId);
if (!doc) return;
try {
// Apply operations with operational transformation
const transformedOps = await this.applyOperationalTransform(
documentId,
operations,
doc.version
);
// Update document
for (const op of transformedOps) {
this.applyOperation(doc, op);
}
doc.version++;
doc.lastModified = new Date();
// Broadcast operations to other collaborators
socket.to(`doc:${documentId}`).emit('documentOperations', {
documentId,
operations: transformedOps,
version: doc.version
});
// Acknowledge to sender
socket.emit('operationAcknowledged', {
documentId,
version: doc.version
});
// Persist changes
await this.persistDocumentChanges(documentId, transformedOps);
} catch (error) {
socket.emit('operationError', {
documentId,
error: error.message
});
}
}
private applyOperationalTransform(
documentId: string,
operations: DocumentOperation[],
expectedVersion: number
): DocumentOperation[] {
const doc = this.documents.get(documentId)!;
if (doc.version !== expectedVersion) {
// Transform operations against concurrent operations
const concurrentOps = this.operationQueue.get(documentId) || [];
return this.transformOperations(operations, concurrentOps);
}
return operations;
}
private transformOperations(
clientOps: DocumentOperation[],
serverOps: DocumentOperation[]
): DocumentOperation[] {
let transformedOps = [...clientOps];
for (const serverOp of serverOps) {
transformedOps = transformedOps.map(clientOp => {
return this.transformOperation(clientOp, serverOp);
});
}
return transformedOps;
}
private transformOperation(
clientOp: DocumentOperation,
serverOp: DocumentOperation
): DocumentOperation {
// Simplified operational transformation logic
if (serverOp.type === 'insert') {
if (clientOp.position >= serverOp.position) {
return {
...clientOp,
position: clientOp.position + (serverOp.content?.length || 0)
};
}
} else if (serverOp.type === 'delete') {
if (clientOp.position >= serverOp.position) {
return {
...clientOp,
position: Math.max(
serverOp.position,
clientOp.position - (serverOp.length || 0)
)
};
}
}
return clientOp;
}
private applyOperation(doc: DocumentState, operation: DocumentOperation): void {
switch (operation.type) {
case 'insert':
doc.content =
doc.content.slice(0, operation.position) +
(operation.content || '') +
doc.content.slice(operation.position);
break;
case 'delete':
doc.content =
doc.content.slice(0, operation.position) +
doc.content.slice(operation.position + (operation.length || 0));
break;
case 'retain':
// No change to content for retain operations
break;
}
}
private handleCursorUpdate(socket: any, data: {
documentId: string;
position: number;
}): void {
const { documentId, position } = data;
const userId = socket.data.userId;
const doc = this.documents.get(documentId);
const collaborator = doc?.collaborators.get(userId);
if (collaborator) {
collaborator.cursor.position = position;
socket.to(`doc:${documentId}`).emit('cursorMoved', {
userId,
position
});
}
}
}
```
## 4. Live Updates and Notifications
### Real-time Notification System
```typescript
interface Notification {
id: string;
userId: string;
type: 'message' | 'mention' | 'system' | 'update';
title: string;
content: string;
data?: any;
priority: 'low' | 'normal' | 'high' | 'urgent';
createdAt: Date;
readAt?: Date;
actionUrl?: string;
}
class NotificationManager {
private userNotifications: Map<string, Notification[]> = new Map();
private pushService: PushNotificationService;
constructor(private io: Server) {
this.pushService = new PushNotificationService();
this.setupNotificationHandlers();
}
private setupNotificationHandlers(): void {
this.io.on('connection', (socket) => {
socket.on('markNotificationRead', this.handleMarkAsRead.bind(this, socket));
socket.on('markAllRead', this.handleMarkAllRead.bind(this, socket));
socket.on('getNotifications', this.handleGetNotifications.bind(this, socket));
});
}
async sendNotification(notification: Notification): Promise<void> {
const { userId } = notification;
// Store notification
if (!this.userNotifications.has(userId)) {
this.userNotifications.set(userId, []);
}
this.userNotifications.get(userId)!.push(notification);
// Persist to database
await this.persistNotification(notification);
// Send real-time notification if user is online
const userSocket = this.findUserSocket(userId);
if (userSocket) {
userSocket.emit('notification', notification);
} else {
// Send push notification if user is offline
await this.pushService.sendPushNotification(userId, notification);
}
// Update notification count
await this.updateNotificationCount(userId);
}
async sendBulkNotification(
userIds: string[],
notificationTemplate: Omit<Notification, 'id' | 'userId' | 'createdAt'>
): Promise<void> {
const notifications = userIds.map(userId => ({
...notificationTemplate,
id: this.generateNotificationId(),
userId,
createdAt: new Date()
}));
// Send notifications in batches
const batchSize = 100;
for (let i = 0; i < notifications.length; i += batchSize) {
const batch = notifications.slice(i, i + batchSize);
await Promise.all(
batch.map(notification => this.sendNotification(notification))
);
}
}
private findUserSocket(userId: string): any {
for (const [, socket] of this.io.sockets.sockets) {
if (socket.data.userId === userId) {
return socket;
}
}
return null;
}
private async updateNotificationCount(userId: string): Promise<void> {
const notifications = this.userNotifications.get(userId) || [];
const unreadCount = notifications.filter(n => !n.readAt).length;
const userSocket = this.findUserSocket(userId);
if (userSocket) {
userSocket.emit('notificationCountUpdated', { unreadCount });
}
}
}
// Live data updates for dashboard/analytics
class LiveDataManager {
private dataStreams: Map<string, any> = new Map();
constructor(private io: Server) {
this.setupDataStreams();
}
private setupDataStreams(): void {
// Real-time analytics updates
setInterval(() => {
this.broadcastAnalyticsUpdate();
}, 5000);
// Live user activity
setInterval(() => {
this.broadcastUserActivity();
}, 2000);
// System status updates
setInterval(() => {
this.broadcastSystemStatus();
}, 10000);
}
private async broadcastAnalyticsUpdate(): Promise<void> {
const analytics = await this.getRealtimeAnalytics();
this.io.to('analytics').emit('analyticsUpdate', {
timestamp: new Date(),
data: analytics
});
}
private async broadcastUserActivity(): Promise<void> {
const activeUsers = await this.getActiveUserCount();
const onlineUsers = this.io.sockets.sockets.size;
this.io.emit('userActivityUpdate', {
activeUsers,
onlineUsers,
timestamp: new Date()
});
}
private async broadcastSystemStatus(): Promise<void> {
const systemStatus = await this.getSystemHealth();
this.io.emit('systemStatusUpdate', systemStatus);
}
}
```
## 5. Gaming and Interactive Features
### Multiplayer Game Framework
```typescript
interface GameState {
id: string;
type: string;
players: Map<string, Player>;
status: 'waiting' | 'playing' | 'paused' | 'finished';
currentTurn?: string;
board?: any;
settings: GameSettings;
createdAt: Date;
startedAt?: Date;
finishedAt?: Date;
}
interface Player {
id: string;
username: string;
score: number;
isReady: boolean;
joinedAt: Date;
lastAction?: Date;
}
interface GameAction {
type: string;
playerId: string;
data: any;
timestamp: Date;
}
class GameManager {
private games: Map<string, GameState> = new Map();
private playerGameMap: Map<string, string> = new Map();
constructor(private io: Server) {
this.setupGameHandlers();
}
private setupGameHandlers(): void {
this.io.on('connection', (socket) => {
socket.on('createGame', this.handleCreateGame.bind(this, socket));
socket.on('joinGame', this.handleJoinGame.bind(this, socket));
socket.on('leaveGame', this.handleLeaveGame.bind(this, socket));
socket.on('gameAction', this.handleGameAction.bind(this, socket));
socket.on('playerReady', this.handlePlayerReady.bind(this, socket));
});
}
private handleCreateGame(socket: any, data: {
gameType: string;
settings: GameSettings;
}): void {
const { gameType, settings } = data;
const userId = socket.data.userId;
const username = socket.data.username;
const gameId = this.generateGameId();
const game: GameState = {
id: gameId,
type: gameType,
players: new Map(),
status: 'waiting',
settings,
createdAt: new Date()
};
// Add creator as first player
const player: Player = {
id: userId,
username,
score: 0,
isReady: false,
joinedAt: new Date()
};
game.players.set(userId, player);
this.games.set(gameId, game);
this.playerGameMap.set(userId, gameId);
socket.join(`game:${gameId}`);
socket.emit('gameCreated', {
gameId,
game: this.serializeGameState(game)
});
}
private handleJoinGame(socket: any, data: { gameId: string }): void {
const { gameId } = data;
const userId = socket.data.userId;
const username = socket.data.username;
const game = this.games.get(gameId);
if (!game) {
socket.emit('error', { message: 'Game not found' });
return;
}
if (game.status !== 'waiting') {
socket.emit('error', { message: 'Game already started' });
return;
}
if (game.players.size >= game.settings.maxPlayers) {
socket.emit('error', { message: 'Game is full' });
return;
}
// Add player to game
const player: Player = {
id: userId,
username,
score: 0,
isReady: false,
joinedAt: new Date()
};
game.players.set(userId, player);
this.playerGameMap.set(userId, gameId);
socket.join(`game:${gameId}`);
// Notify all players
this.io.to(`game:${gameId}`).emit('playerJoined', {
gameId,
player,
gameState: this.serializeGameState(game)
});
}
private handleGameAction(socket: any, data: {
gameId: string;
action: GameAction;
}): void {
const { gameId, action } = data;
const userId = socket.data.userId;
const game = this.games.get(gameId);
if (!game || !game.players.has(userId)) {
socket.emit('error', { message: 'Invalid game or player' });
return;
}
if (game.status !== 'playing') {
socket.emit('error', { message: 'Game is not active' });
return;
}
// Validate turn if applicable
if (game.currentTurn && game.currentTurn !== userId) {
socket.emit('error', { message: 'Not your turn' });
return;
}
try {
// Process game action based on game type
const result = this.processGameAction(game, action);
// Update game state
this.updateGameState(game, result);
// Broadcast action to all players
this.io.to(`game:${gameId}`).emit('gameAction', {
gameId,
action,
result,
gameState: this.serializeGameState(game)
});
// Check for game end conditions
if (this.checkGameEnd(game)) {
this.endGame(game);
}
} catch (error) {
socket.emit('gameActionError', {
gameId,
action,
error: error.message
});
}
}
private processGameAction(game: GameState, action: GameAction): any {
// Game-specific logic would go here
switch (game.type) {
case 'tic-tac-toe':
return this.processTicTacToeAction(game, action);
case 'chess':
return this.processChessAction(game, action);
case 'card-game':
return this.processCardGameAction(game, action);
default:
throw new Error('Unsupported game type');
}
}
private endGame(game: GameState): void {
game.status = 'finished';
game.finishedAt = new Date();
const winners = this.calculateWinners(game);
this.io.to(`game:${game.id}`).emit('gameEnded', {
gameId: game.id,
winners,
finalScores: Array.from(game.players.values()).map(p => ({
playerId: p.id,
username: p.username,
score: p.score
}))
});
// Clean up
for (const playerId of game.players.keys()) {
this.playerGameMap.delete(playerId);
}
// Keep game in memory for a short time for final results
setTimeout(() => {
this.games.delete(game.id);
}, 60000);
}
}
```
## 6. Performance Optimization and Scaling
### Connection Management and Clustering
```typescript
import cluster from 'cluster';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
class ScalableSocketServer {
private redisAdapter: any;
constructor() {
if (cluster.isMaster) {
this.setupMasterProcess();
} else {
this.setupWorkerProcess();
}
}
private setupMasterProcess(): void {
const numCPUs = require('os').cpus().length;
console.log(`Master ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
}
private async setupWorkerProcess(): Promise<void> {
const app = express();
const server = createServer(app);
const io = new Server(server);
// Redis adapter for horizontal scaling
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Connection limiting
const connectionLimiter = this.createConnectionLimiter();
io.use(connectionLimiter);
// Memory usage monitoring
this.setupMemoryMonitoring();
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'OK',
connections: io.engine.clientsCount,
memory: process.memoryUsage(),
uptime: process.uptime()
});
});
server.listen(process.env.PORT || 3000, () => {
console.log(`Worker ${process.pid} started`);
});
}
private createConnectionLimiter() {
const connectionCounts = new Map<string, number>();
const maxConnectionsPerIP = 10;
return (socket: any, next: any) => {
const ip = socket.request.connection.remoteAddress;
const currentConnections = connectionCounts.get(ip) || 0;
if (currentConnections >= maxConnectionsPerIP) {
return next(new Error('Connection limit exceeded'));
}
connectionCounts.set(ip, currentConnections + 1);
socket.on('disconnect', () => {
const count = connectionCounts.get(ip) || 0;
if (count <= 1) {
connectionCounts.delete(ip);
} else {
connectionCounts.set(ip, count - 1);
}
});
next();
};
}
private setupMemoryMonitoring(): void {
setInterval(() => {
const memUsage = process.memoryUsage();
const memoryThreshold = 500 * 1024 * 1024; // 500MB
if (memUsage.heapUsed > memoryThreshold) {
console.warn('High memory usage detected:', {
heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(memUsage.external / 1024 / 1024)}MB`
});
// Force garbage collection if available
if (global.gc) {
global.gc();
}
}
}, 30000);
}
}
```
## 7. Security and Authentication
### Secure Socket Implementation
```typescript
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
class SecureSocketServer {
private ipAttempts: Map<string, { count: number; lastAttempt: Date }> = new Map();
private suspiciousActivity: Map<string, number> = new Map();
constructor(private io: Server) {
this.setupSecurityMiddleware();
this.setupSecurityMonitoring();
}
private setupSecurityMiddleware(): void {
// JWT Authentication
this.io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
throw new Error('No token provided');
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
const user = await this.validateUser(decoded.userId);
if (!user || !user.isActive) {
throw new Error('Invalid or inactive user');
}
socket.data.userId = user.id;
socket.data.username = user.username;
socket.data.roles = user.roles;
next();
} catch (error) {
this.logSecurityEvent('auth_failed', socket, error.message);
next(new Error('Authentication failed'));
}
});
// Rate limiting per connection
this.io.use((socket, next) => {
const ip = socket.request.connection.remoteAddress;
const now = new Date();
const attempts = this.ipAttempts.get(ip);
if (attempts) {
const timeDiff = now.getTime() - attempts.lastAttempt.getTime();
if (timeDiff < 1000) { // Less than 1 second
attempts.count++;
if (attempts.count > 50) { // 50 connections per second
this.logSecurityEvent('rate_limit_exceeded', socket, `IP: ${ip}`);
return next(new Error('Rate limit exceeded'));
}
} else {
attempts.count = 1;
}
attempts.lastAttempt = now;
} else {
this.ipAttempts.set(ip, { count: 1, lastAttempt: now });
}
next();
});
// Input validation middleware
this.io.use((socket, next) => {
const originalEmit = socket.emit;
socket.emit = (event: string, ...args: any[]) => {
// Validate and sanitize all outgoing data
const sanitizedArgs = args.map(arg => this.sanitizeData(arg));
return originalEmit.call(socket, event, ...sanitizedArgs);
};
// Wrap event handlers to validate incoming data
const originalOn = socket.on;
socket.on = (event: string, handler: Function) => {
const wrappedHandler = (...args: any[]) => {
try {
// Validate and sanitize incoming data
const sanitizedArgs = args.map(arg => this.validateAndSanitizeInput(arg));
return handler(...sanitizedArgs);
} catch (error) {
this.logSecurityEvent('invalid_input', socket, `Event: ${event}`);
socket.emit('error', { message: 'Invalid input' });
}
};
return originalOn.call(socket, event, wrappedHandler);
};
next();
});
}
private setupSecurityMonitoring(): void {
// Monitor for suspicious patterns
setInterval(() => {
for (const [ip, count] of this.suspiciousActivity.entries()) {
if (count > 100) { // Threshold for suspicious activity
console.warn(`Suspicious activity detected from IP: ${ip}`);
// Block IP or take other actions
this.blockIP(ip);
}
}
// Reset counters
this.suspiciousActivity.clear();
}, 60000); // Every minute
// Clean up old IP attempt records
setInterval(() => {
const cutoff = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
for (const [ip, data] of this.ipAttempts.entries()) {
if (data.lastAttempt < cutoff) {
this.ipAttempts.delete(ip);
}
}
}, 300000); // Every 5 minutes
}
private validateAndSanitizeInput(data: any): any {
if (typeof data === 'string') {
// Basic XSS prevention
return data.replace(/<script.*?>.*?<\/script>/gi, '')
.replace(/<iframe.*?>.*?<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
.trim()
.substring(0, 10000); // Limit length
}
if (typeof data === 'object' && data !== null) {
const sanitized: any = {};
for (const [key, value] of Object.entries(data)) {
if (typeof key === 'string' && key.length < 100) { // Limit key length
sanitized[key] = this.validateAndSanitizeInput(value);
}
}
return sanitized;
}
return data;
}
private sanitizeData(data: any): any {
// Remove sensitive information from outgoing data
if (typeof data === 'object' && data !== null) {
const sanitized = { ...data };
delete sanitized.password;
delete sanitized.token;
delete sanitized.secret;
delete sanitized.apiKey;
return sanitized;
}
return data;
}
private logSecurityEvent(type: string, socket: any, details: string): void {
const event = {
type,
timestamp: new Date(),
ip: socket.request.connection.remoteAddress,
userAgent: socket.request.headers['user-agent'],
userId: socket.data.userId,
details
};
console.warn('Security Event:', event);
// Track suspicious activity
const ip = socket.request.connection.remoteAddress;
const current = this.suspiciousActivity.get(ip) || 0;
this.suspiciousActivity.set(ip, current + 1);
// Store in security log database
this.storeSecurityEvent(event);
}
private blockIP(ip: string): void {
// Implementation depends on your infrastructure
// Could use iptables, cloud firewall, or application-level blocking
console.log(`Blocking IP: ${ip}`);
}
}
```
## Implementation Checklist
- [ ] Set up Socket.IO server with proper authentication
- [ ] Implement client connection management and reconnection logic
- [ ] Build real-time chat with room management
- [ ] Add typing indicators and message reactions
- [ ] Implement collaborative editing with operational transformation
- [ ] Create notification system with push notifications
- [ ] Build live data updates for dashboards
- [ ] Add gaming/interactive features if needed
- [ ] Implement proper error handling and validation
- [ ] Set up Redis adapter for horizontal scaling
- [ ] Add security middleware and rate limiting
- [ ] Monitor performance and memory usage
- [ ] Implement proper logging and analytics
- [ ] Test with load testing tools
This comprehensive guide provides the foundation for building robust, scalable real-time applications with Socket.IO, covering everything from basic setup to advanced features like collaborative editing and multiplayer gaming.