Server-Sent Events (SSE)¶
Subskrybuj tematy Notifer w czasie rzeczywistym używając Server-Sent Events (SSE).
Czym jest SSE?¶
Server-Sent Events (SSE) to technologia pozwalająca serwerowi na wysyłanie danych do klienta przez HTTP. Jest idealna dla powiadomień w czasie rzeczywistym, ponieważ:
- ✅ Proste: Działa przez standardowy HTTP (bez specjalnych protokołów)
- ✅ Niezawodne: Automatyczne ponowne łączenie przy utracie połączenia
- ✅ Wydajne: Długotrwałe połączenie z niskim narzutem
- ✅ Uniwersalne: Działa we wszystkich nowoczesnych przeglądarkach i curl
- ✅ Przyjazne dla zapory: Używa standardowych portów HTTP/HTTPS
Przykładowy przepływ:
Klient → Serwer: GET /my-topic/sse
Serwer → Klient: (połączenie pozostaje otwarte)
Serwer → Klient: event: message\ndata: {"message": "Hello"}\n\n
Serwer → Klient: event: message\ndata: {"message": "World"}\n\n
Szybki start¶
Używanie cURL¶
Wynik:
event: message
data: {"id": "uuid", "message": "Pierwsza wiadomość", "priority": 3, "timestamp": "2025-11-22T10:00:00Z"}
event: message
data: {"id": "uuid", "message": "Druga wiadomość", "priority": 5, "timestamp": "2025-11-22T10:01:00Z"}
Używanie JavaScript (przeglądarka)¶
const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Nowa wiadomość:', message);
};
eventSource.onerror = (error) => {
console.error('Błąd SSE:', error);
};
// Zamknij połączenie gdy skończysz
// eventSource.close();
Używanie Python¶
import requests
url = 'https://app.notifer.io/my-topic/sse'
with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data:'):
message_json = decoded_line[5:].strip()
print(message_json)
Subskrybowanie prywatnych tematów¶
Używanie tokena uwierzytelniającego¶
Dla prywatnych tematów dołącz swój token JWT lub token dostępu do tematu:
# Metoda 1: Parametr zapytania
curl -N "https://app.notifer.io/my-private-topic/sse?token=YOUR_JWT_TOKEN"
# Metoda 2: Nagłówek Authorization
curl -N https://app.notifer.io/my-private-topic/sse \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
JavaScript:
// Dołącz token w URL
const eventSource = new EventSource(
'https://app.notifer.io/my-private-topic/sse?token=' + encodeURIComponent(token)
);
Python:
headers = {'Authorization': f'Bearer {token}'}
with requests.get(url, headers=headers, stream=True) as response:
# ... przetwarzaj zdarzenia
Parametry zapytania¶
since_id - Wznów od ostatniej wiadomości¶
Pobierz wszystkie wiadomości od konkretnego ID wiadomości:
Przypadek użycia: Połącz ponownie i nadgon pominięte wiadomości.
Przykład:
let lastMessageId = null;
function connect() {
const url = lastMessageId
? `https://app.notifer.io/my-topic/sse?since_id=${lastMessageId}`
: 'https://app.notifer.io/my-topic/sse';
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
lastMessageId = message.id; // Zapisz dla ponownego połączenia
displayMessage(message);
};
eventSource.onerror = () => {
eventSource.close();
setTimeout(connect, 5000); // Połącz ponownie po 5s
};
}
connect();
token - Uwierzytelnianie¶
Format wiadomości¶
Struktura zdarzenia¶
Pola:
- event: - Typ zdarzenia (zawsze "message" dla powiadomień)
- data: - Dane wiadomości JSON
- Pusta linia - Oznacza koniec zdarzenia
Dane wiadomości¶
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"topic": "my-topic",
"message": "Serwer nie działa!",
"title": "Alert krytyczny",
"priority": 5,
"tags": ["critical", "server"],
"created_at": "2025-11-22T10:30:00Z"
}
Pola:
- id - Unikalny ID wiadomości (UUID)
- topic - Nazwa tematu
- message - Treść wiadomości (zwykły tekst lub Markdown)
- title - Opcjonalny tytuł wiadomości
- priority - Poziom priorytetu (1-5)
- tags - Tablica tagów
- created_at - Znacznik czasu (ISO 8601)
Obsługa ponownego łączenia¶
Połączenia SSE automatycznie łączą się ponownie przy zerwaniu, ale powinieneś obsługiwać ponowne łączenia z gracją:
Przeglądarka (EventSource)¶
function createConnection() {
const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');
eventSource.onopen = () => {
console.log('✅ Połączono');
};
eventSource.onerror = (error) => {
console.error('❌ Błąd połączenia:', error);
// Przeglądarka automatycznie połączy ponownie
// EventSource ma wbudowaną logikę ponawiania
};
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
return eventSource;
}
let connection = createConnection();
// Ręczne ponowne połączenie jeśli potrzebne
function reconnect() {
connection.close();
connection = createConnection();
}
Python (ręczne ponowne łączenie)¶
import requests
import time
import json
def subscribe(topic, token=None):
url = f'https://app.notifer.io/{topic}/sse'
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
while True:
try:
print('🔌 Łączenie...')
with requests.get(url, headers=headers, stream=True) as response:
response.raise_for_status()
print('✅ Połączono')
for line in response.iter_lines():
if line:
decoded = line.decode('utf-8')
if decoded.startswith('data:'):
message_json = decoded[5:].strip()
message = json.loads(message_json)
handle_message(message)
except requests.exceptions.RequestException as e:
print(f'❌ Błąd połączenia: {e}')
print('🔄 Ponowne łączenie za 5 sekund...')
time.sleep(5)
def handle_message(message):
print(f"📬 [{message['priority']}] {message['title']}: {message['message']}")
subscribe('my-topic')
Obsługa błędów¶
Kody statusu HTTP¶
| Kod | Znaczenie | Akcja |
|---|---|---|
200 |
Sukces | Połączenie ustanowione |
401 |
Brak autoryzacji | Sprawdź token uwierzytelniający |
404 |
Temat nie znaleziony | Zweryfikuj nazwę tematu |
403 |
Zabronione | Sprawdź uprawnienia dostępu do tematu |
429 |
Ograniczenie liczby żądań | Poczekaj i spróbuj ponownie z backoff |
Obsługa błędów w JavaScript¶
const eventSource = new EventSource('https://app.notifer.io/my-topic/sse');
eventSource.addEventListener('error', (event) => {
if (event.target.readyState === EventSource.CLOSED) {
console.error('Połączenie zamknięte przez serwer');
} else if (event.target.readyState === EventSource.CONNECTING) {
console.log('Ponowne łączenie...');
}
});
Przykłady z rzeczywistości¶
Pulpit monitorowania serwera¶
const topics = ['prod-web', 'prod-api', 'prod-db'];
const connections = {};
topics.forEach(topic => {
const eventSource = new EventSource(`https://app.notifer.io/${topic}/sse`);
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
updateDashboard(topic, message);
// Pokaż alert dla krytycznych wiadomości
if (message.priority >= 4) {
showAlert(message);
playAlertSound();
}
};
connections[topic] = eventSource;
});
function updateDashboard(topic, message) {
const container = document.getElementById(`topic-${topic}`);
const messageEl = document.createElement('div');
messageEl.className = `message priority-${message.priority}`;
messageEl.innerHTML = `
<strong>${message.title}</strong>
<p>${message.message}</p>
<small>${new Date(message.created_at).toLocaleString()}</small>
`;
container.prepend(messageEl);
}
Monitor alertów Python¶
import requests
import json
from datetime import datetime
def monitor_alerts(topic, priority_threshold=3):
"""Monitoruj temat i wyświetlaj tylko ważne alerty"""
url = f'https://app.notifer.io/{topic}/sse'
print(f'📡 Monitorowanie {topic} (priorytet >= {priority_threshold})')
with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line and line.startswith(b'data:'):
data = line[5:].strip()
message = json.loads(data)
if message['priority'] >= priority_threshold:
timestamp = datetime.fromisoformat(message['created_at'].replace('Z', '+00:00'))
print(f"\n🚨 [{message['priority']}] {message['title']}")
print(f" {message['message']}")
print(f" Tagi: {', '.join(message['tags'])}")
print(f" Czas: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
monitor_alerts('production-alerts', priority_threshold=4)
Subskrypcja wielotematowa (Python)¶
import requests
import json
from threading import Thread
def subscribe_topic(topic, callback):
"""Subskrybuj temat w oddzielnym wątku"""
url = f'https://app.notifer.io/{topic}/sse'
while True:
try:
with requests.get(url, stream=True) as response:
for line in response.iter_lines():
if line and line.startswith(b'data:'):
message = json.loads(line[5:].strip())
callback(topic, message)
except Exception as e:
print(f'Błąd w {topic}: {e}')
time.sleep(5)
def handle_message(topic, message):
print(f"[{topic}] {message['title']}: {message['message']}")
# Subskrybuj wiele tematów równolegle
topics = ['deployments', 'errors', 'monitoring']
threads = []
for topic in topics:
thread = Thread(target=subscribe_topic, args=(topic, handle_message))
thread.daemon = True
thread.start()
threads.append(thread)
# Utrzymuj główny wątek żywym
for thread in threads:
thread.join()
SSE vs WebSocket¶
| Funkcja | SSE | WebSocket |
|---|---|---|
| Kierunek | Tylko serwer → klient | Dwukierunkowe |
| Protokół | HTTP | WS/WSS |
| Ponowne łączenie | Automatyczne | Ręczne |
| Przypadek użycia | Aktualizacje w czasie rzeczywistym | Czat, gry |
| Złożoność | Proste | Bardziej złożone |
| Wsparcie przeglądarek | Wszystkie nowoczesne przeglądarki | Wszystkie nowoczesne przeglądarki |
Kiedy używać SSE: - ✅ Jednokierunkowe powiadomienia (serwer → klient) - ✅ Prosta konfiguracja i debugowanie - ✅ Potrzebne automatyczne ponowne łączenie - ✅ Środowiska tylko HTTP
Kiedy używać WebSocket: - ✅ Potrzebna dwukierunkowa komunikacja - ✅ Bardzo częste aktualizacje - ✅ Aplikacje do gier lub czatu
Dla Notifer zalecane jest SSE, ponieważ: - Powiadomienia są jednokierunkowe (serwer → klient) - Automatyczne ponowne łączenie upraszcza kod klienta - Działa ze standardową infrastrukturą HTTP
Wsparcie przeglądarek¶
SSE jest wspierane we wszystkich nowoczesnych przeglądarkach:
- ✅ Chrome/Edge 6+
- ✅ Firefox 6+
- ✅ Safari 5+
- ✅ Opera 11+
- ❌ Internet Explorer (nie wspierane)
Polyfills: Dla wsparcia IE użyj EventSource polyfill
Dobre praktyki¶
1. Zapisuj ID ostatniej wiadomości¶
Zawsze śledź ID ostatniej wiadomości dla ponownego połączenia:
let lastId = localStorage.getItem('lastMessageId');
const url = lastId
? `https://app.notifer.io/topic/sse?since_id=${lastId}`
: 'https://app.notifer.io/topic/sse';
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
lastId = message.id;
localStorage.setItem('lastMessageId', lastId);
};
2. Obsługuj zmiany widoczności¶
Wstrzymaj/wznów połączenie gdy zakładka jest ukryta:
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// Opcjonalnie zamknij połączenie gdy zakładka jest ukryta
// eventSource.close();
} else {
// Połącz ponownie gdy zakładka jest widoczna
// eventSource = createConnection();
}
});
3. Zaimplementuj wykrywanie heartbeat¶
Wykrywaj nieaktywne połączenia:
let heartbeatTimeout;
eventSource.onmessage = (event) => {
clearTimeout(heartbeatTimeout);
// Oczekuj wiadomości w ciągu 60 sekund
heartbeatTimeout = setTimeout(() => {
console.warn('Nie otrzymano heartbeat, ponowne łączenie...');
eventSource.close();
eventSource = createConnection();
}, 60000);
};
4. Używaj wykładniczego backoff¶
Dla ręcznych ponownych połączeń:
import time
def exponential_backoff(attempt, max_wait=60):
"""Oblicz opóźnienie backoff z wykładniczym wzrostem"""
wait = min(2 ** attempt, max_wait)
return wait
attempt = 0
while True:
try:
subscribe(topic)
attempt = 0 # Zresetuj po sukcesie
except Exception as e:
wait = exponential_backoff(attempt)
print(f'Ponowne łączenie za {wait}s...')
time.sleep(wait)
attempt += 1
Ograniczenia¶
- Rozmiar wiadomości: Maks. 4 000 znaków na wiadomość
- Limit połączeń: 100 równoczesnych połączeń SSE na IP (plan darmowy)
- Historia wiadomości: Ostatnie 100 wiadomości w pamięci podręcznej (12 godzin TTL)
- Limit przeglądarki: 6 równoczesnych połączeń SSE na domenę (ograniczenie przeglądarki)
Rozwiązywanie problemów¶
Połączenie natychmiast się zamyka¶
Problem: Połączenie SSE otwiera się i natychmiast zamyka.
Rozwiązania: 1. Sprawdź token uwierzytelniający (dla prywatnych tematów) 2. Zweryfikuj, czy temat istnieje 3. Sprawdź ustawienia sieci/zapory 4. Przejrzyj konsolę przeglądarki pod kątem błędów
Brak otrzymywanych wiadomości¶
Problem: Połączono, ale nie otrzymuję wiadomości.
Rozwiązania:
1. Opublikuj wiadomość testową, aby zweryfikować temat
2. Sprawdź parametr since_id (może być zbyt niedawny)
3. Zweryfikuj, że sieć nie buforuje odpowiedzi
4. Użyj flagi -N z curl (wyłącz buforowanie)
Wysokie zużycie pamięci¶
Problem: Zakładka przeglądarki zużywa zbyt dużo pamięci.
Rozwiązania: 1. Ogranicz wyświetlane wiadomości (zachowaj ostatnie 50-100) 2. Zamknij połączenie gdy zakładka jest ukryta 3. Okresowo czyszcz stare wiadomości z DOM
Następne kroki¶
- Subskrypcja WebSocket - Dwukierunkowa alternatywa
- Przewodnik aplikacji web - Subskrybuj przez interfejs web
- Przewodnik aplikacji mobilnej - Subskrybuj na iOS/Android
- Przewodnik publikowania - Wysyłaj wiadomości do tematów
Wskazówka: Używaj SSE dla większości subskrypcji w czasie rzeczywistym - jest proste, niezawodne i działa wszędzie! Używaj WebSocket tylko jeśli potrzebujesz dwukierunkowej komunikacji.