Search code examples
delphiindyindy10idhttp

HTTP continuous packeted stream with Indy


I have a JSON-RPC service which for one of the requests returns a continuous stream of JSON objects.

I.e. :

{id:'1'}
{id:'2'}
//30 minutes of no data
{id:'3'}
//...

Of course, there's no Content-Length because the stream is endless.

I'm using custom TStream descendant to receive and parse the data. But internally TIdHttp buffers the data and does not pass it to me until RecvBufferSize bytes are received.

This results in:

{id:'1'} //received
{id:'2'} //buffered by Indy but not received
//30 minutes of no data
{id:'3'} //this is where Indy commits {id:'2'} to me

Obviously this won't do because the message which mattered 30 minutes ago should have been delivered 30 minutes ago.

I'd like Indy to do just what sockets do: read up to RecvBufferSize or less if there's data available and return immediately.

I've found this discussion from 2005 where some poor soul tried to explain the problem to Indy developers but they didn't understand him. (Read it; it's a sad sight)

Anyway, he worked around this by writing custom IOHandler descendant, but that was back in 2005, maybe there are some ready solutions today?


Solution

  • While using TCP stream was an option, in the end I went with original solution of writing custom TIdIOHandlerStack descendant.

    The motivation was that with TIdHTTP I know what doesn't work and only need to fix that, while switching to lower level TCP means new problems can arise.

    Here's the code that I'm using, and I'm going to discuss the key points here.

    New TIdStreamIoHandler has to inherit from TIdIOHandlerStack.

    Two functions need to be rewritten: ReadBytes and ReadStream:

    function TryReadBytes(var VBuffer: TIdBytes; AByteCount: Integer;
      AAppend: Boolean = True): integer; virtual;
    procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1;
      AReadUntilDisconnect: Boolean = False); override;
    

    Both are modified Indy functions which can be found in IdIOHandler.TIdIOHandler. In ReadBytes the while clause has to be replaced with a singe ReadFromSource() request, so that TryReadBytes returns after reading up to AByteCount bytes in one go.

    Based on this, ReadStream has to handle all combinations of AByteCount (>0, <0) and ReadUntilDisconnect (true, false) to cyclically read and then write to stream chunks of data arriving from the socket.

    Note that ReadStream need not terminate prematurely even in this stream version if only part of the requested data is available in the socket. It just has to write that part to the stream instantly instead of caching it in FInputBuffer, then block and wait for the next part of data.