Als Node.js-Framework auf Serverseite bietet Nest.js die Verwendung von Websockets an. Websockets ermöglichen eine Verbindung zwischen Client und Server zum Austausch von Daten, ohne eine explizite Request auf Clientseite stellen zu müssen. Wie Websockets in Nest.js umgesetzt werden, wird in diesem Artikel dargestellt.

Nest.js nutzt eine Websocket API namens Socket.io, welche für Node.js geschrieben wurde. Socket.io bezeichnet sich selbst als “real-time, bidirectional and event-based communication”. Um die bidirektionale Kommunikation zu demonstrieren, wird im zweiten Abschnitt des Artikels eine kleine Client-Anwendung mit Angular geschrieben.

Neulingen von Nest.js empfehle ich meinen Artikel zu den Grundlagen von Nest.js.

Websocket einem Nest-Projekt hinzufügen

Wie eingangs schon erwähnt, nutzt Nest unter der Haube Socket.io. Daher ist einer der ersten Schritte das Hinzufügen von Socket.io zum Projekt:

$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
$ npm i --save-dev @types/socket.io

Mit dem Decorator @WebSocketGateway wird die Funktionalität von Socket.io einer Klasse hinzugefügt. Diese Klasse nennt man Gateway. Darüber hinaus gibt es die Interfaces OnGatewayConnection und OnGatewayDisconnect. Sie sind dazu da, bei Verbindungsaufbau und -abbruch Aktionen auszuführen.

Im folgenden Beispiel werden wir eine Liveticker-App schreiben. Deshalb heißt der Handler @SubscribeMessage('liveticker') handleLiveticker(). Und wir legen einen Websocket-Server mit @WebSocketServer an:

@WebSocketGateway()
export class LivetickerGateway implements OnGatewayConnection, OnGatewayDisconnect {

    @WebSocketServer() server: Server;

    views: number = 0;

    async handleConnection() {
        this.views++;
        this.server.emit('views', this.views);
    }

    async handleDisconnect() {
        this.views--;
        this.server.emit('views', this.views);
    }

    @SubscribeMessage('liveticker')
    handleLiveticker(client: Client, data: unknown): Observable<WsResponse<TickerMessage>> {
        return from(mockData).pipe(
            concatMap((tickerMessage: TickerMessage) => {
                return of({event: 'liveticker', data: tickerMessage}).pipe(
                    delay(2000),
                );
            }),
        );
    }

}

// Abb. 1

Mit der Variable views tracken wir die Anzahl der aktuellen Verbindungen. Immer wenn sie sich ändert, emittet der Server den neuen Wert.

handleLiveticker gibt ein Observable zurück. Die Funktion nutzt Mockdaten, welche alle zwei Sekunden (delay(2000)) übertragen werden. Das gesamte Projekt ist auf GitHub veröffentlicht. Dort können auch die Mockdaten eingesehen werden.

Das LivetickerGateway ist Teil des LivetickerModule, welches wiederum im ApplicationModule importiert wird.

Websocket im Client einbinden

Zum Einbdinden des Websockets im Client erstellen wir mit der Angular CLI eine Anwendung und importieren ‘ngx-socket-io’:

$ npm i --save ngx-socket-io

Danach kreieren wir einen ApiService, welcher wie folgt aussieht:

import {Socket} from 'ngx-socket-io';
// Weitere imports 

@Injectable({
    providedIn: 'root'
})
export class ApiService {

    constructor(
        private socket: Socket,
    ) {
    }

    receiveUpdates(): Observable<TickerMessage> {
        this.socket.emit('liveticker');
        return this.socket.fromEvent('liveticker');
    }

    getViews(): Observable<number> {
        return this.socket.fromEvent('views');
    }

}

// Abb. 2

Der Service greift auf zwei Events des Websockets zu: ‘views’ und ‘liveticker’. ‘liveticker’ stellt die Daten des Livetickers bereit und ‘views’ gibt Auskunft, wieviele Personen im Augenblick den Liveticker verfolgen.

Der ApiService wird in einer Komponente injiziert und benutzt:

export class AppComponent implements OnInit {
  views: Observable<number>;
  private tickerMessages: TickerMessage[];

  constructor(
    private apiService: ApiService,
  ) {
    this.tickerMessages = [];
  }

  ngOnInit(): void {
    this.apiService.receiveUpdates().subscribe((tickerMessage: TickerMessage) => {
      console.log('Receiving update:', tickerMessage);
      this.tickerMessages.push(tickerMessage);
    });

    this.views = this.apiService.getViews();
  }

}

// Abb. 3

Wir subscriben auf apiService.receiveUpdates() und pushen jede neue tickerMessage in ein Array. Das Array wird in einer Tabelle im HTML angezeigt:

<table border="1px black solid">
  <thead>
  <tr>
    <th>Minute</th>
    <th>Aktion</th>
    <th>Beschreibung</th>
  </tr>
  </thead>
  <tbody>
  <tr *ngFor="let tickerMessage of tickerMessages">
    <td>{{tickerMessage.timeInSec | secToMin}}</td>
    <td>{{tickerMessage.type}}</td>
    <td>{{tickerMessage.message}}</td>
  </tr>
  </tbody>
</table>

// Abb. 4

Die Pipe secToMin formatiert die Zeitangabe. Der Code dazu ist ebenfalls auf GitHub zu finden.

Nun kann der Server mit npm run start und der Client mit ng serve gestartet werden. Abschließend ein Screenshot der Anwendung zum Zeitpunkt, wo gerade zwei Verbindungen zum Server bestehen und zwei der drei Mockdaten bereits gesendet wurden: