WIRL - Chunked Encoding and SSE
WiRL supports SSE (Server-Sent Events) and chunked transfer encoding, which allows you to send a response divided into multiple blocks or chunks. This chapter explores both of these powerful features.
DEMO
You can find a demo showing how to use the SSE and Chunked Encoding in the demo\23.Chunks
folder.
SSE - Server-Sent Events
SSE is a specification that allows the server to send events to the client, something normally not possible with standard HTTP. What happens is that the client connects to a specific endpoint on the server and keeps the connection permanently open. Even in case of disconnection (due to network problems or other issues), the client must take care of restoring it as soon as possible.
Implementation
From WiRL's perspective, a resource that implements these events needs to be built following a specific pattern since it's not just about sending a simple object to the client. Let's first look at how to declare the method that will implement the resource:
[GET]
[Produces(TMediaType.TEXT_EVENT_STREAM)]
function ServerSideEvents([QueryParam('tag')] const ATag: string): TWiRLSSEResponse;
The first thing to note is the Produces
attribute with the value TEXT_EVENT_STREAM
, as the SSE protocol uses a specific Content-Type. The HTTP method in this case is GET; you can use any HTTP method you like, but if the client is written in JavaScript, the standard API only supports GET. Then follows the parameters, which can be of any type, and finally the response type which has to be TWiRLSSEResponse
.
Now let's look at the implementation:
function TMyResource.ServerSideEvents(const ATag: string): TWiRLSSEResponse;
begin
Result := TWiRLSSEResponse.Create(
procedure (AWriter: IWiRLSSEResponseWriter)
var
LMessage: string;
begin
// Continue while the server is "alive"
while FServer.Active do
begin
// Read a message from the queue
LMessage := MessageQueue.PopItem;
if LMessage <> '' then
// If found, send it to the client
AWriter.Write(LMessage);
end;
end
);
end;
As you can see the TWiRLSSEResponse
class has a constructor with only one argument. That's an anonymous method that will provide the events. The anonymous method continues as long as the server is "alive" (the FServer
object can be retrieved via WiRL's Context Injection). In this example, the messages are in the MessageQueue
object (defined as a thread-safe queue: TThreadedQueue<string>
). The program attempts to extract a message from the queue and, if found, sends it to the client through the object referenced by the IWiRLSSEResponseWriter
interface. Let's see the methods of this interface:
procedure Write(const AValue: string);
This is the basic method for sending an event to the client. Note that the content of an event can only be a string. To send more complex objects, it's necessary to use some encoding, such as Base64 for binary data.
procedure Write(const AEvent, AValue: string);
This method is similar to the previous one but has the additional event
parameter that allows "categorizing" the message. In fact, the JavaScript API allows the client to receive only messages belonging to a certain category.
procedure Write(const AEvent, AValue: string; ARetry: Integer);
This version of Write
has the additional ARetry
parameter that tells the client how long to wait before opening a new connection once the current one is closed. Indeed, apart from network errors, the server could close the connection at any time.
procedure WriteComment(const AValue: string);
This method sends a comment, which the client should generally ignore. Its main purpose is to keep the connection alive, as clients or proxies might close idle connections.
The Client
Currently, WiRL doesn't offer a built-in way to read incoming events via SSE, although it is of course possible to use Indy components (TIdHTTP
) or the new THttpClient
.
If the client is a browser, you can use the EventSource
object:
const evtSource = new EventSource("/rest/app/myevent");
evtSource.onmessage = (event) => {
console.log(event.data);
};
Chunked Transfer Encoding
Another similar feature is support for chunked transfer encoding. This function allows sending data from server to client in blocks. This can be useful in several cases:
- When the total size of the content is not known in advance, for example during dynamic generation of web pages or content.
- For real-time data streaming, allowing the client to start processing data before it has been completely received.
- To improve perceived response times, as the browser can start rendering parts of the page while others are still downloading.
- In cases of large file transfers, where sending content in chunks may be more efficient than transmitting a single large block.
Implementation
Implementing a resource that uses chunked transfer encoding is not too different from the SSE case. In both cases, the resource produces a result gradually over time. Let's first look at the method interface that implements the resource:
[GET]
[Produces(TMediaType.TEXT_PLAIN)]
function Chunks([QueryParam('chunks'), DefaultValue('5')] ANumOfChunks: Integer): TWiRLChunkedResponse;
In this case, the HTTP method is GET but it can be any method; the Content-Type, which the Produces
attribute refers to, can also be any type, and it refers to the entire output of the resource, not the individual chunk. The parameters can clearly be of any type, while what distinguishes a chunked resource is the return type, which must be TWiRLChunkedResponse
.
The implementation, as in the previous case, will have to provide the TWiRLChunkedResponse
constructor with an anonymous procedure that will send individual chunks to the client:
function TMyResource.Chunks(ANumOfChunks: Integer): TWiRLChunkedResponse;
begin
Result := TWiRLChunkedResponse.Create(
procedure (AWriter: IWiRLResponseWriter)
var
LCounter: Integer;
begin
// Send data in ANumOfChunks chunks
for LCounter := 1 to ANumOfChunks do
begin
// Send a single chunk
AWriter.Write(IntToStr(LCounter));
// Simulate the wait required to get the next data
Sleep(1000);
end;
end
);
end;
In this example, the response is sent divided into different chunks determined by the client. Each chunk contains binary data. However, the object referenced by the IWiRLResponseWriter
interface has several methods:
procedure Write(const AValue: string; AEncoding: TEncoding = nil);
Allows sending a string by transforming it to binary with the specified encoding. In the absence of encoding, UTF-8 is used.
procedure Write(const AValue: TBytes);
This method allows sending binary data directly using TBytes
.
Client
At the moment, TWiRLClient
doesn't provide any special mechanism for reading data divided into chunks. The component will return the entire response once all chunks have been received. However, both the TIdHTTP
component and THttpClient
can handle incoming chunks in real-time via events. For example, you can use the TIdHTTP
component in this way:
procedure TForm1.ButtonIdHTTP1Click(Sender: TObject);
begin
// Hook the OnChunkReceived event
IdHTTP1.OnChunkReceived := IdHTTP1ChunkReceived;
// Make the call
IdHTTP1.Get('http://localhost:8080/rest/app/chunk');
end;
procedure TForm1.IdHTTP1ChunkReceived(Sender: TObject;
var Chunk: TIdBytes);
var
LText: string;
begin
// Convert the chunk to string
LText := IndyTextEncoding_UTF8.GetString(Chunk);
// and add it to a memo
MemoLog.Lines.Text := MemoLog.Lines.Text + LText;
end;
Conclusion
In this chapter, we've provided an overview of using chunks and SSE with the latest version of WiRL. By downloading the source code from GitHub, you can test Demo 23.Chunks which provides various examples of use. The current client support is somewhat limited, but future releases will offer improved features in the TWiRLClient
component.