Menu
Zurück zum Blog
1 min read
KI-Entwicklung

Socket.IO für AI Chat Applications: Real-Time Implementation Guide

Production-ready AI Chat mit Socket.IO. Rooms, Typing Indicators, Message History und LLM-Streaming für skalierbare Chat-Anwendungen.

Socket.IOAI ChatReal-Time ChatNode.js ChatWebSocket ChatLLM Streaming
Socket.IO für AI Chat Applications: Real-Time Implementation Guide

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

import rateLimit from 'socket.io-rate-limiter';

io.use(rateLimit({
  windowMs: 1000,  // 1 Sekunde
  max: 10          // Max 10 Events pro Sekunde
}));

2. Input Validation

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

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