TIpTCPServer and Client in one application - delphi

I make an application where the client and the server are in the same program. I use Delphi XE7 and components TIpTCPServer / ... Client. But when I try to close the server with the client connected (in the same window), the program stops responding. Perhaps this is something related to multithreading. How to implement a program with a client and server in one application and is this the right approach?
procedure TfrmMain.startClick(Sender: TObject);
begin
if (server.active) then stopServer()
else startServer();
end;
procedure TfrmMain.startServer();
var
binding: TIdSocketHandle;
begin
server.bindings.clear();
try
server.defaultPort := strToInt(port.text);
binding := server.bindings.add();
binding.ip := ip;
binding.port := strToInt(port.text);
server.active := true;
if (server.active) then begin
addToLog('Server started');
start.caption := 'Stop';
end;
except on e: exception do
addToLog('Error: ' + e.message + '.');
end;
end;
procedure TfrmMain.stopServer();
begin
server.active := false;
server.bindings.clear();
if (not(server.active)) then begin
addToLog('Server stopped');
start.caption := 'Start';
end
else addToLog('Server shutdown error.');
end;
procedure TfrmMain.serverConnect(AContext: TIdContext);
var
i: integer;
begin
addToLog('New client: ' + aContext.connection.socket.binding.peerIP + '.');
clients.clear();
for i := 0 to server.contexts.lockList.count - 1 do begin
with TIdContext(server.contexts.lockList[i]) do
clients.items.add(connection.socket.binding.peerIP);
end;
server.contexts.unlockList();
end;
procedure TfrmMain.serverDisconnect(AContext: TIdContext);
begin
addToLog('Client ' + aContext.connection.socket.binding.peerIP + ' disconnected from the server.');
end;
procedure TfrmMain.clientConnected(Sender: TObject);
begin
addToConsole('You connected to server successfully.');
end;
procedure TfrmMain.clientDisconnected(Sender: TObject);
begin
addToConsole('The connection to the server was interrupted.');
end;
and connection code:
client.host := ip;
try
client.connect();
except on e: exception do
addToConsole('Error: ' + e.message);
end;

I see a number of issues with this code.
How are addToLog() and addToConsole() implemented? Are they thread-safe? Remember that TIdTCPServer is a multi-threaded component, its events are fired in the context of worker threads, not the main UI thread, so any access to the UI, shared variables, etc must be synchronized.
What is clients? Is it is a UI control? You need to sync access to it so you don't corrupt its content when multiple threads try to access it at the same time.
Your use of the TIdTCPServer.Contexts property is not adequately protected from exceptions. You need a try..finally block so you can call Contexts.UnlockList() safely.
More importantly, you are calling Contexts.LockList() too many times in your serverConnect() loop (this is the root cause of your problem). LockList() returns a TIdContextList object. Inside your loop, you should be accessing that list's Items[] property instead of calling LockList() again. Because you do not have a matching UnlockList() for each LockList(), once a client connects to your server, the Contexts list becomes deadlocked, and can no longer be accessed once serverConnect() exits, which includes when clients connect/disconnect, and during TIdTCPServer shutdown (such as in your case).
serverDisconnect() is not removing any items from clients. serverConnect() should not be resetting clients at all. It should add only the calling TIdContext to clients, and then serverDisconnect() should remove that same TIdContext from clients later.
With that said, try something more like this:
procedure TfrmMain.addToConsole(const AMsg: string);
begin
TThread.Queue(nil,
procedure
begin
// add AMsg to console ...
end
);
end;
procedure TfrmMain.addToLog(const AMsg: string);
begin
TThread.Queue(nil,
procedure
begin
// add AMsg to log ...
end
);
end;
procedure TfrmMain.startClick(Sender: TObject);
begin
if server.Active then
stopServer()
else
startServer();
end;
procedure TfrmMain.startServer();
var
binding: TIdSocketHandle;
begin
server.Bindings.Clear();
try
server.DefaultPort := StrToInt(port.Text);
binding := server.Bindings.Add();
binding.IP := ip;
binding.Port := StrToInt(port.Text);
server.Active := True;
addToLog('Server started');
start.Caption := 'Stop';
except
on e: Exception do
addToLog('Error: ' + e.message + '.');
end;
end;
procedure TfrmMain.stopServer();
begin
try
server.Active := False;
server.Bindings.Clear();
addToLog('Server stopped');
start.Caption := 'Start';
except
on e: Exception do
addToLog('Server shutdown error.');
end;
end;
procedure TfrmMain.serverConnect(AContext: TIdContext);
var
PeerIP: string;
begin
PeerIP := AContext.Binding.PeerIP;
addToLog('New client: ' + PeerIP + '.');
TThread.Queue(nil,
procedure
{
var
i: integer;
list: TIdContextList;
}
begin
{
clients.clear();
list := server.Contexts.LockList;
try
for i := 0 to list.count - 1 do begin
clients.Items.Add(TIdContext(list[i]).Binding.PeerIP);
end;
finally
list.UnlockList();
end;
}
// I'm assuming clients is a UI control whose Items property
// is a TStrings object. If not, adjust this code as needed...
clients.Items.AddObject(PeerIP, AContext);
end;
);
end;
procedure TfrmMain.serverDisconnect(AContext: TIdContext);
begin
addToLog('Client ' + AContext.Binding.PeerIP + ' disconnected from the server.');
TThread.Queue(nil,
procedure
var
i: Integer;
begin
// I'm assuming clients is a UI control whose Items property
// is a TStrings object. If not, adjust this code as needed...
i := clients.Items.IndexOfObject(AContext);
if i <> -1 then
clients.Items.Delete(i);
end
);
end;
procedure TfrmMain.clientConnected(Sender: TObject);
begin
addToConsole('You connected to server successfully.');
end;
procedure TfrmMain.clientDisconnected(Sender: TObject);
begin
addToConsole('The connection to the server was interrupted.');
end;

Related

Client/Server application

I am writing a client / server application. There is one server and several clients.
When connecting a client, the task is to add its IP address to the ListBox, and when disconnecting the client, remove it from the ListBox. Then exchange messages between the client and server.
Three questions arose: when a client connects, its IP address is added to the ListBox, but when disconnected, it is not deleted from there, here is the code:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
ListLink : Integer;
Thread : Pointer;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient.Create;
Client.DNS := AContext.Connection.Socket.Binding.PeerIP;
Client.ListLink := ListBox1.Items.Count;
Client.Thread := AContext;
ListBox1.Items.Add(Client.DNS);
AContext.Data := Client;
Clients.Add(Client);
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
sleep(2000);
Client :=Pointer (AContext.Data);
Clients.Delete(Client.ListLink);
ListBox1.Items.Delete(ListBox1.Items.IndexOf(Client.DNS));
Client.Free;
AContext.Data := nil;
end;
The second question, when exchanging messages, the letters in Cyrillic are given as "???", all Google went through it and it was not possible to find an error.
And the third question, on the client is a timer that listens to messages from the server, when the timer is turned on, the client application hangs tight, putting all this into the stream is the same trouble, the code:
if not IdTCPClient1.Connected then
Exit;
s := IdTCPClient1.Socket.ReadLn;
if s <> '' then
Label1.Text := s;
I see quite a few problems with your code.
On the server side, you need to get rid of the TSimpleClient.ListLink field. You are misusing it, causing bad behaviors in your code since you don't keep it updated as clients are added/removed. Think of what happens when you have 2 clients connected, where ListLink is 0 and 1 respectively, and then the 1st client disconnects. The ListLink for the 2nd client will become invalid since you don't decrement it from 1 to 0.
Also TIdTCPServer is a multi-threaded component, its events are fired in the context of worker threads, but your event handler code is not thread-safe. You MUST synchronize with the main UI thread when accessing UI controls from worker threads, and you MUST protect your Clients list from concurrent access across thread boundaries. In this case, you don't really need your own Clients list to begin with as TIdTCPServer has its own thread-safe Contexts list that you can use to access the connected clients.
You are also not handling Unicode at all. By default, Indy's default byte encoding for Unicode strings is US-ASCII, which is why you are getting ? for non-ASCII characters. You can use the IOHandler's DefStringEncoding property to set a different byte encoding, such as IndyTextEncoding_UTF8 (if you are using Delphi 2007 or earlier, you might need to also use the IOHandler's DefAnsiEncoding property to specify how your ANSI strings are converted to/from Unicode. By default, it is set to IndyTextEncoding_OSDefault).
Try something more like this:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
Thread : Pointer;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create;
begin
inherited;
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient.Create;
Client.DNS := PeerIP;
Client.Thread := AContext;
AContext.Data := Client;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
try
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
finally
{ The anonymous procedure being passed to TThread.Queue() above captures
the Client variable itself, not its value. On ARC platforms, we need to
prevent Free() setting the variable to nil before it can be passed to
IndexOfObject(), and also because IndexOfObject() expects a live object
anyway. ARC will free the object when the anonymous procedure exits. On
non-ARC platforms, it is OK to Free() the object here, the variable will
not change value, and IndexOfObject() does not need a live object... }
{$IFNDEF AUTOREFCOUNT}
Client.Free;
{$ENDIF}
AContext.Data := nil;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
Alternatively, you can derive TSimpleClient from TIdServerContext and get rid of the Thread field altogether:
type
TSimpleClient = class(TIdServerContext)
DNS,
Name : String;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil); override;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TIdContextThreadList = nil);
begin
inherited Create(AConnection, AYarn, AList);
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
Self.Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TSimpleClient;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient(AContext);
Client.DNS := PeerIP;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext);
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
On the client side, you are reading from the socket in the main UI thread, but Indy uses blocking sockets, and so its reading methods will block the calling thread until the requested data arrives. DON'T block the main UI thread! Read only if there is actually something available to read, or else move the reading into a separate worker thread. For example:
IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
...
IdTCPClient1.Disconnect;
...
procedure TForm1.Timer1Timer(Sender: TObject);
var
s: string;
begin
if IdTCPClient1.Connected and (not IdTCPClient1.IOHandler.InputBufferIsEmpty) then
begin
s := IdTCPClient1.IOHandler.ReadLn;
if s <> '' then
Label1.Text := s;
end;
end;
Alternatively:
type
TReadingThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TReadingThread.Execute;
var
s: String;
begin
while not Terminated do
begin
s := Form1.IdTCPClient1.IOHandler.ReadLn;
if s <> '' then
begin
TThread.Queue(nil,
procedure
begin
Form1.Label1.Text := s;
end
);
end;
end;
end;
...
var
ReadingThread: TReadingThread = nil;
...
IdTCPClient1.Connect;
IdTCPClient1.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
ReadingThread := TReadingThread.Create(False);
...
ReadingThread.Terminate;
try
IdTCPClient1.Disconnect;
finally
ReadingThread.WaitFor;
ReadingThread.Free;
end;
Thank you so much Remy, your answer really helped me sort out my problem. I targeted Windows and Android platforms. I fixed your code a little and it worked for me:
type
TSimpleClient = class(TObject)
DNS,
Name : String;
Thread : Pointer;
OutgoingMsgs : TIdThreadSafeStringList;
HasOutgoingMsgs : Boolean;
constructor Create;
destructor Destroy; override;
procedure Queue(const Msg: string);
procedure FlushMsgs;
end;
constructor TSimpleClient.Create;
begin
inherited;
OutgoingMsgs := TIdThreadSafeStringList.Create;
end;
destructor TSimpleClient.Destroy;
begin
OutgoingMsgs.Free;
inherited;
end;
procedure TSimpleClient.Queue(const Msg: string);
var
List: TStringList;
Client: TSimpleClient;
begin
List := OutgoingMsgs.Lock;
try
List.Add(Msg);
HasOutgoingMsgs := True;
Client.FlushMsgs;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TSimpleClient.FlushMsgs;
var
List: TStringList;
begin
List := OutgoingMsgs.Lock;
try
while List.Count > 0 do
begin
TIdContext(Thread).Connection.IOHandler.WriteLn(List[0]);
List.Delete(0);
end;
HasOutgoingMsgs := False;
finally
OutgoingMsgs.Unlock;
end;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
PeerIP: string;
Client: TSimpleClient;
begin
PeerIP := AContext.Binding.PeerIP;
Client := TSimpleClient.Create;
Client.DNS := PeerIP;
Client.Thread := AContext;
AContext.Data := Client;
TThread.Queue(nil,
procedure
begin
ListBox1.Items.AddObject(PeerIP, Client);
end
);
AContext.Connection.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Client : TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
try
TThread.Queue(nil,
procedure
var
Index: Integer;
begin
Index := ListBox1.Items.IndexOfObject(Client);
if Index <> -1 then
ListBox1.Items.Delete(Index);
end;
);
finally
{ The anonymous procedure being passed to TThread.Queue() above captures
the Client variable itself, not its value. On ARC platforms, we need to
prevent Free() setting the variable to nil before it can be passed to
IndexOfObject(), and also because IndexOfObject() expects a live object
anyway. ARC will free the object when the anonymous procedure exits. On
non-ARC platforms, it is OK to Free() the object here, the variable will
not change value, and IndexOfObject() does not need a live object... }
{$IFNDEF AUTOREFCOUNT}
Client.Free;
{$ENDIF}
AContext.Data := nil;
end;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Client: TSimpleClient;
begin
Client := TSimpleClient(AContext.Data);
if Client.HasOutgoingMsgs then
Client.FlushMsgs
else
Sleep(100);
end;
procedure TForm1.SendMessageToClient(Client: TSimpleClient; const Msg: string);
var
List: TIdContextList;
begin
List := IdTCPServer1.Contexts.LockList;
try
if List.IndexOf(TIdContext(Client.Thread)) <> -1 then // still connected?
Client.Queue(Msg);
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Index: Integer;
Msg: string;
Client: TSimpleClient;
begin
Index := ListBox1.ItemIndex;
if Index = -1 then Exit;
Msg := Edit1.Text;
if Msg = '' then Exit;
Client := TSimpleClient(ListBox1.Items.Objects[Index]);
SendMessageToClient(Client, Msg);
end;
I added a call to the FlushMsgs method from the TSimpleClient.Queue procedure and messages started to be sent, the list of clients is updated every time clients are connected and disconnected, and the server stopped hanging. Thanks again Remy, you helped a lot to speed up the development, golden man.

Delphi indy send stream to client

I am new with indy servers and so I'm struggling for this simple task. I have to create a server and upload a little file; its size is always 128 bytes. Then when someone opens the homepage of the server the file is sent automatically. So:
Upload a file (the one that is 128 bytes) on the disk
Open a browser like Firefox
Type the url (below you can see that I've set 127.0.0.1:798) and when you press enter there is a white page but a dialog appears asking you to download the file.
I have written this code so far:
procedure TForm1.Button1Click(Sender: TObject);
begin
// IP = 127.0.0.1:798 (port is 798)
IdTCPServer1.Active := true;
Memo1.Lines.Add('Server started at: ' + TimeToStr(Now) + slinebreak);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
IdTCPServer1.Active := false;
Memo1.Lines.Add('Server stopped at: ' + TimeToStr(Now));
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var a: TFileStream;
begin
a := TFileStream.Create('C:\Users\defaulr.user\Desktop\datfile.pkm', fmOpenWrite);
AContext.Connection.IOHandler.Write(a);
end;
This is the form:
Start is Button1 and End is Button2. As you can see I am loading in a stream the file and then I try to send it as output when I open the page. Is this the proper way to do it?
Since you are accessing the file via a web browser, you should be using TIdHTTPServer instead of TIdTCPServer:
procedure TForm1.Button1Click(Sender: TObject);
begin
// IP = 127.0.0.1:798 (port is 798)
IdHTTPServer1.Active := true;
Memo1.Lines.Add('Server started at: ' + TimeToStr(Now));
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
IdHTTPServer1.Active := false;
Memo1.Lines.Add('Server stopped at: ' + TimeToStr(Now));
end;
// TIdHTTPServer.OnCommandGet event handler...
procedure TForm1.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
if ARequestInfo.Document = '/' then
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ServeFile(AContext, 'C:\Users\defaulr.user\Desktop\datfile.pkm');
// alternatively:
// AResponseInfo.SmartServeFile(AContext, ARequestInfo, 'C:\Users\defaulr.user\Desktop\datfile.pkm');
end else
AResponseInfo.ResponseNo := 404;
end;

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
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;

Delphi TCPClient Issue

I am new to Delphi and trying to convert vb.net apps to learn. The issue I am having is reading from a TCP/IP host. Currently I can connect via telnet to the device, send a command, and the device will send data non-stop until all data is sent. This could be simply two characters followed by CR/LF, or it could be several rows of varing length data. Each row is end is CR/LF. Prior to writing code, we were able to telnet via Hyperterminal to the device. Send a command, and, with the capture text enabled save to a text file.
Below is the code I have so far. I have not coded for saving to text file (one step at a time). The data is pipe delimited. I have no control on the format or operatation of the device aside from sending commands and receiving data. It works most of the time however there are times when not all of the data (65 records for testing) are received. I will greatly appreciate guidence and feel free to comment on my code, good or bad.
function Parse(Char, S: string; Count: Integer): string;
var
I: Integer;
T: string;
begin
if S[Length(S)] <> Char then
S := S + Char;
for I := 1 to Count do
begin
T := Copy(S, 0, Pos(Char, S) - 1);
S := Copy(S, Pos(Char, S) + 1, Length(S));
end;
Result := T;
end;
procedure TForm2.btnEXITClick(Sender: TObject);
begin
if idTcpClient1.connected then
begin
idTcpClient1.IOHandler.InputBuffer.clear;
idTcpClient1.Disconnect;
end;
Close;
end;
procedure TForm2.btnSendDataClick(Sender: TObject);
var
mTXDataString : String;
RXString : String;
begin
IdTCPClient1.Host := IPAddress.Text;
IdTCPClient1.Port := StrToInt(IPPort.Text);
mTXDataString := mTXData.Text + #13#10;
IdTCPClient1.Connect;
If IdTCPClient1.Connected then
begin
IdTCPClient1.IOHandler.Write(mTXDataString);
mTXDataString := mTXData.Lines.Text;
if MTXDataString.Contains('SCHEMA') then
begin
mRXData.Lines.Add(IdTCPClient1.IOHandler.ReadLn);
while not (IdTCPClient1.IOHandler.InputBufferIsEmpty) do
begin
RXString := IdTCPClient1.IOHandler.ReadLn;
If (RXString <> '') and (RXString <> '??') then
begin
//Add received data to RXmemo
mRXData.Lines.Add(RXString);
//Determine number of records to received based on schema data
lblRecords.Caption := Parse(',', RXString, 2);
end;
end; //while not
end // if
else
if mTXDataString.Contains('DATA') then
begin
mRXData.Lines.Add(IdTCPClient1.IOHandler.ReadLn);
while not (IdTCPClient1.IOHandler.InputBufferIsEmpty) do
begin
RXString := IdTCPClient1.IOHandler.ReadLn;
If (RXString <> '') and (RXString <> '??') then
begin
mRXData.Lines.Add(RXString);
end; // if
end; //while not
end; // if Schema or not
end; // if Connected
IdTCPClient1.Disconnect;
end; //Procedure
HyperTerminal and Telnet apps display whatever data they receive, in real-time. TIdTCPClient is not a real-time component. You control when and how it reads. If you are expecting data to arrive asynchronously, especially if you don't know how many rows are going to be received, then you need to perform the reading in a timer or worker thread, eg:
procedure TForm2.TimerElapsed(Sender: TObject);
var
S: String;
begin
if IdTCPClient1.IOHandler = nil then Exit;
if IdTCPClient1.IOHandler.InputBufferIsEmpty then
begin
IdTCPClient1.IOHandler.CheckForDataOnSource(50);
if IdTCPClient1.IOHandler.InputBufferIsEmpty then Exit;
end;
S := IdTCPClient1.IOHandler.ReadLn;
// use S as needed ...
end;
Or:
type
TMyThread = class(TThread)
protected
fClient: TIdTCPClient;
procedure Execute; override;
public
constructor Create(aClient: TIdTCPClient);
end;
constructor TMyThread.Create(aClient: TIdTCPClient);
begin
inherited Create(False);
fClient := aClient;
end;
procedure TMyThread.Execute;
var
S: String;
begin
while not Terminated do
begin
S := fClient.IOHandler.ReadLn;
// use S as needed ...
end;
end;
Or, if the server supports the actual Telnet protocol, have a look at using Indy's TIdTelnet component instead.

AccessViolation with TThread.Synchronize and DLLs

I'm using the WorkerThread from DelphiPraxis with Delphi XE2.
http://www.delphipraxis.net/93835-workerthread-der-diener-im-hintergrund.html
In my JobThread, I'm loading a DLL, which does some waiting (for Testing..)
function DLLClass.doStuff(): boolean;
var
I: integer;
begin
try
for I := 0 to 100 do
begin
sleep(10);
if (assigned(StatusCallback)) then
StatusCallback(PWideChar(I));
end;
Result := true;
except
on e: exception do
begin
error := 'DLL FEHLER: ' + e.ClassName + ' - ' + e.Message;
Result := false;
end;
end;
end;
The "StatusCallback" is a Reference to a procedure in the Thread whichis loading the DLL:
TStatusUpdate = procedure(Status: PWideChar) of object; stdcall;
My Callback looks like this:
procedure JobThread.statuscall(status: pwidechar); stdcall;
begin
//saving the Status in a global Variable..
if Assigned(OnStatus) then
fThread.Synchronize(syncStatus);
end;
Which calls:
procedure JobThread.syncStatus;
begin
if Assigned(OnStatus) then
begin
OnStatus(self);
end;
end; //<- AV here!
OnStatus eventhandler:
Procedure TfMain.uploadStatus(aJob: TWorkerThreadJob);
begin
//doing nothing.. yet an AV
sleep(10);
end;
I think the problem is somehow related to the DLL not being able to Synchronize with the MainThread.
Any ideas on working around the Synchronize (if it's actually the problem)?

Resources