Atozed Forums

Full Version: IdTcpServer Kick Client (Disconnect) INVALID HANDLE Exception
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello everybody,
I have a TCP Server application as ConsoleApp. Sometimes i need Disconnect to some client/s. I use like this code in main loop.
But I get an error after Disconnect  ----> INVALID HANDLE

I gues(probally) after disconnect then lost thread handle.

How to solve this issue
Code:
var
  LList: TList;
  n1: Integer;
begin
  while True do
  begin
    Sleep(1);

    LList := IdTCPServer1.Contexts.LockList;
    try
      for n1 := 0 to LList.Count-1 do
        if TIdContext(LList[n1]).IsKickedClient then
          TIdContext(LList[n1]).Connection.Disconnect;
    finally
      IdTCPServer1.Contexts.UnlockList;
      LList := nil;
    end;
  end;
end;
(06-17-2020, 05:58 PM)3ddark Wrote: [ -> ]I have a TCP Server application as ConsoleApp. Sometimes i need Disconnect to some client/s. I use like this code in main loop.
But I get an error after Disconnect  ----> INVALID HANDLE

I gues(probally) after disconnect then lost thread handle.

What does the call stack actually look like when the exception is raised? I suspect the error is due to an invalid socket handle, not a thread handle, such as if the client were already disconnected but its context hadn't been removed from the list yet. You might need to set another flag in the OnDisconnect event that your loop can then look for, so it doesn't try to disconnect clients that are already disconnect(ing|ed).

Is there a particular reason why you are kicking clients from your main thread, and not from the individual client threads? For instance, by having the main thread merely set a flag that the OnExecute event can then look for to call Disconnect().
Project Explain
IoT Device <--------> TCP Server(Internet) <--------> PC Client
-----------------IOT SIDE-------------------------
IoT Device Connect to TCP Server (Add to DB and MemTables)(table online_client)
IoT Device Login and Ident yourself with ID(GSM No) Update DB and MemTables(table online_client)Client ID and times data
IoT Device Every 1min Send heartbeat Data to Server
------------------------------------------------------
-----------------PC CLIENT SIDE-----------------
PC Connect to TCP Server (Add to DB and MemTables)(table online_client)
PC Login and Ident yourself with ID(UserName) Update DB and MemTables(table online_client =>Client ID and times data) -- After add db and memtable(online_pair)
PC Request from IoT Device over the TCP Server. After IoT device Response to PC over TCP Server
------------------------------------------------------

IoT Device or PC can suddenly shut down.

In this case, it seems that there is still a connection in Server Contexts.List (List.Count=1). Actually, List.Count=0

I want to disconnect connections and remove FDMemTables that do not send or receive data in the last 5 minutes.

Debugger Exception Notification
---------------------------
Project LiftNetServer.exe raised exception class $C0000008 with message 'c0000008 INVALID_HANDLE'.
---------------------------

Code:
procedure TServer.spAddOnlineClient(pIP, pClientID: string; pPort: Integer; pIsLift: Boolean);
var
  connectDate: TDateTime;
  bool: string;
begin
  if dm.qryAddOnlineClient.Connection.Connected then
  begin
    try
      connectDate := Now;

      if pIsLift then bool := 'True' else bool := 'False';

      dm.qryAddOnlineClient.SQL.Text := 'SELECT spadd_online_client(' +
       QuotedStr(pIP) + '::text, ' +
       QuotedStr(pPort.ToString) + '::text, ' +
       QuotedStr(pClientID) + '::text, ' +
       bool + ', ' +
       QuotedStr(DateTimeToStr(connectDate)) + '::timestamp without time zone)';
      dm.qryAddOnlineClient.Open;
      if not dm.qryAddOnlineClient.Fields.Fields[0].IsNull then
        FIdDB := dm.qryAddOnlineClient.Fields.Fields[0].AsInteger
      else
        FIdDB := 0;

      //add record to memtable for fast work without db
      tblOnlineClient.Append;
      tblOnlineClient.FieldByName('id').AsInteger := FIdDB;
      tblOnlineClient.FieldByName('ip').AsString := pIP;
      tblOnlineClient.FieldByName('port').AsInteger := pPort;
      tblOnlineClient.FieldByName('client_id').AsString := pClientID;
      tblOnlineClient.FieldByName('is_lift').AsBoolean := pIsLift;
      tblOnlineClient.FieldByName('connect_date').AsDateTime := connectDate;
      tblOnlineClient.FieldByName('last_update').Value := Null;
      tblOnlineClient.Post;
    finally
      dm.qryAddOnlineClient.Close;
    end;
  end;
end;

procedure TServer.spDeleteOnlineClient(pIP: string; pPort: Integer);
begin
  if dm.qryDeleteOnlineClient.Connection.Connected then
  begin
    try
      //delete online_client and online_pair records
      dm.qryDeleteOnlineClient.SQL.Text := 'SELECT spdel_online_client(' +
       QuotedStr(pIP) + '::text, ' +
       QuotedStr(pPort.ToString) + '::text' + ')';
      dm.qryDeleteOnlineClient.Execute;


      //delete from memtable
      tblOnlineClient.Filter := ' ip=' + QuotedStr(pIP) + ' and port=' + QuotedStr(pPort.ToString);
      tblOnlineClient.Filtered := True;
      tblOnlineClient.IndexFieldNames := 'connect_date:D';
      tblOnlineClient.First;
      while not tblOnlineClient.Eof do begin
        tblOnlineClient.Delete;
        if tblOnlineClient.RecordCount > 0 then
          tblOnlineClient.Next;
      end;

      tblOnlinePair.Filter := ' (pc_ip='   + QuotedStr(pIP) + ' and pc_port=' +   QuotedStr(pPort.ToString) + ') ' +
                           ' or (lift_ip=' + QuotedStr(pIP) + ' and lift_port=' + QuotedStr(pPort.ToString) + ') ';
      tblOnlinePair.Filtered := True;
      tblOnlinePair.First;
      while not tblOnlinePair.Eof do begin
        tblOnlinePair.Delete;
        tblOnlinePair.Next;
      end;
    finally
      tblOnlineClient.Filter := '';
      tblOnlineClient.Filtered := False;
      tblOnlinePair.Filter := '';
      tblOnlinePair.Filtered := False;
      dm.qryDeleteOnlineClient.Close;
    end;
  end;
end;

procedure TServer.kickClient(AContext: IdContext.TIdContext);
begin
  if not Assigned(AContext) then
    Exit;

  writeToConsole('Disconnect:' + AContext.Binding.PeerIP + ':' + AContext.Binding.PeerPort.ToString, True);

  //update database and memtables
  spDeleteOnlineClient(AContext.Binding.PeerIP, AContext.Binding.PeerPort);
  //after disconnect client
  AContext.Connection.Disconnect;  //HERE IS THE PROBLEM after call this procedure
end;

//main loop
var
  Server: TServer;
begin
  Server := TServer.Create(cloud_port);
  try
    Server.IdTCPServer.Active := True;
    Server.writeToConsole('TCP Socket Server App Started', False);
    Server.writeToConsole('Listen Port ' + cloud_port, False);

    n1 := 0;
    Server.writeToConsole('Start...', False);

    while True do
    begin
      try
        Sleep(1);
        Inc(n1);

        //every 2 sec kick old client
        if n1 = 2000 then
        begin
          .......
          n1 := 0;

          Writeln(TimeToStr(Now) + ' Before : TCP Client=' + Server.IdTCPServer.Contexts.Count.ToString + ' - Table Count=' + Server.tblOnlineClient.RecordCount.ToString);
          Server.FLockList := Server.IdTCPServer.Contexts.LockList;
          try
            for n2 := Server.FLockList.Count-1 downto 0 do begin
              AContext := Server.FLockList[n2];

              if GetTickDiff(TConnection(AContext).LastSendRecv, Ticks) >= 300000 then  //Kill the zombie connection if you haven't received and sent data in the last 5 minutes
              begin
                Writeln(TimeToStr(Now) + ' : Kicked Old Client ID=' + TConnection(AContext).ClientID);
                Server.kickClient(AContext);
              end;
            end;
          finally
             Server.IdTCPServer.Contexts.UnlockList;
             Server.FLockList := nil;
          end;
        end;
      except
        on E: Exception do
          Writeln(E.ClassName, ': ', E.Message);
      end;
    end;
  finally
    Server.Destroy;
  end;
end.

procedure TServer.IdTCPServerConnect(AContext: TIdContext);
begin
  if Assigned(AContext) then
  begin
    if AContext.Binding.Port = IdTCPServer.Bindings.DefaultPort then
    begin
      //save client info to db at the first connection
      //Cant detect at the first connection IoT Device or PC. Have my hand only ip and port datas
      TConnection(AContext).LastSendRecv := Ticks;
      spAddOnlineClient(AContext.Binding.PeerIP, '', AContext.Binding.PeerPort, False); //add db and memtable
      writeToConsole('Welcome guest :) ' + AContext.Binding.PeerIP + ':' + AContext.Binding.PeerPort.ToString, False);
    end;
  end;
end;

procedure TServer.IdTCPServerDisconnect(AContext: TIdContext);
begin
  writeToConsole('Disconnect:' + AContext.Binding.PeerIP + ':' + AContext.Binding.PeerPort.ToString + ' Good Bye', False); // write to console screen
  spDeleteOnlineClient(AContext.Binding.PeerIP, AContext.Binding.PeerPort); //at the disconnect remove client from db and memtable
end;

procedure TServer.IdTCPServerExecute(AContext: TIdContext);
var
  RCIdByte: TIdBytes;

  vSessionPC, vSessionLift: TSession;
  vSessionPair: TSessionPair;
begin
  AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;

  SetLength(RCIdByte, 0); //clean buffer

  AContext.Connection.IOHandler.ReadBytes(RCIdByte, -1); //read package

  FLockList := IdTCPServer.Contexts.LockList; //lock and make process
  try
    if Length(RCIdByte) > 0 then
    begin
      //prepare and validate package
      TTools.copyTIDArrayToArray(RCIdByte, RC);
      TTools.parseRCData2(RC, FDataPacket);

      if FDataPacket.IsKickedClient then
        kickClient(AContext) //kick client if data package is invalid
      else
      begin
        TConnection(AContext).LastSendRecv := Ticks; //update last action time at the receive or send data

        if FDataPacket.Direction = PC_TO_LIFT then  //dats came from PC to IoT Device
        begin
          if FDataPacket.Cmd = CMD_LOGIN_PC then
          begin
            vSessionLift := getOnlineClientData(FDataPacket.GSMNo, AContext.Binding.PeerIP, AContext.Binding.PeerPort);
            TConnection(AContext).ClientID := vSessionLift.ID;
            if (vSessionLift.IP = '') then
            begin
              TTools.preArrayData(TR, HEADER_LIFT_TO_PC, FDataPacket.GSMNo, FDataPacket.PcID, '', CMD_ERR_LIFT_IS_MISSING);
              sendData(TR, AContext.Binding.PeerIP, AContext.Binding.PeerPort.ToString);
              Exit;
            end;

            //Update DB and MemTable client_id and other infos
            spEditOnlineClient(AContext.Binding.PeerIP, FDataPacket.PcID, AContext.Binding.PeerPort, False);
            vSessionPC := getOnlineClientData(FDataPacket.PcID, AContext.Binding.PeerIP, AContext.Binding.PeerPort);
            //add online pair table and memtable
            spAddOnlinePair(vSessionLift.IP, vSessionLift.ID, vSessionLift.Port.ToInteger, vSessionLift.ConnectDate,
                            vSessionPC.IP, vSessionPC.ID, vSessionPC.Port.ToInteger, vSessionPC.ConnectDate);
          end
          else if FDataPacket.Cmd = CMD_DATA_PC_TO_LIFT then
          begin
            //two way comm. PC Request from the IoT Device
            vSessionPair := getOnlinePairDataByLiftID(AContext.Binding.PeerIP, FDataPacket.GSMNo, AContext.Binding.PeerPort);
            if (vSessionPair.LiftIP <> '') AND (vSessionPair.PcIP <> '') then //find pair and send data to pair
            begin
              spUpdateLastActtion(AContext.Binding.PeerIP, AContext.Binding.PeerPort); //update last_update, last action times
              sendData(RC, vSessionPair.LiftIP, vSessionPair.LiftPort); //send package to IoT device
            end;
          end;
        end
        else if FDataPacket.Direction = LIFT_TO_PC then
        begin
          if FDataPacket.Cmd = CMD_LOGIN_LIFT then
          begin
            FDataPacket.LiftSerialNo := TTools.getLiftSerialNo(FDataPacket);
            FDataPacket.Password := TTools.getLiftPassword(FDataPacket);

            //Update DB and MemTable client_id and other infos
            spEditOnlineClient(AContext.Binding.PeerIP, FDataPacket.GSMNo, AContext.Binding.PeerPort, True);
            //Update iot_device table and MemTables last_action time
            spAddOrEdtLift(AContext.Binding.PeerIP, AContext.Binding.PeerPort);
          end
          else if FDataPacket.Cmd = CMD_HB_LIFT then  //periodic(60sec) data heartbeat with some data
          begin
            spUpdateLiftHeartbeatData(AContext.Binding.PeerIP, AContext.Binding.PeerPort, False);
          end
          else if FDataPacket.Cmd = CMD_DATA_LIFT_TO_PC then
          begin
            //two way comm. IoT Device Response the PC
            vSessionPair := getOnlinePairDataByPcID(AContext.Binding.PeerIP, FDataPacket.PcID, AContext.Binding.PeerPort);
            if (vSessionPair.LiftIP <> '') AND (vSessionPair.PcIP <> '') then
            begin
              spUpdateLastActtion(AContext.Binding.PeerIP, AContext.Binding.PeerPort);
              sendData(RC, vSessionPair.PcIP, vSessionPair.PcPort);
            end;
          end;
        end;
      end;
    end;

  finally
    IdTCPServer.Contexts.UnlockList;
    FLockList := nil;
  end;
end;
(06-18-2020, 06:14 AM)3ddark Wrote: [ -> ]IoT Device or PC can suddenly shut down.

In this case, it seems that there is still a connection in Server Contexts.List (List.Count=1). Actually, List.Count=0

I want to disconnect connections and remove FDMemTables that do not send or receive data in the last 5 minutes.

Why are you trying to handle this in the main thread, and not in the individual connection threads? I would suggest setting up the tables and login in the OnConnect event, and logout and remove the tables in the OnDisconnect event, and then have the OnExecute event monitor connections, handle requests/responses, deal with timeouts, and disconnects as needed. You don't need to involve the main thread for any of this at all.

Also, there is no good reason to lock the server's Contexts list inside the OnExecute event when you are not actively enumerating active connections. You can freely use the AContext object inside the event without locking the list. Based on the code you have shown, that locking should be done only inside of your sendData() method instead, when passing data to another TIdContext to send.

Also, you should not be sharing DB components across thread boundaries at all. Your use of the DB queries is not thread-safe, multiple client threads can run queries at the same time, overwriting each other. You are not serializing access to the DB components at all.

Each client thread should use its own separate DB connection and query components. I would suggest using a pool of DB connections. When a given client thread wants to access the DB, it can pull an active DB connection from the pool (adding one if needed), run queries on that DB connection as needed, and then put it back in the pool for later reuse (and then have a separate thread that monitors the pool to close connections that have been idle for awhile).

(06-18-2020, 06:14 AM)3ddark Wrote: [ -> ]Debugger Exception Notification
---------------------------
Project LiftNetServer.exe raised exception class $C0000008 with message 'c0000008 INVALID_HANDLE'.
---------------------------

You didn't provide the call stack for the exception, like I asked for. I need to see EXACTLY where Disconnect() is causing the exception to be raised from.