Indy 10 IdTCPClient Reading Data using a separate thread?

11,402

Solution 1

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;

Solution 2

You're on the right track. Indy is intended to be used like that. It uses blocking sockets, so the ReadBytes call doesn't return until it's read what you've asked for. Contrast that with non-blocking sockets, where a call may return early, so you either poll or get notified asynchronously to determine when a request has been filled.

Indy is designed with the expectation that the socket objects have their own threads (or fibers). Indy comes with TIdAntifreeze for the folks who want to drag and drop socket components onto their forms and data modules and use the Indy components from the main GUI thread, but that's not generally a good idea if you can avoid it.

Since your thread cannot work without FSocket being assigned, I advise you to simply receive that value in the class's constructor. Assert in the constructor if it's not assigned. Furthermore, it is an error to create your thread non-suspended, so why even give the option? (If the thread is not created suspended, then it will start running, check whether FSocket is assigned, and fail because the creating thread hasn't gotten to assign that field yet.)

Share:
11,402

Related videos on Youtube

jamiei
Author by

jamiei

Java, Clojure, Python, and Ruby. Hobby Delphi and Oxygene developer. Always happy to learn and to be corrected if/when I'm wrong.

Updated on April 17, 2022

Comments

  • jamiei
    jamiei about 2 years

    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!

  • jamiei
    jamiei about 15 years
    Ah yes, You're absolutely correct about the CreateSuspended. That is an error in the paste, I copied a constructor from a default thread because my original passes something else that I felt would unnecessarily complicate the code! My Apologies!
  • jamiei
    jamiei almost 15 years
    This is beautifully elegant, but have you posted the full implementation anywhere? I wasn't able to find the full implemantation of the TMotileThreading class in your post.
  • Conrad Hildebrand
    Conrad Hildebrand almost 15 years
    I added my implementation to the answer.
  • jamiei
    jamiei almost 15 years
    Thank-you Smasher - I can't remember why I didn't accept this when originally posted but it's now accepted. ;)
  • Conrad Hildebrand
    Conrad Hildebrand almost 15 years
    No problem, glad to have helped