HTTP Streaming: jedna odpowiedź, wiele fragmentów danych
- data
- kategoria
- Backend
- także w
- Frontend · Networking
- czytanie
- 2 min / 344 słów
Long polling pozwala serwerowi opóźnić odpowiedź.
Ale kiedy event już się pojawi, response się kończy:
request
<- event
EOF
Następny event wymaga następnego requestu.
HTTP nie wymaga jednak, aby całe body było dostępne przed rozpoczęciem odpowiedzi.
Response może powstawać w czasie
Model może wyglądać tak:
client -> request
client <- headers
client <- bytes
client <- bytes
client <- bytes
client <- EOF
Najważniejsza zmiana:
response received
nie jest pojedynczym momentem.
Headers mogą zostać odebrane, gdy body nadal jest produkowane.
Fetch wystawia strumień
const response = await fetch("/events");
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {
stream: true
});
process(chunk);
}
response.body jest ReadableStream.
To pozwala aplikacji konsumować dane bez czekania na zakończenie response.
Chunk nie jest wiadomością
To jedna z najważniejszych rzeczy w streamingu.
Jeżeli serwer logicznie wysłał:
{"id":42,"status":"completed"}
klient może otrzymać bytes jako:
{"id":42,
a potem:
"status":"completed"}
albo kilka logicznych wiadomości w jednym odczycie.
Granice chunków transportowych nie są granicami rekordów aplikacyjnych.
Dlatego potrzebujemy framingu.
NDJSON
Jednym z prostych formatów jest Newline Delimited JSON:
{"id":1,"status":"processing"}
{"id":2,"status":"processing"}
{"id":1,"status":"completed"}
Każdy rekord kończy newline.
Klient buforuje dane do momentu znalezienia separatora:
bytes
|
v
buffer
|
v
newline found
|
v
parse one JSON object
Alternatywą może być length-prefix framing, własny format binarny albo inny protokół aplikacyjny.
HTTP streaming daje bytes.
Znaczenie bytes należy do nas.
Streaming nie oznacza HTTP/1.1 chunked encoding
Historycznie streaming HTTP/1.1 często kojarzył się z:
Transfer-Encoding: chunked
Ale to szczegół framingu HTTP/1.1.
W HTTP/2 i HTTP/3 body również może być dostarczane stopniowo, mimo że protokół nie używa tego samego mechanizmu Transfer-Encoding: chunked.
Dlatego poprawny model to:
streaming HTTP response
a nie:
chunked encoding = streaming
Backpressure
Streaming tworzy nowy problem.
Co jeśli backend produkuje dane szybciej niż klient je przetwarza?
producer >>> consumer
Bufor nie może rosnąć bez końca.
Streams mają pojęcie backpressure: wolniejszy konsument powinien wpływać na tempo, w jakim dane są czytane i propagowane przez pipeline.
Ale na poziomie całego systemu nadal trzeba zdecydować:
- buforować,
- spowolnić producenta,
- odrzucać dane,
- zamknąć połączenie.
To nie jest detal implementacyjny. Dla realtime może być częścią semantyki produktu.
Gdzie używamy raw HTTP streaming?
Dobre przypadki to:
- token streaming z modelu językowego,
- log output,
- progres długiego procesu,
- duże wyniki produkowane inkrementalnie,
- NDJSON feeds.
Jeżeli komunikacja jest po prostu:
one request
-> incremental response
-> done
nie potrzebujemy WebSocketa.
Czego nam nadal brakuje?
Mamy trwały response i możemy wysyłać kolejne rekordy.
Ale sami musimy ustalić:
How is an event framed?
Does it have an ID?
How does reconnect work?
How does the client resume?
Dla jednokierunkowego strumienia z serwera do przeglądarki platforma webowa ma gotową odpowiedź.
Server-Sent Events nie tworzy nowego transportu.
Standaryzuje event stream nad HTTP.