Files
Portfolio/blog-posts/25-socketio-ai-chat.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

615 lines
16 KiB
Markdown

# 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<string, NodeJS.Timeout>();
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<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [isAITyping, setIsAITyping] = useState(false);
const [streamingMessage, setStreamingMessage] = useState<string>('');
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<HTMLDivElement>(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<HTMLInputElement>) => {
setInput(e.target.value);
if (e.target.value) {
startTyping();
} else {
stopTyping();
}
};
return (
<div className="flex flex-col h-full">
{/* Connection Status */}
<div className={`px-4 py-2 text-sm ${
isConnected ? 'bg-green-100' : 'bg-red-100'
}`}>
{isConnected ? 'Connected' : 'Reconnecting...'}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(message => (
<div
key={message.id}
className={`p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-100 ml-auto max-w-[80%]'
: 'bg-gray-100 mr-auto max-w-[80%]'
}`}
>
{message.content}
</div>
))}
{/* Streaming Message */}
{streamingMessage && (
<div className="bg-gray-100 mr-auto max-w-[80%] p-3 rounded-lg">
{streamingMessage}
<span className="animate-pulse"></span>
</div>
)}
{/* AI Typing Indicator */}
{isAITyping && !streamingMessage && (
<div className="bg-gray-100 mr-auto p-3 rounded-lg">
<span className="animate-pulse"> </span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="Nachricht eingeben..."
className="flex-1 px-4 py-2 border rounded-lg"
disabled={!isConnected}
/>
<button
type="submit"
disabled={!isConnected || !input.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
>
Senden
</button>
</div>
</form>
</div>
);
}
```
---
## 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/)