Direkt zum Inhalt

Implementierung eines Instant-Messaging-Systems mit Mercure

Inhaltsverzeichnis
  1. Einführung in Mercure
  2. Systemarchitektur
  3. Installation und Konfiguration von Mercure
  4. Apache und SSL Konfiguration
  5. JWT-Schlüssel Erstellung
  6. Backend-Implementierungsbeispiel
  7. Frontend-Implementierungsbeispiel

1. Einführung in Mercure

Mercure ist ein Echtzeit-Kommunikationsprotokoll, das auf Server-Sent Events (SSE) basiert. Im Gegensatz zu WebSockets verwendet Mercure das standardmäßige HTTP-Protokoll, was die Implementierung einfacher macht und mit HTTP-Caching kompatibel ist.

Vorteile von Mercure

  • Einfach: Verwendet HTTP/1.1 und HTTP/2
  • Ressourcenschonend: Weniger Ressourcen als WebSockets
  • Kompatibel: Funktioniert hinter Proxies
  • Automatische Wiederverbindung: Behandelt Verbindungstrennungen
  • Native Unterstützung in den meisten Browsern

2. Systemarchitektur

Instant-Messaging-Architektur über Mercure

  1. Der Client (Browser, Mobile App, etc.) öffnet eine HTTP/HTTPS-Verbindung zum Mercure-Hub (über die vom Apache-Vhost verwaltete Domain, z.B. messages.habeuk.com).
  2. Der Apache-Vhost fungiert ausschließlich als Proxy: Er leitet die Anfrage an den Mercure-Container weiter (oft auf 127.0.0.1:3000).
  3. Die Symfony-Anwendung veröffentlicht eine Nachricht an Mercure (über das HubInterface und ein Update).
  4. Der Mercure-Hub sendet diese Nachricht dann in Echtzeit an alle abonnierten Clients zum relevanten Topic (über die anfangs geöffnete SSE-Verbindung).

3. Installation und Konfiguration von Mercure

3.1. Docker Compose Konfiguration

Erstellen Sie einen Ordner, in dem Sie den Mercure-Server verwalten möchten (z.B. 'mercure_habeuk'), wechseln Sie in diesen Ordner und erstellen Sie die Mercure-Konfigurationsdatei (docker-compose.yml).

# compose.yaml
services:
  mercure:
    image: dunglas/mercure
    restart: unless-stopped
    container_name: mercure_habeuk
    environment:
      # Au niveau de mercure on ecoute sur le port 80.( mercure est dans un docker ).
      SERVER_NAME: ':80'
      MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}"
      MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" 
      MERCURE_EXTRA_DIRECTIVES: |
        cors_origins https://www.habeuk.com https://habeuk.com     
        anonymous    
    ports:
      - "127.0.0.1:3000:80"   
    # configuration avec volumes nommés gerer par docker ( /var/lib/docker/volumes/... )
    volumes:
      - mercure_data:/data
      - mercure_config:/config
volumes:
  mercure_data:
  mercure_config:

Erklärungen:

SERVER_NAME: ':80' → die Zahl 80 gibt den Port an, auf dem der Mercure-Server innerhalb des Docker-Containers lauscht. Da Mercure in einem isolierten Container läuft, steht dieser Port nicht in Konflikt mit dem Port des Host-Rechners. Man hätte bei Bedarf auch einen anderen internen Port wählen können.

MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_PUBLISHER_JWT_KEY}" und MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_SUBSCRIBER_JWT_KEY}" → dies sind die JWT-Schlüssel, die jeweils zum Veröffentlichen und Abonnieren von Mercure-Ereignissen verwendet werden. Sie sind in der Umgebungsdatei mercure_habeuk/.env definiert. 

MERCURE_EXTRA_DIRECTIVES → ermöglicht das Hinzufügen zusätzlicher Anweisungen zur Mercure-Hub-Konfiguration (es ist wie ein kleiner interner Konfigurationsblock).

cors_origins https://www.habeuk.com https://habeuk.com → definiert die erlaubten Ursprünge (CORS), die eine Verbindung zum Mercure-Hub herstellen dürfen. Dies bedeutet, dass nur Webseiten, die von https://www.habeuk.com und https://habeuk.com bereitgestellt werden, die Berechtigung haben: 
- Ereignisse zu abonnieren (über EventSource);
- und Veröffentlichungsanfragen zu senden (sofern die JWT-Berechtigungen es erlauben).

anonymous → erlaubt Verbindungen ohne JWT-Token für Abonnements. Mit anderen Worten können Clients bestimmte Topics abonnieren, ohne sich authentifizieren zu müssen, was für öffentliche Feeds nützlich ist (wie für alle sichtbare Nachrichten oder globale Benachrichtigungen).

ports:
127.0.0.1:3000:80 → weist Docker an, Port 80 des Mercure-Containers auf Port 3000 des Host-Rechners umzuleiten, jedoch nur zugänglich von 127.0.0.1 (localhost).

Mit anderen Worten: - Der Mercure-Dienst lauscht auf Port 80 innerhalb des Containers. - Dieser Port wird lokal auf Port 3000 des Host-Rechners verfügbar gemacht. - Die Adresse 127.0.0.1 beschränkt den Zugriff auf den lokalen Rechner (niemand kann von außen auf Port 3000 zugreifen).

3.2. Starten des Dienstes

Mercure installieren und starten

docker compose up -d

Dienst stoppen und neu starten

docker compose down && docker compose up -d

3.3. Testen des Mercure-Dienstes

Zuerst prüfen wir, ob der Mercure-Server auf der definierten IP-Adresse und dem Port läuft. (direkt auf dem VPS)

curl http://127.0.0.1:3000/.well-known/mercure?topic=test

Wenn alles korrekt ist, sollte der Befehl eine SSE-Verbindung öffnen.

Wir möchten, dass der Mercure-Server über die Subdomain messages.habeuk.com erreichbar ist.

Ändern Sie die Datei /etc/hosts, indem Sie Folgendes hinzufügen:

127.0.0.1	messages.habeuk.com

Führen Sie dann einen weiteren Test durch:

curl http://messages.habeuk.com:3000/.well-known/mercure?topic=test

Wenn alles korrekt ist, sollte der Befehl eine SSE-Verbindung öffnen.

4. Apache und SSL Konfiguration

Wir möchten eine saubere Adresse, d.h. messages.habeuk.com ohne Portnummer. Apache wird eingreifen, um Anfragen, die auf messages.habeuk.com eingehen, transparent für den Benutzer an die Adresse 127.0.0.1:3000 weiterzuleiten.

Wir müssen einen Virtual Host in /etc/apache2/sites-available/messages.habeuk.com.conf hinzufügen:

<VirtualHost *:80>
    ServerName messages.habeuk.com
    # Proxy alle HTTP-Anfragen an Mercure (Port 3000)
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
    #
    ErrorLog /var/www/habeuk/logs/error-mercure.log
    CustomLog /var/www/habeuk/logs/access-mercure.log combined
</VirtualHost>

Aktivierung der Apache 2 Module:

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo a2enmod rewrite

Aktivierung des Virtual Hosts:

sudo a2ensite messages.habeuk.com.conf

Apache 2 neu starten:

sudo systemctl restart apache2

Führen Sie dann einen Test durch:

curl http://messages.habeuk.com/.well-known/mercure?topic=test

Wenn alles korrekt ist, sollte der Befehl eine SSE-Verbindung öffnen.

Sie können auch direkt in Ihrem Browser prüfen: http://messages.habeuk.com sollte die Mercure-Startseite anzeigen.

Aktivieren Sie SSL auf Ihrer Subdomain messages.habeuk.com indem Sie diese Dokumentation befolgen.

Aktualisieren Sie die Virtual-Host-Datei /etc/apache2/sites-available/messages.habeuk.com.conf:

<VirtualHost *:80>
    ServerName messages.habeuk.com
    Redirect permanent / https://messages.habeuk.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName messages.habeuk.com
    # Proxy toutes les requêtes HTTPS vers Mercure (port 3000)
    ProxyPreserveHost On
    ProxyRequests Off
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
    #
    ErrorLog /var/www/habeuk/logs/error-mercure.log
    CustomLog /var/www/habeuk/logs/access-mercure.log combined
    # Activation SSL
    SSLEngine On
    SSLCertificateFile /etc/letsencrypt/live/habeuk.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/habeuk.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>

Wiederholen Sie abschließend den vorherigen Test, dieses Mal mit HTTPS:
https://messages.habeuk.com

5. JWT-Schlüssel Erstellung

Die Sicherheit von Mercure basiert auf JWT (JSON Web Tokens), die mit kryptografischen Schlüsseln signiert sind. Generieren Sie ausreichend lange zufällige Geheimnisse mit dem folgenden Befehl:

openssl rand -base64 32

Erstellen Sie dann die Datei mercure_habeuk/.env und fügen Sie den folgenden Inhalt hinzu:

MERCURE_PUBLISHER_JWT_KEY=your-secure-publisher-key-from-openssl 
MERCURE_SUBSCRIBER_JWT_KEY=your-secure-subscriber-key-from-openssl 

6. Backend-Implementierungsbeispiel (via Symfony)

<?php

// src/Service/ChatService.php
namespace App\Service;

use App\Entity\ {
  Notification,
  NotificationReceipt
};
use App\Enum\NotificationType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;

class SendNotificationByMercureService {

  public function __construct(private EntityManagerInterface $em, private HubInterface $hub, private SerializerInterface $serializer, private LoggerInterface $logger) {
  }

  public function publishViaMercure(Notification $notification): void {
    $receipts = $notification->getReceipts();
    $topics = [];
    if ($receipts->count() > 0) {
      foreach ($receipts->getValues() as $receipt) {
        /**
         *
         * @var NotificationReceipt $receipt
         */
        $topics[] = 'notification/' . $receipt->getRecipient()->getId();
      }
      try {
        $result = $this->serializer->serialize($notification, 'json',
          [
            AbstractNormalizer::GROUPS => [
              'notification:read'
            ],
            AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
              return $object->getId();
            }
          ]);
        $type = $notification->getType() == NotificationType::NEW_MESSAGE ? "new_message" : "notification";
        $update = new Update($topics, $result, true, null, $type);
        $this->hub->publish($update);
      }
      catch (\Exception $e) {
        $this->logger->error($e->getMessage());
      }
    }
  }
}

7. Frontend-Implementierungsbeispiel

// mercureClient.js
class MercureClient {
    constructor(url) {
        this.eventSource = new EventSource(url, { withCredentials: true });
        this.eventSource.onerror = (err) => {
            console.error("Mercure connection error:", err);
        };
    }

    build() {
        const events = ["chat", "read_message", "notification", "new_message"];
        events.forEach((event_name) => {
            this.Listerner(event_name);
        });
        this.eventSource.onmessage = (event) => {
            console.log("MercureClient event onmessage : ", event);
            alert("ezsd");
        };
    }

    Listerner(event_name) {
        this.eventSource.addEventListener(event_name, (event) => {
            const data = JSON.parse(event.data);
            this.dispatchCustomEvent(event_name, data);
        });
    }

    dispatchCustomEvent(event_name, data) {
        console.log("dispatchCustomEvent event_name : ", event_name, data);
        document.dispatchEvent(
            new CustomEvent(`mercure-${event_name}`, {
                detail: data,
            })
        );
    }
}
export default MercureClient;
Profile picture for user admin Stephane K

Écrit le

Il y'a 1 Tag
Modifié
Il y'a 1 Tag
Lädt...
WhatsApp
Habeuk Support: +49 152 108 01753
Senden