How to send a command to one client, but not to all?

I am writing a simple client / server chat program with Indy 10. My server (idtcpserver) sends a command to the client and the client responds, but when more than one client is connected and the server sends the command, all clients have connected sending data to the server.

How can I send a command to a specified client and not all?

+4
source share
2 answers

The only way a command could be sent to all connected clients is that your code iterates over all the clients sending the command to each of them. So just delete this loop or at least change it to send only to a specific client that interests you.

The best place to send a command to a client in order to avoid distortion of communication with this client due to overlapping commands occurs from within this OnExecute native client, for example:

 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 streams, then it is best to provide this client with its own queue of outgoing commands, and then send this message to the OnExecute client when it is safe. Other threads may optionally insert commands into the queue.

 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; 
+5
source

Typically, in a client / server setup, the client initiates a contact and the server responds. Using events opened by IdTCPServer is always a context (connection), so you do not need to do anything special.

To start contact with the server on the client, you will need to track connected clients and use the connection of the desired client to send him a message. To do this, you need a list where you need to keep connected clients and you need to implement handlers for 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; 

If you want to send data to a specific client, you can do this using the usual methods of sending data over a TCP connection. But first you need to find the specific client you need in the FClients list.

How you identify specific customers is entirely up to you. This will depend entirely on the information that you exchange between the client and the server when the client first connects and identifies itself. Having said that, the mechanism will be the same regardless of this information.

TIDContext is the ancestor of the TIdServerContext class used by Indy to store connection information. You can go down from the TIdServerContext to have a place where you can store your own connection data.

 type TMyContext = class(TIdServerContext) private // MyInterestingUserDetails... 

Tell Indy to use the native TIdServerContext descendant using its ContextClass property. Of course, you need to do this before activating your server, for example, in OnCreate.

 procedure TForm1.HandleTcpServerCreate(Sender: TObject); begin FIdTcpServer1.ContectClass = TMyContext; end; 

And then you can use your own class wherever you have the TIdContext parameter, dropping it into your own class:

 procedure TForm1.HandleClientConnect(aThread: TIDContext); var MyContext: TMyContext; begin MyContext := aThread as TMyContext; end; 

Finding a specific client’s connection then becomes a matter of repeating the list of your FClients and checking if it contains the TMyContext that 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; 

Change As Remy notes in the comments: to ensure thread safety, you must keep the list blocked when writing to the client (which is not so good for bandwidth and performance), or in Remy the words:

"The best option is to provide TMyContext with your own TIdThreadSafeStringList for outgoing data, and then provide this client with the OnExecute event for this list to the client when it is safe. Other OnExecute client events can then insert data into it if necessary."

+6
source

All Articles