Einzelnen Beitrag anzeigen

nuclearping

Registriert seit: 7. Jun 2008
708 Beiträge
 
Delphi 10.2 Tokyo Professional
 
#1

[Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel

  Alt 27. Sep 2012, 11:16
Hallo!

Ich bastel gerade am Netzwerkcode für mein kleines Spiel und nutze dafür Indy 10. Hab noch nie was mit Indy gemacht und in den letzten Tagen viel gelesen, viele Demos angeschaut (an dieser Stelle herzlichen Dank an bernhard_LA für seine Demos) und versucht, viel zu verstehen. Und so ganz grob vom Prinzip her funktioniert es schonmal, dass es eine Serveranwendung gibt, wo sich ein Game-Client problemlos verbinden kann (Betonung liegt auf ein). Problematisch wird's, wenn ein zweiter Client hinzukommt und damit die Kommunikation etwas komplexer wird.

Mir gehts nicht um das grundlegende theoretische Prinzip einer solchen Kommunikation, sondern um den technischen Aspekt, besonders in Zusammenhang mit Indy 10.

Zum Beispiel: Jeder Client stellt sich nach der Connection und dem Login beim Server vor, wie sich's für Gentlemen gehört: "Ich bin Charakter Müller, habe das Sprite A, besitze 100 HP und stehe an Position X, Y". Damit weiß der Server bescheid. Er prüft nun in regelmäßigen Abständen die Distanz zwischen den Clients.

Wenn ein Client in Sichtweite des anderen kommt, wird dieser erstmal in eine Liste aufgenommen, wenn er noch nicht drin ist, und dabei wird eine Nachricht an den sichtenden Client vorbereitet "Hey, hier kommt Client X in Sicht" und in eine Nachrichtenschleife gesteckt.

Das Problem, was diesem konkreten Fall deutlich wird: Sobald der 2. Client sich beim Server vorgestellt hat, wird er von der Sichtbarkeits-Routine erfasst, es wird eine Nachricht erstellt und diese wird sofort gesendet.

Der 2. GameClient erhält nun diese "Hey, hier kommt Client X in Sicht" noch bevor er überhaupt mit der Anmeldung richtig durch war, was ihn gegen die Wand fahren lässt, weil er damit nicht rechnet.

Wie oben schon gesagt: Was hier deutlich wird und auch an anderen Stellen auftreten kann, dass sich Nachrichten überlappen können. Dass der Server eine Mitteilung zu machen hat und damit eine laufende Kommunikation zw. Client und Server unterbricht, bzw. stört.

Wie kann ich das verhindern? Der Server darf nichts rausschicken, solange wie eine Clientanfrage noch nicht beantwortet ist.

Mit "Prüfvariablen" kann ich scheinbar wegen der Threadsicherheit nicht arbeiten. Zumindest habe ich das schon versucht und es hat nicht geklappt. Jeder Klient hat eine Variable "IsWaitingForResponse" bekommen. Die wurde am Anfang von "OnExecute" auf TRUE gesetzt und am Ende wieder auf FALSE. In der Nachrichtenschleife wurde diese dann geprüft, mit der Feststellung im Debugger, dass sie irgendwie ständig auf TRUE steht, obwohl keine Kommunikation stattfand ...

Message-Handler
Delphi-Quellcode:
unit UMessageHandler;

interface

uses
  Classes, IdTask, IdContext, IdTCPServer, IdSchedulerOfThreadDefault,
  IdYarn, IdThread,

  UServerTypes;

type
  TMessageItem = record
    Id: Integer;
    Delete: Boolean;
    Context: TIdContext;
    Data: TNetworkDataRecord;
  end;
  PMessageItem = ^TMessageItem;

  TMessageHandler = class(TIdThread)
    constructor Create;
    destructor Destroy; override;
  protected
    FMessageList: TList;

    procedure Run; override;
  public
    function AddMessage(AMessage: TMessageItem): Integer;
    procedure RemoveMessage(Id: Integer);
  end;

var
  MessageHandler: TMessageHandler;

implementation

uses
  SysUtils,
  Windows,
  Forms,

  IdSync,

  UEventLogHandler,
  UServerClasses,
  UServerFunctions;

procedure ClearMessageList(AList: TList);
var
  i: Integer;
  Item: PMessageItem;
begin
  for i := 0 to AList.Count - 1 do
    begin
      Item := PMessageItem(AList[i]);
      Dispose(Item);
    end;
  AList.Clear;
end;

{ TMessageHandler }

function TMessageHandler.AddMessage(AMessage: TMessageItem): Integer;
var
  Item: PMessageItem;
begin
  New(Item);
  Item^ := AMessage;
  Item^.Delete := FALSE;
  Item^.Id := FMessageList.Add(Item);
  Result := Item^.Id;
end;

constructor TMessageHandler.Create;
begin
  inherited Create;

  FMessageList := TList.Create;
end;

destructor TMessageHandler.Destroy;
begin
  ClearMessageList(FMessageList);
  FreeAndNil(FMessageList);
  inherited;
end;

procedure TMessageHandler.Run;
var
  Buffer: TBytes;
  MsgIndex: Integer;
  Item: PMessageItem;
  Client: TClientContext;
begin
  MsgIndex := 0;
  while not Terminated do
    begin
      if MsgIndex > FMessageList.Count then
        MsgIndex := 0;

      if (FMessageList.Count = 0) then
        begin
          Sleep(1);
          Continue;
        end;

      Item := PMessageItem(FMessageList[MsgIndex]);
      if Item^.Delete then
        begin
          RemoveMessage(Item^.Id);
          MsgIndex := 0;
          Continue;
        end;

      Client := TClientContext(Item^.Context.Data);
      if not Assigned(Client) then
        Continue;

      Buffer := DataRecordToByteArray(Item^.Data);
      try
        if (Item^.Context.Connection.IOHandler.Connected) and not
          Client.WaitingForResponse then // Client.WaitingForResponse ergab immer TRUE, obwohl es keinen Sinn machte
          begin
            if not SendBuffer(Item^.Context, Buffer) then
              TIdNotify.NotifyMethod(EventLogHandler.SendBufferError)
            else
              Item^.Delete := TRUE;
          end;
      finally
        Finalize(Buffer);
      end;

      //Inc(MsgIndex);
    end;
end;

procedure TMessageHandler.RemoveMessage(Id: Integer);
var
  i: Integer;
  Item: PMessageItem;
begin
  for i := 0 to FMessageList.Count - 1 do
    begin
      Item := PMessageItem(FMessageList[i]);
      if Item^.Id = Id then
        begin
          FMessageList.Delete(i);
          Dispose(Item);
          Exit;
        end;
    end;
end;

end.
OnConnect/OnDisconnect/OnExecute-Behandlung
Delphi-Quellcode:
procedure TMyGameServer.OnClientConnect(AContext: TIdContext);
var
  Context: TClientContext;
begin
  FStatus := ssClientConnected;
  Context := TClientContext.Create(AContext);
  Context.ClientsList := FClientsList;
  AContext.Data := Context;
  AddClient(AContext);
end;

procedure TMyGameServer.OnClientDisconnect(AContext: TIdContext);
begin
  FStatus := ssClientDisconnected;
  if Assigned(AContext.Data) then
    begin
      if AContext.Data is TClientContext then
        begin
          RemoveClient(AContext);
          TClientContext(AContext.Data).Free;
        end;
      AContext.Data := nil;
    end;
end;

procedure TMyGameServer.OnServerExecute(AContext: TIdContext);
var
  ClientContext: TClientContext;
begin
  AContext.Connection.IOHandler.ReadTimeout := 10000;
  FStatus := ssProcessingData;
  try
    ClientContext := TClientContext(AContext.Data);
  except
    ClientContext := nil;
  end;

  if Assigned(ClientContext) then
    try
      if not ClientContext.ProcessData(AContext) then
        begin
          FStatus := ssError;
          TIdNotify.NotifyMethod(EventLogHandler.GetBufferError);
        end;
      FStatus := ssIdle;
    except
      FStatus := ssError;
      TIdNotify.NotifyMethod(EventLogHandler.ProcessError);
    end;
end;
TClientContext.ProcessData
Delphi-Quellcode:
unit UServerClasses;

interface

...

type
  TClientContext = class(TIdThreadSafe)
    ...
  public
    function ProcessData(AContext: TIdContext): Boolean; overload;
  end;

implementation

...

function TClientContext.ProcessData(AContext: TIdContext): Boolean;
var
  Buffer: TBytes;
begin
  FIsWaitingForResponse := FALSE; // TRUE ergibt, dass der Wert bei Prüfung in Nachrichtenschleife immer auf TRUE steht, obwohl er unten wieder auf FALSE gesetzt wird :(
  Result := FALSE;
  try
    if not ReceiveBuffer(AContext, Buffer) then
      Exit;

    if Length(Buffer) = 0 then
      Exit;

    Lock;
    try
      try
        FData := ByteArrayToDataRecord(Buffer);
      finally
        Finalize(Buffer);
      end;

      ProcessData;
    finally
      Unlock;
    end;

    Buffer := DataRecordToByteArray(ResponseData);
    try
      if not SendBuffer(AContext, Buffer) then
        Exit;
    finally
      Finalize(Buffer);
    end;

    Result := TRUE;
  finally
    FIsWaitingForResponse := FALSE;
  end;
end;

procedure TClientContext.UpdateClients;
var
  i: Integer;
  Context: TIdContext;
  ClientContext: TClientContext;
  DistanceBetween: Double;
begin
  for i := 0 to FClientsList.Count - 1 do
    begin
      if not FCanUpdateClients then
        Exit;

      Context := TIdContext(FClientsList[i]);
      if Context = FContext then
        Continue;

      ClientContext := (Context.Data as TClientContext);
      DistanceBetween := GetDistance(
        FCharacterPosition.X, FCharacterPosition.Y,
        ClientContext.PositionData.X, ClientContext.PositionData.Y);
      // Kommt ein Character in den Sichtbereich?
      if DistanceBetween <= CLIENT_VISIBILITY_DISTANCE then
        begin
          if not HasNearClient(Context) and IsValidClient(ClientContext) then
            AddNearClient(Context);
        end
      else
        begin
          // Verlässt ein sichtbarer Character den Sichtbereich?
          if HasNearClient(Context) then
            begin
              if DistanceBetween > CLIENT_VISIBILITY_DISTANCE then
                RemoveNearClient(Context);
            end;
        end;
    end;
end;

procedure TClientContext.AddNearClient(AContext: TIdContext);
begin
  FNearClientsList.Add(AContext);
  HandleNearClient(clAppear, AContext);
end;

procedure TClientContext.HandleNearClient(ACommand: TCommandsList;
  AContext: TIdContext);
var
  Data: TNetworkDataRecord;
  CharacterData: TIntroductionRecord;
  Buffer: TBytes;
  i: Integer;
begin
  // ...

  Broadcast(Data, FNearClientsList);
end;

procedure TClientContext.Broadcast(AData: TNetworkDataRecord; AClientsList: TList);
var
  i: Integer;
  Msg: TMessageItem;
  Context: TIdContext;
  List: TList;
begin
  List := AClientsList;
  for i := 0 to List.Count - 1 do
    begin
      Context := TIdContext(List[i]);
      if Context = FContext then
        Continue;
      Msg.Context := Context;
      Msg.Data := AData;
      MessageHandler.AddMessage(Msg);
    end;
end;

end.
Ich hoffe, es ist nicht zu viel und nicht zu wenig Code und dass ihr durchblickt.

Bin für jede Anregung und Hilfe dankbar.
  Mit Zitat antworten Zitat