# Socket.IO für AI Chat Applications: Real-Time Implementation Guide **Meta-Description:** Production-ready AI Chat mit Socket.IO. Rooms, Typing Indicators, Message History und LLM-Streaming für skalierbare Chat-Anwendungen. **Keywords:** Socket.IO, AI Chat, Real-Time Chat, Node.js Chat, WebSocket Chat, LLM Streaming, Chat Application --- ## Einführung Socket.IO ist der De-facto-Standard für Real-Time-Kommunikation in Node.js. Für AI-Chat-Anwendungen bietet es **bidirektionale Events, automatische Reconnection und Room-basierte Isolation**. --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ AI CHAT WITH SOCKET.IO │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Client Server │ │ ┌─────────────┐ ┌─────────────────────┐ │ │ │ React │ socket.io │ Express + │ │ │ │ App │◄──────────────►│ Socket.IO │ │ │ └─────────────┘ └──────────┬──────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ LLM Service │ │ │ │ (Claude/GPT) │ │ │ └─────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ Redis │ │ │ │ (Sessions/Cache) │ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Server Implementation ### Basic Setup ```typescript // src/server.ts import express from 'express'; import { createServer } from 'http'; import { Server, Socket } from 'socket.io'; import { Redis } from 'ioredis'; import Anthropic from '@anthropic-ai/sdk'; const app = express(); const server = createServer(app); const io = new Server(server, { cors: { origin: process.env.CLIENT_URL, credentials: true }, pingTimeout: 60000, pingInterval: 25000 }); const redis = new Redis(process.env.REDIS_URL!); const anthropic = new Anthropic(); // Middleware io.use(async (socket, next) => { const token = socket.handshake.auth.token; try { const user = await verifyToken(token); socket.data.user = user; next(); } catch (error) { next(new Error('Authentication failed')); } }); // Connection Handler io.on('connection', (socket: Socket) => { const userId = socket.data.user.id; console.log(`User connected: ${userId}`); // Persönlicher Room für User socket.join(`user:${userId}`); setupChatHandlers(socket); setupTypingHandlers(socket); socket.on('disconnect', () => { console.log(`User disconnected: ${userId}`); }); }); server.listen(3000); ``` ### Chat Event Handlers ```typescript // src/handlers/chat.ts interface ChatMessage { id: string; conversationId: string; role: 'user' | 'assistant'; content: string; timestamp: Date; } function setupChatHandlers(socket: Socket) { const userId = socket.data.user.id; // Conversation beitreten socket.on('join:conversation', async (conversationId: string) => { // Berechtigung prüfen const hasAccess = await checkConversationAccess(userId, conversationId); if (!hasAccess) { socket.emit('error', { message: 'Access denied' }); return; } socket.join(`conversation:${conversationId}`); // History laden const history = await loadConversationHistory(conversationId); socket.emit('conversation:history', history); }); // Neue Nachricht senden socket.on('message:send', async (data: { conversationId: string; content: string; }) => { const { conversationId, content } = data; // User-Nachricht speichern const userMessage: ChatMessage = { id: crypto.randomUUID(), conversationId, role: 'user', content, timestamp: new Date() }; await saveMessage(userMessage); // An alle in der Conversation senden io.to(`conversation:${conversationId}`).emit('message:new', userMessage); // AI Response generieren await generateAIResponse(socket, conversationId, content); }); // Conversation verlassen socket.on('leave:conversation', (conversationId: string) => { socket.leave(`conversation:${conversationId}`); }); } ``` ### AI Response mit Streaming ```typescript // src/handlers/ai-response.ts async function generateAIResponse( socket: Socket, conversationId: string, userMessage: string ) { const responseId = crypto.randomUUID(); // Typing indicator starten io.to(`conversation:${conversationId}`).emit('ai:typing', { conversationId, isTyping: true }); try { // Conversation History laden const history = await loadConversationHistory(conversationId); // Stream starten const stream = await anthropic.messages.stream({ model: 'claude-3-haiku-20240307', max_tokens: 1000, system: 'Du bist ein hilfreicher Assistent.', messages: history.map(m => ({ role: m.role, content: m.content })) }); let fullContent = ''; // Token-by-Token streamen for await (const event of stream) { if (event.type === 'content_block_delta') { const delta = event.delta.text; fullContent += delta; // Delta an Client senden io.to(`conversation:${conversationId}`).emit('message:delta', { messageId: responseId, conversationId, delta, fullContent }); } } // Vollständige Nachricht speichern const assistantMessage: ChatMessage = { id: responseId, conversationId, role: 'assistant', content: fullContent, timestamp: new Date() }; await saveMessage(assistantMessage); // Completion Event io.to(`conversation:${conversationId}`).emit('message:complete', { messageId: responseId, conversationId }); } catch (error) { io.to(`conversation:${conversationId}`).emit('ai:error', { conversationId, error: 'AI response failed' }); } finally { // Typing indicator stoppen io.to(`conversation:${conversationId}`).emit('ai:typing', { conversationId, isTyping: false }); } } ``` ### Typing Indicators ```typescript // src/handlers/typing.ts function setupTypingHandlers(socket: Socket) { const userId = socket.data.user.id; const typingTimeouts = new Map(); socket.on('typing:start', (conversationId: string) => { // An andere User im Room senden socket.to(`conversation:${conversationId}`).emit('user:typing', { userId, conversationId, isTyping: true }); // Auto-Stop nach 3 Sekunden const existing = typingTimeouts.get(conversationId); if (existing) clearTimeout(existing); typingTimeouts.set(conversationId, setTimeout(() => { socket.to(`conversation:${conversationId}`).emit('user:typing', { userId, conversationId, isTyping: false }); }, 3000)); }); socket.on('typing:stop', (conversationId: string) => { const existing = typingTimeouts.get(conversationId); if (existing) clearTimeout(existing); socket.to(`conversation:${conversationId}`).emit('user:typing', { userId, conversationId, isTyping: false }); }); socket.on('disconnect', () => { // Cleanup typingTimeouts.forEach(timeout => clearTimeout(timeout)); }); } ``` --- ## Client Implementation ### React Hook ```typescript // src/hooks/useChat.ts import { useEffect, useState, useCallback, useRef } from 'react'; import { io, Socket } from 'socket.io-client'; interface Message { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; isStreaming?: boolean; } export function useChat(conversationId: string) { const socketRef = useRef(null); const [messages, setMessages] = useState([]); const [isConnected, setIsConnected] = useState(false); const [isAITyping, setIsAITyping] = useState(false); const [streamingMessage, setStreamingMessage] = useState(''); useEffect(() => { // Socket Connection const socket = io(process.env.NEXT_PUBLIC_WS_URL!, { auth: { token: getAuthToken() }, transports: ['websocket'] }); socketRef.current = socket; socket.on('connect', () => { setIsConnected(true); socket.emit('join:conversation', conversationId); }); socket.on('disconnect', () => { setIsConnected(false); }); // Event Handlers socket.on('conversation:history', (history: Message[]) => { setMessages(history); }); socket.on('message:new', (message: Message) => { setMessages(prev => [...prev, message]); }); socket.on('message:delta', ({ messageId, delta, fullContent }) => { setStreamingMessage(fullContent); }); socket.on('message:complete', ({ messageId }) => { setMessages(prev => [ ...prev, { id: messageId, role: 'assistant', content: streamingMessage, timestamp: new Date() } ]); setStreamingMessage(''); }); socket.on('ai:typing', ({ isTyping }) => { setIsAITyping(isTyping); }); socket.on('ai:error', ({ error }) => { console.error('AI Error:', error); setIsAITyping(false); }); return () => { socket.emit('leave:conversation', conversationId); socket.disconnect(); }; }, [conversationId]); const sendMessage = useCallback((content: string) => { if (socketRef.current) { socketRef.current.emit('message:send', { conversationId, content }); } }, [conversationId]); const startTyping = useCallback(() => { socketRef.current?.emit('typing:start', conversationId); }, [conversationId]); const stopTyping = useCallback(() => { socketRef.current?.emit('typing:stop', conversationId); }, [conversationId]); return { messages, streamingMessage, isConnected, isAITyping, sendMessage, startTyping, stopTyping }; } ``` ### Chat Component ```tsx // src/components/Chat.tsx import { useState, useRef, useEffect } from 'react'; import { useChat } from '../hooks/useChat'; export function Chat({ conversationId }: { conversationId: string }) { const { messages, streamingMessage, isConnected, isAITyping, sendMessage, startTyping, stopTyping } = useChat(conversationId); const [input, setInput] = useState(''); const messagesEndRef = useRef(null); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, streamingMessage]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage(input); setInput(''); stopTyping(); } }; const handleInputChange = (e: React.ChangeEvent) => { setInput(e.target.value); if (e.target.value) { startTyping(); } else { stopTyping(); } }; return (
{/* Connection Status */}
{isConnected ? 'Connected' : 'Reconnecting...'}
{/* Messages */}
{messages.map(message => (
{message.content}
))} {/* Streaming Message */} {streamingMessage && (
{streamingMessage}
)} {/* AI Typing Indicator */} {isAITyping && !streamingMessage && (
● ● ●
)}
{/* Input */}
); } ``` --- ## Scaling mit Redis Adapter ```typescript // src/server.ts import { createAdapter } from '@socket.io/redis-adapter'; import { createClient } from 'redis'; const pubClient = createClient({ url: process.env.REDIS_URL }); const subClient = pubClient.duplicate(); await Promise.all([pubClient.connect(), subClient.connect()]); io.adapter(createAdapter(pubClient, subClient)); // Jetzt können mehrere Server-Instanzen kommunizieren ``` --- ## Best Practices ### 1. Rate Limiting ```typescript import rateLimit from 'socket.io-rate-limiter'; io.use(rateLimit({ windowMs: 1000, // 1 Sekunde max: 10 // Max 10 Events pro Sekunde })); ``` ### 2. Input Validation ```typescript import { z } from 'zod'; const messageSchema = z.object({ conversationId: z.string().uuid(), content: z.string().min(1).max(4000) }); socket.on('message:send', async (data) => { const result = messageSchema.safeParse(data); if (!result.success) { socket.emit('error', { message: 'Invalid message format' }); return; } // Process... }); ``` ### 3. Error Handling ```typescript socket.on('error', (error) => { console.error('Socket error:', error); socket.emit('error', { message: 'An error occurred' }); }); io.engine.on('connection_error', (error) => { console.error('Connection error:', error); }); ``` --- ## Fazit Socket.IO für AI Chat bietet: 1. **Bidirektionale Kommunikation**: Perfekt für Streaming-Responses 2. **Rooms**: Isolation für Conversations 3. **Auto-Reconnection**: Robuste Verbindung 4. **Skalierbarkeit**: Redis Adapter für Multi-Server Für produktionsreife AI-Chat-Anwendungen ist Socket.IO die pragmatische Wahl. --- ## Bildprompts 1. "Chat interface with AI assistant, real-time message bubbles appearing, modern app design" 2. "WebSocket connection diagram between client and server, data flowing both ways" 3. "Multiple users in chat room with AI, collaborative interface, friendly tech illustration" --- ## Quellen - [Socket.IO Documentation](https://socket.io/docs/v4/) - [Socket.IO Best Practices](https://ably.com/topic/socketio) - [Building Real-Time Chat with Socket.IO](https://socket.io/get-started/chat) - [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/)