5

Indy 10を使用して簡単なクライアント/サーバーチャットプログラムを作成しています。サーバー(idtcpserver)がクライアントにコマンドを送信すると、クライアントが応答しますが、複数のクライアントが接続され、サーバーがコマンドを送信すると、すべてのクライアントが接続されます。サーバーにデータを送信します。

すべてではなく、指定されたクライアントにコマンドを送信するにはどうすればよいですか?

4

2 に答える 2

6

通常、クライアント/サーバーのセットアップでは、クライアントが連絡を開始し、サーバーが応答します。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イベントは、必要に応じてデータをそのリストにプッシュできます。 。」

于 2012-12-27T12:27:44.563 に答える
6

接続されているすべてのクライアントにコマンドを送信できる唯一の方法は、コードがすべてのクライアントをループして、各クライアントにコマンドを送信している場合です。したがって、単にそのループを削除するか、少なくとも関心のある特定のクライアントにのみ送信するように変更してください。

コマンドの重複によるそのクライアントとの通信の破損を回避するために、クライアントにコマンドを送信するのに最適な場所は、そのクライアント自身の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;
于 2012-12-27T21:28:36.037 に答える