Help me een WebSocket-aanwezigheidssysteem implementeren om online gebruikers te volgen.
description
Door het implementeren van een aanwezigheidssysteem kan ik de betrokkenheid van gebruikers vergroten, de interactiemogelijkheden verbeteren en gebruikers realtime inzicht geven in online activiteit.
prompt
try_prompt
Ik wil een WebSocket-aanwezigheidssysteem ontwikkelen waarmee ik in realtime kan volgen welke gebruikers online zijn. Mijn servertechnologie is: {{server_technologie}}. Mijn clienttechnologie is: {{client_technologie}}. De primaire gebruiksfunctie voor deze aanwezigheids ... more
generate_helper
...
tags
ai_answers
provider: openai
model: gpt-4.1-nano
Zeker! Hier zijn architecturale aanbevelingen, ontwerpprincipes en voorbeeldcode om een efficiënt en betrouwbaar WebSocket-gebaseerd aanwezigheidssysteem te ontwikkelen voor jouw chatapplicatie met Node.js en React. Focus ligt op het omgaan met disconnects en het behouden van accurate online status.
### Architecturale Aanbevelingen
1. **Gebruik een WebSocket-server op de backend**
- Bijvoorbeeld met `ws` of `socket.io` (aanbevolen wegens ingebouwde reconnect en presence functionaliteiten).
- Zorg dat de server gebruikers identificeert via een unieke gebruikers-ID of sessie-token.
2. **Identificatie en Authenticatie**
- Bij verbinding verstuur je authenticatietokens (bijv. JWT) om te verifiëren wie er verbonden is.
- Sla bij connectie de gebruikersstatus op in een centrale dataopslag (bijvoorbeeld Redis).
3. **Presence State Management**
- Bewaar de online status in een snelle, gedeelde data-store zoals Redis.
- Gebruik bijvoorbeeld Redis Sets (`SADD`, `SREM`) voor het bijhouden van online gebruikers.
4. **Heartbeat Mechanisme**
- Clients sturen periodiek een heartbeat (ping) naar de server.
- Server markeert gebruikers als offline als er geen heartbeat binnen een bepaalde timeout wordt ontvangen.
5. **Handling Disconnections**
- Gebruik WebSocket-events zoals `close` en `error` om disconnects te detecteren.
- Bij een disconnect verwijder je de gebruiker uit de online set en zendt je een update naar andere clients.
6. **Reconnection Strategie**
- Client-side: implementeer automatische reconnections met backoff.
- Server-side: bij reconnection blijft de gebruiker herkend via de authenticatie-token en wordt de aanwezigheidstatus hersteld.
7. **Realtime Updates**
- Wanneer de status van een gebruiker verandert, stuur een broadcast naar alle verbonden clients over wie online is.
---
### Ontwerpprincipes
- **Robuustheid**: Zorg dat disconnects correct worden afgehandeld, zodat de status altijd up-to-date is.
- **Schaalbaarheid**: Gebruik Redis of een andere centrale dataopslag om aanwezigheid te beheren, zodat meerdere serverinstantiaties kunnen draaien.
- **Beveiliging**: Verifieer clients bij connectie en voorkom spoofing van status updates.
- **Gebruik van bestaande bibliotheken**: `socket.io` biedt ingebouwde reconnects en heartbeat-mechanismen, wat het eenvoudiger maakt.
---
### Voorbeeldimplementatie
#### Server-side (Node.js met socket.io)
```javascript
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const redis = require('redis');
const app = express();
const server = http.createServer(app);
const io = socketIO(server);
const redisClient = redis.createClient();
const ONLINE_USERS_SET = 'online_users';
// Helper: voeg gebruiker toe
function markUserOnline(userId) {
redisClient.sadd(ONLINE_USERS_SET, userId);
}
// Helper: verwijder gebruiker
function markUserOffline(userId) {
redisClient.srem(ONLINE_USERS_SET, userId);
}
// Helper: haal alle online gebruikers
function getOnlineUsers(callback) {
redisClient.smembers(ONLINE_USERS_SET, callback);
}
io.on('connection', (socket) => {
console.log('Client verbonden');
// Veronderstel dat client authenticatie token meestuurt
socket.on('authenticate', (token) => {
// Verifieer token en krijg userId
const userId = verifyToken(token); // implementeer je eigen verificatie
if (userId) {
socket.userId = userId;
markUserOnline(userId);
// Notify alle clients over nieuwe online status
broadcastPresence();
// Start heartbeat
socket.isAlive = true;
}
});
// Heartbeat ontvangen
socket.on('heartbeat', () => {
socket.isAlive = true;
});
// Bij disconnect
socket.on('disconnect', () => {
if (socket.userId) {
markUserOffline(socket.userId);
broadcastPresence();
}
});
});
// Periodieke check op dead connections
setInterval(() => {
io.clients((error, clients) => {
if (error) throw error;
clients.forEach((socketId) => {
const socket = io.sockets.sockets.get(socketId);
if (socket) {
if (socket.isAlive === false) {
socket.disconnect(true);
} else {
socket.isAlive = false;
socket.emit('ping');
}
}
});
});
}, 30000); // elke 30 seconden
// Broadcast online users
function broadcastPresence() {
getOnlineUsers((err, users) => {
io.emit('presenceUpdate', users);
});
}
function verifyToken(token) {
// Voeg hier je JWT-verificatie toe
// Retourneer userId of null
return someVerificationFunction(token);
}
server.listen(3000, () => {
console.log('Server gestart op poort 3000');
});
```
#### Client-side (React met socket.io-client)
```jsx
import { useEffect } from 'react';
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
function App() {
useEffect(() => {
// Authenticatie
socket.emit('authenticate', 'YOUR_JWT_TOKEN');
// Ontvang aanwezigheid updates
socket.on('presenceUpdate', (users) => {
console.log('Online gebruikers:', users);
});
// Heartbeat
const heartbeatInterval = setInterval(() => {
socket.emit('heartbeat');
}, 10000); // elke 10 seconden
// Handle pings van server
socket.on('ping', () => {
socket.emit('heartbeat');
});
return () => {
clearInterval(heartbeatInterval);
socket.disconnect();
};
}, []);
return (
<div>
{/* UI componenten */}
</div>
);
}
export default App;
```
---
### Samenvatting
- Gebruik `socket.io` voor betrouwbaarheid en reconnects.
- Beheer aanwezigheid via Redis Sets.
- Implementeer heartbeat en disconnect-handler om disconnects correct te verwerken.
- Verspreid aanwezigheidssituaties realtime naar alle clients.
- Beveilig de verbinding met authenticatie.
Door deze aanpak krijg je een schaalbaar, robuust en realtime aanwezigheidssysteem dat goed omgaat met disconnects.
provider: deepseek
model: deepseek-chat
Zeker, hier een uitgebreid antwoord voor het bouwen van een WebSocket-aanwezigheidssysteem met Node.js en React voor een chatapplicatie, specifiek gericht op het afhandelen van disconnects.
## Architecturale Aanbevelingen
### 1. Server-Side Architectuur
```javascript
// Aanbevolen structuur
class PresenceSystem {
constructor() {
this.activeUsers = new Map(); // userID -> UserInfo
this.roomPresence = new Map(); // roomID -> Set van userIDs
}
addUser(userId, userInfo, roomId) {
this.activeUsers.set(userId, { ...userInfo, lastSeen: Date.now(), roomId });
this._addToRoom(roomId, userId);
}
removeUser(userId) {
const user = this.activeUsers.get(userId);
if (user) {
this._removeFromRoom(user.roomId, userId);
this.activeUsers.delete(userId);
}
}
}
```
### 2. Client-Side State Management
```javascript
// React Context voor aanwezigheid
import React, { createContext, useContext, useReducer } from 'react';
const PresenceContext = createContext();
const presenceReducer = (state, action) => {
switch (action.type) {
case 'USER_ONLINE':
return {
...state,
onlineUsers: { ...state.onlineUsers, [action.userId]: action.userInfo }
};
case 'USER_OFFLINE':
const { [action.userId]: removed, ...remaining } = state.onlineUsers;
return { ...state, onlineUsers: remaining };
case 'SET_CURRENT_USER':
return { ...state, currentUser: action.userId };
default:
return state;
}
};
```
## Ontwerpprincipes voor Disconnect Handling
### 1. Heartbeat Mechanisme
```javascript
// Server-side heartbeat
class HeartbeatManager {
constructor(presenceSystem, interval = 30000) {
this.presenceSystem = presenceSystem;
this.interval = interval;
this.startHeartbeat();
}
startHeartbeat() {
setInterval(() => {
const now = Date.now();
for (const [userId, userInfo] of this.presenceSystem.activeUsers) {
if (now - userInfo.lastSeen > this.interval * 2) {
console.log(`User ${userId} timeout, marking as offline`);
this.presenceSystem.removeUser(userId);
this._broadcastUserOffline(userId);
}
}
}, this.interval);
}
}
```
### 2. Graceful Disconnect Handling
```javascript
// Client-side WebSocket management
class WebSocketManager {
constructor(url) {
this.url = url;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
this.sendPresence('online');
};
this.ws.onclose = (event) => {
console.log('WebSocket disconnected:', event);
this.handleReconnection();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
handleReconnection() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++;
console.log(`Reconnecting... attempt ${this.reconnectAttempts}`);
this.connect();
}, this.reconnectDelay * this.reconnectAttempts);
}
}
sendPresence(status) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'presence',
status: status,
userId: this.userId,
timestamp: Date.now()
}));
}
}
}
```
## Implementatie Voorbeelden
### Server-side (Node.js + WebSocket)
```javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
class PresenceServer {
constructor() {
this.presenceSystem = new PresenceSystem();
this.heartbeatManager = new HeartbeatManager(this.presenceSystem);
}
start() {
wss.on('connection', (ws, request) => {
console.log('Nieuwe client verbonden');
const userId = this.extractUserId(request);
const roomId = this.extractRoomId(request);
// Gebruiker toevoegen aan aanwezigheidssysteem
this.presenceSystem.addUser(userId, { ws }, roomId);
// Broadcast naar andere gebruikers
this.broadcastToRoom(roomId, {
type: 'user_online',
userId: userId
});
ws.on('message', (message) => {
this.handleMessage(ws, userId, message);
});
ws.on('close', () => {
console.log(`Client ${userId} verbroken`);
this.handleDisconnection(userId, roomId);
});
ws.on('error', (error) => {
console.error(`WebSocket error voor user ${userId}:`, error);
this.handleDisconnection(userId, roomId);
});
});
}
handleDisconnection(userId, roomId) {
// Directe disconnect handling
this.presenceSystem.removeUser(userId);
// Broadcast offline status
this.broadcastToRoom(roomId, {
type: 'user_offline',
userId: userId,
timestamp: Date.now()
});
}
handleMessage(ws, userId, message) {
try {
const data = JSON.parse(message);
switch (data.type) {
case 'heartbeat':
// Update last seen
const user = this.presenceSystem.activeUsers.get(userId);
if (user) {
user.lastSeen = Date.now();
}
break;
case 'presence':
this.handlePresenceUpdate(userId, data);
break;
}
} catch (error) {
console.error('Ongeldig bericht:', error);
}
}
broadcastToRoom(roomId, message) {
const roomUsers = this.presenceSystem.roomPresence.get(roomId);
if (roomUsers) {
roomUsers.forEach(userId => {
const user = this.presenceSystem.activeUsers.get(userId);
if (user && user.ws.readyState === WebSocket.OPEN) {
user.ws.send(JSON.stringify(message));
}
});
}
}
}
```
### Client-side (React Hook)
```javascript
import { useEffect, useRef, useState } from 'react';
export const usePresence = (userId, roomId) => {
const [onlineUsers, setOnlineUsers] = useState({});
const [isConnected, setIsConnected] = useState(false);
const wsManagerRef = useRef(null);
const heartbeatIntervalRef = useRef(null);
useEffect(() => {
// Initialiseer WebSocket manager
wsManagerRef.current = new WebSocketManager(`ws://localhost:8080?userId=${userId}&roomId=${roomId}`);
// Stel heartbeat in
heartbeatIntervalRef.current = setInterval(() => {
if (wsManagerRef.current) {
wsManagerRef.current.sendPresence('heartbeat');
}
}, 25000); // Elke 25 seconden
// Cleanup bij unmount
return () => {
if (heartbeatIntervalRef.current) {
clearInterval(heartbeatIntervalRef.current);
}
if (wsManagerRef.current) {
wsManagerRef.current.sendPresence('offline');
}
};
}, [userId, roomId]);
// WebSocket event handlers
useEffect(() => {
const ws = wsManagerRef.current?.ws;
if (!ws) return;
const handleMessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'user_online':
setOnlineUsers(prev => ({
...prev,
[data.userId]: { status: 'online', lastSeen: Date.now() }
}));
break;
case 'user_offline':
setOnlineUsers(prev => {
const { [data.userId]: removed, ...remaining } = prev;
return remaining;
});
break;
case 'presence_update':
// Update specifieke gebruiker status
setOnlineUsers(prev => ({
...prev,
[data.userId]: { ...prev[data.userId], ...data.userInfo }
}));
break;
}
};
ws.addEventListener('message', handleMessage);
return () => ws.removeEventListener('message', handleMessage);
}, []);
return { onlineUsers, isConnected };
};
```
### Gebruik in React Component
```javascript
import React from 'react';
import { usePresence } from './usePresence';
const ChatRoom = ({ roomId, currentUser }) => {
const { onlineUsers, isConnected } = usePresence(currentUser.id, roomId);
return (
<div className="chat-room">
<div className="presence-indicator">
<span className={`status ${isConnected ? 'online' : 'offline'}`}>
{isConnected ? 'Online' : 'Offline'}
</span>
<div className="online-users">
<h4>Online Gebruikers ({Object.keys(onlineUsers).length})</h4>
{Object.entries(onlineUsers).map(([userId, userInfo]) => (
<div key={userId} className="user-status">
<span className="user-name">{userInfo.name}</span>
<span className="status-dot online"></span>
</div>
))}
</div>
</div>
{/* Chat componenten */}
</div>
);
};
```
## Aanvullende Tips
1. **Exponentiële Backoff**: Implementeer exponentiële backoff voor reconnect pogingen
2. **Local Storage**: Sla connection state lokaal op voor page refresh scenarios
3. **Status Indicatoren**: Toon duidelijke status (online/offline/connecting)
4. **Error Boundaries**: Wrap WebSocket code in React error boundaries
5. **Monitoring**: Log disconnect redenen voor debugging
Dit systeem biedt robuuste disconnect handling met automatische reconnects, heartbeat monitoring en real-time status updates voor je chat applicatie.

