Entwickler-Ecke

Internet / Netzwerk - Dauer-Datenpaket über TCP


IhopeonlyReader - Do 11.07.13 22:00
Titel: Dauer-Datenpaket über TCP
guten tag,
http://www.entwickler-ecke.de/viewtopic.php?t=111794
Stream oder String?


Mr_Emre_D - Fr 12.07.13 10:04

IhopeonlyReader hat folgendes geschrieben:


Delphi-Quelltext
1:
2:
ZuSendenderText := IntToStr( Zahl ) + #13#10 + IntToStr(Punkt.X) + #13#10 + IntToStr( Punkt.Y ) 
#13#10+ Zeichen +#13#10 +  IntToStr( UndRest ) + #13#10 + Text;



Kommt ganz drauf an - wenn es um Geschwindigkeit geht - als Stream, sonst String.
Warum?
IntToStr(Zahl) wandelt die Zahl in einen String.
Dieser String kann eine maximale Länge von 11 ("-2147483648") haben (Integer = [-2^31,2^31]).
Wir nehmen den schlimmsten Fall an:
Du schickst nun entweder 11 Bytes (als String - String besteht aus (Ansi)Chars wobei ein AnsiChar = 1 Byte)
oder nur 4 Bytes als Integer / Stream...

Ich habe bisher solche Sachen nie über String gelöst! Es ist jedoch nicht unüblich, dass Spiele das auch mal über Strings lösen!
Beispiel: Call of Duty 1.
(Du kannst gerne mal die Pakete analysieren! Sind Strings - sofern ich das richtig in Erinnerung habe).

Edit:
Im anderen Thread gibts du ein Beispiel für das Versenden über Streams an - das ist aber so nicht korrekt!
Du kannst keine dynamischen Typen so verschicken (dyn. im Sinne von - Größe nicht bekannt --> Strings)
Wenn du einen beliebigen String verschicken willst, so musst du eine Länge davor packen:
<LängeVonA><StringA>

Das ganze ginge, wenn es sich um einen Shortstring handelt:
Str: String[10]; // bei der ist die Länge konstant (siehe SizeOf())
Im Speicher befindet sich eine Byte-Zahl, die angibt, wie lang so ein String ist:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
var
  a: string[10];

begin
  a := 'A1234';
  writeln('Laenge: ', pByte(@a)^, ' - erstes Zeichen: ', pAnsiChar(integer(@a) + 1)^);
  writeln(SizeOf(A));
  a := 'B234512345';
  writeln('Laenge: ', pByte(@a)^, ' - erstes Zeichen: ', pAnsiChar(integer(@a) + 1)^);
  writeln(SizeOf(A));


IhopeonlyReader - Sa 13.07.13 23:17

ok, aber wie soll ich die länge anhängen?
mit sizeof() bestimmen und das alt 32-bit variable anhängen?


Mr_Emre_D - So 14.07.13 07:09

Joa, muss aber ned eine 32 Bit Zahl sein - mit 32 bit kannste 2^32 Zahlen darstellen, du wirst aber wohl kaum einen 2^32 Zeichen langen String verschicken..
Ich schätz, dass da ein Word (2^16 - 65536) auch aussreicht. Du könntest noch weiter runtergehen, wenn du weißt, dass z.B. der String nicht länger sein kann. Damit sparst du Bandbreite, was bei Spielen, die Pakete "spammen" sehr wichtig ist.


IhopeonlyReader - So 14.07.13 16:20

mhh... aber dann müsste das doch an den Anfang vom Stream oder?
Weil ein Stream wird normalerweise ja partweise geschickt.. oder?

also sozusagen wird aus
0110 0110 0001 0001 1010 0101
0000 0000 0000 0000 0000 0000 0000 0011 0110 0110 0001 0001 1010 0101

oder? dann muss 0011 (3 Byte) + 4 Byte von der SizeÜbergabe addiert werden und dann hat man die Gesamtgröße des streams..
Wenn man sich aber auch nur ein bisschen dabei vertut, liest mal die falschen werte als Größe aus, und das hätte böse Konsequenzen

Somit müsste ich:
- Die Größe SICHER am Anfang übergeben
Nachteil: Jedes packet (Datenpaket) wäre größer
- Jedes Datenpacket gleichgroß halten (wenn es nur Koordinaten (also Integer Variablen) sind, dann wären sie ja wirklich immer konstant)
Nachteil: Strings und Arrays müssen "eingeschränkt" oder auf Server und Client parallel laufen


Mr_Emre_D - Mo 15.07.13 10:19

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
mhh... aber dann müsste das doch an den Anfang vom Stream oder?
Weil ein Stream wird normalerweise ja partweise geschickt.. oder?

also sozusagen wird aus
0110 0110 0001 0001 1010 0101
0000 0000 0000 0000 0000 0000 0000 0011 0110 0110 0001 0001 1010 0101

oder? dann muss 0011 (3 Byte) + 4 Byte von der SizeÜbergabe addiert werden und dann hat man die Gesamtgröße des streams..
Wenn man sich aber auch nur ein bisschen dabei vertut, liest mal die falschen werte als Größe aus, und das hätte böse Konsequenzen


Ja, das stimmt so ca. schon, jedoch musst du nicht als Größenangabe eine 32 Bit Zahl nehmen, wie bereits zuvor geschildert!
(0000 0000 0000 0000 0000 0000 0000 0011 = 4 * 8 = 32 Bits)

Du kannst statt 4, auch 1 (=Byte statt DWORD) z.b. nehmen, WENN du weißt, dass die Daten, die folgen, sowieso nie größer 2^(1*8 ) sein werden können!

Eine Größenangabe muss nicht an den Anfang des Streams sondern vor einem dynamischen Typ:
Beispiel, du willst folgendes Record verschicken:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
TMyRec = packed record
  SomeNr: Integer;
  SomeOtherNr: Single;
  FirstDynamicField: Array of Byte;
  SecondDynamicField: String;
end;

Der Stream dazu würde wie folgt aussehen:

Quelltext
1:
2:
3:
4:
5:
6:
<SomeNr|4 Bytes>
<SomeOtherNr|4 Bytes>
<Length(FirstDynamicField)|2 Bytes>
<FirstDynamicField|x Bytes>
<Length(SecondDynamicField)|2 Bytes>
<SecondDynamicField|y Bytes>


Beschreibung der Zeilen:
1. Integer ist 4 Byte (4*8 Bits) groß
2. Single (Fließkommatyp) auch 4 Byte
3. Längenangabe über dyn. Array - hier habe ich 2 Byte verwendet, kommt aber drauf an - wenn das Array größer sein kann als 2^(2 * 8 ) = 2^16 = 65536, dann müsste man das vergrößern
4. FirstDynamicField - hier stehen nun x Bytes
5. ~ #3
6. ~ #4

Erst dann, wenn du das so machst, kann der Empfänger die Daten auch richtig "aufnehmen".
Er kriegt nämlich einen Stream mit Nullen udn Einsen (Zahlen, Bytes eig.)
Er weiß, dass die ersten zwei Felder eine konstante Größe haben, da kann er direkt 2 * 4 Bytes auslesen und in seinen Record speichern.
Bei den dyn. Feldern muss er nun wissen, wie groß die sind, damit er zuerst die Größe des Arrays/Strings setzen und dann die Daten "aufnehmen" kann.

Das Datenpaket ist nur geringfügig größer!

Zitat:

Wenn man sich aber auch nur ein bisschen dabei vertut, liest mal die falschen werte als Größe aus, und das hätte böse Konsequenzen

Jup, du darfst dich nicht vertun.


IhopeonlyReader - Mo 15.07.13 15:04

ok, du verwendest packed record, warum kein normales record?
Den Aufbau versteh ich jetzt, DANKE :)

Nun überlege ich, dass ganze noch "optional" zu trennen, da ich z.B. am Anfang die Map übermittle, falls sie nicht vorhanden ist, später Spielerpositionen austausche, und ggf. neue Spieler hinzufüge, muss ich natürlich jeden Stream "erkenntlich" machen...
ich würde also IMMER eine bestimme 8-Bitfolge als "Erkennung" nehmen...

Vom Server zum Client:
0000 0001 (Map kontrolle)
0000 0101 (Länge des Strings (Name der Map) =5 Zeichen = 5*8Bit = 5 Byte)
0100 0001 ('A') 0100 0010 ('B') 0100 0100 ('C') 0000 0001 (' ') 0010 0001 ('!') (= 'ABC !') (String an sich)
0000 0100 0101 0111 (=1111) (Dateigröße zur Kontrolle ob richtige Map mit richtigem Namen vorhanden)

Als Antwort vom Client:
1000 0001 (Antwort: Mapkontrolle)
0000 0001 (True (vorhanden))

Server weiß nun bescheid und antwortet:
0000 0010 (Spieler namen)
0000 0010 (Spieleranzahl (2)(Empfänge zählt mit))
0000 0001 (Länge des Spielernamens Nr 1 -> 1 Zeichen -> 1 Byte)
0100 0001 (Spielername 'A')
0000 0001 (Team 1)
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0001 (Position X [Integer] = 17)
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 (Position Y [Integer] = 256)
0000 0001 (Länge des Spielernamens Nr 1 -> 1 Zeichen -> 1 Byte)
0100 0010 (Spielername 'B')
0000 0010 (Team 2)
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 (Position X [Integer] = 1)
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 (Position Y [Integer] = 1)


und so weiter...

Ich würde also immer einen "flag" (ist das richtig?) am Anfang setzten (1 Byte), dieser besteht immer aus den selben Bestandteilen,
der Positionsaustausch z.B. Spielernamen als Flag, Spieleranzahl, und dann pro Spieler -> Länge des Namens, Array of Char (String), Team, Position (sollte ich ggf. als SmallInt und nicht als Integer wie normale TPoint Koordinaten behandeln)

Wäre das ein "gutes" Komunikationsverhältnis zwischen Server und Client?
Du kann man auch einzelne Bits in einen Stream einfügen (vorallem bei Booleans hilfreich)?, wenn ja wie?


Gerd Kayser - Mo 15.07.13 15:22

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
ok, du verwendest packed record, warum kein normales record?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
type TMyRec1 = record
  a : Word;
  b : integer;
end;

type TMyRec2 = packed record
  a : Word;
  b : integer;
end;

[ ... ]

procedure TForm1.Button2Click(Sender: TObject);
var
  X : TMyRec1;
  Y : TMyRec2;
begin
  ShowMessage(IntToStr(SizeOf(X)));
  ShowMessage(IntToStr(SizeOf(Y)));
end;


Hängt mit der Ausrichtung der Variablen im Speicher zusammen (Ausrichtung an Adressen, die durch 4 teilbar sind).


IhopeonlyReader - Mo 15.07.13 15:41

Ein Packet record besitzt also die Größe aller beinhalteten Variablen und ein "normales" record die nächstgrößere ByteAnzahl, die durch 4 teilbar ist.
Danke für die Info, wusste ich nicht :oops:


OlafSt - Mo 15.07.13 15:43

Ich glaub, den Cursor auf "packed" zu stellen und F1 zu drücken hätte dir das peinliche Erlebnis erspart ;)


IhopeonlyReader - Mo 15.07.13 15:47

Nein :D ich weiß nicht warum, aber die PE von Delphi 7, die ich besitzte kann das nicht unter F1..
Mit Strg+Leertaste kann ich immerhin die "möglichen" Proceduren/Variablen.. ansehen, aber eine "eingebaute"-Delphi-Hilfe habe ich bei mir noch nicht gefunden


Gerd Kayser - Mo 15.07.13 17:30

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
ein "normales" record die nächstgrößere ByteAnzahl, die durch 4 teilbar ist.

Nein, nicht ganz korrekt. Die Ausrichtung findet ggf. bei jeder Variablen im Record statt.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
type TMyRec = record
  a : word;
  b : word;
  c : word;
  d : int64;
end;

SizeOf gibt hier 16 zurück. Bei einem packed Record sind es hingegen nur 14. Hängt immer damit zusammen, wie der Record aufgebaut ist.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
type TMyRec = record
  a : word;
  b : word;
  d : int64;
  c : word;
end;

Hier sind es sogar 24.


Mr_Emre_D - Mo 15.07.13 20:34

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
ok, du verwendest packed record, warum kein normales record?
Den Aufbau versteh ich jetzt, DANKE :)

Nun überlege ich, dass ganze noch "optional" zu trennen, da ich z.B. am Anfang die Map übermittle, falls sie nicht vorhanden ist, später Spielerpositionen austausche, und ggf. neue Spieler hinzufüge, muss ich natürlich jeden Stream "erkenntlich" machen...
ich würde also IMMER eine bestimme 8-Bitfolge als "Erkennung" nehmen...

*SNIP*

und so weiter...

Ich würde also immer einen "flag" (ist das richtig?) am Anfang setzten (1 Byte), dieser besteht immer aus den selben Bestandteilen,
der Positionsaustausch z.B. Spielernamen als Flag, Spieleranzahl, und dann pro Spieler -> Länge des Namens, Array of Char (String), Team, Position (sollte ich ggf. als SmallInt und nicht als Integer wie normale TPoint Koordinaten behandeln)

Wäre das ein "gutes" Komunikationsverhältnis zwischen Server und Client?
Du kann man auch einzelne Bits in einen Stream einfügen (vorallem bei Booleans hilfreich)?, wenn ja wie?


Ja, man nennt diese Werte/Variablen auch Paket-IDs
Im Grunde wirst du sehr viele Identifiers im Laufe der Zeit unterbringen, da es verschiedenste Arten von Requests/Responses zwischen Client <-> Server geben wird:
Spiel wird gestartet,
Spieler Position geändert (passiv - externer Event, aktiv - eigener Event)
ein Objekt wurde modifiziert (wie?),
Spiel terminiert (game over?!)

Das sind alles individuelle Pakete, die ihre unique ids haben müssen, womit dann die Kommunikation zwischen Client und Server funktionieren kann!


IhopeonlyReader - Mo 15.07.13 21:48

ok, vielen dank, somit wäre das jetzt "abgehakt"..

Weiter zum Thema Dauer-Senden habe ich noch die frage:
- Einzelne Bits anhängbar?
Soviel ich weiß, kann man nur Bytes anhängen, man könnte also mehrere Bits zusammenfassen, aber direkt nur 1 Bit anhängen geht nicht.... -> richtig?

- In welchen Abständen sollte soetwas gesendet werden (CLIENT)?
- jede Änderung (lags?!)
- alle x millisek
- nach jedem x-ten durchlauf

- In welchen Abständen sollte der Server etwas senden?
- bei jeder angekommen Änderung
- wenn jeder Spieler einmal gesendet hat
- alle x millisek

oder: wie kann der Server sozusagen auf "abruf" immer den aktuellsten Stream bereit haben.. das heißt, Client A sendet und fragt danach direkt den Stream ab.
Wenn das so aussäh
Client A sendet, Client A will "empfangen", Server sendet, Client B sendet, Server sendet, Client B will "empfangen"
Dann hätte Client B ja 2 Streams hintereinander weg zu bearbeiten oder? wie kann ich soetwas vermeiden?

Nachtrag: Sollte ich eine extra "schick mir Infos"-Packet-ID einrichten? also Client A schickt diese ID und Server schickt nur zu Client A....?


jaenicke - Mo 15.07.13 22:35

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
Nein :D ich weiß nicht warum, aber die PE von Delphi 7, die ich besitzte kann das nicht unter F1..
Du hast vermutlich WinHelp nicht installiert:
http://www.delphi-library.de/viewtopic.php?p=544403
(Das gibt es auch für Windows 8:
http://www.microsoft.com/de-de/download/details.aspx?id=35449)


Mr_Emre_D - Mo 15.07.13 22:58

Zitat:
ok, vielen dank, somit wäre das jetzt "abgehakt"..

Weiter zum Thema Dauer-Senden habe ich noch die frage:
- Einzelne Bits anhängbar?
Soviel ich weiß, kann man nur Bytes anhängen, man könnte also mehrere Bits zusammenfassen, aber direkt nur 1 Bit anhängen geht nicht.... -> richtig?

Zu granular! Arbeite lieber im Byte Bereich (und höher). Wenn es sonst unbedingt sein muss, dann seh dich um nach Bit-Operationen:
SHL, SHR, AND, OR, NOT
Bits kannst du aber nicht einem Stream hinzufügen, da die kleinste Einheit ein Byte ist - du kannst immer Vielfache von 8 Bits haben, diese kannste du aber beliebig modifizieren..

Zitat:

- In welchen Abständen sollte soetwas gesendet werden (CLIENT)?
- jede Änderung (lags?!)
- alle x millisek
- nach jedem x-ten durchlauf

- In welchen Abständen sollte der Server etwas senden?
- bei jeder angekommen Änderung
- wenn jeder Spieler einmal gesendet hat
- alle x millisek


Um was für eine Art von Spiel handelt es sich? Es sind nämlich Welten zwischen verschiedenen Spiel-Typen - vergleich mal ein FPS Game mit einem Brettspiel!
Sofern es mehr in Richtung FPS geht (also sofern die Objekte zeitecht sich verändern), dann würd ich mit ca. 10-15 Paketen/sec (also 1000/15 = 66mSec Abstände) arbeiten.
10 ist ca die Hälfte von 24 (24 ist so ca die Anzahl der Bilder, die das menschliche Auge pro Sekunde unterscheiden/aufnehmen/sehen kann).
Ich habe irgendwo mal aufgeschnappt, dass man das so machen soll und den Rest einfach interpolieren soll! (ganz widersprüchlich zum Nyquist-Theorem)
Spiel dich rum und schau, welche Werte am besten passen.

Zitat:

oder: wie kann der Server sozusagen auf "abruf" immer den aktuellsten Stream bereit haben.. das heißt, Client A sendet und fragt danach direkt den Stream ab.
Wenn das so aussäh
Client A sendet, Client A will "empfangen", Server sendet, Client B sendet, Server sendet, Client B will "empfangen"
Dann hätte Client B ja 2 Streams hintereinander weg zu bearbeiten oder? wie kann ich soetwas vermeiden?

Nachtrag: Sollte ich eine extra "schick mir Infos"-Packet-ID einrichten? also Client A schickt diese ID und Server schickt nur zu Client A....?

Aktuellsten Stream? Meinst du den aktuellen Zustand aller Clients?
Ein gut design'tes Spiel hat sowieso die meiste Logik auf der Serverseite - dh. die ganzen Daten sind dem Server bekannt; braucht Client A die Positiondaten von allen anderen (Spiel neu gejoint z.B.),
so kann das der Server ganz einfach bereitstellen!
Ich schätze mal, du hast es andersrum - unterlass das, solche Spiele sind am einfachsten zu "hacken"..


Mr_Emre_D - Mo 15.07.13 23:28

Hier, dürfte sich für Unerfahrene als nützlich erweisen...

[OT]
Kann man den sich wiederholenden Teil eigentlich eleganter lösen? (Generics?)
[/OT]


jaenicke - Di 16.07.13 07:24

user profile iconMr_Emre_D hat folgendes geschrieben Zum zitierten Posting springen:
[OT]
Kann man den sich wiederholenden Teil eigentlich eleganter lösen? (Generics?)
[/OT]

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
  TBitSet<T, K: record> = record
  private
    function GetBit(const AIndex: Integer): Boolean;
    procedure SetBit(const AIndex: Integer; const AValue: Boolean);
  public
    class operator implicit(const AValue: T): TBitSet<T, K>;
    class operator implicit(const AValue: TBitSet<T, K>): T;
    property Bit[const AIndex: Integer]: Boolean read GetBit write SetBit; default;
    case Boolean of
      True:   (Value: T);
      False:  (Lo: K; Hi: K);
  end;

  TWordBitSet = TBitSet<Word, Byte>;
  TLongIntBitSet = TBitSet<LongInt, TWordBitSet>;

// ...
function TBitSet<T, K>.GetBit(const AIndex: Integer): Boolean;
begin
  Result := PInteger(@Value)^ or (1 shl AIndex) > 0;
end;

class operator TBitSet<T, K>.implicit(const AValue: T): TBitSet<T, K>;
begin
  Result.Value := AValue;
end;

class operator TBitSet<T, K>.implicit(const AValue: TBitSet<T, K>): T;
begin
  Result := AValue.Value;
end;

procedure TBitSet<T, K>.SetBit(const AIndex: Integer; const AValue: Boolean);
var
  ByteIndex: Integer;
  BytePointer: PByte;
begin
  ByteIndex := AIndex div 8;
  if SizeOf(T) > ByteIndex then
  begin
    BytePointer := @PByteArray(@Value)^[ByteIndex];
    if AValue then
      BytePointer^ := BytePointer^ or (1 shl (AIndex mod 8))
    else
      BytePointer^ := BytePointer^ and not (1 shl (AIndex mod 8));
  end
  else
    raise Exception.Create('Invalid bit index!');
end;
Beispiel:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
var
  a: TLongIntBitSet;
begin
  a := $12345678;
  ShowMessage('$' + IntToHex(a.Lo.Hi, 8)); // $00000056
  a.Bit[12] := False;
  ShowMessage('$' + IntToHex(a.Lo.Hi, 8)); // $00000046
Effizienter ist aber eine andere Umsetzung...


IhopeonlyReader - Di 16.07.13 11:01

ich habe auch mal eine "Bit-Boolean"-Unit erstellt (basierend auf Byte und eine auf 32-bit), diese würden ebenfalls dafür geeinget sein oder?...


Und zu der Spielart:
Zur Übung verwende ich 2 Arten
- Vier gewinnt -> denke ich mal sollte nach jeder Änderung an alle geschickt werden
- 2D Egoshooter -> sollte entweder auf Abfrage oder in einem 66ms-Interval

was ist sinnvoller? Abfrage oder Interval


Mr_Emre_D - Di 16.07.13 11:25

Zitat:

was ist sinnvoller? Abfrage oder Interval


Zum 2d-Shooter:
Warum nicht..? [http://1.bp.blogspot.com/-Lo9wZsPUiU0/UN6Uc84lyLI/AAAAAAAADgU/Ieq0YTNS334/s320/why-not-both.jpg]

Also du schickst im 66ms Interval die Pakete, sofern etwas verlangt wird, sonst niks (damit beseitigst du unnötigen traffic)
Am einfachsten ne Queue verwenden, wo du die zu abschickenden Pakete draufpusht, und im 66ms Interval abschickst!~


IhopeonlyReader - Di 16.07.13 12:01

user profile iconMr_Emre_D hat folgendes geschrieben Zum zitierten Posting springen:

Zum 2d-Shooter:
Warum nicht..? [http://1.bp.blogspot.com/-Lo9wZsPUiU0/UN6Uc84lyLI/AAAAAAAADgU/Ieq0YTNS334/s320/why-not-both.jpg]

wie beides?
Entweder Client schickt ID, Server schickt verlangte Daten
oder Server schickt Daten alle x Millisek

ODER Auf Abfrage und falls Client sich x-Sek nicht gemeldet hat auf Connection prüfen


IhopeonlyReader - Do 18.07.13 19:25

Ich hätte da dann noch die Frage:

Wann soll ich "wie" abfragen und senden?

(Client (Server weiß ich inzwischn))
Beim Client hatte ich gedacht, dass z.B. auf "w" an den Server "nach oben bewegen" gesendet wird, der Server verarbeitet dass dann.. auf abfrage sieht man dann ja die Änderung, dies würde dann zu laggs führen oder?

Wenn ich z.B. ALLES in die "onread"-procedure packe, also am ende der onread procedure ein "neues packet anfordere" dann könnte ein verlorenes Packet zu kommunikationsstillstand führen...
Wie soll ich also "regelmäßig" befehle anfordern, ohne ein Overstack oder sonstiges zu erreichen, also warten bis ein Datenpaket angekommen ist


Mr_Emre_D - Do 18.07.13 20:14

Zunächst einmal hat, sofern du es benutzt, TCP Sicherheitsmechanismen eingebaut, damit Pakete nicht verloren gehen.
Das Einzige, was geschehen kann, ist, dass der Empfangs-Stack überläuft - dann aber gibt der send() Befehl weniger zurück, was bedeutet, dass nicht alles geschickt werden konnte! Es ist ja ein Streaming-Protokoll.

Weiters würde ich das Empfangen vom Verarbeiten der Daten trennen. Du empfängst den Datenstrom, haust es synchronisiert auf ne Queue oder was auch immer für ne Datenstruktur. Ein anderer Thread verarbeitet dann diese Queue. Damit sorgst du, dass der Receiving-Thread nicht allzu sehr stockt.

Achja, wie zuvor schon gesagt - Pakete gehen nicht verloren, jedoch, so wie ich mal hier und da beschrieben habe, kommen Pakete nicht genauso an, wie du sie abschickst!
Sprich, wenn du 100 bytes abschickst (und dir der send() Befehl auch 100 zurückliefert, was bedeutet, dass wirklich 100 bytes abgeschickt worden sind), dann kann es passieren, dass du per recv() 100 bytes empfängst oder auch z.B. 2x50er oder 1x50, 2x25er oder 10*10er Pakete, oder oder oder... das wirste übrigens lokal selten feststellen bemerken; hat mich damals einige Nerven gekostet, bis ich das rausgefunden habe!
Beachte das bitte!


IhopeonlyReader - Do 18.07.13 20:28

user profile iconMr_Emre_D hat folgendes geschrieben Zum zitierten Posting springen:

Sprich, wenn du 100 bytes abschickst (und dir der send() Befehl auch 100 zurückliefert, was bedeutet, dass wirklich 100 bytes abgeschickt worden sind), dann kann es passieren, dass du per recv() 100 bytes empfängst oder auch z.B. 2x50er oder 1x50, 2x25er oder 10*10er Pakete, oder oder oder... das wirste übrigens lokal selten feststellen bemerken; hat mich damals einige Nerven gekostet, bis ich das rausgefunden habe!
Beachte das bitte!

genau, deshalb wollte ich abfragen, wann das paket vollständig ist und es dann verarbeiten und dann neu "abfragen"...
das ganze in threads zu trennen halte ich ebenfalls für sinnvoller... aber ich überlege, ob es nicht sehr "laggy" ist, wenn eigentlich NICHTS über den Client läuft...
im Client stehen zum spiel dann eigentlich nur pro taste 1 befehl
z.B.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
var pStream: TMemoryStream;
begin
pStream := TMemoryStream.create;

if GetASyncKeyState( Ord('W') )>0 then pStream.Write( WillSpringen, 1); //der einzeiler !

try
  Client.Socket.SendStream( pStream );
except
  if Assigned( pStream ) then pStream.Free;
  end;
end;

der Server würde dieses "springen" dann umsetzten und bei der näcjhsten Abfrage entsprechend die neue Position liefern.. nur wenn man das umsetzt, wirkt es als wenn das Spiel immer "nachzieht"..
bis jetzt hat der Client keine Ahnung zu welchem Spieler er eigentlich gehört.. er sendet nur wohin "sein" spieler sich bewegen soll....
Der Server ordnet dem Socket (Client/Spieler) dann entsprechend die Daten zu...

Dadurch will ich das ganze "hacksicherER" machen...


Mr_Emre_D - Do 18.07.13 21:12

In der Praxis ist es afaik so, dass, wenn du w drückst und du den Befehl an den Server schickst, du deine Positionsupdates weiterhin machen darfst und sollst, damit
dieser Lag nicht entsteht.
Der Server wrid dir früher oder später (im ms Bereich) antworten und dann gleichst du ab, bzw setzt die Position neu, sofern sie zu sehr von der Antwort abweicht!

Btw dein Code ist voller Fehler:
1. du erstellst ein Stream, den du nur bei Fehler (except) freigibst -> Speicherleck
2. du erstellst IMMER ein Stream, auch wenn nichts zu schicken ist
3. du schickst die Daten einfach so, ohne auf meine Nachricht zuvor Rücksicht zu nehmen! (Edit: Ok, kann sein, dass das kein Problem ist, da evt. SendStream() das intern berücksichtigt; ich habe da mit eher Low-Level Komponenten gearbeitet)


IhopeonlyReader - Fr 19.07.13 11:05

1. SendStream gibt den Stream nach senden soviel ich weiß frei!, somit muss ich nur, falls das nicht geklappt hat den stream freigeben
2. Es wird sonst ein "ja, ich bin noch anwesend und lagge nicht"-Stream übermittelt... hierdurch kann die Ping über den Server errechnet werden und es gibt einen "anwesenheitsnachweis" (ja ok, aus meinem Beispielcode geht das nicht hervor)
3. Du meinst, dass man das in kleinere Packete (JA mit C... packtet = Datenpaket) schickt? darum kümmert sich SendStream ;)

p.S.: ich werde mit den Standart-Delphi- Internet-Komponenten ( TClientSocket und TServerSocket ) arbeiten ;)


IhopeonlyReader - Fr 19.07.13 14:22

user profile iconMr_Emre_D hat folgendes geschrieben Zum zitierten Posting springen:
In der Praxis ist es afaik so, dass, wenn du w drückst und du den Befehl an den Server schickst, du deine Positionsupdates weiterhin machen darfst und sollst, damit dieser Lag nicht entsteht.
Der Server wrid dir früher oder später (im ms Bereich) antworten und dann gleichst du ab, bzw setzt die Position neu, sofern sie zu sehr von der Antwort abweicht!


ok, dass heißt der Client muss wissen welche Position zu ihm gehört... somit muss "im" Server noch einiges an "hack-verteididung" eingebaut werden... wie z.B. Zeit-Check (sendet ein Client zu schnell ?, wenn ja dann kick because hacking


jaenicke - Fr 19.07.13 16:16

Der Server muss einfach der Master sein. Der Client sendet Daten an den Server (z.B. Stoß mit Stärke XY in Richtung XY) und zeichnet selbst als ob das so in Ordnung wäre weiter. Der Server prüft diese Daten auf Plausibilität und schickt die eigenen berechneten Positionen zum Client. Der Client prüft nun, ob diese von der Anzeige zu sehr abweichen, wenn ja, korrigiert er die eigene Anzeige mit den Serverdaten.


IhopeonlyReader - Sa 20.07.13 15:35

das hieße,
1. Der Client muss wissen welche Positionsdaten zu ihm gehören ! (ok...)
2. Also: Client sendet vom ihm veränderten positionsdaten an Server... UND setzt die bei sich schon
3. Server prüft auf Richtigkeit, übernimmt/ passt sie an
4. Server schickt an Client.. Client überprüft ob eigene "schon neu veränderte pos" stimmen kann

aber wie soll ich das mit den anderen Clients machen?
soll ich die Laufrichtung dann mit übertragen, damit jeder Client es für sich "errschäten" kann?
und z.B. beim egoshooter, wie soll man da das "treffen" veranstalten.. denn
0.1 Jeder Client hätte unterschiedliche Pos-Daten vom Getroffenen.. (aber nahe beinander)
0.2 Wenn der Client einfach "Person X getroffen" an Server schickt, wäre es zu leicht zu hacken...
0.3 Wenn die Clients nur die vom Server zur verfügung gestellten Daten der anderen User nutz und nicht abändert
hat dies
0.3.1 Lags
0.3.2 fehler
zur folge..
fehler insofern, dass man auf Person x schießen kann, weil die pos auf dem Server noch nicht geupdatet wurde...

Das Senden/empfangen habe ich verstanden, aber die grundzuüge was Server, was Client macht muss ich noch verstehen.. soll ich dafür einen Extra thread machen?


jaenicke - Sa 20.07.13 16:00

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
1. Der Client muss wissen welche Positionsdaten zu ihm gehören ! (ok...)
2. Also: Client sendet vom ihm veränderten positionsdaten an Server... UND setzt die bei sich schon
3. Server prüft auf Richtigkeit, übernimmt/ passt sie an
4. Server schickt an Client.. Client überprüft ob eigene "schon neu veränderte pos" stimmen kann
Richtig, wobei weniger eine einzelne Position interessant ist als vielmehr die geplante Bewegung. Denn die kann beim Client dann ablaufen, der Server berechnet das gleiche und im Idealfall kommt das gleiche heraus, wenn vom Server die Antwort kommt.
Der Server schickt ja auch keine Einzelpositionen von bewegenden Zielen, sondern deren Bewegungsdaten. Die Positionen einzeln zu schicken wäre viel zu aufwendig und würde stark laggen.

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
aber wie soll ich das mit den anderen Clients machen?
soll ich die Laufrichtung dann mit übertragen, damit jeder Client es für sich "errschäten" kann?
Der andere Client schickt das ja zuerst an den Server und der weiter. Die Berechnung sollte nicht lange dauert. Das heißt es bringt keinen großen Vorteil, wenn die Clientdaten ungeprüft zu anderen Clients geschickt werden, kann aber gravierende Nachteile bringen, wenn jemand cheaten will. Deshalb ist das vermutlich weniger sinnvoll.


IhopeonlyReader - Sa 20.07.13 18:36

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:
Der andere Client schickt das ja zuerst an den Server und der weiter. Die Berechnung sollte nicht lange dauert. Das heißt es bringt keinen großen Vorteil, wenn die Clientdaten ungeprüft zu anderen Clients geschickt werden, kann aber gravierende Nachteile bringen, wenn jemand cheaten will. Deshalb ist das vermutlich weniger sinnvoll.


nein ich meine
Client A ---(Position + Bewegungsrichtung) --> Server
Client A setzt Position von sich
Server verarbeitet alle angekommen Informationen (prüft auf Richtigkeit etc...)
Client B fragt alle Daten ab
Server weiß, wann letzte Abfrage von Client B war, und schickt nur Veränderungen !
Server ---(Position + Bewegungsrichtung von A) ---> Client B
Client B setzt Spieler A auf richtige Position und lässt ihn "weiterlaufen"

würde Client A wenn er z.B. immer abwechselnd nach links und nach rechts geht, vbei sich zwar laufen, aber beim Server kaum Änderungen eingehen und beim Client B würde Spieler A nach links gehen, nach abfrage umgesetzt werden, wieder zu einer seite gehen, wieder zurückgesetzt werden... ?

oder ist das ganze in so einem kurzen "abstand" (abfrage, senden), dass man wirklich mitkriegt, dass dauerhaft links/rechts gegangen wird?



Nebeninformation: ich habe noch nicht mit der Umsetzung angefangen ! ich wollte mir in diesem Thread eine genaue Vorstellung machen, damit ich beim programmieren nicht überlegen muss wie ich das mache, sondern direkt mit einer Qualitativ halbwegs hohen Variante anfange.. geht nicht nur schneller, sondern ist oft auch "besser"


Mr_Emre_D - Sa 20.07.13 22:08

Ich würd ab dem 4. Punkt ein bisschen anders vorgehen:

wenn sich ein neuer Client verbindet, dann kriegt er sofort die notwendigen Daten, die er dann darstellen muss (Spielerposition usw)

der Client "fragt" nicht nach Daten; der Server leitet sie immer dann weiter, wenn notwendig - sprich wenn Client A seine Spielerposition geändert hat, so teilt er das dem Server mit; daraufhin teilt der Server allen anderen das mit, somit sind dann alle ca auf demselben Stand!

Edit: Um was für ein Spiel handelt es sich denn? (Nicht Genre; das haste mir schon bereits verraten)


IhopeonlyReader - So 21.07.13 00:59

Naja, wie ich bereits erwähnte, habe ich noch nicht angefangen..
Ich werde eine Karte haben, die aus 16x16 Pixel großen Feldern besteht...
Diese wird am Anfang "überprüft" etc.. Ggf. Gedownloadet.. ( warscheinlich auf einem extra anmelde Server? Damit es nicht laggt..)
Am anfang werden es nur boolean arrays sein, die angeben ob dad Feld betretbar ist oder nicht..
Die Spieler sind aber Feldunabhängig, das heißt sie können auch zwischen 2 Feldern stehen...
Ein Spieler hat ein aussehen (gibt paar zur Auswahl), und eine Waffe (haben ebenfalls alle dieselben zur Auswahl).. Bei betreten oder ändern werden diese entsprechend übermittelt..

Nun kann man per wasd oder angepasster Steuerung die eigene spielfigur Steuern und schießen...

Wenn du etwas anderes Wissen willst, formuliere deine Frage bitte antwortenorientierter.

Ein normales 2d Egoshooter halt :D


Mr_Emre_D - So 21.07.13 02:30

Hast du ne Geschichte, ne Idee hinter dem Spiel oder programmierst du nur um ein Spiel zu programmieren?
2d Egoshooter? Iwie passt das ja kaum zusammen - die Egoperspektive hat man ja bei den meisten 2d Spielen nicht - außer bei Moorhuhn. Wirds nun Moorhun-like?
Wird es evt. top-down oder eher sidescrolling?...

PS: Du stellst dir viel zu viele unnötige Fragen. Fang erst einmal an, das meiste ergibt sich sowieso. Und wenn du mal stecken bleibst, fragste eben konkreter nach. Das meiste ist eig. schon abgedeckt; technisch wars ja bisher auch kaum.

Gn8


IhopeonlyReader - So 21.07.13 11:57

Ups.. Ich dachte Egoshooter wäre der Oberbegriff der -Person-shooter.
Wie ich gerade nachgelesen habe, ist Egoshooter=First-Person-shooter...
Mein Spiel wird allerdings ein third- Person shooter..
Tut mir leid um die Verwirrung


Und ja vom technischen habe ich alles verstanden überlege nur, was gesendet werden muss...


IhopeonlyReader - Sa 10.08.13 16:54

So... nach vielem Planen nur noch eine offene Frage :D
Wenn der Server in mehreren Threads arbeitet
- Empfang-Thread der alles auf eine QueQue haut
- Sende-Thread der die "aktuellen" Daten sendet...

Nur 1. Wenn ich 2 threads haben die senden/empfangen, dann wird es sicherlich fehler geben, da ein Server das sicherlich nicht gleichzeitig kann oder?
2. Gibt es dann ja keine Thread-Proceduren (die per message aufgerufen werden) sondern ich müsste irgendwie im Execute ne dauerschleife haben, die das von Person zu Person abfragt oder? und wie ist das mit Person-disconnections? müsste dann ja so aussehen

Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
while not Terminated do
begin
for C:=1 to AnzahlSpieler do //nicht AcitveConnections-1 sonst bemerkt man disconnections nicht
 begin
 //hat spieler was gesendet
 //wenn ja empfangen (evt Extra thread erstellen der das empfängt?)
 //also pro Client 1 thread der empfängt?? 
 end;
end;

nur habe ich noch nie mit senden/empfangen in extra threads gearbeitet.. kann man jemand helfen oder links/tutorials dazu posten?


IhopeonlyReader - So 11.08.13 18:56

push


IhopeonlyReader - Mo 12.08.13 17:13

Multihreating mit einem Server.. keiner ne Idee?


Martok - Mi 14.08.13 00:49

user profile iconIhopeonlyReader hat folgendes geschrieben Zum zitierten Posting springen:
Multihreating mit einem Server.. keiner ne Idee?
Eher zu viele Ideen ;-)
Das ganze Thema ist schon etwas kompliziert und damit man sowas ordentlich machen (du hast gesagt du möchtest bevor du etwas tust möglichst viel wissen) kann, muss man ganz schön was mitbringen. Der NetCode den wir aktuell bei BitSpace verwenden ist mittlerweile die 5. Iteration verschiedener Konzepte, die ich so über die (letzten 3-4) Jahre auf die Beine gestellt (und wieder verworfen und verbessert) habe.

Ein guter Einstieg ist immer die Tutorial-Reihe [http://www.entwickler-ecke.de/topic_60744.html] von user profile iconNarses. Wenn du die soweit verstanden hast, dass du zusammen mit TMessageThread [http://www.entwickler-ecke.de/viewtopic.php?t=90333] z.B. einen kompletten NBFPA-Server in einen Thread [http://www.entwickler-ecke.de/viewtopic.php?p=667420#667420] verlegt bekommst, hast du schonmal viel gewonnen ;) Das reicht dann für die ersten Spiele auf jeden Fall aus.

Zurück zum Threading.
Die Frage ist, wie viele Threads du überhaupt brauchst. Ein Thread der die ganze Zeit nur dumm rumsteht weil er nichts zu tun hat (oder viel schlimmer, weil er auf ein Lock wartet {Hausaufgabe: Thread-Synchronisation lesen; Extra Credit: Lock-Free data structures. Krasses Zeug, hilft wenn du wirklich große Dinge bauen willst} ) bringt ja eher wenig, andersrum kann ein monolithischer Serverthread auch nur eine begrenzte Zahl Clients bedienen, falls dort direkt Logik abgewickelt wird. Je nach erwarteter Client-Anzahl kann es sogar reichen, Server-Logik und Netzwerk im gleichen Thread zu haben - das wäre dann der einfache Fall, den ich oben als Anfangsaufgabe erwähnt hatte.

Wenn du dann noch etwas über Game-Networking im Speziellen lernen möchtest, hilft auch die Dokumentation der wundervollen (aber etwas teuren) Bibliothek RakNet [http://www.jenkinssoftware.com/raknet/manual/]. Dort wird auch sehr schön in technische Details gegangen, jedenfalls wenn man etwas Vorwissen mitbringt kann man sehr gut zwischen den Zeilen lesen was die intern tun. Nicht alles davon würde ich so auch machen (unter anderem bauen die TCP komplett in eigenem Code nach und verwenden nach außen hin UDP - etwas blödsinnig wenn du mich fragst, den gleichen Effekt würde man auch mit hinreichend kurzen Priority Queues erreichen, genau das tun wir bei BitSpace), aber viele Dinge besonders was die Integration in die Anwendung angeht sind durchaus sehr elegant gelöst (so gut wie das eine reine C-API halt kann :zwinker:)

Und wenn du das alles durch hast, sprechen wir uns wieder - oder du planst das nicht alles 100% durch und fängst einfach mal an. An Problemen lernt sich sowas mMn besser als aus Texten - du solltest nur davon ausgehen, dass dein erster Versuch nicht wirklich ideal sein wird und unter irgendwelchen Fällen (Local, LAN, WLAN, Internet, mehr oder weniger CPU-Kerne...) komplett zusammebricht. Da zeigt sich dann auch sonst die Codequalität, wenn du gut warst, kannst du den Netcode austauschen ohne den Rest einreißen zu müssen :mrgreen:

Ich hoffe mal, das hilft dir irgendwie. Man kann auf jeden Fall viel dabei lernen ;)
Viele Grüße,
Martok