- Einführung in Mercure
- Systemarchitektur
- Installation und Konfiguration von Mercure
- Apache und SSL Konfiguration
- JWT-Schlüssel Erstellung
- Backend-Implementierungsbeispiel
- 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
- 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
). - Der Apache-Vhost fungiert ausschließlich als Proxy: Er leitet die Anfrage an den Mercure-Container weiter (oft auf
127.0.0.1:3000
). - Die Symfony-Anwendung veröffentlicht eine Nachricht an Mercure (über das
HubInterface
und einUpdate
). - 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;