Python UDP-Communication Class

Vorgeschichte

2020 kam ein Freund auf mich zu und fragte mich, ob es nicht irgendwie möglich wäre seine smarte Haustürklingel in die von ihm installierte Hausautomatisierung zu integrieren. Beim Betätigen der Klingel sollte ein Bediendisplay aus dem Bildschirmschoner in den Anzeigemodus wechseln und so das Videobild der in der Klingelanlage verbauten Kamera angezeigt werden, ohne das man vorher auf den Bildschirm klicken muss. Da die eingesetzte Hausautomatisierungslösung auf CentOS aufgesetzt war, brauchte er eine Schnittstellenlösung welche dort leicht zu implementieren war. Damit war damals bereits der Grundstein für die Python UDP-Communication Class gelegt.

 

Warum UDP Kommunikation

Auf diese Frage gibt es eine ganz einfache Antwort: Die Klingel stellte nur die Möglichkeit, ein UDP-Datagramm an einen vorkonfigurierbaren Port zu senden, bereit. Das gezielte Erhalten einer Botschaft via TCP gab es schlichtweg nicht. Damit war die Auswahl auf UDP beschränkt und die Umsetzung bereits festgelegt.

 

Ist UDP potentiell unzuverlässiger / unsicherer als TCP

Ja! Bei einer per UDP versendeten Botschaft fehlen, im Gegensatz zu TCP,  Mechanismen welche den Empfang der Pakete an der Gegenstelle garantieren und das Senden der Botschaft bei Bedarf wiederholen. So verfügt UDP zum Beispiel nicht über den bei TCP üblichen 3-way-handshake!
Aber: Ich habe die hier zur Klasse zusammengeschnürten UDP-Funktionen bereits in mehreren privaten Projekten verwendet und auch in einem beruflichen Projekt, wo UDP als Schnittstelle (Interface) einer auf Python basierten Automatisierungssoftware und eines von mir geschriebenen Windows-Tools zur Fernsteuerung einer GUI fungierte, verwendet. Bei keinem dieser Projekte gab es Anzeichen für Botschaftsverlust oder Fehler, welche mit der Nutzung von UDP in Verbindung stehen könnten.   

 

Weshalb aus dem Script eine Klasse wurde

Nachdem das „Türklingelproblem“ per Python UDP Script gelöst war, fand ich immer wieder Anwendungsfälle für diesen Weg der Kommunikation. Möchte man zum Beispiel unter Windows Anwendungen unterschiedlicher Herkunft miteinander „kommunizieren“ lassen, so ist dies eine der einfachsten Möglichkeiten. Die Kommunikation über Netzwerk-Ports ist in den meisten mir bekannten Programmiersprachen möglich. Wenn man die Schnittstellen hierfür einmal in den bevorzugt verwendeten Sprachen (in meinem Fall C++, C# und Python) umgesetzt hat, neigt man dazu diese gern erneut zu verwenden.

Dabei habe ich am Anfang immer die wichtigen Teile aus einem der Scripte herauskopiert und in das aktuelle Projekt eingefügt. Nachdem man das jedoch drei oder vier Mal gemacht hat, denkt man sich irgendwann, dass es sich vielleicht doch lohnen könnte die Code-Schnipsel in eine Klasse zu packen. Diese lässt sich dann recht einfach in Folgeprojekte einbinden und das ständige Kopieren des Codes entfällt.  

Bei der Umsetzung der Klassenmethoden habe ich mich daran orientiert, wofür ich die UDP-Kommunikation bereits eingesetzt habe und wozu ich sie eventuell noch gebrauchen könnte. Diese Funktionen habe ich anschließend soweit abstrahiert, dass eine Wiederverwendung in anderen Projekten (meiner Meinung nach) einfach zu bewerkstelligen ist. Ich wollte es dabei mit der Fülle an Methoden erst einmal nicht übertreiben und Spezialisierungen auf einzelne Anwendungsfälle dann in den jeweiligen Anwendungen / Projekten selbst umsetzen.  

 

Klassendiagramm UDP-Communication Class

 

Klassendiagramm der UDP-Communication Class – UdpConnect

 

Verwendung der Klasse

Die Verwendung der Klasse möchte ich kurz anhand der mitgelieferten Beispieldatei „example.py“ erläutern. In dieser habe ich eingehend alle Klassenmethoden nochmals erklärt und darauf folgend einen an das Ping-Pong Verfahren angelehnten Beispielablauf implementiert, auf welchen ich hier ein wenig näher eingehen möchte. Nach dem Importieren der Klasse werden zwei Instanzen mit dem Namen „connection_1“ und „connection_2“ angelegt. Dem Konstruktor müssen dabei jeweils die folgenden Parameter übergeben werden:

 

s_ip_address
[string]
Die IP Adresse welche nach eingehenden Daten überwacht werden soll. Soll die eingehenden Verbindung unabhängig von der IP Adresse auf alle Daten am angegebenen Port reagieren, so kann diesem Parameter ein leerer String übergeben werden!
i_port
[integer]
Der zu überwachende UDP-Port im Bereich von 0 bis 65535. Frei verwendbare Ports findet man im Bereich zwischen 49152 und 65535. Die in der example.py verwendeten Ports liegen zwar unterhalb der 49152, waren bei mir jedoch nicht genutzt und funktionierten somit ohne Probleme. Für mehr Informationen zu den Ports siehe Liste der standartisierten Ports*.
b_print_information [boolean]Ein optionaler Parameter vom Typ bool (True | False). Setzt man den Parameter auf True, werden die in den Methoden erfassten Statusinformationen an die Konsole zurückgegeben. Da dies nicht erforderlich und evtl. nur für Debug-Zwecke nützlich ist, wurde die Standardvorgabe auf den Wert False gesetzt.
Parameter für den Konstruktor einer Instanz der Klasse UdpConnect

 

Hinweis: Die zweite Verbindung „connection_2“ im Beispiel ist nur nötig um das automatische Senden, Empfangen und Weiterleiten zwischen zwei UDP-Ports zu demonstrieren. Im Regelanwendungsfall reicht es aus für die eingehende Kommunikation eine einzelne Klasseninstanz anzulegen, da das Senden von Daten auf eine gewünschte IP Adresse / einen gewünschten Port ebenfalls über diese Klasseninstanz erfolgen kann.

 

from classes.class_udp import UdpConnect

# EXAMPLES FOR UDP CLASS USAGE

# instantiate udp class and bind to ip address / port
# to listen on given port to all IPs pass first parameter as empty string ""

connection_1 = UdpConnect("127.0.0.1", 47681, True)
connection_2 = UdpConnect("127.0.0.1", 47683, True)

 

Anschließend rufe ich die Methode „get_network_information()“ auf und gebe per „print„-Befehl den Hostnamen und die IP-Adresse des benutzten Rechners aus. Dieser Aufruf dient hier zwar nur als Nutzerinformation, könnte in anderen Projekten aber auch zum Aufbau der Verbindung verwendet werden. Um dafür im Vorfeld keine Instanz erzeugen zu müssen, ist ein direkter Aufruf, wie ich ihn beim Hostname verwende, ebenfalls möglich. 

 

print("Local hostname:" + str(UdpConnect.get_network_information(None)[0]))
print("Local IP:" + str(connection_1.get_network_information()[1]) + "\n")

 

Vor der nun folgenden „while True:“ Endlosschleife sendet zuerst „connection_1“ das Wort „Ping“ an den Port „47681„, also an den gleichen Port auf welchem „connection_1“ auch nach „Nachrichten lauscht“. Dieser Schritt dient als Einstieg in das „Ping-Pong-Verfahren“, weshalb er auch außerhalb der Endlosschleife durchgeführt wird. Innerhalb der Schleife wird dann zuerst die „forward_data( … )“ Methode aufgerufen. Diese Methode wartet auf eine in „connection_1“ eingehende Botschaft (in diesem Fall das davor gesendete „Ping„)  und leitet diese auf die innerhalb „forward_data( … )“ angegebene IP Adresse / den angegebenen Port weiter. 

 

# send "Ping" via connection_1 to listen port of connection_1
connection_1.send("Ping", "127.0.0.1", 47681)

while True:

    # read and forward information ("Ping") from 127.0.0.1 / 47681 to 127.0.0.1 / 47682
    connection_1.forward_data("127.0.0.1", 47683)

 

Da die unter „forward_data( … )“ angegebene IP / Port Konfiguration der von „connection_2“ entspricht, wird das „Ping“ also an „connection_2“ weitergeleitet (ge-forwarded). Im folgenden Schritt wird mittels „if udp_in[0] == „Ping“:“ geprüft, ob das gesendete „Ping“ auch an „connection_2“ angekommen ist. Ist dies der Fall, antwortet „connection_2“ mit einem „Pong„, welches wiederum an „connection_1“ gesendet und da ebenfalls geprüft / empfangen wird. Ist das „Pong“ angekommen, sendet „connection_1“ ein „Ping“ und der ganze Vorgang beginnt durch die „while True:“ Schleife von vorn.

  

    # receive single message on connection_2 
    udp_in = connection_2.receive(True)

    # check if message was "Ping"
    if udp_in[0] == "Ping":
        print("Received   data \"Ping\" from " + str(udp_in[1][0]) + ":" + str(udp_in[1][1]))  # print received data and sender information
        connection_2.send("Pong", "127.0.0.1", 47681)  # reply with "Pong"
    else:
        print("Oh no! Couldn't find Ping on connection_2! Abort execution...")
        exit()

    # receive single message on connection_1 
    udp_in = connection_1.receive(True)

    # check if message was "Pong"
    if udp_in[0] == "Pong":
        print("Received   data \"Pong\" from " + str(udp_in[1][0]) + ":" + str(udp_in[1][1]))  # print received data and sender information
        connection_1.send("Ping", "127.0.0.1", 47681)  # reply with "Ping"
    else:
        print("Oh no! Couldn't find Pong on connection_1! Abort execution...")
        exit()

 

Hinweis: Der eingebaute Counter „loop_count“ dient lediglich der Visualisierung, dass der Vorgang nicht zum stehen gekommen ist und immer noch läuft. Sollte ein gesendetes „Ping“ oder „Pong“ nicht ankommen, bricht der Vorgang durch die jeweilige „else“ Verzweigung ab und das Programm wird beendet! Somit ist sofort sichtbar, falls eine Botschaft verloren, nicht gesendet oder nicht empfangen wurde.

 

Return-values von receive( … )

Wie im Beispiel vielleicht bereits gesehen, liefert die receive( … ) Methode nicht nur den empfangenen Wert (Daten) als String, sondern auch die Senderinformationen (PI Adresse, Port) als Tuple. Dies habe ich so umgesetzt, um auch innerhalb des jeweiligen Programmcodes noch die Möglichkeit zu haben die IP Adresse und den Port des Senders kontrollieren zu können.

  

connection_1.receive(True)[0]Index[0] von connection_1.receive(True) enthält die empfangenen Daten als String
connection_1.receive(True)[1][0]Index[1] von connection_1.receive(True) enthält die Senderinformationen als Tuple.
Die IP Adresse des Senders befindet sich hierbei an an erster Stelle, welche dem Index[0] entspricht.
connection_1.receive(True)[0][1]An zweiter Stelle der Tuple befindet sich der Port über welchen die Nachricht gesendet wurde.
Übersicht der Return-values von receive( … )

 

Um alle Informationen innerhalb des Hauptprogrammes, im aktuellen Fall also der „example.py“, zu erhalten, weist man einfach den Aufruf von „connection_1.receive“ einer Variablen „udp_in“ zu und kann die Informationen aus dieser anschließend problemlos auswerten!

 

    udp_in = connection_1.receive(True)

    udp_in[0]     # holds the message as string 
    udp_in[1][0]  # holds the ip address from message sender as string
    udp_in[1][1]  # holds the port number from message sender as integer

 

Die Methode handle_received_data( … )

Innerhalb der Klasse befindet sich eine Methode mit dem Namen „handle_received_data( … )„. Diese dient als Vorbereitung zur klasseninternen Verarbeitung von empfangenen Daten, da ich die Klasse des Öfteren als Interface verwende und die Reaktion auf eingehende Daten nicht im Hauptprogrammteil erledigen möchte. Außerdem werden alle Daten welche über die Verwendung von „receive_loop( … )“ empfangen werden an diese Methode weitergeleitet, da ein „return“ hier schlichtweg zum Abbruch der Endlosschleife führen würde.  

Die Verwendung von „handle_received_data( … )“ ist denkbar einfach. Im jetzigen Roh-Zustand macht die Methode nichts anderes als die empfangenen Daten per „print“ in der Konsole auszugeben. Zur Steuerung, ob die Methode auch beim Aufruf von „receive( … )“ ausgeführt werden soll, dient die Flagvariable „b_return_data„, welche beim Aufruf von „receive( … )optional gesetzt werden kann.

Wird „b_return_data“ als „True“ gesetzt, so wird der empfangene String und die Senderinformationen direkt an die aufrufende Funktion / Instanz zurück geliefert. Wird hingegen ein „False“ oder auch kein Parameterwert übergeben (da „b_return_data“ mit „False“ als Standardwert vorbelegt ist), so werden die Daten ebenfalls an die „handle_received_data( … )“ Methode übergeben und können dort verarbeitet werden. Welches Vorgehen hier besser und sinnvoller ist, kann man dann von Anwendungsfall zu Anwendungsfall entscheiden. 

 

    def handle_received_data(self, s_data: bytes, tpl_address: tuple):
        print("Received   data \"" + s_data.decode("cp1252") + "\" from " + str(tpl_address[0]) + ":" + str(tpl_address[1]))

 

Testlauf der Klasse

Mit dem „Ping-Pong-Verfahren“ lässt sich nun recht einfach ein (lokaler) Dauertestlauf der UDP-Verbindung umsetzen. Startet man das Beispiel, so läuft es so lang, bis ein Paket verfälscht wird oder gar verloren geht. Ich konnte hier keinen genauen Wert zur Fehlerhäufigkeit ermitteln. Der früheste Abbruch war bei ca. 20 durchgeführten Testläufen nach 176747 Ping-Pong Durchgängen, womit die prozentuale Fehlerquote im Promillebereich liegt.

 

Abbruch des Ping-Pong-Verfahrens nach 176747 durchlaufenen Programmzyklen

 

Fazit

Mit der UDP-Communication Class habe ich mir pythonseitig die Möglichkeit geschaffen, recht einfach, schnell und komfortabel eine Kommunikation mit anderen Anwendungen, ganz gleich in welcher Programmiersprache geschrieben, realisieren zu können. Dabei habe ich versucht den Umfang der Funktionen so abstrakt wie möglich zu halten, damit bei der Wiederverwendung keine „Programmier-Leichen“ in andere Anwendungen mit übernommen werden. 

Da mir das Thema Spaß macht, habe ich damit begonnen die Klasse auch in anderen Programmiersprachen umzusetzen (C#, C++ [geplant]). Außerdem arbeite ich zur Zeit an einem Chat-Beispielprogramm in Python, welches die Funktionalität der hier vorgestellten Klasse noch einmal zur Geltung bringen soll. Wenn ich dieses fertiggestellt habe, werde ich es natürlich ebenfalls auf https://www.langer-sebastian.de* veröffentlichen.

 

Download Python UDP-Communication Class

Für meine Freunde und alle Interessierten biete ich hier die Python UDP-Communication Class zum Download an. Da es mit den *.ZIP Dateien evtl. zu Problemen beim Download kommen kann (ZIP-Archive werden in einigen Web-Browsern als potentielle Bedrohung erkannt und der Download blockiert), stelle ich die Datei sowohl als *.ZIP, als auch als *.7z zur Verfügung. Für die letztere Datei benötigt man das kostenlose Kompressionsprogramm 7-Zip* was hier* heruntergeladen werden kann. (https://www.7-zip.de/).  

Download “Python UDP-Communication Class (Zip-Archiv)” Python-UDP-Communication-Class.zip – 2-mal heruntergeladen – 10 kB

Download “Python UDP-Communication Class (7-Zip-Archiv)” Python-UDP-Communication-Class.7z – Einmal heruntergeladen – 6 kB

 

Debugging:

Trotz größter Sorgfalt und mehrfachem Testen kommt es immer wieder vor, dass sich in Software Bugs oder Fehler einschleichen, welche beim Erstellen übersehen, oder einfach nicht gefunden werden. Wenn jemand so einen Fehler finden sollte, oder sonstige Anregungen, Ideen oder Verbesserungen zum Programm hat, dann wäre es schön wenn dies einfach im Kommentarbereich kommuniziert wird. So kann ich die Änderungen in das nächste Versions-Update einfach mit einfließen lassen. 

 

Haftungsausschluss:

Die hier veröffentlichte Software wurde auf mehreren Systemen fehlerfrei getestet. Dennoch kann für evtl. Beschädigungen, Instabilitäten oder sonstige Beeinträchtigungen, welche unmittelbar durch die Installation, Nutzung, oder in sonstiger Weise in Zusammenhang stehend mit der hier zum Download angebotenen Software auftreten keinerlei Haftung übernommen werden. Der Download, die Installation und Nutzung geschehen auf eigenes Risiko! Bei Problemen wenden sie sich bitte an info@langer-sebastian.de!

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.