Delphi(XE2)Indy(10)多线程Ping

11
我有一个房间里面有60台电脑/设备(40台电脑和20台基于Windows CE的示波器),我想知道哪些设备还在运行,可以使用ping命令。首先,我编写了一个标准的ping命令(请参见这里Delphi Indy Ping Error 10040),现在它可以正常工作,但是当大多数计算机离线时,需要很长时间。
因此,我正在尝试编写一个多线程ping程序,但我遇到了一些困难。我在互联网上只看到了很少的例子,并且没有一个符合我的需求,这就是为什么我要自己编写它的原因。
我使用XE2和Indy 10,窗体只包含一个备忘录和一个按钮。
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.

我的问题是当我取消注释“sleep(10)”时,它“似乎”有效,而没有它则“似乎”不起作用。这肯定意味着我在编写线程时错过了一点。
换句话说,当代码中有Sleep(10)时,每次我点击按钮检查连接时,结果都是正确的。
没有sleep(10),它大部分时间都有效,但有时结果是错误的,对于离线计算机会给我一个ping回显,而在线计算机则没有ping回显,好像ping回复没有分配到正确的线程。
任何评论或帮助都受欢迎。
----- 编辑/重要 -----
作为这个问题的一般跟进,@Darian Miller在这里 https://code.google.com/p/delphi-stackoverflow/开始了一个Google Code项目,这是一个工作基础。我将他的答案标记为“接受的答案”,但用户应参考这个开源项目(所有信用归属于他),因为它肯定会在未来得到扩展和更新。

我想再重申一下我的建议——放弃使用标准的TThread,而是使用AsyncCalls或OmniThreads。你的线程实现在我看来大多数情况下都是单线程的! - Arioch 'The
'inherited Create(false);' - 这样做不会在构造函数完成字段初始化之前可能导致线程运行吗? - Martin James
而且,ICMP ping没有套接字上下文或类似的东西。我不确定发送ping的线程是否会收到gnip,因此findex将不匹配:“ReplyStatus.SequenceId = findex”通常是错误的。“好像ping回复没有分配给正确的线程”-是的。 - Martin James
1
@Martin,我不知道idICMP的实现方式,但是使用Windows API的示例表明最明显的实现方式将不涉及线程和消息队列,而只使用回调函数。坦率地说,如果主题发起者在这里放弃Indy并直接使用API,他可以轻松地实现单线程(或至少可能是双线程)应用程序的并行ping,这将更加轻便和简单。我不知道Windows会以哪种方法和在哪个线程/上下文中调用该回调函数... - Arioch 'The
+1 @Martin。非常有趣的观点。 - HpTerm
4个回答

11
根本问题在于ping是一种无连接的流量。如果您有多个正在同时ping网络的TIdIcmpClient对象,那么一个TIdIcmpClient实例可以接收属于另一个TIdIcmpClient实例的回复。您正在尝试通过检查SequenceId值在线程循环中解决这个问题,但您没有考虑到TIdIcmpClient已经在内部执行了相同的检查。它会在循环中读取网络回复,直到收到它预期的回复或者超时(ReceiveTimeout)发生为止。如果它收到了一个不是它所期望的回复,它会简单地丢弃该回复。因此,如果一个TIdIcmpClient实例丢弃了另一个TIdIcmpClient实例期望的回复,那么该回复将不会被您的代码处理,而那个TIdIcmpClient可能会收到另一个TIdIcmpClient的回复,以此类推。通过添加Sleep(),您正在减少(但不是消除)ping重叠的机会。
对于您想要做的事情,很抱歉您不能直接使用TIdIcmpClient来并行运行多个ping。它根本没有设计用于这种情况。它无法按照您需要的方式区分回复数据。因此,您必须序列化您的线程,以便只有一个线程可以一次调用TIdIcmpClient.Ping()。如果无法对 ping 进行序列化,您可以尝试将 TIdIcmpClient 的源代码复制到自己的代码中。创建 41 个线程 - 40 个设备线程和 1 个响应线程。创建一个所有线程共享的单个套接字,让每个设备线程使用该套接字准备并发送其各自的 ping 请求到网络。然后,让响应线程不断从同一套接字读取回复,并将它们路由回适当的设备线程以进行处理。这样稍微麻烦一些,但会给您想要的多 ping 并发性能。
如果您不想费那么大劲儿,另一种选择是使用已经支持同时 ping 多台机器的第三方应用程序,例如 FREEPing

谢谢您的回复。您用言语表达了我已经在想的事情。我尝试在多线程中执行的原因是因为我记得在某个地方读到过 Indy 10 是多线程的,并且 SequenceID 在这里用于标识正确的线程。但是您确认了我所期望的:这种方式不可能实现。另一点是,不幸的是我不能使用 FREEPing,因为我需要在程序内部知道哪台计算机正在运行。 - HpTerm
你知道Windows是否使用ICMP.dll,还是他们自己重写了一些定制内容?我的问题是:我在命令提示符窗口中执行的ping是多线程的还是有上下文的。如果我打开60个cmd窗口并在每个窗口中启动ping,每个窗口都会得到正确的回复吗?如果是的话,我可以使用Delphi以这种方式完成。 - HpTerm
2
Windows也存在我所描述的同样问题。如果您打开多个命令提示符窗口并运行Windows的“ping”实用程序,它也可能受到多个实例干扰彼此回复的影响。在您自己的代码中,如果您通过单个套接字发送ping请求,并有一个单独的线程从该套接字读取所有回复,则不会遇到重叠问题。 - Remy Lebeau

5
Remy解释了问题...我一直想在Indy中做这个,所以我把一个可能的解决方案发布到了一个新的Google Code项目中,而不是在这里留下一个很长的评论。这是一个初步的尝试,如果你有一些需要整合的变化,请告诉我: https://code.google.com/p/delphi-vault/ 这段代码有两种方式来Ping...像你的例子一样使用多线程客户端,或者使用一个简单的回调过程。编写于Indy10和Delphi的后续版本。
你的代码最终会使用TThreadedPing的子类,定义一个SynchronizedResponse方法:
  TMyPingThread = class(TThreadedPing)
  protected
    procedure SynchronizedResponse(const ReplyStatus:TReplyStatus); override;
  end;

为了启动一些客户端线程,代码将变成这样:

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;

线程响应在同步方法中被调用:
procedure TMyPingThread.SynchronizedResponse(const ReplyStatus:TReplyStatus);
begin
  frmThreadedPingSample.Memo1.Lines.Add(TPingClient.FormatStandardResponse(ReplyStatus));
end;

哇,你做了很多工作。非常好的答案。现在我正在修改我的代码以使用您的实现。也许有两个评论。第一个是添加/修改以在某处具有pingReply的布尔答案。就像在我的情况下,我只想知道计算机是否连接。另一件事是,您的代码可能会引起许多人的兴趣,但现在它仅适用于我的需求:defReceiveTimeout = 200;和defIPVersion = Id_IPv4;用户应该能够更改这些值以匹配他的特定需求(您尝试过ipv6吗?它是否有效)。 - HpTerm
+1,很棒的打击,为StackOverflow项目创建了一个共享空间!这给我带来了一个想法,可以创建我的个人项目,并始终分享整个项目,因为我通常(几乎总是)测试我发布的东西,所以现在我将把整个项目提交到个人存储中,并在答案中包含完整项目的链接。 - TLama
@Darian 我仔细阅读了你的代码。你能确认我理解得正确吗?你的代码的主要思路是发送多个ping线程,但在同一个过程(InternalHandelReply)中获取所有的回复,使用SequenceId来区分它们。这种做法(正如Remy所说)也可以处理每个回复而不会丢弃任何一个,因此对待它们都是平等的,我的理解正确吗?... - HpTerm
因为实际上结果与我的“无线程代码”并没有太大的区别。在我的情况下,当未连接时ping响应大约需要3秒钟。我期望的是在3秒钟后获得所有回复。在当前代码中,我必须等待每台计算机3秒钟,60台PC = 3分钟,这太长了...我正在寻找一种方法来一次获取40个回复并解析它们。@Remy的想法是拥有60个线程都回复同一个单一的replythread,这就是我正在尝试做的,这样我就可以使用sequenceID手动解析回复(你明白我的意思吗)。 - HpTerm
我将源代码移动到一个新的、更通用命名的Google Code项目中:https://code.google.com/p/delphi-vault/,并添加了几个源单元。 - Darian Miller
显示剩余2条评论

1

我没有尝试过你的代码,所以这都是假设,但我认为你搞混了线程并出现了经典的竞态条件。我重申建议使用AsyncCallsOmniThreadLibrary - 它们更简单,可以避免你自己犯错。

  1. 线程的目的是为了减少主线程的负载。线程构造函数应该尽量少地记住参数。我个人会将idICMP的创建移动到.Execute方法中。如果由于任何原因它需要创建内部同步对象,如窗口和消息队列或信号等,我希望它已经在新生成的线程中发生。

  2. .Execute中没有"inherited;"的意义。最好删除它。

  3. 消除所有异常是不好的风格。你可能有错误——但没有办法知道它们。你应该将它们传播到主线程并显示它们。OTL和AC可以帮助你完成这项工作,而对于tThread,你必须手动完成。如何处理在AsyncCalls函数中抛出的异常而不调用.Sync?

  4. 异常逻辑是有缺陷的。如果抛出异常,循环就没有意义——如果没有设置成功的Ping,那么为什么要等待响应?你的循环应该在与发出ping相同的try-except框架内。

  5. doOnPingReplyfIdIcmpClient.Free之后执行,但访问了fIdIcmpClient的内部。尝试将.Free更改为FreeAndNil吗? 这是使用释放后的指针的经典错误。 正确的方法是:
    5.1. 在doOnPingReply中释放对象
    5.2. 或在调用SynchronizeidICMP.Free之前,将所有相关数据从doOnPingReply复制到TThread的私有成员变量中(并且只在doOnPingReply中使用这些变量)
    5.3. 只在TMyThread.BeforeDestructionTMyThread.Destroy中执行fIdIcmpClient.Free。毕竟,如果你选择在构造函数中创建对象,那么你应该在匹配的语言结构——析构函数中释放它。

  6. 由于你不保留对线程对象的引用,所以While not Terminated循环似乎是多余的。只需创建通常的无限循环并调用break即可。

  7. 上述循环会占用CPU,就像自旋循环一样。请在循环内调用Sleep(0);Yield();,以给其他线程更好的机会来完成它们的工作。不要反对操作系统调度程序——你不处于速度关键路径上,没有理由在这里制造spinlock


总的来说,我认为:

  • 4和5对你来说是关键性的漏洞
  • 1和3可能会影响你,也可能不会。最好“保险起见”,而不是冒险尝试并调查它们是否有效。
  • 2和7 - 风格不佳,2涉及语言,7涉及平台
  • 6要么你有扩展应用程序的计划,要么你违反了YAGNI原则,我不知道。
  • 坚持使用复杂的TThread而不是OTL或AsyncCalls - 战略性错误。不要在跑道上放车,使用简单的工具。

有趣的是,这是一个FreeAndNil可能会暴露并使其明显的错误示例,而那些反对FreeAndNil的人则声称它“隐藏”了错误。

谢谢您的长篇回答,我会认真阅读。我马上就要离开了,周一会检查所有内容。 - HpTerm
你的答案分析了我的线程,并指出了其中的错误。其他人讨论过“ping”的另一个主要局限性,这是关键点。但是,你的评论对于我的知识非常重要,我感谢你。在实施@Dorian提供的代码之前,我首先根据你的评论纠正了自己的代码。在这里,我必须告诉你,.FreeAndNil不是idICMP的方法,唯一可用的是.Free (??) - HpTerm
1
它不是方法 - 方法无法更改变量的值。它是一个过程。FreeAndNil(obj); - Arioch 'The
哦,抱歉。有趣的是,我从1.0版本开始使用Delphi,甚至不知道那个FreeAndNil的东西;-) - HpTerm
它在D1中缺失,在D4或D5左右被引入。 - Arioch 'The

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 }
.
.
.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接