6 Netzwerk-Clients und -Server

6.1 Einführung in Netzwerkprogrammierung

Die Netzwerkprogrammierung bezieht sich auf die Erstellung von Programmen, die in der Lage sind, mit anderen Systemen über das Netzwerk zu kommunizieren. Dazu werden in der Regel Kommunikationsprotokolle wie TCP, UDP, HTTP oder SMTP verwendet. Die Kommunikation erfolgt über Sockets, die den Datenaustausch zwischen den beteiligten Systemen ermöglichen.

Ein Socket ist ein Endpunkt einer Kommunikationsverbindung, der eine eindeutige Adresse und einen bestimmten Port aufweist. Ein Serverprogramm kann beispielsweise auf einem bestimmten Port lauschen und auf eingehende Verbindungen von Clientprogrammen warten, die eine Verbindung zu diesem Port herstellen möchten. Wenn eine Verbindung hergestellt wird, kann das Serverprogramm Daten an den Client senden oder von ihm empfangen.

Ein Clientprogramm kann eine Verbindung zu einem Serverprogramm herstellen, indem es dessen IP-Adresse und Portnummer angibt. Sobald die Verbindung hergestellt ist, kann der Client Daten an den Server senden oder von ihm empfangen.

Die Netzwerkprogrammierung erfordert ein Verständnis der Netzwerkarchitektur und der Netzwerkprotokolle sowie der verschiedenen Methoden zur Übertragung von Daten zwischen verschiedenen Systemen. In Python gibt es Module wie socket, urllib und smtplib, die die Netzwerkprogrammierung unterstützen.

6.2 Das socket-Modul

import socket

# Erstellt einen Socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Binde den Socket an eine Adresse und einen Port
server_socket.bind(('localhost', 8000))

# Warte auf eingehende Verbindungen
server_socket.listen()

print('Server läuft auf Port 8000...')

while True:
    # Akzeptiere eingehende Verbindungen
    client_socket, address = server_socket.accept()
    
    # Empfange Daten vom Client
    data = client_socket.recv(1024)
    print(f"Empfangene Daten: {data.decode('utf-8')}")
    
    # Sende Daten zurück zum Client
    response = "Hallo Client!"
    client_socket.send(response.encode('utf-8'))
    
    # Schließe die Verbindung zum Client
    client_socket.close()

Dieses Beispiel zeigt, wie man einen TCP-Server mit dem socket-Modul erstellt. Zuerst wird ein Socket erstellt und an eine Adresse und einen Port gebunden. Dann wartet der Server auf eingehende Verbindungen und akzeptiert sie. Sobald eine Verbindung hergestellt wurde, empfängt der Server Daten vom Client, sendet eine Antwort zurück und schließt die Verbindung.

6.3 TCP-Server und -Clients

# Server
import socket

HOST = ''  # Symbolischer Name, der alle verfügbaren Schnittstellen anspricht
PORT = 8888  # Eine ungültige Portnummer wählen

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)
    print('Server läuft auf Port', PORT)
    conn, addr = s.accept()
    with conn:
        print('Verbunden von', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

# Client
import socket

HOST = 'localhost'  # Der Remote-Hostname oder die IP-Adresse
PORT = 8888         # Der Remote-Port, auf dem der Server hört

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

print('Empfangene Daten:', repr(data))

In diesem Beispiel erstellen wir einen TCP-Server, der auf Port 8888 lauscht, und einen TCP-Client, der sich mit diesem Server verbindet und eine Nachricht sendet. Der Server empfängt die Nachricht und sendet sie zurück an den Client, der sie dann empfängt und ausgibt.

In der Serverimplementierung rufen wir socket.bind() auf, um den Socket an den angegebenen Hostnamen und Port zu binden, und dann socket.listen() auf, um den Socket in den empfangsbereiten Modus zu versetzen. Wir rufen dann socket.accept() auf, um auf eingehende Verbindungen zu warten. Wenn eine Verbindung empfangen wird, akzeptieren wir sie mit conn, addr = s.accept() und empfangen dann Daten vom Client mit data = conn.recv(1024). Wenn wir keine Daten mehr empfangen, brechen wir die Verbindung ab. Bevor wir die Verbindung schließen, senden wir die empfangenen Daten zurück an den Client mit conn.sendall(data).

In der Clientimplementierung rufen wir socket.connect() auf, um eine Verbindung zum Server herzustellen, und senden dann eine Nachricht mit socket.sendall(). Wir empfangen dann die Antwort des Servers mit socket.recv(1024) und geben sie aus.

Dieses Beispiel zeigt die grundlegenden Konzepte der TCP-Server- und Client-Programmierung in Python und kann als Ausgangspunkt für die Entwicklung von robusten Serveranwendungen dienen.

6.4 UDP-Server und -Clients

UDP-Server:

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('localhost', 8888))

print("UDP server is listening...")

while True:
    data, address = server_socket.recvfrom(1024)
    print(f"Received {data.decode()} from {address}")

UDP-Client:

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

message = "Hello, UDP server!"
client_socket.sendto(message.encode(), ('localhost', 8888))

Beachte, dass bei UDP-Verbindungen keine explizite Verbindung zwischen Server und Client aufgebaut wird. Der Client sendet einfach seine Nachrichten an die Adresse und den Port des Servers, und der Server empfängt sie, ohne zuvor eine Verbindung aufzubauen. Auch die Fehlerbehandlung ist bei UDP-Verbindungen weniger robust als bei TCP-Verbindungen, da Pakete verloren gehen können.

6.5 Verwenden von Threads für mehrere Verbindungen

Das Verwenden von Threads ist eine Möglichkeit, um mehrere Verbindungen gleichzeitig in einem Server zu verarbeiten. Dazu kann das threading-Modul in Python genutzt werden.

Beim Multithreaded Server wird für jeden Client eine eigene Instanz des Servers gestartet, um parallele Verbindungen zu ermöglichen. Die Verarbeitung der Verbindungen erfolgt dann in eigenen Threads, wodurch jeder Thread eine Verbindung parallel abarbeiten kann.

Das threading-Modul bietet hierfür die Klasse Thread an, die zur Erstellung von Threads verwendet werden kann. Jeder Thread kann dann in einer Schleife auf neue Verbindungen warten und diese parallel abarbeiten.

Bei der Verwendung von Threads müssen jedoch auch Synchronisationsprobleme und Thread-Sicherheit beachtet werden. Es kann vorkommen, dass verschiedene Threads gleichzeitig auf dieselben Ressourcen zugreifen und somit Dateninkonsistenzen oder andere Fehler verursachen können. Daher ist es wichtig, den Zugriff auf gemeinsame Ressourcen in Threads zu synchronisieren und sicherzustellen, dass nur ein Thread gleichzeitig auf eine Ressource zugreifen kann.

import socket
import threading

# Server-Adresse und -Port
SERVER_ADDRESS = 'localhost'
SERVER_PORT = 12345

# Handler-Funktion für Client-Verbindung
def handle_client(client_socket, client_address):
    print('Verbindung von', client_address)

    # Empfangen und Senden von Daten
    data = client_socket.recv(1024)
    client_socket.sendall(data)

    # Verbindung schließen
    client_socket.close()
    print('Verbindung zu', client_address, 'geschlossen')

# Erstellen des Serversockets
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((SERVER_ADDRESS, SERVER_PORT))
server_socket.listen(5)

print('Server gestartet auf', SERVER_ADDRESS, ':', SERVER_PORT)

# Endlosschleife für Verbindungsaufnahme
while True:
    # Warten auf eingehende Verbindungen
    client_socket, client_address = server_socket.accept()

    # Starten eines Threads für Client-Verarbeitung
    client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
    client_thread.start()

Dieser Server empfängt Verbindungen von Clients und startet für jede Verbindung einen separaten Thread, um die Verarbeitung parallel auszuführen. Der handle_client-Handler empfängt die Daten vom Client und sendet sie zurück, bevor er die Verbindung schließt.

Es ist wichtig zu beachten, dass die Synchronisation und Thread-Sicherheit bei der Verwendung von Threads für die Verarbeitung von Verbindungen berücksichtigt werden müssen, um Probleme wie Rennbedingungen und Deadlocks zu vermeiden.

6.6 SSL/TLS-Verbindungen

SSL (Secure Sockets Layer) und TLS (Transport Layer Security) sind Verschlüsselungsprotokolle, die verwendet werden, um sichere Verbindungen zwischen Netzwerkanwendungen herzustellen. Das ssl-Modul in Python bietet eine einfache Möglichkeit, SSL/TLS-Verbindungen in Python zu implementieren.

Um einen SSL/TLS-Server in Python zu erstellen, muss ein SSL/TLS-Zertifikat erstellt werden. Dies kann mithilfe des openssl-Befehls erfolgen. Anschließend kann ein SSL/TLS-Server erstellt werden, indem das Socket erstellt und mit dem ssl.wrap_socket() -Methode in eine verschlüsselte Verbindung umgewandelt wird.

Ein Beispiel für einen einfachen SSL/TLS-Server:

import socket, ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('server.crt', 'server.key')

bindsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bindsocket.bind(('localhost', 10023))
bindsocket.listen(5)

while True:
    newsocket, fromaddr = bindsocket.accept()
    conn = context.wrap_socket(newsocket, server_side=True)
    conn.send(b"Hello, client!")
    conn.close()

Um einen SSL/TLS-Client in Python zu erstellen, muss ebenfalls eine SSL/TLS-Verbindung mit dem ssl.wrap_socket() -Methode erstellt werden. Der Client kann dann Daten über diese Verbindung senden und empfangen.

Ein Beispiel für einen einfachen SSL/TLS-Client:

import socket, ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations('server.crt')

clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sslsocket = context.wrap_socket(clientsocket, server_hostname='localhost')
sslsocket.connect(('localhost', 10023))

print(sslsocket.recv(1024))
sslsocket.close()

Das ssl-Modul bietet auch eine einfache Möglichkeit, die Verschlüsselung zu testen, indem es eine verschlüsselte Verbindung mit sich selbst erstellt. Dadurch kann man sicherstellen, dass die Implementierung korrekt ist und die Verbindung sicher ist.

import socket, ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('server.crt', 'server.key')

bindsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bindsocket.bind(('localhost', 10023))
bindsocket.listen(5)

with context.wrap_socket(socket.socket()) as ssock:
    ssock.connect(('localhost', 10023))
    ssock.sendall(b"Hello, server!")
    print(ssock.recv(1024))

conn, addr = bindsocket.accept()
with conn:
    sslconn = context.wrap_socket(conn, server_side=True)
    print(sslconn.recv(1024))
    sslconn.sendall(b"Hello, client!")

Dieses Beispiel erstellt sowohl einen Server als auch einen Client und stellt sicher, dass beide Enden der Verbindung korrekt funktionieren und sicher sind.

6.7 HTTP-Server und -Clients

Das HTTP-Protokoll ist ein grundlegendes Konzept für die Übertragung von Daten im World Wide Web. HTTP steht für Hypertext Transfer Protocol und ist das Standardprotokoll für die Kommunikation zwischen Webservern und Webclients. Es definiert die Regeln für die Formatierung von Nachrichten, die Art der Nachrichten und die Aktionen, die von einem Webclient oder Webserver ausgeführt werden können.

Ein Webclient (z.B. ein Webbrowser) sendet eine HTTP-Anforderung an einen Webserver, um eine bestimmte Ressource (z.B. eine HTML-Seite oder eine Datei) anzufordern. Der Webserver empfängt die Anforderung, verarbeitet sie und sendet eine HTTP-Antwort zurück, die die angeforderte Ressource enthält oder einen Fehlercode und eine Fehlermeldung, falls die Ressource nicht verfügbar ist oder ein anderer Fehler aufgetreten ist.

HTTP-Anforderungen und -Antworten bestehen aus einem Header und einem optionalen Body. Der Header enthält Metadaten zur Identifizierung der Nachricht, einschließlich des Verwendungszwecks, des Zielservers und der Version des Protokolls. Der Body enthält die eigentlichen Daten, z.B. eine HTML-Seite oder ein JSON-Dokument.

HTTP-Server und -Clients können mit verschiedenen Programmiersprachen implementiert werden, darunter Python. Python bietet verschiedene Bibliotheken und Module, die die Implementierung von HTTP-Servern und -Clients erleichtern, z.B. das urllib-Modul und das http.server-Modul.

Das Verständnis von HTTP-Protokollen und -Clients ist von wesentlicher Bedeutung für die Entwicklung von Webanwendungen und die Interaktion mit dem World Wide Web.

Anfragemethode Beschreibung
GET Abruf von Daten und Ressourcen
POST Übermittlung von Daten zur Verarbeitung
PUT Aktualisierung oder Erstellung von Daten oder Ressourcen
DELETE Löschen von Daten oder Ressourcen

Die GET-Methode wird verwendet, um Daten oder Ressourcen von einem Server abzurufen. Die angeforderten Informationen werden in der Regel als Teil der URL angegeben, und die Antwort wird in Form von Text, HTML oder anderen Daten zurückgegeben.

Die POST-Methode wird verwendet, um Daten an einen Server zu senden, die dann verarbeitet werden sollen. Die Daten werden normalerweise in einem Formular eingegeben und können aus Text, Dateien oder anderen Inhalten bestehen. Der Server kann dann auf diese Daten zugreifen und entsprechend reagieren.

Die PUT-Methode wird verwendet, um Daten oder Ressourcen auf einem Server zu aktualisieren oder zu erstellen. Die angegebenen Daten ersetzen die vorhandenen Daten oder erstellen eine neue Ressource. Diese Methode kann beispielsweise verwendet werden, um eine Datei auf einem Server zu aktualisieren oder eine neue Datei zu erstellen.

Die DELETE-Methode wird verwendet, um Daten oder Ressourcen auf einem Server zu löschen. Wenn eine DELETE-Anfrage gesendet wird, wird die angegebene Ressource entfernt oder gelöscht. Diese Methode kann beispielsweise verwendet werden, um eine Datei auf einem Server zu löschen oder eine Benutzerdatenbank zu bereinigen.

6.8 Einfacher HTTP-Server mit dem http.server-Modul

Das http.server-Modul ist eine Standardbibliothek in Python, die verwendet wird, um einen einfachen HTTP-Server zu erstellen. Mit diesem Modul kann ein Server erstellt werden, der auf Anforderungen von Clients (z. B. Browser) antwortet.

Ein HTTP-Server kann sehr nützlich sein, um statische Inhalte zu liefern, z. B. HTML-, CSS- oder JavaScript-Dateien. Es kann auch verwendet werden, um dynamische Inhalte zu liefern, indem es mit einer Programmiersprache wie Python oder PHP kombiniert wird.

Die Verwendung des http.server-Moduls ist sehr einfach. Es gibt nur eine Klasse namens HTTPServer, die zum Erstellen eines Servers verwendet wird. Eine Instanz dieser Klasse muss mit einem Hostnamen und einem Port erstellt werden. Der Server kann dann mit der serve_forever()-Methode gestartet werden.

from http.server import HTTPServer, SimpleHTTPRequestHandler

host = 'localhost'
port = 8000

httpd = HTTPServer((host, port), SimpleHTTPRequestHandler)
httpd.serve_forever()

Das obige Beispiel erstellt einen einfachen HTTP-Server, der auf localhost auf Port 8000 lauscht. Wenn ein Client eine Anforderung an den Server sendet, gibt der Server den angeforderten Inhalt zurück. Standardmäßig wird der aktuelle Arbeitsverzeichnisinhalt zurückgegeben.

Es ist auch möglich, einen benutzerdefinierten RequestHandler zu erstellen, indem eine Unterklasse von BaseHTTPRequestHandler erstellt wird. Die Methoden do_GET(), do_POST(), do_HEAD() und andere können überschrieben werden, um die Verarbeitung von Anforderungen anzupassen.

from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write(b'Hello, world!')

host = 'localhost'
port = 8000

httpd = HTTPServer((host, port), MyHTTPRequestHandler)
httpd.serve_forever()

Dieses Beispiel erstellt einen Server, der eine einfache Hello, world!-Nachricht zurückgibt, wenn eine GET-Anforderung empfangen wird.

Das http.server-Modul ist für Test- oder Entwicklungszwecke geeignet, aber für den Einsatz in der Produktion sind leistungsfähigere und sicherere Server wie Apache oder Nginx zu bevorzugen.

6.9 Einfacher HTTP-Client mit dem urllib-Modul

Das urllib-Modul ist ein Standard-Modul in Python, das die Arbeit mit URLs, HTTP-Anfragen und -Antworten vereinfacht. Das urllib.request-Modul in diesem Paket stellt eine einfache Möglichkeit dar, HTTP-Anfragen zu erstellen und HTTP-Antworten zu verarbeiten.

Um eine HTTP-Anfrage zu erstellen, können wir die Funktion urllib.request.urlopen() verwenden, die eine URL als Argument annimmt. Diese Funktion öffnet eine Verbindung zum angegebenen Server und gibt ein Objekt zurück, das die HTTP-Antwort repräsentiert. Wir können dann auf den Inhalt der Antwort zugreifen und sie weiterverarbeiten.

import urllib.request

# Eine GET-Anfrage an eine URL senden
response = urllib.request.urlopen('http://example.com/')
html = response.read()
print(html)

Um eine HTTP-POST-Anfrage zu senden, können wir ein urllib.request.Request-Objekt erstellen und die POST-Daten als Parameter übergeben. Wir können auch die Header anpassen, indem wir ein dict-Objekt an die headers-Parameter übergeben.

import urllib.request
import urllib.parse

# Eine POST-Anfrage an eine URL senden
url = 'http://httpbin.org/post'
data = {'name': 'John Doe', 'age': 25}
data = urllib.parse.urlencode(data).encode('utf-8')
headers = {'User-Agent': 'Mozilla/5.0'}
req = urllib.request.Request(url, data=data, headers=headers)
response = urllib.request.urlopen(req)
json_response = response.read().decode('utf-8')
print(json_response)

Das urllib-Modul bietet auch andere Methoden wie PUT, DELETE und HEAD, die auf ähnliche Weise verwendet werden können.

Zusätzlich zum Erstellen von HTTP-Anfragen bietet das urllib-Modul auch Methoden zum Parsen von URLs und zum Verarbeiten von Cookies.