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


我有一个有60台计算机/设备的房间(40台计算机和20台基于Windows CE的示波器),我想知道哪个和每个人都在使用ping。首先我写了一个标准ping(见这里 Delphi Indy Ping错误10040),现在工作正常,但大多数计算机离线时需要很长时间。

所以我想要做的就是写一个MultiThread 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)在代码中时。每次我点击按钮来检查连接结果是否正确。

没有睡眠(10),它正在“大部分”工作,但有时结果是错误的,在离线计算机上给我一个ping回声,在线计算机上没有ping回应,因为ping回复没有分配给正确线。

欢迎任何评论或帮助。

-----编辑/重要-----

作为这个问题的一般跟进,@ Darian Miller开始了 Google Code项目在这里  https://code.google.com/p/delphi-stackoverflow/ 这是一个工作的基础。我将他的答案标记为“已接受的答案”,但是用户应该参考这个开源项目(所有信用都属于他),因为它将来肯定会得到扩展和更新。


4253
2017-10-12 11:56


起源

我重复我的建议 - 放弃股票TThread并使用AsyncCalls或OmniThreads。你的线程实现对我来说似乎大多是单线程的! - Arioch 'The
'继承创造(假);' - 在ctor完成其字段的初始化之前,是否可以运行该线程? - Martin James
..并且,ICMP ping没有套接字上下文等。我不相信发送ping的线程会收到一个gnip,所以findex将不匹配:'ReplyStatus.SequenceId = findex'通常是false。 '就好像ping回复没有分配给正确的帖子' - 是的。 - Martin James
@Martin我不知道idICMP的实现是什么 使用Windows API的示例 建议最明显的实现不会使用任何关于线程和消息队列但只使用回调。坦率地说,我认为如果topicstarter在这里放弃了Indy并且直接使用了API - 他可以轻松地通过单线程(或者至少可能是双线程)应用程序实现并行ping,这样可以更轻松,更简单。我不知道通过哪种方法以及Windows将调用该回调的线程/上下文... - Arioch 'The
+1 @Martin。非常有趣的一点。 - HpTerm


答案:


Remy解释了这些问题......我想在Indy做一段时间这样做,所以我发布了一个可能的解决方案,我刚刚把它放在一个新的Google Code项目中,而不是在这里做长评。这是第一次尝试,如果你有一些变化需要我知道: https://code.google.com/p/delphi-vault/

此代码有两种方法可以Ping:多线程客户端,如示例中所示,或者使用简单的回调过程。为Indy10及更高版本的Delphi编写。

您的代码最终将使用定义SynchronizedResponse方法的TThreadedPing后代:

  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;

4
2017-10-13 05:46



哇,你做了很多工作。很好的答案。现在我正在修改我的代码以使用您的实现。也许2条评论。第一个是添加/修改以获得pingReply的布尔答案。在我的情况下,我只是想知道是/否是连接的计算机。另一件事是你的代码可以引起很多人的兴趣,现在它特定于我的需求:defReceiveTimeout = 200;和defIPVersion = Id_IPv4;用户应该能够更改这些值以匹配他的特定需求(你尝试过ipv6吗?它是否正常工作)。 - HpTerm
+1,很好的命中为StackOverflow项目创建共享位置!这让我有了一个想法,让我个人的,并总是分享整个项目,因为我通常(几乎总是)测试我发布的东西,所以现在我只是将完整的项目提交到该个人存储,并包括一个链接到整个项目的答案。 - TLama
@Darian我仔细阅读了你的代码。你能否证实我的理解是正确的。您的代码中的主要思想是发送多个ping线程,但在相同的过程(InternalHandelReply)中获取所有回复,使用SequenceId知道哪个是哪个。这样做(也像雷米所说的那样)也是为了对待每一个回复而不丢弃任何回复,因此对待它们?我对么 ? ... - HpTerm
...因为实际上结果与我的“无线程代码”没有多大区别。在我的情况下,未连接时的ping响应大约是3秒。我期待的是在3秒后获得所有回复。在当前的代码中,我必须等待每台计算机3秒,60 PC = 3分钟这太长了..我正在寻找一种方法来同时获得40个回复并解析它们。 @Remy有60个线程全部回复同一个回复线程的想法是我想要做的,所以我可以用sequenceID手动解析回复(你明白我的意思) - HpTerm
请注意,我更正了数据。我有40台计算机和20台示波器(基于Windows CE),其中60台设备不是40,正如我先说的那样。 - HpTerm


根本问题是ping是无连接流量。如果你有多个 TIdIcmpClient 同时ping网络的对象,一个 TIdIcmpClient 实例可以收到实际属于另一个的回复 TIdIcmpClient 实例。您正在尝试通过检查来解决线程循环中的问题 SequenceId 价值观,但你没有考虑到这一点 TIdIcmpClient 已经在内部做同样的检查。它在循环中读取网络回复,直到它收到预期的回复,或直到 ReceiveTimeout发生。如果它收到回复它没有预期,它只是丢弃该回复。所以,如果一个 TIdIcmpClient 实例丢弃另一个回复 TIdIcmpClient 实例期待,您的代码和其他代码将不会处理该回复 TIdIcmpClient 可能会收到另一个 TIdIcmpClient而是回复,等等。通过添加 Sleep(),你正在减少(但不是消除)ping相互重叠的可能性。

对于您尝试做的事情,您将无法使用 TIdIcmpClient 对不起,有多个ping并行运行。它根本就不是为此而设计的。它无法以您需要的方式区分回复数据。您将必须序列化您的线程,因此只有一个线程可以调用 TIdIcmpClient.Ping() 一次。

如果序列化ping不是您的选项,您可以尝试复制部分 TIdIcmpClient将源代码放入您自己的代码中。运行41个线程 - 40个设备线程和1个响应线程。创建一个所有线程共享的单个套接字。让每个设备线程使用该套接字准备并将其各自的ping请求发送到网络。然后让响应线程连续读取来自同一套接字的回复,并将它们路由回适当的设备线程进行处理。这是一个更多的工作,但它将为您提供您正在寻找的多平行并行性。

如果您不想解决所有问题,另一种方法是使用已支持同时ping多台计算机的第三方应用程序,例如 FREEPing


11
2017-10-13 00:16



谢谢回复。你说出我已经在想的话。为什么我尝试在多线程中执行它是因为我记得在某处读取了indy 10是多线程的,而SequenceID在这里是为了识别正确的线程。但你确认我的期待:这是不可能的。另一点是,不幸的是我不能使用FREEPing,因为我需要知道我的程序“内部”哪台计算机已启动。 - HpTerm
你知道Windows是否使用ICMP.dll,或者他们是否重新编写了自定义内容。我的问题如下:我在命令提示符窗口多线程中执行的ping是否具有上下文。如果我打开60 cmd的窗口并在每个窗口中启动ping,每个窗口都会得到正确的答复吗?如果是的话,我可以使用delphi这样做 - HpTerm
Windows遇到了我描述的同样问题。如果您打开多个命令提示符窗口并运行Windows的“ping”实用程序,它也可能会受到多个实例干扰彼此的回复。在您自己的代码中,如果您通过单个套接字发送ping请求,并且有一个线程读取该套接字的所有回复,则不会遇到重叠问题。 - Remy Lebeau


我没有尝试你的代码,所以这都是假设,但我认为你搞砸了线程并获得了经典 race condition。我重申我的建议使用 AsyncCalls 要么 OmniThreadLibrary  - 它们更简单,并且可以节省你几次“射击自己的脚”的尝试。

  1. 线程用于最小化主线程负载。线程构造函数应该记住参数的最小工作。就个人而言,我已经将idICMP的创作转移到了 .Execute 方法。如果由于任何原因它想要创建其内部同步对象,如窗口和消息队列或信号或其他什么,我希望它已经发生在一个新的衍生线程中。

  2. 对“继承”没有任何意义;在.Execute。更好地删除它。

  3. 消除所有异常是不好的风格。您可能有错误 - 但无法了解它们。您应该将它们传播到主线程并显示它们。 OTL和AC可以为您提供帮助,而对于tThread,您必须手动完成。 如何在不调用.Sync的情况下处理AsyncCalls函数中抛出的异常?

  4. 异常逻辑存在缺陷。抛出异常没有意义 - 如果没有设置成功的Ping - 那么为什么要等待响应呢?你循环应该在发出ping的同一个try-except帧内。

  5. 你的 doOnPingReply 执行AFTER fIdIcmpClient.Free 但访问 fIdIcmpClient的内部。尝试改变.Free for FreeAndNil? 这是在释放它之后使用死指针的经典错误。 正确的方法是:
    5.1。要么释放对象 doOnPingReply
    5.2。或复制所有相关数据 doOnPingReply 在调用两者之前,先访问TThread的私人会员 Synchronize 和 idICMP.Free (并且只使用那些vars doOnPingReply ) 5.3。只做 fIdIcmpClient.Free 内 TMyThread.BeforeDestruction 要么 TMyThread.Destroy。毕竟,如果您选择在构造函数中创建对象 - 那么您应该在匹配的语言构造中释放它 - 析构函数。

  6. 因为你没有保留对线程对象的引用 - 那 While not Terminated 循环似乎多余。只需通常永远循环和呼叫休息。

  7. 前面提到的循环是CPU饥饿的,它就像自旋循环。请打电话 Sleep(0); 要么 Yield(); 内部循环让其他线程更有机会完成他们的工作。不要在这里使用agaisnt OS调度程序 - 你不是一个速度关键的路径,没有理由 spinlock 这里。


总的来说,我认为:

  • 4和5是你的关键错误
  • 1和3作为潜在的问题可能会影响或者可能不会影响。你最好'安全'而不是做冒险的事情,并调查他们是否会工作。
  • 2和7--糟糕的风格,2关于语言和7关于平台
  • 6要么你有计划扩展你的应用程序,要么你打破了YAGNI原则,不知道。
  • 坚持使用复杂的TThread而不是OTL或AsyncCalls - 战略错误。你不是把车放在跑道上,使用简单的工具。

有趣,这是bug的例子 FreeAndNil 可能暴露并显而易见,而FreeAndNil-haters声称它“隐藏”了错误。


1
2017-10-12 12:41



感谢您的回答,我会仔细阅读,我必须尽快离开,并将在周一检查所有这些。 - HpTerm
打印到纸上并在空闲时读取:-) - Arioch 'The
您的答案分析了我的主题以及您在其中看到的错误。其他人讨论过“ping”的其他主要限制,这是关键点。但是你的评论对我的知识非常重要,我感谢你。在实现@Dorian给出的代码之前,我首先使用您的评论更正了我的代码。在这里我必须告诉你.FreeAndNil不是idICMP的方法。唯一可用的是.Free(??) - HpTerm
它不是方法 - 方法不能改变变量的值。这是一个程序。 FreeAndNil(obj); - Arioch 'The
哦对不起。有趣的是,从版本1.0开始使用delphi,我现在甚至没有关于FreeAndNil的东西;-) - HpTerm


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

0
2017-07-11 02:35