Autor Beitrag
Narses
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Administrator
Beiträge: 9749
Erhaltene Danke: 984

W2k .. W7pro
TP3 .. D7pro .. D10.1
BeitragVerfasst: Mi 31.05.06 00:17 
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:
ausblenden 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:
// --- Allgemein ---
type

  TMyRecord = record
    Name:  String[30]; // statischer String, 30 Zeichen Länge
    Alter: Integer;    // Skalar, 4 Bytes
  end;

var
  MyRecord: TMyRecord;
  i: Integer;

// --- Server ---

with MyRecord do begin // Werte zuweisen
  Name  := 'Charles Cros';
  Alter := 166;
end;

// und an alle Clients senden
for i := 0 to ServerSocket1.Socket.ActiveConnections-1 do
  ServerSocket1.Socket.Connections[i].SendBuf(MyRecord,SizeOf(MyRecord));

// --- Client ---

procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
begin
  Socket.ReceiveBuf(MyRecord,SizeOf(MyRecord)); // Achtung! Potentiell defekter Code, s.u.
  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:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
type
  TMyRecord = record
    Name:  String;  // dynamischer String, Länge zur Entwurfszeit unbekannt!
    Alter: Integer; // Skalar, 4 Bytes
  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 Suche in: Delphi-Forum, Delphi-Library 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ß. :shock: :arrow: Wieder Datensalat... :roll: 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... :nut: Mit der Lösung dieses Ansatzes bin ich schon auf halbem Wege zum Protokoll und deshalb ist das Versenden von records dämlich. :P

Zum Abschluss noch die Erklärung, warum diese Zeilen praktisch "kaputt" sind:
ausblenden Delphi-Quelltext
1:
2:
Socket.ReceiveBuf(MyRecord,SizeOf(MyRecord)); // Achtung! Potentiell defekter Code
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)! :shock: 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.

Für diesen Beitrag haben gedankt: turn-table