I am writing a simple client/server chat program with Indy 10. My server (idtcpserver) sends a command to the client, and the client answers, but when more than one client is connected and the server sends a command, all the clients connected send data to the server.
How I can send a command to a specified client and not all?
The only way a command could be sent to all connected clients is if your code is looping through all of the clients sending the command to each one. So simply remove that loop, or at least change it to only send to the specific client that you are interested in.
The best place to send a command to a client, to avoid corrupting the communications with that client due to overlapping commands, is from within that client's own OnExecute
event, eg:
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
begin
...
if (has a command to send) then
begin
AContext.Connection.IOHandler.WriteLn(command here);
...
end;
...
end;
If you need to send commands to a client from other threads, then it is best to give that client its own queue of outbound commands and then have that client's OnExecute
event send the queue when it is safe to do so. Other threads can push commands into the queue when needed.
type
TMyContext = class(TIdServerContext)
public
ClientName: String;
Queue: TIdThreadSafeStringList;
constructor Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil); override;
destructor Destroy; override;
end;
constructor TMyContext.Create(AConnection: TIdTCPConnection; AYarn: TIdYarn; AList: TThreadList = nil);
begin
inherited Create(AConnection, AYarn, AList);
Queue := TIdThreadSafeStringList.Create;
end;
destructor TMyContext.Destroy;
begin
Queue.Free;
inherited Destroy;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
IdTCPServer1.ContextClass := TMyContext;
end;
procedure TForm1.SendCommandToClient(const ClientName, Command: String);
var
List: TList;
I: Ineger;
Ctx: TMyContext;
begin
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
Ctx := TMyContext(List[I]);
if Ctx.ClientName = ClientName then
begin
Ctx.Queue.Add(Command);
Break;
end;
end;
finally
IdTCPServer1.Context.UnlockList;
end;
end;
procedure TForm1.IdTCPServer1Connect(AContext: TIdContext);
var
List: TList;
I: Ineger;
Ctx, Ctx2: TMyContext;
ClientName: String;
begin
Ctx := TMyContext(AContext);
ClientName := AContext.Connection.IOHandler.ReadLn;
List := IdTCPServer1.Contexts.LockList;
try
for I := 0 to List.Count-1 do
begin
Ctx2 := TMyContext(List[I]);
if (Ctx2 <> Ctx) and (Ctx.ClientName = ClientName) then
begin
AContext.Connection.IOHandler.WriteLn('That Name is already logged in');
AContext.Connection.Disconnect;
Exit;
end;
end;
Ctx.ClientName = ClientName;
finally
IdTCPServer1.Context.UnlockList;
end;
AContext.Connection.IOHandler.WriteLn('Welcome ' + ClientName);
end;
procedure TForm1.IdTCPServer1Disconnect(AContext: TIdContext);
var
Ctx: TMyContext;
begin
Ctx := TMyContext(AContext);
Ctx.ClientName = '';
Ctx.Queue.Clear;
end;
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
Ctx: TMyContext;
Queue: TStringList;
begin
Ctx := TMyContext(AContext);
...
Queue := Ctx.Queue.Lock;
try
while Queue.Count > 0 do
begin
AContext.Connection.IOHandler.WriteLn(Queue[0]);
Queue.Delete(0);
...
end;
...
finally
Ctx.Queue.Unlock;
end;
end;
Normally in a client/server setup, the client initiates contact and the server responds. Using the events exposed by the IdTCPServer this is always context (connection) specific so you wouldn't have to do anything special.
To initiate contact from the server to the client, you would have to keep track of connected clients and use the connection of the desired client to send it a message. To do so you need a list in which to hold the connected clients and need to implement handlers for the OnConnect and OnDisconnect events.
type
TForm1 = class(TForm)
private
FClients: TThreadList;
procedure TForm1.HandleClientConnect(aThread: TIDContext);
begin
FClients.Add(aThread);
end;
procedure TForm1.HandleClientDisconnect(aThread: TIDContext);
begin
FClients.Remove(aThread);
end;
When you want to send data to a specific client, you can do that using the normal methods for sending data over a TCP connection. But first you will need to find the specific client you need in your FClients list.
How you will identify specific clients is entirely up to you. It will depend entirely on the information you exchange between client and server when a client first connects and identifies itself. Having said that the mechanism will be the same regardless of that information.
TIDContext is the ancestor of the TIdServerContext class used by Indy to hold the connection details. You can descend from TIdServerContext to have a place where you can store your own details for a connection.
type
TMyContext = class(TIdServerContext)
private
// MyInterestingUserDetails...
Tell Indy to use your own TIdServerContext descendant using its ContextClass
property. You would of course need to do this before activating your server, for example in the OnCreate.
procedure TForm1.HandleTcpServerCreate(Sender: TObject);
begin
FIdTcpServer1.ContectClass = TMyContext;
end;
And then you can use your own class everywhere you have a TIdContext parameter by casting it to your own class:
procedure TForm1.HandleClientConnect(aThread: TIDContext);
var
MyContext: TMyContext;
begin
MyContext := aThread as TMyContext;
end;
Finding the connection of a specific client then becomes a matter of iterating over your FClients list and checking whether the TMyContext it contains is the one you want:
function TForm1.FindContextFor(aClientDetails: string): TMyContext;
var
LockedList: TList;
idx: integer;
begin
Result := nil;
LockedList := FClients.LockList;
try
for idx := 0 to LockedList.Count - 1 do
begin
MyContext := LockedList.Items(idx) as TMyContext;
if SameText(MyContext.ClientDetails, aClientDetails) then
begin
Result := MyContext;
Break;
end;
end;
finally
FClients.UnlockList;
end;
Edit: As Remy points out in comments: for thread safety you should keep the list locked while writing to the client (which is not such a good thing for throughput and performance) or, in Remy's words:
"A better option is to give TMyContext its own TIdThreadSafeStringList for outbound data, and then have that client's OnExecute event write that list to the client when it is safe to do so. Other clients' OnExecute events can then push data into that list when needed."