2
votes

I am quite new to Indy, have implemented very basic idTCPServer and IdTCPClient between 2 pc's on local LAN (no internet), but now need 3 machines to talk! (Environment is Delphi 2010/Win7/DBExpress/MySQL). Application is to control a real-time sailing event. A "master" pc and a "slave" (now need 2 slaves!). The slave pc's enable sailors to register their details for the event (stored into MySQL tables). The master pc controls a) when registration screen opens/closes, b) sends event details to slaves, c) send real-time race countdown/start and race elapsed times to slaves which they must display and react to (closing registration screen etc). Master needs to know when new person has signed in or out to update it's racelist.

Currently I implemented (my first Indy program) with IDTCPServer on master. IdTCPClient on slave tells master when new SignIn/Out, and continually sends "time request" text msgs to server, as I dont know how to send message from a TCPServer!).

This I think is not best way to do it, and now the club wants TWO "Sign On" slaves I need to re-visit whole thing, so I ask your advice please...

Which Indy components are best to use? Is it best to have TCPServer on "master" pc? Should the server broadcast to the 2 slaves? and (please!) is there any example that has similar functionality that I can use as a base to help me implement? Many thanks Chris

1
Chris, don't forget to accept the answers which helped you to resolve your questions ;-)TLama

1 Answers

8
votes

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;