Eine vollständige Anleitung zur Socket-Programmierung in Python
Geräte miteinander zu verbinden, um Informationen auszutauschen, darum geht es bei der Vernetzung. Sockets sind ein wesentlicher Bestandteil einer effektiven Netzwerkkommunikation, da sie das zugrunde liegende Konzept sind, um Nachrichten zwischen Geräten über lokale oder globale Netzwerke und verschiedenen Prozessen auf demselben Rechner zu übertragen. Sie bieten eine Low-Level-Schnittstelle, die eine feinkörnige Kontrolle über den zu sendenden oder zu empfangenden Datenverkehr ermöglicht.
Diese niedrige Ebene macht es möglich, sehr leistungsfähige Kommunikationskanäle (oder benutzerdefinierte Protokolle) für bestimmte Anwendungsfälle mit geringem Overhead zu erstellen, der bei herkömmlichen Protokollen, die auf der Socket-Kommunikation aufbauen, auftreten kann.
Das macht Sockets besonders nützlich für Echtzeit-Client-Server-Anwendungen, die auf sofortigen Nachrichtenaustausch angewiesen sind oder mit großen Datenmengen arbeiten.
In diesem Artikel werden wir die Grundlagen der Socket-Programmierung behandeln und eine Schritt-für-Schritt-Anleitung für die Erstellung von Socket-basierten Client- und Server-Anwendungen mit Python geben. Also lasst uns ohne Umschweife gleich loslegen!
Grundlagen der Netzwerkarbeit
Die Vernetzung ermöglicht Kommunikation und Informationsaustausch jeglicher Art.
Es ist ein Prozess, bei dem zwei oder mehr Geräte miteinander verbunden werden, damit sie Informationen austauschen können. Eine Sammlung solcher miteinander verbundener Geräte wird als Netzwerk bezeichnet.
Es gibt viele Netzwerke, die wir in unserer physischen Welt beobachten können: Flug- oder Stromleitungsnetze oder Städte, die über Autobahnen miteinander verbunden sind, sind einige gute Beispiele.
Genauso gibt es in der Informationstechnologie zahlreiche Netzwerke. Das bekannteste ist das Internet, das globale Netzwerk, das unzählige Geräte miteinander verbindet und das du wahrscheinlich gerade nutzt, um diesen Artikel zu lesen.
Arten von Netzwerken
Das Internet enthält viele weitere Netzwerke, die sich durch ihre Größe oder andere Eigenschaften unterscheiden: zum Beispiel lokale Netzwerke (LANs), die in der Regel Computer verbinden, die sich in unmittelbarer Nähe zueinander befinden. Maschinen in Unternehmen oder anderen Institutionen (Banken, Universitäten usw.) oder auch deine privaten Geräte, die an einen Router angeschlossen sind, bilden ein solches Netzwerk.
Es gibt auch größere oder kleinere Netzwerktypen wie PANs (Personal Area Network), bei denen es sich einfach um dein Smartphone handelt, das über Bluetooth mit einem Laptop verbunden ist, MANs (Metropolitan Area Network), die Geräte in der ganzen Stadt miteinander verbinden können, und WANs (Wide Area Network), die ganze Länder oder die ganze Welt abdecken können. Und ja, das größte WAN-Netzwerk ist das Internet selbst.
Es versteht sich von selbst, dass Computernetzwerke sehr komplex sein können und aus vielen Elementen bestehen. Eines der grundlegendsten und wichtigsten Primitive ist ein Kommunikationsprotokoll.
Arten von Netzwerkkommunikationsprotokollen
Kommunikationsprotokolle legen die Regeln fest, wie und in welchem Format Informationen gesendet und empfangen werden sollen. Diese Protokolle sind in einer Hierarchie zusammengefasst, um die verschiedenen Aufgaben bei der Netzwerkkommunikation zu verwalten.
Mit anderen Worten: Einige Protokolle regeln, wie die Hardware Pakete empfängt, sendet oder weiterleitet, während andere Protokolle auf einer höheren Ebene angesiedelt sind und sich z. B. mit der Kommunikation auf Anwendungsebene befassen.
Zu den häufig verwendeten und weithin bekannten Netzwerkkommunikationsprotokollen gehören:
Wi-Fi
Ein Beispiel für ein Link-Layer-Protokoll, d.h. es sitzt sehr nah an der Hardware und ist für die physische Übertragung von Daten von einem Gerät zum anderen in einer drahtlosen Umgebung verantwortlich.
IP (Internetprotokoll)
IP ist ein Protokoll der Netzwerkschicht, das hauptsächlich für das Routing von Paketen und die IP-Adressierung zuständig ist.
TCP (Transmission Control Protocol)
Ein zuverlässiges, verbindungsorientiertes Protokoll, das Vollduplex-Kommunikation ermöglicht und die Datenintegrität und -übermittlung sicherstellt. Dies ist ein Protokoll der Transportschicht, das Verbindungen verwaltet, Fehler erkennt und den Informationsfluss kontrolliert.
UDP (User Datagram Protocol)
Ein Protokoll aus der gleichen Protokollsuite wie TCP. Der Hauptunterschied besteht darin, dass UDP ein einfacheres, schnelleres, aber unzuverlässigeres verbindungsloses Protokoll ist, das keine Zustellungsprüfungen durchführt und dem Paradigma des "fire-and-forget" folgt. Wie TCP befindet sich auch UPD auf der Transportschicht.
HTTP (Hypertext Transfer Protocol)
Ein Protokoll der Anwendungsschicht und das am häufigsten verwendete Protokoll für die Kommunikation zwischen Browsern und Servern im Internet, das vor allem für die Bereitstellung von Websites verwendet wird. Es versteht sich von selbst, dass auch dieser Artikel, den du gerade liest, über HTTP bereitgestellt wurde. Das HTTP-Protokoll baut auf TCP auf und verwaltet und überträgt für Webanwendungen relevante Informationen wie Header, die zur Übertragung von Metadaten und Cookies verwendet werden, verschiedene HTTP-Methoden (GET, POST, DELETE, UPDATE) usw.
MQTT (Message Queue Telemetry Transport)
Ein weiteres Beispiel für ein Protokoll auf Anwendungsebene, das für Geräte mit begrenzter Rechenleistung und Batterielebensdauer verwendet wird, die unter unzuverlässigen Netzwerkbedingungen arbeiten (z. B. Gassensoren in einem Bergbaugebiet oder einfach eine intelligente Glühbirne in deinem Haus). MQTT ist ein Standard-Nachrichtenprotokoll, das im IoT (Internet der Dinge) verwendet wird. Sie ist leicht und einfach zu bedienen und verfügt über eingebaute Übermittlungsmechanismen für mehr Zuverlässigkeit. Wenn du daran interessiert bist, dieses Protokoll mit Python zu nutzen, kannst du diesen Python MQTT-Leitfaden lesen, der einen detaillierten Überblick über den Paho MQTT-Client gibt.
Eine wichtige Beobachtung ist, dass alle oben genannten Protokolle unter der Haube Sockets verwenden, aber ihre eigene Logik und Datenverarbeitung darüber legen. Das liegt daran, dass Sockets eine Low-Level-Schnittstelle für die Netzwerkkommunikation in modernen Geräten sind, wie wir im nächsten Abschnitt erläutern werden.
Wichtige Konzepte und Begriffe
Natürlich gibt es noch eine Menge anderer wichtiger Konzepte und Begriffe, die im Zusammenhang mit Netzwerken verwendet werden. Im Folgenden findest du eine kurze Übersicht über einige der wichtigsten Punkte, die im weiteren Verlauf des Lehrgangs auftauchen können:
- Paket: eine Standardeinheit für die Datenübertragung in einem Computernetzwerk (man könnte es umgangssprachlich mit dem Begriff "Nachricht" vergleichen).
- Endpunkt: ein Ziel, an dem die Pakete ankommen.
- IP-Adresse: Eine numerische Kennung, die ein Gerät im Netzwerk eindeutig identifiziert. Ein Beispiel für eine IP-Adresse ist: 192.168.0.0
- Ports: eine numerische Kennung, die einen Prozess, der auf einem Gerät läuft und bestimmte Netzwerkkommunikationen abwickelt, eindeutig identifiziert: Er bedient zum Beispiel deine Website über HTTP. Während eine IP-Adresse das Gerät identifiziert, identifiziert ein Port die Anwendung (jede Anwendung ist ein Prozess oder besteht aus Prozessen). Bekannte Beispiele für Ports sind: Port 80, der üblicherweise von Serveranwendungen verwendet wird, um den HTTP-Verkehr zu verwalten, und Port 443 für HTTPS (sicheres HTTP).
- Gateway: eine besondere Art von Netzwerkknoten (Gerät), der als Zugangspunkt von einem Netzwerk zu einem anderen dient. Diese Netzwerke können sogar unterschiedliche Protokolle verwenden, so dass eine Protokollübersetzung durch das Gateway erforderlich sein kann. Ein Beispiel für ein Gateway ist ein Router, der ein lokales Heimnetzwerk mit dem Internet verbindet.
Steckdosen verstehen
Was ist eine Steckdose?
Ein Socket ist eine Schnittstelle (Tor) für die Kommunikation zwischen verschiedenen Prozessen, die sich auf demselben oder auf verschiedenen Rechnern befinden. Im letzteren Fall sprechen wir von Netzwerk-Sockets.
Netzwerk-Sockets abstrahieren die Verbindungsverwaltung. Du kannst sie dir als Connection Handler vorstellen. Vor allem in Unix-Systemen sind Sockets einfach Dateien, die dieselben Schreib- und Leseoperationen unterstützen, aber alle Daten über das Netzwerk senden.
Wenn sich ein Socket im Listening- oder Verbindungsstatus befindet, ist er immer an eine Kombination aus einer IP-Adresse und einer Portnummer gebunden, die den Host (Rechner/Gerät) und den Prozess identifiziert.
Wie Steckdosenverbindungen funktionieren
Sockets können auf eingehende Verbindungen warten oder selbst ausgehende Verbindungen herstellen. Wenn eine Verbindung hergestellt wird, wird der Listening-Socket (Server-Socket) zusätzlich an die IP und den Port der verbindenden Seite gebunden.
Oder alternativ wird ein neuer Socket erstellt, der nun an zwei Paare von IP-Adressen und Portnummern eines Listeners und eines Requestors gebunden ist. Auf diese Weise können sich zwei verbundene Sockets auf verschiedenen Rechnern gegenseitig identifizieren und eine einzige Verbindung für die Datenübertragung nutzen, ohne den lauschenden Socket zu blockieren, der in der Zwischenzeit weiter auf andere Verbindungen wartet.
Der Socket, der die Verbindung herstellt (Client-Socket), wird beim Verbindungsaufbau implizit an die IP-Adresse des Geräts und eine zufällig zugängliche Portnummer gebunden. Beim Verbindungsaufbau erfolgt dann eine Bindung an die IP und den Port der anderen Kommunikationsseite, ähnlich wie bei einem Listening-Socket, ohne dass ein neuer Socket erstellt wird.
Sockets im Kontext von Netzwerken
In diesem Tutorial geht es nicht um die Implementierung von Sockets, sondern darum, was Sockets im Kontext von Netzwerken bedeuten.
Man kann sagen, dass ein Socket ein Verbindungsendpunkt (Verkehrsziel) ist, der auf der einen Seite mit der IP-Adresse des Host-Rechners und der Portnummer der Anwendung verbunden ist, für die der Socket erstellt wurde, und auf der anderen Seite mit der IP-Adresse und dem Port der Anwendung, die auf einem anderen Rechner läuft, zu dem die Verbindung hergestellt wird.
Socket Programmierung
Wenn wir über Socket-Programmierung sprechen, instanziieren wir Socket-Objekte in unserem Code und führen Operationen mit ihnen durch (hören, verbinden, empfangen, senden usw.). In diesem Zusammenhang sind Sockets einfach spezielle Objekte, die wir in unserem Programm erstellen und die spezielle Methoden für die Arbeit mit Netzwerkverbindungen und Datenverkehr haben.
Unter der Haube rufen diese Methoden den Kernel deines Betriebssystems auf, genauer gesagt den Netzwerkstapel, der ein spezieller Teil des Kernels ist, der für die Verwaltung von Netzwerkoperationen zuständig ist.
Sockets und Client-Server-Kommunikation
Es ist auch wichtig zu erwähnen, dass Sockets oft im Zusammenhang mit der Client-Server-Kommunikation auftreten.
Die Idee ist einfach: Sockets beziehen sich auf Verbindungen; sie sind Verbindungshandler. Wenn du im Internet Daten senden oder empfangen willst, stellst du eine Verbindung her (die über eine Schnittstelle namens Sockets initiiert wird).
Entweder du oder die Partei, mit der du dich verbinden willst, fungiert als Server und eine andere Partei als Client. Während ein Server den Clients Daten zur Verfügung stellt, stellen die Clients proaktiv eine Verbindung her und fordern Daten von einem Server an. Ein Server lauscht über einen Listening-Socket auf neue Verbindungen, baut sie auf, erhält die Anfragen des Clients und übermittelt die angeforderten Daten in seiner Antwort an den Client.
Auf der anderen Seite erstellt ein Client ein Socket mit der IP-Adresse und dem Port des Servers, mit dem er sich verbinden möchte, baut eine Verbindung auf, übermittelt seine Anfrage an den Server und erhält Daten als Antwort. Dieser nahtlose Informationsaustausch zwischen den Client- und Server-Sockets bildet das Rückgrat verschiedener Netzwerkanwendungen.
Sockets als Basis für Netzwerkprotokolle
Die Tatsache, dass Sockets ein Rückgrat bilden, bedeutet auch, dass es verschiedene Protokolle gibt, die auf ihnen aufbauen und verwendet werden. Sehr verbreitet sind UDP und TCP, über die wir bereits kurz gesprochen haben. Sockets, die eines dieser Transportprotokolle verwenden, werden UDP- oder TCP-Sockets genannt.
IPC-Steckdosen
Neben den Netzwerksteckdosen gibt es auch andere Arten. Zum Beispiel IPC (Inter Process Communication) Sockets. IPC-Sockets sind dafür gedacht, Daten zwischen Prozessen auf demselben Rechner zu übertragen, während Netzwerk-Sockets das Gleiche über das Netzwerk tun können.
Das Gute an IPC-Sockets ist, dass sie einen großen Teil des Overheads vermeiden, der durch das Erstellen von Paketen und das Auflösen der Routen zum Senden der Daten entsteht. Da im Kontext von IPC Sender und Empfänger lokale Prozesse sind, hat die Kommunikation über IPC-Sockets normalerweise eine geringere Latenz.
Unix-sockets
Ein gutes Beispiel für IPC-Sockets sind Unix-Sockets, die, wie alles in Unix, nur Dateien im Dateisystem sind. Sie werden nicht durch die IP-Adresse und den Port identifiziert, sondern durch den Dateipfad auf dem Dateisystem.
Netzwerksockel als IPC-Sockel
Beachte, dass du genauso gut Netzwerksockets für die Kommunikation zwischen den Prozessen verwenden kannst, wenn sich sowohl der Server als auch der Empfänger auf localhost befinden (d.h. eine IP-Adresse 127.0.0.1 haben).
Auf der einen Seite führt dies natürlich zu einer zusätzlichen Latenzzeit, da der Netzwerkstack deine Daten verarbeitet, aber auf der anderen Seite müssen wir uns keine Gedanken über das zugrunde liegende Betriebssystem machen, da Netzwerksockets auf allen Systemen vorhanden sind und funktionieren, im Gegensatz zu IPC-Sockets, die spezifisch für ein bestimmtes Betriebssystem oder eine Betriebssystemfamilie sind.
Python Socket Library
Für die Socket-Programmierung in Python verwenden wir die offizielle eingebaute Python-Socket-Bibliothek, die aus Funktionen, Konstanten und Klassen besteht, die zum Erstellen, Verwalten und Arbeiten mit Sockets verwendet werden. Zu den häufig verwendeten Funktionen dieser Bibliothek gehören:
- socket(): Erzeugt einen neuen Socket.
- bind(): Verknüpft den Socket mit einer bestimmten Adresse und einem bestimmten Port.
- listen(): Beginnt, auf dem Socket auf eingehende Verbindungen zu lauschen.
- accept(): Nimmt eine Verbindung von einem Client an und gibt einen neuen Socket für die Kommunikation zurück.
- connect(): Stellt eine Verbindung zu einem entfernten Server her.
- send(): Sendet Daten über den Socket.
- recv(): Empfängt Daten vom Socket.
- close(): Schließt die Socket-Verbindung.
Python Socket Beispiel
Werfen wir einen Blick auf die Socket-Programmierung anhand eines praktischen Beispiels in Python. Unser Ziel ist es, zwei Anwendungen zu verbinden und sie miteinander kommunizieren zu lassen. Wir werden die Python-Socket-Bibliothek verwenden, um eine Server-Socket-Anwendung zu erstellen, die mit einem Client über ein Netzwerk kommuniziert und Informationen austauscht.
Überlegungen und Einschränkungen
Beachte jedoch, dass unser Beispiel zu Lehrzwecken vereinfacht ist und die Anwendungen lokal ausgeführt werden und nicht über das tatsächliche Netzwerk kommunizieren - wir werden eine Loopback-Localhost-Adresse verwenden, um den Client mit dem Server zu verbinden.
Das bedeutet, dass sowohl der Client als auch der Server auf demselben Rechner laufen und der Client eine Verbindung zu demselben Rechner initiiert, auf dem er läuft, wenn auch zu einem anderen Prozess, der den Server darstellt.
Laufen auf verschiedenen Maschinen
Alternativ könntest du deine Anwendungen auf zwei verschiedenen Geräten installieren und beide mit demselben Wi-Fi-Router verbinden, wodurch ein lokales Netzwerk entsteht. Dann könnte der Client, der auf einem Gerät läuft, sich mit dem Server verbinden, der auf einem anderen Rechner läuft.
In diesem Fall musst du jedoch die IP-Adressen kennen, die dein Router deinen Geräten zugewiesen hat, und diese anstelle der Loopback-IP-Adresse localhost (127.0.0.1) verwenden (um die IP-Adressen zu sehen, verwende den Terminalbefehl ifconfig
für Unix-ähnliche Systeme oder ipconfig
- für Windows). Nachdem du die IP-Adressen deiner Anwendungen erhalten hast, kannst du sie im Code entsprechend ändern, und das Beispiel wird trotzdem funktionieren.
Wie auch immer, wir werden mit unserem Beispiel beginnen. Du musst natürlich Python installiert haben, wenn du mitmachen willst.
Socket-Server in Python erstellen
Beginnen wir mit der Erstellung eines Socket-Servers (insbesondere eines Python-TCP-Servers, da er mit TCP-Sockets arbeiten wird, wie wir noch sehen werden), der Nachrichten mit den Clients austauschen wird. Um die Terminologie zu verdeutlichen: Obwohl technisch gesehen jeder Server ein Socket-Server ist, da Sockets immer unter der Haube verwendet werden, um Netzwerkverbindungen zu initiieren, verwenden wir den Ausdruck "Socket-Server", weil unser Beispiel explizit von der Socket-Programmierung Gebrauch macht.
Befolge also die folgenden Schritte:
Python-Datei mit einigen Boilerplates erstellen
- Erstellen Sie eine Datei namens
server.py
- Importiere das Modul
socket
in dein Python-Skript.
import socket
- Füge eine Funktion namens
run_server
hinzu. Dort werden wir den Großteil unseres Codes hinzufügen. Wenn du deinen Code in die Funktion einfügst, vergiss nicht, ihn richtig einzurücken:
def run_server():
# your code will go here
Socket-Objekt instanziieren
Als nächsten Schritt erstellst du in run_server
ein Socket-Objekt mit der Funktion socket.socket()
.
Das erste Argument (socket.AF_INET
) gibt die IP-Adressfamilie für IPv4 an (andere Optionen sind: AF_INET6
für IPv6-Familie und AF_UNIX
für Unix-Sockets)
Das zweite Argument (socket.SOCK_STREAM)
zeigt an, dass wir einen TCP-Socket verwenden.
Bei der Verwendung von TCP stellt das Betriebssystem eine zuverlässige Verbindung mit ordnungsgemäßer Datenzustellung, Fehlererkennung und -weiterleitung sowie Flusskontrolle her. Du musst dich nicht um die Umsetzung all dieser Details kümmern.
Es gibt auch eine Option zur Angabe eines UDP-Sockets: socket.SOCK_DGRAM
. Dadurch wird ein Socket erstellt, der alle Funktionen von UDP unter der Haube implementiert.
Wenn du auf einer niedrigeren Ebene arbeiten und dein eigenes Transportschichtprotokoll auf dem TCP/IP-Netzwerkschichtprotokoll aufbauen möchtest, das von Sockets verwendet wird, kannst du den Wert socket.RAW_SOCKET
für das zweite Argument verwenden. In diesem Fall übernimmt das Betriebssystem keine Protokollfunktionen auf höherer Ebene für dich und du musst alle Header, Verbindungsbestätigungen und Weiterleitungsfunktionen selbst implementieren, wenn du sie brauchst. Es gibt noch weitere Werte, die du in der Dokumentation nachlesen kannst.
# create a socket object
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Server-Socket an IP-Adresse und Port binden
Lege den Hostnamen oder die Server-IP und den Port fest, um die Adresse anzugeben, von der aus der Server erreichbar ist und wo er auf eingehende Verbindungen wartet. In diesem Beispiel lauscht der Server auf dem lokalen Rechner - dies wird durch die Variable server_ip
definiert, die auf 127.0.0.1
(auch localhost genannt) gesetzt ist.
Die Variable port
wird auf 8000
gesetzt. Dies ist die Portnummer, über die die Serveranwendung vom Betriebssystem identifiziert wird (es wird empfohlen, Werte über 1023 für deine Portnummern zu verwenden, um Kollisionen mit Ports zu vermeiden, die von Systemprozessen verwendet werden).
server_ip = "127.0.0.1"
port = 8000
Bereite den Socket für den Empfang von Verbindungen vor, indem du ihn an die IP-Adresse und den Port bindest, die wir zuvor definiert haben.
# bind the socket to a specific address and port
server.bind((server_ip, port))
Lauschen auf eingehende Verbindungen
Richte mit der Funktion listen
einen Listening-Status im Server-Socket ein, um eingehende Client-Verbindungen empfangen zu können.
Diese Funktion akzeptiert ein Argument namens backlog
, das die maximale Anzahl der nicht akzeptierten Verbindungen in der Warteschlange angibt. In diesem Beispiel verwenden wir den Wert 0
für dieses Argument. Das bedeutet, dass nur ein einziger Client mit dem Server interagieren kann. Ein Verbindungsversuch eines Clients, der durchgeführt wird, während der Server mit einem anderen Client arbeitet, wird abgelehnt.
Wenn du einen Wert angibst, der größer ist als 0
, z.B. 1
, teilt er dem Betriebssystem mit, wie viele Clients in die Warteschlange aufgenommen werden können, bevor die Methode accept
für sie aufgerufen wird.
Sobald accept
aufgerufen wird, wird ein Kunde aus der Warteschlange entfernt und wird nicht mehr auf dieses Limit angerechnet. Das wird vielleicht klarer, wenn du dir weitere Teile des Codes ansiehst, aber was dieser Parameter im Wesentlichen bewirkt, lässt sich wie folgt veranschaulichen: Sobald dein zuhörender Server die Verbindungsanfrage erhält, fügt er diesen Client zur Warteschlange hinzu und nimmt seine Anfrage an. Wenn der Server, bevor er intern accept
für den ersten Client aufrufen konnte, eine Verbindungsanfrage von einem zweiten Client erhält, schiebt er diesen zweiten Client in dieselbe Warteschlange, vorausgesetzt, es ist genug Platz darin. Die Größe genau dieser Warteschlange wird durch das Argument backlog gesteuert. Sobald der Server den ersten Client annimmt, wird dieser aus der Warteschlange entfernt und der Server beginnt mit ihm zu kommunizieren. Der zweite Client steht immer noch in der Warteschlange und wartet darauf, dass der Server frei wird und die Verbindung annimmt.
Wenn du das Backlog-Argument weglässt, wird es auf den Standardwert deines Systems gesetzt (unter Unix kannst du diesen Standardwert normalerweise in der Datei /proc/sys/net/core/somaxconn
einsehen).
# listen for incoming connections
server.listen(0)
print(f"Listening on {server_ip}:{port}")
Eingehende Verbindungen annehmen
Als nächstes wartest du und akzeptierst eingehende Client-Verbindungen. Die Methode accept
hält den Ausführungsthread an, bis ein Kunde eine Verbindung herstellt. Dann wird ein Tupelpaar von (conn, address)
zurückgegeben, wobei address ein Tupel aus der IP-Adresse und dem Port des Clients ist und conn
ein neues Socket-Objekt, das eine Verbindung mit dem Client teilt und zur Kommunikation mit ihm verwendet werden kann.
accept
erstellt einen neuen Socket, um mit dem Client zu kommunizieren, anstatt den Listening-Socket (in unserem Beispiel server
genannt) an die Adresse des Clients zu binden und für die Kommunikation zu verwenden, denn der Listening-Socket muss auf weitere Verbindungen von anderen Clients hören, sonst würde er blockiert werden. Natürlich bearbeiten wir in unserem Fall immer nur einen einzigen Client und lehnen dabei alle anderen Verbindungen ab, aber das wird relevanter, wenn wir zu dem Beispiel mit dem Multithreading-Server kommen.
# accept incoming connections
client_socket, client_address = server.accept()
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")
Kommunikationsschleife schaffen
Sobald eine Verbindung mit dem Client hergestellt wurde (nach dem Aufruf der Methode accept
), starten wir eine Endlosschleife zur Kommunikation. In dieser Schleife rufen wir die Methode recv
des client_socket
Objekts auf. Diese Methode empfängt die angegebene Anzahl von Bytes vom Client - in unserem Fall 1024.
1024 Byte ist nur eine gängige Konvention für die Größe der Nutzlast, da es sich um eine Zweierpotenz handelt, die für Optimierungszwecke möglicherweise besser ist als ein beliebiger Wert. Du kannst diesen Wert jedoch nach Belieben ändern.
Da die vom Kunden in der Variablen request
empfangenen Daten in binärer Form vorliegen, haben wir sie mit der Funktion decode
von einer Folge von Bytes in eine Zeichenkette umgewandelt.
Dann haben wir eine if-Anweisung, die die Kommunikationsschleife verlässt, wenn wir eine ”close”
Nachricht erhalten. Das bedeutet, dass unser Server, sobald er einen ”close”
String in der Anfrage erhält, die Bestätigung an den Client zurückschickt und die Verbindung mit ihm beendet. Andernfalls geben wir die empfangene Nachricht auf der Konsole aus. In unserem Fall ist die Bestätigung nur das Senden eines ”closed”
Strings an den Kunden.
Beachte, dass die Methode lower
, die wir auf die Zeichenfolge request
in der if-Anweisung anwenden, sie einfach in Kleinbuchstaben umwandelt. Auf diese Weise ist es egal, ob die Zeichenfolge close
ursprünglich mit Groß- oder Kleinbuchstaben geschrieben wurde.
# receive data from the client
while True:
request = client_socket.recv(1024)
request = request.decode("utf-8") # convert bytes to string
# if we receive "close" from the client, then we break
# out of the loop and close the conneciton
if request.lower() == "close":
# send response to the client which acknowledges that the
# connection should be closed and break out of the loop
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
Antwort an den Kunden zurücksenden
Jetzt sollten wir die normale Antwort des Servers an den Client behandeln (das heißt, wenn der Client die Verbindung nicht schließen möchte). Füge innerhalb der while-Schleife direkt nach print(f"Received: {request}")
die folgenden Zeilen ein, die einen Antwortstring (in unserem Fall”accepted”
) in Bytes umwandeln und an den Client senden. Wenn der Server also eine Nachricht vom Client erhält, die nicht ”close”
lautet, sendet er als Antwort den String ”accepted”
aus:
response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)
Ressourcen freisetzen
Sobald wir die endlose while-Schleife verlassen, ist die Kommunikation mit dem Client abgeschlossen, also schließen wir den Client-Socket mit der Methode close
, um die Systemressourcen freizugeben. Mit der gleichen Methode schließen wir auch den Server-Socket, wodurch unser Server effektiv heruntergefahren wird. In einem realen Szenario würden wir natürlich wollen, dass unser Server weiterhin andere Clients abhört und nicht nach der Kommunikation mit einem einzigen herunterfährt, aber keine Sorge, wir werden weiter unten zu einem anderen Beispiel kommen.
Füge die folgenden Zeilen nach der unendlichen while-Schleife ein:
# close connection socket with the client
client_socket.close()
print("Connection to client closed")
# close server socket
server.close()
Hinweis: Vergiss nicht, die Funktion run_server
am Ende deiner server.py
Datei aufzurufen. Verwende einfach die folgende Codezeile:
run_server()
Vollständiges Server-Socket-Code-Beispiel
Hier ist der vollständige server.py
Quellcode:
import socket
def run_server():
# create a socket object
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1"
port = 8000
# bind the socket to a specific address and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen(0)
print(f"Listening on {server_ip}:{port}")
# accept incoming connections
client_socket, client_address = server.accept()
print(f"Accepted connection from {client_address[0]}:{client_address[1]}")
# receive data from the client
while True:
request = client_socket.recv(1024)
request = request.decode("utf-8") # convert bytes to string
# if we receive "close" from the client, then we break
# out of the loop and close the conneciton
if request.lower() == "close":
# send response to the client which acknowledges that the
# connection should be closed and break out of the loop
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)
# close connection socket with the client
client_socket.close()
print("Connection to client closed")
# close server socket
server.close()
run_server()
Um dieses einfache Beispiel nicht zu verkomplizieren, haben wir die Fehlerbehandlung weggelassen. Du solltest natürlich try-except Blöcke hinzufügen und sicherstellen, dass du die Sockets in der finally
Klausel immer schließt. Lies weiter und wir werden uns ein fortgeschritteneres Beispiel ansehen.
Client-Socket in Python erstellen
Nachdem du deinen Server eingerichtet hast, musst du im nächsten Schritt einen Client einrichten, der sich mit deinem Server verbindet und Anfragen an ihn sendet. Beginnen wir also mit den folgenden Schritten:
Python-Datei mit einigen Boilerplates erstellen
- Erstellen Sie eine neue Datei mit dem Namen
client.py
- Importiere die Socket-Bibliothek:
import socket
- Definiere die Funktion
run_client
, in der wir unseren gesamten Code unterbringen werden:
def run_client():
# your code will go here
Socket-Objekt instanziieren
Als Nächstes verwendest du die Funktion socket.socket()
, um ein TCP-Socket-Objekt zu erstellen, das dem Client als Kontaktpunkt mit dem Server dient.
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Verbinden mit dem Server-Socket
Gib die IP-Adresse und den Port des Servers an, um eine Verbindung zu ihm herstellen zu können. Diese sollten mit der IP-Adresse und dem Port übereinstimmen, die du zuvor in server.py
eingestellt hast.
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
Stelle eine Verbindung mit dem Server her, indem du die Methode connect
auf dem Client-Socket-Objekt verwendest. Beachte, dass wir den Client-Socket nicht an eine IP-Adresse oder einen Port gebunden haben. Für den Client ist das normal, denn connect
wählt automatisch einen freien Port und eine IP-Adresse, die die beste Route zum Server von den Netzwerkschnittstellen des Systems (in unserem Fall127.0.0.1
) bietet, und bindet den Client-Socket an diese.
# establish connection with server
client.connect((server_ip, server_port))
Kommunikationsschleife schaffen
Nachdem wir eine Verbindung hergestellt haben, starten wir eine unendliche Kommunikationsschleife, um mehrere Nachrichten an den Server zu senden. Wir erhalten die Eingaben des Benutzers mit der in Python eingebauten Funktion input
, kodieren sie dann in Bytes und schneiden sie auf maximal 1024 Bytes zu. Danach senden wir die Nachricht mit client.send
an den Server.
while True:
# input message and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
Umgang mit der Antwort des Servers
Sobald der Server eine Nachricht vom Client erhält, antwortet er darauf. In unserem Client-Code wollen wir nun die Antwort des Servers empfangen. Dafür verwenden wir in der Kommunikationsschleife die Methode recv
, um maximal 1024 Bytes zu lesen. Dann wandeln wir die Antwort von Bytes in eine Zeichenkette um, indem wir decode
verwenden und dann prüfen, ob sie dem Wert ”closed”
entspricht. Wenn dies der Fall ist, brechen wir aus der Schleife aus, was, wie wir später sehen werden, die Verbindung des Kunden beenden wird. Andernfalls geben wir die Antwort des Servers in der Konsole aus.
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
Ressourcen freisetzen
Am Ende der while-Schleife schließt du die Socket-Verbindung des Clients mit der Methode close
. Dadurch wird sichergestellt, dass die Ressourcen ordnungsgemäß freigegeben werden und die Verbindung beendet wird (d. h. wenn wir die Nachricht “closed”
erhalten und die while-Schleife verlassen).
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
Hinweis: Vergiss auch hier nicht, die Funktion run_client
, die wir oben implementiert haben, am Ende der Datei wie folgt aufzurufen:
run_client()
Vollständiges Beispiel für Client-Socket-Code
Hier ist der vollständige client.py
Code:
import socket
def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))
while True:
# input message and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
run_client()
Teste deinen Client und Server
Um die Server- und Client-Implementierung zu testen, die wir oben geschrieben haben, führe Folgendes durch:
- Öffne zwei Terminalfenster gleichzeitig.
- Navigiere in einem Terminalfenster zu dem Verzeichnis, in dem sich die Datei
server.py
befindet, und führe den folgenden Befehl aus, um den Server zu starten:
python server.py
Dadurch wird der Server-Socket an die localhost-Adresse (127.0.0.1) auf Port 8000 gebunden und beginnt, auf eingehende Verbindungen zu warten.
- Navigiere im anderen Terminal zu dem Verzeichnis, in dem sich die Datei
client.py
befindet, und führe den folgenden Befehl aus, um den Client zu starten:
python client.py
Dadurch wird der Benutzer zur Eingabe aufgefordert. Du kannst dann deine Nachricht eintippen und die Eingabetaste drücken. Dadurch wird deine Eingabe an den Server übertragen und in seinem Terminalfenster angezeigt. Der Server schickt seine Antwort an den Client und dieser fragt dich erneut nach der Eingabe. Dies wird so lange fortgesetzt, bis du den String ”close”
an den Server sendest.
Arbeiten mit mehreren Clients - Multithreading
Im vorherigen Beispiel haben wir gesehen, wie ein Server auf die Anfragen eines einzelnen Clients reagiert. In vielen praktischen Situationen müssen jedoch zahlreiche Clients gleichzeitig eine Verbindung zu einem einzigen Server herstellen. Hier kommt das Multithreading ins Spiel. Multithreading wird in Situationen verwendet, in denen du mehrere Aufgaben (z. B. mehrere Funktionen) gleichzeitig ausführen musst.
Die Idee ist, einen Thread zu erstellen, der ein unabhängiger Satz von Anweisungen ist, die vom Prozessor verarbeitet werden können. Threads sind viel leichter als Prozesse, weil sie innerhalb eines Prozesses leben und du ihnen nicht viele Ressourcen zuweisen musst.
Grenzen von Multithreading in Python
Beachte, dass Multithreading in Python begrenzt ist. Die Standard-Python-Implementierung (CPython) kann Threads nicht wirklich parallel ausführen. Aufgrund der globalen Interpretersperre (GIL) kann immer nur ein einziger Thread ausgeführt werden. Das ist jedoch ein separates Thema, das wir hier nicht diskutieren werden. Für unser Beispiel reicht es aus, eine begrenzte Anzahl von CPython-Threads zu verwenden, um den Punkt zu treffen. In der Praxis solltest du dich jedoch mit asynchroner Programmierung befassen, wenn du Python verwenden willst. Wir werden jetzt nicht darüber sprechen, denn es ist ein separates Thema und abstrahiert normalerweise von einigen Low-Level-Socket-Operationen, auf die wir uns in diesem Artikel konzentrieren.
Beispiel für einen Multithreading-Server
Das folgende Beispiel zeigt dir, wie du deinen Server mit Multithreading ausstatten kannst, um eine große Anzahl von Clients zu bedienen. Beachte, dass wir dieses Mal auch einige grundlegende Fehlerbehandlungen mit den try-except-finally-Blöcken hinzufügen werden. Um loszulegen, befolge die folgenden Schritte:
Thread-spawnende Serverfunktion erstellen
Importiere in deiner Python-Datei die Module socket
und threading
, um sowohl mit Sockets als auch mit Threads arbeiten zu können:
import socket
import threading
Definiere die Funktion run_server
, die, wie im obigen Beispiel, einen Server-Socket erstellt, ihn bindet und auf die eingehenden Verbindungen hört. Rufe dann accept
in einer unendlichen while-Schleife auf. Dadurch wird immer nach neuen Verbindungen gesucht. Nachdem accept
eine eingehende Verbindung erhalten hat und zurückkehrt, erstellst du einen Thread mit dem threading.Thread
Konstruktor. Dieser Thread führt die Funktion handle_client
aus, die wir später definieren werden, und übergibt client_socket
und addr
als Argumente (das Tupeladdr
enthält eine IP-Adresse und einen Port des verbundenen Clients). Nachdem der Thread erstellt wurde, rufen wir start
auf, um mit seiner Ausführung zu beginnen.
Vergiss nicht, dass der Aufruf von accept
blockiert ist. Wenn wir also bei der ersten Iteration der while-Schleife die Zeile mit accept
erreichen, halten wir an und warten auf eine Kundenverbindung, ohne etwas anderes auszuführen. Sobald der Client eine Verbindung hergestellt hat, kehrt die Methode accept
zurück und wir setzen die Ausführung fort: Wir spawnen einen Thread, der sich um den besagten Client kümmert, und gehen zur nächsten Iteration über, wo wir wieder beim Aufruf von accept
anhalten und darauf warten, dass sich ein weiterer Client verbindet.
Am Ende der Funktion gibt es eine Fehlerbehandlung, die sicherstellt, dass der Server-Socket immer geschlossen wird, falls etwas Unerwartetes passiert.
def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")
while True:
# accept a client connection
client_socket, addr = server.accept()
print(f"Accepted connection from {addr[0]}:{addr[1]}")
# start a new thread to handle the client
thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
thread.start()
except Exception as e:
print(f"Error: {e}")
finally:
server.close()
Beachte, dass der Server in unserem Beispiel nur angehalten wird, wenn ein unerwarteter Fehler auftritt. Andernfalls wird es unbegrenzt auf die Clients warten, und du musst das Terminal beenden, wenn du es stoppen willst.
Client-Handling-Funktion erstellen, die in einem separaten Thread läuft
Definiere nun oberhalb der Funktion run_server
eine weitere Funktion namens handle_client
. Diese Funktion wird in einem separaten Thread für jede Verbindung des Clients ausgeführt. Sie erhält das Socket-Objekt des Clients und das Tupel addr
als Argumente.
Innerhalb dieser Funktion machen wir das Gleiche wie im Single-Thread-Beispiel plus etwas Fehlerbehandlung: Wir starten eine Schleife, um Nachrichten vom Client mit recv
zu erhalten.
Dann prüfen wir, ob wir eine Abschlussmeldung erhalten haben. Wenn ja, antworten wir mit dem String ”closed”
und schließen die Verbindung, indem wir die Schleife verlassen. Andernfalls geben wir den Anfragestring des Kunden in der Konsole aus und fahren mit der nächsten Schleifeniteration fort, um die Nachricht des nächsten Kunden zu empfangen.
Am Ende dieser Funktion gibt es eine Fehlerbehandlung für unerwartete Fälle (except
Klausel) und eine finally
Klausel, in der wir client_socket
mit close
freigeben. Diese finally
Klausel wird immer ausgeführt, egal was passiert, und stellt sicher, dass der Client-Socket immer ordnungsgemäß freigegeben wird.
def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when hanlding client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")
Wenn handle_client
zurückkehrt, wird auch der Thread, der ihn ausführt, automatisch freigegeben.
Hinweis: Vergiss nicht, am Ende deiner Datei die Funktion run_server
aufzurufen.
Vollständiges Beispiel für Multithreading-Server-Code
Nun wollen wir den kompletten Multithreading-Servercode zusammenstellen:
import socket
import threading
def handle_client(client_socket, addr):
try:
while True:
# receive and print client messages
request = client_socket.recv(1024).decode("utf-8")
if request.lower() == "close":
client_socket.send("closed".encode("utf-8"))
break
print(f"Received: {request}")
# convert and send accept response to the client
response = "accepted"
client_socket.send(response.encode("utf-8"))
except Exception as e:
print(f"Error when hanlding client: {e}")
finally:
client_socket.close()
print(f"Connection to client ({addr[0]}:{addr[1]}) closed")
def run_server():
server_ip = "127.0.0.1" # server hostname or IP address
port = 8000 # server port number
# create a socket object
try:
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to the host and port
server.bind((server_ip, port))
# listen for incoming connections
server.listen()
print(f"Listening on {server_ip}:{port}")
while True:
# accept a client connection
client_socket, addr = server.accept()
print(f"Accepted connection from {addr[0]}:{addr[1]}")
# start a new thread to handle the client
thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
thread.start()
except Exception as e:
print(f"Error: {e}")
finally:
server.close()
run_server()
Hinweis: Um mögliche Probleme wie Wettlaufsituationen oder Dateninkonsistenzen beim Umgang mit Multithreading-Servern zu vermeiden, ist es in einem realen Code unerlässlich, Thread-Sicherheits- und Synchronisierungstechniken zu berücksichtigen. In unserem einfachen Beispiel ist das jedoch kein Problem.
Client-Beispiel mit einfacher Fehlerbehandlung
Da wir nun eine Server-Implementierung haben, die mehrere Clients gleichzeitig bedienen kann, können wir dieselbe Client-Implementierung wie in den ersten grundlegenden Beispielen verwenden, um eine Verbindung herzustellen, oder wir können sie leicht anpassen und eine Fehlerbehandlung hinzufügen. Unten findest du den Code, der mit dem vorherigen Client-Beispiel identisch ist und um try-except-Blöcke ergänzt wurde:
import socket
def run_client():
# create a socket object
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_ip = "127.0.0.1" # replace with the server's IP address
server_port = 8000 # replace with the server's port number
# establish connection with server
client.connect((server_ip, server_port))
try:
while True:
# get input message from user and send it to the server
msg = input("Enter message: ")
client.send(msg.encode("utf-8")[:1024])
# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")
# if server sent us "closed" in the payload, we break out of
# the loop and close our socket
if response.lower() == "closed":
break
print(f"Received: {response}")
except Exception as e:
print(f"Error: {e}")
finally:
# close client socket (connection to the server)
client.close()
print("Connection to server closed")
run_client()
Testen des Multithreading-Beispiels
Wenn du die Multi-Client-Implementierung testen willst, öffne mehrere Terminalfenster für Clients und eines für den Server. Starte zunächst den Server mit python server.py
. Danach startest du ein paar Clients mit python client.py
. In den Server-Terminalfenstern siehst du, wie neue Clients mit dem Server verbunden werden. Du kannst nun mit dem Senden von Nachrichten von verschiedenen Clients fortfahren, indem du Text in die jeweiligen Terminals eingibst. Alle Nachrichten werden verarbeitet und auf der Konsole auf der Serverseite ausgegeben.
Socket-Programmierung Anwendungen in der Datenwissenschaft
Während jede Netzwerkanwendung Sockets verwendet, die vom Betriebssystem unter der Haube erzeugt werden, gibt es zahlreiche Systeme, die sich speziell auf die Socket-Programmierung verlassen, entweder für bestimmte spezielle Anwendungsfälle oder um die Leistung zu verbessern. Aber wie genau ist Socket-Programmierung im Kontext der Datenwissenschaft nützlich? Sie spielt auf jeden Fall eine wichtige Rolle, wenn es darum geht, große Datenmengen schnell zu empfangen oder zu versenden. Daher wird die Socket-Programmierung hauptsächlich für die Datenerfassung und Echtzeitverarbeitung, das verteilte Rechnen und die Kommunikation zwischen den Prozessen verwendet. Aber sehen wir uns einige besondere Anwendungen im Bereich der Datenwissenschaft genauer an.
Datenerfassung in Echtzeit
Sockets werden häufig verwendet, um Echtzeitdaten aus verschiedenen Quellen zu sammeln und weiterzuverarbeiten, an eine Datenbank oder eine Analysepipeline weiterzuleiten usw. Ein Socket kann zum Beispiel verwendet werden, um Daten von einem Finanzsystem oder einer Social-Media-API zu empfangen und anschließend von Data Scientists zu verarbeiten.
Verteiltes Rechnen
Datenwissenschaftler/innen können Socket-Konnektivität nutzen, um die Verarbeitung und Berechnung großer Datensätze auf mehrere Rechner zu verteilen. Die Socket-Programmierung wird häufig in Apache Spark und anderen verteilten Computing-Frameworks für die Kommunikation zwischen den Knotenpunkten verwendet.
Modell-Einsatz
Die Socket-Programmierung kann verwendet werden, wenn maschinelle Lernmodelle an die Nutzer/innen ausgeliefert werden, so dass Vorhersagen und Vorschläge sofort zur Verfügung stehen. Um die Entscheidungsfindung in Echtzeit zu erleichtern, können Datenwissenschaftler/innen leistungsstarke, socketbasierte Serveranwendungen nutzen, die große Datenmengen aufnehmen, sie mit Hilfe von trainierten Modellen verarbeiten, um Vorhersagen zu treffen, und die Ergebnisse dann schnell an den Client zurücksenden.
Kommunikation zwischen Prozessen (IPC)
Sockets können für IPC verwendet werden, wodurch verschiedene Prozesse, die auf demselben Rechner laufen, miteinander kommunizieren und Daten austauschen können. Dies ist in der Datenwissenschaft nützlich, um komplexe und ressourcenintensive Berechnungen auf mehrere Prozesse zu verteilen. Die Subprocessing-Bibliothek von Python wird häufig zu diesem Zweck verwendet: Sie erzeugt mehrere Prozesse, um mehrere Prozessorkerne zu nutzen und die Leistung der Anwendung bei umfangreichen Berechnungen zu steigern. Die Kommunikation zwischen solchen Prozessen kann über IPC-Sockets erfolgen.
Zusammenarbeit und Kommunikation
Die Socket-Programmierung ermöglicht die Kommunikation und Zusammenarbeit zwischen Datenwissenschaftlern in Echtzeit. Um eine effektive Zusammenarbeit und den Wissensaustausch zu ermöglichen, werden sockelbasierte Chat-Apps oder Plattformen zur kollaborativen Datenanalyse eingesetzt.
Es ist erwähnenswert, dass Datenwissenschaftler/innen in vielen der oben genannten Anwendungen nicht direkt mit Sockets arbeiten müssen. In der Regel verwenden sie Bibliotheken, Frameworks und Systeme, die alle Low-Level-Details der Socket-Programmierung weglassen. Unter der Haube basieren jedoch alle diese Lösungen auf der Socket-Kommunikation und nutzen die Socket-Programmierung.
Herausforderungen und Best Practices bei der Socket-Programmierung
Da Sockets ein Low-Level-Konzept zur Verwaltung von Verbindungen sind, müssen Entwickler, die mit ihnen arbeiten, die gesamte erforderliche Infrastruktur implementieren, um robuste und zuverlässige Anwendungen zu erstellen. Das bringt natürlich eine Menge Herausforderungen mit sich. Es gibt jedoch einige bewährte Verfahren und allgemeine Richtlinien, die du befolgen kannst, um diese Probleme zu überwinden. Im Folgenden findest du einige der am häufigsten auftretenden Probleme bei der Socket-Programmierung sowie einige allgemeine Tipps:
Verbindungsmanagement
Mit vielen Verbindungen gleichzeitig zu arbeiten, mehrere Clients zu verwalten und die effiziente Bearbeitung gleichzeitiger Anfragen zu gewährleisten, kann sicherlich eine Herausforderung sein und ist nicht trivial. Es erfordert sorgfältiges Ressourcenmanagement und Koordination, um Engpässe zu vermeiden
Bewährte Praktiken
- Behalte den Überblick über aktive Verbindungen mithilfe von Datenstrukturen wie Listen oder Wörterbüchern. Oder verwende fortschrittliche Techniken wie Connection Pooling, die ebenfalls zur Skalierbarkeit beitragen.
- Verwende Threading oder asynchrone Programmiertechniken, um mehrere Client-Verbindungen gleichzeitig zu bearbeiten.
- Schließe Verbindungen richtig, um Ressourcen freizugeben und Speicherlecks zu vermeiden.
Fehlerbehandlung
Der Umgang mit Fehlern wie Verbindungsabbrüchen, Zeitüberschreitungen und Problemen bei der Datenübertragung ist entscheidend. Der Umgang mit diesen Fehlern und die Bereitstellung einer angemessenen Rückmeldung an die Kunden kann eine Herausforderung sein, vor allem bei der Socket-Programmierung auf niedriger Ebene.
Bewährte Praktiken
- Verwende try-except-finally-Blöcke, um bestimmte Arten von Fehlern abzufangen und zu behandeln.
- Gib aussagekräftige Fehlermeldungen aus und erwäge eine Protokollierung, um die Fehlersuche zu erleichtern.
Skalierbarkeit und Leistung
Die Sicherstellung einer optimalen Leistung und die Minimierung von Latenzzeiten sind bei großen Datenströmen oder Echtzeitanwendungen von zentraler Bedeutung.
Bewährte Praktiken
- Optimiere deinen Code auf Leistung, indem du unnötige Datenverarbeitung und Netzwerk-Overhead minimierst.
- Implementiere Pufferungstechniken, um große Datenübertragungen effizient zu bewältigen.
- Ziehe Techniken zur Lastverteilung in Betracht, um die Kundenanfragen auf mehrere Serverinstanzen zu verteilen.
Sicherheit und Authentifizierung
Die Sicherung der socketbasierten Kommunikation und die Implementierung geeigneter Authentifizierungsmechanismen kann schwierig sein. Die Gewährleistung des Datenschutzes, die Verhinderung unbefugten Zugriffs und der Schutz vor böswilligen Aktivitäten erfordern eine sorgfältige Prüfung und Implementierung von sicheren Protokollen.
Bewährte Praktiken
- Nutze SSL/TLS Sicherheitsprotokolle, um eine sichere Datenübertragung durch Verschlüsselung der Informationen zu gewährleisten.
- Stelle die Identität des Kunden sicher, indem du sichere Authentifizierungsmethoden wie Token-basierte Authentifizierung, Public-Key-Kryptografie oder Benutzername/Passwort einsetzt.
- Stelle sicher, dass vertrauliche Daten wie Passwörter oder API-Schlüssel geschützt und verschlüsselt oder idealerweise gar nicht gespeichert werden (nur ihre Hashes, falls erforderlich).
Zuverlässigkeit und Ausfallsicherheit des Netzwerks
Der Umgang mit Netzwerkunterbrechungen, schwankenden Bandbreiten und unzuverlässigen Verbindungen kann eine Herausforderung sein. Die Aufrechterhaltung einer stabilen Verbindung, die ordnungsgemäße Behandlung von Verbindungsabbrüchen und die Implementierung von Wiederverbindungsmechanismen sind für robuste Netzwerkanwendungen unerlässlich.
Bewährte Praktiken
- Verwende Keepalive-Meldungen, um inaktive oder abgebrochene Verbindungen zu erkennen.
- Implementiere Timeouts, um eine unendliche Blockierung zu vermeiden und eine rechtzeitige Bearbeitung von Antworten zu gewährleisten.
- Implementiere eine exponentielle Backoff-Wiederverbindungslogik, um eine Verbindung wiederherzustellen, wenn sie verloren gegangen ist.
Wartbarkeit des Codes
Zu guter Letzt sei noch die Wartbarkeit des Codes erwähnt. Da die Socket-Programmierung auf niedriger Ebene stattfindet, müssen die Entwickler mehr Code schreiben. Das kann schnell zu einem unwartbaren Spaghetti-Code werden. Deshalb ist es wichtig, den Code so früh wie möglich zu organisieren und zu strukturieren und zusätzliche Mühe in die Planung der Code-Architektur zu investieren.
Bewährte Praktiken
- Unterteile deinen Code in Klassen oder Funktionen, die idealerweise nicht zu lang sein sollten.
- Schreibe frühzeitig Unit-Tests, indem du deine Client- und Server-Implementierungen mockst.
- Ziehe in Erwägung, mehr High-Level-Bibliotheken für den Umgang mit Verbindungen zu verwenden, es sei denn, du musst unbedingt Socket-Programmierung verwenden.
Nachbereitung: Socket-Programmierung in Python
Sockets sind ein wesentlicher Bestandteil aller Netzwerkanwendungen. In diesem Artikel haben wir uns mit der Socket-Programmierung in Python beschäftigt. Hier sind die wichtigsten Punkte, die du dir merken solltest:
- Sockets sind Schnittstellen, die die Verbindungsverwaltung abstrahieren.
- Sockets ermöglichen die Kommunikation zwischen verschiedenen Prozessen (normalerweise ein Client und ein Server) lokal oder über ein Netzwerk.
- In Python wird die Arbeit mit Sockets durch die
socket
Bibliothek erledigt, die unter anderem ein Socket-Objekt mit verschiedenen Methoden wierecv
,send
,listen
,close
bereitstellt. - Die Socket-Programmierung hat verschiedene nützliche Anwendungen in der Datenwissenschaft, z. B. Datenerfassung, Kommunikation zwischen Prozessen und verteiltes Rechnen.
- Zu den Herausforderungen bei der Socket-Programmierung gehören Verbindungsmanagement, Datenintegrität, Skalierbarkeit, Fehlerbehandlung, Sicherheit und Wartbarkeit des Codes.
Mit Socket-Programmierkenntnissen können Entwickler effiziente Echtzeit-Netzwerkanwendungen erstellen. Wenn sie die Konzepte und Best Practices beherrschen, können sie das volle Potenzial der Socket-Programmierung ausschöpfen, um zuverlässige und skalierbare Lösungen zu entwickeln.
Die Socket-Programmierung ist jedoch eine sehr einfache Technik, die nur schwer anzuwenden ist, weil Anwendungsingenieure jedes kleine Detail der Anwendungskommunikation berücksichtigen müssen.
Heutzutage müssen wir sehr oft nicht mehr direkt mit Sockets arbeiten, da sie in der Regel von höheren Bibliotheken und Frameworks gehandhabt werden, es sei denn, es besteht die Notwendigkeit, die Leistung aus der Anwendung herauszuquetschen oder sie zu skalieren.
Wenn du Sockets verstehst und weißt, wie die Dinge unter der Haube funktionieren, kannst du dich als Entwickler oder Datenwissenschaftler besser orientieren und das ist immer eine gute Idee.
Wenn du mehr über die Rolle von Python in der Netzwerkanalyse erfahren möchtest, schau dir unseren Kurs Intermediate Network Analysis in Python an. Du kannst auch unseren Python-Programmier-Kurs besuchen, um deine Python-Programmierkenntnisse zu verbessern.
Serhii ist ein Software-Ingenieur mit 3 Jahren Erfahrung in den meisten Backend-Technologien wie DBMS, DB-spezifischen Sprachen, Python, Nodejs usw.. Er hat auch Erfahrung mit Frontend-Frameworks wie VueJS oder ReactJS.