WIRL - Chunked encoding e SSE
Luca Minuti

Luca Minuti @lminuti

About: Full Stack Developer and Architect. Mainly involved in Delphi, Java, and JavaScript. I'm a Sencha MVP and speaker at technical conferences.

Location:
Italy
Joined:
Dec 29, 2023

WIRL - Chunked encoding e SSE

Publish Date: Apr 2
0 0

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.

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment