401 lines
9.8 KiB
Markdown
401 lines
9.8 KiB
Markdown
|
|
# WebSocket Patterns Reference
|
||
|
|
|
||
|
|
## Rooms and Namespaces
|
||
|
|
|
||
|
|
### Rooms (Channel Grouping)
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const io = require('socket.io')(3000);
|
||
|
|
|
||
|
|
io.on('connection', (socket) => {
|
||
|
|
// Join a room
|
||
|
|
socket.on('join-room', (roomId) => {
|
||
|
|
socket.join(roomId);
|
||
|
|
socket.emit('joined', { room: roomId });
|
||
|
|
|
||
|
|
// Notify others in room
|
||
|
|
socket.to(roomId).emit('user-joined', {
|
||
|
|
userId: socket.id,
|
||
|
|
timestamp: Date.now()
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Leave a room
|
||
|
|
socket.on('leave-room', (roomId) => {
|
||
|
|
socket.leave(roomId);
|
||
|
|
socket.to(roomId).emit('user-left', { userId: socket.id });
|
||
|
|
});
|
||
|
|
|
||
|
|
// Send to specific room
|
||
|
|
socket.on('message', ({ roomId, text }) => {
|
||
|
|
io.to(roomId).emit('message', {
|
||
|
|
userId: socket.id,
|
||
|
|
text,
|
||
|
|
timestamp: Date.now()
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get all rooms a socket is in
|
||
|
|
console.log('Socket rooms:', socket.rooms); // Set { socketId, roomId1, roomId2 }
|
||
|
|
|
||
|
|
// Disconnect from all rooms
|
||
|
|
socket.on('disconnect', () => {
|
||
|
|
// Automatically leaves all rooms
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Broadcast to all sockets in a room
|
||
|
|
io.to('room123').emit('announcement', 'Hello room!');
|
||
|
|
|
||
|
|
// Broadcast to multiple rooms
|
||
|
|
io.to('room1').to('room2').emit('multi-room', 'data');
|
||
|
|
|
||
|
|
// Broadcast to room except specific socket
|
||
|
|
socket.to('room123').emit('message', 'Others see this');
|
||
|
|
|
||
|
|
// Get all sockets in a room
|
||
|
|
const sockets = await io.in('room123').fetchSockets();
|
||
|
|
console.log(`Room has ${sockets.length} connections`);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Namespaces (Logical Separation)
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Admin namespace
|
||
|
|
const adminNs = io.of('/admin');
|
||
|
|
adminNs.on('connection', (socket) => {
|
||
|
|
console.log('Admin connected:', socket.id);
|
||
|
|
|
||
|
|
socket.on('admin-action', (data) => {
|
||
|
|
// Admin-only events
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Chat namespace
|
||
|
|
const chatNs = io.of('/chat');
|
||
|
|
chatNs.on('connection', (socket) => {
|
||
|
|
console.log('Chat user connected:', socket.id);
|
||
|
|
|
||
|
|
socket.on('message', (msg) => {
|
||
|
|
chatNs.emit('message', msg); // Broadcast to all in /chat
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Dynamic namespaces
|
||
|
|
io.of(/^\/workspace-\d+$/).on('connection', (socket) => {
|
||
|
|
const namespace = socket.nsp;
|
||
|
|
console.log(`Connected to ${namespace.name}`);
|
||
|
|
|
||
|
|
socket.on('message', (data) => {
|
||
|
|
namespace.emit('message', data);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Broadcasting Patterns
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Broadcast to everyone including sender
|
||
|
|
io.emit('event', data);
|
||
|
|
|
||
|
|
// Broadcast to everyone except sender
|
||
|
|
socket.broadcast.emit('event', data);
|
||
|
|
|
||
|
|
// Broadcast to specific room
|
||
|
|
io.to('room1').emit('event', data);
|
||
|
|
|
||
|
|
// Broadcast to room except sender
|
||
|
|
socket.to('room1').emit('event', data);
|
||
|
|
|
||
|
|
// Broadcast to multiple rooms
|
||
|
|
io.to('room1').to('room2').emit('event', data);
|
||
|
|
|
||
|
|
// Broadcast to all connected clients in namespace
|
||
|
|
io.of('/namespace').emit('event', data);
|
||
|
|
|
||
|
|
// Volatile messages (ok to drop if client not ready)
|
||
|
|
socket.volatile.emit('high-frequency', data);
|
||
|
|
|
||
|
|
// Broadcast with acknowledgment (to all clients)
|
||
|
|
io.timeout(5000).emit('event', data, (err, responses) => {
|
||
|
|
if (err) {
|
||
|
|
console.log('Some clients did not acknowledge');
|
||
|
|
} else {
|
||
|
|
console.log('All clients acknowledged:', responses);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Acknowledgments
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Server expects acknowledgment
|
||
|
|
socket.emit('question', 'Do you agree?', (answer) => {
|
||
|
|
console.log('Client answered:', answer);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Client sends acknowledgment
|
||
|
|
socket.on('question', (data, callback) => {
|
||
|
|
console.log('Server asked:', data);
|
||
|
|
callback('Yes, I agree');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Timeout for acknowledgment
|
||
|
|
socket.timeout(5000).emit('request', data, (err, response) => {
|
||
|
|
if (err) {
|
||
|
|
console.log('Client did not acknowledge in time');
|
||
|
|
} else {
|
||
|
|
console.log('Got response:', response);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Error handling in acknowledgment
|
||
|
|
socket.on('save-data', (data, callback) => {
|
||
|
|
try {
|
||
|
|
saveToDatabase(data);
|
||
|
|
callback({ success: true });
|
||
|
|
} catch (error) {
|
||
|
|
callback({ success: false, error: error.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Presence System
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const redis = require('ioredis');
|
||
|
|
const redisClient = new redis();
|
||
|
|
|
||
|
|
class PresenceManager {
|
||
|
|
async userConnected(userId, socketId) {
|
||
|
|
const key = `user:${userId}:sockets`;
|
||
|
|
|
||
|
|
// Add socket to user's socket set
|
||
|
|
await redisClient.sadd(key, socketId);
|
||
|
|
|
||
|
|
// Set TTL to auto-cleanup stale connections
|
||
|
|
await redisClient.expire(key, 3600);
|
||
|
|
|
||
|
|
// Get total connections for user
|
||
|
|
const socketCount = await redisClient.scard(key);
|
||
|
|
|
||
|
|
// If first connection, mark user as online
|
||
|
|
if (socketCount === 1) {
|
||
|
|
await redisClient.hset('presence', userId, JSON.stringify({
|
||
|
|
status: 'online',
|
||
|
|
lastSeen: Date.now()
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Notify friends
|
||
|
|
const friends = await this.getUserFriends(userId);
|
||
|
|
friends.forEach(friendId => {
|
||
|
|
io.to(`user:${friendId}`).emit('presence', {
|
||
|
|
userId,
|
||
|
|
status: 'online'
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return socketCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
async userDisconnected(userId, socketId) {
|
||
|
|
const key = `user:${userId}:sockets`;
|
||
|
|
|
||
|
|
// Remove socket from set
|
||
|
|
await redisClient.srem(key, socketId);
|
||
|
|
|
||
|
|
const socketCount = await redisClient.scard(key);
|
||
|
|
|
||
|
|
// If no more connections, mark offline
|
||
|
|
if (socketCount === 0) {
|
||
|
|
await redisClient.hset('presence', userId, JSON.stringify({
|
||
|
|
status: 'offline',
|
||
|
|
lastSeen: Date.now()
|
||
|
|
}));
|
||
|
|
|
||
|
|
const friends = await this.getUserFriends(userId);
|
||
|
|
friends.forEach(friendId => {
|
||
|
|
io.to(`user:${friendId}`).emit('presence', {
|
||
|
|
userId,
|
||
|
|
status: 'offline',
|
||
|
|
lastSeen: Date.now()
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return socketCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
async getUserStatus(userId) {
|
||
|
|
const data = await redisClient.hget('presence', userId);
|
||
|
|
return data ? JSON.parse(data) : { status: 'offline' };
|
||
|
|
}
|
||
|
|
|
||
|
|
async getBulkPresence(userIds) {
|
||
|
|
const pipeline = redisClient.pipeline();
|
||
|
|
userIds.forEach(id => pipeline.hget('presence', id));
|
||
|
|
|
||
|
|
const results = await pipeline.exec();
|
||
|
|
return userIds.map((id, i) => ({
|
||
|
|
userId: id,
|
||
|
|
...JSON.parse(results[i][1] || '{"status":"offline"}')
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
const presence = new PresenceManager();
|
||
|
|
|
||
|
|
io.on('connection', async (socket) => {
|
||
|
|
const userId = socket.handshake.auth.userId;
|
||
|
|
|
||
|
|
await presence.userConnected(userId, socket.id);
|
||
|
|
socket.join(`user:${userId}`);
|
||
|
|
|
||
|
|
socket.on('disconnect', async () => {
|
||
|
|
await presence.userDisconnected(userId, socket.id);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Get presence for user's friends
|
||
|
|
socket.on('get-presence', async (friendIds, callback) => {
|
||
|
|
const presenceData = await presence.getBulkPresence(friendIds);
|
||
|
|
callback(presenceData);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Message Queue Pattern
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// Queue messages when client disconnected
|
||
|
|
const messageQueue = new Map();
|
||
|
|
|
||
|
|
io.on('connection', (socket) => {
|
||
|
|
const userId = socket.handshake.auth.userId;
|
||
|
|
|
||
|
|
// Deliver queued messages on connect
|
||
|
|
const queuedMessages = messageQueue.get(userId) || [];
|
||
|
|
if (queuedMessages.length > 0) {
|
||
|
|
socket.emit('queued-messages', queuedMessages);
|
||
|
|
messageQueue.delete(userId);
|
||
|
|
}
|
||
|
|
|
||
|
|
socket.on('disconnect', () => {
|
||
|
|
// Mark user as disconnected
|
||
|
|
setTimeout(() => {
|
||
|
|
const userSockets = io.sockets.sockets;
|
||
|
|
const hasOtherConnection = Array.from(userSockets.values())
|
||
|
|
.some(s => s.handshake.auth.userId === userId);
|
||
|
|
|
||
|
|
if (!hasOtherConnection) {
|
||
|
|
// User fully disconnected, queue new messages
|
||
|
|
messageQueue.set(userId, []);
|
||
|
|
}
|
||
|
|
}, 1000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Queue message if user offline
|
||
|
|
async function sendMessage(userId, message) {
|
||
|
|
const userOnline = await isUserOnline(userId);
|
||
|
|
|
||
|
|
if (userOnline) {
|
||
|
|
io.to(`user:${userId}`).emit('message', message);
|
||
|
|
} else {
|
||
|
|
const queue = messageQueue.get(userId) || [];
|
||
|
|
queue.push(message);
|
||
|
|
messageQueue.set(userId, queue);
|
||
|
|
|
||
|
|
// Persist to database for longer-term storage
|
||
|
|
await saveMessageToDb(userId, message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Pub/Sub Pattern
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
const EventEmitter = require('events');
|
||
|
|
|
||
|
|
class MessageBus extends EventEmitter {
|
||
|
|
constructor(io, redis) {
|
||
|
|
super();
|
||
|
|
this.io = io;
|
||
|
|
this.redis = redis;
|
||
|
|
this.setupSubscriptions();
|
||
|
|
}
|
||
|
|
|
||
|
|
setupSubscriptions() {
|
||
|
|
// Subscribe to Redis channels
|
||
|
|
this.redis.psubscribe('room:*', (err, count) => {
|
||
|
|
console.log(`Subscribed to ${count} channels`);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.redis.on('pmessage', (pattern, channel, message) => {
|
||
|
|
const data = JSON.parse(message);
|
||
|
|
const roomId = channel.split(':')[1];
|
||
|
|
|
||
|
|
// Emit to Socket.IO room
|
||
|
|
this.io.to(roomId).emit(data.event, data.payload);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
publish(roomId, event, payload) {
|
||
|
|
// Publish to Redis (distributed across servers)
|
||
|
|
this.redis.publish(
|
||
|
|
`room:${roomId}`,
|
||
|
|
JSON.stringify({ event, payload })
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage
|
||
|
|
const messageBus = new MessageBus(io, redisClient);
|
||
|
|
|
||
|
|
io.on('connection', (socket) => {
|
||
|
|
socket.on('send-message', ({ roomId, text }) => {
|
||
|
|
// Publish to all servers via Redis
|
||
|
|
messageBus.publish(roomId, 'message', {
|
||
|
|
userId: socket.id,
|
||
|
|
text,
|
||
|
|
timestamp: Date.now()
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## Backpressure Handling
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
io.on('connection', (socket) => {
|
||
|
|
const MAX_BUFFER_SIZE = 10000;
|
||
|
|
let bufferSize = 0;
|
||
|
|
|
||
|
|
const originalEmit = socket.emit.bind(socket);
|
||
|
|
|
||
|
|
socket.emit = function(event, ...args) {
|
||
|
|
bufferSize++;
|
||
|
|
|
||
|
|
if (bufferSize > MAX_BUFFER_SIZE) {
|
||
|
|
console.warn('Buffer overflow, dropping message');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = originalEmit(event, ...args);
|
||
|
|
|
||
|
|
// Track buffer drain
|
||
|
|
socket.once('drain', () => {
|
||
|
|
bufferSize = 0;
|
||
|
|
});
|
||
|
|
|
||
|
|
return result;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Monitor buffer size
|
||
|
|
socket.on('drain', () => {
|
||
|
|
console.log('Socket buffer drained');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|