Netzwerk in Python

Netzwerk Programmierung ist ein Thema, dass mich fasziniert, seit ich mein erstes Modem 1989 gekauft habe. Aus dieser Faszination entstand mein eigenes Mailboxprogrammauf dem Commodore Amiga, dass mit dem Z-Netz kommunizieren konnte. Die Faszination habe ich ins Internet-Zeitalter mitgenommen. Hier will ich dir vermitteln, wie du in Python Daten per TCP/IP austauschen kannst.

1Python Programmierkurs
2Python: Methoden
3Kontrollstrukturen
4Strings in Python
5Container
6Objekte in Python
7Module
8Exceptions in Python
9Typkonvertierung
10Python und Dateien
11Datum und Zeit mit Python verarbeiten
12Multithreading
13Netzwerk in Python
14Logging in Python
15GPIO
16Automatische Tests
17Datenbanken mit Python
18Python: Generatoren und List Comprehension
19Python: Webseiten mit Flask
20Python virtuelle Umgebungen
21Interrupts & Signale
22NumPy
23Matplotlib
24match
25Reguläre Ausdrücke Python Tutorial Teil 25

Netzwerk im Internet

Das hauptsächlich benutzte Protokoll im Internet ist das Transmission Control Protocol (TCP), um mir hier langwierige Erklärungen zu ersparen, verweise ich dich auf den Artikel in der Wikipedia.

Request / Response Mechanismus

Request / Response Mechanismus zwischen Client und Server im Netzwerk
Request / Response Mechanismus zwischen Client und Server im Netzwerk

Üblicherweise stellt ein Client einen Request an den Server, der daraufhin mit einem Response antwortet. Beispielsweise ist beim Web-Browser der Request, eine bestimmte Datei zu schicken, der Web-Server packt entweder den Inhalt der Datei in den Request oder eine Fehlermeldung, falls diese nicht verfügbar ist.
Üblicherweise wird der Server nicht einen Request verarbeiten, sondern eine große Menge Anforderungen vearbeiten, weswegen der Servercode üblicherweise im Multithreading programmiert wird.

Netzwerk ZUgriff mit dem urllib Package

Zunächst will ich dir zeigen, wie einfach die index.html dieser Website mit Python runterladen kannst. Dazu gibt es das urllib Package

# encoding: UTF-8
import urllib.request

# URL öffnen und Daten anfordern. Ohne spezielle Dateiangabe
# liefert der Webserver immer die index.html zurück.
fp = urllib.request.urlopen('https://raspithek.de')

# Daten auslesen
data = fp.read()

# Die Daten sin vom Datentyp Bytes und müssen daher erst
# zum UTF-8 String umkodiert werden.
str = data.decode('utf8')

fp.close()

print(str)

Die Methode urlopen() setzt den Request ab und wartet auf den Response.

Netzwerk ZUgriff mit dem request Package

Wie so oft gibt es ein Package, dass uns die Arbeit noch etwas einfacher macht. Mit dem requests Package entfällt das umkodieren des Response-Inhalts zu einem String.

import requests

url = requests.get('https://raspithek.de')


data = url.text


print(data)

Fibonacci Server

Als Beispiel für ein Client/Server-Modell, wollen wir im folgenden einen Fibonacci Server an den Start bringen. Der Server nimmt eine Integer Zahl entgegen und berechnet die dazugehörige Zahl aus der Fibonacci Folge und sendet das Ergebnis an den Client zurück. Dazu benötigen wir erstmal etwas Theorie


Die Fibonacci Folge

Die Fibonacci Folge wurde vom Mathematiker Leonardo da Pisa (auch Fibonacci genannt) im 13. Jhdt. beschrieben, um das Wachstum einer Kaninchen Population zu beschreiben. Ein Element der Folge berechnet sich aus der Summe seiner beiden Vorgänger in der Folge oder mathematisch ausgedrückt f(n)= fn(n-1) + f(n-2), wobei f(0) = 0 und f(1) = 1 definiert sind.

Zum besseren Verständnis liste ich die ersten zehn Elemente der Fibonacci Folge hier mal auf.

ListenelementWert
00
11
21
32
43
55
68
713
821
934

Als Profi hast du vermutlich selbst gerade ausgerechnet, dass das f(10) = 55 ist.


Einschub Rekursion

Die Fibonacci Folge ist das klassische Beispiel für eine rekursive Funktion. Das ist eine Funktion, die sich selber mit anderen Parameterwerten wieder aufruft.

Zu einer rekursiven Methode gehören immer der Rekursionsaufruf und die Rekursionsbedingung.

Der Rekursionsaruf sorgt, wie der Name sagt, dass sich die Funktion selbst wieder aufruft.
Die Rekursionsbedingung dient dazu, die Rekursion an geeigneter Stelle zu beenden, da der Aufruf sonst unendlich weiterlaufen würde.

#fib.py 
import sys

# berechnet rekursiv die Fibonaccizahl n.
def fib(n):

# Rekursionsbedingung
  if n in [0,1]:
    return 1
  else:
# Rekursionsaufruf
    return fib(n-1) + fib(n-2)

element = int(sys.argv[1])
print(fib(element))

Ich habe hier eine Neuerung eingebaut, nähmlich das Auslesen eines über die Kommandozeile übergeben Parameters an das Programm. Du kannst mit dem Aufruf

python fib.py 6

von aussen steuern, welches Element der Fibonacci Reihe berechnet werden soll. Der erste übergebene Parameter wird dem Programm in sys.arg[1] mitgeliefert. Der zweite in arg[2], der dritte in arg[3] usw. In argv[0] steht übrigens der Dateiname des Programm selbst.

Der Parameter wird dann als Integer in die Methode fib() gesteckt, die sich mit

fib(n-1) + fib(n-2)

zweimal selber rekursiv aufruft, das ist der Rekursionsaufruf (der hier sogar zweimal passiert).

Die Rekursionsbedingung zum Beenden der Rekursion passiert mit

 if n in [0,1]:
    return 1

Da fib(0 und fib(1) jeweils als 1 definiert sind.

Protokolle, Ports und Sockets

Ein Socket ist ein Objekt, dass uns Python zur Verfügung stellt, um über einen Port Daten zu senden und zu empfangen.

schematische Darstellung des Zusammenspiels von Protokollen, Ports und Sockets im Netzwerk

Jetzt geht’s endlich ans Netzwerk. Du kennst natürlich einige Protokolle im Internet wie bspw. http, ssh und ftp. Jedem Protokoll ist ein Port zugeordnet (http:80, ssh 22, ftp, 21). Der Port dient dazu, auf dem Zielcomputer den ankommenden Request dem richtigen Programm zuzuordnen, da ein Computer durchaus mehrere Services anbieten kann.

Der Fibonacci Server

Unser Testserver soll das gleiche machen, wie das obige Programm, mit dem Unterschied, dass der Kommdozeilenparameter übers Netzwerk empfangen werden soll und das Ergebnis auf gleichem Weg zurückgeschickt wird. Zunächst brauchen wir einige Konstanten. Da diese für Server und Client identisch sind, habe ich sie in eine Datei const.py im Modul network ausgelagert.

import socket

# TCP Port
__PORT__ = 6554

#Server-IP 
# für den Server (immer localhost)
__SERVERLOCAL__ = socket.gethostbyname(socket.gethostname())

#für Server auf einem Remote-Pi
__SERVERREMOTE__ = socket.gethostbyname('gabbo')

# Server-Adresse (IP,Port)
#lokal
__ADDRESSLOCAL__ = (__SERVERLOCAL__,__PORT__)
#remote
__ADDRESSREMOTE__ = (__SERVERREMOTE__,__PORT__)

Mit der Methode socket.gethostbyname() wird die IP des Zielservers ermittelt und damit zusammen mit dem __PORT__ ein Tupel mit der Adresse des Servers gebildet. Da Client und Server in diesem Beispiel auf einem Raspberry Pi laufen sollen, kannst du den Hostname mit socket.gethostname() auslesen. Soll der Server auf einem anderen Raspi laufen, benutzt du __ADRESSREMOTE__ und trägst bei __SERVERREMOTE__ den Hostname deine Raspberry Pi ein..

Nun kommen wir zum eigentlichen Servercode.

#! /usr/bin/python3

import socket
import threading
from network.const import __ADDRESS__

# Server-Socket aufbauen
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#Socket an unsere Server-Adresse binden
server.bind(__ADDRESS__)

# Set mit den verbundenen Clients
clients = set()

# Locks für den Zugriff auf das Set
clients_lock = threading.Lock()

# Unsere Arbeitsmethode: rekursive Fibonacci-Reihe
def fib(n):

# Rekursionsbedingung
  if n in [0,1]:
    return 1
  else:
# Rekursionsaufruf
    return fib(n-1) + fib(n-2)

#bearbeitet eine eingehende Connection annehmen, pro Client  ein seperater Thread
def handle_connection(conn,addr):
  print(f"[NEW CONNECTION] {addr} Connected")

  try:
    connected = True
    while connected:
      msg = conn.recv(64).decode('utf-8')
      print(f'[RECEIVED] {msg}')
      if not msg:
        break
      else:
        if msg.startswith('fib:'):
          element = int(msg[4:])
          fibo = fib(element)
          print(f'[SENDING] fib({element})={fibo}')
# Ergebnis an Client senden.
          with clients_lock:
            for c in clients:
              c.sendall(f'{fibo}'.encode('utf-8'))
        else:
          pass

        if msg == 'quit':
          print('[DISCONNECT]')
          connected = False

      with clients_lock:
        for c in clients:
          c.sendall(f"[{addr}] {msg}".encode('utf-8'))

  finally:
# Clientverbindung abbauen und aus Set entfernen.
      with clients_lock:
        clients.remove(conn)

        conn.close()


# Start des Servers
def start():
    print('[SERVER STARTED]!')
# Horchen nach eingehender Verbindung
    server.listen()
    while True:
# Neue Verbindung annehmen

        conn, addr = server.accept()
# Neuen Client in Set aufnehmen
        with clients_lock:
            print(f'[ADDING CONNECTION] {conn}')
            clients.add(conn)

#Thread zur Bearbeitung des Requests starten.
        thread = threading.Thread(target=handle_connection, args=(conn, addr))
        thread.start()

#Hauptprogramm: Serverstart einleiten
if __name__ == '__main__':
  start()

Ich habe zwei neue Dinge eingebaut, zum einen den „Shebang“ in der ersten Zeile (ja, das heißt wirklich so). Dies erspart dir dir Angabe des Python-Interpreters zum Programmstart. Wenn du mit

chmod u+x fibserver.py

Das Skript als Executable markierst, findet die Shell den Interpreter über den Shebang und führt diesen mit Angabe des Pythoncodes aus. Du kannst den Server einfach mit

./fibserver.py

starten. Die andere Neuheit ist dieser Codeteil

#Hauptprogramm: Serverstart einleiten
if __name__ == '__main__':
  start()

Dies ist eine Konvention, die im Hauptprogramm auftauchen sollte. Andere Programmierer, die das sehen, wissen dann, dass diese Datei das Hauptprogramm enthält.

Ansonsten macht das Serverprogramm nicht anderes, als auf Port 6554 auf eine eingehende Verbindung zu warten. Sobald ein Client diese aufbaut, wird in einem neuen Thread der Request bearbeitet und die angeforderte Fibonaccizahl berechnet. Ich habe mich hier für ein einfaches Protokoll entschieden. Der Request muss lediglich den String fib:<element> enthalten.

Ein Request, der nicht mit fib: beginnt wird einfach ignoriert, sonsten wird mittels

element = int(msg[4:])

Die Elementnummer aus dem Request isoliert. Nach der Berechnung der Fibonaccizahl wird diese einfach über die sendall() Methode an alle verbundenen Clients zurückgesendet (das ist etwas praxisfremd und soll nur nochmal den Locking-Mechanismus für Threads demonstrieren). Wenn allerdings im Request der String ‚quit‘ gesendet wird, wird der Thread für diesen Client beendet und die dazugehörige Verbindung abgebaut.

Der Fibonacci Client

Der Fibonacci Client hingegen hat folgende Aufgabe: Er baut eine Verbindung zum Server auf und nimmt dann eine Eingabe über die Tastatur entgegen. Diese wird dann direkt an den Server geschickt oder mit ‚q‘ die Verbindung abgebaut.


Test

Um die Kommunikation zwischen Client und Server zu testen, musst du zwei ssh-Verbindungen aufmachen. In der einen startest du den Server

./fibserver.py

Im anderen den Client

python fibclient.py

Ich hab den Server mal auf dem Raspberry Pi aus dem Homserver-Projekt gestartet.Ich stell hier mal die Ausgaben des Servers und des Clients untereinander:

Ausgabe von fibserver.py
Ausgabe von fibclient.py

Das war’s zum Thema Netzwerkprogrammierung, im nächsten Teil will ich dir das Logging etwas näher bringen.

Schreibe einen Kommentar

Cookie Consent Banner von Real Cookie Banner