Search code examples
delphiindyindy10delphi-xe8

how do I disconnect inactive clients with TIdTCPServer?


I am trying to disconnect inactive clients that are connected to TIdTCPServer, whether those clients are disconnected from their Internet or for a period of inactive time.

I tried to set timeouts in the OnConnect event like the following:

procedure TservForm.TcpServerConnect(AContext: TIdContext);
begin
  AContext.Connection.IOHandler.ReadTimeout := 26000;
  AContext.Binding.SetSockOpt(Id_SOL_SOCKET, Id_SO_SNDTIMEO, 15000);
end; 

But it seems a disconnect is not triggered after the client connection is lost.

I tried to use SetKeepAliveValues(), but it takes too much time to get an inactive client disconnected.

Is there a more helpful way to disconnect inactive clients? So if the client did not receive or send anything, for example in 30 seconds, the server will disconnect it?

on execute event

procedure TservForm.TcpServerExecute(AContext: TIdContext);
var
  Connection: TConnection;
  cmd: String;
  Cache, OutboundCmds: TStringList;
  MS: TMemoryStream;
  I: integer;
  S: String;
begin
  Connection := AContext as TConnection;

  // check for pending outbound commands...
  OutboundCmds := nil;
  try
    Cache := Connection.OutboundCache.Lock;
    try
      if Cache.Count > 0 then
      begin
        OutboundCmds := TStringList.Create;
        OutboundCmds.Assign(Cache);
        Cache.Clear;
      end;
    finally
      Connection.OutboundCache.Unlock;
    end;

    if OutboundCmds <> nil then
    begin
      for I := 0 to OutboundCmds.Count - 1 do
      begin
        AContext.Connection.IOHandler.Writeln(OutboundCmds.Strings[I],
          IndyTextEncoding_UTF8);
        MS := TMemoryStream(OutboundCmds.Objects[I]);
        if MS <> nil then
        begin
          AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
          AContext.Connection.IOHandler.LargeStream := true;
          AContext.Connection.IOHandler.Write(MS, 0, true);
        end;
      end;
    end;
  finally
    if OutboundCmds <> nil then
    begin
      for I := 0 to OutboundCmds.Count - 1 do
        OutboundCmds.Objects[I].Free;
    end;
    OutboundCmds.Free;
  end;

  // check for a pending inbound command...
  if AContext.Connection.IOHandler.InputBufferIsEmpty then
  begin
    AContext.Connection.IOHandler.CheckForDataOnSource(100);
    AContext.Connection.IOHandler.CheckForDisconnect;
    if AContext.Connection.IOHandler.InputBufferIsEmpty then
    begin
    Exit;
    end;
  end;

  cmd := AContext.Connection.Socket.ReadLn(IndyTextEncoding_UTF8);

  ...............
  ...............

Solution

  • The client does not disconnect because the ReadLn() is not reached during idle times, so the ReadTimeout does not have effect, and if you are not sending a lot of commands then the socket buffer is not filling up so SO_SNDTIMEO does not have an effect, either.

    Since you are already doing some manual timeout handling, you can expand on it to handle an idle timeout as well, eg:

    type
      TConnection = class(TIdServerContext)
        ...
      public
        LastSendRecv: LongWord;
        ...
      end;
    
    ...
    
    procedure TservForm.TcpServerConnect(AContext: TIdContext);
    var
      Connection: TConnection;
    begin
      Connection := AContext as TConnection;
      AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
      AContext.Connection.IOHandler.LargeStream := True;
      AContext.Connection.IOHandler.ReadTimeout := 30000;
      AContext.Binding.SetSockOpt(Id_SOL_SOCKET, Id_SO_SNDTIMEO, 15000);
      Connection.LastSendRecv := Ticks;
    end; 
    
    procedure TservForm.TcpServerExecute(AContext: TIdContext);
    var
      Connection: TConnection;
      cmd: String;
      Cache, OutboundCmds: TStringList;
      MS: TMemoryStream;
      I: integer;
      S: String;
    begin
      Connection := AContext as TConnection;
    
      // check for pending outbound commands...
      OutboundCmds := nil;
      try
        Cache := Connection.OutboundCache.Lock;
        try
          if Cache.Count > 0 then
          begin
            OutboundCmds := TStringList.Create;
            OutboundCmds.Assign(Cache);
            Cache.Clear;
          end;
        finally
          Connection.OutboundCache.Unlock;
        end;
    
        if OutboundCmds <> nil then
        begin
          for I := 0 to OutboundCmds.Count - 1 do
          begin
            AContext.Connection.IOHandler.WriteLn(OutboundCmds.Strings[I]);
            MS := TMemoryStream(OutboundCmds.Objects[I]);
            if MS <> nil then               
              AContext.Connection.IOHandler.Write(MS, 0, true);    
          end;
          Connection.LastSendRecv := Ticks;
        end;
      finally
        if OutboundCmds <> nil then
        begin
          for I := 0 to OutboundCmds.Count - 1 do
            OutboundCmds.Objects[I].Free;
        end;
        OutboundCmds.Free;
      end;
    
      // check for a pending inbound command...
      if AContext.Connection.IOHandler.InputBufferIsEmpty then
      begin
        AContext.Connection.IOHandler.CheckForDataOnSource(100);
        AContext.Connection.IOHandler.CheckForDisconnect;
        if AContext.Connection.IOHandler.InputBufferIsEmpty then
        begin
          if GetTickDiff(Connection.LastSendRecv, Ticks) >= 30000 then
            AContext.Connection.Disconnect;
          Exit;
        end;
      end;
    
      cmd := AContext.Connection.Socket.ReadLn;    
      Connection.LastSendRecv := Ticks;
    
      ...
    end;