简体   繁体   中英

Asynchronous read using TIdTCPClient

I´m new to Delphi and I´m trying to do some networks operations. In this case I want to connect to a (let´s call it) a notification server that will send strings whenever some event occurs.

My first approach is this one: I run the TIdTCPClient on its own thread and set a ReadTimeout so I´m not always blocked. This way I can check the Terminated status of the thread.

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

Reading the ReadLn code I´ve seen that it´s doing some active wait in a repeat until loop that checks some buffer size all the time.

Is there a way to do this asynchronously in the way that TIdTCPServer works with the OnExecute, etc methods? Or, at least, some way to avoid that active wait.

You can do this in a separate thread.

TIdTCPServer uses threads in the background to support listening for and communicating with multiple clients.

Since a TIdTCPClient connects to one server, I think it doesn't have this feature built in, but you can create and use a TIdTCPClient in a separate thread yourself, so to me your solution is fine. I'd solve it in the same way.

It shouldn't be a problem if you make the timeout quite small The socket is still open in that period so you won't miss data. You could set the timeout to a small value like 10ms. That way, your thread won't be lingering for a long time, but the timeout is long enough not to cause significant overhead of exiting and re-entering the readln.

Indy uses blocking sockets, both client and server side. There is nothing asynchronous about it. In the case of TIdTCPServer , it runs each client socket in a separate worker thread, just like you are trying to do in your client. TIdTCPClient 1 is not multi-threaded, so you have to run your own thread.

1 : If you upgrade to Indy 10, it has a TIdCmdTCPClient client that is multi-threaded, running its own thread for you, triggering TIdCommandHandler.OnCommand events for packets received from the server.

ReadLn() runs a loop until the specified ATerminator is found in the InputBuffer , or until a timeout occurs. Until the ATerminator is found, ReadLn() reads more data from the socket into the InputBuffer and scans it again. The buffer size checking is just to make sure it doesn't re-scan data it has already scanned.

The only way to "wake up" a blocking ReadLn() call (or any blocking socket call, for that matter) is to close the socket from the another thread. Otherwise, you just have to wait for the call to timeout normally.

Also note that ReadLn() does not raise an EIdReadTimeout exception when it times out. It sets the ReadLnTimedout property to True and then returns a blank string, eg:

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;

If you don't like this model, you don't have to use Indy. A more efficient and responsive model would be to use WinSock directly instead. You can use Overlapped I/O with WSARecv() , and create a waitable event via CreateEvent() or TEvent to signal thread termination, and then your thread can use WaitForMultipleObjects() to wait on both socket and termination at the same time while sleeping when there is nothing to do, eg:

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;

If you are using Delphi XE2 or later, TThread has a virtual TerminatedSet() method you can override to signal hTermEvent when TThread.Terminate() is called. Otherwise, just call SetEvent() after calling Terminate() .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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