Wie prüfen, ob eine TCP-Verbindung noch steht?

Begonnen von Dr. Boris Neubert, 04 März 2014, 19:00:04

Vorheriges Thema - Nächstes Thema

Dr. Boris Neubert

Hallo,

ich habe seit einiger Zeit meinen zweiten AVRNETIO im Betrieb, der mit der Original-Pollin-Firmware läuft. Ich kommuniziere mit dem AVRNETIO über (mein) ECMD-Modul via telnet. Es kommt nun vor, daß die telnet-Verbindung nicht mehr steht, wenn ich einen Befehl sende. Gibt es eine Möglichkeit, vor dem write auf den socket herauszufinden, ob die Verbindung noch existiert,  um diese dann ggf. zunächst neu aufzubauen? Ist das in DevIO.pm ggf. sogar bereits umgesetzt (muß ECMD noch auf Verwendung von DevIO migrieren)?

Viele Grüße
Boris
Globaler Moderator, Developer, aktives Mitglied des FHEM e.V. (Marketing, Verwaltung)
Bitte keine unaufgeforderten privaten Nachrichten!

rudolfkoenig

ZitatGibt es eine Möglichkeit, vor dem write auf den socket herauszufinden, ob die Verbindung noch existiert
Mir ist keins bekannt (und wuerde mich auch ueberraschen), keepalive wuerde auch nur begrenzt helfen. DevIo_SimpleWrite prueft z.Zt nicht mal den Rueckgabewert von syswrite, die Verbindung optional neu zu oeffnen finde ich aber sinnvoll.

ntruchsess

so wie ein tcp-socket funktioniert gibt es keine Möglichkeit vor dem Schreiben zuverlässig herauszufinden, ob die Verbindung noch steht ohne irgendwas zu schreiben. Die Verbindung existiert ja nur virtuell und besteht ausschließlich darin, dass sich beide Seiten jeweils das Tupel aus lokaler und remote ip-addresse und port gemerkt haben, auf der Leitung passiert nur dann was, wenn etwas zu übertragen ist. Und selbst dann, wenn man schreibt, merkt es der Socket potientiell auch nur am Ausbleiben der Antwort (mit großzügigen Timeouts), dass die Gegenstelle nicht mehr erreichbar ist. Wenn das OS keepalive unterstützt und der Socket dafür konfiguriert ist, dann schickt er regelmäßig Packete an die Gegenseite um die Erreichbarkeit zu testen. Aber auch das ist keine Garantie - nach einer Antwort auf ein Keep-alive-packet kann die Gegenstelle ja weiterhin verschwinden ohne die Verbindung sauber abzubauen. Um eine Logik, die bei Nicht-antwort der Gegenseite die Verbindung neu aufbaut, kommt man daher prinzipiell nicht herum.

Gruß,

Norbert
while (!asleep()) {sheep++};

betateilchen

Am gleichen Problem scheitert übrigens regelmäßig 93_DbLog, wenn das Loggen auf einen MySQL Server erfolgt, der nicht auf localhost läuft. Und das Log-Modul bekommt nichts davon mit, dass das Socket nicht mehr existiert und schreibt einfach munter weiter ins Nirvana.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

Dr. Boris Neubert

Wollen wir das Problem grundsätzlich in DevIO.pm lösen oder bleibt es jedem Modul selbst überlassen?

Ich denke an eine Funktion,  die ähnlich wie expect einen String sendet, dann auf einen String wartet (regex). Wenn ein Timeout kommt,  wird die Verbindung neu aufgebaut und es nochmal versucht. Im Fehlerfall dann Fehlermeldung und Abbruch.

Andere Idee?

Viele Grüße
Boris
Globaler Moderator, Developer, aktives Mitglied des FHEM e.V. (Marketing, Verwaltung)
Bitte keine unaufgeforderten privaten Nachrichten!

rudolfkoenig

Gerne in DevIo.pm, aber nur mit Timeout (siehe DevIo_TimeoutRead)

Fuer das erwaehnte Problem (Verbindung tot) ist das Warten auf einem String nur dann notwendig, wenn das Modul sonst nicht vom FD liest.

fhem.pl wuerde sonst kurz nach dem Schreiben das Modul-eigene ReadFn aufrufen, der dann mit einem sysread feststellen kann, dass die Verbindung tot ist.

ntruchsess

Zitat von: rudolfkoenig am 06 März 2014, 10:33:21...kurz nach dem Schreiben[...]mit einem sysread feststellen kann, dass die Verbindung tot ist.
Genau das geht bei einer TCP-verbindung eben nicht (zuverlässig) sondern nur dann, wenn die Verbindung (von der Gegenseite) sauber geschlossen wurde oder die Gegenseite ankommende Pakete zu einer lokal schon geschlossenen Verbindung mit einem RST abweist um zu melden, dass die Verbindung nicht mehr gültig ist. Ist die Gegenseite z.B. ein NetIO der sich einfach aufgehängt hat, so dass die Pakete ins Leere gehen, dann muss erst mal der TCP-timeout vergehen, bevor der FD ungültig wird. In diesem Fall ist das explizite Warten auf eine bestimmte Response sinnvoller als sich auf den Fehler beim Sysread zu verlassen.

Wenn man nicht synchron (a la DevIO_TiumeoutRead) auf das Ende des Timeouts warten will, dann könnte eine neue Schreib-methode im DevIO im Device-hash einen Eintrag ablegen, bis wann spätestens die Antwort erwartet wird und per InternalTimer auf kurz nach diesem Zeitpunkt eine Methode schedulen, die überprüft, ob dieser Eintrag noch da bzw. schon abgelaufen ist. Beim Lesen der Antwort über die ReadFn wird der Eintrag wieder entfernt. Immer wenn die Antwort zu spät kommt und der InternalTimer vorher aufgerufen wird hat man einen Timeout (asynchron) erkannt.
while (!asleep()) {sheep++};

rudolfkoenig

Klingt sinnvoll. Weisst Du wie lang der TCP-Timeout ist?

ntruchsess

Der Timeout ist nicht fest und nicht für alle Systeme gleich. Er hängt üblicherweise (wenn der Stack nach rfc2988 implementiert ist) von der Verbindungsqualität (genauer gesagt der Roundtriptime) ab. Nach meiner eigenen Erfahrung dauert es auf Linux typischerweise eine meist zweistellige Zahl an Minuten (bei Mobilfunkverbindungen kann das aber auch mal eine Stunde sein) bis die Verbindung per Timeout geschlossen wird.
while (!asleep()) {sheep++};

Dr. Boris Neubert

Hallo,

ich schlage folgendes vor (nicht getestet!).

########################
# Write something, then read something
# reopen device if timeout occurs and write again, then read again
sub DevIo_Expect($$$)
{
  my ($hash, $msg, $timeout) = @_;
 
  # write something
  return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
  # read answer
  my $answer= DevIo_TimeoutRead($hash, $timeout);
  return $answer unless($answer eq ""):
  # reopen device
  # unclear how to know whether the following succeeded
  DevIo_OpenDev($hash, 1, undef); # where to get the initfn from?
  # write something again
  return undef unless defined(DevIo_SimpleWrite($hash, $msg, 0));
  # read answer again
  $answer= DevIo_TimeoutRead($hash, $timeout);
  # success
  return $answer unless($answer eq ""):
  # ultimate failure
  return undef; # undef means ultimate failure
}


Viele Grüße
Boris
Globaler Moderator, Developer, aktives Mitglied des FHEM e.V. (Marketing, Verwaltung)
Bitte keine unaufgeforderten privaten Nachrichten!

rudolfkoenig

Probleme mit der vorgeschlagenen DevIO_Expect:
- FHEM wartet exklusiv auf diesem Geraet.
- Falls ein Modul mit dieser Funktion von einem lokalen USB-Stick liest, und waehrend des Aufrufs der Stick entfernt wird, dann wird das zu SIGSEGV/Blockieren/reboot-Notwendigkeit fuehren, weil in solchen Faellen das Oeffnen nicht sofort erfolgen darf. Das wird in DevIO_Disconnected beruecksichtigt.

Ich finde den (leider komplizierteren) Vorschlag von ntruchsess den richtigen Weg.

Dr. Boris Neubert

Seufz...

Ich mache mich mal dran, ECMD auf DevIO zu migrieren und erledige das dann gleich mit. Ich stelle den Code dann erstmal im Forum zum testen ein.

Viele Grüße
Boris
Globaler Moderator, Developer, aktives Mitglied des FHEM e.V. (Marketing, Verwaltung)
Bitte keine unaufgeforderten privaten Nachrichten!

Dr. Boris Neubert

Nachdem ich mich jetzt an die Sache drangegeben habe, kommen auch schon die Verständnisprobleme.

Norberts Vorschlag beschreibt eine asynchrone Kommunikation.

Bei get oder set würde man aber das Ergebnis synchron benötigen: ich sende einen Befehl an das Gerät und bekomme die Antwort oder eine Fehlermeldung zurück. Wie funktioniert das, wenn FHEM während des Wartens auf die Antwort etwas anderes machen darf?

Viele Grüße
Boris
Globaler Moderator, Developer, aktives Mitglied des FHEM e.V. (Marketing, Verwaltung)
Bitte keine unaufgeforderten privaten Nachrichten!

rudolfkoenig

Synchron funktioniert es nicht bzw. mir faellt nichts einfaches dafuer ein.

ntruchsess

#14
Zitat von: Dr. Boris Neubert am 10 März 2014, 19:40:23Wie funktioniert das, wenn FHEM während des Wartens auf die Antwort etwas anderes machen darf?

Das betreffende Modul braucht eine ReadFn, die aus der Fhem-mainloop aufgerufen wird, wenn auf der Socketverbindung Daten eingetroffen sind. Dazu noch eine Funktion, die prüfen kann, ob der gewünschte Timeout abgelaufen ist. Anstelle eines Aufrufs von 'Set/get ...' mit Warten auf die Antwort schickt das betreffende Modul einfach den Request raus, setzt ein 'timeoutflag' und startet den InternalTimer mit der TimeoutFunktion. Anschließend tut es erst mal nichts mehr. Kommen Daten über die ReadFn rein, werden die ausgewertet, die Readings geupdatet und das TimeoutFlag gelöscht. Wird vorher die Timeoutfunktion aufgerufen, schreibt diese etwas dazu passendes in die Readings. Reagieren tut man auf das Ganze, indem man die passenden Notifys auf die gewünschten Readings setzt. (Eines für erfolgreichen Empfang der Antwort, eines für Timeout).

Alternativ könntest Du auch die Perl-protothreads-library benutzen. Dann sieht der code etwas 'linearer' aus, so als ob Du in einem Hintergrundthread erst den Request abschickst und dann auf den Timeout wartest. Das Updaten der Readings für Erfolg oder Timeout würde dann aus dem gleichen Protothread heraus erfolgen. Aber letztendlich ist so ein Protothread auch nur eine Statemachine, die man immer wieder aufrufen muss, und die bei jedem Aufruf gegebenenfalls ein Stückchen weiterläuft. D.h. man kann auch nicht synchron zu einem Get oder Set darauf warten, sondern startet den Protothread und tut dann erst mal nix mehr (Außer dass man Protothread z.B. aus einer ReadyFn zyklisch aufrufen läßt oder dafür einen InternalTimer zyklisch immer wieder neu startet). Reagieren tut man dann genauso über Notifys.

while (!asleep()) {sheep++};