telnet ueber loopback

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Ich hatte ein kleines Problem mit einer telnet socket Verbindung ueber Loopback.
Die Suche hier hat nichts ergeben; aber vielleicht hatte ich nur falsche Suchbegriffe ...
Ich habe das Problem dann auch etwas umstaendlicher geloest, aber mich interessiert, warum das einfache Programm nicht geht.

Das Problem in Kurzform:
Die Verbindung Client -> Server geht.
Die Verbindung Server -> Client macht viele retransmisions; bzw timeout. Oder haengt ohne timeout, auch wenn der Server Daten liefert.

Mehr Details:
Ein (python) Protokollkonververter (https://www.dk1ri.de/dhw/IC705_interface_python.zip, noch mit kleinen Fehlern;
der server Teil steht in io_handling) steuert ein Geraet ueber USB.
Der Konverter hat einen terminal und einen telnet Server Zugang mit Threading (nicht blockierend)
Das zu steuernde Gerät hat ca 500 Parameter, die ein (Python) Clientprogramm lesen soll, damit ein Apache Server mit PHP die Daten
sofort verwenden kann. Das Abrufen aller Daten vom Geraet dauert.
Der client ist sehr einfach. Das wesentliche ist in einer Schleife, die mit i die Steurbefehle fuer das Geraet liefert:

while...:
...
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.settimeout(2)
s.connect((HOST, PORT))
s.sendall(bytes(send_string, 'utf-8'))
data = s.recv(1024)
s.close()
print("da",str(data))
trx_value_f.write(str(data))
trx_value_f.write(chr(10))
i += 1
repetitions = 0
except socket.timeout:
s.close()
data = ""
print("timeout", send_string, repetitions)
repetitions += 1
if repetitions == 5:
time.sleep(1)
# 5 repetions -> ingnore , step to next
i += 1
repetitions = 0
trx_value_f.write("x "+ send_string)
trx_value_f.write(chr(10))

Der Win10 Laptop hat eine Last von 25%, Speicher 40%; aendert sich kaum, waehrend die beiden Programme laufen.
Die timeouts haeufen sich, je laenger das Clientprogramm laeuft, ist auch bei jedem Lauf unterschiedlich.
Ich muss noch erwaehnen, dass "keine Antwort" des Servers eigentlich erlaubt sein soll.


Ich habe es dann mir threading auf der Clientseite, etwas handshake und timeout Ueberwachung hinbekommen.
Das ganze laeuft so auch wesentlich schneller.
Aber warum hat die obige einfache Loesung so viele Verbindungsfehler?

Guenter
Sirius3
User
Beiträge: 15981
Registriert: Sonntag 21. Oktober 2012, 17:20

Aus dem Code läßt sich kaum rauslesen, was wo Probleme macht.
`s` ist wie fast jeder einbuchstabige Variablennamen schlecht.
Ein timeout von 2s ist vielleicht etwas kurz.
Das recv ist wie bei fast allem Code, den man hier so zu lesen bekommt, falsch.
Das s.close gehört da nicht hin, dafür ist ja with da.
Was bedeutet `trx_value_f`? Vor allem trx und f? Benutze keine kryptischen Abkürzungen.
Die Stringrepräsentation eines Bytes-Objekts ist selten ein sinnvolle Art mit Daten weiter zu arbeiten.
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Den Code (ebenso wie das threading) habe ich von irgendwoher uebernommen.
Das mag sicher nicht der schoenste Code sein, aber Pycharm meckert ihn nicht (auch nicht als Warnung) an.
2s: das laeft ja 5 mal..
recv: wenn es falsch ist: ich weiss leider nicht, wie es richtig geht.
s.close: ok, kann weg
trx_value_f: trx ist eine fuer die Benutzer des Programms gaengige Abkuerzung fuer Transceiver (Sende-Empfaenger).
trx_value_f ist das Dateihandle.
Die Datei wird dann von php gelesen. und bei der Verwendung macht ein String vielleicht Sinn; wenn nicht,kann ich es ja leicht aendern.

Nach meinem Wissen funktioniert der loopback ueber interne Buffer, und mir sind die retransmissions ziemlich unerklaerlich. Aber
vielleicht gibt es ja wirklich ein beseres recv?
__deets__
User
Beiträge: 11923
Registriert: Mittwoch 14. Oktober 2015, 14:29

Pycharm kann aber auch keine Programmierfehler auf höheren Ebenen anmeckern. Nur weil das nix sagt, ist Code noch lange nicht fehlerfrei.

recv hat das Problem, dass es mir beliebig wenig Daten zurückkommen kann. Und darauf bist du nicht vorbereitet. Wenn hier wirklich Telnet zugrundeliegende, dann ist sowas wie readline zu bevorzugen. Weil das so lange liest, bis ein newline kommt.
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Das stimmt, manchmal kamen nur 2 Byte, es muessen aber mindestens 3 Byte sein (bis zu ein paar hundert. oder es kommt eben nichts.
Was macht recv, wenn nur ein Teil empfaengen wurde? schliesst es die Verbindung oder wartet es auf ein retransmission?
Meine sendedaten haben keinen Abschluss wie newline. kann ich auch schlecht so verwenden, das das letzte Datenbyte ein newline sein kann.
Grundsaetzlich koennte ich zwar herausfinden, wie lange jede Antwort sein muss; das ist aber ein ziemlicher Aufwand: Da tcp ja eine "sichere" Uebertragung ist, wollte das aber vermeiden.
Unabhaengig von Teildaten: Bei mir kamen aber bis zum timeout offenbar manchmal gar keine Daten an :(
Sirius3
User
Beiträge: 15981
Registriert: Sonntag 21. Oktober 2012, 17:20

TCP ist ein Strom an Bytes, da gibt es keine Nachrichtengrenzen. Du mußt wissen, wie viele Bytes Du erwartest, und so lange recv aufrufen, bis die korrekte Anzahl an Bytes gelesen worden ist.
Einfacher ist es `socket.makefile` zu benutzen, und mit read garantiert die richtige Anzahl an Bytes zu lesen.
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

socket.makefile kannte ich nicht. Muss ich probieren.
Ist mir noch ein Fehler unterlaufen: Das Protokoll ist zwar binaer ohne Header und Ende, aber die Uebertragung zum Terminal und telnet ist in Hex: also readline geht dann auch,
Aber bleibt immer noch die Frage: warum gibt es ueberhaupt timeout und mit threading geht es problemlos (Allerdings habe da noch nicht im Detail ueberpruft, on die empfangengen Daten korrekt sind; readline werde ich also auch beim threading einbauen)
__deets__
User
Beiträge: 11923
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ohne zwei lauffähige Stücke Code, die das reproduzieren, kann man da nicht viel sagen. Dazu müsste dein Server also her, und soweit gemockt, dass er ohne die dedizierte Hardware funktioniert.
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Hat leider etwas gedauert:
http://dk1ri.de/socket_test.zip

Bitte ____bitte_lesen lesen :)

Ich hoffe, dass das reicht...
ggf hilft noch
https://dk1ri.de/dhw/ICOM_interface.pdf weiter
Sirius3
User
Beiträge: 15981
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast in Deinem Thread-Client drei Threads, wobei der erste nur die Wohnung heizt, der zweite ständig gelesene Daten mit neuen Daten überschreibt und der dritte auch die meiste Zeit die Wohnung heizt, außer hin und wieder was sendet.
Die Klassen bekommen in __init__ Variablen übergeben, die nie verwendet werden, was die Klassen ziemlich überflüssig machen.
Globale Variablen sind an sich schon ein NoGo. Bei Threads sind die natürlich noch viel schlimmer, weil es da wirklich jederzeit passieren kann, dass sich deren Wert ändert.
Zusammengefasst, garantiert Dein Code, dass niemand weiß, was gelesen und was über den Socket gesendet wird. Wenn das funktioniert, dann wirklich nur mit sehr viel Glück.
Mit threads kommuniziert man immer über Queues.

Wenn Du eine Datei löschst um sie dann per "a"ppend zu öffnen, dann kannst Du auch gleich "w"rite benutzen, denn das löscht den Inhalt der Datei genauso.
Die if-Abfragen in der while-Schleife sind so verschachtelt und undurchsichtig, dass es wirklich schwierig ist, zu verstehen, was da eigentlich wann passiert.
Das liegt daran, dass Du keine Synchronisation zwischen Threads und Hauptprogramm hast.
Ist es wirklich wichtig, an EINEM Leerzeichen zu splitten, normalerweise würde man ein split ohne Argument benutzen.
`lstrip` macht nicht das, was Du denkst, ist in diesem Fall zufällig aber kein schwerer Fehler, sondern einfach nur wieder Glück. Wenn man Hex-Ziffern haben möchte, dann benutzt man das passende Formatstring-Format und nicht `hex`.
Was ist der Grund, warum bei einer Länge von send_string von 1 und 3 eine 0 angehängt wird?

Wenn ich das richtig interpretiere dann sollte der Code so aussehen:

Code: Alles auswählen

import socket
import threading
import queue

# read actual configaration of device
# HOST = '192.168.2.100'
HOST = '127.0.0.1'
PORT = 23
TRX_VALUE_FILENAME = "_trx_value"

def client_read_thread(client_socket, queue):
    while True:
        queue.put(client_socket.recv(4096))

def client_write_thread(client_socket, queue):
    while True:
        client_socket.sendall(queue.get())

def start_ethernet_client(host, port):
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    read_queue = queue.Queue()
    write_queue = queue.Queue()
    threading.Thread(target=client_read_thread, args=(client_socket, read_queue), daemon=True).start()
    threading.Thread(target=client_write_thread, args=(client_socket, write_queue), daemon=True).start()
    return read_queue, write_queue

def main():
    read_queue, write_queue = start_ethernet_client(HOST, PORT)
    with open("_ask_tokens") as file:
        ask_tokens = list(file)
    with open(TRX_VALUE_FILENAME, "w") as output:
        for token in ask_tokens:
            lines = token.split()
            text = f"{int(lines[0]):04x}"
            if len(lines) > 1:
                text += lines[1]
            write_queue.put(text.encode())
            try:
                data = queue.get(timeout=5)
            except queue.Empty:
                print("timeout")
            else:
                out = "".join(f"{char:02x}" for char in data)
                print("client recceived ", out)
                output.write(out + "\n")

if __name__ == "__main__":
    main()
Jetzt machst Du aber im Hauptprogramm nichts anderes, als auf den Lese-Thread zu warten, den Schreib-Thread ist wohl genauso überflüssig, weil der Server wohl nichts macht, solange nichts geschrieben worden ist.
Also ist das Äquivalent zu einer nicht-Thread-Version:

Code: Alles auswählen

import socket

# read actual configaration of device
# HOST = '192.168.2.100'
HOST = '127.0.0.1'
PORT = 23
TRX_VALUE_FILENAME = "_trx_value"

def main():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    client_socket.set_timeout(5)
    with open("_ask_tokens") as file:
        ask_tokens = list(file)
    with open(TRX_VALUE_FILENAME, "w") as output:
        for token in ask_tokens:
            lines = token.split()
            text = f"{int(lines[0]):04x}"
            if len(lines) > 1:
                text += lines[1]
            client_socket.sendall(text.encode())
            try:
                data = client_socket.recv(4096)
            except socket.timeout:
                print("timeout")
            else:
                out = "".join(f"{char:02x}" for char in data)
                print("client recceived ", out)
                output.write(out + "\n")

if __name__ == "__main__":
    main()
Was sind jetzt die unterschiede zwischen meiner und Deiner nicht-thread-Version?
Bei Dir ist der Timeout auf 2 gesetzt, hier auf 5. Du öffnest ständig neue Sockets, hier nicht.
Also sind beide Versionen mit nichten gleich.
Und jetzt noch die richtige Version, die makefile nutzt:

Code: Alles auswählen

import socket

# read actual configaration of device
# HOST = '192.168.2.100'
HOST = '127.0.0.1'
PORT = 23
TRX_VALUE_FILENAME = "_trx_value"

def main():
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect((host, port))
    client_socket.set_timeout(5)
    socketfile = client_socket.makefile('rwb')
    with open("_ask_tokens") as file:
        ask_tokens = list(file)
    with open(TRX_VALUE_FILENAME, "w") as output:
        for token in ask_tokens:
            lines = token.split()
            text = f"{int(lines[0]):04x}"
            if len(lines) > 1:
                text += lines[1]
            socketfile.write(text.encode())
            try:
                data = socketfile.readline()
            except socket.timeout:
                print("timeout")
            else:
                out = "".join(f"{char:02x}" for char in data)
                print("client recceived ", out)
                output.write(out + "\n")

if __name__ == "__main__":
    main()
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Danke fuer Deine Muehe, das muss ich erst verdauen :)
Ich muss noch erwaehnen, dass der "Server" mein 2. python Programm ist (abgesehen von einigem Kleinkram), und da sicher einiges eleganter geht.
Das 1. ist ein commandrouter mit (prinzipiell) beliebig vielen Eingabestreams (telnet, i2c, UBB..) und beliebig vielen angeschlossenen Geraeten.
Alles soll nicht blockierend funktionen und das verwendete Threading habe ich natuerlich nicht selbst erfunden.
Da der "Server" (mein 2. Programm) in einigen Teilen aehnliches macht, habe ich den commandrouter als Grundlage genommen und daher ist er teilweise komplizierter als er sein muesste.
Sowohl der Server als auch der commandrouten analysieren eingehende Streams in (quasi) Echtzeit. Sie koennen die Laenge berechnen und einige Fehler (falsche Paramter, falsche Eingabe) erkennen. Das Protokoll selbst erwartet aber eine fehlerfreie Datenuebertragung; also keine Datenverfaelschung.
Nicht gueltiges wird verworfen, nach einem timeout alles. Bei gueltigen Daten werden die an das (ggf pasende) Geraet geschickt und weiteres an das Geraet bleibt erst mal im Puffer stecken (oder geblockt), bis der Befehl abgeschlossen ist.
Dieser Ablauf funktioniert hoffentlich nicht nur zufaellig.
Der Client kann die Laenge nicht unbedingt berechnen. Daher habe ich da jetzt newline am Server ergaenzt.
Hex liefert : 0: 0x0, ich moechte aber als Ausgabe immer 2Byte (das geht sicher einfacher :) )
Nochmal vielen Dank, ich muss das erst mal umsetzen.
__deets__
User
Beiträge: 11923
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du wuerdest dir dein Leben wahrscheinlich deutlich vereinfachen, wenn du auf eine der etablierten Middlewares wie ZEROMQ oder nanomsg setzen wuerdest. Damit bekommst du robustere Messaging-Systeme, mit verschiedenen Topologien, automatischem wiederverbinden, verschiedene Bus-Topologien, etc. pp.
dk1ri
User
Beiträge: 23
Registriert: Sonntag 2. November 2014, 11:08

Als ich den commandrouter vor einigen Jahren geschrieben habe, habe ich mir schon andere Lösungen angesehen; ob die von Dir genannten dabei waren, weiss ich nicht mehr. Ob sie das noetige koennen, muesste ich noch herausfinden: es gibt ja keine feste Verbindung vom User zu den Geraeten, das ergibt sich ja aus den ersten Datenbytes (commands)
Im Sytem ist der "Server" das Geraet und der Client der User. Da es nur ein Geraet ist, ist kein commandrouter noetig.
Ich sehe mir andere Loesungen nochmal an; aber es gibt schon noch ein paar weitere Randbedingungen.
Antworten