Reputation: 507
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.
Upvotes: 4
Views: 3984
Reputation: 598299
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()
.
Upvotes: 5
Reputation: 116190
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.
Upvotes: 4