使用 TIdTCPClient 异步读取

Asynchronous read using TIdTCPClient

我是 Delphi 的新手,我正在尝试进行一些网络操作。在这种情况下,我想连接到一个(我们称之为)通知服务器,只要发生某些事件,它就会发送字符串。

我的第一种方法是: 我 运行 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 代码,我发现它在重复执行一些主动等待,直到一直检查缓冲区大小的循环。

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

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

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

由于 TIdTCPClient 连接到一个服务器,我认为它没有内置此功能,但您可以自己在单独的线程中创建和使用 TIdTCPClient,所以对我来说您的解决方案很好。我会用同样的方法解决它。

如果您将超时设置得非常小,应该没有问题。套接字在那段时间仍然打开,因此您不会丢失数据。您可以将超时设置为较小的值,例如 10 毫秒。这样,您的线程就不会长时间徘徊,但超时时间足够长,不会导致退出和重新进入 readln 的大量开销。

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

1:如果您升级到 Indy 10,它有一个 TIdCmdTCPClient 多线程客户端,运行 为您提供自己的线程, 为从服务器收到的数据包触发 TIdCommandHandler.OnCommand 个事件。

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

"wake up" 阻塞 ReadLn() 调用(或任何阻塞套接字调用,就此而言)的唯一方法是从另一个线程关闭套接字。否则只能等待调用正常超时。

另请注意,ReadLn() 在超时时不会引发 EIdReadTimeout 异常。它将 ReadLnTimedout 属性 设置为 True 然后 returns 一个空字符串,例如:

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;

如果你不喜欢这个模型,你可以不用 Indy。一种更有效和响应更快的模型是直接使用 WinSock。您可以使用 Overlapped I/O 和 WSARecv(), and create a waitable event via CreateEvent() or TEvent to signal thread termination, and then your thread can use 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() 方法,您可以在调用 TThread.Terminate() 时覆盖以向信号 hTermEvent 发出信号.否则,在调用 Terminate().

之后调用 SetEvent()