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.
source to share
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()
.
source to share
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.
source to share