Indy 10を使用して簡単なクライアント/サーバーチャットプログラムを作成しています。サーバー(idtcpserver)がクライアントにコマンドを送信すると、クライアントが応答しますが、複数のクライアントが接続され、サーバーがコマンドを送信すると、すべてのクライアントが接続されます。サーバーにデータを送信します。
すべてではなく、指定されたクライアントにコマンドを送信するにはどうすればよいですか?
Indy 10を使用して簡単なクライアント/サーバーチャットプログラムを作成しています。サーバー(idtcpserver)がクライアントにコマンドを送信すると、クライアントが応答しますが、複数のクライアントが接続され、サーバーがコマンドを送信すると、すべてのクライアントが接続されます。サーバーにデータを送信します。
すべてではなく、指定されたクライアントにコマンドを送信するにはどうすればよいですか?
通常、クライアント/サーバーのセットアップでは、クライアントが連絡を開始し、サーバーが応答します。IdTCPServerによって公開されたイベントを使用すると、これは常にコンテキスト(接続)固有であるため、特別なことをする必要はありません。
サーバーからクライアントへの接続を開始するには、接続されているクライアントを追跡し、目的のクライアントの接続を使用してメッセージを送信する必要があります。これを行うには、接続されたクライアントを保持するためのリストが必要であり、OnConnectおよびOnDisconnectイベントのハンドラーを実装する必要があります。
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;
特定のクライアントにデータを送信する場合は、TCP接続を介してデータを送信するための通常の方法を使用して送信できます。ただし、最初に、FClientsリストで必要な特定のクライアントを見つける必要があります。
特定のクライアントをどのように識別するかは、完全にあなた次第です。これは、クライアントが最初に接続して自身を識別するときに、クライアントとサーバーの間で交換する情報に完全に依存します。とは言っても、その情報に関係なくメカニズムは同じです。
TIDContextは、接続の詳細を保持するためにIndyによって使用されるTIdServerContextクラスの祖先です。TIdServerContextから派生して、接続に関する独自の詳細を保存できる場所を作成できます。
type
TMyContext = class(TIdServerContext)
private
// MyInterestingUserDetails...
Indyに、そのプロパティを使用して独自のTIdServerContext子孫を使用するように指示しContextClass
ます。もちろん、たとえばOnCreateでサーバーをアクティブ化する前に、これを行う必要があります。
procedure TForm1.HandleTcpServerCreate(Sender: TObject);
begin
FIdTcpServer1.ContectClass = TMyContext;
end;
そして、TIdContextパラメーターを独自のクラスにキャストすることで、TIdContextパラメーターがあるすべての場所で独自のクラスを使用できます。
procedure TForm1.HandleClientConnect(aThread: TIDContext);
var
MyContext: TMyContext;
begin
MyContext := aThread as TMyContext;
end;
次に、特定のクライアントの接続を見つけることは、FClientsリストを繰り返し処理し、それに含まれるTMyContextが必要なものであるかどうかを確認することです。
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;
編集:Remyがコメントで指摘しているように:スレッドセーフのために、クライアントへの書き込み中はリストをロックしておく必要があります(これはスループットとパフォーマンスにとってそれほど良いことではありません)、またはRemyの言葉で:
「より良いオプションは、TMyContextにアウトバウンドデータ用の独自のTIdThreadSafeStringListを指定し、安全なときにそのクライアントのOnExecuteイベントにそのリストをクライアントに書き込むようにすることです。他のクライアントのOnExecuteイベントは、必要に応じてデータをそのリストにプッシュできます。 。」
接続されているすべてのクライアントにコマンドを送信できる唯一の方法は、コードがすべてのクライアントをループして、各クライアントにコマンドを送信している場合です。したがって、単にそのループを削除するか、少なくとも関心のある特定のクライアントにのみ送信するように変更してください。
コマンドの重複によるそのクライアントとの通信の破損を回避するために、クライアントにコマンドを送信するのに最適な場所は、そのクライアント自身のOnExecute
イベント内からです。例:
procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
begin
...
if (has a command to send) then
begin
AContext.Connection.IOHandler.WriteLn(command here);
...
end;
...
end;
他のスレッドからクライアントにコマンドを送信する必要がある場合は、そのクライアントに独自のアウトバウンドコマンドのキューを指定し、OnExecute
安全なときにそのクライアントのイベントにキューを送信させるのが最善です。他のスレッドは、必要に応じてコマンドをキューにプッシュできます。
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;