Moin!
Problemstellung: Ich habe ein
record definiert und möchte dieses zur Datenübertragung im Netzwerk verwenden. Allerdings klappt es nicht, wenn bestimmte Datentypen im
record enthalten sind. Entweder kommen die Daten nicht an (bzw. nur "Müll") oder es hagelt Fehlermeldungen (Exceptions, Zugriffsverletzungen, etc.). Woran kann das liegen?
Hinweis: Ich versuche die dahinter liegende Problematik allgemein zu behandeln, allerdings am Beispiel der Socket-Komponenten (TServer-/TClientSocket). Da das Grundproblem nichts mit dem verwendeten WSA-Wrapper zu tun hat, spielt es keine Rolle, ob statt dessen die Indy-Komponenten oder noch ganz andere Komponenten zum Einsatz kommen.
Zunächst ein Code-Beispiel:
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:
| type
TMyRecord = record Name: String[30]; Alter: Integer; end;
var MyRecord: TMyRecord; i: Integer;
with MyRecord do begin Name := 'Charles Cros'; Alter := 166; end;
for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do ServerSocket1.Socket.Connections[i].SendBuf(MyRecord,SizeOf(MyRecord));
procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); begin Socket.ReceiveBuf(MyRecord,SizeOf(MyRecord)); ShowMessage(MyRecord.Name+' ist '+IntToStr(MyRecord.Alter)+' Jahre alt.'); end; |
Sieht man von der allgemein nicht besonders guten Idee ab, überhaupt ein
record zu versenden (dazu später mehr), wird der obige Code mehr oder weniger gut funktionieren. Wenn wir jetzt die Deklaration verändern zu:
Delphi-Quelltext
1: 2: 3: 4: 5:
| type TMyRecord = record Name: String; Alter: Integer; end; |
wird der Name nicht mehr (korrekt) übertragen. "Wieso nicht? Habe ich auf meinem PC probiert, klappt!", höre ich da schon...
Ja, weil es auf dem selben PC (im selben Programm) tatsächlich klappen kann, aber wenn die Datenübertragung zu einer anderen Maschine hin erfolgt, wird es nicht mehr klappen.
Grund: Die Zeichen statischer Strings liegen direkt an der Adresse der Variablen im Speicher (hier: innerhalb des Speicherbereichs, an dem das
record im RAM liegt) - weil die Länge bekannt ist! Die Zeichen dynamischer Strings liegen aber auf dem Heap, an der Adresse der Stringvariablen befindet sich nur eine Verwaltungsstruktur (Descriptor), die unter anderem den Zeiger auf die Daten auf dem Heap enthält. Faktisch liegen also die Daten des Strings nicht innerhalb des Speicherbereichs des
records, sondern woanders! Deshalb funktioniert das Senden des Speicherbereichs, an dem das
record im RAM liegt, nicht mehr, wenn man dynamische Strings (ganz allgemein: dynamische Objekte) verwendet.
Warum aber klappt das dann auf der selben Maschine? Das liegt daran, dass eine Kopie des Descriptors gesendet wird und somit auf die Stringdaten auf dem Heap zugegriffen werden kann.
Wir haben aber nur eine Referenz auf die Stringdaten gesendet, nicht die Stringdaten selbst!
Das gleiche Problem haben wir mit Klasseninstanzen, zum Beispiel mal
TBitmap. Eine Variable dieses Typs ist nur ein Zeiger auf eine dynamische Datenstruktur auf dem Heap, so dass beim Senden einer solchen Variablen der Zeiger und nicht die Daten selbst transportiert würden. Auf der selben Maschine klappt das dann (solange das Objekt nicht an anderer Stelle z.B. freigegeben wird), aber auf einer anderen Maschine liegen ja nicht die passenden Daten auf dem Heap oder der verfügbare Speicherplatz ist noch nicht einmal so groß, wie die Adresse, auf die da verwiesen wird. Solche Fälle führen dann zu den Zugriffsverletzungen.
Problemlösung: Die Daten selbst senden, nicht die Referenzen! Wer hätte es gedacht... logisch. Jetzt sollte aber schon klar werden, dass es dann keine gute Idee ist, dafür ein
record zu verwenden, denn da drin sind die Daten ja nicht gespeichert. Um dieses Problem allgemein zu lösen, benötigen wir ein
PROTOKOLL und nicht die
record-Krücke. Leider ist das Thema Protokoll aber etwas umfangreicher, so dass es den Rahmen dieses FAQ-Beitrags sehr schnell sprengen würde.
Hier ist ein Tutorial das ausführlich erläutert, was ein Protokoll ist und wie man so etwas entwickelt.
Sind records denn wirklich so "schlecht" für diesen Zweck?!
Ein weiterer Grund, der dringend gegen die Verwendung von
records als Protokoll-Ersatz spricht, ist die Versionsproblematik. Nehmen wir mal an, es gibt verschiedene Programmversionen, die alle eine unterschiedliche
record-Deklaration verwenden. Zum Beispiel könnte das Alter (aus welchen Gründen auch immer) vor dem Namen kommen, oder der Name hat eine andere Anzahl Zeichen (um mal gar nicht erst mit dynamischen Strings zu kommen
), oder oder... In diesen Fällen ist "Datensalat" schon vorprogrammiert (wörtlich sozusagen
), da die Programme sich ja auf einen bestimmten
record-Aufbau verlassen.
Jetzt könnte man, um dieser Problematik zu begegnen, einfach immer zum Beispiel einen Integer an den Anfang des
records stellen, in dem die Versionsnummer oder -kennung des
record-Aufbaus gespeichert ist. So könnte ein Empfänger an diesem ersten Wert erkennen, welche
record-Version vorliegt. Was aber passiert wohl, wenn eine neuere, und zwar längere,
record-Version gesendet wurde, die der Empfänger noch gar nicht kennen kann? Dann werden die restlichen Daten, die für den Empfänger "überzählig" sind, nicht gelesen, da er davon ja gar nichts weiß.
Wieder Datensalat...
OK, OK, nächste Idee: nach dem Versionsinteger einfach noch einen Längeninteger, dann habe ich ja auch wieder die Größe des
records mit im Spiel. Klar kann man das machen, wenn man denn so sehr in
records verliebt ist, dass man davon die Finger nicht loskriegt. Ist aber alles Schund, weil... wie verwende ich denn die
record-Länge, wenn diese erst im
record enthalten ist, das ich ja noch gar nicht gelesen habe...
Mit der Lösung dieses Ansatzes bin ich schon auf halbem Wege zum Protokoll und deshalb ist das Versenden von
records dämlich.
Zum Abschluss noch die Erklärung, warum diese Zeilen praktisch "kaputt" sind:
Delphi-Quelltext
1: 2:
| Socket.ReceiveBuf(MyRecord,SizeOf(MyRecord)); ShowMessage(MyRecord.Name+' ist '+IntToStr(MyRecord.Alter)+' Jahre alt.'); |
Wie aus der DOH hervorgeht, liefert die Methode
.ReceiveBuf(var Buf; Count: Integer) zurück, wieviele Zeichen tatsächlich gelesen wurden. Es ist also gar nicht sicher, dass
SizeOf(MyRecord) Bytes gelesen wurden (weil zum Beispiel einfach noch nicht alle Daten eingetroffen sind -> langsame Verbindung)!
Wenn wir also die Menge tatsächlich gelesener Zeichen nicht auswerten, wissen wir ja gar nicht, ob das komplette
record überhaupt angekommen ist. Damit würden wir dann möglicherweise im
ShowMessage auf undefinierte Werte zugreifen, und das ist sicher nicht sinnvoll.
Fazit: records sind kein brauchbarer Ersatz für ein Protokoll.
cu
Narses
There are 10 types of people - those who understand binary and those who don´t.