Nell'ultima versione di WiRL è stato inserito il supporto per SSE (Server-sent events o eventi lato server) e per la modalità di trasferimento chunked, che permette di inviare una risposta dividendola in vari blocchi/chunks.
SSE - Server-sent events
SSE è una tecnologia molto interessante che permette di inviare degli eventi dal server al client, cosa che con HTTP normalmente sarebbe impossibile. Quello che avviene è che il client si collega ad un particolare endpoint del server e mantiene la connessione perennemente aperta, anche in caso di disconnessione (per problemi di rete o quant'altro) il client si deve occupare di ripristinarla non appena possibile.
Implementazione
Anche dal punto di vista di WiRL la risorsa che implementa questi eventi deve essere costruita in maniera un po' particolare visto che non si tratta di inviare un semplice oggetto al client. Vediamo innanzitutto come si dichiara il metodo che andrà ad implementare la risorsa:
[GET]
[Produces(TMediaType.TEXT_EVENT_STREAM)]
function ServerSideEvents([QueryParam('tag')] const ATag: string): TWiRLSSEResponse;
La prima cosa che si nota è l'attributo Produces
con il valore TEXT_EVENT_STREAM, questo perché il protocollo prevede una specifico content-type
da usare per SSE. Il metodo HTTP in questo caso è GET
; tecnicamente potrebbe essere usato anche un altro metodo ma se il client è scritto in JavaScript la libreria standard supporta solo GET. Di seguito vediamo dei parametri, che possono essere di qualunque tipo, e infine il tipo di risposta che deve essere per forza TWiRLSSEResponse
.
Vediamo adesso l'implementazione:
function TMyResource.ServerSideEvents(const ATag: string): TWiRLSSEResponse;
begin
Result := TWiRLSSEResponse.Create(
procedure (AWriter: IWiRLSSEResponseWriter)
var
LMessage: string;
begin
// Continua finche il server è "vivo"
while FServer.Active do
begin
// Legge un messaggio dalla coda
LMessage := MessageQueue.PopItem;
if LMessage <> '' then
// Se lo trova lo invia al client
AWriter.Write(LMessage);
end;
end
);
end;
Come si vede nell'esempio la classe TWiRLSSEResponse
prende come primo parametro un metodo anonimo che provvederà ad inviare gli eventi. Il metodo anonimo prosegue finche il server è "vivo" (l'oggetto FServer
può essere recuperato tramite la Context Injection di WiRL). In questo caso i messaggi sono nell'oggetto MessageQueue
(definito come una coda thread-safe: TThreadedQueue<string>
). Il programma tenta di estrarre un messaggio dalla coda e se lo trova lo invia al client tramite l'oggetto referenziato dall'interfaccia IWiRLSSEResponseWriter
. Ma vediamo quali metodi fornisce questa interfaccia:
procedure Write(const AValue: string);
Questa è la funzione base che permette di inviare un evento al client. Notare che il contenuto di un evento può essere esclusivamente una stringa. Per inviare oggetto più complessi è necessario usare qualche codifica, come da esempio Base64 nel caso volessimo inviare un dato binario.
procedure Write(const AEvent, AValue: string);
Questo metodo è simile al precedente ma ha in più il parametro event che permette di "categorizzare" il messaggio. In effetti l'API JavaScript permette al client di ricevere solo i messaggi appartenenti ad una certa categoria.
procedure Write(const AEvent, AValue: string; ARetry: Integer);
Questa versione di Write ha in più il parametro ARetry
che indica al client quanto tempo aspettare prima di aprire una nuova connessione una volta che quella corrente venga chiusa. In effetti, a parte errori di rete, il server potrebbe benissimo chiudera la connessione in qualunque momento.
procedure WriteComment(const AValue: string);
Questo metodo invia un commento, in generale il client dovrebbe ignorarlo, lo scopo è di solito quello di tenere la connessione attiva, questo perché se in una connessione aperta non possano dati per troppo tempo il client o anche un eventuale proxy potrebbero decidere di chiuderla.
Il client
Attualmente WiRL non fornisce un modo semplice per leggere gli eventi in arrivo tramite SSE, anche se è ovviamente possibile usare i componenti Indy (TIdHTTP
) o il nuovo THttpClient
.
Se invece il cilent è il browser è possibile usare l'oggetto EventSource che serve proprio a questo.
const evtSource = new EventSource("/rest/app/myevent");
evtSource.onmessage = (event) => {
console.log(event.data);
};
Chunked transfer encoding
L'altra possibilità inserita con l'ultima versione è quella di usare il transfer encoding di tipo chunked. Questa funzione permette di inviare i dati dal server al client a blocchi. Questo può essere utile in diversi casi:
- Quando la dimensione totale del contenuto non è nota in anticipo, per esempio durante la generazione dinamica di pagine web o contenuti.
- Per lo streaming di dati in tempo reale, consentendo al client di iniziare a elaborare i dati mentre vengono ancora trasmessi.
- Per migliorare i tempi di risposta percepiti, poiché il browser può iniziare a renderizzare parti della pagina mentre altre sono ancora in fase di download.
- Nei casi di grandi trasferimenti di file, dove inviare il contenuto in chunk (blocchi) può essere più efficiente della trasmissione di un singolo blocco grande.
Implementazione
Implementare una risorsa che faccia uso di Chunked transfer encoding non è troppo diverso dal caso di SSE, in effetti in entrambi i casi abbiamo una risorsa che produce un risultato in maniera diluita nel tempo. Vendiamo innanzitutto l'interfaccia del metodo che implementa la risorsa:
[GET]
[Produces(TMediaType.TEXT_PLAIN)]
function Chunks([QueryParam('chunks'), DefaultValue('5')] ANumOfChunks: Integer): TWiRLChunkedResponse;
In questo caso il metodo HTTP è una GET ma può essere qualunque; anche il Content-Type, al quale l'attributo Produces
fa riferimento, può essere qualunque, e non si riferisce al singolo chunk ma all'intero output della risorsa. I parametri possono chiaramente essere di qualsiasi tipo mentre quello che distingue una risorsa chunked è il tipo restituito, che deve essere TWiRLChunkedResponse
.
L'implementazione, come nel caso precedente, dovrà fornire al costruttore di TWiRLChunkedResponse
una procedura anonima che invierà i singoli chunk al client.
function TMyResource.Chunks(ANumOfChunks: Integer): TWiRLChunkedResponse;
begin
Result := TWiRLChunkedResponse.Create(
procedure (AWriter: IWiRLResponseWriter)
var
LCounter: Integer;
begin
// Invia i dati in ANumOfChunks chunks
for LCounter := 1 to ANumOfChunks do
begin
// Invia il singolo chunk
AWriter.Write(IntToStr(LCounter));
// Simula l'attesa necessaria per ottenere il dato successivo
Sleep(1000);
end;
end
);
end;
In questo caso la risposta viene inviata suddivisa in diversi chunk decisi dal client. Un singolo chunk contiene un dato binario. L'oggetto referenziato dall'interfaccia IWiRLResponseWriter
ha però diversi metodi:
procedure Write(const AValue: string; AEncoding: TEncoding = nil);
Permette di inviare una stringa trasformandola il binario con l'encoding specificato. In assenza di encoding viene usato UTF-8.
procedure Write(const AValue: TBytes);
Questo metodo permette di inviare dati binary usando direttamente TBytes
.
Client
Al momento TWiRLClient
non prevede nessun meccanismo particolare per la lettura dei dati divisi in chunk. Il componete restituirà l'intera risposta una volta avvenuta la ricezione di tutti i chunks. Però sia il componente TIdHTTP
che THttpClient
tramite degli eventi possono leggere i chunk man mano che arrivano. Per esempio è possibile usare il componente TIdHTTP in questo modo:
procedure TForm1.ButtonIdHTTP1Click(Sender: TObject);
begin
// Aggancia l'evento OnChunkReceived
IdHTTP1.OnChunkReceived := IdHTTP1ChunkReceived;
// Effettua la chiamata
IdHTTP1.Get('http://localhost:8080/rest/app/chunk');
end;
procedure TForm1.IdHTTP1ChunkReceived(Sender: TObject;
var Chunk: TIdBytes);
var
LText: string;
begin
// Converte il chunk in stringa
LText := IndyTextEncoding_UTF8.GetString(Chunk);
// e lo aggiunge ad un memo
MemoLog.Lines.Text := MemoLog.Lines.Text + LText;
end;
Conclusione
In questo articolo ho cercato di fornire una panoramica sull'utilizzo di chunk e SSE con l'ultima versione di WiRL. Scaricando i sorgenti da GitHub è possibile testare il Demo 23.Chunks che fornisce vari esempi d'uso. Al momento la parte client è un po' debole ma con le prossime release verrà fornito anche un supporto migliorato per il componente TWiRLClient
.