3 min čítania
Prázdny polling
Kolega vám niečo pošle. Nemali by ste kvôli tomu obnovovať stránku.
Prvá verzia to riešila pollingom: prehliadač sa každých pár sekúnd pýtal „niečo nové?“ a väčšinou počul „nie“. Skrátenie intervalu len znásobí prázdne požiadavky a notifikácia aj tak mešká až o celý interval. Polling škáluje cenu pýtania sa, nie rýchlosť dozvedenia sa.
Problém je jednosmerný: server vie, kedy sa niečo stane, klient sa to len potrebuje dozvedieť. To je vysielanie, nie rozhovor.
Server-Sent Events
SSE sedí presne: jedno dlho žijúce HTTP spojenie, do ktorého server posiela dáta. Žiadny polling, žiadny WebSocket handshake pre kanál, ktorý tečie len jedným smerom. V Spring Boote je to jeden SseEmitter na používateľa, držaný v registri:
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(@AuthenticationPrincipal User user) {
SseEmitter emitter = new SseEmitter(Duration.ofMinutes(30).toMillis());
registry.add(user.id(), emitter);
emitter.onCompletion(() -> registry.remove(user.id(), emitter));
emitter.onTimeout(() -> registry.remove(user.id(), emitter));
return emitter;
}
void notify(UserId recipient, Notification event) {
for (SseEmitter emitter : registry.get(recipient)) {
try {
emitter.send(SseEmitter.event().name("stream").data(event));
} catch (IOException dead) {
registry.remove(recipient, emitter);
}
}
}Pasca s vlastnou hlavičkou
Stream je za rovnakou autentifikáciou ako všetko ostatné: za hlavičkou Authorization. Natívny EventSource v prehliadači ju poslať nedokáže.
event-source-polyfill hovorí rovnakým protokolom, ale prijme skutočné hlavičky. Práve tá hlavička bola jediný dôvod siahnuť po ňom.
Opätovné pripojenie je samostatná téma: natívny EventSource aj polyfill sa pripájajú znova sami. Tu je ich predvolené opakovanie nahradené obmedzeným exponenciálnym backoffom:
import { EventSourcePolyfill } from "event-source-polyfill";
connect(retry = 0): void {
const stream = new EventSourcePolyfill("/api/stream", {
headers: { Authorization: `Bearer ${this.token}` },
});
stream.addEventListener("stream", (event) => {
this.show(JSON.parse((event as MessageEvent).data));
retry = 0;
});
stream.onerror = () => {
stream.close();
const delay = Math.min(30_000, 1_000 * 2 ** retry);
window.setTimeout(() => this.connect(retry + 1), delay);
};
}Práve ten backoff je rozdiel medzi demom a niečím, na čo sa dá spoľahnúť celý pracovný deň: spadnuté spojenie sa zotaví samo a nikto nemusí obnovovať stránku.
Tú istú myšlienku, aktualizácie posielané zo servera cez jedno dlho žijúce spojenie, som neskôr spracoval do malej Kotlin knižnice: stream-pulse, publikovanej na JitPacku.
Výsledok
Notifikácie prichádzajú v momente, keď kolega koná, nie s oneskorením o interval. Server neodpovedá na žiadne prázdne polly. Spadnuté spojenie sa pripojí samo.
Nástroj bol malý. Inžinierstvo bolo všetko okolo neho.
Zdieľať článok