Search code examples
delphiindy10

Indy : Treatment when connection between server and clients will be broken abnormally


Everyone. I am developing Server/Clients program on based Indy TCP controls. Now, I faced some uncertain problems.. It's just about the connection broken abnormally... Let us suppose network status is broken unexpectedly, or Server application is terminated abnormally, so client can't communicate with server anymore... then clients will occure exceptions like as "connection reset by peer" or "connection refused..." In these cases, how to treate these exceptions smartly ? I want that client will connect again automatically and communicate normally after recovering of server status... If you have a good idea, please share it.....

Below is my code. I have used two timer controls. One is to send alive and confirm networks status(5000ms). If network status is ok, then this timer is dead, and another timer is enable. Second timer is to send info to server(1000ms)

If in second timer, exception occures then it's disabled, and the 1st timer is enabled again.

when "connection refused" is occured, then try except block can catch it. But if "Connection reset by peer" is occured, then try except block can't catch it.

{sendbuffer funtion}

function SendBuffer(AClient: TIdTCPClient; ABuffer: TBytes): Boolean; overload;
begin
  try
    Result := True;
    try
      AClient.IOHandler.Write(LongInt(Length(ABuffer)));
      AClient.IOHandler.WriteBufferOpen;
      AClient.IOHandler.Write(ABuffer, Length(ABuffer));
      AClient.IOHandler.WriteBufferFlush;
    finally
      AClient.IOHandler.WriteBufferClose;
    end;
  except
    Result := False;
  end;

end;

{alive timer}

procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
var
  infoStr : string;
begin
  if not IdTCPClient_StrSend.Connected then
  begin
    try
      if IdTCPClient_StrSend.IOHandler <> nil then
      begin
        IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
        IdTCPClient_StrSend.IOHandler.WriteBufferClear;
      end;
      IdTCPClient_StrSend.Connect;
    except on E: Exception do
      begin
        SAOutMsg := 'connect fail : ' + E.ToString ;
        Exit;
      end;
    end;
    SAOutMsg := 'connect success : ';

    if IdTCPClient_StrSend.Connected then
    begin

      IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
      IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);

      infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
      try
        IdTCPClient_StrSend.IOHandler.WriteLn(infoStr, nil);
      except on E: Exception do
        begin
          SAOutMsg := 'login info send fail : ';
          Exit;
        end;
      end;
      SAOutMsg := 'login info send success : ';
      try
        if IdTCPClient_StrSend.IOHandler.ReadLn() = 'OK' then
        begin
          Timer_StrAlive.Enabled := False;
          Timer_Str.Enabled := True;
        end;
      except on E: Exception do
        begin
          SAOutMsg := 'login fail : ' + E.ToString ;
          Exit;
        end;
      end;
      SAOutMsg := 'login ok : ' ;
    end;
  end;
end;

{send part}

procedure TClientForm.Timer_StrTimer(Sender: TObject);
var
  LBuffer: TBytes;
  LClientRecord: TClientRecord;
begin
//  IdTCPClient_StrSend.CheckForGracefulDisconnect(False);
    if not IdTCPClient_StrSend.Connected then
    begin
      Timer_Str.Enabled := False;
      Timer_StrAlive.Enabled := True;
      Exit;
    end;

  if IdTCPClient_StrSend.Connected then
  begin

    LClientRecord.data1 := str1;
    LClientRecord.data2:= Trim(str2);
    LClientRecord.data3 := Trim(str3);


    LBuffer := MyRecordToByteArray(LClientRecord);

    IdTCPClient_StrSend.IOHandler.CheckForDisconnect(True, True);
    IdTCPClient_StrSend.IOHandler.CheckForDataOnSource(100);

    if (SendBuffer(IdTCPClient_StrSend, LBuffer) = False) then
    begin
      SOutMsg := 'info send fail' ;
      IdTCPClient_StrSend.Disconnect(False);
      if IdTCPClient_StrSend.IOHandler <> nil then
        IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
      Timer_Str.Enabled := False;
      Timer_StrAlive.Enabled := True;
      Exit;
    end

Solution

  • Exceptions related to lost connections, like "connection reset by peer", do not occur until you perform a socket read/write operation after the OS has detected the lost connection (usually after an internal timeout period has elapsed) and invalidated the socket connection. Most Indy client components do not perform such operations automatically, you have to tell them to do so (TIdTelnet and TIdCmdTCPClient being notable exceptions to that rule, as they run internal reading threads). So simply wrap your socket operations in a try/except block, and if you catch an Indy socket exception (EIdSocketError or descendant, for instance) then you can call Disconnect() and Connect() to re-connect.

    "connection refused" can only occur when calling Connect(). It usually means the server was reached but could not accept the connection at that time, either because there is no listening socket on the requested IP/port, or there are too many pending connections in the listening socket's backlog (it could also mean a firewall blocked the connection). Again, simply wrap Connect() in a try/except to handle the error so you can call Connect() again. You should wait a small timeout period before doing so, to allow the server some time to possibly clear up whatever condition made it refuse the connection in the first place (assuming a firewall is not the issue).

    Indy relies heavily on exceptions for error reporting, and to a lesser degree for status reporting. So you usually need to make use of try/except handlers when using Indy.

    Update: I see a few problems in your code. SendBuffer() is not implementing writing buffering correctly. And most of the calls to Connected(), and all of the calls to CheckForDisconnect() and CheckForDataOnSource(), are overkill and should be removed completely. The only calls that make sense to keep are the first call to Connected() in each timer.

    Try something more like this:

    {sendbuffer function}

    function SendBuffer(AClient: TIdTCPClient; const ABuffer: TBytes): Boolean; overload;
    begin
      Result := False;
      try
        AClient.IOHandler.WriteBufferOpen;
        try
          AClient.IOHandler.Write(LongInt(Length(ABuffer)));
          AClient.IOHandler.Write(ABuffer);
          AClient.IOHandler.WriteBufferClose;
        except
          AClient.IOHandler.WriteBufferCancel;
          raise;
        end;
        Result := True;
      except
      end;
    end;
    

    {alive timer}

    procedure TClientForm.Timer_StrAliveTimer(Sender: TObject);
    var
      infoStr : string;
    begin
      if IdTCPClient_StrSend.Connected then Exit;
    
      try
        IdTCPClient_StrSend.Connect;
      except
        on E: Exception do
        begin
          SAOutMsg := 'connect fail : ' + E.ToString;
          Exit;
        end;
      end;
    
      try
        SAOutMsg := 'connect success : ';
    
        infoStr := MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME;
        try
          IdTCPClient_StrSend.IOHandler.WriteLn(infoStr);
        except
          on E: Exception do
          begin
            E.Message := 'login info send fail : ' + E.Message;
            raise;
          end;
        end;
        SAOutMsg := 'login info send success : ';
    
        try
          if IdTCPClient_StrSend.IOHandler.ReadLn() <> 'OK' then
            raise Exception.Create('not OK');
        except
          on E: Exception do
          begin
            E.Message := 'login fail : ' + E.Message;
            raise;
          end;
        end;
        SAOutMsg := 'login ok : ' ;
      except
        on E: Exception do
        begin
          SAOutMsg := E.ToString;
          IdTCPClient_StrSend.Disconnect(False);
          if IdTCPClient_StrSend.IOHandler <> nil then
            IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
          Exit;
        end;
      end;
    
      Timer_StrAlive.Enabled := False;
      Timer_Str.Enabled := True;
    end;
    

    {send part}

    procedure TClientForm.Timer_StrTimer(Sender: TObject);
    var
      LBuffer: TBytes;
      LClientRecord: TClientRecord;
    begin
      if not IdTCPClient_StrSend.Connected then
      begin
        Timer_Str.Enabled := False;
        Timer_StrAlive.Enabled := True;
        Exit;
      end;
    
      LClientRecord.data1 := str1;
      LClientRecord.data2:= Trim(str2);
      LClientRecord.data3 := Trim(str3);
    
      LBuffer := MyRecordToByteArray(LClientRecord);
    
      if not SendBuffer(IdTCPClient_StrSend, LBuffer) then
      begin
        SOutMsg := 'info send fail' ;
        IdTCPClient_StrSend.Disconnect(False);
        if IdTCPClient_StrSend.IOHandler <> nil then
          IdTCPClient_StrSend.IOHandler.InputBuffer.Clear;
        Timer_Str.Enabled := False;
        Timer_StrAlive.Enabled := True;
        Exit;
      end
    
      ...
    end;
    

    Now, with that said, using Indy inside of timers in the main UI thread is not the best, or even the safest, way to use Indy. This kind of logic would work much better in a worker thread instead, eg:

    type
      TStrSendThread = class(TThread)
      private
        FClient: TIdTCPClient;
        ...
      protected
        procedure Execute; override;
        procedure DoTerminate; override;
      public
        constructor Create(AClient: TIdTCPClient); reintroduce;
      end;
    
    constructor TStrSendThread.Create(AClient: TIdTCPClient);
    begin
      inherited Create(False);
      FClient := AClient;
    end;
    
    procedure TStrSendThread.Execute;
    var
      LBuffer: TIdBytes;
      ...
    begin
      while not Terminated do
      begin
        Sleep(ConnectInterval);
        if Terminated then Exit;
    
        try
          FClient.Connect;
          try
            // report status to main thread as needed...
    
            FClient.IOHandler.WriteLn(MY_MAC_ADDRESS+'|'+MY_COMPUTER_NAME);
            if FClient.IOHandler.ReadLn() <> 'OK' then
              raise Exception.Create('error message');
    
            // report status to main thread as needed...
    
            while not Terminated do
            begin
              Sleep(SendInterval);
              if Terminated then Exit;
              ...
              if not SendBuffer(FClient, LBuffer) then
                raise Exception.Create('error message');
            end;
          finally
            FClient.FDisconnect(False);
            if FClient.IOHandler <> nil then
              FClient.IOHandler.InputBuffer.Clear;
          end;
        except
          on E: Exception do
          begin
            // report error to main thread as needed...
          end;
        end;
      end;
    end;
    
    procedure TStrSendThread.DoTerminate;
    begin
      // report status to main thread as needed...
      inherited;
    end;
    

    private
      Thread: TStrSendThread;
    
    ...
    
    // Timer_StrAliveTimer.Active := True;
    if Thread = nil then
      Thread := TStrSendThread.Create(IdTCPClient_StrSend);
    
    ...
    
    // Timer_StrAliveTimer.Active := False;
    if Thread <> nil then
    begin
      Thread.Terminate;
      Thread.WaitFor;
      FreeAndNil(Thread);
    end;