Asynchronous Read Using TIdTCPClient

I am new to Delphi and I am trying to do some network operations. In this case, I want to connect to a notification server (let it call it) that will send strings whenever an event occurs.

My first approach is this: I run the TIdTCPClient on my thread and set the ReadTimeout so I am not always blocked. This way I can check the status of the completed 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 code ReadLn I saw that it does some waiting in iteration until the loop checks the buffer size all the time.

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

+3


source to share


2 answers


Indy uses blocking sockets, both client and server. There is nothing asynchronous about this. In case, TIdTCPServer

it starts each client socket on a separate worker thread, just like you are trying to do in your client. TIdTCPClient

1 is not multi-threaded, so you need to start your own thread.

1 : if you upgrade to Indy 10, it has a client TIdCmdTCPClient

that is multithreaded, starts its own thread for you, triggering events TIdCommandHandler.OnCommand

for packets received from the server.

ReadLn()

starts a loop until the specified ATerminator

is found in InputBuffer

, or until a timeout occurs. Until found ATerminator

, ReadLn()

reads more data from the socket InputBuffer

and checks it again. Checking the size of the buffer is to make sure it doesn't rescan the already checked data.

The only way to "wake up" a blocking call ReadLn()

(or any call to a blocking socket, for that matter) is to close the socket from another thread. Otherwise, you just have to wait for the call to start normally.

Also note that it ReadLn()

doesn't throw an exception EIdReadTimeout

when it expires. It sets the property ReadLnTimedout

to True and then returns an empty string, for example:



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 need to use Indy. A more efficient and responsive model would be to use WinSock directly. You can use Overlapped I / O with WSARecv()

and create a pending event via CreateEvent()

or TEvent

to signal the end of the thread, and then your thread can use WaitForMultipleObjects()

to wait for both socket and completion at the same time when you sleep when there is nothing to do, e.g .:

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 newer, TThread

has a virtual TerminatedSet()

method, you can override the signal hTermEvent

when it is called TThread.Terminate()

. Otherwise, just call SetEvent()

after the call Terminate()

.

+3


source


You can do this in a separate thread.

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



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

This shouldn't be a problem if you keep the timeout pretty small. The socket is still open during this period, so you won't miss any data. You can set the timeout to a small value such as 10ms. This way your stream won't linger for long, but the timeout is long enough not to impose significant readln logout and re-input overhead.

+3


source







All Articles