agapanthus: Ein Socket pro Nachricht?

Guten Morgen

Ich stehe vor folgendem Problem: Ich muß per TCP 5 Informationen eines Servers abfragen. Dazu gibt es 5 verschiedene Nachrichten, die asyncron (timergesteuert) gesendet werden müssen. Die Serverantworten unterscheiden sich nur durch den Inhalt (es sind Meßwerte verschiedener Sensoren), am Format kann ich nicht erkennen, welches Response zu welchem Request gehört. Ich hab nun 3 Lösungsmöglichkeiten, die ich hier mal zur Diskussion stellen will, jede hat Vor- und Nachteile. Und vielleicht gibts ja noch eine 4. geniale Lösung...

1. Für jede einzelne Nachricht öffne ich ein Socket, sende mein Request und lausche an diesem Socket auf die Antwort, ist die da, schließe ich das Socket. Beim nächsten Request: open socket, connect, send, receive, close socket ... So bin ich sicher, daß ich die Antworten den Requests eindeutig zuordnen kann.
Nachteil: Es werden ständig Sockets geöffnet und geschlossen
Vorteil: einfaches timeout handling, kommt innerhalb eines Intervals keine Antwort, wird das Socket einfach geschlossen.

2. Ich öffne 5 Sockets - für jeden Nachrichtentyp eins - und sende die Requests abhängig von Typ über das entsprechende Socket. Erst mit Programmende werden die Sockets geschlossen.
Nachteil: Was passiert, wenn plötzlich ein paar Nachrichtentypen dazu kommen (vielleicht nichts bei cleverer Programmierung)?
Vorteil: antwortet der Server auf ein Request zu spät ist zwar die Reihenfolge der empfangenen Responses falsch, nicht aber die Zuordnung zum Request.

3. Ich öffne 1 Socket, erzeuge mir einen Nachrichtenpuffer, in den die zu sendenden Nachrichten entsprechend der Reihenfolge durch einen "Haupt"-Thread eingetragen werden. Ein anderer Thread durchsucht den Buffer nach dem nächsten zu sendenden Request, sendet und wartet auf die Antwort, erst wenn die da ist, nimmt er sich den nächsten Request vor. Auch hier wird erst mit Programmende das Socket geschlossen.
Vorteil: nur ein Socket - resourcensparend.
Nachteil: Es kann zu Nachrichtenverzögerungen kommen, was hier allerdings nicht wirklich stört. Schwieriges timeout handling: Kommt ein Response nicht, kann ich nicht ewig warten - ich werde die anderen Requests dann nicht los. Einfach 's nächste senden geht nicht, kommt die verspätete Antwort dann, wird sie dem falschen Request zugeordnet. Mögliche Lösung: bei Timeout Socket schließen und wieder öffnen.

Ich brauche nicht zu erwähnen, daß am Server natürlich nix geändert werden darf - ein "ordentliches" Nachrichtenformat würde die Sache vereinfachen... :(

Achja, das Programm muß unter Windows laufen, was vielleicht bei Variante 1 für Probleme sorgt. In einem ersten Versuch wird ein Programm (geschrieben als Consolenapplikation mit Delphi7), das nur open socket, connect, send, close socket macht, zum Speicherfresser. Windows (Delphi?) belegt offensichtlich für jedes geöffnete Socket Speicher, gibt den aber bei close socket nicht (gleich?) wieder frei.

Ich freue mich schon auf die Antworten bzw. auf eine rege Diskussion...

Danke,
Gruß Frank

  1. Hallo Freunde des gehobenen Forumsgenusses,

    Was wird da gemessen, in welchen Zeitintervallen wird gemessen und wie gut ist die Verbindung (Ping-Zeit, Durchsatz, Entfernung) zwischen Server und Client? Spielt es eine Rolle, in welcher zeitlichen Reihenfolge du die Antworten bekommst?

    Gruß
    Alexander Brock

    1. Hallo Alexander

      Was wird da gemessen, in welchen Zeitintervallen wird gemessen und wie gut ist die Verbindung (Ping-Zeit, Durchsatz, Entfernung) zwischen Server und Client?

      Das Netz ist schnell (100MBit/s), kaum Netzlast, ping < 10ms, max. ein Switch zw. Client und Server, gemessen wird je nach Nachrichtentyp in einem Interval zw. einer und 60 Sekunden.

      Spielt es eine Rolle, in welcher zeitlichen Reihenfolge du die Antworten bekommst?

      Eigentlich schon. Es soll der zeitlicher Verlauf aufgenommen werden. Andererseits geht es auch um Tendenzen, einzelne Ausreißer sind verkraftbar. Aber eignetlich will ichs richtig machen...

      Sollte sich der Speicherfresser als Fehler in meinem Programm herausstellen, würde ich Variante 1 bevorzugen. Und Ihr?

      Schönen Sonntag nach,
      Gruß Frank

      1. Es wäre wirklich nett, wenn Du mal auf die Frage eingingst, wie Du die Speicherfresserei festgestellt hast!

        1. Hallo,

          Es wäre wirklich nett, wenn Du mal auf die Frage eingingst, wie Du die Speicherfresserei festgestellt hast!

          Das hat er doch längst getan!

          Ciao,
           Martin

          --
          Der Gast geht solange zum Tresen, bis er bricht.
          1. Das hat er doch längst getan!

            Sorry, hatte ich übersehen.

            Ich würde das Modell mit einem Socket je Nachricht mal in einer Release-Version und außerhalb des Debuggers und mit einigen 1000 Nachrichten testen. Erst dann läßt sich definitiv sagen, ob Speicher verloren geht, oder nicht.

  2. Achja, das Programm muß unter Windows laufen, was vielleicht bei Variante 1 für Probleme sorgt. In einem ersten Versuch wird ein Programm (geschrieben als Consolenapplikation mit Delphi7), das nur open socket, connect, send, close socket macht, zum Speicherfresser. Windows (Delphi?) belegt offensichtlich für jedes geöffnete Socket Speicher, gibt den aber bei close socket nicht (gleich?) wieder frei.

    Wie hast Du denn festgestellt, daß es zum Speicherfresser wird?

    Für meine Begriffe riecht das eher nach einem Speicherleck in Deinem Programm.

    1. Hallo,

      Windows (Delphi?) belegt offensichtlich für jedes geöffnete Socket Speicher, gibt den aber bei close socket nicht (gleich?) wieder frei.

      Wie hast Du denn festgestellt, daß es zum Speicherfresser wird?
      Für meine Begriffe riecht das eher nach einem Speicherleck in Deinem Programm.

      Oder in Borlands Delphi-Compiler.

      Schönen Abend noch,
       Martin

      --
      Paradox ist, wenn der Innenminister sich äußert und der Außenminister sich erinnert.
      1. Oder in Borlands Delphi-Compiler.

        Das wohl kaum: Der Compiler hat sich zur Programmlaufzeit längst verabschiedet.

        Es käme allenfalls die Delphi-Laufzeitbibliothek in Frage. Da der Gebrauch von Sockets aber nicht so exotisch ist, daß sich in den Grundroutinen dafür noch derart gravierende Eier finden lassen, halte ich für weniger wahrscheinlich.

        1. Hallo,

          Oder in Borlands Delphi-Compiler.
          Das wohl kaum: Der Compiler hat sich zur Programmlaufzeit längst verabschiedet.
          Es käme allenfalls die Delphi-Laufzeitbibliothek in Frage.

          Ja stimmt, das meinte ich auch eigentlich. Es sei denn, der Compiler hat generell einen Bug bei der Code-Generierung. Dann müssten sich solche Memory Leaks aber in allen damit übersetzten Programmen auswirken.
          Davon abgesehen habe ich in der Vergangenheit mit verschiedenen Borland-Compilern durchweg sehr gute Erfahrungen gemacht. Allerdings ist die ursprüngliche Firma Borland ja von Inprise übernommen worden - vielleicht ereilt die dasselbe Schicksal wie die Norton-Produkte, deren Qualität, Stabilität und Zuverlässigkeit ja auch dramatisch den Bach runtergeht, seit Norton von Symantec aufgekauft wurde.

          Da der Gebrauch von Sockets aber nicht so exotisch ist, daß sich in den Grundroutinen dafür noch derart gravierende Eier finden lassen, halte ich für weniger wahrscheinlich.

          Zumal es sich bei all diesen Funktionen lediglich um Wrapper für Win32-Systemfunktionen handeln dürfte.

          Schönen Tag noch,
           Martin

          --
          Butterkeksverteiler zu werden ist vermutlich eine der wenigen beruflichen Perspektiven, die sich noch bieten, wenn man einen an der Waffel hat.
          1. Eine Möglichkeit besteht noch: Im Debugger werden u.U. die Speicherblocks, die freigegeben werden, in eine interne Liste eingetragen und erst viel später wirklich freigegeben, um Zugriffe über ungültig gewordene Pointer aufzuspüren.

            Es kann dann so aussehen, als würde immer mehr Speicher angefordert, aber nicht freigegeben - deshalb meine Frage, wie der 'Speicherverbrauch' festgestellt wurde.

          2. Hallo.

            Allerdings ist die ursprüngliche Firma Borland ja von Inprise übernommen worden - vielleicht ereilt die dasselbe Schicksal wie die Norton-Produkte, deren Qualität, Stabilität und Zuverlässigkeit ja auch dramatisch den Bach runtergeht, seit Norton von Symantec aufgekauft wurde.

            Da darf ich dich insofern beruhigen, als dass Borland niemals aufgekauft, sondern schlicht und schlecht umbenannt wurde. Und welche der ursprünglichen Norton-Produkte gibt es denn heute überhaupt noch? Was Symantec heute unter dieser Marke verkauft, wurde größtenteils von ganz anderen Unternehmen zugekauft -- meist mitsamt der Unternehmen.
            MfG, at

    2. Moin.

      Wie hast Du denn festgestellt, daß es zum Speicherfresser wird?

      Starten und per Taskmanager den Prozess "beobachten"

      Für meine Begriffe riecht das eher nach einem Speicherleck in Deinem Programm.

      Das will ich nicht ausschließen und werde das Programm nochmal dahingehend untersuchen. Ich habe mir den Borland-Debugger noch nicht genau genug angesehen, vielleicht hilft mir der ja auch weiter.

      Gruß Frank

  3. Moin.

    Asche auf mein Haupt - der Speicherfresser geht auf meine Kappe. Ich habe zum Empfangen der Nachrichten einen Thread gestartet und zur Überwachung einen Timer gestartet. Nach erfolgreichem Empfang muß ich den Timer natürlich stoppen und nicht bei nächster Gelegenheit wieder einen neuen starten...

    Bleibt noch die Frage, welche der 3 Varianten die beste ist - ich tendiere zu 1. Was meint Ihr?

    Gruß Frank

    1. Moin.

      Noch etwas zum "Speicherfresser" - Das Problem mit dem Timer war eins, und hier ist noch eins, ich verstehe es noch nicht ganz, vielleicht finde ich hier Hilfe...

      Das kleine Programm unten startet in einer Schleife 500 Threads (function) und wartet dann 5 Sekunden, in diser Zeit haben sich alle Threads wieder beendet. Die Threads machen nichts weiter als eine Ausgabe. Der Anstieg des Speicherverbrauchs hängt z.B. von der Länge des ausgegebenen Strings ab, je länger der String, um so schneller der Anstieg.

      Kommentiere ich nun ExitThread(0) aus, dann bleibt der Speicherverbrauch konstant. Warum? Laut WinAPI wird der ThreadStack beim Aufruf von ExitThread wieder freigegeben:

      "ExitThread is the preferred method of exiting a thread. When this function is called (either explicitly or by returning from a thread procedure), the current thread's stack is deallocated and the thread terminates. The entry-point function of all attached dynamic-link libraries (DLLs) is invoked with a value indicating that the thread is detaching from the DLL."

      Darf ich ExitThread nicht in einer Funktion aufrufen? Zugegeben: ExitThread ist einer Funktion irgendwie doppelt gemoppelt: den Rückgabewert setzte ich mit result, ExitThread tut dies auch noch. Starte ich den Thread als Prozedur, ist der Speicherverbrauch konstant, hier macht ExitThread auch Sinn, so kann ich auch einen Wert zurückgeben, den ich mit GetExitCodeThread abfragen könnte.

      Also: Warum frisst ein Thread, der als Funktion gestartet wurde und der mit der "preferred method" ExitThread beendet wurde, Speicher?

      Ein etwas verwirrter Gruß an die Spezialisten von
       Frank

      P.S. ich werde die Frage wohl auch noch in einem Delphi-Forum stellen...

      program Thread;
      {$APPTYPE CONSOLE}
      uses windows, sysutils;

      //*******************************************************************
      //   myThreadProcedure
      //*******************************************************************
      function myThread(p: Pointer); stdcall;
      begin
        write(' ' + intToStr(Random(1000)));
        ExitThread(0);
      end;

      //*******************************************************************
      //   myThreadFunction
      //*******************************************************************
      function myThread(p: Pointer): DWORD; stdcall;
      begin
        write(' ' + intToStr(Random(1000)));
        result := 0;
        ExitThread(0);
      end;

      //*******************************************************************
      //   main
      //*******************************************************************
      var
        tId:DWORD;
        i:integer = 0;
      begin
        while (TRUE) do begin
          CreateThread(nil, 0, @myThreadFunction, nil, 0, tId);
      //    CreateThread(nil, 0, @myThreadProcedure, nil, 0, tId);
          inc(i);
          if i > 500 then begin
           write(#10#13 + '**********Pause*************' + #10#13);
           sleep(5000);
           i:=0;
          end;
        end;
      end.

      1. Hallo Frank,

        Also: Warum frisst ein Thread, der als Funktion gestartet wurde und der mit der "preferred method" ExitThread beendet wurde, Speicher?

        Naja, was heißt hier "freigegeben"? Ich vermute mal einfach stark, dass der Speicher lediglich wieder dem Programm zur Verfügung steht, nicht dem Betriebsystem. Ich kenne mich mit Windows-Interna nicht so wirklich aus, daher erkläre ich Dir mal, wie's unter Linux aussieht, unter Windows sieht es mit Sicherheit fast genauso aus.

        Unter Linux kann ein Programm per Funktion malloc() dynamisch Speicher vom Betriebsystem anfordern und per free() wieder freigeben. malloc() ist allerdings *keine* Systemfunktion. Diese Funktion ruft nämlich selbst die Systemfunktion brk() auf, die den Heap eines Prozesses vergrößern kann. Die Funktion malloc() unerhält im Heap des Programms einen Speicherpool. Fordert ein Programm nun Speicher per malloc() an, wird geschaut, ob der Speicherpool groß genug ist, wenn ja, wird ein Zeiger aus dem Speicherpool zurückgegeben, wenn nein, wird brk() aufgerufen, um den Heap zu vergrößern. Wird nun der Speicher per free() wieder freigegeben, wird dieser Speicherblock dem Speicherpool wieder zugewiesen - was jedoch *nicht* geschieht ist, dass der Heap wieder verkleinert wird - der bleibt so groß. Für das Betriebsystem (es weiß ja nicht, wie das Programm intern seinen Speicher verwendet) sieht es so aus, als ob das Programm den Speicher noch verwenden würde. Wenn das Programm nun erneut Speicher anfordert, wird zuerst der Speicher verwendet, der bereits vom Programm reserviert wurde - und nur, falls dieser ausgehen sollte, wird mehr Speicher verwendet.

        Bei Dir scheint genau das der Fall zu sein: Du hast am Anfang sehr viele Threads, die viel Speicher fressen, die beenden sich dann und der Speicher bleibt vom Programm genutzt - das Programm nutzt aber im weiteren Verlauf nicht mehr so viel Speicher, dass es diese Grenze wieder verschieben könnte - der Speicherverbrauch bleibt dann für die restliche Ausführungszeit konstant.

        Viele Grüße,
        Christian

        1. Moin Christian.

          Also: Warum frisst ein Thread, der als Funktion gestartet wurde und der mit der "preferred method" ExitThread beendet wurde, Speicher?

          Das Problem war, daß ich das Handle nicht wieder geschlossen habe, damit wurden das immer mehr und mehr... Die Diskussion und die Lösung gibts im Delphi-Forum.

          Die ungenutzten Handles gammelten also im Speicher (Heap) rum, und ich hab mir immer neue geholt und den Heap damit vergrößert. Daß Windows das Handle eines mittels ExitThread beendeten Threads nicht selbst wieder frei gibt, ist auch klar, nur darüber kann ich nach Threadende den Rückgabewert (GetExitCodeThread) des Threads noch ermitteln, nach closeHandle ist diese Info weg.

          Gruß Frank