1. Was bringen einem SSE?
Bei der REST-Schnittstelle hat man typischerweise einen Client, der auf Api-Endpunkte des Servers zugreift, dabei hat er verschiedene Methoden zur Auswahl:
-
GET→Hole dir Daten vom Server
-
POST→Lege neue Daten am Server an
-
PUT→Update Daten am Server
-
DELETE→Lösche Daten vom Server
Das Entscheidende hierbei ist, dass der Server nur auf Anfrage des Clients Antworten kann, das heißt, dass der Server nie eine Nachricht senden kann, ohne dass der Client davor einen REST-Aufruf abgesetzt hat.
Jedoch wird genau das in manchen Fällen benötigt, wie zum Beispiel bei einem Echtzeit-Aktienkurs-Ticker, einer Live Sportübertragung oder einem Bestellsystem.
In diesen Fällen muss der Server seine Clients über neue Daten informieren, kann aber ohne vorherige GET-Request nichts machen. Daher ist eine recht simple Methode Polling, bei dieser schickt der Client in regelmäßigen Abständen GET-Requests an den Server, um nach neuen Daten zu fragen. Polling ist relativ weit verbreitet aufgrund seiner Einfachheit, jedoch ist das nicht die effektivste Methode, da der Server unnötig viel belastet wird.
2. Wieso nicht Websockets oder MQTTs?
SSE, Websockets und MQTTs sind auf den ersten Blick alle ähnlich und man könnte theoretisch statt SSE auch die anderen 2 verwenden, jedoch erfüllt jeder dieser 3 Technologien einen unterschiedlichen Zweck. Wir schauen uns diese Unterschiede anhand des Bestellsystem-Beispiels von oben an.
SSE | WebSockets | MQTT | |
---|---|---|---|
Richtung |
One-way (Server ➡️ Client) |
Two-way |
Two-way |
Browser Support |
✅ Native in browsers |
✅ via JS |
✅ via JS |
Reconnect Handling |
✅ Built-in |
❌ Manual |
✅ Built-in |
Binary Data |
❌ Text only |
✅ |
✅ |
Komplexität |
🟢 Simple |
🟡 Medium |
🔴 Higher |
Gut für |
Live feeds, Benachrichtigungen |
Chats |
IoT |
Der größte Unterschied ist, dass SSE nur vom Server zum Client gehen, wobei Websockets bidirektional sind, MQTTs ebenso, auch wenn das eigentlich nicht so vorgesehen ist.
Ein weiterer wichtiger Punkt ist die Komplexität. SSE werden von den Browsern unterstützt und man braucht keine js-Library. Websockets und MQTTs sind zwar nicht nativ unterstützt, können aber trotzdem durch js-Libraries genutzt werden, denn es gibt für alles eine js-Library (is-odd).
Dadurch ist es viel einfacher mit SSE zu arbeiten, vor allem im Vergleich zum MQTT, für den noch ein extra Broker aufgesetzt werden muss.
2.1. Genauere Erklärung von MQTT

Da MQTTs ein weniger komplexer sind, kommt hier noch einmal eine kurze Erläuterung mit einer Grafik.
ein MQTT eignet sich gut, wenn man mehrere Publisher hat, die Daten veröffentlichen und dann über den Broker die Subscriber benachrichtigen. Noch dazu kann man sehen, dass es sich um verschiedene Daten handelt, die veröffentlicht werden, einmal die Temperatur, einmal die Windgeschwindigkeit und einmal der Standort. Der Broker kümmert sich darum, dass alle Subscriber nur die Daten erhalten, die sie auch brauchen, zum Beispiel Temperatur.
Bei einem Bestellsystem hingegen haben wir zwar mehrere "Publisher", die verschiedenen Kellner, die gerade die Bestellungen aufnehmen, jedoch sind alle Daten die da aufgenommen werden, von derselben Art, nur der Inhalt unterscheidet sich. Dazu haben wir nur einen "Subscriber" und das ist, der Bildschirm in der Küche, der die Bestellungen anzeigt.
Natürlich ist eine Umsetzung mit MQTT möglich jedoch unnötig aufwendig. |
2.2. Genauere Erklärung von Websockets
Bein einem Websocket ist es so, dass der Client einen Request abschickt und darauf ein Handshake folgt, danach können bidirektional Nachrichten verschickt werden.
Für ein Bestellsystem ist auch das schon zu viel. Das Tablet vom Kellner, auf dem die Bestellung eingegeben wird, schickt die Bestellung zum Server, dieser kann dann per SSE den Bildschirm in der Küche mit der neuen Bestellung benachrichtigen. Falls die Bestellung abgearbeitet ist, kann diese per Knopfdruck am Bildschirm gelöscht werden. Dann wird ein DELETE-Request abgesetzt, damit der Server die Bestellung auch bei sich löscht.
Natürlich ist eine Umsetzung mit Websockets möglich jedoch unnötig aufwendig. |
Es kann nur Text über SSE verschickt werden |
3. Aufbau von SSE mit Beispielen
Die Beispiele bauen auf dem vorhin erwähnten Bestellsystem auf |
3.1. Bestellsystem-Aufbau

Das Bestellsystem-Beispiel ist so aufgebaut, dass es 2 Frontends gibt, die OrderPage repräsentiert das Tablet des Kellners, der dann über eine Post-Request die Bestellung aufgibt. Der Server nimmt diese Bestellung auf und schickt sie über SSE an die ViewPage (Monitor in der Küche).
OrderPage

ViewPage

3.2. SSE-Aufbau
Bildquelle → hier
Server Sent Events fangen damit an, dass der Client erst einmal eine Verbindung herstellen muss, das kann im Code wie folgt aussehen:
const eventSource = new EventSource("http://localhost:8080/view/stream"); (1)
eventSource.onmessage = (event) => { (2)
console.log(event.data) (3)
};
Der Client code ist in Js geschrieben, da SSE meistens im Browser verwendet werden, jedoch sind auch andere Sprachen möglich |
1 | In erster Linie erstellen wir ein neues EventSource Objekt, dem wir die URL zum Endpunkt mitgeben, über den es die Events erhält |
2 | Wir hängen an die .onmessage Property eine callback function an, die bei jeder neuen Nachricht ausgeführt wird |
3 | Wir logen die Daten des gesendeten Events |
Da wir jetzt den Code für den Client haben, müssen wir noch den Code auf der Serverseite schreiben. In diesem Beispiel ist der Code in Java geschrieben, da für den Server Quarkus verwendet wurde.
@Path("/view")
public class ViewPage {
@Inject
OrderRepo repo; (1)
// ...
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS) (2)
public Multi<Order> streamOrders() { (3)
return repo.getOrderStream(); (4)
}
}
1 | In unserer Klasse, die den Endpunkt beinhaltet, injizieren wir zuerst unser Repository, das alle Daten speichert |
2 | Wir müssen mit der @Produces Annotation angeben, dass dieser Endpunkt SSE zurückgibt |
3 | Da wir bei unserem Bestellsystem die Küche über neue Bestellungen informieren wollen, geben wir hier immer die neue Bestellung zurück |
4 | Das Repo gibt uns ein Multi zurück da wir hier einen Stream verfolgen, das ermöglicht es uns benachrichtigt zu werden, sobald ein neuer Eintrag ins Repo erfolgt, das heißt, sobald eine neue Bestselling auftritt, schicken wir diese gleich weiter |
4. Restlicher Code fürs Beispiel
Aufbau des Quarkusprojektes |
src └── main ├── java │ └── at │ └── htl │ └── leonding │ ├── entities │ │ ├── Items.java │ │ └── Order.java │ ├── repos │ │ └── OrderRepo.java │ └── views │ ├── OrderPage.java │ └── ViewPage.java └── resources ├── application.properties ├── META-INF │ └── resources │ ├── order.js │ ├── orderStyle.css │ └── viewStyle.css └── templates ├── orderPage.qute.html └── viewPage.qute.html
public enum Items {
Coffee(3),
Water(1),
Juice(2),
Sandwich(4);
private final int price;
Items(int price) {
this.price = price;
}
public int getPrice() {
return price;
}
}
In dem Items-Enum befinden sich alle Artikel, sowie die Preise dafür.
public class Order {
private Map<Items, Integer> items;
private int total;
public Order() {
}
public Order(Map<Items, Integer> items, int total) {
this.items = items;
this.total = total;
}
public Map<Items, Integer> getItems() {
return items;
}
public void setItems(Map<Items, Integer> items) {
this.items = items;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
}
Die Order-Klasse repräsentiert eine Bestellung mit einer Variable 'total', die den Gesamtbetrag enthält und eine Map 'items', die den Artikel und die Anzahl enthält.
@ApplicationScoped
public class OrderRepo {
private final List<Order> orders = new ArrayList<>();
private final SubmissionPublisher<Order> publisher = new SubmissionPublisher<>();
public void addOrder(Order order) { (1)
orders.add(order);
publisher.submit(order);
}
public Multi<Order> getOrderStream() { (3)
return Multi.createFrom().publisher(publisher)
.emitOn(Infrastructure.getDefaultExecutor());
}
public List<Order> getOrders() { (2)
return orders;
}
}
Wir haben ein Dummy-Repository, dass die Daten nicht in einer Datenbank speichert, sondern in Memory in einer Liste hält. Das Repo braucht ein ApplicationScoped Annotation, damit dasselbe Repo in den verschiedenen Klassen des Projekts injiziert werden kann.
1 | Bei einer neuen Bestellung wird diese in eine normale Liste hinzugefügt, sowie in ein SubmissionPublisher-Objekt |
2 | Falls man alle Orders haben möchte, wird die normale Liste zurückgegeben |
3 | Das SubmissionPublisher-Objekt verwenden wir dazu, um bei neuen Einträgen, die neue Bestellung zu verschicken, somit können wir über SSE den Bildschirm in der Küche benachrichtigen |
@Path("/order") (1)
public class OrderPage {
private final Template orderPage; (2)
@Inject
OrderRepo repo; (3)
public OrderPage(Template orderPage) {
this.orderPage = requireNonNull(orderPage, "page is required");
}
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get() {
return orderPage.data("items", Items.values()); (4)
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void increaseOrders(Order order){ (5)
repo.addOrder(order);
}
}
Die OrderPage-Klasse wird für Qute verwendet, daher sind das Template oben, der Konstruktor und der GET-Endpunkt notwendig.
1 | Oben legen wir den Pfad fest, unter welchem man auf die Resource zugreifen kann |
2 | Das Template muss denselben Namen, wie die HTML-Datei haben |
3 | Hier injizieren wir das Repo, welches unsere Bestellungen beinhaltet |
4 | Wir geben dem Template die Werte aus dem Enum mit, damit der Nutzer dann aus einer Liste aussuchen kann, die mit unseren Artikeln übereinstimmt |
5 | Bei einer neuen Bestellung wird ein POST-Request abgesetzt, in dem wir nur die Bestellung ins Repo schreiben |
{@at.htl.leonding.entities.Items item}
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Order</title>
<link rel="stylesheet" href="orderStyle.css">
<script src="order.js"></script>
</head>
<body>
<div id="upper">
<h1>Bestellung aufgeben</h1>
</div>
<div id="lower">
<div id="left">
<label for="itemSelect">Wähle ein Item:</label>
<select id="itemSelect">
<option value="" selected disabled>-- Bitte ein Item wählen --</option>
{#for item in items} (1)
<option value="{item.name}">{item.name} - ${item.price}</option>
{/for}
</select>
<label id="menge" for="quantityInput">Menge:</label>
<input type="number" id="quantityInput" value="" min="1">
<button onclick="addItem()">Hinzufügen</button>
</div>
<div id="right">
<h2>Bestellung:</h2>
<ul id="orderList"></ul>
<button onclick="submitOrder()">Bestellung aufgeben</button>
</div>
</div>
</body>
</html>
Im Frontendteil kann der Benutzer in einem Select die Artikel aussuchen und über einen Input die Anzahl bestimmen. Zum Schluss kann er über einen Button die Bestellung absenden.
1 | Qute erlaubt es uns die Daten, die wir im Java-Teil übers Template mitgegeben haben im HTML-Code zu verwenden. In dem Fall iterieren wir über alle Artikel und tragen diese als Option im Select ein. |
Javascript-Code
const prices = {
"Coffee": 3,
"Water": 1,
"Juice": 2,
"Sandwich": 4
};
function addItem() {
const itemSelect = document.getElementById("itemSelect");
const quantityInput = document.getElementById("quantityInput");
const orderList = document.getElementById("orderList");
const selectedItem = itemSelect.value;
const quantity = quantityInput.value;
if (!selectedItem || quantity <= 0) {
alert("Bitte ein gültiges Item und eine Menge auswählen.");
return;
}
let existingItem = null;
for (let listItem of orderList.children) {
if (listItem.textContent.includes(selectedItem)) {
existingItem = listItem;
break;
}
}
if (existingItem) {
const currentQuantity = parseInt(existingItem.textContent.split(":")[1]);
existingItem.textContent = selectedItem + " - Menge: " + (currentQuantity + parseInt(quantity));
} else {
const listItem = document.createElement("li");
listItem.textContent = selectedItem + " - Menge: " + quantity;
orderList.appendChild(listItem);
}
quantityInput.value = null;
itemSelect.selectedIndex = 0;
}
function submitOrder() {
alert("Bestellung wurde aufgegeben!");
const orderList = document.getElementById("orderList");
const orderItems = { };
for (let listItem of orderList.children) {
const itemText = listItem.textContent;
const [itemName, quantity] = itemText.split(" - Menge: ");
orderItems[itemName] = parseInt(quantity);
}
orderList.replaceChildren();
fetch('/order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ items: orderItems, total: calculateTotal(orderItems) })
});
}
function calculateTotal(orderItems) {
let total = 0;
for (const item in orderItems) {
const quantity = orderItems[item];
const price = prices[item];
total += price * quantity;
}
return total;
}
Css-Code
body {
height: 96vh;
width: 100vw;
padding: 0;
margin: 0;
}
#upper {
width: 100%;
height: 20%;
align-content: center;
justify-content: center;
align-self: center;
text-align: center;
}
h1{
width: 100%;
align-content: center;
justify-content: center;
align-items: center;
}
#lower {
width: 100%;
height: 80%;
display: flex;
flex-direction: row;
}
#left{
width: 50%;
height: 100%;
border-right: black solid 2px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#right {
width: 50%;
height: 100%;
border-left: black solid 2px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
#menge{
margin-top: 150px;
}
label, select {
width: 320px;
}
input {
width: 312px;
margin-bottom: 150px;
}
@Path("/view")
public class ViewPage {
@Inject
OrderRepo repo;
@Inject
ObjectMapper objectMapper;
private final Template viewPage;
public ViewPage(Template viewPage) {
this.viewPage = requireNonNull(viewPage, "page is required");
}
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get() throws JsonProcessingException {
List<Order> orders = repo.getOrders();
String ordersJson = objectMapper.writeValueAsString(orders);
return viewPage.data("orders", ordersJson);
}
@GET
@Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS) (1)
public Multi<Order> streamOrders() { (2)
return repo.getOrderStream(); (3)
}
}
Hier ist der Aufbau relativ ähnlich wie zum vorherigen Qute Endpunkt, nur das wir hier einen ObjectMapper verwenden, um alle bisherigen Bestellungen aus dem Repo ans Qute-Template zu schicken.
Auf dem '/stream' Pfad ist der Endpunkt, der die SSE schickt.
1 | In der 'Produces' Annotation müssen wir angeben, dass SSE zurückgegeben werden |
2 | Dadurch dass wir einen Stream zurückgeben von einem Multi Typ, muss dieser auch im Methodenkopf angegeben werden |
3 | Dadurch dass der Stream immer benachrichtigt wird, bei neuen Einträgen, schicken wir immer ein neues SSE bei einer neuen Bestellung. |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>View</title>
<link rel="stylesheet" href="viewStyle.css">
</head>
<body id="body">
<script>
const eventSource = new EventSource("http://localhost:8080/view/stream"); (1)
eventSource.onmessage = (event) => {
console.log(event.data)
makeEntry(JSON.parse(event.data));
};
let orders = JSON.parse('{orders.raw}'); (2)
for (let order of orders){
makeEntry(order);
}
function makeEntry(order){ (3)
let orderList = document.getElementById("body");
let div = document.createElement("div");
let p = document.createElement("p")
let ul = document.createElement("ul");
p.innerText = "total: " + order.total;
for(let [item, quantity] of Object.entries(order.items)){
let li = document.createElement("li");
li.innerText = quantity + ": " + item;
ul.appendChild(li);
}
div.appendChild(p);
div.appendChild(ul);
orderList.appendChild(div);
}
</script>
</body>
</html>
1 | Im ersten Teil des Codes im Frontend, eröffnen wir eine Verbindung für unsere SSE, so wie oben vorgezeigt, für jede eingehende Bestellung wird dann die makeEntry-Funktion aufgerufen |
2 | Die Bestellungen, die wir im Template mitgegeben haben, werden jetzt in ein Objekt geparsed, über das wir dann darüber iterieren und die makeEntry-Funktion aufrufen |
3 | Die Funktion macht nichts weiters, als für jede Bestellung einen eigenen HTML-Tag zu erstellen, der dann angezeigt wird |
Css-Code
body {
height: 96vh;
width: 100vw;
padding: 0;
margin: 0;
display: flex;
flex-direction: row;
}
div{
width: 200px;
height: 290px;
color: white;
background: rgb(71,212,224);
background: linear-gradient(45deg, rgba(71,212,224,1) 0%, rgba(252,69,248,1) 100%);
margin: 20px;
text-align: center;
border-radius: 10px;
font-size: 30px;
}
ul{
display: inline-block;
text-align: left;
}