11

I have a room with 60 computers/devices (40 computers and 20 oscilloscopes Windows CE based) and I would like to know which and every one is alive using ping. First I wrote a standard ping (see here Delphi Indy Ping Error 10040), which is working fine now but takes ages when most computers are offline.

So what I am trying to do is to write a MultiThread Ping but I am quite struggling with it. I have seen only very few examples over the internet and no one was matching my needs, that's why I try to write it myself.

I use XE2 and Indy 10 and the form is only constitued of a memo and a button.

unit Main;

interface

uses
  Winapi.Windows, System.SysUtils, System.Classes, Vcl.Forms,
  IdIcmpClient, IdGlobal, Vcl.StdCtrls, Vcl.Controls;

type
  TMainForm = class(TForm)
    Memo1: TMemo;
    ButtonStartPing: TButton;
    procedure ButtonStartPingClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

type
  TMyPingThread = class(TThread)
  private
    fIndex : integer;
    fIdIcmpClient: TIdIcmpClient;
    procedure doOnPingReply;
  protected
    procedure Execute; override;
  public
    constructor Create(index: integer);
  end;

var
  MainForm: TMainForm;
  ThreadCOunt : integer;

implementation

{$R *.dfm}

constructor TMyPingThread.Create(index: integer);
begin
  inherited Create(false);

  fIndex := index;
  fIdIcmpClient := TIdIcmpClient.Create(nil);
  fIdIcmpClient.ReceiveTimeout := 200;
  fIdIcmpClient.PacketSize := 24;
  fIdIcmpClient.Protocol := 1;
  fIdIcmpClient.IPVersion := Id_IPv4;

  //first computer is at adresse 211
  fIdIcmpClient.Host := '128.178.26.'+inttostr(211+index-1);

  self.FreeOnTerminate := true;
end;

procedure TMyPingThread.doOnPingReply;
begin
  MainForm.Memo1.lines.add(inttostr(findex)+' '+fIdIcmpClient.ReplyStatus.Msg);
  dec(ThreadCount);

  if ThreadCount = 0 then
    MainForm.Memo1.lines.add('--- End ---');
end;

procedure TMyPingThread.Execute;
begin
  inherited;

  try
    fIdIcmpClient.Ping('',findex);
  except
  end;

  while not Terminated do
  begin
    if fIdIcmpClient.ReplyStatus.SequenceId = findex then Terminate;
  end;

  Synchronize(doOnPingReply);
  fIdIcmpClient.Free;
end;

procedure TMainForm.ButtonStartPingClick(Sender: TObject);
var
  i: integer;
  myPing : TMyPingThread;
begin
  Memo1.Lines.Clear;

  ThreadCount := 0;
  for i := 1 to 40 do
  begin
    inc(ThreadCount);
    myPing := TMyPingThread.Create(i);
    //sleep(10);
  end;
end;

end.

My problem is that it "seems" to work when I uncomment the "sleep(10)", and "seems" not to be working without it. This for sure means I am missing a point in the threading I have written.

In other words. When Sleep(10) is in the code. Every time I clicked the button to get to check the connections the result was correct.

Without the sleep(10), it is working "most" of the time but some times the result is wrong giving me a ping echo on offline computers and no ping echo on online computer, as is the ping reply was not assigned to the correct thread.

Any comment or help is welcome.

----- EDIT / IMPORTANT -----

As a general follow up of this question, @Darian Miller started a Google Code project here https://code.google.com/p/delphi-stackoverflow/ which is a working basis. I mark his answer as the "accepted answer" but users should refer to this open source project (all the credit belongs to him) as it will surely be extended and updated in the future.

Community
  • 1
  • 1
HpTerm
  • 8,151
  • 12
  • 51
  • 67
  • I'd repeat my suggestion - drop stock TThread and use either AsyncCalls or OmniThreads. Your thread implementation seems mostly single-threaded to me! – Arioch 'The Oct 12 '12 at 12:22
  • 'inherited Create(false);' - will that not, potentialy, run the thread before the ctor has finished initialising its fields? – Martin James Oct 12 '12 at 12:30
  • ..and, ICMP ping has no socket context or the like. I'm not convinced that a gnip will be received by the thread that sent the ping, so findex will not match: 'ReplyStatus.SequenceId = findex' will often be false. 'as if the ping reply was not assigned to the correct thread' - yes. – Martin James Oct 12 '12 at 12:34
  • 1
    @Martin i dunno what is implementation of idICMP but [sample of using Windows API](http://citforum.ru/nets/articles/ping/) suggest that most obvious implementation would use nothing about threads and message queues but only a callback. Frankly, i think if topicstarter dropped Indy here and used API directly - he could easily achieve parallel ping with single-threading (or at least maybe bi-threading) application, that would be much less heavy and much more simple. I dunno by which method and in which threads/context Windows would call that callback though... – Arioch 'The Oct 12 '12 at 12:53
  • +1 @Martin. Very interesting point. – HpTerm Nov 19 '12 at 08:25

4 Answers4

11

The root problem is that pings are connectionless traffic. If you have multiple TIdIcmpClient objects pinging the network at the same time, one TIdIcmpClient instance can receive a reply that actually belongs to another TIdIcmpClient instance. You are trying to account for that in your thread loop, by checking SequenceId values, but you are not taking into account that TIdIcmpClient already does that same check internally. It reads network replies in a loop until it receives the reply it is expecting, or until the ReceiveTimeout occurs. If it receives a reply it is not expecting, it simply discards that reply. So, if one TIdIcmpClient instance discards a reply that another TIdIcmpClient instance was expecting, that reply will not get processed by your code, and that other TIdIcmpClient will likely receive another TIdIcmpClient's reply instead, and so on. By adding the Sleep(), you are decreasing (but not eliminating) the chances that pings will overlap each other.

For what you are attempting to do, you won't be able to use TIdIcmpClient as-is to have multiple pings running in parallel, sorry. It is simply not designed for that. There is no way for it to differentiate reply data the way you need it. You will have to serialize your threads so only one thread can call TIdIcmpClient.Ping() at a time.

If serializing the pings is not an option for you, you can try copying portions of TIdIcmpClient's source code into your own code. Have 41 threads running - 40 device threads and 1 response thread. Create a single socket that all of the threads share. Have each device thread prepare and send its individual ping request to the network using that socket. Then have the response thread continuously reading replies from that same socket and routing them back to the appropriate device thread for processing. This is a bit more work, but it will give you the multiple-ping parallelism you are looking for.

If you don't want to go to all that trouble, an alternative is to just use a third-party app that already supports pinging multiple machines at the same time, like FREEPing.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • thanks for the reply. You put in words what I was already thinking. Why I tried performing that in multithread is because I remember reading somewhere that indy 10 was multithread and that SequenceID was here to identify the correct thread. But you confirm me what I was expecting : It is not possible that way. Another point is that unfortunately I cannot use FREEPing because I need to know "internally" of my program which computer is up. – HpTerm Oct 15 '12 at 06:58
  • do you know if Windows uses ICMP.dll or if they have re-written something custom to them. My question is the following : is the ping that I performed in a command prompt window multithread or do it has a contexte. If I open 60 cmd windows and launch a ping in each, will each window get its correct reply ? If yes I could use delphi to do it that way – HpTerm Oct 15 '12 at 16:48
  • 2
    Windows suffers from the same problem I described. If you open multiple command-prompt windows and run Windows' "ping" utility, it can also suffer from multiple instances interferring with each other's replies. In your own code, if you send out the ping requests over a single socket, and have a single thread reading all of the replies from that socket, you will not suffer from the overlap issue. – Remy Lebeau Oct 15 '12 at 20:04
5

Remy explained the problems... I've wanted to do this in Indy for a while so I posted a possible solution that I just put together to a new Google Code project instead of having a long comment here. It's a first-stab sort of thing, let me know if you have some changes to integrate: https://code.google.com/p/delphi-vault/

This code has two ways to Ping...multi-threaded clients as in your example, or with a simple callback procedure. Written for Indy10 and later versions of Delphi.

Your code would end up using a TThreadedPing descendant defining a SynchronizedResponse method:

  TMyPingThread = class(TThreadedPing)
  protected
    procedure SynchronizedResponse(const ReplyStatus:TReplyStatus); override;
  end;

And to fire off some client threads, the code becomes something like:

procedure TfrmThreadedPingSample.butStartPingClick(Sender: TObject);
begin
  TMyPingThread.Create('www.google.com');
  TMyPingThread.Create('127.0.0.1');
  TMyPingThread.Create('www.shouldnotresolvetoanythingatall.com');
  TMyPingThread.Create('127.0.0.1');
  TMyPingThread.Create('www.microsoft.com');
  TMyPingThread.Create('127.0.0.1');
end;

The threaded response is called in a synchronized method:

procedure TMyPingThread.SynchronizedResponse(const ReplyStatus:TReplyStatus);
begin
  frmThreadedPingSample.Memo1.Lines.Add(TPingClient.FormatStandardResponse(ReplyStatus));
end;
Darian Miller
  • 7,808
  • 3
  • 43
  • 62
  • Wouah, you dit a lot of work. Great answer. Right now I'm modifying my code to use your implementation. Perhaps 2 comments. Fist one is to add/modify to be have somewhere a boolean answer of the pingReply. As in my case I just want to know yes/no is the computer connected. Another thing is that your code can interest many people and right now it is specific to my needs : defReceiveTimeout = 200; and defIPVersion = Id_IPv4; the user should be able to change these value to match his specific need (did you try ipv6 ? is it working). – HpTerm Oct 15 '12 at 07:33
  • +1, nice hit to make a shared place for StackOverflow projects! That brings me an idea to make my personal one and always share the whole project since I usually (almost always) test the stuff I'm posting, so now I'll just commit the full project to that personal storage and include also a link to the full project to the answer. – TLama Oct 15 '12 at 08:37
  • @Darian I have read your code carefully. Can you confirm me that I understood it correctly. The main idea in your code is to send multiple ping thread but get all the reply in the same procedure (InternalHandelReply), using SequenceId to know which is which. That way of doing (also as Remy said) also to treat every reply without discarding any of them and therefore treating them all ? Am I correct ? ... – HpTerm Oct 15 '12 at 12:21
  • ... because in fact the result is not that much different than my "no thread code". In my case the ping response when not connected is about 3 seconds. What I was expecting was to have ALL the replies after 3 seconds. In the current code I have to wait for each computer for 3 sec, 60 PC = 3 minutes which is too long .. I am, looking for a way of getting the 40 replies at once and parse through them. The idea of @Remy of have 60 thread all replying to the same single replythread is what I am trying to do, so I can parse the replies manually with the sequenceID (do you understand what i mean) – HpTerm Oct 15 '12 at 12:26
  • Note that I corrected the data. I have 40 computers and 20 oscilloscopes (windows CE based) that's 60 devices not 40 as I first said. – HpTerm Oct 15 '12 at 12:31
  • InternalHandleReply is a private method internal to the class, you shouldn't be using that. Either call the class procedure Ping() directly and provide a callback procedure, or define a TThreadedPing descendant and override the SynchronizedResponse method. – Darian Miller Oct 15 '12 at 13:27
  • I moved the source to a new, more generic named, Google Code Project: https://code.google.com/p/delphi-vault/ and have added a few more source units – Darian Miller Nov 24 '12 at 17:27
1

I did not try your code, so that is all hypothetical, but i think you messed the threads and got classic race condition. I restate my advice to use AsyncCalls or OmniThreadLibrary - they are much simpler and would save you few attempts at "shooting your own foot".

  1. Threads are made to minimize main-thread load. Thread constructor should do minimal work of remembering parameters. Personally i'd moved idICMP creation into .Execute method. If for any reason it would want to create its internal synchronization objects, like window and message queue or signal or whatever, i'd like it to happen already in a new spawned thread.

  2. There is no sense for "inherited;" in .Execute. Better remove it.

  3. Silencing all exceptions is bad style. You probably have errors - but have no way to know about them. You should propagate them to main thread and display them. OTL and AC help you in that, while for tThread you have to do it manually. How to Handle Exceptions thrown in AsyncCalls function without calling .Sync?

  4. Exception logic is flawed. There is no point to have a loop if exception thrown - if no succesful Ping was set - then why waiting for response ? You loop should go within same try-except frame as issuing ping.

  5. Your doOnPingReply executes AFTER fIdIcmpClient.Free yet accesses fIdIcmpClient's internals. Tried changing .Free for FreeAndNil ? That is a classic mistake of using dead pointer after freeing it. The correct approach would be to:
    5.1. either free the object in doOnPingReply
    5.2. or copy all relevant data from doOnPingReply to TThread's private member vars before calling both Synchronize and idICMP.Free (and only use those vars in doOnPingReply ) 5.3. only do fIdIcmpClient.Free inside TMyThread.BeforeDestruction or TMyThread.Destroy. Afterall, if you chosen to create the object in constructor - then you should free it in the matching language construct - destructor.

  6. Since you do not keep references to the thread objects - that While not Terminated loop seems redundant. Just make usual forever-loop and call break.

  7. The aforementioned loop is CPU-hungry, it is like spin-loop. Please call Sleep(0); or Yield(); inside loop to give other threads better chance to do their work. Don't work agaisnt OS scheduler here - you are not in a speed-critical path, no reason to make spinlock here.


Overall, i consider:

  • 4 and 5 as critical bugs for you
  • 1 and 3 as a potential gotcha maybe influencing or maybe not. You'd better 'play safe' rather than doing risky things and investigating if they would work or not.
  • 2 and 7 - bad style, 2 regarding language and 7 regarding platform
  • 6 either you have plans to extend your app, or you broke YAGNI principle, dunno.
  • Sticking with complex TThread instead of OTL or AsyncCalls - strategic errors. Don't you put rooks on your runway, use simple tools.

Funny, this is example of the bug that FreeAndNil could expose and make obvious, while FreeAndNil-haters are claiming it "conceals" bugs.

Community
  • 1
  • 1
Arioch 'The
  • 15,799
  • 35
  • 62
  • Thanks for the long answer, I will read it carefully, I have to leave soon, and will check all that on Monday. – HpTerm Oct 12 '12 at 13:00
  • Your answers analyses my thread and the errors you see in it. There is other major limitations of "ping", discussed by the others, that is the key point here. However your commentaries are very important to my knowledge and I thank you for that. Before implementing the code given by @Dorian I first corrected my code with your commentaries. Here I must tell you that .FreeAndNil is not a method of idICMP. The only available is .Free (??) – HpTerm Oct 15 '12 at 13:24
  • 1
    it is not method - method could not change the value of variable. It is a procedure. `FreeAndNil(obj);` – Arioch 'The Oct 15 '12 at 13:30
  • Oh sorry. Funny thing, working with delphi since version 1.0, and I did not even now about that FreeAndNil stuff ;-) – HpTerm Oct 15 '12 at 13:33
  • it was lacking in D1, been introduced circa D4 or D5 – Arioch 'The Oct 15 '12 at 14:22
0
// This is my communication unit witch works well, no need to know its work but your
// ask   is in the TPingThread class.

UNIT UComm;

INTERFACE

USES
  Windows, Messages, SysUtils, Classes, Graphics, Controls, ExtCtrls, Forms, Dialogs,
  StdCtrls,IdIcmpClient, ComCtrls, DB, abcwav, SyncObjs, IdStack, IdException, 
  IdTCPServer, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient, IdContext,
  UDM, UCommon;

TYPE
  TNetworkState = (nsNone, nsLAN, nsNoLAN, nsNet, nsNoNet);
  TDialerStatus = (dsNone, dsConnected, dsDisconnected, dsNotSync);

  { TBaseThread }

  TBaseThread = Class(TThread)
  Private
    FEvent : THandle;
    FEventOwned : Boolean;
    Procedure ThreadTerminate(Sender: TObject); Virtual;
  Public
    Constructor Create(AEventName: String);
    Property EventOwned: Boolean Read FEventOwned;
  End;

  .
  .
  .

  { TPingThread }

  TPingThread = Class(TBaseThread)
  Private
    FReply : Boolean;
    FTimeOut : Integer;
    FcmpClient : TIdIcmpClient;
    Procedure ReplyEvent(Sender: TComponent; Const AReplyStatus: TReplyStatus);
  Protected
    Procedure Execute; Override;
    Procedure ThreadTerminate(Sender: TObject); Override;
  Public
    Constructor Create(AHostIP, AEventName: String; ATimeOut: Integer);
    Property Reply: Boolean Read FReply;
  End;

  .
  .
  .


{ =============================================================================== }

IMPLEMENTATION

{$R *.dfm}

USES
  TypInfo, WinSock, IdGlobal, UCounter, UGlobalInstance, URemoteDesktop;
  {IdGlobal: For RawToBytes function 10/07/2013 04:18 }

{ TBaseThread }

//---------------------------------------------------------
Constructor TBaseThread.Create(AEventName: String);
Begin
  SetLastError(NO_ERROR);
  FEvent := CreateEvent(Nil, False, False, PChar(AEventName));
  If GetLastError = ERROR_ALREADY_EXISTS
    Then Begin
           CloseHandle(FEvent);
           FEventOwned := False;
         End
    Else If FEvent <> 0 Then
           Begin
             FEventOwned := True;
             Inherited Create(True);
             FreeOnTerminate := True;
             OnTerminate := ThreadTerminate;
           End;
End;

//---------------------------------------------------------
Procedure TBaseThread.ThreadTerminate(Sender: TObject);
Begin
  CloseHandle(FEvent);
End;

{ TLANThread }
 .
 .
 .

{ TPingThread }

//---------------------------------------------------------
Constructor TPingThread.Create(AHostIP: String; AEventName: String; ATimeOut: Integer);
Begin
  Inherited Create(AEventName);
  If Not EventOwned Then Exit;
  FTimeOut := ATimeOut;
  FcmpClient := TIdIcmpClient.Create(Nil);
  With FcmpClient Do
  Begin
    Host := AHostIP;
    ReceiveTimeOut := ATimeOut;
    OnReply := ReplyEvent;
  End;
End;

//---------------------------------------------------------
Procedure TPingThread.Execute;
Begin
  Try
    FcmpClient.Ping;
    FReply := FReply And (WaitForSingleObject(FEvent, FTimeOut) = WAIT_OBJECT_0);
  Except
    FReply := False;
  End;
End;

//---------------------------------------------------------
Procedure TPingThread.ReplyEvent(Sender: TComponent; Const AReplyStatus: TReplyStatus);
Begin
  With AReplyStatus Do
  FReply := (ReplyStatusType = rsEcho) And (BytesReceived <> 0);
  SetEvent(FEvent);
End;

//---------------------------------------------------------
Procedure TPingThread.ThreadTerminate(Sender: TObject);
Begin
  FreeAndNil(FcmpClient);
  Inherited;
End;

{ TNetThread }
.
.
.