- 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>
471 lines
13 KiB
Markdown
471 lines
13 KiB
Markdown
# Server-Sent Events vs. WebSockets: Die richtige Wahl für Real-Time
|
||
|
||
**Meta-Description:** Detaillierter Vergleich von SSE und WebSockets. Unidirektionale vs. bidirektionale Kommunikation, Use Cases und Performance-Analyse.
|
||
|
||
**Keywords:** Server-Sent Events, WebSocket, SSE, Real-Time, Streaming, EventSource, HTTP Streaming, Push Notifications
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
Beide Technologien ermöglichen Server-Push – aber sie lösen unterschiedliche Probleme. SSE für **einfaches Streaming**, WebSockets für **bidirektionale Kommunikation**.
|
||
|
||
---
|
||
|
||
## Quick Comparison
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SSE vs. WEBSOCKET │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Server-Sent Events (SSE) WebSocket │
|
||
│ ───────────────────────── ───────────────────── │
|
||
│ │
|
||
│ Direction: Direction: │
|
||
│ Server → Client (only) Server ↔ Client (both) │
|
||
│ │
|
||
│ Protocol: Protocol: │
|
||
│ HTTP/1.1 or HTTP/2 ws:// or wss:// │
|
||
│ │
|
||
│ Reconnection: Reconnection: │
|
||
│ Built-in automatic Manual implementation │
|
||
│ │
|
||
│ Browser Support: Browser Support: │
|
||
│ All modern (no IE) All modern + IE10+ │
|
||
│ │
|
||
│ Use Cases: Use Cases: │
|
||
│ - LLM Streaming - Chat Applications │
|
||
│ - Live Feeds - Gaming │
|
||
│ - Notifications - Collaborative Editing │
|
||
│ - Stock Tickers - Voice/Video │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Detaillierter Vergleich
|
||
|
||
| Aspekt | SSE | WebSocket |
|
||
|--------|-----|-----------|
|
||
| **Richtung** | Unidirektional (Server→Client) | Bidirektional |
|
||
| **Protokoll** | HTTP | WS/WSS |
|
||
| **Datenformat** | Text (UTF-8) | Text + Binary |
|
||
| **Reconnection** | Automatisch | Manuell |
|
||
| **Event IDs** | Built-in | Manuell |
|
||
| **Proxy/Firewall** | Besser (HTTP) | Problematisch |
|
||
| **HTTP/2 Multiplexing** | Ja | Nein (eigene Connection) |
|
||
| **Max Connections** | Browser-Limit (~6/Domain) | Unbegrenzt |
|
||
| **Memory Overhead** | Niedriger | Höher |
|
||
| **Setup-Komplexität** | Einfach | Komplexer |
|
||
|
||
---
|
||
|
||
## SSE Implementation
|
||
|
||
### Server (Node.js/Express)
|
||
|
||
```typescript
|
||
// src/sse/server.ts
|
||
import express from 'express';
|
||
|
||
const app = express();
|
||
|
||
app.get('/events', (req, res) => {
|
||
// SSE Headers
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
|
||
// Flushes für sofortige Delivery
|
||
res.flushHeaders();
|
||
|
||
// Client-ID für Tracking
|
||
const clientId = Date.now();
|
||
console.log(`SSE client connected: ${clientId}`);
|
||
|
||
// Event senden
|
||
const sendEvent = (event: string, data: any, id?: string) => {
|
||
if (id) res.write(`id: ${id}\n`);
|
||
res.write(`event: ${event}\n`);
|
||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||
};
|
||
|
||
// Initial Event
|
||
sendEvent('connected', { clientId });
|
||
|
||
// Periodische Updates
|
||
const interval = setInterval(() => {
|
||
sendEvent('heartbeat', { timestamp: Date.now() });
|
||
}, 30000);
|
||
|
||
// Cleanup bei Disconnect
|
||
req.on('close', () => {
|
||
clearInterval(interval);
|
||
console.log(`SSE client disconnected: ${clientId}`);
|
||
});
|
||
});
|
||
|
||
app.listen(3000);
|
||
```
|
||
|
||
### Client (Browser)
|
||
|
||
```typescript
|
||
// src/sse/client.ts
|
||
class SSEClient {
|
||
private eventSource: EventSource | null = null;
|
||
private reconnectAttempts = 0;
|
||
|
||
connect(url: string) {
|
||
this.eventSource = new EventSource(url);
|
||
|
||
this.eventSource.onopen = () => {
|
||
console.log('SSE connected');
|
||
this.reconnectAttempts = 0;
|
||
};
|
||
|
||
this.eventSource.onerror = (error) => {
|
||
console.error('SSE error:', error);
|
||
// EventSource reconnects automatically
|
||
};
|
||
|
||
// Named Events
|
||
this.eventSource.addEventListener('connected', (event) => {
|
||
const data = JSON.parse(event.data);
|
||
console.log('Client ID:', data.clientId);
|
||
});
|
||
|
||
this.eventSource.addEventListener('heartbeat', (event) => {
|
||
const data = JSON.parse(event.data);
|
||
console.log('Heartbeat:', data.timestamp);
|
||
});
|
||
|
||
// Generic message event
|
||
this.eventSource.onmessage = (event) => {
|
||
console.log('Message:', event.data);
|
||
};
|
||
}
|
||
|
||
disconnect() {
|
||
this.eventSource?.close();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## SSE für LLM Streaming
|
||
|
||
### Perfekter Use Case: AI Response Streaming
|
||
|
||
```typescript
|
||
// src/api/chat.ts
|
||
import Anthropic from '@anthropic-ai/sdk';
|
||
|
||
app.post('/api/chat/stream', async (req, res) => {
|
||
const { message } = req.body;
|
||
|
||
// SSE Headers
|
||
res.setHeader('Content-Type', 'text/event-stream');
|
||
res.setHeader('Cache-Control', 'no-cache');
|
||
res.setHeader('Connection', 'keep-alive');
|
||
|
||
const anthropic = new Anthropic();
|
||
|
||
try {
|
||
const stream = await anthropic.messages.stream({
|
||
model: 'claude-3-haiku-20240307',
|
||
max_tokens: 1000,
|
||
messages: [{ role: 'user', content: message }]
|
||
});
|
||
|
||
for await (const event of stream) {
|
||
if (event.type === 'content_block_delta') {
|
||
res.write(`event: delta\n`);
|
||
res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
|
||
}
|
||
}
|
||
|
||
// Stream Ende
|
||
res.write(`event: done\n`);
|
||
res.write(`data: {}\n\n`);
|
||
res.end();
|
||
|
||
} catch (error) {
|
||
res.write(`event: error\n`);
|
||
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
||
res.end();
|
||
}
|
||
});
|
||
```
|
||
|
||
### Client für LLM Streaming
|
||
|
||
```typescript
|
||
// src/hooks/useStreamingChat.ts
|
||
export function useStreamingChat() {
|
||
const [response, setResponse] = useState('');
|
||
const [isStreaming, setIsStreaming] = useState(false);
|
||
|
||
const sendMessage = async (message: string) => {
|
||
setResponse('');
|
||
setIsStreaming(true);
|
||
|
||
const eventSource = new EventSource(
|
||
`/api/chat/stream?message=${encodeURIComponent(message)}`
|
||
);
|
||
|
||
eventSource.addEventListener('delta', (event) => {
|
||
const { text } = JSON.parse(event.data);
|
||
setResponse(prev => prev + text);
|
||
});
|
||
|
||
eventSource.addEventListener('done', () => {
|
||
eventSource.close();
|
||
setIsStreaming(false);
|
||
});
|
||
|
||
eventSource.addEventListener('error', (event) => {
|
||
console.error('Stream error');
|
||
eventSource.close();
|
||
setIsStreaming(false);
|
||
});
|
||
};
|
||
|
||
return { response, isStreaming, sendMessage };
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## WebSocket Implementation
|
||
|
||
### Server
|
||
|
||
```typescript
|
||
// src/websocket/server.ts
|
||
import { WebSocketServer, WebSocket } from 'ws';
|
||
|
||
const wss = new WebSocketServer({ port: 3001 });
|
||
|
||
wss.on('connection', (ws: WebSocket) => {
|
||
console.log('WebSocket client connected');
|
||
|
||
// Bidirektionale Kommunikation
|
||
ws.on('message', (data) => {
|
||
const message = JSON.parse(data.toString());
|
||
|
||
// Echo oder Processing
|
||
ws.send(JSON.stringify({
|
||
type: 'response',
|
||
data: `Received: ${message.content}`
|
||
}));
|
||
});
|
||
|
||
// Server-initiated Push
|
||
const interval = setInterval(() => {
|
||
if (ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({
|
||
type: 'heartbeat',
|
||
timestamp: Date.now()
|
||
}));
|
||
}
|
||
}, 30000);
|
||
|
||
ws.on('close', () => {
|
||
clearInterval(interval);
|
||
console.log('WebSocket client disconnected');
|
||
});
|
||
});
|
||
```
|
||
|
||
### Client
|
||
|
||
```typescript
|
||
// src/websocket/client.ts
|
||
class WebSocketClient {
|
||
private ws: WebSocket | null = null;
|
||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||
|
||
connect(url: string) {
|
||
this.ws = new WebSocket(url);
|
||
|
||
this.ws.onopen = () => {
|
||
console.log('WebSocket connected');
|
||
};
|
||
|
||
this.ws.onmessage = (event) => {
|
||
const message = JSON.parse(event.data);
|
||
this.handleMessage(message);
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket disconnected');
|
||
this.scheduleReconnect();
|
||
};
|
||
|
||
this.ws.onerror = (error) => {
|
||
console.error('WebSocket error:', error);
|
||
};
|
||
}
|
||
|
||
send(data: any) {
|
||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||
this.ws.send(JSON.stringify(data));
|
||
}
|
||
}
|
||
|
||
private handleMessage(message: any) {
|
||
switch (message.type) {
|
||
case 'response':
|
||
console.log('Response:', message.data);
|
||
break;
|
||
case 'heartbeat':
|
||
console.log('Heartbeat:', message.timestamp);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private scheduleReconnect() {
|
||
this.reconnectTimeout = setTimeout(() => {
|
||
this.connect(this.ws?.url || '');
|
||
}, 3000);
|
||
}
|
||
|
||
disconnect() {
|
||
if (this.reconnectTimeout) {
|
||
clearTimeout(this.reconnectTimeout);
|
||
}
|
||
this.ws?.close();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Entscheidungsmatrix
|
||
|
||
```typescript
|
||
function chooseProtocol(requirements: Requirements): 'SSE' | 'WebSocket' {
|
||
// WebSocket wenn bidirektional benötigt
|
||
if (requirements.bidirectional) {
|
||
return 'WebSocket';
|
||
}
|
||
|
||
// WebSocket für Binary Data
|
||
if (requirements.binaryData) {
|
||
return 'WebSocket';
|
||
}
|
||
|
||
// WebSocket für viele Connections
|
||
if (requirements.connectionsPerDomain > 6) {
|
||
return 'WebSocket';
|
||
}
|
||
|
||
// SSE für einfaches Streaming
|
||
if (requirements.useCase === 'llm-streaming' ||
|
||
requirements.useCase === 'notifications' ||
|
||
requirements.useCase === 'live-feed') {
|
||
return 'SSE';
|
||
}
|
||
|
||
// SSE für bessere Proxy-Kompatibilität
|
||
if (requirements.behindCorporateProxy) {
|
||
return 'SSE';
|
||
}
|
||
|
||
// Default: SSE (einfacher)
|
||
return 'SSE';
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Use Case Empfehlungen
|
||
|
||
| Use Case | Empfehlung | Grund |
|
||
|----------|------------|-------|
|
||
| **LLM Response Streaming** | SSE | Unidirektional, einfach |
|
||
| **Chat Application** | WebSocket | Bidirektional nötig |
|
||
| **Live Notifications** | SSE | Server-Push genügt |
|
||
| **Collaborative Editing** | WebSocket | Echtzzeit-Sync nötig |
|
||
| **Stock Ticker** | SSE | Nur Server→Client |
|
||
| **Online Gaming** | WebSocket | Low-Latency bidirektional |
|
||
| **File Upload Progress** | WebSocket | Client muss auch senden |
|
||
| **Social Feed Updates** | SSE | Einfacher Server-Push |
|
||
|
||
---
|
||
|
||
## Hybrid Approach
|
||
|
||
```typescript
|
||
// Kombiniere beide für optimale Ergebnisse
|
||
class HybridRealtime {
|
||
private sse: EventSource | null = null;
|
||
private ws: WebSocket | null = null;
|
||
|
||
connect(sseUrl: string, wsUrl: string) {
|
||
// SSE für Server-Push (Notifications, Updates)
|
||
this.sse = new EventSource(sseUrl);
|
||
this.sse.addEventListener('notification', this.handleNotification);
|
||
this.sse.addEventListener('update', this.handleUpdate);
|
||
|
||
// WebSocket für bidirektionale Kommunikation (Chat, Actions)
|
||
this.ws = new WebSocket(wsUrl);
|
||
this.ws.onmessage = this.handleWebSocketMessage;
|
||
}
|
||
|
||
// Sende nur über WebSocket
|
||
sendMessage(content: string) {
|
||
this.ws?.send(JSON.stringify({ type: 'chat', content }));
|
||
}
|
||
|
||
// Empfange über beide Kanäle
|
||
private handleNotification = (event: MessageEvent) => {
|
||
// SSE Notification
|
||
};
|
||
|
||
private handleWebSocketMessage = (event: MessageEvent) => {
|
||
// WebSocket Response
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
**Wähle SSE wenn:**
|
||
- Nur Server→Client Kommunikation nötig
|
||
- LLM/AI Response Streaming
|
||
- Einfachheit gewünscht
|
||
- Proxy-Kompatibilität wichtig
|
||
|
||
**Wähle WebSocket wenn:**
|
||
- Bidirektionale Kommunikation nötig
|
||
- Binary Data übertragen wird
|
||
- Sehr niedrige Latenz kritisch
|
||
- Viele simultane Verbindungen
|
||
|
||
Für LLM-Streaming ist SSE der klare Gewinner – einfacher, effizienter und perfekt für den Use Case.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Two data streams - one flowing one direction, one flowing both ways, comparison visualization"
|
||
2. "Server pushing data to multiple clients, server-sent events concept, clean diagram"
|
||
3. "Bidirectional communication tunnel between devices, WebSocket visualization, tech art"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [MDN: Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
||
- [MDN: WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||
- [HTTP/2 Server Push vs SSE](https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/)
|
||
- [Choosing Between SSE and WebSocket](https://ably.com/blog/websockets-vs-sse)
|