![]() |
[Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
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 ... :gruebel: Message-Handler
Delphi-Quellcode:
OnConnect/OnDisconnect/OnExecute-Behandlung
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.
Delphi-Quellcode:
TClientContext.ProcessData
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;
Delphi-Quellcode:
Ich hoffe, es ist nicht zu viel und nicht zu wenig Code und dass ihr durchblickt.
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. Bin für jede Anregung und Hilfe dankbar. :) |
AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
Hi,
es ist vermutlich besser, das Protokoll erstmal ohne eine Implementierung zu entwerfen. Ein ordentliches Zustandsdiagramm zwingt dich, das Protokoll richtig zu durchdenken, Abläufe kannst du dir mit (Überraschung) einem Ablaufdiagramm visualisieren. Wenn du Sender und Empfänger als Zustandsautomaten modellierst, wird die Implementation nachher sehr viel einfacher, als wenn du einfach drauf los codest. Dein Problem ist nicht wirklich die konkrete Implementation, sondern das Konzept: Zitat:
Dein konkretes Problem würde eine Art Kanäle/Ports lösen. Beim Lesen der Nachrichten kann festgelegt werden, aus welchem Kanal gelesen wird (Authentifizierung/Broadcast/usw.). Alle anderen Nachrichten bleiben in einer Warteschlange. Das könnte aber sehr unübersichtlich werden. |
AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
Danke für die Tipps. :)
Ich bin zugegebenermaßen auch nicht so der Theoretiker. Ich kann minutenlang vor einer Mindmap oder einem Ablaufplan sitzen, ohne dass da viel in meinem Kopf passiert, aber wenn ich 'ne IDE vor mir hab und "drauflos programmieren" oder mich mit Leuten darüber austauschen kann, kommen die Ideen, Zusammenhänge und Abläufe automatisch. Aber das verläuft nicht so chaotisch, wie's vielleicht klingen mag. Ich achte da schon immer auf ordentliches objekt-orientiertes Design, dass ich generische Klassen erstelle und diese sinnvoll vererbe und erweitere. Und viel mehr als ein paar Sachen umstrukturieren oder neu verschachteln muss ich da inzwischen auch nicht mehr. :mrgreen: Das Protokoll ist mir im Kopf soweit eigentlich schon klar: Jede aktive oder passive Zustandsänderung seitens des Clients wird an den Server gemeldet, der wiederum an alle relevanten (in der Nähe befindlichen) Clients ein Statusupdate des jeweiligen Characters sendet und sich mit diesen synchronisiert. Also wenn ein Charakter anfängt zu laufen, wird ein STARTWALKING an den Server geschickt, der schickt an die anderen Clients nun das Update STARTWALKING CharacterID, ebenso mit STOPWALKING oder wenn der Character sich dreht, anfängt zu zaubern, den Bogen spannt, Schaden von einer anderen Quelle bekommt, sich heilt, usw. Danach folgt ein SYNCPOSITION, SYNCSTATUS, SYNCirgendwas, um Lags / Desyncs zu korrigieren. Es scheitert eben "nur" an der technischen Hürde und meiner Unerfahrenheit mit Indy bzw. generell des für mich neuen Sektors der Netzwerkkommunikation. Und ich denke auf technische Hürden trifft man auch, wenn das Protokoll vorher theoretisch klar ist, man aber noch nie mit der entsprechenden Schnittstelle gerarbeitet hat. Oder? Aber der Tipp mit verschiedenen Kanälen hat mir geholfen. Hab mich diesbezüglich mal belesen und ein paar gut verständliche Beispiele gefunden, zB ![]() |
AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
Bist du dir sicher, dass "TMessageHandler.Run;"
überschrieben werden muss, anstelle "TMessageHandler.Execute"? |
AW: [Indy 10] TCP Server-Client: Sichere Kommunikation für ein Spiel
Ja, nachdem ich durch das "Multiple Ports"-Beispiel durchgestiegen bin, musste ich feststellen, dass ich das Pferd hier ohnehin von hinten aufgezäumt habe. :D
Hab den ganzen Code inzwischen verworfen und an Hand dieses Beispiels das nun so neu geschrieben, dass es erstens Indy- und Thread-Konform ist, zweitens Sinn macht und drittens auch funktioniert. Hab's noch nicht in's Spiel integriert, erstmal nur 'ne Anwendung für "Trockenübungen" und Stresstests. Läuft gut. :thumb: |
Alle Zeitangaben in WEZ +1. Es ist jetzt 06:33 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz