Using TIdTCPServer
on the master and TIdTCPClient
on the slaves is the right way to go.
One way to send messages from the server to clients is to use the server's Threads
property (Indy 9 and earlier) or Contexts
property (Indy 10) to access the list of currently connected clients. Each client has a TIdTCPConnection
object associated with it for communicating with that client. When needed, you can lock the server's client list, loop through it writing your message to each client, and then unlock the list:
Indy 9:
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdPeerThread(List[I]).Connection.WriteLn(S);
except
end;
end;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Indy 10:
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Coun-1 do
begin
try
TIdContext(List[I]).Connection.IOHandler.WriteLn(S);
except
end;
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
This has some disadvantages, though.
One disadvanage is that messages are serialized so if one client freezes up, subsequent clients will not receive their messages in a timely manner. Another issue is that clients run in their own threads on the server, so when sending data to a client from multiple threads at the same time, you would have to provide your own per-connection locking mechanism (such as a critical section or mutex) around every write access to a connection to avoid overlapping data and corrupting your communications
To avoid those pitfalls, it is better to give each client an outbound queue of messages, and then let the server's OnExecute
even send the queued messages on its own schedule. That way, multiple clients can receive messages in parallel instead of serially:
Indy 9:
uses
..., IdThreadSafe;
procedure TMaster.IdTCPServer1Connect(AThread: TIdPeerThead);
begin
AThread.Data := TIdThreadSafeStringList.Create;
end;
procedure TMaster.IdTCPServer1Disconnect(AThread: TIdPeerThead);
begin
AThread.Data.Free;
AThread.Data := nil;
end;
procedure TMaster.IdTCPServer1Execute(AThread: TIdPeerThead);
var
Queue: TIdThreadSafeStringList;
List: TStringList;
Tmp: TStringList;
I: Integer;
begin
...
Queue := TIdThreadSafeStringList(AThread.Data);
List := Queue.Lock;
try
if List.Count > 0 then
begin
Tmp := TStringList.Create;
try
Tmp.Assign(List);
List.Clear;
except
Tmp.Free;
raise;
end;
end;
finally
Queue.Unlock;
end;
if Tmp <> nil then
try
AThread.Connection.WriteStrings(Tmp, False);
finally
Tmp.Free;
end;
...
end;
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdThreadSafeStringList(TIdPeerThread(List[I]).Data).Add(S);
except
end;
end;
finally
IdTCPServer1.Threads.UnlockList;
end;
end;
Indy 10:
uses
..., IdThreadSafe;
procedure TMaster.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Data := TIdThreadSafeStringList.Create;
end;
procedure TMaster.IdTCPServer1Disconnect(AContext: TIdContext);
begin
AContext.Data.Free;
AContext.Data := nil;
end;
procedure TMaster.IdTCPServer1Execute(AContext: TIdContext);
var
Queue: TIdThreadSafeStringList;
List: TStringList;
Tmp: TStringList;
I: Integer;
begin
...
Queue := TIdThreadSafeStringList(AContext.Data);
List := Queue.Lock;
try
if List.Count > 0 then
begin
Tmp := TStringList.Create;
try
Tmp.Assign(List);
List.Clear;
except
Tmp.Free;
raise;
end;
end;
finally
Queue.Unlock;
end;
if Tmp <> nil then
try
AContext.Connection.IOHandler.Write(Tmp, False);
finally
Tmp.Free;
end;
...
end;
procedure TMaster.SendMsg(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdThreadSafeStringList(TIdContext(List[I]).Data).Add(S);
except
end;
end;
finally
IdTCPServer1.Contexts.UnlockList;
end;
end;
Even with the queuing in place to address multithreading concerns, another disadvantage is that things still do not play very well if your server's OnExecute
event or CommandHandlers
collection has to send data back to a client in response to a command from the client at the same time that the server is broadcasting messages to those same clients. After a client sends a command and tries to read back a response, it may receive a broadcast instead, and the real response will be received after sending another command later on. The client would have to detect broadcasts so it can keep reading until it gets the response it is expecting.
You are essentially asking for two separate communication models. One model allows a client to send commands to the server (SignIn/Out, etc), and another model where the server sends real-time broadcasts to clients. Trying to manage those two models over a single connection is doable, but tricky. A simple solution would be to move the broadcasts to another TIdTCPServer
that just sends broadcasts and does nothing else. The master can have two TIdTCPServer
components running, listening on different ports, and then each slave can have two TIdTCPClient
components running, one for sending commands and one for receiving broadcasts. The downside is that each slave would have to keep 2 connections to the master, which can eat up network bandwidth if you have a lot of slaves connected at one time. But it does keep your coding fairly simple on both sides.
Indy 9:
procedure TMaster.IdTCPServer1Execute(AThread: TIdPeerThread);
var
S: String;
begin
S := AThread.Connection.ReadLn;
if S = 'SignIn' then
...
else if S = 'SignOut' then
...
else
...
end;
procedure TMaster.SendBroadcast(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer2.Threads.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdPeerThread(List[I]).Connection.WriteLn(S);
except
end;
end;
finally
IdTCPServer2.Threads.UnlockList;
end;
end;
.
procedure TSlave.Connect;
begin
IdTCPClient1.Connect;
IdTCPClient2.Connect;
end;
procedure TSlave.SignIn;
begin
IdTCPClient1.SendCmd('SignIn');
...
end;
procedure TSlave.SignOut;
begin
IdTCPClient1.SendCmd('SignOut');
...
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient2.InputBuffer.Size = 0 then
IdTCPClient2.ReadFromStack(True, 0, False);
while IdTCPClient2.InputBuffer.Size > 0 do
begin
S := IdTCPClient2.ReadLn;
... handle broadcast ...
end;
except
on E: EIdException do
IdTCPClient2.Disconnect;
end;
end;
Indy 10:
procedure TMaster.IdTCPServer1Execute(AContext: TIdContext);
var
S: String;
begin
S := AContext.Connection.IOHandler.ReadLn;
if S = 'SignIn' then
...
else if S = 'SignOut' then
...
else
...
end;
procedure TMaster.SendBroadcast(const S: String);
var
List: TList;
I: Integer;
begin
List := IdTCPServer2.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
try
TIdContext(List[I]).Connection.IOHandler.WriteLn(S);
except
end;
end;
finally
IdTCPServer2.Contexts.UnlockList;
end;
end;
.
procedure TSlave.Connect;
begin
IdTCPClient1.Connect;
IdTCPClient2.Connect;
end;
procedure TSlave.SignIn;
begin
IdTCPClient1.SendCmd('SignIn');
...
end;
procedure TSlave.SignOut;
begin
IdTCPClient1.SendCmd('SignOut');
...
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient2Connect(Sener: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient2.IOHandler.InputBufferIsEmpty then
IdTCPClient2.IOHandler.CheckForDataOnSource(0);
while not IdTCPClient2.IOHandler.InputBufferIsEmpty do
begin
S := IdTCPClient2.IOHandler.ReadLn;
... handle broadcast ...
end;
except
on E: EIdException do
IdTCPClient2.Disconnect;
end;
end;
If using separate connections for commands and broadcasts is not an option for whatever reason, then you essentially need to design your communication protocol to work asynchronously, meaning a client can send a command to the server and not wait for the response to come back right away. The client would have to do all of its reading from inside a timer/thread, and then identify whether each received message is a broadcast or a response to a previous command and act accordingly:
Indy 9:
procedure TSlave.IdTCPClient1Connect(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient1Disconnect(Sender: TObject);
begin
Timer1.Enabled := False;
end;
procedure TSlave.PostCmd(const S: String);
begin
IdTCPClient1.WriteLn(S);
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient1.InputBuffer.Size = 0 then
IdTCPClient1.ReadFromStack(True, 0, False);
while IdTCPClient1.InputBuffer.Size > 0 do
begin
S := IdTCPClient1.ReadLn;
if (S is a broadcast) then
... handle broadcast ...
else
... handle a command response ...
end;
except
on E: EIdException do
IdTCPClient1.Disconnect;
end;
end;
Indy 10:
procedure TSlave.IdTCPClient1Connect(Sender: TObject);
begin
Timer1.Enabled := True;
end;
procedure TSlave.IdTCPClient1Disconnect(Sender: TObject);
begin
Timer1.Enabled := False;
end;
procedure TSlave.PostCmd(const S: String);
begin
IdTCPClient1.IOHandler.WriteLn(S);
end;
procedure TSlave.Timer1Elapsed(Sender: TObject);
var
S: String;
begin
try
if IdTCPClient1.IOHandler.InputBufferIsEmpty then
IdTCPClient1.IOHandler.CheckForDataOnSource(0);
while not IdTCPClient1.IOHandler.InputBufferIsEmpty do
begin
S := IdTCPClient1.IOHandler.ReadLn;
if (S is a broadcast) then
... handle broadcast ...
else
... handle a command response ...
end;
except
on E: EIdException do
IdTCPClient1.Disconnect;
end;
end;
accept the answers
which helped you to resolve your questions ;-) – TLama