Warning: Table './usr_web1030_3/variable' is marked as crashed and should be repaired query: SELECT * FROM variable in /var/www/web1030/html/mkernel.de/includes/database.mysql.inc on line 128

Warning: Cannot modify header information - headers already sent by (output started at /var/www/web1030/html/mkernel.de/includes/database.mysql.inc:128) in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 726

Warning: Cannot modify header information - headers already sent by (output started at /var/www/web1030/html/mkernel.de/includes/database.mysql.inc:128) in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 727

Warning: Cannot modify header information - headers already sent by (output started at /var/www/web1030/html/mkernel.de/includes/database.mysql.inc:128) in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 728

Warning: Cannot modify header information - headers already sent by (output started at /var/www/web1030/html/mkernel.de/includes/database.mysql.inc:128) in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 729
[Coder's Concept] Networking | Drupal

[Coder's Concept] Networking

  • warning: Cannot modify header information - headers already sent by (output started at /var/www/web1030/html/mkernel.de/includes/database.mysql.inc:128) in /var/www/web1030/html/mkernel.de/includes/common.inc on line 148.
  • user warning: Table './usr_web1030_3/variable' is marked as crashed and should be repaired query: UPDATE variable SET value = 'a:17:{i:0;i:62;i:1;i:61;i:2;i:59;i:3;i:31;i:4;i:30;i:5;i:29;i:6;i:24;i:7;i:21;i:8;i:15;i:9;i:14;i:10;i:11;i:11;i:7;i:12;i:6;i:13;i:5;i:14;i:3;i:15;i:2;i:16;i:1;}' WHERE name = 'menu_masks' in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 609.
  • user warning: Table './usr_web1030_3/variable' is marked as crashed and should be repaired query: UPDATE variable SET value = 'a:0:{}' WHERE name = 'menu_expanded' in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 609.
  • user warning: Table './usr_web1030_3/variable' is marked as crashed and should be repaired query: DELETE FROM variable WHERE name = 'menu_rebuild_needed' in /var/www/web1030/html/mkernel.de/includes/bootstrap.inc on line 634.

In der modernen IT-Welt ist nichts wichtiger geworden als das Netz. Das Grundwissen zum Thema Netzwerkkommunikation ist jedoch inzwischen gefährdet. Ich möchte daher einmal genau aufzeigen, worauf alle Kommunikationsabstraktionen zurückzuführen sind und auf welche Fehler bei deren Anwendung man stossen kann. Back to Basics quasi.

Zunächst einmal: Das Thema Hardware berücksichtige ich nicht. Ich gehe in den Situationsbeschreibungen davon aus, das die kommunizierenden Rechner beide eine eindeutige IP erhalten haben und auch dazu in der Lage sind, sich gegenseitig zu erreichen (per ping, z.B.). Dabei sollte schon eins klar werden: Gateways sind für Applikationen, die Netzwerkkommunikation machen, transparent. Proxies nicht.

Ursprung der Netzwerkporgrammierung

Also, zunächst einmal einen kleinen Ausflug in Geschichte: Netze entstanden lange bevor der PC mit Windows auf dem Markt war. Ursprünglich entstand der Bedarf von Netzwerkkommunikation auf Unix-Systemen (oder etwas, das einem Unix sehr ähnlich war). Da unter Unix alles eine Datei ist (Pipes sind Dateien, der Monitor im Textmodus ist eine write-only Datei, die Tastatur eine read-only Datei), wurde auch die API für Netzwerkkommunikation so implementiert, das eine Verbindung zwischen zwei Netzteilnehmern eine Datei ist. Das wiederum bedeutet, das eine Übertragung als Datenstrom von Bytes organisiert ist und nicht als Datenstrom von Paketen.

Es entstand eine API, die als BSD Sockets bekannt ist. Dahinter verbirgt sich eine Sammlung von Funktionen und Strukturen, die primär den Verbindungsaufbau und -abbau organisieren. Zusätzlich wurden noch die Funktionen zum senden und empfangen bereitgestellt, wobei dies lediglich eine Verschönerung der Errorcodes bedeutet, denn Dateien kann man ja auch ohne send- und receive- Kommandos lesen (so auch sockets).

Ein socket (unter Unix realisiert als eine offene Datei) stellt eine Aktive Verbindung dar. Wobei man zwei Sockettypen unterscheiden muss: Server-sockets und Verbindungssockets.

Server- und Clientsockets

Ein Server-socket wird an eine (oder mehrere) IP und an einen Port gebunden und ermöglicht es, eingehende Verbindungen zu akzeptieren. Eine Applikation, die auf einem Rechner läuft, ist also anhand der IP des Rechners und ihrem Port eindeutig identifizierbar. Jeder Port kann für jedes Protokoll nur ein mal belegt werden. Serversockets dienen als Brutstätte für Clientsockets und können daher nicht gelesen und geschrieben werden. Stattdessen wird eine Funktion bereitgestellt, die neue Sockets erzeugt, wenn sich ein Clientsocket verbindet.

Netzwerkprotokolle gibt es eine ganze Reihe, wobei die bekanntesten wohl UDP und TCP sind. Wichtig zu Ports ist noch zu wissen, das die Ports unterhalb von 1024 für das System und den Administrator reserviert sind. Da es sich jedoch dabei um einen Int32 handelt, dürfte das bei 65536 Ports wohl nicht sonderlich weh tun.

Doch nun zu einem Beispiel:
Angenommen, Joe möchte eine Serverapplikation programmieren, die eine Liste von Meldungen puffert und eine Clientapplikation, die diese Liste verwalten (anzeigen, hinzufügen, löschen) kann.

Zunächst muss also die Serverapplikation einen Port an sich "binden", um für alle anderen Applikationen im Netz erreichbar zu sein:

socket s=socket(protokoll);
bind(s,ip,port);
listen(s,backlog);

Das sind (abstrakt) die dafür notwendigen Funktionsaufrufe. Zunächst muss ein socket erzeugt werden. Anschließend wird der socket an einen Port und eine IP gebunden, d.H. eingehende Anfragen an den Host, die diesen Port adressieren, werden dem socket zugeordnet. Das Binden alleine reicht jedoch nicht aus. Damit das System eingehende Verbindungen verarbeitet, muss der Socket noch in den zuhörenden Modus versetzt werden. Backlog gibt dabei an, wie viele Verbindungsanfragen das System des Hosts puffern soll.

Die Clientseite sieht wiefolgt aus:

socket s = socket(protokoll);
connect(s,ip,port);

connect sorgt dafür, das eine Verbindung um Zielhost aufgebaut wird. Der Zielhost wird diesen Verbindungseingang jedoch nur puffern. Die Applikation, die den socket gebunden hat, muss das System des hosts danach fragen:

socket clientsock=accept(s);

Wird dabei etwas klar? Während der Client nur einen Socket erzeugt und diesen mit dem Host verbindet, wirkt der gebundene Socket des Servers als "Socket-Fabrik". Serverseitig muss also immer eine Menge von Sockets verwaltet werden.

Anschließend können Sockets mit:

sent=send(s,buf,len);

und

received=recv(s,buf,len);

verwendet werden. Doch genau hier kann die API eine trügerische Annahme erzeugen:
Da es allgemein bekannt ist, das Netzwerkdatenverkehr paketorientiert ist, entsteht oft die Vermutung und die daraus resultierende (fehlerhafte) Programmierung, das zu jedem send() ein recv() gehört, da man mit send() ein paket sendet und mit recv() eines empfängt. Weit gefehlt!

Sockets != Nachrichtenbasierte Kommunikation

Zunächst mal, warum sollte jedes send() ein einzelnes Paket erzeugen? Immerhin kann man beliebig große Puffer übergeben. Und vor allem, wie soll das System reagieren, wenn mit einem send() mehr Daten gesendet wurden als das recv() an puffergröße beim Aufruf erhalten hat?

Um es klar zu stellen: die Kommunikation ist asynchron und nicht symmetrisch. Oder anders: Was mit einem send() gesendet wurde, kann mit mehreren recv()-Aufrufen verarbeitet werden und anders herum. Es handelt sich (logisch) um eine Datei, und in der wird ja auch nicht gelesen wie geschrieben.

Ein großes Problem bei der Programmierung von Netzwerkkommunikation sind Fehler. Da jederzeit die Verbindung abreißen kann (Netzwerkkabel herausgezogen, IP geändert, 24-Stunden-Trennung, Hardwarefehler, Stromausfälle bei den Übertragungsmedien, etc...), kann jeder Funktionsaufruf fehlschlagen unabhängig vom Zustand der Applikation. Es müssen in allen Teilen der Programme, die Netzwerkübertragungen durchführen, saubere Fehlerabfangsroutinen implementiert werden, um die Stabilität des Programms aufrecht zu erhalten. Es ist immens wichtig, sämtliche Netzwerkfehler zu verarbeiten.

The Internet, that's where all the CPUs live

Ein anderes, aus meiner Sicht noch größeres Problem (Netzwerkfehler kann man recht leicht in den Griff bekommen, man muss ja nur auf die Rückgabewerte aller Funktionen acht geben), ist die Kommunikation und die Implementierung eines Übertragungsprotokolles selbst. Da die Infrastruktur nicht anbietet, festzustellen, was vom Sender als ein Datenpaket gemeint ist, muss bei dem Entwurf eines Protokolls darauf geachtet werden, das es immer möglich ist, das Ende eines Pakets im Datenstrom zu erkennen, und somit auch den Anfang eines Pakets. Ausserdem muss man mit etwas anderem kämpfen: Die Übertragung von Daten ohne Konvertierung ist nämlich nicht gegeben. Das Problem rührt daher, das Prozessoren unterschiedliche Byte-Orderings haben. Und da nicht bekannt ist, welchen Prozessortyp der Empfänger hat, muss man Vorkehrungen treffen. Es wurde daher einmal eine "Network Byte-Order" definiert, also die Byte-Order der Daten, die übertragen werden. Jedes System liefert dazu Hilfsfunktionen an, die die Daten konvertieren:
hton()
ntoh
()

Deshalb wird vielleicht klar, warum ftp,http,smtp,pop und imap als Klartext-ASCII-Protokolle definiert wurden: ASCII ist platformunabhängig. Allerdings verbraucht eine Zahl in ASCII mehr Speicher im Datenstrom als eine Zahl in ihrer binären Form. Daher sind ASCII-basierte Protokolle bei performanzlastigen Situationen nicht empfehlenswert.

Blocking / Nonblocking Operations

Nun zum letzten Problem bei der Implementierung von netzwerkenden Programmen: Blocking vs. Nonblocking.
Standardmäßig sind alle Netzwerkoperationen blockierend, d.H. das das System die Applikation schlafen legt, bis die Aufforderung vollständig abgearbeitet werden kann. ein accept() legt also so lange lahm, bis wenigstens eine eingehende Verbindungsanfrage vorliegt. recv() so lange, bis Daten in der Größe vorliegen, wie sie verlangt wurden und send() so lange, bis alle angeforderten Daten gesendet werden konnten. Netzwerkprogrammierer kommen also nicht darum herum, Multithreading-Know-How aufzubauen.

Selbst bei Multithreading ist das blockierungsproblem nicht weg: Threads können erst (nach aktuellen Konventionen zumindest... quick and dirty geht ja immer) beendet werden, wenn sie die blockierende Funktion verlassen und sich selbst beenden. Die Antwort darauf lautet: Blockierung deaktivieren.
Dabei entsteht aber ein ganz anderes Problem: wenn die Blockierung deaktiviert wird, muss man die CPU entlasten, weil eine while-Schleife in der permanent accept()ed wird, 100 Prozent CPU-Last erzeugt. Ausserdem liefern recv() nicht immer alle Daten zurück, sondern nur die, die sich beim Aufruf bereits in den lokalen Puffern befanden. Send() sendet möglicherweise nur einen Teil der angeforderten Daten, und der Rest muss daher erneut gesendet werden.

Fazit

Ich fasse zusammen:

  • Netzwerkprogrammierung erzeugt mehr if()-Statements
  • Netzwerkprogrammierung erzeugt mehr Threads
  • Netzwerkprogrammierung bei aktiviertem Blocking erzeugt träge reagierende Threads.
  • Netzwerkprogrammierung bei deaktiviertem Blocking erzeugt mehr while() und mehr sleep()

Ach ja, wer denkt, er kann ja mit den Standard-Timeouts leben, die das System vorgibt: so ein send() kann bei einer gezogenen Netzwerkverbindung auch schon mal 20 Minuten blockieren, bis der Verbindungsfehler gemeldet wird. Bei recv() ist es ähnlich.

Ich hoffe, es ist deutlich geworden, das diese Schicht 0 der Netzwerkprogrammierung (Schicht 0, weil es darunter nix gibt, es ist schon das rudimentärste, was man verwenden kann) recht komplex in der Verwendung ist. Selbst kleine Funktionalitäten können sehr viel Quellcode produzieren. Daher ist es bei den modernen Plattformen empfehlenswert, eine Abstraktionsschicht höher (RPC, .NET Remoting, Win32 Pipes, etc...) einzusteigen. Dennoch darf man nie vergessen, worauf alle Applikationen beruhen, um zu vergessen, wo gewisse Verhaltensweisen herkommen. Daher ja auch dieser Post. Netzwerkprogrammierung gehört halt doch irgendwie zu den häßlichen Dingen des Programmierens.

In diesem Sinne,
Happy Coding!