2017-05-26 149 views
3

我是Delphi的新手,我正尝试做一些网络操作。在这种情况下,我想连接到(让我们称之为)通知服务器,该服务器在发生某些事件时将发送字符串。使用TIdTCPClient异步读取

我的第一个方法是这样的: 我在自己的线程上运行TIdTCPClient并设置ReadTimeout,所以我并不总是被阻塞。这样我可以检查线程的终止状态。

ConnectionToServer.ReadTimeout := MyTimeOut; 
while(Continue) do 
begin 
    // 
    try 
     Command := ConnectionToServer.ReadLn(); 
    except 
    on E: EIdReadTimeout do 
     begin 
       //AnotarMensaje(odDepurar, 'Timeout ' + E.Message); 
     end; 
    on E: EIdConnClosedGracefully do 
     begin 
       AnotarMensaje(odDepurar, 'Conexión cerrada ' + E.Message); 
       Continue := false; 
     end; 
    on E: Exception do 
     begin 
       AnotarMensaje(odDepurar, 'Error en lectura ' + E.Message); 
       Continue := false; 
     end; 
    end; 
    // treat the command 
    ExecuteRemoteCommand(Command);  
    if(self.Terminated) then 
    begin 
     Continue := false; 
    end; 
end; // while continue 

读ReadLn代码从来就看出it's在做重复一些积极等到循环,检查某种缓冲区大小,所有的时间。

有没有办法以TIdTCPServer与OnExecute等方法一起工作的方式异步执行此操作?或者,至少,有些方法可以避免主动等待。

回答

3

Indy使用阻塞套接字,客户端和服务器端。关于它没有任何异步。在TIdTCPServer的情况下,它将在单独的工作线程中运行每个客户端套接字,就像您在客户端尝试执行的一样。 TIdTCPClient 不是多线程的,所以你必须运行你自己的线程。

:如果您升级到印第安纳波利斯10,它有一个TIdCmdTCPClient客户端是多线程的,运行其自己的线程你,触发TIdCommandHandler.OnCommand事件从服务器接收的数据包。

ReadLn()运行一个循环,直到在InputBuffer中找到指定的ATerminator,或者直到发生超时。在找到ATerminator之前,ReadLn()会将更多数据从套接字读取到InputBuffer并再次扫描。缓冲区大小检查只是为了确保它不会重新扫描已经扫描的数据。

“唤醒”阻塞ReadLn()调用(或任何阻塞的套接字调用,就此而言)的唯一方法是从另一个线程关闭套接字。否则,你只需等待通话超时。请注意0​​在超时时不会引发异常。它设置ReadLnTimedout属性为True,然后返回一个空字符串,如:

ConnectionToServer.ReadTimeout := MyTimeOut; 

while not Terminated do 
begin 
    try 
    Command := ConnectionToServer.ReadLn; 
    except 
    on E: Exception do 
    begin 
     if E is EIdConnClosedGracefully then 
     AnotarMensaje(odDepurar, 'Conexión cerrada') 
     else 
     AnotarMensaje(odDepurar, 'Error en lectura: ' + E.Message); 
     Exit; 
    end; 
    end; 

    if ConnectionToServer.ReadLnTimedout then begin 
    //AnotarMensaje(odDepurar, 'Timeout'); 
    Continue; 
    end; 

    // treat the command 
    ExecuteRemoteCommand(Command);  
end; 

如果你不喜欢这种模式,你不必用印。一个更高效和响应的模型应该是直接使用WinSock。您可以使用WSARecv()的Overlapped I/O,并通过CreateEvent()TEvent创建一个等待事件以表示线程终止,然后您的线程可以使用WaitForMultipleObjects()同时等待套接字和终端,同时休眠时无需任何操作例如:

hSocket = socket(...); 
connect(hSocket, ...); 
hTermEvent := CreateEvent(nil, True, False, nil); 

... 

var 
    buffer: array[0..1023] of AnsiChar; 
    wb: WSABUF; 
    nRecv, nFlags: DWORD; 
    ov: WSAOVERLAPPED; 
    h: array[0..1] of THandle; 
    Command: string; 
    Data, Chunk: AnsiString; 
    I, J: Integer; 
begin 
    ZeroMemory(@ov, sizeof(ov)); 
    ov.hEvent := CreateEvent(nil, True, False, nil); 
    try 
    h[0] := ov.hEvent; 
    h[1] := hTermEvent; 

    try 
     while not Terminated do 
     begin 
     wb.len := sizeof(buffer); 
     wb.buf := buffer; 

     nFlags := 0; 

     if WSARecv(hSocket, @wb, 1, @nRecv, @nFlags, @ov, nil) = SOCKET_ERROR then 
     begin 
      if WSAGetLastError() <> WSA_IO_PENDING then 
      RaiseLastOSError; 
     end; 

     case WaitForMultipleObjects(2, PWOHandleArray(@h), False, INFINITE) of 
      WAIT_OBJECT_0: begin 
      if not WSAGetOverlappedResult(hSocket, @ov, @nRecv, True, @nFlags) then 
       RaiseLastOSError; 

      if nRecv = 0 then 
      begin 
       AnotarMensaje(odDepurar, 'Conexión cerrada'); 
       Exit; 
      end; 

      I := Length(Data); 
      SetLength(Data, I + nRecv); 
      Move(buffer, Data[I], nRecv); 

      I := Pos(Data, #10); 
      while I <> 0 do 
      begin 
       J := I; 
       if (J > 1) and (Data[J-1] = #13) then 
       Dec(J); 

       Command := Copy(Data, 1, J-1); 
       Delete(Data, 1, I); 

       ExecuteRemoteCommand(Command); 
      end; 
      end; 

      WAIT_OBJECT_0+1: begin 
      Exit; 
      end; 

      WAIT_FAILED: begin 
      RaiseLastOSError; 
      end; 
     end; 
     end; 
    except 
     on E: Exception do 
     begin 
     AnotarMensaje(odDepurar, 'Error en lectura ' + E.Message); 
     end; 
    end; 
    finally 
    CloseHandle(ov.hEvent); 
    end; 
end; 

如果使用的Delphi XE2或更高版本,TThread具有虚拟TerminatedSet()方法可以重写到信号hTermEventTThread.Terminate()被调用。否则,致电Terminate()后致电SetEvent()

+0

感谢您的分解。我不担心ReadLn的阻塞性质,我担心检查缓冲区大小的繁忙等待循环。 我坚持使用老版本的Indy和Delphi 7(遗留代码),所以没有太大的改变空间。 –

+1

@HéctorC。然后考虑彻底摆脱读取超时,让'ReadLn'在没有数据要读取时阻塞线程,然后在准备好终止线程时断开客户端连接。 –

3

您可以在单独的线程中执行此操作。

TIdTCPServer在后台使用线程来支持侦听并与多个客户端通信。

由于TIdTCPClient连接到一台服务器,我认为它没有内置此功能,但您可以在独立的线程中自行创建和使用TIdTCPClient,因此对您而言,您的解决方案很好。我会以同样的方式解决它。

如果您使超时时间很短,那么这个问题应该不会成为问题在那段时间内,套接字仍处于打开状态,因此您不会错过任何数据。您可以将超时设置为10ms等小数值。这样,你的线程很长时间不会徘徊,但是超时足够长,不会导致退出和重新进入readln的显着开销。

+0

这就是我所做的。但是我必须定期从ReadLn操作中退出以检查Terminated线程属性。 另外,当我在别处终止这个线程时,我必须等到ReadLn超时退出线程。 –

+1

是的。如果你让超时时间很短,这是一个问题吗?在此期间套接字仍处于打开状态,因此您可以将超时设置为10ms等较小值。这样,你的线程很长时间不会徘徊,但是超时足够长,不会导致退出和重新进入readln的显着开销。 – GolezTrol

+1

顺便说一句,我很好奇,并搜索了一下。我发现了类似的问题[Indy 10 IdTCPClient使用单独的线程读取数据](https://stackoverflow.com/questions/554142/indy-10-idtcpclient-reading-data-using-a-separate-thread)和[Indy TIdTCPClient阅读数据](https://stackoverflow.com/questions/17372366/delphi-indy-tidtcpclient-reading-data),这两者都显示了更详细的例子,说明如何实现这个线程,并克服与这个事件同步的问题主线程。 – GolezTrol