IdTcpServer Kick Client (Disconnect) INVALID HANDLE Exception - 3ddark - 06-17-2020
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;
RE: IdTcpServer Kick Client (Disconnect) INVALID HANDLE Exception - rlebeau - 06-17-2020
(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().
RE: IdTcpServer Kick Client (Disconnect) INVALID HANDLE Exception - 3ddark - 06-18-2020
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;
RE: IdTcpServer Kick Client (Disconnect) INVALID HANDLE Exception - rlebeau - 06-19-2020
(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.
|