24 WebSockets in Angular - Kompakter Leitfaden

24.1 Grundlagen von WebSockets

WebSockets bieten eine bidirektionale, voll-duplexe Kommunikation zwischen Client und Server über eine dauerhafte Verbindung. Im Gegensatz zu HTTP-Anfragen ermöglichen WebSockets kontinuierliche Verbindungen für Echtzeitdaten.

Vorteile: - Echtzeitkommunikation ohne wiederholte Anfragen - Geringere Latenz und reduzierter Overhead - Bidirektionale Kommunikation

Typische Anwendungsfälle: - Chat-Anwendungen - Live-Updates für Dashboards - Multiplayer-Spiele - Kollaborative Editoren - Finanzanwendungen mit Kursdaten

24.2 1. Native WebSocket-API in Angular

Der einfachste Ansatz ist die Verwendung der nativen WebSocket-API in einem Angular-Service:

// websocket.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class WebSocketService {
    private socket: WebSocket;
    private messagesSubject = new BehaviorSubject<any[]>([]);
    public messages$ = this.messagesSubject.asObservable();

    constructor() {}

    public connect(url: string): void {
        this.socket = new WebSocket(url);

        this.socket.onopen = (event) => {
            console.log('WebSocket verbunden:', event);
        };

        this.socket.onmessage = (event) => {
            const message = JSON.parse(event.data);
            const currentMessages = this.messagesSubject.value;
            this.messagesSubject.next([...currentMessages, message]);
        };

        this.socket.onerror = (event) => {
            console.error('WebSocket-Fehler:', event);
        };

        this.socket.onclose = (event) => {
            console.log('WebSocket-Verbindung geschlossen:', event);
        };
    }

    public sendMessage(message: any): void {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(message));
        } else {
            console.error('WebSocket ist nicht verbunden.');
        }
    }

    public disconnect(): void {
        if (this.socket) {
            this.socket.close();
        }
    }
}

Verwendung in einer Komponente:

// chat.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebSocketService } from './websocket.service';
import { Subscription } from 'rxjs';

@Component({
    selector: 'app-chat',
    template: `
    <div class="chat-container">
      <div class="messages">
        <div *ngFor="let msg of messages">{{ msg.text }}</div>
      </div>
      <div class="input-area">
        <input [(ngModel)]="messageText" placeholder="Nachricht eingeben..." />
        <button (click)="sendMessage()">Senden</button>
      </div>
    </div>
  `
})
export class ChatComponent implements OnInit, OnDestroy {
    messages: any[] = [];
    messageText: string = '';
    private subscription: Subscription;

    constructor(private webSocketService: WebSocketService) {}

    ngOnInit() {
        this.webSocketService.connect('wss://example.com/chat');
        this.subscription = this.webSocketService.messages$.subscribe(messages => {
            this.messages = messages;
        });
    }

    sendMessage() {
        if (this.messageText.trim()) {
            this.webSocketService.sendMessage({ text: this.messageText });
            this.messageText = '';
        }
    }

    ngOnDestroy() {
        this.webSocketService.disconnect();
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }
}

24.3 2. RxJS mit WebSockets

Für reaktive Anwendungen ist die Integration von RxJS mit WebSockets ideal:

// rxjs-websocket.service.ts
import { Injectable } from '@angular/core';
import { Observable, Subject, BehaviorSubject } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { catchError, tap, switchAll, takeUntil } from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class RxjsWebSocketService {
    private socket$: WebSocketSubject<any>;
    private messagesSubject = new Subject<Observable<any>>();
    public messages$ = this.messagesSubject.pipe(switchAll());
    private connectionStatus = new BehaviorSubject<boolean>(false);
    public connectionStatus$ = this.connectionStatus.asObservable();
    private destroy$ = new Subject<void>();

    public connect(url: string): void {
        if (!this.socket$ || this.socket$.closed) {
            this.socket$ = webSocket(url);

            const messages = this.socket$.pipe(
                tap({
                    next: () => this.connectionStatus.next(true),
                    error: error => {
                        console.error('WebSocket-Fehler:', error);
                        this.connectionStatus.next(false);
                    },
                    complete: () => this.connectionStatus.next(false)
                }),
                takeUntil(this.destroy$),
                catchError(error => {
                    console.error('WebSocket-Fehler, versuche Wiederverbindung:', error);
                    return this.connect(url);
                })
            );

            this.messagesSubject.next(messages);
        }
    }

    public sendMessage(message: any): void {
        if (this.socket$ && !this.socket$.closed) {
            this.socket$.next(message);
        }
    }

    public disconnect(): void {
        if (this.socket$) {
            this.socket$.complete();
            this.destroy$.next();
            this.connectionStatus.next(false);
        }
    }
}

24.4 3. Socket.IO mit Angular

Socket.IO bietet erweiterte Funktionen wie automatische Wiederverbindung:

npm install socket.io-client @types/socket.io-client
// socket-io.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { io, Socket } from 'socket.io-client';

@Injectable({
    providedIn: 'root'
})
export class SocketIoService {
    private socket: Socket;

    connect(url: string, options?: any): void {
        this.socket = io(url, options);
    }

    disconnect(): void {
        if (this.socket) {
            this.socket.disconnect();
        }
    }

    on<T>(eventName: string): Observable<T> {
        return new Observable<T>(observer => {
            this.socket.on(eventName, (data: T) => {
                observer.next(data);
            });

            return () => {
                this.socket.off(eventName);
            };
        });
    }

    emit(eventName: string, data?: any): void {
        this.socket.emit(eventName, data);
    }

    joinRoom(room: string): void {
        this.socket.emit('join', room);
    }

    leaveRoom(room: string): void {
        this.socket.emit('leave', room);
    }
}

24.5 4. SignalR für .NET-Backends

Für .NET-Backends ist SignalR eine gute Option:

npm install @microsoft/signalr
// signalr.service.ts
import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
    providedIn: 'root'
})
export class SignalRService {
    private hubConnection: HubConnection;
    private connectionStatus = new BehaviorSubject<boolean>(false);
    public connectionStatus$ = this.connectionStatus.asObservable();

    public createConnection(url: string): void {
        this.hubConnection = new HubConnectionBuilder()
            .withUrl(url)
            .withAutomaticReconnect()
            .build();

        this.hubConnection.onreconnecting(() => this.connectionStatus.next(false));
        this.hubConnection.onreconnected(() => this.connectionStatus.next(true));
        this.hubConnection.onclose(() => this.connectionStatus.next(false));
    }

    public startConnection(): Promise<void> {
        return this.hubConnection.start()
            .then(() => {
                this.connectionStatus.next(true);
            })
            .catch(err => {
                console.error('SignalR-Verbindungsfehler:', err);
                throw err;
            });
    }

    public stopConnection(): Promise<void> {
        return this.hubConnection.stop()
            .then(() => {
                this.connectionStatus.next(false);
            });
    }

    public on<T>(methodName: string, callback: (data: T) => void): void {
        this.hubConnection.on(methodName, callback);
    }

    public invoke<T>(methodName: string, ...args: any[]): Promise<T> {
        return this.hubConnection.invoke<T>(methodName, ...args);
    }
}

24.6 5. WebSockets mit Angular Signals (Angular 16+)

Ab Angular 16 können Signals für reaktive WebSocket-Integration verwendet werden:

// websocket-signals.service.ts
import { Injectable, signal, computed } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class WebSocketSignalsService {
    private socket: WebSocket | null = null;

    // Signals
    public connected = signal<boolean>(false);
    public messages = signal<any[]>([]);
    public error = signal<string | null>(null);

    // Computed Values
    public messageCount = computed(() => this.messages().length);
    public lastMessage = computed(() => {
        const msgs = this.messages();
        return msgs.length > 0 ? msgs[msgs.length - 1] : null;
    });

    connect(url: string): void {
        if (this.socket) {
            this.disconnect();
        }

        this.socket = new WebSocket(url);

        this.socket.onopen = () => {
            this.connected.set(true);
            this.error.set(null);
        };

        this.socket.onmessage = (event) => {
            try {
                const message = JSON.parse(event.data);
                this.messages.update(msgs => [...msgs, message]);
            } catch (err) {
                console.error('Fehler beim Parsen der WebSocket-Nachricht:', err);
            }
        };

        this.socket.onerror = () => {
            this.error.set('WebSocket-Fehler aufgetreten');
        };

        this.socket.onclose = () => {
            this.connected.set(false);
        };
    }

    disconnect(): void {
        if (this.socket) {
            this.socket.close();
            this.socket = null;
        }
    }

    sendMessage(message: any): void {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(message));
        } else {
            this.error.set('Nachricht kann nicht gesendet werden: WebSocket ist nicht verbunden');
        }
    }

    clearMessages(): void {
        this.messages.set([]);
    }
}

24.7 Sicherheitsaspekte bei WebSockets

24.8 Best Practices

  1. Verbindungsmanagement:
  2. Fehlerbehandlung:
  3. Performanz:
  4. Ressourcenmanagement:

24.9 Moderne WebSocket-Features in Angular

24.10 Beispiel einer modernen WebSocket-Implementierung mit Signals

// chat.component.ts
import { Component, inject, signal, effect, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { WebSocketSignalsService } from './websocket-signals.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
    selector: 'app-chat',
    standalone: true,
    template: `
    <div class="chat-container">
      <header>
        <h1>Chat</h1>
        <div class="connection-status" [class.connected]="wsService.connected()">
          {{ wsService.connected() ? 'Verbunden' : 'Getrennt' }}
        </div>
      </header>
      
      @defer {
        <div class="messages">
          @for (message of wsService.messages(); track message.id) {
            <div class="message" [class.own]="message.sender === username()">
              <strong>{{ message.sender }}</strong>: {{ message.text }}
            </div>
          } @empty {
            <div class="no-messages">Keine Nachrichten</div>
          }
        </div>
      } @loading {
        <div class="loading">Lade Nachrichten...</div>
      }
      
      <div class="input-area">
        <input [formControl]="messageInput" placeholder="Nachricht eingeben..." />
        <button (click)="sendMessage()" [disabled]="!wsService.connected()">Senden</button>
      </div>
    </div>
  `
})
export class ChatComponent implements OnDestroy {
    wsService = inject(WebSocketSignalsService);
    messageInput = new FormControl('');
    username = signal<string>('Benutzer' + Math.floor(Math.random() * 1000));

    constructor() {
        // Verbindung beim Start herstellen
        this.wsService.connect('wss://example.com/chat');

        // Verbindungsstatus überwachen mit effect()
        effect(() => {
            if (this.wsService.connected()) {
                console.log('Verbindung hergestellt, sende Anmeldung');
                this.wsService.sendMessage({
                    type: 'login',
                    username: this.username()
                });
            }
        });
    }

    sendMessage() {
        const text = this.messageInput.value?.trim();
        if (text && this.wsService.connected()) {
            this.wsService.sendMessage({
                type: 'message',
                sender: this.username(),
                text,
                timestamp: new Date().toISOString()
            });
            this.messageInput.setValue('');
        }
    }

    ngOnDestroy() {
        this.wsService.disconnect();
    }
}

WebSockets in Angular bieten eine leistungsstarke Möglichkeit für Echtzeitkommunikation in modernen Webanwendungen. Mit den passenden Tools und Best Practices können robuste, sichere und performante Echtzeitfunktionen implementiert werden.

Für die meisten Anwendungsfälle reicht einer der folgenden Ansätze: - Native WebSockets für einfache Anwendungen - RxJS-WebSockets für reaktive Programmierung - Socket.IO für zusätzliche Features und Browserkompatibilität - SignalR für .NET-Backends - Angular Signals für moderne reaktive Zustandsverwaltung