Jump to content
Yaron

Stable remote control using Indy TCP/IP

Recommended Posts

Using Delphi 10.3.3:

My goal is to have a stable client/server remote control over TCP/IP that can automatically recover in cases where the network disconnects occasionally.

 

I use a structure Remi suggested of creating a reading thread that tries to readln with a 1sec time out:

    FClient.OnDisconnected := OnDisconnect;
    FClient.IOHandler.ReadTimeout := 1000; // lets us exit the read operation after 1sec of inactivity

    While Terminated = False do
    Begin
      If FClient.Connected = True then
      Try
        FData := FClient.IOHandler.ReadLn(IndyTextEncoding_UTF8);
      Except
        // catch exception & update UI
        Try
          If FClient.Connected = True then FClient.Disconnect;
        except
          on E: Exception do
          Begin
            // Catch disconnect exception
          End;
        end;
      End;
    End;  

I have a client PC connected to a router via cable and the server PC connected to the same router via WiFi.

 

After connecting the client to the server, I disconnect the server's wifi access and absolutely nothing happens (I waited over a minute).

The OnDisconnected event isn't triggered, calling FClient.IOHandler.ReadLn doesn't return any exception and FClient.Connected remains True.

 

If I then try to call FClient.IOHandler.WriteLn, within about 10 seconds a "Socket Error # 10054 - Connection reset by peer" exception is raised on the ReadLn code about (not on the WriteLn function).

In that exception, I call "If FClient.Connected = True then FClient.Disconnect" which raises another "Socket Error # 10054 - Connection reset by peer" exception.

Finally, my code loops to the beginning and then the same exception is triggered on the initial "If FClient.Connected = True" line.

 

Obviously, I'm not handling things correctly, I wasn't expecting "If FClient.Connected" to raise exceptions and I was hoping the OnDisconnect event to trigger, but are there other pitfalls I might be missing?

Edited by Yaron

Share this post


Link to post
3 hours ago, Yaron said:

I use a structure Remi suggested of creating a reading thread that tries to readln with a 1sec time out:

That structure is incomplete for what I would have suggested for this kind of situation.  See further below.

Quote

The OnDisconnected event isn't triggered

The OnDisconnect event is not called until the Disconnect() method is called.  You really shouldn't be using the OnDisconnect event in this situation, though.

Quote

calling FClient.IOHandler.ReadLn doesn't return any exception

The TIdIOHandler.ReadLn() method does not raise an exception on timeout (see this and this).  Instead, it sets the TIdIOHandler.ReadLnTimedOut property to True and returns a blank string, which you are not checking for. 

Quote

FClient.Connected remains True.

The Connected() method will return True if there is any unread data in the TIdIOHandler.InputBuffer that can continue to satisfy read operations without having to go back to the underlying socket to get more data.  Once the InputBuffer is exhausted, only then does Connected() check the socket, and if the socket has been *gracfully* closed, or there is a reading error, THEN it returns False.

Quote

If I then try to call FClient.IOHandler.WriteLn, within about 10 seconds a "Socket Error # 10054 - Connection reset by peer" exception is raised on the ReadLn code about (not on the WriteLn function).

Simply shutting off the WiFi does not close the socket connection *gracefully*, so it may take time for the OS to detect the TCP connection is dead and invalidate the socket so read/write operations can start reporting errors.  If you find that the OS takes too long to report dead connections, you can enable TCP-level keep-alives on the underlying socket.  Or simply use your own timeouts in your code and bail out manually if your timeouts elapse.

Quote

In that exception, I call "If FClient.Connected = True then FClient.Disconnect" which raises another "Socket Error # 10054 - Connection reset by peer" exception.

Connected() should not be raising an exception.  Although Connected() may perform a read operation, it should be ignoring any socket errors on that read.  If that is not the case, then please file a bug report.  Which version of Indy are you using exactly?

 

Disconnect() calls Connected() internally if its optional ANotifyPeer parameter is True.  When a socket read/write exception occurs, you don't really know the state of the communication anymore, so the only viable thing to do is to just close the connection immediately and not try to exchange any more data.  Try calling Disconnect() with its ANotifyPeer parameter set to False (it is True by default).

Quote

Finally, my code loops to the beginning and then the same exception is triggered on the initial "If FClient.Connected = True" line.

Because you weren't able to fully disconnect.

Quote

Obviously, I'm not handling things correctly, I wasn't expecting "If FClient.Connected" to raise exceptions and I was hoping the OnDisconnect event to trigger, but are there other pitfalls I might be missing?

You really shouldn't be using Connected() at all.  Try something more like this instead:

FClient.ConnectTimeout := 5000;
FClient.ReadTimeout := 1000;

while not Terminated do
begin
  try
    FClient.Connect;
  except
    // handle connect error as needed...
    for I := 1 to 5 do
    begin
      if Terminated then Break;
      Sleep(1000);
    end;
    continue;
  end;
  try
    FClient.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
    while not Terminated do
    begin
      FData := FClient.IOHandler.ReadLn;
      if (FData = '') and FClient.IOHandler.ReadLnTimedOut then
      begin
        // handle timeout as needed...
      end else begin
        // handle data as needed...
      end;
    end;
    FClient.Disconnect;
  except
    FClient.Disconnect(False);
    // handle exception as needed...
  end;
end;

 

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
10 hours ago, Lars Fosdal said:

What about EIdConnClosedGracefully? That is a kind of an annoying one.

Why? Simply add it to the list of exceptions the debugger should ignore and you will never see it again.

Share this post


Link to post
3 hours ago, Remy Lebeau said:

What about it?

It's the most annoying exception, ever. It's a permanent "ignore" in my exception tracking code. It'd be great if it was just removed, permanently.

Share this post


Link to post
37 minutes ago, Dave Nottage said:

It's the most annoying exception, ever. It's a permanent "ignore" in my exception tracking code. It'd be great if it was just removed, permanently.

I disagree that it should be removed.  It is a legitimate runtime error condition.  Data was attempted to be read/sent over a socket after the socket was closed.  If you choose to have your debugger ignore that exception, that is entirely up to you.  Doesn't mean the exception itself doesn't have meaning or value.

Share this post


Link to post
5 hours ago, Remy Lebeau said:

Data was attempted to be read/sent over a socket after the socket was closed

OK, for some reason I was under the impression that it was being raised when the socket was closed without an attempted read/write. I'll have to revisit that, thanks 🙂

 

Share this post


Link to post
49 minutes ago, Dave Nottage said:

OK, for some reason I was under the impression that it was being raised when the socket was closed without an attempted read/write.

Nope.  Now THAT would be a stupid thing for it to do :classic_biggrin:  If you close the socket yourself, it should just close, no mess, no fuss.  Any read/write operation performed on the socket AFTER you or the peer has closed the connection will fail with EIdConnClosedGracefully.

Share this post


Link to post

The reason is that I seem to get it without having had a disconnect event.  If I have had observed the disconnect, I obviously would not try to send data on a closed socket.

Share this post


Link to post
3 minutes ago, Lars Fosdal said:

The reason is that I seem to get it without having had a disconnect event.  If I have had observed the disconnect, I obviously would not try to send data on a closed socket.

How can you observe the disconnect on blocking sockets unless you try to read/write?

Share this post


Link to post

I've had the socket open doing writes and reads.  I still keep the connection open as I intend to send more shortly.  If the remote point then disconnects gracefully - should I not be notified?

Share this post


Link to post
3 hours ago, Lars Fosdal said:

I've had the socket open doing writes and reads.  I still keep the connection open as I intend to send more shortly.  If the remote point then disconnects gracefully - should I not be notified?

How do you imagine that notification with blocking sockets where you usually do a loop until recv or send returns -1? Even select won't tell whether connection has been reset.

Share this post


Link to post

When you put it that way, I wonder why I had the notion that a remote TCP host's willful disconnect actually signaled the connected.

Share this post


Link to post
On 1/22/2020 at 11:56 PM, Lars Fosdal said:

The reason is that I seem to get it without having had a disconnect event.

Client side or server side?  On the client side, OnDisconnect is only fired when Disconnect() is called, it is not an asynchronous event. Indy is largely synchronous only, so it is hard to get this exception without do something to trigger it.  Server side might make more sense, since Indy servers generally run a continuous reading thread, so the exception is more likely to happen on that side, but just let the server handle it internally for you.

  • Like 1

Share this post


Link to post
On 1/23/2020 at 12:05 AM, Lars Fosdal said:

I've had the socket open doing writes and reads.  I still keep the connection open as I intend to send more shortly.  If the remote point then disconnects gracefully - should I not be notified?

Not when using blocking sockets, no.  There are no asynchronous events in that situation.  A remote disconnect is reported only when an I/O operation fails due to the peer having already closed the connection on their end.  A peer disconnecting gracefully is typically discovered as a read operation that returns 0 bytes rather than an error code.  That is the condition that Indy uses to raise EIdConnClosedGracefully for a remote disconnect, so a read has to be performed to get that exception in most cases.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post

I have some experience that using Indy TCP client to remote control some device through WiFi.

 

My experience is that when the distance between mobile and device is out of range, you can test the connection by send some data and wait it timeout and in except section to close connection, like this:

 

try

  IdTCPClient1.Send(....);

except

  IdTCPClient1.Disconnect;

end;

 

and then try re-connect again, and again, and when mobile is into the range of device WiFi, you can connect again.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×