Search code examples
multithreadingdelphisocketsindy

Indy 10 IdTCPClient Reading Data using a separate thread?


Question: What I'm looking for is the most typical or best practice way to use a separate thread to receive data using an IdTCPClient in Indy 10.

Background: The below code is a sample of what I'm trying to do with the actual data processing parts removed for clarity. The Idea of the Thread is to receive all data (Variable size with a header declaring the rest of the message length) and to then to parse it (That's what the HandleData procedure does) and trigger an Event Handler depending on the command.

The TIdIOHandlerSocket is passed to the thread by the main application which also Writes data to the socket as and when it is required.

TScktReceiveThread = class(TThread)
  private
    { Private declarations }
    procedure HandleData;
  protected
    procedure Execute; override;
  public
    FSocket: TIdIOHandlerSocket;
    constructor Create(CreateSuspended: boolean);
  end;


procedure TScktReceiveThread.Execute;
var
  FixedHeader: TBytes;
begin
  Assert(FSocket <> nil, 'You must assign the connected socket to the receiving thread');
  SetLength(FixedHeader, 2);
   while not Terminated do
    begin
      if not FSocket.Connected then
        Suspend
      else
        begin
          FSocket.CheckForDataOnSource(10);
          if not FSocket.InputBufferIsEmpty then
           begin
            FSocket.ReadBytes(FixedHeader, SizeOf(FixedHeader), false);
            // Removed the rest of the reading and parsing code for clarity
            Synchronize(HandleData);
           end;
        end;
    end;
end;

As a prefix, I have used another StackOverflow question which deals with the server components of Indy: "Delphi 2009, Indy 10, TIdTCPServer.OnExecute, how to grab all the bytes in the InputBuffer" to get the basis of what I have so far.

Thanks for any help!


Solution

  • If you want to avoid the overhead imposed by creating thread classes for each and every client-server data exchange, you could create a motile threading class as described in

    http://delphidicas.blogspot.com/2008/08/anonymous-methods-when-should-they-be.html

    I had the same problem a few days ago and I just wrote me a class TMotileThreading which has static functions that let me create threads using the new anonymous method feature of D2009. Looks something like this:

    type
      TExecuteFunc = reference to procedure;
    
      TMotileThreading = class
      public
        class procedure Execute (Func : TExecuteFunc);
        class procedure ExecuteThenCall (Func : TExecuteFunc; ThenFunc : TExecuteFunc);
      end;
    

    The second procedure allows me to perform a client-server communication like in your case and do some stuff whenever the data has arrived. The nice thing about anonymous methods is that you can use the local variables of the calling context. So a communication looks something like this:

    var
      NewData  : String;
    begin
      TMotileThreading.ExecuteThenCall (
        procedure
        begin
          NewData := IdTCPClient.IOHandler.Readln;
        end,
        procedure
        begin
          GUIUpdate (NewData);
        end);
     end;
    

    The Execute and ExecuteThenCall method simply create a worker thread, set FreeOnTerminate to true to simplify memory management and execute the provided functions in the worker thread's Execute and OnTerminate procedures.

    Hope that helps.

    EDIT (as requested the full implementation of class TMotileThreading)

    type
      TExecuteFunc = reference to procedure;
    
      TMotileThreading = class
      protected
        constructor Create;
      public
        class procedure Execute (Func : TExecuteFunc);
        class procedure ExecuteAndCall (Func : TExecuteFunc; OnTerminateFunc : TExecuteFunc;
                                    SyncTerminateFunc : Boolean = False);
      end;
    
      TMotile = class (TThread)
      private
        ExecFunc             : TExecuteFunc;
        TerminateHandler     : TExecuteFunc;
        SyncTerminateHandler : Boolean;
      public
        constructor Create (Func : TExecuteFunc); overload;
        constructor Create (Func : TExecuteFunc; OnTerminateFunc : TExecuteFunc;
                            SyncTerminateFunc : Boolean); overload;
        procedure OnTerminateHandler (Sender : TObject);
        procedure Execute; override;
      end;
    
    implementation
    
    constructor TMotileThreading.Create;
    begin
      Assert (False, 'Class TMotileThreading shouldn''t be used as an instance');
    end;
    
    class procedure TMotileThreading.Execute (Func : TExecuteFunc);
    begin
      TMotile.Create (Func);
    end;
    
    class procedure TMotileThreading.ExecuteAndCall (Func : TExecuteFunc;
                                                     OnTerminateFunc : TExecuteFunc;
                                                     SyncTerminateFunc : Boolean = False);
    begin
      TMotile.Create (Func, OnTerminateFunc, SyncTerminateFunc);
    end;
    
    constructor TMotile.Create (Func : TExecuteFunc);
    begin
      inherited Create (True);
      ExecFunc := Func;
      TerminateHandler := nil;
      FreeOnTerminate := True;
      Resume;
    end;
    
    constructor TMotile.Create (Func : TExecuteFunc; OnTerminateFunc : TExecuteFunc;
                                SyncTerminateFunc : Boolean);
    begin
      inherited Create (True);
      ExecFunc := Func;
      TerminateHandler := OnTerminateFunc;
      SyncTerminateHandler := SyncTerminateFunc;
      OnTerminate := OnTerminateHandler;
      FreeOnTerminate := True;
      Resume;
    end;
    
    procedure TMotile.Execute;
    begin
      ExecFunc;
    end;
    
    procedure TMotile.OnTerminateHandler (Sender : TObject);
    begin
      if Assigned (TerminateHandler) then
        if SyncTerminateHandler then
          Synchronize (procedure
                       begin
                         TerminateHandler;
                       end)
        else
          TerminateHandler;
    end;