Indy TCPClient的OnDisconnect事件无法正常工作

3
type
  TForm8 = class(TForm)
    idtcpclnt1: TIdTCPClient;
    idtcpsrvr1: TIdTCPServer;
    procedure FormCreate(Sender: TObject);
    procedure idtcpsrvr1Execute(AContext: TIdContext);
    procedure idtcpclnt1Disconnected(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form8: TForm8;

implementation

{$R *.dfm}

procedure TForm8.FormCreate(Sender: TObject);
begin
  idtcpclnt1.Connect;
end;

procedure TForm8.idtcpsrvr1Execute(AContext: TIdContext);
begin
  AContext.Connection.Disconnect(true); //this gets called
end;

procedure TForm8.idtcpclnt1Disconnected(Sender: TObject);
begin
  ShowMessage('true'); //but this does not
end;

OnDC事件为何不会被处理?
3个回答

12

Indy客户端组件不是事件驱动的(有少数例外,比如TIdTelnet)。当服务器在其端断开连接时,TIdTCPClient.OnDisconnect事件不会被触发,就像你所假设的那样。这是出于设计考虑。直到它再次尝试访问套接字时,TIdTCPClient才会知道断开连接的情况,并引发异常,例如EIdConnClosedGracefully异常。只有当客户端调用TIdTCPClient.Disconnect()方法时,TIdTCPClient.OnDisconnect事件才会被触发,而你没有这么做。

为了检测使用TIdTCPClient的服务器端断开连接,必须定期从套接字中读取数据,例如通过计时器或单独的线程。


这真的相当不令人满意。如果有一个OnDisconnect事件,那么人们期望它在所有情况下都会在断开连接时立即引发(而不仅仅是在调用disconnect时,在这种情况下,事件并没有向应用程序提供任何信息)。必须使用计时器通过执行虚拟读取来轮询连接关闭,这真的很丑陋,并导致其他不良副作用。似乎总是需要大量额外的工作(即代码)才能使Indy正常工作。这样的库难道不应该为我们处理这些事情吗? - Peter Ball
@PeterBall “这真的非常不令人满意。” - 不管你喜欢与否,这就是Indy的设计方式(请阅读“Indy简介”页面)。事件用于异步处理,但Indy客户端主要是用于同步使用的。 - Remy Lebeau

1
你可以通过在客户端添加定时器例程来让客户端请求断开连接 - 这是最简单的方法。
procedure TForm1.Timer1Timer(Sender: TObject);
   begin                    
      idTCPClient1.Connected;  // Works in Indy for Delphi XE4
   // Be aware this is a property read with side effects
   // It shouldn't get optimized out, but if it does, 
   // then add the appropriate directives to prevent that.
   end;                    

这将使代码的行为与旧的TClientSocket(以及TidTelnet)相同。如果服务器突然消失(即触发计时器例程检测到此情况),它会向OnStatus事件产生hsDisconnected标志。但是,由于服务器丢失造成的此特殊情况不会触发OnDisconnect事件,只会触发OnStatus事件。因此,最好始终使用OnStatus来捕获所有断开连接,无论是客户端还是服务器引起的。我使用了100ms的定时器,但我想你可以将其设置得更频繁或缓慢 - 它似乎不会造成任何伤害。
注意:对于DELPHI 7(可能也适用于D7和XE4之间的其他版本),您需要稍微改变一下做法。
procedure TForm1.Timer1Timer(Sender: TObject);
   begin                    
   // This no longer works this way in Indy for XE4, but works in Indy for D7 ... 
      idTCPClient1.CheckForGracefulDisconnect(FALSE);  
   end; 

顺便说一下 - 如果你使用的是Delphi 6,就忘记Indy吧,因为它在那个时候太过错误了。


1
如果您使用的是 Delphi 6 或 5,不要忘记 Indy - 您可以使用当前版本的 Indy(10.6)而不是这些旧版 Delphi 中捆绑的原始 Indy。 - mjn
谢谢指出。我确实在某个地方读到过这个问题,并一直在思考是更好地保留早期的Delphi版本和其原始的Indy安装,还是将它们全部升级。显然,全部升级会存在破坏其他相关内容的风险,当然你也会失去尝试使用不兼容Indy安装的旧代码的能力。API从来都不是一个稳定的东西! - Alex T
顺便提一下,自从我写了这篇文章以来,我发现在计时器中进行实际数据轮询具有相同的效果。例如:CheckForDataOnSource(100{ms}); 如果输入缓冲区为空,则退出;这将使iDTCPClient1.Connected变得不必要。 - Alex T

1

好的,我的错误。你的代码不起作用,但这是正确的。让我解释一下为什么:

AContext.Connection.Disconnect(true) 方法调用了 DisconnectNotifyPeer,而在 TCPServer 中没有实现。为什么?因为它不应该。

当您在服务器上断开连接时,indy 所做的就是使套接字无效并关闭它。客户端只有在尝试发送某些请求时才会注意到服务器已断开连接。而你的代码没有做到这一点。这是 indy 的默认行为。

为了通知客户端服务器已断开连接,indy 和其他套件都应该实现我们所谓的“心跳”。心跳是一种技术,定期尝试向套接字发送小数据包,以便检测它是否仍然存活。检测套接字断开连接的唯一方法是尝试在该套接字中写入某些内容。搜索“心跳”并了解一下即可明白我的意思。

编辑

查看this


当前版本是随 Delphi XE 一起发布的版本。你能告诉我你的版本号吗? - netboy
问题:服务器不是由我设计的,因此我无法向其发送任何数据包,否则它将中断当前连接。 - netboy
@netboy,我不明白你说的话。你不用担心服务器断开连接的问题,因为indy会为你处理这个问题。你有收到任何错误吗?具体是什么情况发生了? - Rafael Colucci
1
请看这里。-> 指定的消息 [245209] 未被找到。 - mjn
@RafaelColucci 不需要发送小的心跳数据包。如果远程端关闭了连接,那么适当的数据包已经被交换,并且本地操作系统知道连接已经关闭。不幸的是,Indy没有将此转发给应用程序,除非应用程序费尽心思地探测连接。 - Peter Ball

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