Socket-Programmierung in Python (Anleitung)

Socket-Programmierung in Python (Anleitung)

Sockets und die Socket-API werden zum Senden von Nachrichten über ein Netzwerk verwendet. Sie liefern eine Form voninter-process communication (IPC). Das Netzwerk kann ein logisches, lokales Netzwerk zum Computer oder ein Netzwerk sein, das physisch mit einem externen Netzwerk verbunden ist und über eigene Verbindungen zu anderen Netzwerken verfügt. Das offensichtliche Beispiel ist das Internet, mit dem Sie über Ihren ISP eine Verbindung herstellen.

Dieses Tutorial enthält drei verschiedene Iterationen zum Erstellen eines Socket-Servers und -Clients mit Python:

  1. Wir beginnen das Tutorial mit einem Blick auf einen einfachen Socket-Server und -Client.

  2. Sobald Sie die API und die Funktionsweise in diesem ersten Beispiel gesehen haben, sehen wir uns eine verbesserte Version an, die mehrere Verbindungen gleichzeitig verarbeitet.

  3. Schließlich werden wir einen Beispielserver und -client erstellen, der wie eine vollwertige Socket-Anwendung funktioniert und über einen eigenen benutzerdefinierten Header und Inhalt verfügt.

Am Ende dieses Tutorials erfahren Sie, wie Sie die Hauptfunktionen und -methoden in Pythonssocket moduleverwenden, um Ihre eigenen Client-Server-Anwendungen zu schreiben. Dazu gehört, dass Sie zeigen, wie Sie mit einer benutzerdefinierten Klasse Nachrichten und Daten zwischen Endpunkten senden, auf denen Sie aufbauen und die Sie für Ihre eigenen Anwendungen verwenden können.

Die Beispiele in diesem Tutorial verwenden Python 3.6. Sie finden diesource code on GitHub.

Networking und Sockets sind große Themen. Es wurden wörtliche Bände darüber geschrieben. Wenn Sie noch keine Erfahrung mit Sockets oder Netzwerken haben, ist es völlig normal, wenn Sie sich mit all den Begriffen und Teilen überfordert fühlen. Ich weiß, dass ich es getan habe!

Lassen Sie sich jedoch nicht entmutigen. Ich habe dieses Tutorial für Sie geschrieben. Wie bei Python können wir jeweils ein wenig lernen. Verwenden Sie die Lesezeichenfunktion Ihres Browsers und kehren Sie zurück, wenn Sie für den nächsten Abschnitt bereit sind.

Lass uns anfangen!

Hintergrund

Steckdosen haben eine lange Geschichte. Ihre Verwendung vonoriginated with ARPANET im Jahr 1971 und später wurde eine API im 1983 veröffentlichten Betriebssystem Berkeley Software Distribution (BSD) mit dem NamenBerkeley sockets.

Als das Internet in den 1990er Jahren mit dem World Wide Web begann, tat dies auch die Netzwerkprogrammierung. Webserver und Browser waren nicht die einzigen Anwendungen, die neu verbundene Netzwerke nutzen und Sockets verwenden. Client-Server-Anwendungen aller Art und Größe waren weit verbreitet.

Obwohl sich die zugrunde liegenden Protokolle, die von der Socket-API verwendet werden, im Laufe der Jahre weiterentwickelt haben und wir neue gesehen haben, ist die Low-Level-API heute dieselbe geblieben.

Die häufigste Art von Socket-Anwendungen sind Client-Server-Anwendungen, bei denen eine Seite als Server fungiert und auf Verbindungen von Clients wartet. Dies ist die Art von Anwendung, die ich in diesem Tutorial behandeln werde. Insbesondere betrachten wir die Socket-API fürInternet sockets, manchmal auch als Berkeley- oder BSD-Sockets bezeichnet. Es gibt auchUnix domain sockets, die nur zur Kommunikation zwischen Prozessen auf demselben Host verwendet werden können.

Socket API Übersicht

Pythonssocket module bietet eine Schnittstelle zuBerkeley sockets API. Dies ist das Modul, das wir in diesem Tutorial verwenden und diskutieren werden.

Die primären Socket-API-Funktionen und -Methoden in diesem Modul sind:

  • socket()

  • bind()

  • listen()

  • accept()

  • connect()

  • connect_ex()

  • send()

  • recv()

  • close()

Python bietet eine praktische und konsistente API, die diesen Systemaufrufen, ihren C-Gegenstücken, direkt zugeordnet wird. Wir werden uns im nächsten Abschnitt ansehen, wie diese zusammen verwendet werden.

Als Teil seiner Standardbibliothek verfügt Python auch über Klassen, die die Verwendung dieser Socket-Funktionen auf niedriger Ebene erleichtern. Obwohl dies in diesem Lernprogramm nicht behandelt wird, finden Sie insocketserver module ein Framework für Netzwerkserver. Es gibt auch viele Module, die übergeordnete Internetprotokolle wie HTTP und SMTP implementieren. Eine Übersicht finden Sie unterInternet Protocols and Support.

TCP-Sockets

Wie Sie in Kürze sehen werden, erstellen wir ein Socket-Objekt mitsocket.socket() und geben den Socket-Typ alssocket.SOCK_STREAM an. Wenn Sie dies tun, wird als StandardprotokollTransmission Control Protocol (TCP) verwendet. Dies ist eine gute Standardeinstellung und wahrscheinlich das, was Sie wollen.

Warum sollten Sie TCP verwenden? Das Transmission Control Protocol (TCP):

  • Is reliable: Pakete, die im Netzwerk verworfen wurden, werden vom Absender erkannt und erneut übertragen.

  • Die Daten vonHas in-order data delivery:werden von Ihrer Anwendung in der Reihenfolge gelesen, in der sie vom Absender geschrieben wurden.

Im Gegensatz dazu sind die mitsocket.SOCK_DGRAMerstellten Sockets vonUser Datagram Protocol (UDP)nicht zuverlässig, und die vom Empfänger gelesenen Daten können aufgrund der Schreibvorgänge des Absenders nicht in der richtigen Reihenfolge sein.

Warum ist das wichtig? Netzwerke sind ein Best-Effort-Bereitstellungssystem. Es gibt keine Garantie dafür, dass Ihre Daten ihr Ziel erreichen oder dass Sie das erhalten, was Ihnen gesendet wurde.

Netzwerkgeräte (z. B. Router und Switches) verfügen über eine begrenzte Bandbreite und eigene Systemeinschränkungen. Sie verfügen wie unsere Clients und Server über CPUs, Speicher, Busse und Schnittstellenpaketpuffer. Mit TCP müssen Sie sich keine Gedanken mehr überpacket loss, Daten, die nicht in der richtigen Reihenfolge eingehen, und viele andere Dinge machen, die bei der Kommunikation über ein Netzwerk immer passieren.

In der folgenden Abbildung sehen wir uns die Reihenfolge der Socket-API-Aufrufe und den Datenfluss für TCP an:

Die linke Spalte repräsentiert den Server. Auf der rechten Seite befindet sich der Client.

Beachten Sie, dass in der oberen linken Spalte die API-Aufrufe des Servers zum Einrichten eines "Listening" -Sockets ausgeführt werden:

  • socket()

  • bind()

  • listen()

  • accept()

Eine Hörbuchse macht genau das, wonach es sich anhört. Es wartet auf Verbindungen von Clients. Wenn ein Client eine Verbindung herstellt, ruft der Serveraccept() auf, um die Verbindung zu akzeptieren oder abzuschließen.

Der Client ruftconnect() auf, um eine Verbindung zum Server herzustellen und den Drei-Wege-Handshake zu initiieren. Der Handshake-Schritt ist wichtig, da er sicherstellt, dass jede Seite der Verbindung im Netzwerk erreichbar ist, dh dass der Client den Server erreichen kann und umgekehrt. Es kann sein, dass nur ein Host, Client oder Server den anderen erreichen kann.

In der Mitte befindet sich der Roundtrip-Bereich, in dem Daten zwischen Client und Server ausgetauscht werden, indemsend() undrecv() aufgerufen werden.

Unten sind der Client und der Serverclose()ihre jeweiligen Sockets.

Echo Client und Server

Nachdem Sie einen Überblick über die Socket-API und die Kommunikation zwischen Client und Server erhalten haben, erstellen wir unseren ersten Client und Server. Wir beginnen mit einer einfachen Implementierung. Der Server gibt einfach alles, was er empfängt, an den Client zurück.

Echo Server

Hier ist der Server,echo-server.py:

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

Note: Machen Sie sich im Moment keine Sorgen, dass Sie alles oben Genannte verstehen. In diesen wenigen Codezeilen ist viel los. Dies ist nur ein Ausgangspunkt, damit Sie einen Basisserver in Aktion sehen können.

Am Ende dieses Tutorials befindet sich einreference section mit weiteren Informationen und Links zu zusätzlichen Ressourcen. Ich werde im gesamten Tutorial auf diese und andere Ressourcen verweisen.

Lassen Sie uns jeden API-Aufruf durchgehen und sehen, was passiert.

socket.socket() erstellt ein Socket-Objekt, dascontext manager type unterstützt, sodass Sie es inwith statement verwenden können. Es ist nicht erforderlich,s.close() aufzurufen:

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    pass  # Use the socket object without calling s.close().

Die ansocket() übergebenen Argumente geben denaddress family und den Socket-Typ an. AF_INET ist die Internetadressfamilie fürIPv4. SOCK_STREAM ist der Socket-Typ fürTCP, das Protokoll, das zum Transport unserer Nachrichten im Netzwerk verwendet wird.

bind() wird verwendet, um den Socket einer bestimmten Netzwerkschnittstelle und Portnummer zuzuordnen:

HOST = '127.0.0.1'  # Standard loopback interface address (localhost)
PORT = 65432        # Port to listen on (non-privileged ports are > 1023)

# ...

s.bind((HOST, PORT))

Die anbind() übergebenen Werte hängen vonaddress family des Sockets ab. In diesem Beispiel verwenden wirsocket.AF_INET (IPv4). Es wird also ein 2-Tupel erwartet:(host, port).

host kann ein Hostname, eine IP-Adresse oder eine leere Zeichenfolge sein. Wenn eine IP-Adresse verwendet wird, solltehost eine IPv4-formatierte Adresszeichenfolge sein. Die IP-Adresse127.0.0.1 ist die Standard-IPv4-Adresse für dieloopback-Schnittstelle, sodass nur Prozesse auf dem Host eine Verbindung zum Server herstellen können. Wenn Sie eine leere Zeichenfolge übergeben, akzeptiert der Server Verbindungen auf allen verfügbaren IPv4-Schnittstellen.

port sollte eine ganze Zahl von1 -65535 sein (0 ist reserviert). Dies ist dieTCP port-Nummer, um Verbindungen von Clients zu akzeptieren. Einige Systeme erfordern möglicherweise Superuser-Berechtigungen, wenn der Port <1024 ist.

Hier ist ein Hinweis zur Verwendung von Hostnamen mitbind():

„Wenn Sie einen Hostnamen im Host-Teil der IPv4 / v6-Socket-Adresse verwenden, zeigt das Programm möglicherweise ein nicht deterministisches Verhalten, da Python die erste von der DNS-Auflösung zurückgegebene Adresse verwendet. Die Socket-Adresse wird abhängig von den Ergebnissen der DNS-Auflösung und / oder der Host-Konfiguration unterschiedlich in eine tatsächliche IPv4 / v6-Adresse aufgelöst. Verwenden Sie für deterministisches Verhalten eine numerische Adresse im Host-Teil. “ (Source)

Ich werde dies später inUsing Hostnames näher erläutern, aber es ist hier erwähnenswert. Verstehen Sie zunächst, dass bei Verwendung eines Hostnamens je nach den Ergebnissen der Namensauflösung unterschiedliche Ergebnisse angezeigt werden können.

Es könnte alles sein. Wenn Sie Ihre Anwendung zum ersten Mal ausführen, ist dies möglicherweise die Adresse10.1.2.3. Das nächste Mal ist es eine andere Adresse,192.168.0.1. Das dritte Mal könnte es172.16.7.8 sein und so weiter.

Wenn Sie mit dem Serverbeispiel fortfahren, ermöglichtlisten() einem Server,accept() Verbindungen herzustellen. Es macht es zu einer "Hörbuchse":

s.listen()
conn, addr = s.accept()

listen() hat einenbacklog-Parameter. Es gibt die Anzahl der nicht akzeptierten Verbindungen an, die das System zulässt, bevor neue Verbindungen abgelehnt werden. Ab Python 3.5 ist dies optional. Wenn nicht angegeben, wird ein Standardwert vonbacklogausgewählt.

Wenn Ihr Server viele Verbindungsanforderungen gleichzeitig empfängt, kann das Erhöhen desbacklog-Werts hilfreich sein, indem die maximale Länge der Warteschlange für ausstehende Verbindungen festgelegt wird. Der Maximalwert ist systemabhängig. Unter Linux finden Sie beispielsweise/proc/sys/net/core/somaxconn.

accept()blocks und wartet auf eine eingehende Verbindung. Wenn ein Client eine Verbindung herstellt, gibt er ein neues Socket-Objekt zurück, das die Verbindung darstellt, und ein Tupel, das die Adresse des Clients enthält. Das Tupel enthält(host, port) für IPv4-Verbindungen oder(host, port, flowinfo, scopeid) für IPv6. SieheSocket Address Families im Referenzabschnitt für Details zu den Tupelwerten.

Eine Sache, die unbedingt verstanden werden muss, ist, dass wir jetzt ein neues Socket-Objekt vonaccept() haben. Dies ist wichtig, da es sich um den Socket handelt, mit dem Sie mit dem Client kommunizieren. Es unterscheidet sich von dem Listening-Socket, mit dem der Server neue Verbindungen akzeptiert:

conn, addr = s.accept()
with conn:
    print('Connected by', addr)
    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.sendall(data)

Nachdem das Client-Socket-Objektconn vonaccept() abgerufen wurde, wird eine Endlosschleife vonwhile verwendet, umblocking calls nachconn.recv() zu schleifen. Dies liest alle Daten, die der Client sendet, und gibt sie mitconn.sendall() zurück.

Wennconn.recv() ein leeresbytes-Objekt,b'', zurückgibt, hat der Client die Verbindung geschlossen und die Schleife wird beendet. Die Anweisungwith wird mitconn verwendet, um den Socket am Ende des Blocks automatisch zu schließen.

Echo Client

Schauen wir uns nun den Client an,echo-client.py:

#!/usr/bin/env python3

import socket

HOST = '127.0.0.1'  # The server's hostname or IP address
PORT = 65432        # The port used by the server

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('Received', repr(data))

Im Vergleich zum Server ist der Client ziemlich einfach. Es erstellt ein Socket-Objekt, stellt eine Verbindung zum Server her und rufts.sendall() auf, um seine Nachricht zu senden. Zuletzt ruft ess.recv() auf, um die Antwort des Servers zu lesen, und druckt sie dann aus.

Ausführen des Echo-Clients und -Servers

Lassen Sie uns den Client und den Server ausführen, um zu sehen, wie sie sich verhalten, und um zu überprüfen, was passiert.

Note: Wenn Sie Probleme haben, die Beispiele oder Ihren eigenen Code über die Befehlszeile auszuführen, lesen SieHow Do I Make My Own Command-Line Commands Using Python?. Wenn Sie unter Windows arbeiten, überprüfen Sie diePython Windows FAQ.

Öffnen Sie ein Terminal oder eine Eingabeaufforderung, navigieren Sie zu dem Verzeichnis, das Ihre Skripte enthält, und führen Sie den Server aus:

$ ./echo-server.py

Ihr Terminal scheint zu hängen. Dies liegt daran, dass der Server in einem Anrufblocked (angehalten) ist:

conn, addr = s.accept()

Es wartet auf eine Clientverbindung. Öffnen Sie nun ein anderes Terminalfenster oder eine andere Eingabeaufforderung und führen Sie den Client aus:

$ ./echo-client.py
Received b'Hello, world'

Im Serverfenster sollte Folgendes angezeigt werden:

$ ./echo-server.py
Connected by ('127.0.0.1', 64623)

In der obigen Ausgabe hat der Server das vons.accept() zurückgegebeneaddr-Tupel gedruckt. Dies ist die IP-Adresse und die TCP-Portnummer des Clients. Die Portnummer64623 unterscheidet sich höchstwahrscheinlich, wenn Sie sie auf Ihrem Computer ausführen.

Socket-Status anzeigen

Verwenden Sienetstat, um den aktuellen Status der Sockets auf Ihrem Host anzuzeigen. Es ist standardmäßig unter MacOS, Linux und Windows verfügbar.

Hier ist die Netstat-Ausgabe von macOS nach dem Start des Servers:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN

Beachten Sie, dassLocal Address127.0.0.1.65432 ist. Wennecho-server.pyHOST = '' anstelle vonHOST = '127.0.0.1' verwendet hätte, würde netstat dies anzeigen:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  *.65432                *.*                    LISTEN

Local Address ist*.65432, was bedeutet, dass alle verfügbaren Hostschnittstellen, die die Adressfamilie unterstützen, zum Akzeptieren eingehender Verbindungen verwendet werden. In diesem Beispiel wurde beim Aufruf vonsocket()socket.AF_INET verwendet (IPv4). Sie können dies in der SpalteProto sehen:tcp4.

Ich habe die Ausgabe oben so angepasst, dass nur der Echoserver angezeigt wird. Abhängig vom System, auf dem Sie es ausführen, werden Sie wahrscheinlich viel mehr Ausgabe sehen. Zu beachten sind die SpaltenProto,Local Address und(state). Im letzten Beispiel oben zeigt netstat, dass der Echoserver einen IPv4-TCP-Socket (tcp4) an Port 65432 an allen Schnittstellen (*.65432) verwendet und sich im Empfangsstatus befindet (LISTEN). s).

Eine andere Möglichkeit, dies zusammen mit zusätzlichen hilfreichen Informationen anzuzeigen, ist die Verwendung vonlsof (offene Dateien auflisten). Es ist standardmäßig unter macOS verfügbar und kann unter Linux mit Ihrem Paketmanager installiert werden, sofern dies noch nicht geschehen ist:

$ lsof -i -n
COMMAND     PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
Python    67982 nathan    3u  IPv4 0xecf272      0t0  TCP *:65432 (LISTEN)

lsof gibt Ihnen dieCOMMAND,PID (Prozess-ID) undUSER (Benutzer-ID) offener Internet-Sockets, wenn Sie die Option-i verwenden. Oben ist der Echo-Server-Prozess.

netstat undlsof bieten viele Optionen und unterscheiden sich je nach Betriebssystem, auf dem Sie sie ausführen. Überprüfen Sie die Seite oder Dokumentation vonmanfür beide. Es lohnt sich auf jeden Fall, ein wenig Zeit mit ihnen zu verbringen und sie kennenzulernen. Du wirst belohnt. Verwenden Sie unter MacOS und Linuxman netstat undman lsof. Verwenden Sie unter Windowsnetstat /?.

Hier ist ein häufiger Fehler, der angezeigt wird, wenn ein Verbindungsversuch zu einem Port ohne Überwachungssocket unternommen wird:

$ ./echo-client.py
Traceback (most recent call last):
  File "./echo-client.py", line 9, in 
    s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused

Entweder ist die angegebene Portnummer falsch oder der Server wird nicht ausgeführt. Oder es gibt eine Firewall im Pfad, die die Verbindung blockiert, was leicht vergessen werden kann. Möglicherweise wird auch der FehlerConnection timed out angezeigt. Fügen Sie eine Firewall-Regel hinzu, mit der der Client eine Verbindung zum TCP-Port herstellen kann!

Im Referenzabschnitt finden Sie eine Liste der gängigenerrors.

Verbindungsfehler

Schauen wir uns genauer an, wie Client und Server miteinander kommunizierten:

Sockets loopback interface

Bei Verwendung derloopback-Schnittstelle (IPv4-Adresse127.0.0.1 oder IPv6-Adresse::1) verlassen Daten niemals den Host oder berühren das externe Netzwerk. In der obigen Abbildung ist die Loopback-Schnittstelle im Host enthalten. Dies stellt die interne Natur der Loopback-Schnittstelle dar und die Verbindungen und Daten, die sie übertragen, sind lokal für den Host. Aus diesem Grund hören Sie auch die Loopback-Schnittstelle und die IP-Adresse127.0.0.1 oder::1, die als "localhost" bezeichnet werden.

Anwendungen verwenden die Loopback-Schnittstelle zur Kommunikation mit anderen auf dem Host ausgeführten Prozessen sowie zur Sicherheit und Isolierung vom externen Netzwerk. Da es intern und nur vom Host aus zugänglich ist, wird es nicht angezeigt.

Sie können dies in Aktion sehen, wenn Sie einen Anwendungsserver haben, der seine eigene private Datenbank verwendet. Wenn es sich nicht um eine Datenbank handelt, die von anderen Servern verwendet wird, ist sie wahrscheinlich so konfiguriert, dass sie nur auf der Loopback-Schnittstelle auf Verbindungen wartet. In diesem Fall können andere Hosts im Netzwerk keine Verbindung herstellen.

Wenn Sie in Ihren Anwendungen eine andere IP-Adresse als127.0.0.1 oder::1 verwenden, ist diese wahrscheinlich an eineEthernet-Schnittstelle gebunden, die mit einem externen Netzwerk verbunden ist. Dies ist Ihr Tor zu anderen Gastgebern außerhalb Ihres "localhost" -Königreichs:

Sockets ethernet interface

Sei vorsichtig da draußen. Es ist eine böse, grausame Welt. Lesen Sie unbedingt den AbschnittUsing Hostnames, bevor Sie sich aus den sicheren Grenzen von „localhost“ begeben. Es gibt einen Sicherheitshinweis, der auch dann gilt, wenn Sie keine Hostnamen und nur IP-Adressen verwenden.

Umgang mit mehreren Verbindungen

Der Echo-Server hat definitiv seine Grenzen. Das größte ist, dass es nur einen Kunden bedient und dann beendet. Der Echo-Client hat diese Einschränkung ebenfalls, es gibt jedoch ein zusätzliches Problem. Wenn der Client den folgenden Aufruf tätigt, gibts.recv() möglicherweise nur ein Byte zurück,b'H' vonb'Hello, world':

data = s.recv(1024)

Das oben verwendetebufsize-Argument von1024 ist die maximale Datenmenge, die gleichzeitig empfangen werden kann. Dies bedeutet nicht, dassrecv()1024 Bytes zurückgibt.

send() verhält sich ebenfalls so. send() gibt die Anzahl der gesendeten Bytes zurück, die möglicherweise kleiner als die Größe der übergebenen Daten ist. Sie sind dafür verantwortlich, dies zu überprüfen undsend() so oft wie nötig aufzurufen, um alle Daten zu senden:

„Die Anwendungen sind dafür verantwortlich, zu überprüfen, ob alle Daten gesendet wurden. Wenn nur ein Teil der Daten übertragen wurde, muss die Anwendung versuchen, die verbleibenden Daten zu übermitteln. “ (Source)

Wir haben dies vermieden, indem wirsendall() verwendet haben:

„Im Gegensatz zu send () sendet diese Methode weiterhin Daten aus Bytes, bis entweder alle Daten gesendet wurden oder ein Fehler auftritt. Keiner wird bei Erfolg zurückgegeben. “ (Source)

Wir haben an dieser Stelle zwei Probleme:

  • Wie gehen wir mit mehreren Verbindungen gleichzeitig um?

  • Wir müssensend() undrecv() anrufen, bis alle Daten gesendet oder empfangen werden.

Was machen wir? Es gibt viele Ansätze fürconcurrency. In jüngerer Zeit ist es ein beliebter Ansatz,Asynchronous I/O zu verwenden. asyncio wurde in Python 3.4 in die Standardbibliothek eingeführt. Die traditionelle Wahl ist die Verwendung vonthreads.

Das Problem mit der Parallelität ist, dass es schwierig ist, das Richtige zu finden. Es gibt viele Feinheiten, die berücksichtigt und verhindert werden müssen. Alles was es braucht ist, dass sich eines davon manifestiert und Ihre Anwendung plötzlich auf nicht so subtile Weise fehlschlägt.

Ich sage dies nicht, um Sie davon abzuhalten, gleichzeitig zu lernen und zu programmieren. Wenn Ihre Anwendung skaliert werden muss, ist es erforderlich, wenn Sie mehr als einen Prozessor oder einen Kern verwenden möchten. In diesem Tutorial verwenden wir jedoch etwas, das traditioneller als Threads ist und über das man leichter nachdenken kann. Wir werden den Urvater der Systemaufrufe verwenden:select().

Mitselect() können Sie überprüfen, ob die E / A an mehr als einem Socket abgeschlossen ist. Sie können alsoselect() aufrufen, um zu sehen, an welchen Sockets E / A zum Lesen und / oder Schreiben bereit sind. Aber das ist Python, also gibt es noch mehr. Wir werden dasselectors-Modul in der Standardbibliothek verwenden, damit die effizienteste Implementierung verwendet wird, unabhängig davon, auf welchem ​​Betriebssystem wir gerade ausgeführt werden:

„Dieses Modul ermöglicht ein hochgradiges und effizientes E / A-Multiplexing, das auf den Grundelementen des ausgewählten Moduls aufbaut. Benutzer werden aufgefordert, dieses Modul stattdessen zu verwenden, es sei denn, sie möchten eine präzise Kontrolle über die verwendeten Grundelemente auf Betriebssystemebene. “ (Source)

Obwohl wir mitselect() nicht gleichzeitig ausgeführt werden können, kann dieser Ansatz abhängig von Ihrer Arbeitslast immer noch sehr schnell sein. Dies hängt davon ab, was Ihre Anwendung tun muss, wenn sie eine Anfrage bearbeitet, und von der Anzahl der Clients, die sie unterstützen muss.

asyncio verwendet kooperatives Multitasking mit einem Thread und eine Ereignisschleife zum Verwalten von Aufgaben. Mitselect() schreiben wir unsere eigene Version einer Ereignisschleife, wenn auch einfacher und synchroner. Wenn Sie mehrere Threads verwenden, müssen Sie derzeitGIL mitCPython and PyPy verwenden, obwohl Sie gleichzeitig arbeiten. Dies begrenzt effektiv den Arbeitsaufwand, den wir ohnehin parallel erledigen können.

Ich sage dies alles, um zu erklären, dass die Verwendung vonselect() eine vollkommen gute Wahl sein kann. Sie müssen nichtasyncio, Threads oder die neueste asynchrone Bibliothek verwenden. In einer Netzwerkanwendung ist Ihre Anwendung normalerweise an E / A gebunden: Sie wartet möglicherweise auf das lokale Netzwerk, Endpunkte auf der anderen Seite des Netzwerks, auf einer Festplatte usw.

Wenn Sie Anforderungen von Clients erhalten, die CPU-gebundene Arbeit initiieren, sehen Sie sich das Modulconcurrent.futuresan. Es enthält die KlasseProcessPoolExecutor, die einen Pool von Prozessen verwendet, um Aufrufe asynchron auszuführen.

Wenn Sie mehrere Prozesse verwenden, kann das Betriebssystem Ihren Python-Code so planen, dass er ohne GIL parallel auf mehreren Prozessoren oder Kernen ausgeführt wird. Ideen und Anregungen finden Sie im PyCon-VortragJohn Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018.

Im nächsten Abschnitt werden Beispiele für einen Server und einen Client vorgestellt, mit denen diese Probleme behoben werden. Sie verwendenselect(), um mehrere Verbindungen gleichzeitig zu verarbeiten, und rufensend() undrecv() so oft wie nötig auf.

Multi-Connection-Client und -Server

In den nächsten beiden Abschnitten erstellen wir einen Server und einen Client, die mehrere Verbindungen mithilfe einesselector-Objekts verarbeiten, das aus demselectors-Modul erstellt wurde.

Multi-Connection Server

Schauen wir uns zunächst den Mehrfachverbindungsservermulticonn-server.py an. Hier ist der erste Teil, der die Hörbuchse einrichtet:

import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

Der größte Unterschied zwischen diesem Server und dem Echoserver ist der Aufruf vonlsock.setblocking(False), um den Socket im nicht blockierenden Modus zu konfigurieren. Anrufe an diesen Socket werden nicht mehrblock. Wenn es mitsel.select() verwendet wird, können wir, wie Sie unten sehen werden, auf Ereignisse an einem oder mehreren Sockets warten und dann Daten lesen und schreiben, wenn es fertig ist.

sel.register() registriert den zu überwachenden Socket mitsel.select() für die Ereignisse, an denen Sie interessiert sind. Für den Listening-Socket möchten wir Leseereignisse:selectors.EVENT_READ.

data wird verwendet, um beliebige Daten zusammen mit dem Socket zu speichern. Es wird zurückgegeben, wennselect() zurückgegeben wird. Wir verwendendata, um zu verfolgen, was am Socket gesendet und empfangen wurde.

Als nächstes folgt die Ereignisschleife:

import selectors
sel = selectors.DefaultSelector()

# ...

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            service_connection(key, mask)

sel.select(timeout=None)blocks, bis Sockets für E / A bereit sind. Es gibt eine Liste von (Schlüssel-, Ereignis-) Tupeln zurück, eines für jeden Socket. key ist einSelectorKeynamedtuple, das einfileobj-Attribut enthält. key.fileobj ist das Socket-Objekt undmask ist eine Ereignismaske der Operationen, die bereit sind.

Wennkey.dataNone ist, wissen wir, dass es sich um eine Hörbuchse handelt, und wir müssenaccept() die Verbindung herstellen. Wir rufen unsere eigene Wrapper-Funktionaccept() auf, um das neue Socket-Objekt abzurufen und beim Selektor zu registrieren. Wir werden es uns gleich ansehen.

Wennkey.data nichtNone ist, wissen wir, dass es sich um einen Client-Socket handelt, der bereits akzeptiert wurde, und wir müssen ihn warten. service_connection() wird dann aufgerufen undkey undmask übergeben, die alles enthalten, was wir für den Betrieb am Socket benötigen.

Schauen wir uns an, was unsereaccept_wrapper()-Funktion bewirkt:

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)

Da der Listening-Socket für das Ereignisselectors.EVENT_READ registriert wurde, sollte er bereit zum Lesen sein. Wir rufensock.accept() auf und rufen dann sofortconn.setblocking(False) auf, um den Socket in den nicht blockierenden Modus zu versetzen.

Denken Sie daran, dass dies das Hauptziel in dieser Version des Servers ist, da wir nicht möchten, dassblock erreicht werden. Wenn es blockiert, wird der gesamte Server blockiert, bis er zurückkehrt. Das heißt, andere Steckdosen warten noch. Dies ist der gefürchtete "Hang" -Zustand, in dem sich Ihr Server nicht befinden soll.

Als Nächstes erstellen wir ein Objekt, das die gewünschten Daten zusammen mit dem Socket mit der Klassetypes.SimpleNamespace enthält. Da wir wissen möchten, wann die Clientverbindung zum Lesen und Schreiben bereit ist, werden beide Ereignisse wie folgt festgelegt:

events = selectors.EVENT_READ | selectors.EVENT_WRITE

Die Maske, der Socket und die Datenobjekte voneventswerden dann ansel.register() übergeben.

Schauen wir uns nunservice_connection() an, um zu sehen, wie eine Clientverbindung behandelt wird, wenn sie bereit ist:

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print('closing connection to', data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print('echoing', repr(data.outb), 'to', data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

Dies ist das Herzstück des einfachen Mehrfachverbindungsservers. key ist dasnamedtuple, das vonselect() zurückgegeben wird und das Socket-Objekt (fileobj) und das Datenobjekt enthält. mask enthält die Ereignisse, die bereit sind.

Wenn der Socket zum Lesen bereit ist, istmask & selectors.EVENT_READ wahr undsock.recv() wird aufgerufen. Alle gelesenen Daten werden andata.outb angehängt, damit sie später gesendet werden können.

Beachten Sie den Blockelse:, wenn keine Daten empfangen werden:

if recv_data:
    data.outb += recv_data
else:
    print('closing connection to', data.addr)
    sel.unregister(sock)
    sock.close()

Dies bedeutet, dass der Client seinen Socket geschlossen hat, also sollte dies auch der Server tun. Vergessen Sie jedoch nicht, zuerstsel.unregister() aufzurufen, damitselect() nicht mehr überwacht werden.

Wenn der Socket zum Schreiben bereit ist, was bei einem fehlerfreien Socket immer der Fall sein sollte, werden alle indata.outb gespeicherten empfangenen Daten mitsock.send() an den Client zurückgesendet. Die gesendeten Bytes werden dann aus dem Sendepuffer entfernt:

data.outb = data.outb[sent:]

Multi-Connection-Client

Schauen wir uns nun den Multi-Connection-Clientmulticonn-client.py an. Es ist dem Server sehr ähnlich, aber anstatt auf Verbindungen zu warten, werden zunächst Verbindungen überstart_connections() initiiert:

messages = [b'Message 1 from client.', b'Message 2 from client.']


def start_connections(host, port, num_conns):
    server_addr = (host, port)
    for i in range(0, num_conns):
        connid = i + 1
        print('starting connection', connid, 'to', server_addr)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(connid=connid,
                                     msg_total=sum(len(m) for m in messages),
                                     recv_total=0,
                                     messages=list(messages),
                                     outb=b'')
        sel.register(sock, events, data=data)

num_conns wird von der Befehlszeile gelesen. Dies ist die Anzahl der Verbindungen, die zum Server hergestellt werden sollen. Genau wie der Server ist jeder Socket auf den nicht blockierenden Modus eingestellt.

connect_ex() wird anstelle vonconnect() verwendet, daconnect() sofort eineBlockingIOError-Ausnahme auslösen würde. connect_ex() gibt zunächst eine Fehleranzeigeerrno.EINPROGRESS zurück, anstatt eine Ausnahme auszulösen, während die Verbindung hergestellt wird. Sobald die Verbindung hergestellt ist, ist der Socket zum Lesen und Schreiben bereit und wird vonselect() als solcher zurückgegeben.

Nach dem Einrichten des Sockets werden die Daten, die mit dem Socket gespeichert werden sollen, mit der Klassetypes.SimpleNamespace erstellt. Die Nachrichten, die der Client an den Server sendet, werden mitlist(messages) kopiert, da jede Verbindungsocket.send() aufruft und die Liste ändert. Alles, was benötigt wird, um zu verfolgen, was der Client senden, senden und empfangen muss, und die Gesamtzahl der Bytes in den Nachrichten wird im Objektdata gespeichert.

Schauen wir unsservice_connection() an. Es ist im Grunde dasselbe wie der Server:

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            print('received', repr(recv_data), 'from connection', data.connid)
            data.recv_total += len(recv_data)
        if not recv_data or data.recv_total == data.msg_total:
            print('closing connection', data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print('sending', repr(data.outb), 'to connection', data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]

Es gibt einen wichtigen Unterschied. Es verfolgt die Anzahl der vom Server empfangenen Bytes, damit es die Seite der Verbindung schließen kann. Wenn der Server dies erkennt, schließt er auch seine Seite der Verbindung.

Beachten Sie, dass der Server dabei davon abhängt, dass sich der Client gut verhält: Der Server erwartet, dass der Client seine Seite der Verbindung schließt, wenn er mit dem Senden von Nachrichten fertig ist. Wenn der Client nicht geschlossen wird, lässt der Server die Verbindung offen. In einer realen Anwendung möchten Sie möglicherweise auf Ihrem Server davor schützen und verhindern, dass sich Clientverbindungen ansammeln, wenn sie nach einer bestimmten Zeit keine Anfrage senden.

Ausführen des Multi-Connection-Clients und -Servers

Lassen Sie uns nunmulticonn-server.py undmulticonn-client.py ausführen. Beide verwenden Befehlszeilenargumente. Sie können sie ohne Argumente ausführen, um die Optionen anzuzeigen.

Übergeben Sie für den Server einehost- undport-Nummer:

$ ./multiconn-server.py
usage: ./multiconn-server.py  

Übergeben Sie für den Client auch die Anzahl der zu erstellenden Verbindungen an den Server,num_connections:

$ ./multiconn-client.py
usage: ./multiconn-client.py   

Unten sehen Sie die Serverausgabe beim Abhören der Loopback-Schnittstelle an Port 65432:

$ ./multiconn-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)
accepted connection from ('127.0.0.1', 61354)
accepted connection from ('127.0.0.1', 61355)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61354)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61355)
closing connection to ('127.0.0.1', 61354)
closing connection to ('127.0.0.1', 61355)

Unten sehen Sie die Client-Ausgabe, wenn zwei Verbindungen zum obigen Server hergestellt werden:

$ ./multiconn-client.py 127.0.0.1 65432 2
starting connection 1 to ('127.0.0.1', 65432)
starting connection 2 to ('127.0.0.1', 65432)
sending b'Message 1 from client.' to connection 1
sending b'Message 2 from client.' to connection 1
sending b'Message 1 from client.' to connection 2
sending b'Message 2 from client.' to connection 2
received b'Message 1 from client.Message 2 from client.' from connection 1
closing connection 1
received b'Message 1 from client.Message 2 from client.' from connection 2
closing connection 2

Anwendungsclient und -server

Das Beispiel für einen Client und Server mit mehreren Verbindungen ist definitiv eine Verbesserung gegenüber dem Ausgangspunkt. Lassen Sie uns jedoch noch einen Schritt weiter gehen und die Mängel des vorherigen "Multiconn" -Beispiels in einer endgültigen Implementierung beheben: den Anwendungsclient und -server.

Wir möchten einen Client und einen Server, die Fehler angemessen behandeln, damit andere Verbindungen nicht betroffen sind. Offensichtlich sollte unser Client oder Server nicht vor Wut zusammenbrechen, wenn eine Ausnahme nicht abgefangen wird. Dies haben wir bisher noch nicht besprochen. Ich habe die Fehlerbehandlung aus Gründen der Kürze und Klarheit in den Beispielen absichtlich weggelassen.

Nachdem Sie mit der grundlegenden API, den nicht blockierenden Sockets undselect()vertraut sind, können wir einige Fehlerbehandlungen hinzufügen und den „Elefanten im Raum“ diskutieren, den ich hinter diesem großen Vorhang vor Ihnen versteckt habe da drüben. Ja, ich spreche von der benutzerdefinierten Klasse, die ich bereits in der Einleitung erwähnt habe. Ich wusste, dass du es nicht vergessen würdest.

Lassen Sie uns zunächst die Fehler beheben:

„Alle Fehler lösen Ausnahmen aus. Die normalen Ausnahmen für ungültige Argumenttypen und Bedingungen für zu wenig Speicher können ausgelöst werden. Ab Python 3.3 erhöhen Fehler im Zusammenhang mit der Socket- oder AdressensemantikOSError oder eine seiner Unterklassen. “ (Source)

Wir müssenOSError fangen. Eine andere Sache, die ich in Bezug auf Fehler nicht erwähnt habe, sind Zeitüberschreitungen. Sie werden sie an vielen Stellen in der Dokumentation sehen. Zeitüberschreitungen treten auf und sind ein „normaler“ Fehler. Hosts und Router werden neu gestartet, Switch-Ports werden defekt, Kabel werden defekt, Kabel werden abgezogen, wie Sie es nennen. Sie sollten auf diese und andere Fehler vorbereitet sein und sie in Ihrem Code behandeln.

Was ist mit dem "Elefanten im Raum"? Wie der Socket-Typsocket.SOCK_STREAM andeutet, lesen Sie bei Verwendung von TCP aus einem kontinuierlichen Bytestrom. Es ist wie beim Lesen aus einer Datei auf der Festplatte, aber stattdessen lesen Sie Bytes aus dem Netzwerk.

Im Gegensatz zum Lesen einer Datei gibt es jedoch keinef.seek(). Mit anderen Worten, Sie können den Socket-Zeiger, falls vorhanden, nicht neu positionieren und sich nach dem Zufallsprinzip um die Daten bewegen, wann immer Sie möchten.

Wenn Bytes an Ihrem Socket ankommen, sind Netzwerkpuffer beteiligt. Sobald Sie sie gelesen haben, müssen sie irgendwo gespeichert werden. Durch erneutes Aufrufen vonrecv() wird der nächste vom Socket verfügbare Bytestrom gelesen.

Dies bedeutet, dass Sie in Blöcken aus dem Socket lesen. Sie müssenrecv() aufrufen und die Daten in einem Puffer speichern, bis Sie genügend Bytes gelesen haben, um eine vollständige Nachricht zu erhalten, die für Ihre Anwendung sinnvoll ist.

Es liegt an Ihnen, zu definieren und zu verfolgen, wo sich die Nachrichtengrenzen befinden. Beim TCP-Socket werden lediglich Rohbytes zum und vom Netzwerk gesendet und empfangen. Es weiß nichts darüber, was diese rohen Bytes bedeuten.

Dies bringt uns dazu, ein Protokoll auf Anwendungsebene zu definieren. Was ist ein Protokoll auf Anwendungsebene? Einfach ausgedrückt, Ihre Anwendung sendet und empfängt Nachrichten. Diese Nachrichten sind das Protokoll Ihrer Anwendung.

Mit anderen Worten, die Länge und das Format, die Sie für diese Nachrichten auswählen, bestimmen die Semantik und das Verhalten Ihrer Anwendung. Dies steht in direktem Zusammenhang mit dem, was ich im vorherigen Absatz zum Lesen von Bytes aus dem Socket erklärt habe. Wenn Sie Bytes mitrecv() lesen, müssen Sie mit der Anzahl der gelesenen Bytes Schritt halten und herausfinden, wo sich die Nachrichtengrenzen befinden.

Wie wird das gemacht? Eine Möglichkeit besteht darin, immer Nachrichten mit fester Länge zu senden. Wenn sie immer gleich groß sind, ist es einfach. Wenn Sie diese Anzahl von Bytes in einen Puffer eingelesen haben, wissen Sie, dass Sie eine vollständige Nachricht haben.

Die Verwendung von Nachrichten mit fester Länge ist jedoch für kleine Nachrichten ineffizient, bei denen Sie zum Ausfüllen Auffüllen benötigen. Außerdem bleibt das Problem, was mit Daten zu tun ist, die nicht in eine Nachricht passen.

In diesem Tutorial verfolgen wir einen allgemeinen Ansatz. Ein Ansatz, der von vielen Protokollen verwendet wird, einschließlich HTTP. Wir stellen Nachrichten einen Header voran, der die Inhaltslänge sowie alle anderen Felder enthält, die wir benötigen. Auf diese Weise müssen wir nur mit dem Header Schritt halten. Sobald wir den Header gelesen haben, können wir ihn verarbeiten, um die Länge des Nachrichteninhalts zu bestimmen, und dann die Anzahl der Bytes lesen, um ihn zu verbrauchen.

Wir implementieren dies, indem wir eine benutzerdefinierte Klasse erstellen, die Nachrichten senden und empfangen kann, die Text oder Binärdaten enthalten. Sie können es für Ihre eigenen Anwendungen verbessern und erweitern. Das Wichtigste ist, dass Sie ein Beispiel dafür sehen können.

Ich muss etwas in Bezug auf Sockets und Bytes erwähnen, das Sie betreffen kann. Wie bereits erwähnt, senden und empfangen Sie beim Senden und Empfangen von Daten über Sockets Rohbytes.

Wenn Sie Daten empfangen und in einem Kontext verwenden möchten, in dem sie als mehrere Bytes interpretiert werden, z. B. eine 4-Byte-Ganzzahl, müssen Sie berücksichtigen, dass sie möglicherweise in einem Format vorliegen, das nicht für die CPU Ihres Computers typisch ist. Der Client oder Server am anderen Ende verfügt möglicherweise über eine CPU, die eine andere Bytereihenfolge als Ihre eigene verwendet. In diesem Fall müssen Sie es in die native Bytereihenfolge Ihres Hosts konvertieren, bevor Sie es verwenden können.

Diese Bytereihenfolge wird alsendiannesseiner CPU bezeichnet. SieheByte Endianness im Referenzabschnitt für Details. Wir vermeiden dieses Problem, indem wir Unicode für unseren Nachrichtenkopf verwenden und die Codierung UTF-8 verwenden. Da UTF-8 eine 8-Bit-Codierung verwendet, gibt es keine Probleme bei der Bytereihenfolge.

Eine Erklärung finden Sie in der Dokumentation zu PythonEncodings and Unicode. Beachten Sie, dass dies nur für den Textkopf gilt. Wir verwenden einen expliziten Typ und eine explizite Codierung, die im Header für den zu sendenden Inhalt, die Nachrichtennutzlast, definiert sind. Auf diese Weise können wir alle gewünschten Daten (Text oder Binärdaten) in einem beliebigen Format übertragen.

Mitsys.byteorder können Sie die Bytereihenfolge Ihres Computers leicht ermitteln. Auf meinem Intel-Laptop geschieht dies beispielsweise:

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'little'

Wenn ich dies in einer virtuellen Maschine ausführe, dieemulateseine Big-Endian-CPU (PowerPC) ist, geschieht Folgendes:

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'big'

In dieser Beispielanwendung definiert unser Protokoll auf Anwendungsebene den Header als Unicode-Text mit einer UTF-8-Codierung. Für den tatsächlichen Inhalt der Nachricht, die Nutzdaten der Nachricht, müssen Sie die Bytereihenfolge bei Bedarf noch manuell austauschen.

Dies hängt von Ihrer Anwendung ab und davon, ob Multibyte-Binärdaten von einem Computer mit einer anderen Endianness verarbeitet werden müssen oder nicht. Sie können Ihrem Client oder Server bei der Implementierung der Binärunterstützung helfen, indem Sie zusätzliche Header hinzufügen und diese zum Übergeben von Parametern verwenden, ähnlich wie bei HTTP.

Mach dir keine Sorgen, wenn dies noch keinen Sinn ergibt. Im nächsten Abschnitt werden Sie sehen, wie all dies funktioniert und zusammenpasst.

Anwendungsprotokoll-Header

Definieren wir den Protokollheader vollständig. Der Protokollheader lautet:

  • Text variabler Länge

  • Unicode mit der Codierung UTF-8

  • Ein Python-Wörterbuch, das mitJSON serialisiert wurde

Die erforderlichen Header oder Unterheader im Wörterbuch des Protokollheaders lauten wie folgt:

Name Beschreibung

byteorder

Die Bytereihenfolge der Maschine (verwendetsys.byteorder). Dies ist für Ihre Bewerbung möglicherweise nicht erforderlich.

content-length

Die Länge des Inhalts in Bytes.

content-type

Die Art des Inhalts in der Nutzlast, z. B.text/json oderbinary/my-binary-type.

content-encoding

Die vom Inhalt verwendete Codierung, z. B.utf-8 für Unicode-Text oderbinary für Binärdaten.

Diese Header informieren den Empfänger über den Inhalt der Nutzdaten der Nachricht. Auf diese Weise können Sie beliebige Daten senden und gleichzeitig genügend Informationen bereitstellen, damit der Inhalt vom Empfänger korrekt dekodiert und interpretiert werden kann. Da sich die Header in einem Wörterbuch befinden, können Sie einfach zusätzliche Header hinzufügen, indem Sie bei Bedarf Schlüssel / Wert-Paare einfügen.

Senden einer Anwendungsnachricht

Es gibt immer noch ein kleines Problem. Wir haben einen Header mit variabler Länge, der schön und flexibel ist, aber woher kennen Sie die Länge des Headers, wenn Sie ihn mitrecv() lesen?

Als wir zuvor über die Verwendung vonrecv() und Nachrichtengrenzen gesprochen haben, habe ich erwähnt, dass Header mit fester Länge ineffizient sein können. Das stimmt, aber wir werden einen kleinen 2-Byte-Header mit fester Länge verwenden, um dem JSON-Header, der seine Länge enthält, ein Präfix voranzustellen.

Sie können sich dies als einen hybriden Ansatz zum Senden von Nachrichten vorstellen. Tatsächlich booten wir den Nachrichtenempfangsprozess, indem wir zuerst die Länge des Headers senden. Dies erleichtert es unserem Empfänger, die Nachricht zu dekonstruieren.

Um Ihnen eine bessere Vorstellung vom Nachrichtenformat zu geben, betrachten wir eine Nachricht in ihrer Gesamtheit:

Sockets application message

Eine Nachricht beginnt mit einem Header fester Länge von 2 Bytes, der eine Ganzzahl in der Reihenfolge der Netzwerkbytes darstellt. Dies ist die Länge des nächsten Headers, des JSON-Headers mit variabler Länge. Sobald wir 2 Bytes mitrecv() gelesen haben, wissen wir, dass wir die 2 Bytes als Ganzzahl verarbeiten und dann diese Anzahl von Bytes lesen können, bevor wir den UTF-8-JSON-Header dekodieren.

DasJSON header enthält ein Wörterbuch mit zusätzlichen Headern. Eines davon istcontent-length, dh die Anzahl der Bytes des Nachrichteninhalts (ohne den JSON-Header). Sobald wirrecv() aufgerufen undcontent-length Bytes gelesen haben, haben wir eine Nachrichtengrenze erreicht und eine gesamte Nachricht gelesen.

Anwendungsnachrichtenklasse

Endlich die Auszahlung! Schauen wir uns die KlasseMessagean und sehen wir, wie sie mitselect()verwendet wird, wenn Lese- und Schreibereignisse auf dem Socket auftreten.

Für diese Beispielanwendung musste ich mir eine Idee einfallen lassen, welche Arten von Nachrichten der Client und der Server verwenden würden. Wir sind zu diesem Zeitpunkt weit über Toy Echo-Clients und -Server hinaus.

Um die Dinge einfach zu halten und dennoch zu demonstrieren, wie die Dinge in einer realen Anwendung funktionieren würden, habe ich ein Anwendungsprotokoll erstellt, das eine grundlegende Suchfunktion implementiert. Der Client sendet eine Suchanforderung und der Server sucht nach einer Übereinstimmung. Wenn die vom Client gesendete Anforderung nicht als Suche erkannt wird, geht der Server davon aus, dass es sich um eine binäre Anforderung handelt, und gibt eine binäre Antwort zurück.

Nachdem Sie die folgenden Abschnitte gelesen, die Beispiele ausgeführt und mit dem Code experimentiert haben, werden Sie sehen, wie die Dinge funktionieren. Sie können dann die KlasseMessageals Ausgangspunkt verwenden und sie für Ihren eigenen Gebrauch ändern.

Wir sind wirklich nicht so weit vom Beispiel für "Multiconn" -Client und -Server entfernt. Der Ereignisschleifencode bleibt inapp-client.py undapp-server.py gleich. Ich habe den Nachrichtencode in eine Klasse mit dem NamenMessage verschoben und Methoden hinzugefügt, um das Lesen, Schreiben und Verarbeiten der Header und Inhalte zu unterstützen. Dies ist ein großartiges Beispiel für die Verwendung einer Klasse.

Wie bereits erwähnt, und Sie werden unten sehen, müssen Sie beim Arbeiten mit Sockets den Status beibehalten. Durch die Verwendung einer Klasse halten wir den gesamten Status, die Daten und den Code in einer organisierten Einheit gebündelt. Eine Instanz der Klasse wird für jeden Socket im Client und Server erstellt, wenn eine Verbindung gestartet oder akzeptiert wird.

Die Klasse ist für den Client und den Server für die Wrapper- und Utility-Methoden meistens gleich. Sie beginnen mit einem Unterstrich wieMessage._json_encode(). Diese Methoden vereinfachen die Arbeit mit der Klasse. Sie helfen anderen Methoden, indem sie kürzer bleiben und dasDRY-Prinzip unterstützen.

DieMessage-Klasse des Servers funktioniert im Wesentlichen genauso wie die des Clients und umgekehrt. Der Unterschied besteht darin, dass der Client die Verbindung initiiert und eine Anforderungsnachricht sendet, gefolgt von der Verarbeitung der Antwortnachricht des Servers. Umgekehrt wartet der Server auf eine Verbindung, verarbeitet die Anforderungsnachricht des Clients und sendet dann eine Antwortnachricht.

Es sieht aus wie das:

Step Endpunkt Aktions- / Nachrichteninhalt

1

Klient

Sendet einMessage mit Anforderungsinhalt

2

Server

Empfängt und verarbeitet die ClientanforderungMessage

3

Server

Sendet einMessage mit Antwortinhalt

4

Klient

Empfängt und verarbeitet die ServerantwortMessage

Hier ist das Datei- und Code-Layout:

Anwendung File Code

Server

app-server.py

Das Hauptskript des Servers

Server

libserver.py

DieMessage-Klasse des Servers

Klient

app-client.py

Das Hauptskript des Clients

Klient

libclient.py

DieMessage-Klasse des Clients

Nachrichteneingabepunkt

Ich möchte diskutieren, wie die KlasseMessagefunktioniert, indem ich zunächst einen Aspekt ihres Designs erwähne, der mir nicht sofort klar war. Erst nachdem ich es mindestens fünf Mal umgestaltet hatte, kam ich zu dem, was es aktuell ist. Why? Status verwalten.

Nachdem einMessage-Objekt erstellt wurde, wird es einem Socket zugeordnet, der mitselector.register() auf Ereignisse überwacht wird:

message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)

Note: Einige der Codebeispiele in diesem Abschnitt stammen aus dem Hauptskript des Servers und der Klasse vonMessage. Dieser Abschnitt und die Diskussion gelten jedoch auch für den Client. Ich werde die Client-Version zeigen und erklären, wenn sie sich unterscheidet.

Wenn Ereignisse am Socket bereit sind, werden sie vonselector.select() zurückgegeben. Wir können dann mithilfe des Attributsdata für das Objektkey einen Verweis auf das Nachrichtenobjekt zurückholen und eine Methode inMessage aufrufen:

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        # ...
        message = key.data
        message.process_events(mask)

Wenn Sie sich die Ereignisschleife oben ansehen, sehen Sie, dass sichsel.select() auf dem Fahrersitz befindet. Es blockiert und wartet oben in der Schleife auf Ereignisse. Es ist für das Aufwachen verantwortlich, wenn Lese- und Schreibereignisse bereit sind, auf dem Socket verarbeitet zu werden. Dies bedeutet indirekt, dass es auch für den Aufruf der Methodeprocess_events() verantwortlich ist. Das meine ich, wenn ich sage, dass die Methodeprocess_events() der Einstiegspunkt ist.

Mal sehen, was dieprocess_events()-Methode bewirkt:

def process_events(self, mask):
    if mask & selectors.EVENT_READ:
        self.read()
    if mask & selectors.EVENT_WRITE:
        self.write()

Das ist gut:process_events() ist einfach. Es kann nur zwei Dinge tun:read() undwrite() aufrufen.

Dies bringt uns zurück zum Verwaltungsstaat. Nach einigen Umgestaltungen entschied ich, dass eine andere Methode, die von Zustandsvariablen mit einem bestimmten Wert abhängt, nur vonread() undwrite() aufgerufen wird. Dies hält die Logik so einfach wie möglich, da Ereignisse zur Verarbeitung auf dem Socket eingehen.

Dies mag offensichtlich erscheinen, aber die ersten Iterationen der Klasse waren eine Mischung aus einigen Methoden, die den aktuellen Zustand überprüften und je nach Wert andere Methoden zum Verarbeiten von Daten außerhalb vonread() oderwrite() genannt wurden. Am Ende erwies sich dies als zu komplex, um es zu verwalten und mitzuhalten.

Sie sollten die Klasse auf jeden Fall an Ihre eigenen Bedürfnisse anpassen, damit sie für Sie am besten funktioniert. Ich würde jedoch empfehlen, die Statusprüfungen und die Aufrufe von Methoden, die von diesem Status abhängen, aufread() undwrite()zu beschränken ) s Methoden, wenn möglich.

Schauen wir unsread() an. Dies ist die Version des Servers, aber die des Clients ist dieselbe. Es wird nur ein anderer Methodenname verwendet,process_response() anstelle vonprocess_request():

def read(self):
    self._read()

    if self._jsonheader_len is None:
        self.process_protoheader()

    if self._jsonheader_len is not None:
        if self.jsonheader is None:
            self.process_jsonheader()

    if self.jsonheader:
        if self.request is None:
            self.process_request()

Die_read()-Methode wird zuerst aufgerufen. Es ruftsocket.recv() auf, um Daten aus dem Socket zu lesen und in einem Empfangspuffer zu speichern.

Denken Sie daran, dass beim Aufruf vonsocket.recv() möglicherweise noch nicht alle Daten eingetroffen sind, aus denen eine vollständige Nachricht besteht. socket.recv() müssen möglicherweise erneut aufgerufen werden. Aus diesem Grund werden für jeden Teil der Nachricht Statusprüfungen durchgeführt, bevor die entsprechende Methode zur Verarbeitung aufgerufen wird.

Bevor eine Methode ihren Teil der Nachricht verarbeitet, prüft sie zunächst, ob genügend Bytes in den Empfangspuffer eingelesen wurden. Wenn dies der Fall ist, verarbeitet es die entsprechenden Bytes, entfernt sie aus dem Puffer und schreibt die Ausgabe in eine Variable, die von der nächsten Verarbeitungsstufe verwendet wird. Da eine Nachricht drei Komponenten enthält, gibt es drei Statusprüfungen undprocess-Methodenaufrufe:

Nachrichtenkomponente Methode Ausgabe

Header mit fester Länge

process_protoheader()

self._jsonheader_len

JSON-Header

process_jsonheader()

self.jsonheader

Inhalt

process_request()

self.request

Schauen wir uns als nächsteswrite() an. Dies ist die Serverversion:

def write(self):
    if self.request:
        if not self.response_created:
            self.create_response()

    self._write()

write() sucht zuerst nachrequest. Wenn eine vorhanden ist und keine Antwort erstellt wurde, wirdcreate_response() aufgerufen. create_response() setzt die Zustandsvariableresponse_created und schreibt die Antwort in den Sendepuffer.

Die Methode_write() ruftsocket.send() auf, wenn sich Daten im Sendepuffer befinden.

Denken Sie daran, dass beim Aufruf vonsocket.send() möglicherweise nicht alle Daten im Sendepuffer für die Übertragung in die Warteschlange gestellt wurden. Die Netzwerkpuffer für den Socket sind möglicherweise voll, undsocket.send() müssen möglicherweise erneut aufgerufen werden. Aus diesem Grund gibt es Zustandsüberprüfungen. create_response() sollte nur einmal aufgerufen werden, es wird jedoch erwartet, dass_write() mehrmals aufgerufen werden muss.

Die Client-Version vonwrite() ist ähnlich:

def write(self):
    if not self._request_queued:
        self.queue_request()

    self._write()

    if self._request_queued:
        if not self._send_buffer:
            # Set selector to listen for read events, we're done writing.
            self._set_selector_events_mask('r')

Da der Client eine Verbindung zum Server herstellt und zuerst eine Anforderung sendet, wird die Statusvariable_request_queued überprüft. Wenn eine Anforderung nicht in die Warteschlange gestellt wurde, ruft siequeue_request() auf. queue_request() erstellt die Anforderung und schreibt sie in den Sendepuffer. Außerdem wird die Statusvariable_request_queued so festgelegt, dass sie nur einmal aufgerufen wird.

Genau wie der Server ruft_write()socket.send() auf, wenn sich Daten im Sendepuffer befinden.

Der bemerkenswerte Unterschied in der Client-Version vonwrite() ist die letzte Überprüfung, ob die Anforderung in die Warteschlange gestellt wurde. Dies wird im AbschnittClient Main Script näher erläutert. Der Grund dafür ist jedoch,selector.select() anzuweisen, die Überwachung des Sockets auf Schreibereignisse zu beenden. Wenn die Anforderung in die Warteschlange gestellt wurde und der Sendepuffer leer ist, sind wir mit dem Schreiben fertig und nur an Leseereignissen interessiert. Es gibt keinen Grund, benachrichtigt zu werden, dass der Socket beschreibbar ist.

Ich werde diesen Abschnitt abschließen, indem ich Ihnen einen Gedanken hinterlasse. Der Hauptzweck dieses Abschnitts war es zu erklären, dassselector.select() über die Methodeprocess_events() in die KlasseMessage aufruft, und zu beschreiben, wie der Status verwaltet wird.

Dies ist wichtig, daprocess_events() während der gesamten Lebensdauer der Verbindung mehrmals aufgerufen wird. Stellen Sie daher sicher, dass alle Methoden, die nur einmal aufgerufen werden sollen, entweder selbst eine Statusvariable überprüfen oder die von der Methode festgelegte Statusvariable vom Aufrufer überprüft wird.

Server-Hauptskript

Im Hauptskriptapp-server.pydes Servers werden Argumente aus der Befehlszeile gelesen, die die Schnittstelle und den Port angeben, die abgehört werden sollen:

$ ./app-server.py
usage: ./app-server.py  

Geben Sie beispielsweise Folgendes ein, um die Loopback-Schnittstelle an Port65432 abzuhören:

$ ./app-server.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)

Verwenden Sie eine leere Zeichenfolge für<host>, um alle Schnittstellen abzuhören.

Nach dem Erstellen des Sockets wirdsocket.setsockopt() mit der Optionsocket.SO_REUSEADDR aufgerufen:

# Avoid bind() exception: OSError: [Errno 48] Address already in use
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Durch Einstellen dieser Socket-Option wird der FehlerAddress already in use vermieden. Dies wird angezeigt, wenn der Server gestartet wird und ein zuvor verwendeter TCP-Socket am selben Port Verbindungen im StatusTIME_WAIThat.

Wenn der Server beispielsweise eine Verbindung aktiv geschlossen hat, bleibt er je nach Betriebssystem mindestens zwei Minuten lang im StatusTIME_WAIT. Wenn Sie versuchen, den Server erneut zu starten, bevor der Status vonTIME_WAITabläuft, erhalten Sie eineOSError-Ausnahme vonAddress already in use. Dies ist ein Schutz, um sicherzustellen, dass verzögerte Pakete im Netzwerk nicht an die falsche Anwendung gesendet werden.

Die Ereignisschleife fängt alle Fehler ab, sodass der Server aktiv bleiben und weiter ausgeführt werden kann:

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            message = key.data
            try:
                message.process_events(mask)
            except Exception:
                print('main: error: exception for',
                      f'{message.addr}:\n{traceback.format_exc()}')
                message.close()

Wenn eine Clientverbindung akzeptiert wird, wird einMessage-Objekt erstellt:

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    message = libserver.Message(sel, conn, addr)
    sel.register(conn, selectors.EVENT_READ, data=message)

Das ObjektMessage ist dem Socket im Aufruf vonsel.register() zugeordnet und wird zunächst so eingestellt, dass es nur auf Leseereignisse überwacht wird. Sobald die Anforderung gelesen wurde, ändern wir sie so, dass sie nur auf Schreibereignisse wartet.

Ein Vorteil dieses Ansatzes auf dem Server besteht darin, dass ein Socket in den meisten Fällen, wenn er fehlerfrei ist und keine Netzwerkprobleme vorliegen, immer beschreibbar ist.

Wenn wirsel.register() anweisen würden, auchEVENT_WRITE zu überwachen, würde die Ereignisschleife sofort aufwachen und uns benachrichtigen, dass dies der Fall ist. Zu diesem Zeitpunkt gibt es jedoch keinen Grund, aufzuwachen undsend() am Socket anzurufen. Es ist keine Antwort zu senden, da eine Anfrage noch nicht bearbeitet wurde. Dies würde wertvolle CPU-Zyklen verbrauchen und verschwenden.

Server-Nachrichtenklasse

Im AbschnittMessage Entry Point haben wir uns angesehen, wie das ObjektMessage in Aktion gerufen wurde, als Socket-Ereignisse überprocess_events() bereit waren. Schauen wir uns nun an, was passiert, wenn Daten auf dem Socket gelesen werden und eine Komponente oder ein Teil der Nachricht bereit ist, vom Server verarbeitet zu werden.

Die Nachrichtenklasse des Servers ist inlibserver.py. Sie finden diesource code on GitHub.

Die Methoden werden in der Klasse in der Reihenfolge angezeigt, in der die Verarbeitung für eine Nachricht erfolgt.

Wenn der Server mindestens 2 Bytes gelesen hat, kann der Header mit fester Länge verarbeitet werden:

def process_protoheader(self):
    hdrlen = 2
    if len(self._recv_buffer) >= hdrlen:
        self._jsonheader_len = struct.unpack('>H',
                                             self._recv_buffer[:hdrlen])[0]
        self._recv_buffer = self._recv_buffer[hdrlen:]

Der Header mit fester Länge ist eine 2-Byte-Ganzzahl in der Netzwerk-Bytereihenfolge (Big-Endian), die die Länge des JSON-Headers enthält. struct.unpack() wird verwendet, um den Wert zu lesen, zu dekodieren und inself._jsonheader_len zu speichern. Nach der Verarbeitung des Teils der Nachricht, für den sie verantwortlich ist, entferntprocess_protoheader() sie aus dem Empfangspuffer.

Genau wie der Header mit fester Länge kann auch der Datenpuffer verarbeitet werden, wenn sich genügend Daten im Empfangspuffer befinden, um den JSON-Header aufzunehmen:

def process_jsonheader(self):
    hdrlen = self._jsonheader_len
    if len(self._recv_buffer) >= hdrlen:
        self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
                                            'utf-8')
        self._recv_buffer = self._recv_buffer[hdrlen:]
        for reqhdr in ('byteorder', 'content-length', 'content-type',
                       'content-encoding'):
            if reqhdr not in self.jsonheader:
                raise ValueError(f'Missing required header "{reqhdr}".')

Die Methodeself._json_decode() wird aufgerufen, um den JSON-Header zu dekodieren und in ein Wörterbuch zu deserialisieren. Da der JSON-Header als Unicode mit einer UTF-8-Codierung definiert ist, wirdutf-8 im Aufruf fest codiert. Das Ergebnis wird inself.jsonheader gespeichert. Nach der Verarbeitung des Teils der Nachricht, für den es verantwortlich ist, entferntprocess_jsonheader() es aus dem Empfangspuffer.

Als nächstes folgt der tatsächliche Inhalt oder die Nutzlast der Nachricht. Es wird durch den JSON-Header inself.jsonheader beschrieben. Wenncontent-length Bytes im Empfangspuffer verfügbar sind, kann die Anforderung verarbeitet werden:

def process_request(self):
    content_len = self.jsonheader['content-length']
    if not len(self._recv_buffer) >= content_len:
        return
    data = self._recv_buffer[:content_len]
    self._recv_buffer = self._recv_buffer[content_len:]
    if self.jsonheader['content-type'] == 'text/json':
        encoding = self.jsonheader['content-encoding']
        self.request = self._json_decode(data, encoding)
        print('received request', repr(self.request), 'from', self.addr)
    else:
        # Binary or unknown content-type
        self.request = data
        print(f'received {self.jsonheader["content-type"]} request from',
              self.addr)
    # Set selector to listen for write events, we're done reading.
    self._set_selector_events_mask('w')

Nachdem der Nachrichteninhalt in der Variablendata gespeichert wurde, entferntprocess_request() ihn aus dem Empfangspuffer. Wenn der Inhaltstyp dann JSON ist, wird er dekodiert und deserialisiert. Wenn dies bei dieser Beispielanwendung nicht der Fall ist, wird davon ausgegangen, dass es sich um eine binäre Anforderung handelt, und der Inhaltstyp wird einfach gedruckt.

Als letztes ändertprocess_request() den Selektor, um nur Schreibereignisse zu überwachen. Im Hauptskript des Servers,app-server.py, ist der Socket zunächst so eingestellt, dass nur Leseereignisse überwacht werden. Nachdem die Anfrage vollständig bearbeitet wurde, sind wir nicht mehr am Lesen interessiert.

Eine Antwort kann jetzt erstellt und in den Socket geschrieben werden. Wenn der Socket beschreibbar ist, wirdcreate_response() vonwrite() aufgerufen:

def create_response(self):
    if self.jsonheader['content-type'] == 'text/json':
        response = self._create_response_json_content()
    else:
        # Binary or unknown content-type
        response = self._create_response_binary_content()
    message = self._create_message(**response)
    self.response_created = True
    self._send_buffer += message

Eine Antwort wird durch Aufrufen anderer Methoden erstellt, abhängig vom Inhaltstyp. In dieser Beispielanwendung wird eine einfache Wörterbuchsuche für JSON-Anforderungen durchgeführt, wennaction == 'search'. Sie können andere Methoden für Ihre eigenen Anwendungen definieren, die hier aufgerufen werden.

Nach dem Erstellen der Antwortnachricht wird die Statusvariableself.response_created so eingestellt, dasswrite()create_response() nicht erneut aufruft. Schließlich wird die Antwort an den Sendepuffer angehängt. Dies wird von_write() gesehen und gesendet.

Eine schwierige Aufgabe war es, die Verbindung zu schließen, nachdem die Antwort geschrieben wurde. Ich habeclose() in der Methode_write() aufgerufen:

def _write(self):
    if self._send_buffer:
        print('sending', repr(self._send_buffer), 'to', self.addr)
        try:
            # Should be ready to write
            sent = self.sock.send(self._send_buffer)
        except BlockingIOError:
            # Resource temporarily unavailable (errno EWOULDBLOCK)
            pass
        else:
            self._send_buffer = self._send_buffer[sent:]
            # Close when the buffer is drained. The response has been sent.
            if sent and not self._send_buffer:
                self.close()

Obwohl es etwas "versteckt" ist, halte ich es für einen akzeptablen Kompromiss, da die KlasseMessagenur eine Nachricht pro Verbindung verarbeitet. Nachdem die Antwort geschrieben wurde, hat der Server nichts mehr zu tun. Es hat seine Arbeit abgeschlossen.

Client-Hauptskript

Im Hauptskriptapp-client.pydes Clients werden Argumente aus der Befehlszeile gelesen und zum Erstellen von Anforderungen und zum Starten von Verbindungen zum Server verwendet:

$ ./app-client.py
usage: ./app-client.py    

Hier ist ein Beispiel:

$ ./app-client.py 127.0.0.1 65432 search needle

Nach dem Erstellen eines Wörterbuchs, das die Anforderung aus den Befehlszeilenargumenten darstellt, werden Host, Port und Anforderungswörterbuch anstart_connection() übergeben:

def start_connection(host, port, request):
    addr = (host, port)
    print('starting connection to', addr)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(False)
    sock.connect_ex(addr)
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    message = libclient.Message(sel, sock, addr, request)
    sel.register(sock, events, data=message)

Mit dem Wörterbuchrequestwird ein Socket für die Serververbindung sowie einMessage-Objekt erstellt.

Wie der Server ist dasMessage-Objekt dem Socket im Aufruf vonsel.register() zugeordnet. Für den Client ist der Socket jedoch zunächst so eingestellt, dass er sowohl auf Lese- als auch auf Schreibereignisse überwacht wird. Sobald die Anfrage geschrieben wurde, ändern wir sie so, dass sie nur auf Leseereignisse wartet.

Dieser Ansatz bietet uns den gleichen Vorteil wie der Server: Wir verschwenden keine CPU-Zyklen. Nachdem die Anfrage gesendet wurde, sind wir nicht mehr an Schreibereignissen interessiert. Es gibt also keinen Grund, sie zu aktivieren und zu verarbeiten.

Client-Nachrichtenklasse

Im AbschnittMessage Entry Point haben wir uns angesehen, wie das Nachrichtenobjekt in Aktion gerufen wurde, als Socket-Ereignisse überprocess_events() bereit waren. Schauen wir uns nun an, was passiert, nachdem Daten auf dem Socket gelesen und geschrieben wurden und eine Nachricht vom Client verarbeitet werden kann.

Die Nachrichtenklasse des Clients ist inlibclient.py. Sie finden diesource code on GitHub.

Die Methoden werden in der Klasse in der Reihenfolge angezeigt, in der die Verarbeitung für eine Nachricht erfolgt.

Die erste Aufgabe für den Client besteht darin, die Anforderung in die Warteschlange zu stellen:

def queue_request(self):
    content = self.request['content']
    content_type = self.request['type']
    content_encoding = self.request['encoding']
    if content_type == 'text/json':
        req = {
            'content_bytes': self._json_encode(content, content_encoding),
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    else:
        req = {
            'content_bytes': content,
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    message = self._create_message(**req)
    self._send_buffer += message
    self._request_queued = True

Die Wörterbücher, die zum Erstellen der Anforderung verwendet werden, befinden sich je nach Übergabe in der Befehlszeile im Hauptskript des Clients,app-client.py. Das Anforderungswörterbuch wird als Argument an die Klasse übergeben, wenn einMessage-Objekt erstellt wird.

Die Anforderungsnachricht wird erstellt und an den Sendepuffer angehängt, der dann von_write() gesehen und gesendet wird. Die Zustandsvariableself._request_queued ist so eingestellt, dassqueue_request() nicht erneut aufgerufen wird.

Nachdem die Anfrage gesendet wurde, wartet der Client auf eine Antwort vom Server.

Die Methoden zum Lesen und Verarbeiten einer Nachricht im Client sind dieselben wie beim Server. Wenn Antwortdaten aus dem Socket gelesen werden, werden die Header-Methodenprocess aufgerufen:process_protoheader() undprocess_jsonheader().

Der Unterschied besteht in der Benennung der endgültigenprocess-Methoden und der Tatsache, dass sie eine Antwort verarbeiten und keine erstellen:process_response(),_process_response_json_content() und_process_response_binary_content().

Zu guter Letzt ist der letzte Aufruf fürprocess_response():

def process_response(self):
    # ...
    # Close when response has been processed
    self.close()
Zusammenfassung der Nachrichtenklasse

Ich werde die Klassendiskussion vonMessageabschließen, indem ich einige Dinge erwähne, die mit einigen der unterstützenden Methoden zu beachten sind.

Alle von der Klasse ausgelösten Ausnahmen werden vom Hauptskript in seinerexcept-Klausel abgefangen:

try:
    message.process_events(mask)
except Exception:
    print('main: error: exception for',
          f'{message.addr}:\n{traceback.format_exc()}')
    message.close()

Beachten Sie die letzte Zeile:message.close().

Dies ist aus mehr als einem Grund eine wirklich wichtige Zeile! Es stellt nicht nur sicher, dass der Socket geschlossen ist, sondernmessage.close() entfernt auch den Socket von der Überwachung durchselect(). Dies vereinfacht den Code in der Klasse erheblich und reduziert die Komplexität. Wenn es eine Ausnahme gibt oder wir selbst explizit eine auslösen, wissen wir, dassclose() sich um die Bereinigung kümmert.

Die MethodenMessage._read() undMessage._write() enthalten auch etwas Interessantes:

def _read(self):
    try:
        # Should be ready to read
        data = self.sock.recv(4096)
    except BlockingIOError:
        # Resource temporarily unavailable (errno EWOULDBLOCK)
        pass
    else:
        if data:
            self._recv_buffer += data
        else:
            raise RuntimeError('Peer closed.')

Beachten Sie die Zeileexcept:except BlockingIOError:.

_write() hat auch eine. Diese Zeilen sind wichtig, da sie einen vorübergehenden Fehler abfangen und mitpass überspringen. Der vorübergehende Fehler liegt vor, wenn der Socketblock würde, z. B. wenn er im Netzwerk oder am anderen Ende der Verbindung (seinem Peer) wartet.

Wenn Sie die Ausnahme mitpass abfangen und überspringen, rufen unsselect() schließlich erneut an und wir erhalten eine weitere Chance, die Daten zu lesen oder zu schreiben.

Ausführen des Anwendungsclients und -servers

Lassen Sie uns nach all dieser harten Arbeit Spaß haben und einige Suchvorgänge durchführen!

In diesen Beispielen führe ich den Server so aus, dass er alle Schnittstellen überwacht, indem ich eine leere Zeichenfolge für das Argumenthostübergebe. Auf diese Weise kann ich den Client ausführen und eine Verbindung von einer virtuellen Maschine in einem anderen Netzwerk herstellen. Es emuliert eine Big-Endian-PowerPC-Maschine.

Starten wir zunächst den Server:

$ ./app-server.py '' 65432
listening on ('', 65432)

Führen Sie nun den Client aus und geben Sie eine Suche ein. Mal sehen, ob wir ihn finden können:

$ ./app-client.py 10.0.1.1 65432 search morpheus
starting connection to ('10.0.1.1', 65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 41}{"action": "search", "value": "morpheus"}' to ('10.0.1.1', 65432)
received response {'result': 'Follow the white rabbit. 🐰'} from ('10.0.1.1', 65432)
got result: Follow the white rabbit. 🐰
closing connection to ('10.0.1.1', 65432)

Auf meinem Terminal wird eine Shell ausgeführt, die eine Textcodierung von Unicode (UTF-8) verwendet, sodass die obige Ausgabe gut mit Emojis gedruckt werden kann.

Mal sehen, ob wir die Welpen finden können:

$ ./app-client.py 10.0.1.1 65432 search 🐶
starting connection to ('10.0.1.1', 65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"action": "search", "value": "\xf0\x9f\x90\xb6"}' to ('10.0.1.1', 65432)
received response {'result': '🐾 Playing ball! 🏐'} from ('10.0.1.1', 65432)
got result: 🐾 Playing ball! 🏐
closing connection to ('10.0.1.1', 65432)

Beachten Sie die über das Netzwerk gesendete Byte-Zeichenfolge für die Anforderung in der Zeilesending. Es ist einfacher zu erkennen, ob Sie nach den hexadezimal gedruckten Bytes suchen, die das Welpen-Emoji darstellen:🐶. Ich konnteenter the emoji für die Suche verwenden, da mein Terminal Unicode mit der Codierung UTF-8 verwendet.

Dies zeigt, dass wir Rohbytes über das Netzwerk senden und diese vom Empfänger dekodiert werden müssen, um korrekt interpretiert zu werden. Aus diesem Grund haben wir uns alle Mühe gegeben, einen Header zu erstellen, der den Inhaltstyp und die Codierung enthält.

Hier ist die Serverausgabe von beiden oben genannten Clientverbindungen:

accepted connection from ('10.0.2.2', 55340)
received request {'action': 'search', 'value': 'morpheus'} from ('10.0.2.2', 55340)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 43}{"result": "Follow the white rabbit. \xf0\x9f\x90\xb0"}' to ('10.0.2.2', 55340)
closing connection to ('10.0.2.2', 55340)

accepted connection from ('10.0.2.2', 55338)
received request {'action': 'search', 'value': '🐶'} from ('10.0.2.2', 55338)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"result": "\xf0\x9f\x90\xbe Playing ball! \xf0\x9f\x8f\x90"}' to ('10.0.2.2', 55338)
closing connection to ('10.0.2.2', 55338)

Sehen Sie sich die Zeilesendingan, um die Bytes zu sehen, die in den Socket des Clients geschrieben wurden. Dies ist die Antwortnachricht des Servers.

Sie können auch das Senden von Binäranforderungen an den Server testen, wenn das Argumentactionnichtsearchlautet:

$ ./app-client.py 10.0.1.1 65432 binary 😃
starting connection to ('10.0.1.1', 65432)
sending b'\x00|{"byteorder": "big", "content-type": "binary/custom-client-binary-type", "content-encoding": "binary", "content-length": 10}binary\xf0\x9f\x98\x83' to ('10.0.1.1', 65432)
received binary/custom-server-binary-type response from ('10.0.1.1', 65432)
got response: b'First 10 bytes of request: binary\xf0\x9f\x98\x83'
closing connection to ('10.0.1.1', 65432)

Dacontent-type der Anforderung nichttext/json ist, behandelt der Server sie als benutzerdefinierten Binärtyp und führt keine JSON-Decodierung durch. Es gibt einfach diecontent-type aus und gibt die ersten 10 Bytes an den Client zurück:

$ ./app-server.py '' 65432
listening on ('', 65432)
accepted connection from ('10.0.2.2', 55320)
received binary/custom-client-binary-type request from ('10.0.2.2', 55320)
sending b'\x00\x7f{"byteorder": "little", "content-type": "binary/custom-server-binary-type", "content-encoding": "binary", "content-length": 37}First 10 bytes of request: binary\xf0\x9f\x98\x83' to ('10.0.2.2', 55320)
closing connection to ('10.0.2.2', 55320)

Fehlerbehebung

Zwangsläufig funktioniert etwas nicht und Sie werden sich fragen, was Sie tun sollen. Keine Sorge, es passiert uns allen. Mithilfe dieses Tutorials, Ihres Debuggers und Ihrer bevorzugten Suchmaschine können Sie hoffentlich wieder mit dem Quellcodeteil beginnen.

Wenn nicht, sollte Ihr erster Stopp die Dokumentation von Pythonsocket moduleein. Stellen Sie sicher, dass Sie die gesamte Dokumentation für jede Funktion oder Methode lesen, die Sie aufrufen. Lesen Sie auch den AbschnittReference, um Ideen zu erhalten. Überprüfen Sie insbesondere den AbschnittErrors.

Manchmal geht es nicht nur um den Quellcode. Der Quellcode ist möglicherweise korrekt und nur der andere Host, der Client oder der Server. Oder es könnte das Netzwerk sein, zum Beispiel ein Router, eine Firewall oder ein anderes Netzwerkgerät, das Man-in-the-Middle spielt.

Für diese Art von Problemen sind zusätzliche Tools unerlässlich. Im Folgenden finden Sie einige Tools und Dienstprogramme, die möglicherweise helfen oder zumindest einige Hinweise geben.

ping

ping prüft, ob ein Host lebt und mit dem Netzwerk verbunden ist, indem eine Echoanforderung vonICMPgesendet wird. Es kommuniziert direkt mit dem TCP / IP-Protokollstapel des Betriebssystems und funktioniert somit unabhängig von Anwendungen, die auf dem Host ausgeführt werden.

Unten finden Sie ein Beispiel für das Ausführen von Ping unter macOS:

$ ping -c 3 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.058 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.165 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.164 ms

--- 127.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.058/0.129/0.165/0.050 ms

Beachten Sie die Statistiken am Ende der Ausgabe. Dies kann hilfreich sein, wenn Sie versuchen, zeitweise Verbindungsprobleme zu entdecken. Gibt es zum Beispiel einen Paketverlust? Wie viel Latenz gibt es (siehe Hin- und Rückfahrzeiten)?

Wenn zwischen Ihnen und dem anderen Host eine Firewall besteht, ist die Echoanforderung eines Pings möglicherweise nicht zulässig. Einige Firewall-Administratoren implementieren Richtlinien, die dies erzwingen. Die Idee ist, dass sie nicht möchten, dass ihre Gastgeber auffindbar sind. Wenn dies der Fall ist und Sie Firewall-Regeln hinzugefügt haben, damit die Hosts kommunizieren können, stellen Sie sicher, dass die Regeln auch ICMP zwischen ihnen zulassen.

ICMP ist das vonping verwendete Protokoll, aber es ist auch das Protokoll, das TCP und andere Protokolle niedrigerer Ebene zur Übermittlung von Fehlermeldungen verwenden. Wenn Sie ein seltsames Verhalten oder langsame Verbindungen feststellen, kann dies der Grund sein.

ICMP-Nachrichten werden nach Typ und Code identifiziert. Um Ihnen einen Eindruck von den wichtigen Informationen zu geben, die sie enthalten, sind hier einige:

ICMP-Typ ICMP-Code Beschreibung

8

0

Echoanforderung

0

0

Echo Antwort

3

0

Zielnetzwerk nicht erreichbar

3

1

Ziel-Host nicht erreichbar

3

2

Zielprotokoll nicht erreichbar

3

3

Zielport nicht erreichbar

3

4

Fragmentierung erforderlich und DF-Flag gesetzt

11

0

TTL ist während des Transports abgelaufen

Informationen zur Fragmentierung und zu ICMP-Nachrichten finden Sie im ArtikelPath MTU Discovery. Dies ist ein Beispiel für etwas, das seltsames Verhalten verursachen kann, das ich zuvor erwähnt habe.

netstat

Im AbschnittViewing Socket State haben wir uns angesehen, wienetstat verwendet werden kann, um Informationen zu Sockets und ihrem aktuellen Status anzuzeigen. Dieses Dienstprogramm ist unter MacOS, Linux und Windows verfügbar.

Die SpaltenRecv-Q undSend-Q wurden in der Beispielausgabe nicht erwähnt. In diesen Spalten wird die Anzahl der Bytes angezeigt, die in Netzwerkpuffern gespeichert sind, die zum Senden oder Empfangen in die Warteschlange gestellt wurden, aber aus irgendeinem Grund von der Remote- oder lokalen Anwendung nicht gelesen oder geschrieben wurden.

Mit anderen Worten, die Bytes warten in Netzwerkpuffern in den Warteschlangen des Betriebssystems. Ein Grund könnte sein, dass die Anwendung CPU-gebunden ist oder auf andere Weise nicht in der Lage ist,socket.recv() odersocket.send() aufzurufen und die Bytes zu verarbeiten. Oder es kann Netzwerkprobleme geben, die sich auf die Kommunikation auswirken, z. B. Überlastung oder Ausfall der Netzwerkhardware oder -verkabelung.

Um dies zu demonstrieren und zu sehen, wie viele Daten ich senden konnte, bevor ein Fehler angezeigt wurde, habe ich einen Testclient geschrieben, der eine Verbindung zu einem Testserver herstellt und wiederholtsocket.send() aufruft. Der Testserver ruft niemalssocket.recv() auf. Es akzeptiert nur die Verbindung. Dies führt dazu, dass die Netzwerkpuffer auf dem Server gefüllt werden, was schließlich einen Fehler auf dem Client auslöst.

Zuerst habe ich den Server gestartet:

$ ./app-server-test.py 127.0.0.1 65432
listening on ('127.0.0.1', 65432)

Dann habe ich den Client ausgeführt. Mal sehen, was der Fehler ist:

$ ./app-client-test.py 127.0.0.1 65432 binary test
error: socket.send() blocking io exception for ('127.0.0.1', 65432):
BlockingIOError(35, 'Resource temporarily unavailable')

Hier ist die Ausgabe vonnetstat, während der Client und der Server noch ausgeführt wurden, wobei der Client die obige Fehlermeldung mehrmals ausdruckt:

$ netstat -an | grep 65432
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  408300      0  127.0.0.1.65432        127.0.0.1.53225        ESTABLISHED
tcp4       0 269868  127.0.0.1.53225        127.0.0.1.65432        ESTABLISHED
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN

Der erste Eintrag ist der Server (Local Address hat Port 65432):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4  408300      0  127.0.0.1.65432        127.0.0.1.53225        ESTABLISHED

Beachten Sie dieRecv-Q:408300.

Der zweite Eintrag ist der Client (Foreign Address hat Port 65432):

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0 269868  127.0.0.1.53225        127.0.0.1.65432        ESTABLISHED

Beachten Sie dieSend-Q:269868.

Der Client hat sicher versucht, Bytes zu schreiben, aber der Server hat sie nicht gelesen. Dies führte dazu, dass die Netzwerkpufferwarteschlange des Servers auf der Empfangsseite und die Netzwerkpufferwarteschlange des Clients auf der Sendeseite gefüllt wurden.

Windows

Wenn Sie mit Windows arbeiten, gibt es eine Reihe von Dienstprogrammen, die Sie unbedingt überprüfen sollten, wenn Sie dies noch nicht getan haben:Windows Sysinternals.

Einer von ihnen istTCPView.exe. TCPView ist ein grafischesnetstat für Windows. Zusätzlich zu Adressen, Portnummern und Socket-Status werden die laufenden Summen für die Anzahl der gesendeten und empfangenen Pakete und Bytes angezeigt. Wie beim Unix-Dienstprogrammlsof erhalten Sie auch den Prozessnamen und die ID. Überprüfen Sie die Menüs auf andere Anzeigeoptionen.

TCPView screenshot

Wireshark

Manchmal müssen Sie sehen, was auf dem Kabel passiert. Vergessen Sie, was im Anwendungsprotokoll steht oder welchen Wert ein Bibliotheksaufruf zurückgibt. Sie möchten sehen, was tatsächlich im Netzwerk gesendet oder empfangen wird. Genau wie bei Debuggern gibt es keinen Ersatz, wenn Sie es sehen müssen.

Wireshark ist eine Netzwerkprotokollanalysator- und Verkehrserfassungsanwendung, die unter anderem unter MacOS, Linux und Windows ausgeführt wird. Es gibt eine GUI-Version mit dem Namenwireshark und eine textbasierte Terminalversion mit dem Namentshark.

Das Ausführen einer Verkehrserfassung ist eine hervorragende Möglichkeit, um zu beobachten, wie sich eine Anwendung im Netzwerk verhält, und um Beweise dafür zu sammeln, was sie sendet und empfängt, wie oft und wie viel. Sie können auch sehen, wann ein Client oder Server eine Verbindung schließt oder abbricht oder nicht mehr reagiert. Diese Informationen können bei der Fehlerbehebung äußerst hilfreich sein.

Es gibt viele gute Tutorials und andere Ressourcen im Web, die Sie durch die Grundlagen der Verwendung von Wireshark und TShark führen.

Hier ist ein Beispiel für eine Verkehrserfassung mit Wireshark auf der Loopback-Schnittstelle:

Wireshark screenshot

Hier ist das gleiche Beispiel, das oben mittshark gezeigt wurde:

$ tshark -i lo0 'tcp port 65432'
Capturing on 'Loopback'
    1   0.000000    127.0.0.1 → 127.0.0.1    TCP 68 53942 → 65432 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=0 SACK_PERM=1
    2   0.000057    127.0.0.1 → 127.0.0.1    TCP 68 65432 → 53942 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=940533635 SACK_PERM=1
    3   0.000068    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
    4   0.000075    127.0.0.1 → 127.0.0.1    TCP 56 [TCP Window Update] 65432 → 53942 [ACK] Seq=1 Ack=1 Win=408288 Len=0 TSval=940533635 TSecr=940533635
    5   0.000216    127.0.0.1 → 127.0.0.1    TCP 202 53942 → 65432 [PSH, ACK] Seq=1 Ack=1 Win=408288 Len=146 TSval=940533635 TSecr=940533635
    6   0.000234    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [ACK] Seq=1 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
    7   0.000627    127.0.0.1 → 127.0.0.1    TCP 204 65432 → 53942 [PSH, ACK] Seq=1 Ack=147 Win=408128 Len=148 TSval=940533635 TSecr=940533635
    8   0.000649    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=147 Ack=149 Win=408128 Len=0 TSval=940533635 TSecr=940533635
    9   0.000668    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [FIN, ACK] Seq=149 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   10   0.000682    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   11   0.000687    127.0.0.1 → 127.0.0.1    TCP 56 [TCP Dup ACK 6#1] 65432 → 53942 [ACK] Seq=150 Ack=147 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   12   0.000848    127.0.0.1 → 127.0.0.1    TCP 56 53942 → 65432 [FIN, ACK] Seq=147 Ack=150 Win=408128 Len=0 TSval=940533635 TSecr=940533635
   13   0.001004    127.0.0.1 → 127.0.0.1    TCP 56 65432 → 53942 [ACK] Seq=150 Ack=148 Win=408128 Len=0 TSval=940533635 TSecr=940533635
^C13 packets captured

Referenz

Dieser Abschnitt dient als allgemeine Referenz mit zusätzlichen Informationen und Links zu externen Ressourcen.

Python-Dokumentation

Fehler

Folgendes stammt aus der Moduldokumentation von Pythonsocket:

„Alle Fehler lösen Ausnahmen aus. Die normalen Ausnahmen für ungültige Argumenttypen und Bedingungen für zu wenig Speicher können ausgelöst werden. Ab Python 3.3 erhöhen Fehler im Zusammenhang mit der Socket- oder AdressensemantikOSError oder eine seiner Unterklassen. “ (Source)

Hier sind einige häufige Fehler, auf die Sie wahrscheinlich beim Arbeiten mit Sockets stoßen werden:

Ausnahme errno Konstante Beschreibung

BlockingIOError

EWOULDBLOCK

Ressource vorübergehend nicht verfügbar. Wenn im nicht blockierenden Modus beispielsweisesend() aufgerufen wird und der Peer beschäftigt ist und nicht liest, ist die Sendewarteschlange (Netzwerkpuffer) voll. Oder es gibt Probleme mit dem Netzwerk. Hoffentlich ist dies ein vorübergehender Zustand.

OSError

EADDRINUSE

Adresse bereits verwendet. Stellen Sie sicher, dass kein anderer Prozess ausgeführt wird, der dieselbe Portnummer verwendet, und Ihr Server die Socket-OptionSO_REUSEADDR:socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) festlegt.

ConnectionResetError

ECONNRESET

Verbindung von Peer zurückgesetzt. Der Remote-Prozess ist abgestürzt oder hat seinen Socket nicht ordnungsgemäß geschlossen (unsauberes Herunterfahren). Oder es gibt eine Firewall oder ein anderes Gerät im Netzwerkpfad, bei dem Regeln fehlen oder sich schlecht verhalten.

TimeoutError

ETIMEDOUT

Zeit abgelaufen für Vorgang. Keine Antwort von Peer.

ConnectionRefusedError

ECONNREFUSED

Verbindung abgelehnt. Keine Anwendung überwacht den angegebenen Port.

Socket-Adressfamilien

socket.AF_INET undsocket.AF_INET6 stellen die Adress- und Protokollfamilien dar, die für das erste Argument zusocket.socket() verwendet wurden. APIs, die eine Adresse verwenden, erwarten, dass sie in einem bestimmten Format vorliegt, je nachdem, ob der Socket mitsocket.AF_INET odersocket.AF_INET6 erstellt wurde.

Adresse Familie Protokoll Adresse Tupel Beschreibung

socket.AF_INET

IPv4

(host, port)

host ist eine Zeichenfolge mit einem Hostnamen wie'www.example.com' oder einer IPv4-Adresse wie'10.1.2.3'. port ist eine ganze Zahl.

socket.AF_INET6

IPv6

(host, port, flowinfo, scopeid)

host ist eine Zeichenfolge mit einem Hostnamen wie'www.example.com' oder einer IPv6-Adresse wie'fe80::6203:7ab:fe88:9c23'. port ist eine ganze Zahl. flowinfo undscopeid repräsentieren diesin6_flowinfo undsin6_scope_id Mitglieder in der C structsockaddr_in6.

Beachten Sie den folgenden Auszug aus der Dokumentation des Python-Socket-Moduls zumhost-Wert des Adresstupels:

„Für IPv4-Adressen werden zwei spezielle Formulare anstelle einer Hostadresse akzeptiert: Die leere Zeichenfolge steht fürINADDR_ANY und die Zeichenfolge'<broadcast>' fürINADDR_BROADCAST. Dieses Verhalten ist nicht mit IPv6 kompatibel. Daher sollten Sie diese vermeiden, wenn Sie IPv6 mit Ihren Python-Programmen unterstützen möchten. “ (Source)

Weitere Informationen finden Sie unterSocket families documentationvon Python.

Ich habe in diesem Lernprogramm IPv4-Sockets verwendet. Wenn Ihr Netzwerk dies unterstützt, versuchen Sie, IPv6 zu testen und nach Möglichkeit zu verwenden. Eine Möglichkeit, dies einfach zu unterstützen, ist die Verwendung der Funktionsocket.getaddrinfo(). Es übersetzt die Argumentehost undport in eine Folge von 5 Tupeln, die alle erforderlichen Argumente zum Erstellen eines mit diesem Dienst verbundenen Sockets enthält. socket.getaddrinfo() versteht und interpretiert übergebene IPv6-Adressen und Hostnamen, die zusätzlich zu IPv4 in IPv6-Adressen aufgelöst werden.

Im folgenden Beispiel werden Adressinformationen für eine TCP-Verbindung anexample.org an Port80 zurückgegeben:

>>>

>>> socket.getaddrinfo("example.org", 80, proto=socket.IPPROTO_TCP)
[(, ,
 6, '', ('2606:2800:220:1:248:1893:25c8:1946', 80, 0, 0)),
 (, ,
 6, '', ('93.184.216.34', 80))]

Die Ergebnisse können auf Ihrem System abweichen, wenn IPv6 nicht aktiviert ist. Die oben zurückgegebenen Werte können verwendet werden, indem sie ansocket.socket() undsocket.connect() übergeben werden. In denExample sectionder Python-Dokumentation zum Socket-Modul finden Sie ein Client- und Server-Beispiel.

Verwenden von Hostnamen

Für den Kontext gilt dieser Abschnitt hauptsächlich für die Verwendung von Hostnamen mitbind() undconnect() oderconnect_ex(), wenn Sie die Loopback-Schnittstelle "localhost" verwenden möchten. Es gilt jedoch jedes Mal, wenn Sie einen Hostnamen verwenden, und es wird erwartet, dass er in eine bestimmte Adresse aufgelöst wird und für Ihre Anwendung eine besondere Bedeutung hat, die sich auf das Verhalten oder die Annahmen auswirkt. Dies steht im Gegensatz zu dem typischen Szenario, in dem ein Client einen Hostnamen verwendet, um eine Verbindung zu einem von DNS aufgelösten Server wie www.example.com herzustellen.

Folgendes stammt aus der Moduldokumentation von Pythonsocket:

„Wenn Sie einen Hostnamen im Host-Teil der IPv4 / v6-Socket-Adresse verwenden, zeigt das Programm möglicherweise ein nicht deterministisches Verhalten, da Python die erste von der DNS-Auflösung zurückgegebene Adresse verwendet. Die Socket-Adresse wird abhängig von den Ergebnissen der DNS-Auflösung und / oder der Host-Konfiguration unterschiedlich in eine tatsächliche IPv4 / v6-Adresse aufgelöst. Verwenden Sie für deterministisches Verhalten eine numerische Adresse im Host-Teil. “ (Source)

Die Standardkonvention für den Namen "https://en.wikipedia.org/wiki/Localhost[localhost]" besteht darin, die Loopback-Schnittstelle in127.0.0.1 oder::1 aufzulösen. Dies wird höchstwahrscheinlich auf Ihrem System der Fall sein, aber möglicherweise nicht. Dies hängt davon ab, wie Ihr System für die Namensauflösung konfiguriert ist. Wie bei allen IT-Dingen gibt es immer Ausnahmen und es gibt keine Garantie dafür, dass die Verwendung des Namens „localhost“ eine Verbindung zur Loopback-Schnittstelle herstellt.

Unter Linux finden Sie beispielsweiseman nsswitch.conf, die Konfigurationsdatei für den Name Service Switch. Ein weiterer Ort, an dem Sie unter MacOS und Linux nachsehen können, ist die Datei/etc/hosts. Unter Windows sieheC:\Windows\System32\drivers\etc\hosts. Die Dateihosts enthält eine statische Namenstabelle, um Zuordnungen in einem einfachen Textformat zu adressieren. DNS ist insgesamt ein weiteres Puzzleteil.

Interessanterweise gibt es zum Zeitpunkt dieses Schreibens (Juni 2018) einen RFC-EntwurfLet ‘localhost’ be localhost, in dem die Konventionen, Annahmen und die Sicherheit unter Verwendung des Namens "localhost" erörtert werden.

Es ist wichtig zu verstehen, dass bei Verwendung von Hostnamen in Ihrer Anwendung die zurückgegebenen Adressen buchstäblich alles sein können. Machen Sie keine Annahmen bezüglich eines Namens, wenn Sie eine sicherheitsrelevante Anwendung haben. Abhängig von Ihrer Anwendung und Umgebung kann dies ein Problem für Sie sein oder auch nicht.

Note: Sicherheitsvorkehrungen und Best Practices gelten weiterhin, auch wenn Ihre Anwendung nicht "sicherheitsrelevant" ist. Wenn Ihre Anwendung auf das Netzwerk zugreift, sollte sie gesichert und gewartet werden. Dies bedeutet mindestens:

  • Systemsoftware-Updates und Sicherheitspatches werden regelmäßig angewendet, einschließlich Python. Verwenden Sie Bibliotheken von Drittanbietern? Wenn ja, stellen Sie sicher, dass diese ebenfalls überprüft und aktualisiert werden.

  • Verwenden Sie nach Möglichkeit eine dedizierte oder hostbasierte Firewall, um Verbindungen nur zu vertrauenswürdigen Systemen einzuschränken.

  • Welche DNS-Server sind konfiguriert? Vertrauen Sie ihnen und ihren Administratoren?

  • Stellen Sie sicher, dass die Anforderungsdaten so weit wie möglich bereinigt und validiert sind, bevor Sie anderen Code aufrufen, der sie verarbeitet. Verwenden Sie dazu (Fuzz-) Tests und führen Sie diese regelmäßig durch.

Unabhängig davon, ob Sie Hostnamen verwenden oder nicht, wenn Ihre Anwendung sichere Verbindungen (Verschlüsselung und Authentifizierung) unterstützen muss, sollten Sie wahrscheinlichTLS verwenden. Dies ist ein eigenes Thema und geht über den Rahmen dieses Tutorials hinaus. Weitere Informationen finden Sie unterssl module documentationvon Python. Dies ist das gleiche Protokoll, das Ihr Webbrowser verwendet, um eine sichere Verbindung zu Websites herzustellen.

Bei zu berücksichtigenden Schnittstellen, IP-Adressen und Namensauflösungen gibt es viele Variablen. Was sollte man tun? Hier sind einige Empfehlungen, die Sie verwenden können, wenn Sie keinen Überprüfungsprozess für Netzwerkanwendungen haben:

Anwendung Verwendungszweck Empfehlung

Server

Loopback-Schnittstelle

Verwenden Sie eine IP-Adresse, z. B.127.0.0.1 oder::1.

Server

Ethernet-Schnittstelle

Verwenden Sie eine IP-Adresse, z. B.10.1.2.3. Verwenden Sie für alle Schnittstellen / Adressen eine leere Zeichenfolge, um mehr als eine Schnittstelle zu unterstützen. Siehe den Sicherheitshinweis oben.

Klient

Loopback-Schnittstelle

Verwenden Sie eine IP-Adresse, z. B.127.0.0.1 oder::1.

Klient

Ethernet-Schnittstelle

Verwenden Sie eine IP-Adresse, um Konsistenz zu gewährleisten und sich nicht auf die Namensauflösung zu verlassen. Verwenden Sie im typischen Fall einen Hostnamen. Siehe den Sicherheitshinweis oben.

Wenn Sie für Clients oder Server den Host authentifizieren müssen, zu dem Sie eine Verbindung herstellen, sollten Sie TLS verwenden.

Anrufe blockieren

Eine Socket-Funktion oder -Methode, mit der Ihre Anwendung vorübergehend angehalten wird, ist ein blockierender Aufruf. Zum Beispielaccept(),connect(),send() undrecv() "Block". Sie kehren nicht sofort zurück. Blockierende Anrufe müssen warten, bis Systemaufrufe (E / A) abgeschlossen sind, bevor sie einen Wert zurückgeben können. Sie als Anrufer werden also blockiert, bis sie abgeschlossen sind oder eine Zeitüberschreitung oder ein anderer Fehler auftritt.

Blockierende Socket-Anrufe können in den nicht blockierenden Modus versetzt werden, sodass sie sofort zurückkehren. In diesem Fall müssen Sie Ihre Anwendung mindestens umgestalten oder neu gestalten, um den Socket-Vorgang zu erledigen, wenn er fertig ist.

Da der Anruf sofort zurückkehrt, sind die Daten möglicherweise nicht bereit. Der Angerufene wartet im Netzwerk und hatte keine Zeit, seine Arbeit abzuschließen. In diesem Fall ist der aktuelle Status der Werterrnosocket.EWOULDBLOCK. Der nicht blockierende Modus wird mitsetblocking() unterstützt.

Standardmäßig werden Sockets immer im Blockierungsmodus erstellt. Eine Beschreibung der drei Modi finden Sie unterNotes on socket timeouts.

Verbindungen schließen

Interessant bei TCP ist, dass es für den Client oder Server völlig legal ist, die Seite der Verbindung zu schließen, während die andere Seite offen bleibt. Dies wird als "halboffene" Verbindung bezeichnet. Es ist die Entscheidung des Antrags, ob dies wünschenswert ist oder nicht. Im Allgemeinen ist dies nicht der Fall. In diesem Zustand kann die Seite, die das Ende der Verbindung geschlossen hat, keine Daten mehr senden. Sie können es nur empfangen.

Ich befürworte nicht, dass Sie diesen Ansatz wählen, aber als Beispiel verwendet HTTP einen Header mit dem Namen "Verbindung", mit dem standardisiert wird, wie Anwendungen offene Verbindungen schließen oder beibehalten sollen. Einzelheiten finden Sie untersection 6.3 in RFC 7230, Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing.

Wenn Sie Ihre Anwendung und ihr Protokoll auf Anwendungsebene entwerfen und schreiben, sollten Sie zunächst herausfinden, wie Verbindungen voraussichtlich geschlossen werden. Manchmal ist dies offensichtlich und einfach, oder es kann einige anfängliche Prototypen und Tests erforderlich sein. Dies hängt von der Anwendung ab und davon, wie die Nachrichtenschleife mit den erwarteten Daten verarbeitet wird. Stellen Sie einfach sicher, dass die Steckdosen nach Abschluss ihrer Arbeiten immer rechtzeitig geschlossen werden.

Byte Endianness

InWikipedia’s article on endianness finden Sie Details dazu, wie verschiedene CPUs die Bytereihenfolge im Speicher speichern. Bei der Interpretation einzelner Bytes ist dies kein Problem. Wenn Sie jedoch mehrere Bytes verarbeiten, die als ein einziger Wert gelesen und verarbeitet werden, z. B. eine 4-Byte-Ganzzahl, muss die Bytereihenfolge umgekehrt werden, wenn Sie mit einem Computer kommunizieren, der eine andere Endianness verwendet.

Die Bytereihenfolge ist auch wichtig für Textzeichenfolgen, die wie Unicode als Mehrbyte-Sequenzen dargestellt werden. Wenn Sie nicht immer "true", strengeASCII verwenden und die Client- und Server-Implementierungen steuern, ist es wahrscheinlich besser, Unicode mit einer Codierung wie UTF-8 oder einer Codierung, diebyte order mark (BOM) unterstützt, zu verwenden.

Es ist wichtig, die in Ihrem Anwendungsschichtprotokoll verwendete Codierung explizit zu definieren. Sie können dies tun, indem Sie festlegen, dass der gesamte Text UTF-8 ist, oder einen Header für die Inhaltscodierung verwenden, der die Codierung angibt. Dies verhindert, dass Ihre Anwendung die Codierung erkennen muss, die Sie nach Möglichkeit vermeiden sollten.

Dies wird problematisch, wenn Daten in Dateien oder einer Datenbank gespeichert sind und keine Metadaten verfügbar sind, die die Codierung angeben. Wenn die Daten an einen anderen Endpunkt übertragen werden, muss versucht werden, die Codierung zu erkennen. Eine Diskussion finden Sie unterWikipedia’s Unicode article, die aufRFC 3629: UTF-8, a transformation format of ISO 10646 verweist:

„RFC 3629, der UTF-8-Standard, empfiehlt jedoch, dass Byte-Ordnungsmarkierungen in Protokollen, die UTF-8 verwenden, verboten sind, erörtert jedoch die Fälle, in denen dies möglicherweise nicht möglich ist. Darüber hinaus bedeutet die große Einschränkung möglicher Muster in UTF-8 (zum Beispiel kann es keine einzelnen Bytes mit gesetztem High-Bit geben), dass es möglich sein sollte, UTF-8 von anderen Zeichencodierungen zu unterscheiden, ohne sich auf die Stückliste zu verlassen. “ (Source)

Die Konsequenz daraus ist, immer die Codierung zu speichern, die für Daten verwendet wird, die von Ihrer Anwendung verarbeitet werden, wenn sie variieren können. Mit anderen Worten, versuchen Sie, die Codierung irgendwie als Metadaten zu speichern, wenn es sich nicht immer um UTF-8 oder eine andere Codierung mit einer Stückliste handelt. Anschließend können Sie diese Codierung zusammen mit den Daten in einem Header senden, um dem Empfänger mitzuteilen, um was es sich handelt.

Die in TCP / IP verwendete Bytereihenfolge beträgtbig-endian und wird als Netzwerkreihenfolge bezeichnet. Die Netzwerkreihenfolge wird verwendet, um Ganzzahlen in unteren Schichten des Protokollstapels wie IP-Adressen und Portnummern darzustellen. Das Socket-Modul von Python enthält Funktionen, die Ganzzahlen in und aus der Netzwerk- und Host-Bytereihenfolge konvertieren:

Funktion Beschreibung

socket.ntohl(x)

Konvertieren Sie positive 32-Bit-Ganzzahlen vom Netzwerk in die Host-Bytereihenfolge. Auf Computern, auf denen die Host-Bytereihenfolge mit der Netzwerkbyte-Reihenfolge übereinstimmt, ist dies ein No-Op. Andernfalls wird eine 4-Byte-Auslagerungsoperation ausgeführt.

socket.ntohs(x)

Konvertieren Sie positive 16-Bit-Ganzzahlen vom Netzwerk in die Host-Bytereihenfolge. Auf Computern, auf denen die Host-Bytereihenfolge mit der Netzwerkbyte-Reihenfolge übereinstimmt, ist dies ein No-Op. Andernfalls wird eine 2-Byte-Auslagerungsoperation ausgeführt.

socket.htonl(x)

Konvertieren Sie positive 32-Bit-Ganzzahlen vom Host in die Netzwerkbyte-Reihenfolge. Auf Computern, auf denen die Host-Bytereihenfolge mit der Netzwerkbyte-Reihenfolge übereinstimmt, ist dies ein No-Op. Andernfalls wird eine 4-Byte-Auslagerungsoperation ausgeführt.

socket.htons(x)

Konvertieren Sie positive 16-Bit-Ganzzahlen vom Host in die Netzwerkbyte-Reihenfolge. Auf Computern, auf denen die Host-Bytereihenfolge mit der Netzwerkbyte-Reihenfolge übereinstimmt, ist dies ein No-Op. Andernfalls wird eine 2-Byte-Auslagerungsoperation ausgeführt.

Sie können auchstruct module verwenden, um Binärdaten mithilfe von Formatzeichenfolgen zu packen und zu entpacken:

import struct
network_byteorder_int = struct.pack('>H', 256)
python_int = struct.unpack('>H', network_byteorder_int)[0]

Fazit

Wir haben in diesem Tutorial viel behandelt. Networking und Sockets sind große Themen. Wenn Sie noch keine Erfahrung mit Netzwerken oder Sockets haben, lassen Sie sich nicht von allen Begriffen und Akronymen entmutigen.

Es gibt viele Teile, mit denen man sich vertraut machen muss, um zu verstehen, wie alles zusammenarbeitet. Genau wie bei Python wird es jedoch sinnvoller, wenn Sie die einzelnen Teile kennenlernen und mehr Zeit mit ihnen verbringen.

Wir haben uns die Low-Level-Socket-API imsocket-Modul von Python angesehen und herausgefunden, wie sie zum Erstellen von Client-Server-Anwendungen verwendet werden kann. Wir haben auch unsere eigene benutzerdefinierte Klasse erstellt und sie als Protokoll auf Anwendungsebene zum Austausch von Nachrichten und Daten zwischen Endpunkten verwendet. Sie können diese Klasse verwenden und darauf aufbauen, um zu lernen und das Erstellen eigener Socket-Anwendungen einfacher und schneller zu gestalten.

Sie finden diesource code on GitHub.

Herzlichen Glückwunsch zum Ende! Sie sind jetzt auf dem besten Weg, Sockets in Ihren eigenen Anwendungen zu verwenden.

Ich hoffe, dieses Tutorial hat Ihnen die Informationen, Beispiele und Inspirationen gegeben, die Sie benötigen, um Ihre Entwicklungsreise für Sockets zu beginnen.