HOWTO zur Socket-Programmierung
*******************************


Zusammenfassung
^^^^^^^^^^^^^^^

Sockets werden fast überall eingesetzt, gehören aber zu den am
schwersten missverstandenen Techniken überhaupt. Dies ist ein
Überblick über Sockets aus 3000 Metern Höhe. Es ist nicht wirklich ein
Tutorial – du wirst noch Arbeit vor dir haben, um etwas zum Laufen zu
bringen. Es behandelt nicht die Feinheiten (und davon gibt es viele),
aber ich hoffe, es gibt dir genug Hintergrund, um anständig mit ihnen
anzufangen.


Sockets
=======

Ich werde nur über INET-Sockets (also IPv4) sprechen, aber die machen
mindestens 99 % der verwendeten Sockets aus. Und ich werde nur über
STREAM-Sockets (also TCP) sprechen – wenn du nicht wirklich weißt, was
du tust (dann ist dieses HOWTO nichts für dich!), bekommst du mit
einem STREAM-Socket besseres Verhalten und besseren Durchsatz als mit
allem anderen. Ich versuche, das Geheimnis zu lüften, was ein Socket
eigentlich ist, und gebe ein paar Hinweise zum Umgang mit
blockierenden und nicht blockierenden Sockets. Anfangen werde ich aber
mit blockierenden Sockets. Du musst wissen, wie die funktionieren,
bevor du dich mit nicht blockierenden befasst.

Ein Teil der Schwierigkeit beim Verständnis dieser Dinge liegt daran,
dass „Socket“ je nach Zusammenhang eine ganze Reihe feiner
Unterschiede bedeuten kann. Unterscheiden wir also zuerst zwischen
einem „Client“-Socket – einem Endpunkt einer Unterhaltung – und einem
„Server“-Socket, der mehr einer Telefonvermittlung gleicht. Die
Clientanwendung (zum Beispiel dein Browser) verwendet ausschließlich
„Client“-Sockets. Der Webserver, mit dem sie redet, verwendet sowohl
„Server“- als auch „Client“-Sockets.


Geschichte
----------

Von den verschiedenen Formen der IPC (Inter Process Communication)
sind Sockets mit Abstand die beliebteste. Auf jeder einzelnen
Plattform gibt es wahrscheinlich andere, schnellere Formen von IPC,
aber für plattformübergreifende Kommunikation sind Sockets so ziemlich
die einzige Möglichkeit.

Erfunden wurden sie in Berkeley als Teil der BSD-Variante von Unix.
Mit dem Internet verbreiteten sie sich wie ein Lauffeuer. Und das mit
gutem Grund – die Verbindung von Sockets mit INET macht es unglaublich
leicht, mit beliebigen Rechnern auf der Welt zu reden (jedenfalls
verglichen mit anderen Ansätzen).


Einen Socket erzeugen
=====================

Grob gesagt hat dein Browser, als du auf den Link geklickt hast, der
dich auf diese Seite gebracht hat, etwa Folgendes getan:

   # create an INET, STREAMing socket
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   # now connect to the web server on port 80 - the normal http port
   s.connect(("www.python.org", 80))

Ist das "connect" abgeschlossen, lässt sich der Socket "s" verwenden,
um eine Anfrage nach dem Text der Seite zu senden. Derselbe Socket
liest die Antwort und wird dann zerstört. Richtig, zerstört. Client-
Sockets werden normalerweise nur für einen Austausch verwendet (oder
eine kleine Reihe aufeinanderfolgender Austausche).

Was im Webserver passiert, ist etwas komplizierter. Zuerst erzeugt der
Webserver einen „Server-Socket“:

   # create an INET, STREAMing socket
   serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   # bind the socket to a public host, and a well-known port
   serversocket.bind((socket.gethostname(), 80))
   # become a server socket
   serversocket.listen(5)

Ein paar Dinge fallen auf: Wir haben "socket.gethostname()" verwendet,
damit der Socket für die Außenwelt sichtbar ist. Hätten wir
"s.bind(('localhost', 80))" oder "s.bind(('127.0.0.1', 80))"
verwendet, hätten wir immer noch einen „Server“-Socket, aber einen,
der nur innerhalb desselben Rechners sichtbar ist. "s.bind(('', 80))"
legt fest, dass der Socket über jede Adresse erreichbar ist, die der
Rechner gerade hat.

Ein zweiter Punkt: Niedrige Portnummern sind üblicherweise für
„allgemein bekannte“ Dienste reserviert (HTTP, SNMP usw.). Wenn du nur
herumspielst, nimm eine schön hohe Nummer (vierstellig).

Und schließlich sagt das Argument von "listen" der Socket-Bibliothek,
dass sie bis zu 5 Verbindungsanfragen (das übliche Maximum) einreihen
soll, bevor sie Verbindungen von außen abweist. Ist der übrige Code
sauber geschrieben, sollte das reichlich sein.

Jetzt, da wir einen „Server“-Socket haben, der an Port 80 lauscht,
können wir in die Hauptschleife des Webservers eintreten:

   while True:
       # accept connections from outside
       (clientsocket, address) = serversocket.accept()
       # now do something with the clientsocket
       # in this case, we'll pretend this is a threaded server
       ct = make_client_thread(clientsocket)
       ct.start()

Tatsächlich gibt es 3 allgemeine Wege, wie diese Schleife arbeiten
könnte: einen Thread losschicken, der "clientsocket" bedient, einen
neuen Prozess für "clientsocket" erzeugen oder diese Anwendung so
umbauen, dass sie nicht blockierende Sockets verwendet und mit
"select" zwischen unserem „Server“-Socket und allen aktiven
"clientsocket"-Objekten umschaltet. Dazu später mehr. Wichtig zu
verstehen ist jetzt: Das ist *alles*, was ein „Server“-Socket tut. Er
sendet keine Daten. Er empfängt keine Daten. Er erzeugt nur
„Client“-Sockets. Jedes "clientsocket" entsteht als Reaktion darauf,
dass irgendein *anderer* „Client“-Socket ein "connect()" auf den Host
und Port macht, an die wir gebunden sind. Sobald wir dieses
"clientsocket" erzeugt haben, kehren wir zum Lauschen auf weitere
Verbindungen zurück. Die beiden „Clients“ können nach Herzenslust
plaudern – sie verwenden einen dynamisch zugewiesenen Port, der nach
dem Ende der Unterhaltung wiederverwendet wird.


IPC
---

Wenn du schnelle IPC zwischen zwei Prozessen auf einem Rechner
brauchst, solltest du dir Pipes oder gemeinsamen Speicher ansehen.
Entscheidest du dich doch für AF_INET-Sockets, binde den
„Server“-Socket an "'localhost'". Auf den meisten Plattformen nimmt
das eine Abkürzung um ein paar Schichten Netzwerkcode herum und ist
ein gutes Stück schneller.

Siehe auch:

  "multiprocessing" bündelt plattformübergreifende IPC in einer API
  auf höherer Ebene.


Einen Socket verwenden
======================

Als Erstes ist zu beachten, dass der „Client“-Socket des Webbrowsers
und der „Client“-Socket des Webservers identische Biester sind. Das
heißt, es ist eine Unterhaltung unter Gleichen. Oder anders gesagt:
*Du als Entwerfer musst entscheiden, welche Regeln des Umgangs für
eine Unterhaltung gelten*. Normalerweise beginnt der Socket, der das
"connect" macht, die Unterhaltung, indem er eine Anfrage oder
vielleicht eine Anmeldung schickt. Aber das ist eine
Entwurfsentscheidung – es ist keine Regel von Sockets.

Nun gibt es zwei Sätze von Verben für die Kommunikation. Du kannst
"send" und "recv" verwenden, oder du kannst deinen Client-Socket in
ein dateiartiges Biest verwandeln und "read" und "write" verwenden.
Letzteres ist der Weg, auf dem Java seine Sockets anbietet. Darüber
werde ich hier nicht sprechen, außer um dich zu warnen, dass du bei
Sockets "flush" verwenden musst. Das sind gepufferte „Dateien“, und
ein verbreiteter Fehler ist, etwas zu "write" und dann auf eine
Antwort zu "read". Ohne ein "flush" dazwischen wartest du
möglicherweise für immer auf die Antwort, weil die Anfrage noch in
deinem Ausgabepuffer stecken kann.

Jetzt kommen wir zum großen Stolperstein bei Sockets: "send" und
"recv" arbeiten auf den Netzwerkpuffern. Sie behandeln nicht
zwangsläufig alle Bytes, die du ihnen gibst (oder von ihnen
erwartest), weil ihr Hauptaugenmerk auf den Netzwerkpuffern liegt. Im
Allgemeinen kehren sie zurück, sobald die zugehörigen Netzwerkpuffer
gefüllt ("send") oder geleert ("recv") sind. Sie sagen dir dann, wie
viele Bytes sie behandelt haben. Es ist *deine* Verantwortung, sie
erneut aufzurufen, bis deine Nachricht vollständig abgearbeitet ist.

Gibt ein "recv" 0 Bytes zurück, bedeutet das, dass die andere Seite
die Verbindung geschlossen hat (oder gerade schließt). Du wirst auf
dieser Verbindung keine weiteren Daten empfangen. Nie mehr.
Möglicherweise kannst du noch erfolgreich Daten senden. Dazu sage ich
später mehr.

Ein Protokoll wie HTTP verwendet einen Socket für nur eine
Übertragung. Der Client sendet eine Anfrage und liest dann eine
Antwort. Das ist alles. Der Socket wird verworfen. Das heißt, dass ein
Client das Ende der Antwort daran erkennen kann, dass er 0 Bytes
empfängt.

Hast du aber vor, deinen Socket für weitere Übertragungen
wiederzuverwenden, musst du dir klarmachen, dass es *kein* EOT (End of
Transfer) *auf einem Socket gibt*. Ich wiederhole: Wenn ein "send"
oder "recv" auf einem Socket zurückkehrt, nachdem es 0 Bytes behandelt
hat, ist die Verbindung abgebrochen. Ist die Verbindung *nicht*
abgebrochen, kannst du auf einem "recv" für immer warten, weil der
Socket dir *nicht* sagt, dass es (vorläufig) nichts mehr zu lesen
gibt. Wenn du darüber ein wenig nachdenkst, kommst du auf eine
grundlegende Wahrheit über Sockets: *Nachrichten müssen entweder eine
feste Länge haben* (pfui), *oder abgegrenzt sein* (Schulterzucken),
*oder angeben, wie lang sie sind* (viel besser), *oder damit enden,
dass die Verbindung geschlossen wird*. Die Wahl ist ganz deine (aber
manche Wege sind richtiger als andere).

Wenn du die Verbindung nicht beenden willst, ist die einfachste Lösung
eine Nachricht mit fester Länge:

   class MySocket:
       """demonstration class only
         - coded for clarity, not efficiency
       """

       def __init__(self, sock=None):
           if sock is None:
               self.sock = socket.socket(
                               socket.AF_INET, socket.SOCK_STREAM)
           else:
               self.sock = sock

       def connect(self, host, port):
           self.sock.connect((host, port))

       def mysend(self, msg):
           totalsent = 0
           while totalsent < MSGLEN:
               sent = self.sock.send(msg[totalsent:])
               if sent == 0:
                   raise RuntimeError("socket connection broken")
               totalsent = totalsent + sent

       def myreceive(self):
           chunks = []
           bytes_recd = 0
           while bytes_recd < MSGLEN:
               chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
               if chunk == b'':
                   raise RuntimeError("socket connection broken")
               chunks.append(chunk)
               bytes_recd = bytes_recd + len(chunk)
           return b''.join(chunks)

Der Sendecode hier ist für fast jedes Nachrichtenschema brauchbar – in
Python sendest du Zeichenketten und kannst mit "len()" deren Länge
bestimmen (selbst wenn sie eingebettete "\0"-Zeichen enthält).
Komplizierter wird es vor allem beim Empfangscode. (Und in C ist es
nicht viel schlimmer, nur dass du "strlen" nicht verwenden kannst,
wenn die Nachricht eingebettete "\0" enthält.)

Die einfachste Verbesserung ist, das erste Zeichen der Nachricht zum
Anzeiger des Nachrichtentyps zu machen und die Länge vom Typ bestimmen
zu lassen. Nun hast du zwei "recv"-Aufrufe – den ersten, um
(mindestens) dieses erste Zeichen zu bekommen, damit du die Länge
nachschlagen kannst, und den zweiten in einer Schleife, um den Rest zu
holen. Entscheidest du dich für den Weg über Trennzeichen, empfängst
du in einer beliebigen Blockgröße (4096 oder 8192 passt häufig gut zu
den Größen der Netzwerkpuffer) und durchsuchst das Empfangene nach
einem Trennzeichen.

Eine Komplikation, die du kennen solltest: Wenn dein
Unterhaltungsprotokoll erlaubt, mehrere Nachrichten unmittelbar
hintereinander zu senden (ohne irgendeine Antwort), und du "recv" eine
beliebige Blockgröße übergibst, liest du möglicherweise den Anfang
einer folgenden Nachricht mit. Den musst du beiseitelegen und
festhalten, bis er gebraucht wird.

Der Nachricht ihre Länge voranzustellen (etwa als 5 Ziffernzeichen)
wird komplizierter, weil du (ob du es glaubst oder nicht)
möglicherweise nicht alle 5 Zeichen in einem "recv" bekommst. Beim
Herumspielen kommst du damit durch. Bei hoher Netzlast bricht dein
Code aber sehr schnell, wenn du nicht zwei "recv"-Schleifen verwendest
– die erste, um die Länge zu bestimmen, die zweite, um den Datenteil
der Nachricht zu holen. Unschön. Dabei wirst du auch entdecken, dass
"send" nicht immer schafft, alles in einem Durchgang loszuwerden. Und
obwohl du das hier gelesen hast, wird es dich irgendwann doch
erwischen!

Im Interesse des Platzes, deiner Charakterbildung (und der Erhaltung
meines Wettbewerbsvorteils) bleiben diese Verbesserungen dem Leser als
Übung überlassen. Gehen wir weiter zum Aufräumen.


Binärdaten
----------

Es ist durchaus möglich, Binärdaten über einen Socket zu senden. Das
Hauptproblem ist, dass nicht alle Rechner dieselben Formate für
Binärdaten verwenden. Die Netzwerk-Bytereihenfolge ist zum Beispiel
Big-Endian, mit dem höchstwertigen Byte zuerst. Eine 16-Bit-Ganzzahl
mit dem Wert "1" wären also die zwei Hex-Bytes "00 01". Die meisten
verbreiteten Prozessoren (x86/AMD64, ARM, RISC-V) sind jedoch Little-
Endian, mit dem niederwertigsten Byte zuerst – dieselbe "1" wäre dort
"01 00".

Socket-Bibliotheken haben Aufrufe, um 16- und 32-Bit-Ganzzahlen
umzuwandeln: "ntohl, htonl, ntohs, htons", wobei „n“ für *network*,
„h“ für *host*, „s“ für *short* und „l“ für *long* steht. Wo die
Netzwerkreihenfolge gleich der Rechnerreihenfolge ist, tun sie nichts,
wo der Rechner die Bytes umgekehrt ablegt, tauschen sie sie passend
um.

In diesen Zeiten der 64-Bit-Rechner ist die ASCII-Darstellung von
Binärdaten häufig kleiner als die binäre Darstellung. Das liegt daran,
dass die meisten Ganzzahlen überraschend oft den Wert 0 oder
vielleicht 1 haben. Die Zeichenkette ""0"" wären zwei Bytes, während
eine vollständige 64-Bit-Ganzzahl 8 belegt. Das passt natürlich nicht
gut zu Nachrichten mit fester Länge. Entscheidungen, Entscheidungen.


Verbindung trennen
==================

Streng genommen sollst du auf einem Socket "shutdown" verwenden, bevor
du ihn mit "close" schließt. Das "shutdown" ist ein Hinweis an den
Socket am anderen Ende. Je nach übergebenem Argument kann es bedeuten
„Ich sende nichts mehr, aber ich lausche weiter“ oder „Ich lausche
nicht mehr, auf Nimmerwiedersehen!“. Die meisten Socket-Bibliotheken
sind allerdings so daran gewöhnt, dass Programmierer dieses Stück
Höflichkeit auslassen, dass ein "close" normalerweise dasselbe ist wie
"shutdown(); close()". In den meisten Fällen ist ein ausdrückliches
"shutdown" also nicht nötig.

Ein Weg, "shutdown" sinnvoll einzusetzen, ist ein HTTP-artiger
Austausch. Der Client sendet eine Anfrage und macht dann ein
"shutdown(1)". Das sagt dem Server: „Dieser Client ist mit dem Senden
fertig, kann aber noch empfangen.“ Der Server kann das „EOF“ daran
erkennen, dass er 0 Bytes empfängt. Er kann davon ausgehen, dass er
die vollständige Anfrage hat. Der Server sendet eine Antwort. Ist das
"send" erfolgreich abgeschlossen, hat der Client tatsächlich noch
empfangen.

Python geht beim automatischen Herunterfahren noch einen Schritt
weiter und sagt: Wird ein Socket von der Speicherbereinigung
eingesammelt, macht er automatisch ein "close", wenn eines nötig ist.
Sich darauf zu verlassen, ist aber eine ganz schlechte Angewohnheit.
Verschwindet dein Socket einfach, ohne ein "close" zu machen, hängt
der Socket am anderen Ende möglicherweise unbegrenzt und denkt, du
seist nur langsam. *Bitte* schließe deine Sockets mit "close", wenn du
fertig bist.


Wenn Sockets sterben
--------------------

Das Schlimmste an blockierenden Sockets ist wahrscheinlich, was
passiert, wenn die andere Seite hart abstürzt (ohne ein "close" zu
machen). Dein Socket hängt dann wahrscheinlich. TCP ist ein
zuverlässiges Protokoll und wartet sehr, sehr lange, bevor es eine
Verbindung aufgibt. Wenn du Threads verwendest, ist der ganze Thread
im Grunde tot. Dagegen kannst du nicht viel tun. Solange du nichts
Dummes tust, etwa eine Sperre halten, während du blockierend liest,
verbraucht der Thread eigentlich nicht viele Ressourcen. Versuche
*nicht*, den Thread abzuschießen – Threads sind unter anderem deshalb
effizienter als Prozesse, weil sie den Aufwand der automatischen
Rückgewinnung von Ressourcen vermeiden. Anders gesagt: Wenn es dir
gelingt, den Thread abzuschießen, ist wahrscheinlich dein ganzer
Prozess im Eimer.


Nicht blockierende Sockets
==========================

Wenn du das Vorangegangene verstanden hast, weißt du schon das meiste
von dem, was du über die Mechanik der Socket-Nutzung wissen musst. Du
verwendest weiterhin dieselben Aufrufe, auf weitgehend dieselbe Weise.
Nur dass deine Anwendung, wenn du es richtig machst, fast von innen
nach außen gekehrt ist.

In Python machst du einen Socket mit "socket.setblocking(False)" nicht
blockierend. In C ist das komplizierter (zum einen musst du dich
zwischen der BSD-Variante "O_NONBLOCK" und der kaum unterscheidbaren
POSIX-Variante "O_NDELAY" entscheiden, die wiederum etwas völlig
anderes ist als "TCP_NODELAY"), aber die Idee ist genau dieselbe. Du
machst das nach dem Erzeugen des Sockets, aber vor seiner Verwendung.
(Wenn du verrückt bist, kannst du auch hin und her schalten.)

Der wesentliche mechanische Unterschied ist, dass "send", "recv",
"connect" und "accept" zurückkehren können, ohne irgendetwas getan zu
haben. Du hast (natürlich) eine Reihe von Möglichkeiten. Du kannst
Rückgabewerte und Fehlercodes prüfen und dich damit allgemein in den
Wahnsinn treiben. Wenn du mir nicht glaubst, probiere es irgendwann
aus. Deine Anwendung wird groß, fehlerhaft und frisst CPU. Lassen wir
also die hirntoten Lösungen und machen es richtig.

Verwende "select".

In C ist "select" zu programmieren recht kompliziert. In Python ist es
ein Kinderspiel, aber nah genug an der C-Fassung, dass du kaum Mühe
damit hast, wenn du "select" in Python verstanden hast:

   ready_to_read, ready_to_write, in_error = \
                  select.select(
                     potential_readers,
                     potential_writers,
                     potential_errs,
                     timeout)

Du übergibst "select" drei Listen: Die erste enthält alle Sockets, von
denen du vielleicht lesen willst, die zweite alle Sockets, auf die du
vielleicht schreiben willst, und die letzte (normalerweise leer
gelassen) die, die du auf Fehler prüfen willst. Beachte, dass ein
Socket in mehr als einer Liste stehen kann. Der Aufruf von "select"
blockiert, aber du kannst ihm eine Zeitbeschränkung mitgeben. Das ist
im Allgemeinen vernünftig – gib ihm eine schön lange Zeitbeschränkung
(sagen wir eine Minute), sofern du keinen guten Grund für etwas
anderes hast.

Zurück bekommst du drei Listen. Sie enthalten die Sockets, die
tatsächlich lesbar und schreibbar sind oder einen Fehler haben. Jede
dieser Listen ist eine (möglicherweise leere) Teilmenge der
entsprechenden Liste, die du übergeben hast.

Steht ein Socket in der Ausgabeliste der lesbaren Sockets, kannst du
so sicher sein, wie man es in diesem Geschäft überhaupt sein kann,
dass ein "recv" auf diesem Socket *irgendetwas* zurückgibt. Dasselbe
gilt für die Liste der schreibbaren Sockets. Du wirst *irgendetwas*
senden können. Vielleicht nicht alles, was du willst, aber
*irgendetwas* ist besser als nichts. (Tatsächlich meldet sich jeder
halbwegs gesunde Socket als schreibbar – es heißt nur, dass im
ausgehenden Netzwerkpuffer Platz ist.)

Wenn du einen „Server“-Socket hast, stecke ihn in die Liste
potential_readers. Kommt er in der Liste der lesbaren Sockets heraus,
wird dein "accept" (so gut wie sicher) funktionieren. Hast du einen
neuen Socket erzeugt, um dich per "connect" mit jemandem zu verbinden,
stecke ihn in die Liste potential_writers. Erscheint er in der Liste
der schreibbaren Sockets, hast du gute Aussichten, dass er verbunden
ist.

Tatsächlich kann "select" auch bei blockierenden Sockets praktisch
sein. Es ist ein Weg, festzustellen, ob du blockieren wirst – der
Socket meldet sich als lesbar, wenn etwas in den Puffern liegt.
Allerdings hilft das immer noch nicht bei dem Problem, festzustellen,
ob die andere Seite fertig oder nur mit etwas anderem beschäftigt ist.

**Achtung, Portabilität**: Unter Unix arbeitet "select" sowohl mit
Sockets als auch mit Dateien. Versuche das nicht unter Windows. Unter
Windows arbeitet "select" nur mit Sockets. Beachte außerdem, dass in C
viele der fortgeschritteneren Socket-Optionen unter Windows anders
gehandhabt werden. Tatsächlich verwende ich unter Windows mit meinen
Sockets üblicherweise Threads (die sehr, sehr gut funktionieren).
