繁体   English   中英

Delphi tIdTCPClient具有计时器事件和其他多线程客户端事件

[英]Delphi tIdTCPClient with timer events and other multi-threaded client side events

我们有一个使用INDY的Delphi客户端服务器应用程序。 客户端具有到服务器的单个tIdTCPClient连接,该连接是多线程的。 客户端“理论上”是一个单一的线程。 但实际上客户端上有多个线程,这就是我的问题所在。 例如,想一下每分钟触发一次从服务器获取数据的计时器。 并考虑当用户在此计时器事件的同时运行命令时会发生什么。 事实上,我的问题是由我们的“报表生成器”报表工具引起的(令人讨厌)坚持加载报表的每一页,这需要一段时间。 该报告运行我们的“特殊”数据集,该数据集具有一次传输批量记录的缓存机制(因此多次调用服务器以获取所有数据)。 同时,如果用户同时做了其他事情,我们似乎正在获取交叉数据。 用户似乎正在获取用于报告的数据。

顺便说一句,这个bug是非常罕见的,但对于拥有世界上最慢的互联网的一个特定客户来说,这种情况很少见(祝我好运 - 我现在有一个测试环境)。

所以在客户端我的代码有点像这样......

procedure DoCommand(MyIdTCPClient:tIdTCPClient; var DATA:tMemoryStream);
var
  Buffer: TBytes;
  DataSize: Integer;
  CommsVerTest: String;
begin
  //Write Data
  MyIdTCPClient.IOHandler.Write(DATA.Size);
  MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));

  //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
  SetLength(Buffer,0); //Clear out buffer
  MyIdTCPClient.IOHandler.ReadBytes(Buffer,6); 
  CommsVerTest:=BytesToString(Buffer);
  if CommsVerTest<>'ABC123' then
    raise exception.create('Invalid Comms');      //It bugs out here in rare cases

  //Get Result Data Back from Server
  DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;   
  Data.SetSize(DataSize);                         //Report thread is stuck here
  MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
end; 

现在,当我调试它时,我可以在此过程中间有两个线程时确认它是错误的。 主线程在异常处停止。 并且报告线程在相同的过程中被卡在其他位置。

因此,在我看来,我需要使上面的过程安全。 我的意思是,如果用户想要做某事,他们只需要等到报告线程结束。

Arrrgh,我以为我的客户端应用程序是单线程的,用于向服务器发送数据!

我认为使用TThread是行不通的 - 因为我无法访问Report Builder中的线程。 我想我需要一个tCriticalSection。

我想我需要创建应用程序,以便上述过程一次只能由一个线程运行。 其他线程必须等待。

有人请帮助解决语法问题。

TIdIOHandler具有Write()Read...()重载,用于发送/接收TStream数据:

procedure Write(AStream: TStream; ASize: TIdStreamSize = 0; AWriteByteCount: Boolean = False); overload; virtual;

procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1; AReadUntilDisconnect: Boolean = False); virtual;

您无需在发送之前将TMemoryStream内容复制到中间TIdBytes ,或者在将其复制回TIdBytes之前将其作为TIdBytes接收。 事实上,您所展示的代码中没有任何内容需要直接使用TIdBytes

procedure DoCommand(MyIdTCPClient: TIdTCPClient; var DATA: TMemoryStream);
var
  CommsVerTest: String;
begin
  //Write Data
  MyIdTCPClient.IOHandler.Write(DATA, 0, True);

  //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
  CommsVerTest := MyIdTCPClient.IOHandler.ReadString(6); 
  if CommsVerTest <> 'ABC123' then
    raise exception.create('Invalid Comms');

  //Get Result Data Back from Server
  DATA.Clear;
  MyIdTCPClient.IOHandler.ReadStream(DATA, -1, False);
end; 

话虽如此,如果你有多个线程同时写入同一个套接字,或者多个线程同时从同一个套接字读取,它们将破坏彼此的数据(或者更糟)。 您需要同步对套接字的访问权限,例如至少对临界区进行访问。 由于您使用TIdTCPClient多线程,因此您需要重新考虑整体客户端设计。

至少,使用现有的逻辑,当您需要发送命令并读取响应时,停止计时器并等待任何待处理的数据交换,然后再发送命令,并且不允许任何其他任何东西访问套接字直到回复回来。 你试图一次做太多而没有同步一切,以避免重叠。

从长远来看,从单个专用线程执行所有读取然后将任何接收到的数据传递到其他线程以便根据需要进行处理会更安全。 但这也意味着改变你的发送逻辑以匹配。 你可以:

  1. 如果您的协议允许您并行运行多个命令,那么您可以随时从任何线程发送命令(只需确保使用关键部分以避免重叠),但不要立即等待响应。 让每个发送线程继续运行并执行其他操作,并让读取线程在预期响应实际到达时异步通知相应的发送线程。

  2. 如果协议不允许并行命令,但仍需要每个发送线程等待其各自的响应,那么为套接字线程提供一个线程安全的队列,其他线程可以在需要时将命令推入其中。 然后,套接字线程可以运行该队列,根据需要定期发送每个命令并一次接收一个响应。 将命令放入队列的每个线程可以包括在响应到达时要发信号通知的TEvent ,这样它们在等待时进入有效的睡眠状态,但是保留了每线程等待逻辑。

谢谢雷米。

TCriticalSection解决了这个问题。 我无法控制第三方报表生成器等内容。 完全在他们自己的线程中运行报告不会有太大的区别 - 他们仍然需要共享相同的连接(我不想要或不需要并行连接)。 无论如何,程序的大部分都在主线程中运行,并且两个线程很少需要同时与服务器通信。

所以TCriticalSection是完美的 - 它阻止了这个过程同时运行两次(即一个线程必须等到第一个完成)。 幸福的是 - 它的工作非常出色。

基本上代码现在看起来像这样:

procedure DoCommand(
    CS:tCriticalSection; 
    MyIdTCPClient:tIdTCPClient; 
    var DATA:tMemoryStream);
var
  Buffer: TBytes;
  DataSize: Integer;
  CommsVerTest: String;
begin
  CS.Enter;     //enter Critical Section
  try
    //Write Data
    MyIdTCPClient.IOHandler.Write(DATA.Size);
    MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));

    //Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
    SetLength(Buffer,0); //Clear out buffer
    MyIdTCPClient.IOHandler.ReadBytes(Buffer,6); 
    CommsVerTest:=BytesToString(Buffer);
    if CommsVerTest<>'ABC123' then
      raise exception.create('Invalid Comms');      

    //Get Result Data Back from Server
    DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;   
    Data.SetSize(DataSize);                         
    MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
  finally
    cs.Leave;
  end;
end;

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM