Socket Programmierung in C

with 4 Kommentare

1 Intro

Hey, Du kriegst das mit der Socketprogrammierung einfach nicht hin? Dieses Zeug ist einfach ein bisschen zu schwierig in den ganzen Manuals? Aber Du möchtest gerne die Netzwerkprogrammierung verstehen, ohne Dich durch zig structs zu bewegen, nur um herauszufinden, ob Du jetzt bind() vor connect() aufrufen musst etc. etc.

Du hast Glück, ich habe mich mit dem ganzen Kram auseinander gesetzt und freue mich Dir zu helfen! Hier bist Du richtig. Dieses Dokument sollte Dir, als halbwegs kompetenter C Programmierer dabei helfen Netzwerkprogrammierung zu verstehen.

Und ich habe es jetzt auch geschafft mit der Zeit zu gehen und habe den Guide auf IPv6 geupdatet. Ich hoffe Du wirst Deine Freude haben.

1.1 Für wen ist das hier?

Dieses Dokument wurde als Tutorial geschrieben, nicht als komplette Referenz. Gerade für Leute, die gerade erst mit der Socket Programmierung anfangen und sich noch nicht wirklich zurecht finden sollte dieser Guide hier nützlich sein.

Und hoffentlich reicht das hier erworbene Wissen aus, damit Du dann auch endlich diese verdammten Manualseiten verstehst.

1.2 Plattform und Compiler

Der hier verwendete Code wurde auf einem Linux PC mit dem gcc (GNU C Compiler) kompiliert. Es sollte jedoch auf jeder Plattform, wo der gcc verwendet werden kann funktionieren. Dies gilt natürlich nicht für Windows, für weitere Informationen kannst Du hier mal nachsehen.

1.3 Offizielle Homepage und Bücher zum Verkauf

Die offizielle Version dieses Dokuments ist http://beej.us/guide/bgnet/ . Dort wirst Du dann auch den Beispielcode finden und dort gibt es auch Links zu anderen Sprachversionen.
Um etwas in der Hand zu halten, kannst Du dort auch Bücher erwerben. Der Autor freut sich bestimmt über Deinen Beitrag. Es sei angemerkt, dass dieses Buch in Englisch verfasst ist.

1.4 Notiz an Solaris/SunOS Programmierer

Wenn Du auf Solaris oder SunOS Betriebssystemen kompilierst, musst Du ein paar Optionen beim Kompilieren beachten, damit die Bibliotheken (Libraries) auch korrekt gelinkt werden. Du machst das, indem Du folgende sog. Switches am Ende des Compile-Befehls hinzufügst „-lnsl -lsocket -lresolv“ . Das Ganze sieht dann wie folgt aus:

$ cc -o server server.c -lnsl -lsocket -lresolv

Wenn Du immer noch Fehler bekommen solltest, kannst Du zusätzlich noch „-lxnet“ an das Ende anhängen. Ich weiß zwar nicht genau was das macht, aber einige Leute brauchten das wohl, um alles ordentlich ans Laufen zu bringen
Es kann auch sein, dass Du Probleme beim Aufruf von setsockopt() bekommst. Das liegt an dem Unterschied zu Linux. Das kannst Du durch ein einfaches Austauschen der folgenden Zeile fixen. Anstelle von

int yes=1;

schreibst Du nun:

char yes='1';

Da ich kein Sun System habe, konnte ich keine der oben genannten Informationen testen, es sind lediglich die Informationen, die mir verschiedene Personen per Mail mitgeteilt haben.

1.5 Notiz für Windows Programmierer

An diesem Punkt habe ich eigentlich immer an Windows herumgenörgelt, was aber damit zusammen hängt, dass ich Windows nicht wirklich mag. Aber ich sollte fair bleiben und Dir sagen, dass Windows auf so vielen System installiert ist, dass man es doch als ordentliches Betriebssystem bezeichnen kann.

Ich kann sagen, seitdem ich Windows seit mehr als einem Jahrzehnt nicht benutzt habe, bin ich wesentlich glücklicher. Ich kann mich zurück lehnen und sagen: „Okay, dann benutz halt Windows“. Aber wenn ich ehrlich bin, ärgert es mich dann doch irgendwie so etwas zu sagen.

So kann ich Dir nur dazu raten Linux, BSD, oder ein anderes Unix basiertes System zu benutzen.

Aber Menschen mögen das, was sie mögen und so wird es Euch Windows Programmierer hoffentlich freuen, dass viele der hier genannten Informationen auch auf Windows übertragbar sind, jedoch mit kleineren Änderungen, wenn überhaupt.

Eine tolle Sache ist, dass ihr Cygwin installieren könnt, welches eine Kollektion an Unix Werkzeugen für Windows bereit stellt. Ich habe gehört, dass so alles auch ohne Veränderung bei Windows kompiliert werden kann.

Es soll ja aber die Menschen geben, die es auf einem ganz normalen Windows System laufen lassen wollen. Das ist scheiße. Rennt los, holt euch Unix. Sofort! Ne, quatsch – ich meine das natürlich nicht ernst, ich probiere ja eigentlich Windows gegenüber freundlich(er) zu sein.

So könnt ihr es hinbekommen (wenn ihr nicht Cygwinn benutzen wollt). Als erster solltet ihr so ziemlich alle System-Header-Dateien vergessen, die hier genannt werden. Alles was ihr einzufügen braucht ist folgendes:

#include <winsock.h>

Warte! Außerdem muss Du auch noch WSAStartup() aufrufen, bevor ihr irgendwas mit der socket Bibliothek machen könnt. Dies kann mit folgendem Code gemacht werden:

#include <winsock.h>
{
        WSADATA wsaData;   // if this doesn't work
        //WSAData wsaData; // then try this instead

        // MAKEWORD(1,1) for Winsock 1.1, MAKEWORD(2,0) for Winsock 2.0:

        if (WSAStartup(MAKEWORD(1,1), &wsaData) != 0) {
            fprintf(stderr, "WSAStartup failed.\n");
            exit(1);
        }

Du musst dem Compiler zusätzlich dazu auffordern die Winsock Bibliothek zu linken, normalerweise heißt die wsock32.lib, oder winsock32.lib, oder ws2_32.lib für Winsock 2.0. Bei VC++ kann dies über das Project Menü gemacht werden. Dort dann auf Settings, Link Tab klicken und nach einer Box mit dem Titel Object/library modules Ausschau halten. wsock32.lib hinzufügen (oder welche Bibliothek Du auch immer benötigst) und fertig.

Dann musst Du nur noch am ende Deiner Socket Bibltiothek WSACleanup() aufrufen. Du kannst online dazu nach Hilfe suchen, falls Du noch mehr Details benötigst.

Wenn Du das gemacht hast, sollte der Rest in diesem Tutorial auch auf Deinem Windows System laufen, mit einigen Ausnahmen. Beispielsweise kannst Du nicht close() aufrufen, wenn Du ein Socket schließen willst – Du benötigst closesocket(). Zudem funktioniert select() nur mit Socket-Deskriptoren, nicht mit Datei-Deskriptoren (wie etwa 0 für stdin).

Es gibt auch noch eine Socket Klasse, die Du benutzten kannst, Csocket. Checke Deine Compiler Manualseiten für weitere Informationen.

Zuletzt bleibt zu sagen, dass Windows kein fork() system call hat, welches ich in meinen Beispielen benutze. Vielleicht musst Du in eine POSIX Bibliothek linken, damit es funktioniert, oder Du kannst CreateProcess() anstelle von fork() benutzen. fork() braucht keine Argumente, CreateProcess() braucht 48 Milliarden Argumente. Wenn Du
darauf keine Lust hast, CreateThread() ist etwas einfach zu verarbeiten… Jedoch ist eine Diskussion rund um das Thema Multithreading außerhalb der Grenzen, die dieses Tutorial hier aufweisen soll.

1.6 Email

Ich bin generell dazu geneigt Fragen per E-Mail zu beantworten, also fühl Dich frei diese an mich zu senden. Jedoch kann ich nicht garantieren, dass ich diese beantworten werde. Wenn ich keine Zeit habe, werde ich Deine Frage in der Regel einfach löschen. Nimm das nicht persönlich.

Generell kann man sagen, je komplexer die Frage, desto geringer wird die Wahrscheinlichkeit, dass ich Dir antworte. Außerdem solltest Du alle wichtigen Informationen die es zur Lösung des Problems braucht bereitstellen (wie zum Beispiel Plattform, Compiler, Error Meldungen und alles andere, was mir helfen könnte das Problem zu lösen), somit ist die Wahrscheinlichkeit eine Antwort zu bekommen höher. Für eine weitere Hilfe verweise ich hier auf Eric Steven Raymond, der ein tolles Dokument geschrieben hat: How To Ask Questions The Smart Way.

Wenn Du keine Antwort erhältst, probiere noch etwas herum, versuche die Antwort zu finden. Falls das alles nichts nützt, dann schreibe mir noch mal mit all den Informationen die Du zusätzlich erhalten hast. Hoffentlich reicht das für mich, um Dir zu helfen.

Nachdem nun geklärt ist, wie Du mir (nicht) schreiben sollst, möchte ich mich für die ganze Unterstützung bedanken, für all die dankbaren Nutzer, die dieses Tutorial gepriesen haben. Es ist ein moralischer Boost und es erfreut mich, dass viele Menschen so viel Gutes hier raus ziehen. :-) Vielen Dank!

1.7 Mirroring

Ihr seid eingeladen diese Seite zu spiegeln, ob das nun öffentlich oder privat ist. Wenn Du diese Seite öffentlich spiegelst und möchtest, dass ich auf Dich linke, dann schreib mir eine kurze Nachricht an su.jeebnull@jeeb.

1.8 Notiz an Übersetzer

Wenn Du dieses Tutorial in eine andere Sprache übersetzen möchtest, schreib mich an (su.jeebnull@jeeb) und ich werde auf Deine Übersetzung verlinken. Fühl Dich frei und füge Deinen Namen und Deine Kontaktinformationen zu der Übersetzung.

Bitte notiere jedoch die Lizenzeinschränkungen, beschrieben in Copyright and Distribution, zu finden weiter unten.

Wenn Du möchtest, dass ich die Übersetzung hoste, dann frag mich einfach. Ich kann auch auf Dich verlinken, wenn Du sie hosten möchtest. Beides ist in Ordnung.

1.9 Copyright and Distribution (original)

Beej’s Guide to Network Programming is Copyright © 2012 Brian „Beej Jorgensen“ Hall.

With specific exceptions for source code and translations, below, this work is licensed under the Creative Commons Attribution- Noncommercial- No Derivative Works 3.0 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/3.0/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.

One specific exception to the „No Derivative Works“ portion of the license is as follows: this guide may be freely translated into any language, provided the translation is accurate, and the guide is reprinted in its entirety. The same license restrictions apply to the translation as to the original guide. The translation may also include the name and contact information for the translator.

The C source code presented in this document is hereby granted to the public domain, and is completely free of any license restriction.

Educators are freely encouraged to recommend or supply copies of this guide to their students.

Contact su.jeebnull@jeeb for more information.

2 Was ist ein Socket

Du hörst immer wieder von diesen „Sockets“ und vielleicht fragst Du Dich ja, was das genau sein soll. Vereinfacht gesagt: Ein Weg um mit anderen Programmen zu sprechen, indem Unix Datei-Deskriptoren verwendet werden.

Wie bitte?

Okay – vielleicht hast Du schon mal einen Unix Hacker sagen hören „Jeez, everything in Unix is a file!“ („Verdammt, in Unix ist doch Alles nur eine Datei!“). Was diese Person damit meint, heißt eigentlich nur, dass Unix Programme bei jeder Art von I/O (Ein- und Ausgabe), Datei-Deskriptoren lesen, bzw. schreiben. Ein Datei-Deskriptor ist einfach ein Integer, der mit einer geöffneten Datei assoziiert wird. Aber (und hier wird’s interessant), diese Datei kann auch eine Netzwerkverbindung, ein FIFO (First In First Out), eine Pipe, ein Terminal, eine lokale Datei, or so ziemlich alles andere sein. Alles in Unix ist eine Datei! Wenn Du also über das Internet mit einem anderen Programm kommunizieren möchtest, machst Du das über einen Datei-Deskriptor, das musst Du mir jetzt einfach mal glauben.

„Woher bekomme ich denn jetzt diesen Datei-Deskriptor für die Netzwerk Verbindung, Du Neun-Mal-Klug?“ Zugegeben, ich nehme mal an, dass Du Dir diese Frage nicht wirklich stellst, aber ich beantworte sie Dir trotzdem: Du rufst die socket() System Routine auf. Es wird ein Socket-Deskriptor zurückgegeben, und Du kannst durch ihn kommunizieren, indem Du spezialisierte Socket Aufrufe nutzt: send() und recv() (Für mehr Informationen: man send, man recv)

„Aber, Warte!“, möchtest Du jetzt vielleicht sagen. „Wenn das ein ganz gewöhnlicher Datei-Deskriptor ist, wieso kann ich dann nicht einfach das normale read() und write() benutzen, um durch das Socket zu kommunizieren?“ Die kurze Antwort ist, „Du kannst!“ Die längere Antwort ist „Du kannst, aber send() und recv() geben Dir viel mehr Kontrolle über die Datenübertragung.“

Und was kommt jetzt? Wie wäre es mit: Es gibt zig verschiedene Sockets. Es gibt DARPA Internet Adressen (Internet Sockets), Datei Namen auf einem Lokalen System (Unix Sockets), CCITT X.25 Adressen (X.25 Sockets, die Du getrost ignorieren kannst) und eventuell noch viele andere, je nachdem was für eine Unix Variante Du laufen hast. Dieses Tutorial befasst sich lediglich mit Ersterem: Internet Sockets.

2.1 Zwei Typen Internet Sockets

Was soll das denn jetzt? Es gibt zwei Internet Sockets? Ja, also nein. Ich meine, ich lüge. Es gibt noch mehr, aber ich wollte Dich nicht erschrecken. Ich werde mich auf die beiden Typen begrenzen. Okay, die sog. „Raw Sockets“ sind auch extrem funktional, aber das sprengt diese Einführung. Ich will Dich jedoch nicht aufhalten, diese mal nachzuschlagen.

Okay, was sind denn jetzt diese zwei Typen? Der eine nennt sich Stream Sockets; der andere heißt Datagram Sockets, welcher hier auch als SOCK_STREAM und SOCK_DGRAM bezeichnet werden. Datagram Sockets werden auch teilweise als verbindungslose Sockets bezeichnet. (Auch wenn sie mittels connect() verbunden werden können. Hier kannst Du connect() nachschlagen)

Stream Sockets sind gute Zwei-Weg-Verbindung Kommunikation-Streams. Wenn Du zwei Elemente in der Reihenfolge „1, 2“ in das Socket jagst, kommen diese auch in der Reihenfolge „1, 2“ am anderen Ende wieder raus. Sie sind zu zudem fehlerfrei. Ich bin mir so sicher, dass diese fehlerfrei sind, dass ich meine Finger in die Ohren stecke und La la la la schreie, wenn mich jemand vom Gegenteil überzeugen möchte.

Was benutzt denn Stream Sockets? Naja, vielleicht hast Du ja schonmal etwas von dem telnet Programm gehört? Alle Tasten die Du drückst, sollen ja auch genau in der Reihenfolge am anderen Ende rauskommen, oder? Außerdem benutzten die Browser das HTTP Protokoll, welches Stream Sockets benutzt, um Webseiten zu erhalten. Du kannst auch so weit gehen und eine telnet Verbindung mit einer Internetseite aufbauen, wenn Du dann GET / HTTP/1.0 eingibst und ENTER 2x drückst, bekommst Du das HTML ausgegeben.

Wie bekommen Stream Sockets diese hohe Qualität an Datenverbindungen hin? Sie benutzen ein Protokoll, welches als Transmission Control Protocol bezeichnet wird, auch bekannt als TCP (für extrem detaillierte Informationen zu TCP, guck mal im RFC 293 nach). TCP sorgt dafür, dass die Daten sequentiell und fehlerfrei ankommen. Eventuell hast Du ja von TCP nur in Begleitung seiner besseren Hälfte gehört, bekannt als TCP/IP, bei dem IP für Internet Protocol steht (siehe RFC 791). IP kümmert sich hauptsächlich um das Internet Routing und kümmert sich weniger um die Datensicherung.

Cool, aber was ist jetzt mit den Datagram Sockets? Wieso bezeichnet man diese als verbindungslos? Wieso kann man sich nicht auf diese verlassen? Hier sind ein paar Fakten: Wenn Du ein Datagram versendest, kann es ankommen. Es kann außerhalb der gesendeten Sortierung ankommen. Aber wenn es ankommt, dann sind die Daten innerhalb des Datagram fehlerfrei.

Datagram Sockets benutzten auch IP für’s Routing, setzten jedoch nicht auf TCP. Sie benutzen UDP (siehe RFC 768)

Und wieso sind diese jetzt verbindungslos? Hauptsächlich, weil Du keine Verbindung offen halten musst, wie bei den Stream Sockets. Du erstellst einfach ein Paket, packst einen IP Header drauf, mit den Zielinformationen, und schickst es los. Keine Verbindung benötigt. Sie werden meistens benutzt, wenn kein TCP Stack verfügbar ist, oder wenn ein paar fallen gelassene Pakete nichts ausmachen. Eine Beispielanwendung ist: tftp (Trivial File Transfer Protocol, der kleine Bruder von FTP), dhcpcd (ein DHCP Client), Multiplayer Spiele, Streaming von Audio und Video, Video-Konferenzen etc.

„Ey, warte mal! Werden tftp und dhcpc benutzt um Binäre Applikationen zu einem anderen Host zu schicken? Daten können also nicht verloren gehen, wenn Du erwartest, dass die Applikation nachher noch funktioniert. Was für eine Art von dunkler Magie passiert denn da, dass das möglich ist?

Das ist möglich, indem tftp und ähnliche Programme ein eigenes Protokoll über dem eigentlichen UDP Protokoll legen. So wird bei jedem über tftp verschickten Paket von der Gegenseite ein Paket abgeschickt, was mitteilt, dass das Paket auch wirklich angekommen ist (ein sog. ACK Paket). Wenn der Sender des ursprünglichen Pakets keine Antwort innerhalb von sagen wir 5 Sekunden erhalten hat, wird er das Paket einfach noch mal verschicken, bis er ein ACK erhält.  Diese Handhabung ist extrem wichtig, wenn man vernünftige SOCK_DGRAM Anwendungen entwickeln möchte.

Für Anwendungen, die nicht absolut genaue Pakete benötigen – wie beispielsweise Spiele, Lieder, oder Videos – ignoriert man einfach die verloren gegangenen Pakete, oder kompensiert das clever. (Quake Spieler kennen diesen Effekt unter dem Namen accursed lag. Mit accursed sind schlimme Beleidigungen gemeint, die hier nicht weiter ausgeführt werden).

Warum sollte man also ein so unzuverlässiges Protokoll verwenden? Dafür gibt es zwei Gründe: Geschwindigkeit und Geschwindigkeit. Es ist viel schneller zu feuern und dann zu vergessen, als im Auge zu behalten, dass die Sortierung nicht durcheinander kommt und so weiter. Wenn Du Chat Nachrichten verschickst, ist TCP super. Wenn Du 40 Positionsupdates pro Sekunde pro Spieler auf der gesamten Welt verarbeiten möchtest, macht es nicht so viel aus, wenn ein oder zwei Positionsdaten auf dem Weg verloren gehen. In solchen Fällen ist UDP eine gute Wahl.

2.2 Low Level Kuriositäten und Netzwerk Theorie

Nachdem ich die verschiedenen Layer der Protokolle erläutert habe, ist es jetzt an der Zeit, über Netzwerke zu reden und ein paar Beispiele aufzuführen, die den Aufbau von SOCK_DGRAM Paketen näher bringen sollen. Vermutlich kannst Du dieses Kapitel getrost überspringen, es schadet jedoch nicht ein bisschen hinter die Kulissen zu schauen.

Hey, es ist an der Zeit die Datenkapselung zu betrachten. Im Grunde sagt sie geht es um Folgendes: Ein Paket wird geboren, ein Paket wird von dem ersten Protokoll (sagen wir mal dem TFTP Protokoll) eingepackt (gekapselt, oder encapsulated): in einen Header (und selten einen Footer). Dann wird das ganze Ding (Also der TFTP Header und die Daten) erneut gekapselt (beispielsweise über UDP), und danach noch mal (IP), um anschließend vom finalen Protokoll der Hardware (physikalisch) verarbeitet zu werden (sagen wir, Ethernet).

Wenn ein anderer Computer dieses Paket erhält, entfernt die Hardware den physikalischen Layer (Ethernet), der Kernel entfernt den IP und UDP Layer, das TFTP Programm den TFTP Header und erhält dann endlich die Daten.

Jetzt kann ich endlich über das eventuell bekannte Layered Network Model (aka ISO/OSI) reden. Das Network Model beschreibt eine Funktionalität, das viele Vorteile bietet. Zum Beispiel kannst Du einfach Programme schreiben, ohne Dir über die physikalischen Mittel Gedanken zu machen (serial, schmales Ethernet, AUI, wasauchimmer), weil die Programme auf den unteren Ebenen sich darum kümmern. Die eigentliche Hardware und Struktur ist dem Socket-Programmierer nicht bekannt.

Ohne Umschweife komme ich einfach zu dem gesamten Model. Für Klausuren darf man so etwas behalten, wir möchten lediglich die Abläufe besser verstehen:

Application
Presentation
Session
Transport
Network
Data Link
Physical

Der physikalische Layer ist die Hardware (serial, Ethernet, etc.). Der Application Layer ist genau so weit von der Hardware entfernt, wie Du Dir vorstellen kannst. Es ist der Ort, an dem die Nutzer mit dem Netzwerk interagieren.

Dieses Modell ist so verallgemeinert, Du könntest es vermutlich zum Reparieren von Autos verwenden, wenn Du es nur wirklich wolltest. Ein in Unix besser passendes Modell ist folgendes:

Application Layer (telnet, ftp, etc.)
Host-to-Host Transport Layer (TCP, UDP)
Internet Layer (IP und Routing)
Network Access Layer (Ethernet, WiFi, oder was auch immer)

Zu diesem Zeitpunkt kannst Dir vermutlich vorstellen, wie dieses Modell mit der Kapselung der originalen Daten zusammenhängt.

Siehst Du wie viel Arbeit es ist, ein einfaches Paket zu erstellen? Und Du musst den Header selber schreiben, indem Du cat benutzt! Ach, war nur ein Scherz. Du musst lediglich über die Stream Sockets mittels send() die Daten auf den Weg schicken. Und alles was Du für die Datagram Sockets brauchst ist die Kapselung des Pakets in der Methode, die Dir beliebt und verschicken kannst Du es dann mittels sendto(). Der Kernel erstellt den Transport Layer für Dich und die Hardware kümmert sich um den Network Access Layer. Ein Lob auf die moderne Technologie.

Und so sind wir am Ende unserer kurzen Einführung in die Netzwerk Theorie angekommen. Ich habe alles ausgelassen, was ich über das Routing erzählen wollte, denn das war genau genommen: Nichts! Ich werde auch nichts darüber erzählen. Der Router entfernt den IP Header, guckt in seiner Routing Tabelle nach, blah blah blah. Wenn Dich das wirklich interessiert, guck doch mal im im IP RFC nach. Und wenn Du es nicht lernst, wirst Du wohl oder übel trotzdem überleben.

3 IP Adressen, structs und mehr

Okay, wir können jetzt endlich mit dem coden anfangen. Zuerst sollten wir zwar noch etwas Code-Fremdes behandeln, aber danach geht’s dann wirklich los.

Es geht um IP Adressen und Ports, nur ganz kurz, damit wir uns da dann nachher nicht mehr drum kümmern müssen.

3.1 IP Adressen (IPv4 und IPv6)

In der guten alten Zeit, als Ben Kenobi noch Obi Wan Kenobi genannt wurde, gab es ein wundervolles Routing System, welches als The Internet Protocol Version 4 bezeichnet wurde, bekannter als IPv4. Die Adressen wurden aus vier Bytes zusammengebaut und wurden normalerweise durch Punkte getrennt. So sah das dann aus: 192.0.2.111

Ich bin mir ziemlich sicher, dass Dir diese Notation nicht ganz unbekannt ist. In der Zeit, als dieser Guide geschrieben wurde benutzt jede einzelne Internetseite IPv4.

Jeder, Obi Wan eingeschlossen, war glücklich, alles lief ordentlich, bis dann ein Miesmacher alle darauf hinwies, dass die IPv4 Adressen nicht ausreichten, dass das Limit bald erreicht sei. Das war Vint Cerf.

(Neben der als Coming IPv4 Apocalypse of Doom and Gloom bezeichneten Warnung, war Vint Cerf übrigens auch bekannt als der Vater des Internets).

Aber wie konnte es denn bitte so weit kommen, dass das Limit an Adressen erreicht wird? Ich meine, es gibt doch Milliarden von IP Adressen mit diesen 32 Bit Adressen. Haben wir denn wirklich Milliarden von Computern da draußen?

Ja.

Außerdem wurde am Anfang, als es nur ein paar Computer gab und jeder dachte, dass es unmöglich sei 1 Milliarde an Adressen zu gebrauchen, ein großer Teil der Adressen an große Unternehmen verteilt. Unternehmen wie Xerox, MIT, Ford, HP, IBM, GE, AT&T und eine kleine Computerschmiede mit dem Namen Apple bekamen somit Millionen von Adressen. Es ist sogar so, dass wir schon vor Jahren am Limit angekommen wären, hätte es nicht Aktionen gegeben, die diese inflationäre Verteilung an IP Adressen vermeiden konnten.

Aber nun leben wir nun mal in einer Zeit, in der jeder Mensch eine IP Adresse haben kann und will, jeder Computer, jedes Telefon, jeder Taschenrechner, jede Parkuhr und jeder Hund (warum eigentlich nicht?) natürlich auch.

Und somit ist dann IPv6 geboren. Und da wir keine Lust haben, diese Stimme von Vint Cerf in ein paar Jahren wieder zu hören, die ruft: „Ich hab’s euch doch gesagt“, wenn wir dann wieder nicht mehr ausreichend Adressen haben, wurden entsprechende Maßnahmen getroffen.

Was heißt das wohl? Wir brauchten viel mehr Adressen. Nicht nur doppelt so viele Adressen, nicht eine Milliarden mal so viele Adressen, auch nicht 1000 Billionen mal so viele Adressen, sondern 79 Millionen Milliarden Billionen mal so viele Adressen. Das sollte vorerst reichen, und Vint Cerf wird sich wohl erst mal wieder anderen Themen widmen.

Du magst jetzt sagen, „Ist das wirklich wahr? Ich glaube einfach nicht an so riesige Zahlen.“ Und es ist wirklich komisch, der Unterschied zwischen 32 Bits und 128 Bits wirkt tatsächlich nicht allzu groß, es sind ja nur 96 Bits mehr, oder? Aber Du musst Dir vor Augen führen, dass wir hier in Potenzen denken: 32 Bits repräsentieren etwa 4 Milliarden Zahlen (2^32), während 128 Bits 340 Billionen Billionen Billionen Nummern (2^128) darstellen. Das sind etwa 1 Millionen IPv4 Adressen für jeden Stern im gesamten Universum.

Du musst diesen Zahlen und Nummern Ausdruck, den Du von IPv4 kennst, jedoch vergessen. Wir benutzen Hexadezimale Ausdrücke, bei dem alle 2 Bytes durch ein Doppelpunkt getrennt wird, wie hier:

2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551

Das ist aber noch nicht alles! Du wirst viele IP Adressen haben, die viele Nullen beinhalten, diese kann man auf die folgende Art und Weise komprimieren:

2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::51

2001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::

0000:0000:0000:0000:0000:0000:0000:0001
::1

Hier werden Nullen, die am Anfang eines jeden Blocks stehen, einfach weggelassen und Reihen von Nullen werden durch zwei Doppelpunkte dargestellt.

Die Adresse ::1 ist eine sog. loopback Adresse. Damit ist die Maschine gemeint, auf der der Aufruf ausging. Das IPv4 Pendant dazu ist die Adresse 127.0.0.1, also der Localhost. Außerdem gibt es noch einen IPv4 Kompatibilitätsmodus für IPv6 Adressen, auf den Du eventuell stoßen wirst. Wenn Du die IPv4 Adresse 192.0.2.33 als IPv6 Adresse benutzten willst, kannst Du das wie folgt tun:

::ffff:192.0.2.33

Wenn das nicht geil ist!
Es ist halt einfach klasse, dass die Entwickler von IPv6 so viele Adressen einfach mal reserviert haben, aber wir haben ja zum Glück so viele, wer zählt denn da noch mit? Es gibt ja noch so viele, die für jeden ausreichen, wo wir wieder zu den Computern, Telefonen, Taschenrechnern und auch dem besten Freund des Menschen kommen. Auf jedem Planeten, in jeder Galaxis.

3.1.1 Subnetze

Aus organisatorischen Gründen ist es eigentlich gar nicht so dumm, wenn man sagt, „der erste Teil einer IP Adresse steht für das Netzwerk und der restliche Teil deklariert den Host“.

Nehmen wir die IPv4 Adresse 192.0.2.12 als Beispiel. Wir sagen, dass die ersten 3 Bytes das Netzwerk sind und das letzte Byte der Host. Somit wäre die 12 dann der 12. Host in dem Netzwerk 192.0.2.0 (beachte, dass wir hier eine 0 als letztes Byte benutzen).

Und jetzt gehen wir noch weiter in die Vergangenheit zurück. Vor einigen Jahrzehnten gab es sogenannte Klassen von Subnetzen, bei denen die ersten ein, zwei, oder 3 Bytes der Adresse der Netzwerkteil war. Wenn Du richtig viel Glück hattest, konntest Du 1 Byte als Netzwerk benutzen und die restlichen 3 als Hosts verwenden. Das sind 24 Bits die Du als Hosts benutzen konntest (etwa 24 Millionen). Diese Netzwerke bezeichnet man als Klasse A Netzwerk. Auf der anderen Seite hatte das Klasse C Netzwerk nur 1 Byte, welches für Hosts benutzt werden konnte (256 hosts).

Du siehst, es gab nur wenige Klasse A Netzwerke, verdammt viele Klasse C Netzwerke und in der Mitte lagen dann die Klasse B Netzwerke.

Der Netzwerkteil der Internet Adresse wird als Netzwerkmaske bezeichnet, welches ein bitweises UND mit der IP Adresse verwendet, um die Netzwerkadresse heraus zu bekommen. Für gewöhnlich sieht man die folgende Form: 255.255.255.0. Angenommen Du hast die IP Adresse 192.0.2.12, dann benutzt man das bitweise UND der Netzmaske um das Netzwerk herauszubekommen. 192.0.2.12 UND 255.255.255.0 ergibt die Netzwerkadresse 192.0.2.0.

Jedoch hat dies für die Ansprüche des Internets nicht gereicht, da wir Klasse C Netzwerke zu schnell verbrauchten und wir hatten definitiv zu wenige Klasse A Netzwerke. Was sollte man also machen. Man ist von der Schreibweise weggegangen, dass man immer nur 8, 16 und 24 Bits nehmen konnte (die 255 pro ‚Block‘), sondern jetzt waren auch Subnetze wie 255.255.255.252 möglich, welches 30 Bits benutzt. Es gilt zu beachten, dass eine Netzmaske immer nur einer Anzahl von 1-Bits sind, die von 0-Bits gefolgt werden. Bei der Netzmaske 255.255.255.252 hat man also 30 Bits für das Netzwerk, somit bleiben 2 Bits für den Host übrig.

Zugegeben, es ist unnötig einen so großen String für die Darstellung einer Nummer, sagen wir 255.192.0.0 als Netzmaske, zu benutzen. Erstens haben Leute keine direkte Vorstellung, wie viele Bits das jetzt sind, außerdem ist das auch nicht wirklich kompakt. Also hat man einen neuen Style eingeführt, der diese Probleme lösen soll. Du fügst einfach ein Slash nach der IP Adresse ein, der gefolgt wird von der Anzahl an Netzwerkbits. Das Ganze sieht dann so aus:

192.0.2.12/30

Bei IPv6 wird das wie folgt umgesetzt:

2001:db8::/32 or 2001:db8:5413:4028::9db9/64

3.1.2 Ports

Wenn Du Dich noch dran erinnerst, habe ich bei dem Layered Network Model davon geredet, dass der Internet Layer (IP) von dem Host-zu-Host Layer (TCP und UDP) getrennt wird. Falls Dir das nicht mehr ganz klar ist, solltest Du das noch mal kurz durchlesen, es ist wichtig, dass Du das verstehst.

Es gibt aber noch eine andere Adresse neben der IP Adresse (welche vom IP Layer benutzt wird). Diese wird von den TCP (Stream Sockets) und auch von UDP (Datagram Sockets) benutzt und ist für die lokale Zuordnung zuständig. Das ist die sogenannte Portnummer und besteht aus 16 Bits.

Stell Dir die IP Adresse wie die Anschrift von einem Hotel vor, in diesem Beispiel ist die Portnummer dann die Zimmernummer.

Wir nehmen an Du hast einen Computer, der sich sowohl um die reinkommenden Mails kümmern soll, gleichzeitig aber auch noch einen Webserver laufen hat, um Webseiten darzustellen. Wie soll man jetzt unterscheiden, welche Pakete zu welcher Anwendung gehören, wenn man nur die IP Adresse besitzt?

Im Internet haben sich gewisse Portnummern für gewisse Services etabliert. Diese kann man bei der Big IANA Port Liste einsehen, wenn Du ein Unix System laufen hast, solltest Du mal in die Datei /etc/services reingucken. HTTP benutzt den Port 80, telnet den Port 23, SMTP den Port 25 und das Spiel Doom übrigens den Port 666. Ports unter 1024 werden übrigens oft als besonders eingestuft und brauchen besondere Rechte, damit diese genutzt werden können.

3.2 Byte Reihenfolge

Es gibt zwei Byte Reihenfolge, im Verlaufe werden diese bekannt werden als Lahm und Schön.

Ich mache natürlich nur Witze, aber die eine Reihenfolge ist tatsächlich schöner als die andere. :)

Es gibt leider keine andere Art es Dir mitzuteilen, also sage ich es Dir direkt ins Gesicht: Dein Computer hat möglicherweise die Bytes in der falschen Reihenfolge gespeichert, ohne dass Du je etwas davon mitbekommen hättest. Man hat es Dir einfach verschwiegen.

Das ist auch eigentlich gar kein so großes Problem, nur haben die Internet Menschen sich darauf geeinigt, dass man für die Darstellung einer 2-Byte Hexadezimalnummer, sagen wir b34f, diese als 2 sequentielle Bytes speichert. Erst b3 und danach 4f. Diese Art der Darstellung bezeichnet man als Big-Endian (wörtlich. „Groß-Endian“).

Nur leider gibt es hier und da ein paar Computer, deren Prozessoren die Bytes in der anderen Reihenfolge speichern. Bei dem Beispiel b34f würde nun sequentiell das erste Byte 4f im Speicher landen, gefolgt von b3. Diese Art der Speicherung nennt man Little-Endian (wörtlich „Klein-Endian“).

Aber ich bin noch nicht fertig mit den Fachbegriffen. Der schönere Big-Endian wird auch Network Byte Order genannt und zwar aus dem einfachen Grund, dass diese Art der Speicherung von den Netzwerken benutzt wird.

Dein Computer hingegen speichert die Bytes in der sog. Host Byte Order. Hast Du ein Intel 80×86 ist die Host Byte Order Little Endian, ist es ein Motorola 68k ist die Host Byte Order Big-Endian. Hast Du einen PowerPC ist die Host Byte Order… Naja, Du verstehst wodrauf ich hinaus will, oder?

Du musst oft beim zusammenbauen von Datenpaketen dadrauf achten, dass Du die Network Byte Order benutzt. Aber wie macht man das, wenn man nicht weiß was die Host Byte Order ist?

Man geht hier ganz einfach vor und geht davon aus, dass die Host Byte Order nie richtig ist. Deswegen wird vor jedem Zusammenbauen der Pakete eine Funktion aufgerufen, die sich darum kümmert, dass alles ordentlich in die Network Byte Order gebracht wird. Die Funktion sorgt also dafür, dass unser Code auf jeder Maschine voll funktionsfähig ist.

Es gibt zwei Arten von Nummern: short (2 Bytes) und long (4 Bytes). Die Funktionen die wir benutzten wollen funktionieren auch mit unsigned Variationen, also müssen wir uns da keine Gedanken drüber machen. Okay, nehmen wir an wir wollen eine short Nummer von der Host Byte Order in die Network Byte Order konvertieren. Wir fangen mit einem „h“ (für Host) an, gefolgt von einem „to“ (engl. „zu“), dann „n“ (für Network) und am Ende ein „s“ (für short). h-to-n-s, oder auch htons() (gelesen als: Host to Network Short).

Es ist wirklich nicht so schwer und hier wird auch wieder klar, warum es so wichtig ist auch die Englischen Begriffe zu kennen.

Du kannst jede Kombination von „n“, „h“, „s“ und „l“ benutzen, hier ist eine kleine Zusammenfassung:

htons()host to network short

htonl()host to network long

ntohs()network to host short

ntohl()network to host long

Du willst also alle Pakete die raus gehen in die Network Byte Order bringen und alle Pakete die dann wieder auf dem PC ankommen in die Host Byte Order umwandeln. Macht sogar ein bisschen Sinn, oder?

Leider kenne ich mich nicht gut mit einer 64 Bit Variante aus, sorry. Und wenn Du Floating Point Nummern benutzten möchtest, kannst Du Dir den Teil über Serialisierungen weiter unten angucken.

Nimm für dieses Tutorial bitte an, dass die Nummern in der Host Byte Order gespeichert sind, es sei denn ich weise extra auf eine andere Reihenfolge hin.

3.3 Structs

Wir haben es endlich geschafft, wir sind endlich da angekommen, wo’s auch wirklich um’s Programmieren geht. In diesem Abschnitte werde ich verschiedene Datentypen behandeln, die von dem Socket Interface benutzt werden, da einige wirklich schwer zu verstehen sind.

Wir fangen mit einem einfachen an, dem Socket-Deskriptor. Ein Socket-Deskriptor hat den folgenden Datentyp:

int

Ein ganz normaler int.

Jetzt wird alles ein wenig verrückt, also probier mitzukommen und alles zu verstehen.

Wir fangen mit unserem ersten Struct an: struct addrinfo. Dieses struct ist eine etwas neuere Erfindung und wird wird genutzt um socket adress structs für die weitere Benutzung vorzubereiten. Außerdem kann man Host Name Lookups und Service Name Lookups damit machen. Das alles wird später einen Sinn ergeben, wenn wir das Ding auch wirklich benutzen, für jetzt reicht es zu wissen, dass es mit eines der ersten Dinge ist, die aufgerufen werden um eine Verbindung aufzubauen.

struct addrinfo {
    int              ai_flags;     // AI_PASSIVE, AI_CANONNAME, etc.
    int              ai_family;    // AF_INET, AF_INET6, AF_UNSPEC
    int              ai_socktype;  // SOCK_STREAM, SOCK_DGRAM
    int              ai_protocol;  // use 0 for "any"
    size_t           ai_addrlen;   // size of ai_addr in bytes
    struct sockaddr *ai_addr;      // struct sockaddr_in or _in6
    char            *ai_canonname; // full canonical hostname

    struct addrinfo *ai_next;      // linked list, next node
};

Du lädst dieses struct, damit Du dann getaddrinfo() aufrufen kannst. Die Funktion gibt einen Pointer zu einer verketteten Liste (auch als Linked List bekannt) zurück, welche mit allen nötigen structs gefüllt ist und allem Sonstigen, was Du noch brauchst.

Du kannst über das ai_family Feld erzwingen, dass IPv4, oder IPv6 benutzt wird, oder benutzt einfach AF_UNSPEC um das jeweils Notwendige zu benutzen. Das ist ziemlich praktisch, da Dein Code nicht an einen der beiden Standards gebunden ist.

Bitte behalte im Hinterkopf, dass es sich um eine verkettete Liste handelt: ai_next zeigt auf das jeweilig nächste Element – Es gibt verschiedene Resultate von denen Du Dir welche aussuchen kannst. Ich würde das Erste nutzen, was dann auch funktioniert, aber eventuell hast Du ja andere Ansprüche als ich.

Dir ist eventuell aufgefallen, dass das ai_addr Feld in struct addrinfo ein Zeiger auf das struct sockaddr ist. Das ist eines der vielen Details, welche die IP Adressen ausmachen.

Häufig brauchst Du gar nichts in diese structs schreiben; oft reicht es aus getaddrinfo() aufzurufen um dein struct addrinfo auszufüllen. Du musst jedoch einige Daten aus diesen structs herausholen, ich zeige Dir hier welche Du brauchst.

Einige structs sind IPv4, andere sind IPv6 und wieder andere können beides. Ich werde anmerken welche was können.

Das struct sockaddr enthält (Überraschung!) Socket Adressen Informationen für viele Typen von Sockets.

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

sa_family kann vieles sein, aber für dieses Beispiel wird es AF_INET (IPv4) oder AF_INET6 (IPv6) sein. sa_data enthält die Daten über die Zieladresse und den Port für das Socket. Das ist aber eher unpraktisch da man die Adresse nicht per Hand eintragen will.

Um mit diesem Problem besser umgehen zu können, haben Entwickler ein neues, leicht verändertes struct programmiert: struct sockaddr_in (wobei in für Internet steht) welches mit IPv4 funktioniert.

Wichtig ist folgendes: Der Zeiger auf struct sockaddr_in kann zu einem Zeiger auf struct sockaddr umgewandelt (engl. cast) werden und vice-versa. Das hat den riesigen Vorteil, dass man beim Aufrufen von connect() zwar einen Zeiger auf struct sockaddr* benötigt, aber man immer noch struct sockaddr_in in der letzten Sekunde umwandeln kann.

// (Nur für IPv4 gedacht--für IPv6 siehe struct sockaddr_in6)

struct sockaddr_in {
    short int          sin_family;  // Address family, AF_INET
    unsigned short int sin_port;    // Port number
    struct in_addr     sin_addr;    // Internet address
    unsigned char      sin_zero[8]; // Same size as struct sockaddr
};

Diese Struktur macht es einfach die Elemente aus der Socket Adresse zu referenzieren. Bitte achte darauf, dass sin_zero (welches gebraucht wird um die um die Größe gleich der Größe von struct sockaddr zu bringen) überall Nullen enthält; dies ist über die Funktion memset() möglich. Etwas ausführlicher aber auf Englisch findet man hier eine Erklärung. Bitte beachte auch, dass sin_family mit sa_family aus dem struct sockaddr zu vergleichen ist und auf AF_INET gesetzt wird. Schließlich muss der sin_port in die Network Byte Order (durch das benutzen von htons() !) gebracht werden.

Lasst uns etwas tiefer eintauchen! Du fragst Dich vielleicht, was dieses sin_addr Feld in dem struct in_addr zu suchen hat. Ich möchte Dich jetzt nicht erschrecken, oder  gar übertreiben, aber es ist mit eines der krassestens Unions:

// (Nur für IPv4 gedacht--für IPv6 siehe struct in6_addr)

// Internet Adresse (eine Struktur aus historischen Gründen)
struct in_addr {
    uint32_t s_addr; // ein 32 Bit int (4 Bytes)
};

Woah! Wenn Du ina als Typ struct sockaddr_in deklariert hast, dann ist ina.sin_addr.s_addr eine Referenz auf die 4-Byte IP Adresse  (in Network Byte Order). Bitte beachte, dass selbst wenn Dein System die von Gott gehasste Union für struct in_addr benutzt, Du immer noch die Referenz der 4-Byte IP Adresse genau so wie oben beschrieben bekommen kannst – das ganze funktioniert dann über #defines.

Und was ist mit IPv6? Ähnliche structs existieren auch hierfür:

// (Nur für IPv6--für IPv4 siehe struct sockaddr_in und struct in6_addr)

struct sockaddr_in6 {
    u_int16_t       sin6_family;   // address family, AF_INET6
    u_int16_t       sin6_port;     // port number, Network Byte Order
    u_int32_t       sin6_flowinfo; // IPv6 flow information
    struct in6_addr sin6_addr;     // IPv6 address
    u_int32_t       sin6_scope_id; // Scope ID
};

struct in6_addr {
    unsigned char   s6_addr[16];   // IPv6 address
};

Bitte beachte, dass IPv6 genau so eine IP Adresse und einen Port benötigt, wie Du das von IPv4 kennst.

Außerdem werde ich hier nichts zu IPv6 flow information oder Scope ID Feldern schreiben. Für jetzt… Das ist ja immer noch ein Dokument für Anfänger. :)

Nun möchte ich noch ein anderes simples struct vorstellen, struct sockaddr_storage, das groß genug ist um sowohl IPv4 als auch IPv6 structs zu halten. Es sieht nun mal so aus, dass Du bei manchen Aufrufen vorher nicht weißt, ob struct sockaddr mit IPv4, oder aber IPv6 Adressen gefüllt wird. Für diesen Fall übergibst Du einfach diese Struktur, die genau so aufgebaut ist wie struct sockaddr, nur dass diese größer ist, dann wandelst Du es zu dem Typ um, den Du brauchst:

struct sockaddr_storage {
    sa_family_t  ss_family;     // address family

    // all this is padding, implementation specific, ignore it:
    char      __ss_pad1[_SS_PAD1SIZE];
    int64_t   __ss_align;
    char      __ss_pad2[_SS_PAD2SIZE];
};

Wichtig hierbei ist, dass Du die Adressfamilie sehen kannst, über das Feld ss_family – Hierüber kannst Du dann sehen, ob es sich um eine AF_INET, oder aber eine AF_INET6 Adresse handelt. Dann kannst Du es zu einem struct sockaddr_in, oder einem struct sockaddr_in6 umwandeln, wenn Du willst.

3.4 IP Adressen, Part Deux

Zu Deinem Glück gibt es einen Haufen von Funktionen, mit denen Du IP Adressen verändern und manipulieren kannst. Du musst Dich also nicht jedes Mal per Hand darum kümmern diese zu ändern, oder sie mittels << in einen long zu stopfen.

Wir nehmen an, Du hast ein struct sockaddr ina und Du hast die IP Adresse „10.12.110.57“ oder „2001:db8:63b3:1::3490“, die Du in dem Verbund (= struct) speichern möchtest. Die Funktion, die Du aufrufen möchtest, inet_pton(), wandelt die IP Adresse, bestehend aus Punkten und Zahlen, in entweder ein struct in_addr, oder ein struct in6_addr um, je nachdem, ob Du AF_INET, oder AF_INET6 angegeben hast. „pton“ steht für „presentation to network“, „printable to network“ ergibt jedoch mehr Sinn und und man kann sich das auch viel besser merken.

Du kannst die Adresse wie folgt umwandeln:

struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6

inet_pton(AF_INET, "192.0.2.1", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6

(Kurze Bemerkung: Ursprünglich hatte man die Adresse umgewandelt, indem man die Funktionen inet_addr() oder die Alternative inet_aton() anfgerufen hat; diese sind aber nicht mehr zeitgemäß, da sie IPv6 nicht unterstützen.)

Das oben beschriebene Beispiel ist nicht sehr robust, da wir nicht prüfen, ob irgendwelche Fehler aufgetreten sind. inet_pton() gibt den Wert -1 zurück, falls irgendetwas schief gelaufen ist und 0, wenn die Adresse fehlerhaft ist. Du solltest also auf jeden Fall checken, ob der Rückgabewert größer als 0 ist und dann entsprechend reagieren.

Super, jetzt kannst Du einen String, der die Adresse enthält in den binären Ausdruck umwandeln. Aber wie sieht es aus, wenn Du aus der binären Darstellung die lesbare String Darstellung bekommen möchtest? Was ist, wenn Du ein struct in_addr hast und Du möchtest die Punkt-und-Zahl Darstellung bekommen? (Oder struct in6_addr, für eine, ähm, „Hexadezimal-und-Doppelpunkt“ Darstellung.) In diesem Fall solltest Du die Funktion inet_ntop() („ntop“ steht für „network to presentation“ – auch hier ist es eventuell wieder einfacher sich „network to printable“ zu merken), wie folgt:

// IPv4:

char ip4[INET_ADDRSTRLEN];  // space to hold the IPv4 string
struct sockaddr_in sa;      // pretend this is loaded with something

inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);

printf("The IPv4 address is: %s\n", ip4);

// IPv6:

char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6;    // pretend this is loaded with something

inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);

printf("The address is: %s\n", ip6);

Wenn Du die Funktion aufrufst, übergibst Du den Adresstype (IPv4 oder IPv6), die Adresse, einen Zeiger auf den String, der das Ergbnis enthalten soll und die maximale Länge des Strings. (Zwei Makros existieren mit der größtmöglichen Größe, die der String benötigt um eine IPv4, bzw. IPv6 zu speichern: INET_ADDRSTRLEN und INET6_ADDRSTRLEN.)

(Auch hier möchte ich kurz zeigen, wie der ursprüngliche Weg war: Die mittlerweile veraltete Funktion zum Umwandeln heißt inet_ntoa(). Auch diese kann kein IPv6.)

Zuletzt sei gesagt, dass diese Adressen nur mit numerischen IP Adressen funktionieren. Sie können keine DNS Lookups nach Hostnamen, wie bspw. „www.example.com“ machen. Dafür brauchst Du getaddrinfo(), wie Du später sehen wirst.

3.4.1 Private (oder nicht verbundene) Netzwerke

Es gibt viele Ort, an denen eine Firewall das Netzwerk vom Rest der Welt versteckt um dieses zu schützen. Und oft werden „interne“ Adressen von der Firewall in „externe“ Adressen konvertiert (diejenigen, die jeder außerhalb kennt). Hierfür wird ein Prozess benutzt, der Network Adress Translation, oder kurz NAT heißt.

Wirst Du schon nervös? „Warum erzählt der denn den ganzen Kram?“

Bleib ruhig, hol Dir ein nicht-alkoholisches Getränk (von mir aus auch ein alkoholisches), denn als Anfänger brauchst Du Dich nicht über NAT kümmern, da alles schön transparent für Dich gemacht wird. Ich wollte nur kurz über das Netzwerk hinter einer Firewall reden, da ich nicht möchte, dass Du wegen der komischen Netzwerknummern verwirrt durch die Gegend rennst.

Ich habe beispielsweise eine Firewall bei mir zuhause. Ich habe zwei statische IP Adressen, die mir von meinem DSL Unternehmen zugeteilt wurden und habe 7 Computer in meinem Netzwerk hängen. Wie ist das möglich? Zwei Computer können doch nicht die selbe Adresse haben, sonst wissen die Pakete doch nicht, zu welchem sie jetzt gehen müssen.

Die Antwort ist denkbar einfach: Sie haben nicht die selbe Adresse. Sie sind in einem privaten Netzwerk mit 24 Millionen IP Adressen. Diese habe ich ganz für mich alleine. Ich möchte kurz erklären, wie das funktioniert:

Wenn ich mich bei einem anderen Computer einlogge, sagt dieser mir, dass ich mich von der Adresse 192.0.2.33 einlogge, welche die öffentliche IP Adresse ist, die ich von meinem ISP (Internet Service Provider) bekommen habe. Wenn ich meinen eigenen Computer frage, was er für eine Adresse hat, dann sagt er mir 10.0.0.5. Wer übersetzt jetzt die IP Adresse von der einen in die andere? Richtig geraten, die Firewall! Diese führt NAT aus.

10.x.x.x ist eines dieser paar reservierten Netzwerke, die entweder bei absolut nicht-angeschlossenen Netzwerken benutzt wird, oder bei Netzwerken, die hinter einer Firewall stehen. Falls Du mehr über private Netzwerknummern erfahren möchtest, kannst Du mehr dazu im RFC 1918 erfahren, aber einige übliche sind 10.x.x.x und 192.168.x.x (x = 0-255). Etwas unüblicher ist 172.y.x.x (wobei y zwischen 16 und 31 liegt).

Netzwerke, die hinter einer Firewall (die NAT benutzt) liegen, müssen nicht zwingend auf einem reservierten Netzwerk liegen, tun dies aber üblicherweise.

(Übrigens: Meine IP Adresse ist nicht wirklich 192.0.2.33. Das 192.0.2.x Netzwerk wird dafür verwendet, „pseudo-echte“ IP Adressen zu benutzen, die in Tutorials, wie diesem hier, verwendet werden können.)

Auch IPv6 hat private Netzwerke. Sie fangen mit fdxx:: an (in der Zukunft vielleicht fcxx::), so wie im RFC 4193 festgelegt ist. NAT und IPv6 werden üblicherweise nicht gemischt, es sei denn (wenn Du nicht unbedingt den IPv6 zu IPv4 Gateway benutzt, der dieses Dokument jedoch sprängen würde) Du möchtest es unbedingt machen – theoretisch ist dies jedoch nicht nötig, da es mit IPv6 so viele Adressen gibt, dass NAT nicht mehr verwendet werden muss.

4 Von IPv4 zu IPv6 springen

Aber ich möchte doch unbedingt wissen, was ich an meinem Code ändern muss um IPv6 ans Laufen zu bekommen? Sag schon, wie geht das?

Ok! Ok!

Zwar ist alles, was ich hier rein schreibe bereits gesagt worden, aber es ist eine kurze Version für die ganz Ungeduldigen. (Natürlich gibt es viel mehr zu diesem Thema zu sagen, aber das hier Genannte reicht für diesen Guide aus.)

  1. Als erstes, solltest Du über getaddrinfo() all die Informationen holen, die Du für struct sockaddr benötigst, anstatt den Verbund (struct) selber aufzubauen. Das hilft Dir Deine IP Version automatisch zu bestimmen und viele Unterschritte zu sparen.
  2. An jedem Punkt, wo Du die IP Version hard-codest (also direkt in den Quellcode schreibst), solltest Du eine Hilfsfunktion benutzen.
  3. Ändere AF_INET zu AF_INET6
  4. Ändere PF_INET zu PF_INET6
  5. Ändere INADDR_ANY Zuweisungen in in6addr_any Zuweisungen. Diese sind etwas anders:
    struct sockaddr_in sa;
    struct sockaddr_in6 sa6;
    
    sa.sin_addr.s_addr = INADDR_ANY;  // benutze meine IPv4 Adresse
    sa6.sin6_addr = in6addr_any; // benutze meine IPv6 Adresse

    Zudem kann der Wert IN6ADDR_ANY_INIT als Initialisierer benutzt werden, wenn struct in6_addr deklariert wird und zwar so:

    	struct in6_addr ia6 = IN6ADDR_ANY_INIT;
  6. Anstatt struct sockaddr_in zu nutzen, solltest Du struct sockaddr_in6 benutzen; mache sicher, dass Du eine „6“ bei allen nötigen Feldern hinzufügst (mehr dazu im Kapitel structs). Es gibt kein sin6_zero Feld.
  7. Anstelle von struct in_addr zu nutzen, solltest Du struct in6_addr benutzten. Auch hier solltest Du sicher gehen, bei allen nötigen Feldern eine „6“ hinzuzufügen (mehr dazu im Kapitel structs).
  8. Anstelle von inet_aton(), oder inet_addr(), benutze inet_pton().
  9. Anstelle von inet_ntoa(), benutze inet_ntop().
  10. Anstelle von gethostbyname(), benutze das bessere getaddrinfo().
  11. Anstelle von gethostbyaddr(), benutze das bessere getnameinfo() (auch wenn gethostbyaddr() mit IPv6 funktioniert).
  12. INADDR_BROADCAST funktioniert nicht mehr. Benutze deswegen IPv6 multicast.

5 System Calls

An dieser Stelle beschäftigen wir uns mit den ganzen System Funktionen (auch System Calls genannt) und noch ein paar anderen Bibliotheksfunktionen, mit denen Du die Netzwerkfunktionalität Deiner Unix Maschine ausreizen kannst – bzw. jeder anderen Maschine, die die Sockets API unterstützt (BSD, Windows, Linux, Mac, etc.). Wenn Du eine dieser Funktionen aufrufst, übernimmt der Kernel alles weitere; automagisch.

Der Teil, an dem die meisten Menschen verzweifeln ist die Reihenfolge, in der die Sachen aufgerufen werden. Eventuell ist Dir schon aufgefallen, dass dir die Manualseiten hier nicht viel bringen. Um Dir aus dieser misslichen Lage zu helfen, habe ich die nötigen Befehle in genau der Reihenfolge hier aufgelistet, wie Du sie auch in Deinen Programmen aufrufen wirst.

Das, zusammen mit ein bisschen Beispielcode, ein paar Keksen und Milch und ein wenig Courage und Du wirst Daten durch das Netz schießen können, als wäre das alles gar nicht so schwer.

(Bitte beachte, dass aus Gründen der Übersichtlichkeit viele der folgenden Beispiele kein ausreichendes Errorchecking implementiert haben. Zudem wird angenommen, dass getaddrinfo() erfolgreichen ausgeführt wird und einen validen Eintrag in die verkettete Liste einträgt.)

5.1 getaddrinfo() – Es kann los gehen!

Diese Funktion arbeitet wirklich hart, sie bringt viele Optionen mit und dennoch ist die Benutzung sehr einfach. Die structs, die Du später benötigst, werden hier für die folgenden Funktionen vorbereitet.

Doch zuerst ein kleines bisschen Geschichte: Ursprünglich hat man eine Funktion gethostbyname() benutzt um einen DNS Lookup zu machen. Die erhaltenen Informationen hat man dann in das struct sockaddr_in eingetragen, welches dann an die entsprechenden Funktionen übergeben wurde.

Das ist jedoch nicht länger nötig, zum Glück. (Außerdem ist es nicht unbedingt angenehm, wenn man IPv4 und IPv6 kompatiblen Code schreiben möchte). Mittlerweile benutzt man die Funktion getaddrinfo(), welche alle möglichen Dinge für Dich übernimmt, dazu gehören DNS und Service Name Lookups. Außerdem werden auch direkt die structs, die Du später brauchen wirst, mit den notwendigen Informationen gefüllt.

Gucken wir uns das mal genauer an!

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *services,	
		const struct addrinfo *hints,
		struct addrinfo **res);

Du gibst dieser Funktion drei Eingabeparameter und die Funktion gibt Dir einen Pointer auf eine verkettete Liste, res, mit den Ausgaben.

Der node Parameter ist der Host Name, bzw. IP Adresse, mit der man sich verbinden möchte.

Als nächstes gibt es den service Parameter, der eine Port Nummer sein kann (Port „80“, zum Beispiel), oder aber der Name zu einem bestimmten Service. Eine Liste kann man entweder in /etc/services aufrufen, oder aber auf der IANA Port Liste nachsehen. Beispiele sind „http“, „ftp“, „telnet“, „smtp“ oder etwas anderes.

Zuletzt der hints Parameter, der auf ein struct addrinfo zeigt, welches bereits mit relevanten Informationen gefüllt wurde.

Hier haben wir einen Beispielsaufruf: Du bist ein Server, der auf Deiner Hostadresse lauscht, auf Port 3490. Bitte beachte, dass nicht wirklich gehorcht wird, sondern hier nur alles für das Horchen auf dem Netzwerk vorbereitet wird.

int status;
struct addrinfo hints;
struct addrinfo *servinfo;  // zeigt auf die Ergebnisse

memset(&hints, 0, sizeof hints); // Kümmert sich darum, dass das struct leer ist
hints.ai_family = AF_UNSPEC;     // Egal, ob IPv4 oder IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE;     // Fülle die IP für mich

if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
    fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
    exit(1);
}

// servinfo zeigt auf eine Linked List, die auf 1, oder mehrere structs zeigt

// ... mach, was auch immer Du machen willst, bist Du servinfo nicht mehr brauchst ....

freeaddrinfo(servinfo); // Leere die Linked List

Bitte beachte, dass ich ai_family auf AF_UNSPEC gesetzt habe, wodurch ich festlege, dass mir egal ist, ob ich nun IPv4, oder IPv6 benutze. Du kannst ai_family natürlich auch auf AF_INET, oder AF_INET6 setzen, wenn Du es spezieller möchtest.

Außerdem habe ich das AI_PASSIVE Flag benutzt. Dieses sagt getaddrinfo(), dass die Adresse meines lokalen Hosts an die Socket-structs übergeben werden soll. Das ist cool, denn dann muss ich das nicht selber machen. (Du kannst natürlich auch eine spezifische Adresse als ersten Parameter an getaddrinfo() übergeben: da, wo ich zur Zeit NULL benutze.)

Dann machen wir den Aufruf. Falls ein Fehler auftritt (getaddrinfo() gibt eine Zahl != 0 zurück), können wir das ausgeben, indem wir die Funktion gai_strerror() benutzen. Falls alles ordentlich funktioniert, zeigt servinfo auf eine verkettete Liste von struct addrinfo-Elementen, wobei jedes einzelne dieser Elemente ein struct sockaddr besitzt: Das können wir dann später benutzen. Ziemlich praktisch, oder?

Am Ende, wenn wir fertig sind mit der verketteten Liste, können und sollten wir den Speicher wieder frei machen, indem wir freeaddrinfo() aufrufen.

Hier haben wir einen anderen Beispielaufruf: Du bist ein Client und möchtest Dich mit einem Server, sagen wir „www.example.com“ auf dem Port 3490 verbinden. Und wieder gilt: Du verbindest Dich nicht wirklich mit dem Server, die Informationen, die für die Verbindung wichtig sind, werden lediglich in die nötigen structs gepackt.

int status;
struct addrinfo hints;
struct addrinfo *servinfo;  // Zeigt auf die Ergebnisse

memset(&hints, 0, sizeof hints); // Kümmert sich darum, dass das struct leer ist
hints.ai_family = AF_UNSPEC;     // gal, ob IPv4 oder IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets

status = getaddrinfo("www.example.net", "3490", &hints, &servinfo);

// servinfo zeigt auf eine verkettete Liste, die auf 1, oder mehrere structs zeigt

// etc.

Ich behaupte ja schon die ganze Zeit, dass servinfo eine verkettete Liste sei, die alle möglichen Informationen über eine Adresse enthält. Warum schreiben wir nicht ein kleines Demo Programm, welches die IP Adresse eines Hosts ausgibt, den man über die Command Line einliest.
Download Code

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main( int argc, char *argv[])
{
	struct addrinfo hints, *res, *p;
	int status;
	char ipstr[INET6_ADDRSTRLEN];

	// Erkläre dem Benutzer, wie man das Programm richtig startet
	if(argc != 2) {
		fprintf(stderr, "usage: showip hostname\n");
		return 1;
	}

	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_UNSPEC; // AF_INET oder AF_INET6 um Version zu wählen
	hints.ai_socktype = SOCK_STREAM;

	if((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
		return 2;
	}

	printf("IP addresses for %s:\n\n", argv[1]);

	for(p = res; p != NULL; p = p->ai_next) {
		void *addr;
		char *ipver;

		// Hole den Pointer zu der Adresse
		// Verschiedene Felder für IPv4 und IPv6
		if (p->ai_family == AF_INET) { //IPv4
			struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
			addr = &(ipv4->sin_addr);
			ipver = "IPv4";
		} else { //IPv6
			struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
			addr = &(ipv6->sin6_addr);
			ipver = "IPv6";
		}

		// Konvertiere die IP in einen String und printe es
		inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
		printf(" %s: %s\n", ipver, ipstr);
	}

		// Speicher von verketteter Liste befreien
		freeaddrinfo(res);

		return 0;
}

Wie man erkennen kann, holt sich der Code die Information zu dem Parameter, den man an das Programm übergeben hat und man kann dann durch das Iterieren der verketteten Liste alle möglichen Sachen machen, in diesem Fall holen wir uns die IP Adresse.

(Leider gibt es eine etwas unschöne Stelle und zwar dort, wo wir bei struct sockaddr auf die verschiedenen Typen achten müssen, je nachdem, welche IP Version wir benutzen. Ich weiß nicht, ob es einen besseren Weg gibt.)

Und da jeder Screenshots mag, hier die Ausgabe im Terminal.

Ausgabe von showip.out mit www.example.com als Beispielseite

Nachdem wir das also geschafft hätten, können wir die Ergebnisse aus getaddrinfo() benutzen, um sie an weitere Socket-Funktionen weiterzugeben, damit wir letztendlich unsere heiß erwartete Netzwerkverbindung aufbauen können.

5.2 socket() – Hole mir den Datei-Deskriptor

Ich glaube ich kann es nicht weiter aufschieben, ich muss jetzt über die socket()-Funktion reden. Hier ist die Zusammenfassung

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

Aber was sind das für Argumente? Eigentlich ist das Ganze gar nicht so schwer, es wird lediglich definiert, was für ein Socket Du haben willst. Heißt: IPv4 oder IPv6, Stream oder Datagram, UDP oder TCP.

Ursprünglich haben Menschen diese ganzen Argumente fest übergeben (hardcoded) und es ist immer noch möglich (domain ist PF_INET oder PF_INET6, type ist SOCK_STREAM oder SOCK_DGRAM, und protocol kann auf 0 gesetzt werden um das nötige Protokoll für den gegebenen Typ zu wählen. Oder aber Du rufst getprotobyname() um das Protokoll zu erfahren, welches Du brauchst, TCP oder UDP.)

Das PF_INET Ding hat eine große Ähnlichkeit zu dem AF_INET, welches Du benutzen kann, wenn Du das sin_family Feld in deinem struct sockaddr_in initialisierst. Diese sind einander sogar so ähnlich, dass sie den selben Wert haben und viele Programmierer werden socket() aufrufen und AF_INET als erstes Argument übergeben, anstelle von PF_INET. Es ist Zeit für eine kleine Geschichte, also hol Dir am besten ein Glas Milch und ein paar Kekse um es Dir gemütlich zu machen. Es war einmal vor langer, langer Zeit, als an dachte, dass vielleicht die Adressfamilie (übrigens steht hierfür das AF in AF_INET) verschiedene Protokolle unterstützen kann, die über ihre Protokollfamilie (du wirst es erahnen: Das PF in PF_INET) angesprochen werden können. Dazu kam es aber leider nie. Und sie lebten glücklich bis an ihr Lebensende. Fertig. Das bedeutet, dass Du am besten AF_INET in Deinem struct sockaddr_in benutzt, aber dafür PF_INET in Deinem Aufruf socket().

Aber jetzt mal weiter im Text. Eigentlich wolltest Du ja die Werte verarbeiten, die Du durch den Aufruft von getaddrinfo() bekommen hast und diese dann an socket() weitergeben. Das funktioniert so:

int s;
struct addrinfo hints, *res;

// Mach den Lookup
// [Und gehe davon aus, dass wir das "hints" struct schon gefüllt hätten]
getaddrinfo("www.example.com", "http", &hints, &res);

// [Hier solltest Du wieder nach Fehlern überprüfen (bei geaddrinfo())
// und durch die verlinkte Liste "res" gehen um valide Einträge zu finden,
// anstatt davon auszugehen, dass der erste Eintrag gut ist. Also komplett
// anders, als wir das hier in den Beispielen machen.
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

socket() gibt Dir einfach einen Socket-Deskriptor zurück, den Du später für die system calls benutzen kannst, oder aber -1, falls es einen Fehler gab. Die globale Variable errno wird auf den Fehlerwert gesetzt (im Manual gibt es mehr Details zu errno, hier wird auch auf Multithread Programme eingegangen).

„Okay, das ist jetzt ja alles schön und gut, ich habe hier jetzt das Socket, aber was kann ich damit wirklich machen?“ Letztendlich bringt Dir das Socket alleine nicht viel, aber wenn Du weiterliest, wirst Du sehen, dass Du weitere System Calls machen musst, sodass man den Nutzen erkennt.

5.3 bind() – Auf welchem Port bin ich eigentlich?

Wenn Du dann erst mal das Socket hast, musst Du es erst einmal mit einem Port auf Deinem Computer assoziieren. (Dies wird üblicherweise gemacht, wenn Du über die Funktion listen() (engl. zuhören) auf eingehende Verbindungen auf einem vorher definierten Port wartest – Online Multiplayer Spiele machen dies beispielsweise, wenn sie Dich um folgendes bitten: „Bitte verbinde Dich mit 192.168.5.10 Port 3490“.) Die Portnummer wird benötigt, damit der Kernel ankommende Pakete einem gewissen Prozess zuordnen können (genauer: dem Datei-Deskriptor eines Prozesses). Wenn Du Dich lediglich mittels connect() verbinden möchtest (weil Du der Client bist, und nicht der Server), dann ist dies möglicherweise unnötig. Du solltest es Dir dennoch durchlesen, für das allgemeine Verständnis.

Hier, wie gewöhnlich, erst einmal eine generelle Übersicht zu dem bind() Aufruf:

#include <sys/types.h>
#include <sys/socket.h<>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

sockfd ist der Datei-Deskriptor, der von socket() zurückgegeben wird. my_addr ist ein Zeiger zu einem struct sockaddr, das die Informationen über Deine Adresse hat, also den Port und die IP Adresse. addrlen ist – man ahnt es – die Länge dieser Adresse in Bytes.

Puh, das ist schon eine Menge an Informationen, die man auf einmal aufnehmen muss. Wir sollten das ganze anhand eines Beispiels mal durchgehen, welches das Socket an den Host des Programms bindet, auf dem Port 3490:

struct addrinfo hints, *res;
int sockfd;

// first, load up address structs with getaddrinfo():

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;  // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;     // fill in my IP for me

getaddrinfo(NULL, "3490", &hints, &res);

// make a socket:

sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

// bind it to the port we passed in to getaddrinfo():

bind(sockfd, res->ai_addr, res->ai_addrlen);

Durch das benutzen des AI_PASSIVE Flags teile ich dem Programm mit, dass es sich an die IP Adresse des Hosts binden soll, auf dem es läuft. Wenn Du das Programm an eine spezielle lokale IP Adresse binden möchtest, kannst Du das AI_PASSIVE Flag einfach weglassen und dafür die IP Adresse als erstes Argument an getaddrinfo() übergeben.

bind() gibt übrigens auch -1 zurück, falls etwas schief läuft, und setzt zudem auch wieder errno auf den Fehlercode.

Damals hat man übrigens viel manuellen Code benutzt, um struct sockaddr_in zu verpacken, bevor man dann bind() aufgerufen hat. Es ist offensichtlich, dass dieser Code IPv4-spezifisch war, aber es hat Dich nichts aufgehalten, das selbe mit IPv6 zu machen, abgesehen von der Tatsache, dass getaddrinfo() wesentlich einfacher zu benutzen ist. Sei’s drum, der alte Code sieht in etwa so aus:

// !!! So hat man das damals gemacht !!!

int sockfd;
struct sockaddr_in my_addr;

sockfd = socket(PF_INET, SOCK_STREAM, 0);

my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);     // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);

bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);

Im oben stehenden Code kann man auch das s_addr Feld auf INADDR_ANY setzen, wenn Du an Deine lokale IP Adresse binden wolltest (wie das AI_PASSIVE Flag, weiter oben.) Die IPv6 Version von INADDR_ANY ist die globale Variable in6addr_any, welche man dem sin6_addr Feld Deines struct sockaddr_in6 zuweisen kann. (Es gibt auch ein Makro IN6ADDR_ANY_INIT, welches Du für Deinen Variabel-Initialisierer benutzen kannst).

Außerdem musst Du beim Aufruf von bind() aufpassen, dass Du einen gültigen Port wählst. Alle Ports unter 1024 sind reserviert (es sei denn Du führst das Programm als Superuser aus)! Aber dafür hast Du noch genug Freiraum. Alle Ports bis zu 65535 sind frei verfügbar (natürlich nur, wenn diese von keinem anderen Programm benutzt werden).

Startest Du ein Server Programm neu und rufst bind() auf, kann es sein, dass dies nicht klappt. Es kommt die Fehlermeldung, dass der Port von einem anderen Programm benutzt wird. Was hat das bitte zu bedeuten? Nun ja, das Socket, das vorher verbunden war, hängt immer noch im Kernel und verstopft den Port. Du kannst jetzt entweder warten, bis dieses automatisch vom Kernel bereinigt wird, indem Du etwa eine Minute wartest, oder aber Du kannst den folgenden Code zu Deinem Programm hinzufügen, sodass es wieder möglich ist, dass Du den Port benutzen kannst:

int yes=1;
//char yes='1'; // Solaris people use this

// lose the pesky "Address already in use" error message
if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
    perror("setsockopt");
    exit(1);
}

Nun noch ein kleiner Hinweis zu bind(): Es gibt Umstände, in denen Du nicht unbedingt bind() aufrufen musst. Wenn Du dich über connect() mit einer anderen, entfernten Maschine verbindest und es Dir egal ist, was für einen lokalen Port Du nimmst, kannst Du einfach connect() aufrufen, die Funktion überprüft, ob das Socket ungebunden ist und wird selbstständig bind() aufrufen und an einen unbenutzten Port binden, wenn nötig. Dies passiert zum Beispiel bei der Benutzung von telnet, bei dem Dich nur der Port der anderen Maschine interessiert.

5.4 connect() – Na Du?

Nehmen wir für ein paar Minuten einmal an, dass Du eine telnet Applikation bist. Dein Benutzer verlangt von Dir (wie im Film Tron), dass Du ihm einen Socket Datei-Deskriptor besorgst. Dies machst Du natürlich gerne und rufst socket() auf. Danach sagt Dir der Benutzer, dass Du Dich mit der Adresse „10.12.110.57“ auf dem Port 23 verbinden sollst (dies ist der Standard telnet Port). Woah – Was machst Du jetzt?

Aber Du hast Glück, denn Du kannst einfach den Teil zu connect() hier benutzen. Also hau rein, lies weiter und probier alles zu verstehen.

Der connect() Aufruf sieht wie folgt aus:

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd ist der nette Socket Datei-Deskriptor, den wir ja schon kennen, dieser wurde ja von socket() zurückgegeben. serv_addr_ ist ein struct sockaddr, der die Zieladresse und den Zielport angibt. Und addrlen ist mal wieder die Länge der Server Adresse in Bytes.

Diese ganzen Informationen können wir als Rückgabewert von getaddrinfo() erhalten, was ziemlich cool ist.

Macht das ganze mittlerweile mehr Sinn? Ich kann kann Dich hier leider nicht hören, deswegen hoffe ich es einfach mal und mache mit einem Beispiel weiter, in welchem wir eine Socket-Verbindung zu der Seite „www.example.com“ auf dem Port 3490 aufbauen:

struct addrinfo hints, *res;
int sockfd;

// first, load up address structs with getaddrinfo():

memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

getaddrinfo("www.example.com", "3490", &hints, &res);

// make a socket:

sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

// connect!

connect(sockfd, res->ai_addr, res->ai_addrlen);

Und nur am Rande: Ältere Programme haben ihre eigene struct sockadd_in ausgefüllt und an connect() weitergegeben. Du kannst das natürlich auch machen, wenn Du willst. Dafür kannst Du oben bei dem Teil zu bind() noch mal gucken, wie das gemacht wird.

Außerdem solltest Du nach dem Aufruf von connect() den Rückgabewert prüfen. Ist dieser -1, so ist irgendetwas falsch gelaufen. Auch hier wird die Variable errno wieder auf den Fehlerwert gesetzt.

Zuletzt solltest Du bemerkt haben, dass bind() nicht aufgerufen wurde. Im Prinzip interessiert es uns überhaupt nicht, welche Port Nummer aufgerufen wird, wir wollen halt nur auf einen speziellen Port zugreifen. Der Kernel kümmert sich um alles und wird uns einen freien Port zuweisen.

5.5 listen() – Vielleicht kriege ich ja einen Aufruf?

So, jetzt werden wir mal ein bisschen die Richtung ändern. Was ist, wenn Du auf eingehende Verbindungen warten möchtest um diese dann entsprechend zu verarbeiten? Hierzu nutzen wir zwei Funktionen, erst einmal hören wir zu – listen() – , dann akzeptieren wir das Ganz – accept().

Der Aufruf von listen() ist relativ einfach, aber muss etwas erklärt werden:

int listen(int sockfd, int backlog); 

sockfd ist mal wieder der Socket Datei-Deskriptor aus dem socket()-Aufruf. backlog ist die Anzahl an eingehenden Verbindung in der Warteschleife, die erlaubt werden. Das bedeutet, dass eingehende Verbindungen in einer Warteschleife verweilen, bis sie akzeptiert werden (durch accept() – siehe unten) und hierfür setzt man ein Limit an Verbindungen, die in dieser Warteschleife verweilen dürfen. Die meisten Systeme setzen diesen Wert auf 20, aber Du solltest auch mit 5 oder 10 zurecht kommen.

Wie wir es mittlerweile gewohnt sind, gibt listen() -1 zurück, falls etwas schief gelaufen ist und setzt auch wieder errno auf den Fehlerwert.

Wie Du Dir vermutlich vorstellen kannst, müssen wir erst einmal bind() aufrufen, bevor wir auf einem Port lauschen (listen()) können, ob etwas rein kommt. Also musst Du Deinen Kumpels mitteilen, auf welchen Port sie sich verbinden müssen). Um also eingehende Verbindungen annehmen zu können musst Du die folgende Sequenz an Aufrufen abarbeiten, damit alles funktioniert:

getaddrinfo();
socket();
bind();
listen();
/* accept() kommt hier hin */ 

Ich habe es mir und Dir einfach gemacht und hier lediglich Beispielcode verwendet, da das alles mittlerweile selbsterklärend sein sollte. Keine Bange, der Code, der unten bei accept() benutzt wird, ist ausführlicher. Der wirklich etwas kompliziertere Teil kommt jetzt beim Aufruf von accept().

5.6 accept() – „Vielen Dank für Ihren Anruf“.

Mach Dich bereit – Der Aufruf von accept() ist etwas merkwürdig. Ich erkläre Dir erst einmal, was passieren wird: Jemand, der weit entfernt sitzt, möchte sich mit Deiner Maschine verbinden auf einen gewissen Port verbinden (über den Aufruf connect()). Mittels listen() nimmst Du diese Verbindungsaufnahme wahr. Die Verbindung kommt in die Warteschlange und wartet nun darauf, dass sie akzeptiert wird. Du rufst also accept() auf und sagst der Funktion, dass sie die ausstehende Verbindung aufnehmen soll. Sie wird Dir einen brandneuen Socket Datei-Deskriptor zurückgeben um mit dieser einen Verbindung zu arbeiten. Ganz genau, auf einmal hast Du zwei Socket Datei-Deskriptoren – zum Preis von einem. Der Ursprüngliche wartet weiterhin auf eingehende Verbindungen, wohingegen der neue genutzt werden kann, um Sachen zu verschicken send() und zu erhalten recv(). Endlich, wir sind da!

Der Aufruf ist wie folg:

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

sockfd ist der Socket-Deskriptor, der auf eingehende Verbindungen wartet. Das ist ja noch einfach. addr ist für gewöhnlich ein Zeiger zu dem lokalen struct sockaddr_storage. Hier werden die Informationen über die eingehende Verbindung rein gehen und hiermit kannst Du herausfinden, welcher Host Dich aufruft und über welchen Port dies geschieht. addrlen ist ein lokaler Integer, welcher auf sizeof(struct sockaddr_storage) gesetzt werden sollte, bevor seine Adresse an accept() weitergegeben wird. accept() wird nämlich nicht mehr Bytes in addr reinpacken. Falls weniger reingepackt werden, wird der Wert von addrlen entsprechend verändert.

Und auch accept() gibt wieder -1 zurück, falls etwas schief ging und setzt errno auf den Fehlerwert.

Auch hier wurde wieder sehr viel Neues eingeführt, was man erst einmal verdauen muss. Deswegen gibt es hier ein Codebeispiel, was das Ganze eventuell verdeutlicht:

#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT "3490"  // the port users will be connecting to
#define BACKLOG 10     // how many pending connections queue will hold

int main(void)
{
    struct sockaddr_storage their_addr;
    socklen_t addr_size;
    struct addrinfo hints, *res;
    int sockfd, new_fd;

    // !! don't forget your error checking for these calls !!

    // first, load up address structs with getaddrinfo():

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;  // use IPv4 or IPv6, whichever
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;     // fill in my IP for me

    getaddrinfo(NULL, MYPORT, &hints, &res);

    // make a socket, bind it, and listen on it:

    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    bind(sockfd, res->ai_addr, res->ai_addrlen);
    listen(sockfd, BACKLOG);

    // now accept an incoming connection:

    addr_size = sizeof their_addr;
    new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);

    // ready to communicate on Socket-Deskriptor new_fd!
    .
    .
    .

Es sei angemerkt, dass wir den Socket-Deskriptor new_fd für alle send() und recv() Aufrufe verwenden. Falls Du nur eine einzige Verbindung haben willst, kannst Du mittels close() den wartenden sockfd schließen und damit weitere eintreffende Verbindungen auf dem selben Port abweisen – falls Du das möchtest.

5.7 send() und recv() – Rede mit mir, verdammt!

Diese beiden Funktionen werden für die Kommunikation über die Stream Sockets benutzt, oder aber die verbundenen Datagram Sockets. Wenn Du die regulären nicht-verbundenen Datagram Sockets benutzen willst, solltest Du Dir den Teil zu sendto() und recvfrom() weiter unten angucken.

Fangen wir mir dem send() Aufruf an:

int send(int sockfd, const void *msg, int len, int flags); 

sockfd ist der Socket-Deskriptor, an den Du die Daten schicken willst (ganz gleich, ob das der ist, den Du über socket() erhalten hast, oder aber über accept()). msg ist ein Zeiger auf die Daten, die Du verschicken möchtest und len ist die Länger der Daten in Byte. Du kannst die flags einfach auf 0 setzen (ansonsten kannst Du im Manual mehr zu den Flags erfahren).

Hier ein bisschen Beispielcode:

char *msg = "Toby war hier!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
. 

send() gibt die Anzahl an Bytes zurück, die auch tatsächlich rausgeschickt werden. Es kann sein, dass diese Zahl kleiner ist als die von Dir angegebene Zahl! Das liegt daran, dass Du der Funktion manchmal einfach zu viele Daten gibst, die sie verschicken soll, es aber nicht kann. Es werden dann so viele Daten wie möglich verschickt und die Funktion erwartet von Dir, dass Du den fehlenden Rest dann hinterher schickst. Ist der zurückgegebene Wert von send() ungleich dem Wert in len, liegt es an Dir den Rest des Strings zu verschicken. Die gute Nachricht ist: Wenn das Paket klein ist (kleiner als 1K oder so), wird die Funktion vermutlich in der Lage sein, die Daten in einem Mal zu verschicken. Beachte: Es wird -1 zurückgegeben, falls ein Fehler aufgetreten ist und errno wird auf den Fehlercode gesetzt.

Der recv() Aufruf ist dem send() Aufruf sehr ähnlich:

int recv(int sockfd, void *buf, int len, int flags);

sockfd ist der Socket-Deskriptor von dem Du liest, buf ist der Buffer von dem die Informationen erhältst, len ist die maximale Länge des Buffers und flags kann wieder auf 0 gesetzt werden (Und auch hier kannst Du im Manual nachlesen, welche Flags man setzten kann).

recv() gibt die Anzahl an in den Buffer gelesenen Bytes an, oder aber -1, falls ein Fehler auftrat (errno wie sonst auch).

Achtung! recv() kann aber auch 0 zurückgeben und das kann wiederum nur eines bedeuten: Die andere Seite hat die Verbindung zu Dir geschlossen.

Das war doch wirklich einfach, oder? Du kannst nun also Daten hin und herschieben, ganz einfach über die Stream Sockets.

sendto() und recvfrom() – Das selbe, aber anders

„Okay, das ist jetzt ja alles schön und gut“, „höre ich Dich jetzt sagen, „aber wie mache ich das jetzt bitte mit nicht-verbundenen Datagram Sockets?“. Das ist alles kein Problem, denn ich habe hier genau das richtige für Dich.

Da Datagram Sockets ja nicht mit dem anderen Host verbunden sind, brauchen wir auf jeden Fall erst einmal die Adresse. Das sieht in etwa so aus:

int sendto(int sockfd, const void *msg, int len, unsigned int flags,
           const struct sockaddr *to, socklen_t tolen); 

Wie Du sehen kannst, ist dieser Aufruf im Prinzip genau das selbe wie der Aufruf von send(), nur dass man noch zusätzlich zwei weitere Sachen mit angibt. to ist ein Zeiger auf das struct sockaddr (welches vermutlich entweder ein struct sockaddr_in, oder ein struct sockaddr_in6, bzw. ein struct sockaddr_storage sein wird, welches Du in etwa einer Minute casten wirst), welches die Ziel-IP-Adresse und den Ziel-Port enthält. tolen, ein int wenn man sich das genauer angucken würde, wird einfach auf sizeof *to oder aber sizeof(struct sockaddr_storage) gesetzt.

Um an die Ziel-Adressen-Struktur zu kommen, kannst Du es Dir entweder über getaddrinfo(), oder über recvfrom() holen. Alternativ kannst Du es auch manuell ausfüllen.

Übrigens ist wird genau so wie bei send(), wird die Anzahl an Bytes angegeben, die wirklich verschickt wurden (welches dann auch wieder weniger als die von Dir angegebene Anzahl an Bytes sei kann, die verschickt werden sollten), oder aber -1 bei einem Fehler.

Genauso ähnlich sind auch recv() und recvfrom(). Hier die Übersicht über recvfrom():

int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
             struct sockaddr *from, int *fromlen); 

Auch hier ist es wieder wie bei recv(), nur um ein paar zusätzliche Parameter ergänzt. from ist ein Zeiger auf das lokale struct sockaddr_storage, welches die Adresse und den Port der ursprünglichen Maschine enthält. fromlen ist ein Zeiger auf einen lokalen int, welcher entweder mit sizeof *from, oder aber mit sizeof(struct sockaddr_storage) initialisiert werden sollte. Wenn die Funktion ihren Wert zurückgibt, wird fromlen die Länge der Adresse sein, die in from gespeichert ist.

recvfrom() gibt die Anzahl an erhaltenen Bytes zurück, oder -1 bei einem Fehler (auch hier wird errno wieder entsprechend gesetzt).

Eine Frage habe ich aber noch: Warum benutzten wir struct sockaddr_storage als Socket Type? Warum nicht einfach struct sockaddr_ benutzen? Die Antwort ist, dass wir uns nicht an ein gewisses Protokoll binden wollen, also wir wollen uns nicht festlegen, ob wir jetzt IPv4, oder aber IPv6 verwenden werden. Deswegen benutzen wir das allgemeine struct sockaddr_storage, bei dem wir sicher sein können, dass es groß genug sein wird.

(Ich habe noch eine kniffligere Frage: Warum ist denn bitte struct sockaddr als solches nicht groß genug für egal welche Adresse?) Wir casten ja sogar das allgemein-nutzbare struct sockaddr_storage zu dem allgemein-nutzbaren struct sockaddr! Das ist doch unnötig und sogar redundant, oder? Die Antwort ist, dass es einfach nicht groß genug ist und eine Veränderung eventuell Probleme nach sich ziehen würde. Deswegen hat man einfach ein neues entworfen.)

Du solltest daran denken, dass wenn Du ein Datagram Socket über connect() verbindest, Du einfach send() und recv() für Deine Transfers verwenden kannst. Das Socket an sich ist zwar immer noch ein Datagram Socket und die Pakete benutzen dann immer noch UDP, aber das Socket Interface wird dann automatisch die Ziel- und Quellinformationen für Dich hinzufügen.

5.9 close() und shutdown() – Es wird Zeit die Sache zu beenden!

Wow! Du hast jetzt also den ganzen Tag Daten rumgeschickt (mit send() und recv()) und irgendwann muss man den Spaß ja auch beenden. Du willst nun also die Verbindung zu Deinem Socket-Deskriptor schließen. Das ist einfach. Du kannst einfach die normale Unix Datei-Deskriptor Funktion close() benutzen.

close(sockfd); 

Das verhindert, dass noch irgendwelche Daten über das Socket geschrieben, oder gelesen werden. Jeder der jetzt noch versucht über das Socket zu schreiben, oder zu lesen, wird einen Fehler erhalten.

Falls Du das Schließen des Sockets etwas besser kontrollieren möchtest, kannst Du auch shutdown() aufrufen. Dadurch wird Dir ermöglicht, die Verbindung in eine bestimmte Richtung zu schließen, oder in beide Richtungen (so wie close() es macht). Hier der Aufbau der Funktion:

int shutdown(int sockfd, int how); 

sockfd ist natürlich der Socket Datei-Deskriptor, den du schließen möchtest und how ist einer der folgenden Werte.

0       Empfangen wird verboten

1       Senden wird verboten

2       Empfangen und Senden wird verboten (wie close())

shutdown() gibt 0 zurück, falls alles gut ging, falls etwas schief lief wird -1 zurück gesetzt – und ja, auch hier wird wieder errno entsprechend gesetzt.

Wenn Du shutdown() für nicht-verbundene Datagram Sockets benutzen willst, wird einfach das Socket deaktiviert, sodass alle weiteren send() und recv() Aufrufe nicht weiter durchkommen (zur Erinnerung: Du konntest über connect() Deine Datagram Sockets verbinden).

Beachte: shutdown() schließt den Datei-Deskriptor nicht, es macht diesen nur unbrauchbar. Um den Socket-Deskriptor frei zu machen muss Du immer noch close() aufrufen.

Mehr kann man dazu aber auch nicht sagen.

(Außer, dass wenn du Windows und Winsock benutzt du closesocket() anstelle von clos() aufrufen musst).

5.10 getpeername() – Wer bist Du eigentlich?

Fie Funktion ist so einfach.

Im Ernst, die Funktion ist so einfach, ich hätte sie beinahe nur beiläufig erwähnt. Ich habe mich anders entschieden und hier ist sie.

Die Funktion getpeername() wird Dir sagen, wer an dem anderen Ender der Verbindung sitzt. Zusammenfassung:

#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *addr, int *addrlen); 

sockfd ist der Deskriptor des verbunden Stream Sockets, addr ist ein Zeiger auf das struct sockaddr (oder struct sockaddr_in), das die Informationen über die andere Seite der Verbindung enthält und addrlen ist ein Zeiger auf ein int, welches mit sizeof *addr oder sizeof(struct sockaddr) initialisiert werden sollte.

Die Funktion gibt auch wieder -1 zurück, falls ein Fehler auftreten sollte und setzt auch wieder errno.

Wenn Du erst die Adresse hast, kannst Du über inet_ntop(), getnameinfo(), oder gethostbyaddr() mehr Informationen bekommen. Nein, Du kannst nicht deren Login-Namen auslesen. (Ok, ok. Wenn der andere Computer einen Ident-Daemon laufen hat, ist dies möglich. Aber das geht für dieses Dokument zu weit. Falls Du mehr wissen willst, solltest Du Dir mal RFC 1413 angucken).

5.11 gethostname() – Wer bin ich?

Sogar noch einfacher als getpeername() ist die Funktion gethostname(). Sie gibt den Namen des Computers aus, auf dem Dein Programm gerade läuft. Der Name kann dann von gethostbyname() benutzt werden um die IP Adresse Deiner lokalen Maschine zu bekommen.

Was könnte denn bitte lustiger sein? Okay, mir fall ein paar Dinge ein, aber die haben jetzt nicht wirklich was mit der Socket Programmierung zu tun. Egal, hier ist die Zusammenfassung:

#include <unistd.h>

int gethostname(char *hostname, size_t size); 

Die Argumente sind einfach: hostname ist ein Zeiger auf ein Array von chars, welche den Hostnamen bei Ablauf der Funktion enthalten werden und size ist die Länge des hostname Arrays – in Bytes.

Die Funktion gibt 0 zurück, falls sie erfolgreich war, ansonsten wird -1 zurückgegeben und errno entsprechend gesetzt, also alles so wie gehabt.

6 Client-Server Hintergrund

Wir leben in einer Welt, die aus Clients und Servern besteht. So ziemlich alles in einem Netzwerk hat mit Client-Prozessen zu tun, die von Server-Prozessen bearbeitet werden, und umgekehrt. Nehmen wir unseren alten Bekannte, Telnet.Wenn Du Dich mit einem anderen Host verbindest, auf Port 23, erweckst Du damit das Programm, das auf dem Server läuft zum Leben. Es bearbeitet dann Deine eingehende Telnet-Verbindung und gibt Dir dann entsprechend Daten aus. Zum Beispiel ein Login-Fenster.

Austausch zwischen Client und Server
Austausch zwischen Client und Server

Der Austausch zwischen dem Client und dem Server wird in obigem Bild veranschaulicht.

Beachte, dass sowohl SOCK_STREAM, SOCK_DGRAM, oder sonst was für den Austausch verwendet werden kann, solange beide das selbe benutzen. Einige gute Beispiele für den Austausch von Client und Server sind Telnet/Telnetd, ftp/ftpd, oder Firefox/Apache. Jedes mal, wenn Du ftp benutzt, antwortet ein Serverprogramm ftpd, das Deine Anfragen beantwortet.

Meistens ist nur ein Server auf einer Maschine installiert, der dann mehrere Anfragen gleichzeitig beantwortet, indem er fork() benutzt. Vereinfach funktioniert das so: Der Server wartet auf eine eingehende Verbindung, akzeptiert diese mittels accept() und dann leitet er sie über fork() an einen Kind-Prozess weiter, welcher die Anfrage eigenständig weiterverarbeiten kann. Dies wird unser Beispielserver im nächsten Teil machen.

6.1 Ein einfacher Stream Server

Dieser Server ist denkbar einfach. Er schickt lediglich den String „Hello World!\n“ über ein Stream Socket. Du musst den Code lediglich in einem Fenster laufen lassen, und Dich in einem anderen über Telnet damit verbinden. Das geht so:

$ telnet remotehostname 3490

Hierbei ist remotehostname der Name der Maschine, auf der Du den Code laufen lässt.

/* Code einfügen */

/*
** server.c -- a stream socket server demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT "3490"  // the port users will be connecting to

#define BACKLOG 10     // how many pending connections queue will hold

void sigchld_handler(int s)
{
    while(waitpid(-1, NULL, WNOHANG) > 0);
}

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
    struct addrinfo hints, *servinfo, *p;
    struct sockaddr_storage their_addr; // connector's address information
    socklen_t sin_size;
    struct sigaction sa;
    int yes=1;
    char s[INET6_ADDRSTRLEN];
    int rv;

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE; // use my IP

    if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and bind to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("server: socket");
            continue;
        }

        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
                sizeof(int)) == -1) {
            perror("setsockopt");
            exit(1);
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("server: bind");
            continue;
        }

        break;
    }

    if (p == NULL)  {
        fprintf(stderr, "server: failed to bind\n");
        return 2;
    }

    freeaddrinfo(servinfo); // all done with this structure

    if (listen(sockfd, BACKLOG) == -1) {
        perror("listen");
        exit(1);
    }

    sa.sa_handler = sigchld_handler; // reap all dead processes
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    printf("server: waiting for connections...\n");

    while(1) {  // main accept() loop
        sin_size = sizeof their_addr;
        new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
        if (new_fd == -1) {
            perror("accept");
            continue;
        }

        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s);
        printf("server: got connection from %s\n", s);

        if (!fork()) { // this is the child process
            close(sockfd); // child doesn't need the listener
            if (send(new_fd, "Hello, world!", 13, 0) == -1)
                perror("send");
            close(new_fd);
            exit(0);
        }
        close(new_fd);  // parent doesn't need this
    }

    return 0;
}

Falls Du neugierig bist. Ich habe den Code in eine große main() Funktion gepackt, damit es syntaktisch einfacher ist. Du kannst den Code gerne aufteilen und in spezielle Funktionen packen, wenn es Dich glücklicher macht.

Zudem ist der Aufruf von sigaction() neu. Der Code wird benutzt um Prozesse zu beenden, die nutzlos sind, nachdem ein Kind-Prozess (erstellt über fork()) beendet wurde. Wenn Du diese Zombie-Prozesse nicht beendest und den Platz wieder frei machst, bekommst Du Probleme mit Deinem System-Administrator.

Die Daten erhältst Du im nächsten Abschnitt.

6.2 Ein einfacher Stream Client

Dieses Programm ist sogar noch einfacher als der Server. Der Client verbindet sich mit dem von Dir angegebenen Server, Port 3490. Danach erhält es den String, den der Server Dir schickt.

/* Code einfügen */

/*
** client.c -- a stream socket client demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

#include <arpa/inet.h>

#define PORT "3490" // the port client will be connecting to 

#define MAXDATASIZE 100 // max number of bytes we can get at once 

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
    int sockfd, numbytes;  
    char buf[MAXDATASIZE];
    struct addrinfo hints, *servinfo, *p;
    int rv;
    char s[INET6_ADDRSTRLEN];

    if (argc != 2) {
        fprintf(stderr,"usage: client hostname\n");
        exit(1);
    }

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and connect to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("client: socket");
            continue;
        }

        if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("client: connect");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "client: failed to connect\n");
        return 2;
    }

    inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr),
            s, sizeof s);
    printf("client: connecting to %s\n", s);

    freeaddrinfo(servinfo); // all done with this structure

    if ((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
        perror("recv");
        exit(1);
    }

    buf[numbytes] = '\0';

    printf("client: received '%s'\n",buf);

    close(sockfd);

    return 0;
}

Du musst den Server starten, bevor sich der Client verbindet, ansonsten kommt die Fehlermeldung „Connection refused“.

Datagram Sockets

Wir haben bisher ja schon die Grundzüge von UDP Datagram Sockets kennen gelernt, als wir uns sendto() und recvfrom() angeguckt haben, deswegen werde ich jetzt einfach ein paar Beispielprogramme vorstellen: talker.c und listener.c.

listener sitzt auf einer Maschine und erwartet eingehende Pakete auf dem Port 4950. talker schickt diese Pakete zu dem Port der Maschine, die durch den Benutzer in der Kommandozeile eingegeben wurde.

Hier der Code für listener.c

/*
** listener.c -- a datagram sockets "server" demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define MYPORT "4950"    // Der Port auf den sich die Benutzer verbinden werden

#define MAXBUFLEN 100

// Hole sockaddr, IPv4 oder IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;
    struct sockaddr_storage their_addr;
    char buf[MAXBUFLEN];
    socklen_t addr_len;
    char s[INET6_ADDRSTRLEN];

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC; // Auf AF_INET setzen, um IPv4 zu erzwingen
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_flags = AI_PASSIVE; // Benutze meine IP

    if ((rv = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // loop through all the results and bind to the first we can
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("listener: socket");
            continue;
        }

        if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
            close(sockfd);
            perror("listener: bind");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "listener: failed to bind socket\n");
        return 2;
    }

    freeaddrinfo(servinfo);

    printf("listener: waiting to recvfrom...\n");

    addr_len = sizeof their_addr;
    if ((numbytes = recvfrom(sockfd, buf, MAXBUFLEN-1 , 0,
        (struct sockaddr *)&their_addr, &addr_len)) == -1) {
        perror("recvfrom");
        exit(1);
    }

    printf("listener: got packet from %s\n",
        inet_ntop(their_addr.ss_family,
            get_in_addr((struct sockaddr *)&their_addr),
            s, sizeof s));
    printf("listener: packet is %d bytes long\n", numbytes);
    buf[numbytes] = '\0';
    printf("listener: packet contains \"%s\"\n", buf);

    close(sockfd);

    return 0;
}

Du siehst, dass wir in unserem Aufruf von getaddrinfo() endlich SOCK_DGRAM benutzen. Zudem müssen wir nicht zuhören (listen()) und akzeptieren (accept()), das ist einer der Vorteile von verbindungslosen Datagram Sockets.

Jetzt gucken wir uns mal talker.c an.

/*
** talker.c -- a datagram "client" demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define SERVERPORT "4950"    // Der Port, mit dem sich die Nutzer verbinden werden

int main(int argc, char *argv[])
{
    int sockfd;
    struct addrinfo hints, *servinfo, *p;
    int rv;
    int numbytes;

    if (argc != 3) {
        fprintf(stderr,"usage: talker hostname message\n");
        exit(1);
    }

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;

    if ((rv = getaddrinfo(argv[1], SERVERPORT, &hints, &servinfo)) != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
        return 1;
    }

    // Gehe durch alle Ergebnisse und erstelle ein Socket
    for(p = servinfo; p != NULL; p = p->ai_next) {
        if ((sockfd = socket(p->ai_family, p->ai_socktype,
                p->ai_protocol)) == -1) {
            perror("talker: socket");
            continue;
        }

        break;
    }

    if (p == NULL) {
        fprintf(stderr, "talker: failed to bind socket\n");
        return 2;
    }

    if ((numbytes = sendto(sockfd, argv[2], strlen(argv[2]), 0,
             p->ai_addr, p->ai_addrlen)) == -1) {
        perror("talker: sendto");
        exit(1);
    }

    freeaddrinfo(servinfo);

    printf("talker: sent %d bytes to %s\n", numbytes, argv[1]);
    close(sockfd);

    return 0;
}

Und das ist alles, was man wissen muss. Lass listener auf einer Maschine laufen, dann starte talker auf einer anderen. Sieh den beiden beim kommunizieren zu und hab Spaß dabei.

Dieses Mal musst Du den Server auf gar nicht laufen lassen. talker kann auch einfach so funktionieren, es feuert halt einfach freudig die Pakete in den Äther, wo sie dann verschwinden, wenn niemand auf der anderen Seite diese entgegennimmt (recvfrom()). Zur Erinnerung: Daten, die über UDP Datagram Sockets verschickt werden, müssen nicht immer ankommen. Es gibt eine Ausnahme, die ich jetzt schon ein paar Mal angesprochen habe. Die verbundenen Datagram Sockets. Nehmen wir an, dass talker connect() aufruft und die Adresse von listener angibt. Von nun an kann talker nur noch mit der dort angegebenen Adresse kommunizieren. Daher musst Du auch nicht sendto() und recvfrom() benutzen, sondern kannst einfach send() und recv() verwenden.

7 Fortgeschrittene Techniken

Zugegeben, das hier sind nicht allzu fortgeschrittene Techniken, aber sie gehen über das Niveau einer Einführung hinaus, wie wir sie in den vorherigen Kapiteln vermittelt haben. Wenn Du bis hierher gekommen bist und alles einigermaßen verstanden hast, dann bist Du definitiv in der Lage, eigenständig Programme zu schreiben, die Daten über Netzwerke transportieren. Herzlichen Glückwunsch. Ab jetzt werden wir uns einige Sachen angucken, die über das Grundverständnis hinaus gehen – wir tauchen in eine neue Welt ein und erforschen diese.

7.1 Blockieren

Blockieren. Warum reden wir hier bitte über’s Blockieren und was ist damit überhaupt gemeint? Das Blockieren ist eigentlich nur ein anderes Wort für Schlafen und wird gerne von Programmierern verwendet um klug zu klingen. Nehmen wir das Programm listener von oben; Dir ist bestimmt aufgefallen, dass es einfach nur im Speicher hängt und darauf wartet, dass Pakete ankommen. Dadurch, dass die Funktion recvfrom() aufgerufen wurde und keine Daten angekommen sind, wurde der Funktion gesagt, dass sie blockieren soll (also schlafen), bis irgendwelche Daten ankommen.

Viele der Funktionen blockieren. accept() blockier. Alle erhaltenden recv() Funktionen blockieren. Die Funktionen dürfen dies, da bei der Erstellung des Socket-Deskriptoren über socket(), der Kernel diesen auf Blockieren gesetzt hat. Möchtest Du nicht, dass der Socket blockiert, dann musst Du fcntl() aufrufen:

#include <unistd.h>
#include <fcntl.h>
.
.
.
sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
.
.
. 

Wenn man ein Socket auf nicht-blockieren setzt, kannst Du das Socket aus dem Socket Informationen holen. Möchtest Du also von einem nicht-blockierenden Socket etwas lesen und es sind keine Daten vorhanden, dann kann es nicht blockieren – es wird -1 zurückgegeben und errno wird auf EWOULDBLOCK gesetzt.

Im Allgemeinen ist es aber keine gute Idee, Informationen so aus dem Socket zu holen. Wenn Du regelmäßig Dein Socket abfragst, ob denn nun etwas da ist, wirst Du unnötig CPU Zeit nehmen. Ein viel schönerer Ansatz ist der Folgende, der mittels select() funktioniert.

7.2 select() – Synchrones Multiplexing

Diese Funktion ist etwas merkwürdig, aber nichts desto trotz sehr nützlich. Stellen wir uns vor wir sind ein Server und wir wollen auf eingehende Verbindungen warten und gleichzeitig von bereits bestehenden Verbindungen lesen.

Kein Problem, wirst Du sagen, man muss einfach ein paar mal accept() und recv() aufrufen. Aber ganz so einfach ist das nicht? Was ist, wenn du den accept() Aufruf blockierst? Wie kannst Du dann gleichzeitig über recv() Daten empfangen? „Benutz halt einfach nicht-blockierende Socket!“ Auf keinen Fall, Du möchtest doch nicht unnötige CPU Zeit verschwenden. „Und wie soll man das dann bitte hinbekommen?“

select() gibt Dir die Möglichkeit, mehrere Sockets auf einmal zu kontrollieren. Es sagt Dir, welche Du lesen kannst und auch in welche Du was schreiben kannst, aber auch, welche Fehler (Exceptions) geworfen haben, falls Dich das interessiert.

Leider ist select(), wenn auch portabel, eine der langsamsten Methoden, um Sockets zu überwachen. Eine mögliche Alternative ist libevent, oder etwas ähnliches.

Ohne lange Reden halten zu wollen, hier die Beschreibung von select():

#include <sys/time.h>
#include <sys/types.h>
#include

int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

Die Funktion überwacht eine Menge von Datei-Deskriptoren; im Besonderen readfds, writefds und exceptfds. Wenn Du gucken möchtest, ob Du von der Standardeingabe und irgendeinem Socket-Deskriptor, sockfd, lesen kannst, dann kannst Du das erreichen, indem Du 0 und sockfd zu der Menge readfds hinzufügst. Der Parameter numfds sollte auf den Wert des höchsten Datei-Deskriptors + 1 gesetzt werden. In diesem Beispiel sollte er demnach auf sockfd+1 gesetzt werden, da er mit Sicherheit größer ist, als die Standardeingabe (0).

Wenn select() fertig ist, wird readfs so verändert worden sein, dass man erfahren kann, welcher der von Dir angegebenen Datei-Deskriptoren fertig ist, um gelesen zu werden. Dies kann man mit dem Makro FD_ISSET() überprüfen.

Bevor wir jetzt weiter machen, möchte ich eben zeigen, wie man diese Mengen verändern kann. Jede Menge hat den Typ fd_set. Die folgenden Makros arbeiten auf diesem Typ.

FD_SET(int fd, fd_set *set);     Add fd to the set.

FD_CLR(int fd, fd_set *set);     Remove fd from the set.

FD_ISSET(int fd, fd_set *set);   Return true if fd is in the set.

FD_ZERO(fd_set *set);            Clear all entries from the set.

Was soll denn eigentlich dieses komische struct timeval sein? Nun ja, manchmal möchte man nicht unendlich lange warten, bis dir jemand etwas zuschickt. Vielleicht möchtest Du auch alle 96 Sekunden „Läuft noch…“ im Terminal ausgeben, auch wenn noch nichts passiert ist. Diese timeval Struktur gibt Dir die Möglichkeit, eine Auszeit festzulegen. Falls die Zeit abgelaufen ist und select() immer noch keine fertigen Datei-Deskriptoren gefunden hat, wird es returnen, damit Du die Möglichkeit hast, weiter zu machen.

struct timeval hat die folgenden Felder:

struct timeval {
    int tv_sec;     // seconds
    int tv_usec;    // microseconds
};

Du kannst einfach tv_sec auf die Sekunden stellen, die Du warten möchtest, und tv_usec auf die Mikrosekunden, die Du warten möchtest. Jo, das hast Du richtig gelesen. Da steht tatsächlich Mikrosekunden, und nicht Millisekunden. Eine Sekunde hat 1000 Millisekunden und eine Millisekunde hat 1000 Mikrosekunde. Somit hat eine Sekunde 1.000.000 Mikrosekunden. Und warum heißt es „usec“? Das „u“ steht für den griechischen Buchstaben µ (My), das im Allgemeinen für „Mikro“ steht. Außerdem sollte noch erwähnt werden, dass beim Zurückgeben der Funktion, timeout nur eventuell erneuert wird, sodass es die übrige Zeit angibt. Das hängt leider von dem jeweiligen Unix ab, auf dem Du Dein Programm laufen lässt.

Strike, wir haben einen mikrosekunden-genauen Timer. Schade ist, dass Du Dich nicht wirklich darauf verlassen solltest. Du musst wahrscheinlich ein bisschen auf Dein Unix warten, egal wie klein Du Dein struct timeval einstellst.

Aber kommen wir zu anderen interessanten Sachen: Wenn Du die Felder in Deinem struct timeval auf 0 setzt, wird select() direkt ablaufen. Wenn Du den Parameter timeout jedoch auf NULL steht, wird es nie ablaufen und Du wirst warten müssen, bis Dein erster Datei-Deskriptor fertig ist. Wenn es Dir also egal ist, wie lange Du warten musst, kannst Du beim Aufruft von select timeval also einfach auf NULL setzen.

Das folgende kleine Snippet wartet 2,5 Sekunden, bis etwas im Standard-Input passiert:

/*
** select.c -- a select() demo
*/

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#define STDIN 0  // Der File Deskriptor für den Standard-Input

int main(void)
{
    struct timeval tv;
    fd_set readfds;

    tv.tv_sec = 2;
    tv.tv_usec = 500000;

    FD_ZERO(&readfds);
    FD_SET(STDIN, &readfds);

    // Um writefds und exceptfds kümmern wir uns nicht:
    select(STDIN+1, &readfds, NULL, NULL, &tv);

    if (FD_ISSET(STDIN, &readfds))
        printf("A key was pressed!\n");
    else
        printf("Timed out.\n");

    return 0;
} 

Wenn Du ein Terminal benutzt, welches pro Zeile buffert (im Englischen ist es eventuell verständlicher: Line buffered Terminal), musst Du die Taste RETURN klicken, ansonsten wird die Zeit einfach so ablaufen.

Nun denken Einige von Euch bestimmt, dass das eine gute Möglichkeit sein könnte, um auf Daten eines Datagram Sockets zu warten – richtig: es könnte so sein. Das Problem dabei ist, dass einige Unix-Betriebssysteme select() so nutzen können, andere aber wiederum nicht. Falls Du es wirklich ausprobieren möchtest, musst Du wohl oder übel in Deine lokalen Manual-Seiten gucken.

Einige Unix-Betriebssysteme erneuern die Zeit in Deinem struct timeval, um zu zeigen, wie viel Zeit noch übrig bleibt, bis der Timer aufläuft. Andere machen das nicht. Du solltst Dich also auch darauf nicht verlassen, wenn Du portablen Code schreiben möchtest. (Du solltest gettimeofday() benutzen, wenn Du die vergangene Zeit aufzeichnen möchtest. Das ist nicht schön, aber so ist es nun mal).

Was passiert eigentlich, wenn ein Socket, das gelesen werden kann, die Verbindung schließt? In dem Fall wird select() den Socket-Deskriptor als „bereit zum Lesen“ setzen. Wenn Du dann wirklich recv() darauf aufrufen willst, wird recv() 0 zurückgeben. Somit weißt Du dann, dass die Verbindung geschlossen wurde.

Eine interessante Sache ist, dass Du bei einem Socket, welches gerade auf eingehende Verbindung wartet (also listen()), einfach den Datei-Deskriptor in die readfds Menge packen kannst, um zu neue Verbindungen überprüfen zu können.

Und das, meine Freunde, ist ein grober Überblick über die extrem starke select() Funktion.

Da mich aber viele darum baten, habe ich hier ein tiefer gehendes Beispiel. Leider ist der Unterschied zwischen dem ziemlich leichten Beispiel da oben und dem Beispiel hier, verdammt groß. Aber guck’s dir mal an und lies dann die Beschreibung, die unten folgt.

/*
** selectserver.c -- Der etwas andere Multiuser Chat Server
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define PORT "9034"   // Der Port, auf dem wir arbeiten wollen

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

int main(void)
{
    fd_set master;    // Die Master File Deskriptoren Liste
    fd_set read_fds;  // Temporäre File Deskriptor Liste für select()
    int fdmax;        // Die maximale Anzahl an File Deskriptoren

    int listener;     // Listening Socket Deskriptor
    int newfd;        // Neuester akzeptierter Socket Deskriptor
    struct sockaddr_storage remoteaddr; // Client Adresse
    socklen_t addrlen;

    char buf[256];    // Buffer für die Client Daten
    int nbytes;

    char remoteIP[INET6_ADDRSTRLEN];

    int yes=1;        // für setsockopt() SO_REUSEADDR, siehe unten
    int i, j, rv;

    struct addrinfo hints, *ai, *p;

    FD_ZERO(&master);    // Platz für die Master- und Temporären Mengen machen
    FD_ZERO(&read_fds);

    // Hole ein Socket und binde es
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    if ((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0) {
        fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
        exit(1);
    }
    
    for(p = ai; p != NULL; p = p->ai_next) {
        listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (listener < 0) { 
            continue;
        }
        
        setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));

        if (bind(listener, p->ai_addr, p->ai_addrlen) < 0) {
            close(listener);
            continue;
        }

        break;
    }

    // Falls wir hier ankommen, bedeutet das, dass das Binden fehlgeschlagen ist
    if (p == NULL) {
        fprintf(stderr, "selectserver: failed to bind\n");
        exit(2);
    }

    freeaddrinfo(ai); // all done with this

    // Höre
    if (listen(listener, 10) == -1) {
        perror("listen");
        exit(3);
    }

    // Füge den Listener zum Master Set hinzu
    FD_SET(listener, &master);

    // Merke Dir den größten File Deskriptor
    fdmax = listener; // Bis jetzt ist es dieser hier

    // main loop
    for(;;) {
        read_fds = master; // Kopieren
        if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
            perror("select");
            exit(4);
        }

        // Gehe durch die aktuellen Verbindungen und sieh nach, ob es Daten gibt, die man lesen kann
        for(i = 0; i <= fdmax; i++) {
            if (FD_ISSET(i, &read_fds)) { // Es gibt Einen!
                if (i == listener) {
                    // Verwalte die Verbindungen
                    addrlen = sizeof remoteaddr;
                    newfd = accept(listener,
                        (struct sockaddr *)&remoteaddr,
                        &addrlen);

                    if (newfd == -1) {
                        perror("accept");
                    } else {
                        FD_SET(newfd, &master); // Zum Master Set hinzufügen
                        if (newfd > fdmax) {    // Weiterhin gucken, welches das größte ist.
                            fdmax = newfd;
                        }
                        printf("selectserver: new connection from %s on "
                            "socket %d\n",
                            inet_ntop(remoteaddr.ss_family,
                                get_in_addr((struct sockaddr*)&remoteaddr),
                                remoteIP, INET6_ADDRSTRLEN),
                            newfd);
                    }
                } else {
                    // Behandle die Daten vom Client
                    if ((nbytes = recv(i, buf, sizeof buf, 0)) <= 0) {
                        // Ist ein Fehler aufgetreten, oder wurde die Verbindung vom Client beendet
                        if (nbytes == 0) {
                            // Die Verbindung wurde geschlossen
                            printf("selectserver: socket %d hung up\n", i);
                        } else {
                            perror("recv");
                        }
                        close(i); // bye!
                        FD_CLR(i, &master); // remove from master set
                    } else {
                        // Wir haben Daten vom Client bekommen
                        for(j = 0; j <= fdmax; j++) {
                            // Schick's an alle
                            if (FD_ISSET(j, &master)) {
                                // Außer die Listener und uns selber
                                if (j != listener && j != i) {
                                    if (send(j, buf, nbytes, 0) == -1) {
                                        perror("send");
                                    }
                                }
                            }
                        }
                    }
                } // END Datenbehandlung vom Nutzer
            } // END neue Verbindung bekommen
        } // END Durchlaufen der File Deskriptoren
    } // END for(;;)--Und Du dachtest vermutlich schon, dass es nie enden wird.!
    
    return 0;
}

Ich habe zwei Datei-Deskriptor-Mengen im Code: master und read_fds. Die erste, master, enthält all die Socket-Deskriptoren, die zur Zeit verbunden sind, aber auch die, die auf neue Verbindungen warten.

Der Grund für die master-Menge ist, dass select() die Menge, die Du an die Funktion weitergibst, verändert, um aussagen zu können, welche der Sockets gelesen werden können. Da ich mir aber die Verbindungen zwischen den verschiedenen select()-Aufrufen merken muss, muss ich diese irgendwo sicher aufbewahren. Erst kurz vor dem Aufruf kopiere ich den Inhalt von master in read_fds.

Heißt das dann nicht, dass ich jedes Mal, wenn ich eine neue Verbindung bekomme, diese dann in die master Menge packen muss? Stimmt! Und jedes Mal, wenn eine Verbindung geschlossen wird, muss ich diese dann auch wieder aus der master Menge schmeißen? Ja, musst Du.

Beachte, dass ich checke, ab wann das listener-Socket gelesen werden kann. Wenn dies möglich ist, heißt das, dass eine neue Verbindung darauf wartet, akzeptiert zu werden. Dies mache ich durch den Aufruf von accept() und füge sie zur master-Menge hinzu. Genau das selbe passiert, wenn die Verbindung zu einem Client gelesen werden kann und recv() 0 zurück gibt; dann weiß ich, dass der Client die Verbindung geschlossen hat und ich muss sie aus der Menge master entfernen.

Falls recv() jedoch nicht 0 zurück gibt, weiß ich, dass Daten übertragen wurden. Diese lese ich natürlich ein und gehe durch die master Liste und sende die Daten an alle verbleibenden Clients.

Und das, mein Freund, war eine gar nicht mal so kleine Zusammenfassung der mächtigen select() Funktion.

Falls Du das Konzept interessant findest, dann kannst Du Dir auch gerne mal die Funktion poll() angucken. Diese Funktion hat eine große Ähnlichkeit mit select(), aber verwaltet die Datei-Deskriptoren anders.

7.3 Partielle send()s – Wie gehe ich damit um?

In dem Teil zu send() haben wir ja festgestellt, dass send() nicht unbedingt alle Bytes verschickt, wie Du ihr gegeben hast. Beispielsweise möchtest Du 512 Byte verschicken, aber die Funktion gibt Dir den Wert 412 zurück. Was ist mit den restlichen 100 Byte passiert?

Nun ja, die sitzen immer noch im Buffer und warten darauf verschickt zu werden. Der Kernel hat sich dazu entschieden, nicht alle Daten zu verschicken und glaub mir, das kannst Du leider nicht beeinflussen. Du kannst Dich aber darum kümmern, die restlichen Daten zu verschicken.

Du könntest also eine Funktion schreiben, die wie folgt aussieht.

#include <sys/types.h>
#include <sys/socket.h>

int sendall(int s, char *buf, int *len)
{
    int total = 0;        // how many bytes we've sent
    int bytesleft = *len; // how many we have left to send
    int n;

    while(total < *len) {
        n = send(s, buf+total, bytesleft, 0);
        if (n == -1) { break; }
        total += n;
        bytesleft -= n;
    }

    *len = total; // return number actually sent here

    return n==-1?-1:0; // return -1 on failure, 0 on success
} 

In diesem Beispiel ist s das Socket an das Du die Daten schicken willst, buf ist der Buffer, der die Daten enthält und len ist ein Zeiger auf ein int, der die Anzahl an Byte im Buffer enthält.

Die Funktion gibt -1 zurück, falls ein Fehler auftritt (errno hat immer noch den Wert vom send() Aufruf). Zudem wird die Anzahl an tatsächlich geschickten Byte in len gespeichert. Das ist dann also die selbe Zahl, wie die Zahl, die Du der Funktion übergeben hast, es sei denn, es ist ein Fehler aufgetreten. sendall() wird probieren, die Daten so gut wie möglich raus zu schicken, aber falls ein Fehler auftritt, kannst Du dich da direkt drum kümmern.

Der Vollständigkeit halber, habe ich einen Beispielaufruf mit eingefügt:

char buf[10] = "Beej!";
int len;

len = strlen(buf);
if (sendall(s, buf, &len) == -1) {
    perror("sendall");
    printf("We only sent %d bytes because of the error!\n", len);

Aber was passiert auf der Seite des Empfangenden, wenn nur der Teil eines Pakets ankommt? Wenn die Pakete unterschiedlich lang sind, woher weiß dann der Empfänger, wo das eine Paket aufhört und das nächste beginnt? Jup, diese Probleme, die im wirklichen Leben auftauchen sind wirklich ätzend. Du wirst diese Daten wohl kapseln müssen (Du erinnerst Dich doch noch an das Kapitel zu Datenkapselung, das wir ganz am Anfang besprochen haben, oder?). Im nächsten Abschnitt wird dies genauer erklärt.

7.4 Serialisierung – Wie man die Daten verpackt

Es ist wirklich einfach, Textdateien über Netzwerke zu verschicken, wie Du vielleicht schon mitbekommen hast. Aber was passiert, wenn Du Binärdaten wie ints oder floats verschicken möchtest? Du hast verschiedene Möglichkeiten, dies zu bewältigen.

  1. Wandle die Zahl in Text um, mit ein Funktion wie sprintf() und verschicke den Text. Der Empfänger wird den Text wieder in eine Zahl umwandeln, indem er Funktionen wie strtol() benutzt.
  2. Verschicke die Daten einfach so, wie sie sind, indem Du einen Zeiger auf die Daten an die Funktion send() übergibst.
  3. Kodiere die Zahl in eine portable binäre Form. Der Receiver kann diese dann decoden

Eine kleine Vorstellung, nur heute Abend in einem Theater Ihrer Wahl.

[Der Vorhang geht auf]
Ich sage: „Ich bevorzuge die dritte Methode!“
[Ende]

Bevor ich jetzt gleich wieder Ernst werde, möchte ich Dir sagen, dass es da draußen auch Bibliotheken gibt, die das für Dich übernehmen können und Du vermutlich echte Schwierigkeiten haben wirst, Deine eigene Bibliothek portabel und vor allem fehlerfrei zu halten. Also mach Deine Hausaufgaben und guck Dich um, bevor Du Dich hinsetzt und Deine eigene Implementierung schreibst. Ich habe diese Infos hier mit rein gepackt, falls es Leute interessiert, wie so etwas implementiert wird.

Ich sollte erwähnen, dass jede der oben vorgestellten Methoden seine Vor- und Nachteile hat, aber wie ich schon gesagt hatte, bevorzuge ich im Allgemeinen die dritte Methode. Ich möchte jetzt aber damit anfangen, die Vor- und Nachteile der beiden anderen Methoden zu besprechen.

Die erste Methode, hat natürlich den Vorteil, dass Du die Daten die Du verschickst und liest, direkt ausgeben kannst. Denn manchmal ist ein für Menschen verständliches (also lesbares) Protokoll wie IRC extrem praktisch. Jedoch ist die Konvertierung langsam und das Ergebnis nimmt fast immer mehr Speicherplatz weg, als die ursprüngliche Zahl.

Die zweite Methode: Die Daten einfach so verschicken. Das ist auf jeden Fall die einfachste Möglichkeit, um Daten zu verschicken, aber auch gefährlich. Man nimmt halt einfach einen Zeiger auf die Daten und rufen send() damit auf.

double d = 3490.15926535;

send(s, &d, sizeof d, 0);  /* DANGER--non-portable! */

Und der Empfänger nimmt die Daten so an.

double d;

recv(s, &d, sizeof d, 0);  /* DANGER--non-portable! */

Schnell und einfach – was kann man daran denn bitte nicht mögen? Nun ja, es ist nun mal so, dass nicht alle Systemearchitekturen double Literale (oder aber auch int) in der selben Bit-Reihenfolge speichern. Wir hatten es ja bereits vom Little- und Big-Endian. Der Code ist somit leider nicht portabel, aber manchmal muss er das auch gar nicht sein und dann ist das ein wirklich schöner, einfacher und vor allem schneller Weg.

Wenn man Integer Zahlen verpackt, haben wir ja bereits die htons() Funktionen betrachtet, welche uns helfen, unsere Programme portabel zu schreiben, indem die Zahlen in die Network Byte Order gebracht werden und warum man das machen sollte. Leider gibt es keine entsprechenden Funktionen für float Zahlen. Gibt es denn keine Möglichkeit, das noch irgendwie zu retten?

Keine Bange, das kriegen wir schon hin. Wir packen die Daten in ein bekanntes Binärformat, was man übrigens auch als serialisieren bezeichnet und der Empfänger kann diese Daten dann wieder entpacken.

Aber was soll denn dieses bekannte Binärformat sein? Wir haben ja bereits das htons() Beispiel gesehen. Hier wird eine Zahl von der Host Byte Order in die Network Byte Order gebracht und um dies wieder rückgängig zu machen wird auf der Empfängerseite ntohs() aufgerufen.

Aber habe ich nicht gerade gesagt, dass es keine Funktion gibt, um Nicht-Integer Zahlen umzuwandeln? Jo, habe ich. Und da es leider keinen Standardweg in C gibt, muss man sich hier etwas eigenes überlegen.

Wir werden nun also die Daten in ein bekanntest Format packen und dieses über’s Netz verschicken, sodass es dann auf der Gegenseite dekodiert wird. Hier ist ein Beispiel, wie wir Floats verpacken können, auch wenn dieses Beispiel hier nur den Grundgedanken liefern soll und an vielen Stellen verbessert werden könnte.

#include <stdint.h>

uint32_t htonf(float f)
{
    uint32_t p;
    uint32_t sign;

    if (f < 0) { sign = 1; f = -f; }
    else { sign = 0; }
        
    p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
    p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction

    return p;
}

float ntohf(uint32_t p)
{
    float f = ((p>>16)&0x7fff); // whole part
    f += (p&0xffff) / 65536.0f; // fraction

    if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set

    return f;
}

Der oben stehende Code ist eine naive Implementierung, die einen float Wert in einer 32-Bit Zahl speichert. Das höchste Bit (31) wird benutzt um das Vorzeichen zu speichern (wobei 1 negativ bedeutet) und die nächsten 7 Bit (30-16) werden benutzt um den ganzzahligen Teil zu speichern. Die übrigen Bit (15-0) speichern den Nachkommateil.

Die Benutzung ist tatsächlich sehr einfach.

#include <stdio.h>

int main(void)
{
    float f = 3.1415926, f2;
    uint32_t netf;

    netf = htonf(f);  // convert to "network" form
    f2 = ntohf(netf); // convert back to test

    printf("Original: %f\n", f);        // 3.141593
    printf(" Network: 0x%08X\n", netf); // 0x0003243F
    printf("Unpacked: %f\n", f2);       // 3.141586

    return 0;
}

Positiv ist, dass dieser Ansatz klein, schnell und einfach ist. Negativ hingegen ist, dass man den Platz nicht sehr effizient nutzt und die Spanne an Zahlen sehr stark eingeschränkt wird. Probier doch mal eine Zahl, die größer als 32767 ist, zu kodieren. Das wird leider nicht klappen. Außerdem kann man an obigem Beispiel erkennen, dass die letzten Dezimalstellen nicht richtig erhalten bleiben.

Aber was kann man denn dann alternativ machen? Naja, der Standard, um float Zahlen zu speichern, ist als IEEE754 bekannt. Die meisten Computer nutzen intern dieses Format um damit zu rechnen, also müsste man dieses Format doch auch gar nicht umwandeln. Hier liegt es wieder an dem Wort „meisten“. Der Code soll ja portabel sein und auf allen Computern funktionieren, deswegen musst Du leider davon ausgehen, dass Dein Code auch auf Computern laufen soll, die floats anders behandeln.

Hier ist der Code, der sowohl floats als auch doubles in das IEEE754 Format konvertiert. Es werden jedoch nicht NaN oder Unendliche Zahlen kodiert, aber der Code könnte dementsprechend angepasst werden.

#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
#define unpack754_32(i) (unpack754((i), 32, 8))
#define unpack754_64(i) (unpack754((i), 64, 11))

uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
    long double fnorm;
    int shift;
    long long sign, exp, significand;
    unsigned significandbits = bits - expbits - 1; // -1 for sign bit

    if (f == 0.0) return 0; // get this special case out of the way

    // check sign and begin normalization
    if (f < 0) { sign = 1; fnorm = -f; }
    else { sign = 0; fnorm = f; }

    // get the normalized form of f and track the exponent
    shift = 0;
    while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
    while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
    fnorm = fnorm - 1.0;

    // calculate the binary form (non-float) of the significand data
    significand = fnorm * ((1LL<<significandbits) + 0.5f);

    // get the biased exponent
    exp = shift + ((1<<(expbits-1)) - 1); // shift + bias

    // return the final answer
    return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}

long double unpack754(uint64_t i, unsigned bits, unsigned expbits)
{
    long double result;
    long long shift;
    unsigned bias;
    unsigned significandbits = bits - expbits - 1; // -1 for sign bit

    if (i == 0) return 0.0;

    // pull the significand
    result = (i&((1LL<<significandbits)-1)); // mask
    result /= (1LL<<significandbits); // convert back to float
    result += 1.0f; // add the one back on

    // deal with the exponent
    bias = (1<<(expbits-1)) - 1;
    shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias;
    while(shift > 0) { result *= 2.0; shift--; }
    while(shift < 0) { result /= 2.0; shift++; }

    // sign it
    result *= (i>>(bits-1))&1? -1.0: 1.0;

    return result;
}

Ich habe ein paar nützliche Makros an den Anfang gepackt, die das Packen und Entpacken der 32-Bit (vermutlich float) und 64-Bit (vermutlich double) Zahlen ermöglichen, aber die pack754() Funktion kann auch direkt aufgerufen werden, sodass diese Daten Bits verarbeiten kann (expbits wird für den normalisierten Exponenten reserviert).

Hier ist ein Anwendungsbeispiel:

#include <stdio.h>
#include <stdint.h> // defines uintN_t types
#include <inttypes.h> // defines PRIx macros

int main(void)
{
    float f = 3.1415926, f2;
    double d = 3.14159265358979323, d2;
    uint32_t fi;
    uint64_t di;

    fi = pack754_32(f);
    f2 = unpack754_32(fi);

    di = pack754_64(d);
    d2 = unpack754_64(di);

    printf("float vorher : %.7f\n", f);
    printf("float encoded: 0x%08" PRIx32 "\n", fi);
    printf("float danach  : %.7f\n\n", f2);

    printf("double vorher : %.20lf\n", d);
    printf("double encoded: 0x%016" PRIx64 "\n", di);
    printf("double danach  : %.20lf\n", d2);

    return 0;
}

Der obige Code produziert die folgende Ausgabe:

float vorher : 3.1415925
float encoded: 0x40490FDA
float danach  : 3.1415925

double vorher : 3.14159265358979311600
double encoded: 0x400921FB54442D18
double danach  : 3.14159265358979311600

Zudem ist noch unklar, wie man structs verpacken kann. Leider ist dies nicht so einfach, da der Compiler auch Zwischenräume in das struct einbauen kann, was dazu führt, dass es keine portable Möglichkeit gibt, diese Daten einfach zu verschicken. Du wirst es langsam nicht mehr hören können: „Man kann das nicht machen“, „Jenes ist auch nicht möglich“ und ein guter Freund hat mir mal gesagt: „Wenn irgendetwas schief geht, liegt das meistens an Microsoft“. Unser Problem hat dieses mal nichts mit Microsoft zu tun, aber generell hat mein Kumpel schon recht.

Aber nun zurück zu den structs: Die einzige Möglichkeit wäre, jedes Feld einzeln zu verpacken, dann zu verschicken und auf der Empfängerseite wieder zu entpacken und in ein struct zusammen zu führen. Und ja, das ist ein großer Aufwand. Man nutzt dafür gerne eine Helfer-Funktion.

In dem Buch „The Practice of Programming“ von Kernighan und Pike, implementieren die Autoren zwei Funktionen pack() und unpack(), die ähnlich wie printf() funktionieren. Diese Funktionen machen genau das, was wir hier benötigen. Leider kann ich nicht auf diese Funktionen verlinken, da diese nicht online veröffentlicht wurden. Aber das Buch an sich ist so gut, dass es sich auf jeden Fall lohnt, auf dieses zu verweisen.

An dieser Stelle möchte ich auf die unter BSD lizensierte TPL C API verweisen, die ich zwar selber noch nie benutzt habe, die aber sehr vielversprechend aussieht. Perl und Python Programmierer können die nativen pack() und unpack() Funktionen angucken. Und Java hat ja das gute, alte Serializable Interface, das für die selben Zwecke verwendet werden kann.

Möchtest Du jedoch dein eigenes Packsystem schreiben, kannst Du den K&P Trick verwenden, denn dort werden variable Argumentlisten genommen, um printf()-ähnliche Funktionen benutzt, um die Pakete zu erstellen. Hier ist eine Version, die ich erstellt habe, um Dir zu zeigen, wie man an so etwas heran gehen kann.

(Dieser Code verweist auf die pack754() Funktionen von oben. Die packi*() Funktionen funktionieren wie die sehr ähnliche htons() Familie, nur dass diese die Daten in einen char Array hauen, anstatt in einen anderen integer).

#include <ctype.h>
#include <stdarg.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>

// various bits for floating point types--
// varies for different architectures
typedef float float32_t;
typedef double float64_t;

/*
** packi16() -- store a 16-bit int into a char buffer (like htons())
*/ 
void packi16(unsigned char *buf, unsigned int i)
{
    *buf++ = i>>8; *buf++ = i;
}

/*
** packi32() -- store a 32-bit int into a char buffer (like htonl())
*/ 
void packi32(unsigned char *buf, unsigned long i)
{
    *buf++ = i>>24; *buf++ = i>>16;
    *buf++ = i>>8;  *buf++ = i;
}

/*
** unpacki16() -- unpack a 16-bit int from a char buffer (like ntohs())
*/ 
unsigned int unpacki16(unsigned char *buf)
{
    return (buf[0]<<8) | buf[1];
}

/*
** unpacki32() -- unpack a 32-bit int from a char buffer (like ntohl())
*/ 
unsigned long unpacki32(unsigned char *buf)
{
    return (buf[0]<<24) | (buf[1]<<16) | (buf[2]<<8) | buf[3];
}

/*
** pack() -- store data dictated by the format string in the buffer
**
**  h - 16-bit              l - 32-bit
**  c - 8-bit char          f - float, 32-bit
**  s - string (16-bit length is automatically prepended)
*/ 
int32_t pack(unsigned char *buf, char *format, ...)
{
    va_list ap;
    int16_t h;
    int32_t l;
    int8_t c;
    float32_t f;
    char *s;
    int32_t size = 0, len;

    va_start(ap, format);

    for(; *format != '\0'; format++) {
        switch(*format) {
        case 'h': // 16-bit
            size += 2;
            h = (int16_t)va_arg(ap, int); // promoted
            packi16(buf, h);
            buf += 2;
            break;

        case 'l': // 32-bit
            size += 4;
            l = va_arg(ap, int32_t);
            packi32(buf, l);
            buf += 4;
            break;

        case 'c': // 8-bit
            size += 1;
            c = (int8_t)va_arg(ap, int); // promoted
            *buf++ = (c>>0)&0xff;
            break;

        case 'f': // float
            size += 4;
            f = (float32_t)va_arg(ap, double); // promoted
            l = pack754_32(f); // convert to IEEE 754
            packi32(buf, l);
            buf += 4;
            break;

        case 's': // string
            s = va_arg(ap, char*);
            len = strlen(s);
            size += len + 2;
            packi16(buf, len);
            buf += 2;
            memcpy(buf, s, len);
            buf += len;
            break;
        }
    }

    va_end(ap);

    return size;
}

/*
** unpack() -- unpack data dictated by the format string into the buffer
*/
void unpack(unsigned char *buf, char *format, ...)
{
    va_list ap;
    int16_t *h;
    int32_t *l;
    int32_t pf;
    int8_t *c;
    float32_t *f;
    char *s;
    int32_t len, count, maxstrlen=0;

    va_start(ap, format);

    for(; *format != '\0'; format++) {
        switch(*format) {
        case 'h': // 16-bit
            h = va_arg(ap, int16_t*);
            *h = unpacki16(buf);
            buf += 2;
            break;

        case 'l': // 32-bit
            l = va_arg(ap, int32_t*);
            *l = unpacki32(buf);
            buf += 4;
            break;

        case 'c': // 8-bit
            c = va_arg(ap, int8_t*);
            *c = *buf++;
            break;

        case 'f': // float
            f = va_arg(ap, float32_t*);
            pf = unpacki32(buf);
            buf += 4;
            *f = unpack754_32(pf);
            break;

        case 's': // string
            s = va_arg(ap, char*);
            len = unpacki16(buf);
            buf += 2;
            if (maxstrlen > 0 && len > maxstrlen) count = maxstrlen - 1;
            else count = len;
            memcpy(s, buf, count);
            s[count] = '\0';
            buf += len;
            break;

        default:
            if (isdigit(*format)) { // track max str len
                maxstrlen = maxstrlen * 10 + (*format-'0');
            }
        }

        if (!isdigit(*format)) maxstrlen = 0;
    }

    va_end(ap);
}

Und hier habe ich ein Demo Programm von dem Code oben, das ein paar Daten in buf lädt und diese dann in die einzelnen Variablen entpackt. Beachte, dass beim Aufruft von unpack() mit einem String als Argument (das Format wird durch „s“ festgelegt), die maximale Länge vorher festgelegt werden sollte, da sonst der Buffer über laufen könnte, z.B. „96s“. Außerdem solltest Du beachten, dass die Daten, die Du über das Netzwerk erhältst, von einem böswilligen Nutzer verschickt wurden, der diese schlecht verpackt hat, um Dein System anzugreifen.

#include <stdio.h>

// various bits for floating point types--
// varies for different architectures
typedef float float32_t;
typedef double float64_t;

int main(void)
{
    unsigned char buf[1024];
    int8_t magic;
    int16_t monkeycount;
    int32_t altitude;
    float32_t absurdityfactor;
    char *s = "Great unmitigated Zot!  You've found the Runestaff!";
    char s2[96];
    int16_t packetsize, ps2;

    packetsize = pack(buf, "chhlsf", (int8_t)'B', (int16_t)0, (int16_t)37, 
            (int32_t)-5, s, (float32_t)-3490.6677);
    packi16(buf+1, packetsize); // store packet size in packet for kicks

    printf("packet is %" PRId32 " bytes\n", packetsize);

    unpack(buf, "chhl96sf", &magic, &ps2, &monkeycount, &altitude, s2,
        &absurdityfactor);

    printf("'%c' %" PRId32" %" PRId16 " %" PRId32
            " \"%s\" %f\n", magic, ps2, monkeycount,
            altitude, s2, absurdityfactor);

    return 0;
}

Egal, ob Du nun Deinen eigenen Code, oder den Code von jemand anderem benutzt, solltest Du ein generelles Set von Verpackmechanismen haben, um Fehler möglichst zu vermeiden.

Was ist denn ein gutes Format, um Daten zu verpacken? Zum Glück wird im RFC 4506, dem External Data Representation Standard bereits in binäres Format für eine Vielzahl an Datentypen definiert, also Floating Point Typen, Integer Typen, Arrays, Rohdaten usw. Ich rate Dir, Dich an diesen Standard zu halten, wenn Du die Daten verpackst, aber natürlich bist Du dazu nicht verpflichtet. Die Datenpack-Polizei steht ja nicht direkt vor Deiner Tür, zumindest hoffe ich, dass sie das nicht tut.

Auf jeden Fall ist es wichtig, dass Du die Daten vor dem verschicken verpackst, wie Du das machst, ist Dir überlassen.

7.5 Datenkapselung im Detail

Jetzt wollen wir uns mal angucken, was Datenkapselung wirklich bedeutet. Im einfachsten Fall fügt man lediglich einen Header an die Daten, der zum Identifizieren der Daten zuständig ist, oder die Datenlänge angibt.

Wie sieht denn dann dieser Header aus? Es handelt sich hierbei lediglich um Binärdaten, die notwendig sind, um die Daten zu verifizieren.

Wow, das bietet viel Spielraum. Deswegen wollen wir uns ein Beispiel angucken. Angenommen Du hast ein Multi-User Chatprogramm, das SOCK_STREAMs benutzt. Wenn ein User etwas eingibt („tippt“), dann müssen zwei Sachen übermittelt werden: Wer hat etwas getippt und was wurde getippt.

So weit, so gut. Was ist daran jetzt schwierig? Das Problem ist, dass die Nachrichten nicht alle gleich lang sind. Eine Person mit dem Namen „tom“ tippt zum Beispiel „Hi“ und eine andere Person mit dem Namen „Benjamin“ könnte „Hey guys what is up?“ schreiben. Diese Daten schickst Du dann also an die Client-Programme. Somit sieht der ausgehende Datenstream wie folgt aus:

t o m H i B e n j a m i n H e y g u y s w h a t i s u p ?

Wie weiß dann der Client, wann die eine Nachricht anfängt und die andere aufhört? Man könnte allen Nachrichten die gleiche Länge geben und dann sendall() aufrufen. Aber das verschwendet unnötig Bandbreite. Wir brauchen schließlich nicht 1024 Byte, nur um „tom“s Nachricht mit dem Inhalt „Hi“ zu übermitteln.

Deswegen ist es besser, wenn wir die Daten in eine passende Header und
Paket Struktur kapseln. Sowohl der Client, als auch der Server wissen, wie man die Daten packt und entpackt. Wir wollen jetzt also ein Protokoll definieren, das beschreibt wie Server und Client miteinander kommunizieren.

Nehmen wir für unser Beispiel einmal an, dass die Usernamen immer die Länge von 8 Zeichen haben und mit ‚\0‘ enden. Außerdem nehmen wir an, dass die Nachrichten unterschiedlich lang sind, jedoch nicht länger als 128 Zeichen. Somit können wir eine einfache Paket Struktur erstellen:

  1. len (1 Byte, unsigned) – Die totale Länge des Pakets, also sowohl Username, als auch die Nachricht
  2. name (8 Byte) – Der Username, falls nötig mit Nullen aufgefüllt
  3. chatdata (n Byte) – Die Nachricht an sich, jedoch nicht länger als 128 Byte. Die Länge des Pakets wird berechnet, indem man die Länge der Nachricht + die Länge des Usernamen (8 Byte) berechnet.

Warum habe ich jetzt 8 Byte und 128 Byte als Feldlimit genommen? Ehrlich gesagt, habe ich diese einfach grob ausgewählt, in der Hoffnung, dass diese ausreichen um die Daten aufzunehmen. Es kann aber auch sein, dass 8 Byte für den Usernamen zu wenig sind und man 30 Byte nimmt. Das liegt an Dir.

Nimmt man jetzt die obige Paket-Definition, dann würde das erste Paket die folgenden Informationen enthalten (Hex und ASCII):

   0A     74 6F 6D 00 00 00 00 00      48 69
(length)  T  o  m    (padding)         H  i

und die zweite Nachricht wird dann entsprechend verpackt:

   18     42 65 6E 6A 61 6D 69 6E      48 65 79 20 67 75 79 73 20 77 ...
(length)  B  e  n  j  a  m  i  n       H  e  y     g  u  y  s     w  ...

Die Länge wird natürlich in der Network Byte Order gespeichert. In diesem Fall handelt es sich nur um ein Byte, da ist das ganze egal, aber im Allgemeinen möchte man die binären Integer Werte in der Network Byte Order speichern.

Wenn Du diese Daten dann verschicken möchtest, sollte dies sicher funktionieren und mit sendall() kannst Du dann auch sicher sein, dass die Daten verschickt werden, auch wenn dies nicht in einem Mal passiert.

Somit musst Du aber auch etwas mehr machen, wenn Du die Daten empfängst. Um sicher zu gehen, solltest Du immer davon ausgehen, dass Du nur Teile der Nachricht erhältstt (beispielsweise „18 42 65 6E 6A“ von Benjamin, denn das ist alles, was wir über recv() erhalten). Also müssen wir recv() wieder und wieder aufrufe, bis das Paket komplett angekommen ist.

Aber wie kriegen wir das jetzt hin? Wir wissen ja, wie viele Byte wir bekommen sollen, da das ja die erste Nummer des Pakets ist. Außerdem wissen wir, dass das Paket maximal 1+8+128, also 137 Byte lang ist (denn so haben wir unser Paket ja definiert).

Es gibt nun mehrere Möglichkeiten. Wir können einfach recv() aufrufen um die Länge des Pakets raus zu bekommen, da wir ja wissen, dass das erste Byte eben diese Länge angibt. Wenn Du diese Länge hast, kannst Du die Funktion immer wieder aufrufen, bis Du das komplette Paket erhalten hast. Der Vorteil ist, dass Du nur einen Buffer brauchst, der so groß ist wie das Paket, der Nachteil ist, dass Du recv() mindestens zwei mal aufrufen musst.

Eine andere Möglichkeit ist, recv() aufzurufen und zu sagen, dass die zu erhaltene Größe der maximalen Paketgröße entspricht. Dann kommt alles, was Du erhälst in den definierten Buffer und du guckst, ob das Paket komplett angekommen ist. Es ist auch möglich, dass Du das nächste Paket erhältstt, also musst Du darauf achten, dass Du dafür ausreichend Platz hast.

Du kannst also einen Array erstellen, der groß genug ist, um zwei Pakete aufzunehmen. Das ist dann Dein Arbeiter Array, aus dem das Paket holst.

Immer wenn Du über recv() aufrufst, kommen die Daten in diesen Arbeiter Array und Du guckst, ob die Daten komplett sind. Das bedeutet, dass die Daten in dem Array größer oder gleich der Anzahl in Deinem Array sind (+1, da die Länge nicht das Byte für die Länge enthält). Wenn die Anzahl an Byte im Buffer kleiner als 1 ist, ist das Paket nicht komplett angekommen. Du musst also einen speziellen Fall vorbereiten, da das erste Byte Müll ist und Du Dich nicht auf die korrekte Paket-Länge verlassen kannst.

Wenn das Paket dann komplett angekommen ist, kannst Du damit machen, was Du willst. Benutz es und entferne es danach aus dem Arbeiter Buffer.

Okay, das war jetzt schon mal etwas komplizierter. Jetzt kommen wir noch zu dem Fall, dass Du mehr als das Paket gelesen hast, also bereits das nächste Paket liest. Du hast also in Deinem Arbeiter Buffer ein komplettes Paket und dann ein (unvollständiges) weiteres Paket. Du kannst nun also das vollständige Paket lesen, dann aus dem Buffer löschen und das nächste Paket an den Anfang des Buffers verschieben., sodass Du beruhigt recv() aufrufen kannst und alles von vorne beginnt.

Man kann das Programm zwar beschleunigen, indem man das unvollständige Paket nicht an den Anfang des Arrays verschiebt und man einen runden Buffer benutzt, aber das sprengt dieses Tutorial dann doch. Wenn Dich dieser Ansatz jedoch interessiert, dann schnapp Dir ein Buch über Datenstrukturen; da kannst Du dann näheres dazu erfahren.

Ich habe nie gesagt, dass das hier einfach sein würde – okay, zugegeben ich habe das gesagt und es ist auch einfach. Man muss sich einfach nur an die Herangehensweise gewöhnen, dann ist das alles mit der Zeit auch wirklich einfach. Versprochen!

Pakete verschicken – Hello World!

Bisher haben wir ja immer Daten von einem Host an einen anderen geschickt. Ist es denn möglich, Daten an mehrere Hosts zu verschicken – und zwar gleichzeitig?

Mit UDP (und zwar nur UDP, nicht TCP) und dem Standard IPv4, wird dies über einen Mechanismus namens broadcasting ermöglicht. Bei IPv6 wird dies übrigens nicht unterstützt, hier muss man auf die bessere Variante namens Multicasting zurückgreifen. Darauf werde ich jedoch nicht weiter eingehen.

Du kannst jedoch nicht einfach wild Pakete durch die Gegend schießen, also broadcasten. Du musst zuerst die Socket Option SO_BROADCAST setzen. Dies ist vergleichbar mit den kleinen Plastik Kästen über den Knöpfen, die Nuklearraketen starten. Es ist eine Vorsichtsmaßnahme, denn Du kannst damit sehr viel Mist bauen.

Im Ernst, denn jedes System, dass ein Broadcast Paket erhält, muss all die Paket-Schichten, die bei der Datenkapselung über das Paket gelegt wurden, entfernen, um zu prüfen an welchen Port die Daten geschickt werden sollen. Dann werden die Daten entweder übergeben, oder aber weggeschmissen. In beiden Fällen ist das eine Menge Arbeit, die eine Maschine da verrichten muss, wenn sie ein Broadcast Paket erhält und da alle Maschinen eines Netzwerks diese Arbeit verrichten müssen, handelt es sich in vielen Fällen um unnötige Arbeit. Als Doom damals raus kam, war dies übrigens der Grund für viele Beschwerden.

Wie spezifiziert man denn jetzt die Zieladresse einer Broadcast Nachricht? Es gibt hierfür zwei Möglichkeiten:

  1. Sende die Daten an eine spezifische Subnetz Broadcast Adresse. Das ist die Subnetz Netzwerk Nummer, bei der alle alle Bits, die den Host beschreiben, auf 1 gesetzt sind. Nehmen wir mein Netzwerk als Beispiel. Zuhause ist mein Netzwerk 192.168.1.0 und meine Netzmaske 255.255.255.0, somit ist das letzte Byte der Adresse die Host Nummer (da die ersten 3 Byte der Netzmaske, die Netzwerk Nummer abbilden). Also ist meine Broadcast Adresse 192.168.1.255. Bei Unix bekommst Du über den ifconfig Befehl übrigens all diese Informationen. Du kannst die Daten des Broadcast Pakets übrigens auch an entfernte Netzwerke, neben Deinem lokalen Netzwerk, schicken. Hierbei läufst Du jedoch Gefahr, dass die Pakete von dem Router des Zielnetzwerks fallen gelassen werden.
  2. Sende die Daten an die „globale“ Broadcast Adresse. Das ist die 255.255.255.255, aka INADDR_BROADCAST. Viele Maschinen werden daraus automatisch die Netzwerk Broadcast Adresse berechnen, jedoch nicht alle. Das hängt von den jeweiligen Maschinen ab. Übrigens schicken Router diese Art der Broadcast Pakete nicht aus dem lokalen Netzwerk raus.

Was passiert nun also, wenn Du probierst Daten an die Broadcast Adresse zu senden, ohne dass Du vorher die SO_BROADCAST Socket Option eingestellt hast? Probieren wir’s mal aus, indem wir unser altes talker and listener Programm ausführen und gucken was passiert.

$ talker 192.168.1.2 foo
sent 3 bytes to 192.168.1.2
$ talker 192.168.1.255 foo
sendto: Permission denied
$ talker 255.255.255.255 foo
sendto: Permission denied

Hier sieht man, dass das nicht klappt, da wir nicht die SO_BROADCAST Socket Option gesetzt haben. Wenn wir das ändern, können wir sendto() beliebig aufrufen.

Und damit haben wir den einzigen Unterschied zwischen einer gewöhnliche UDP Anwendung von einer UDP Anwendung mit Broadcast Support. Nehmen wir jetzt also unsere alte talker Anwendung und fügen einen Abschnitt hinzu, der die SO_BROADCAST Option setzt. Dieses Programm nennen wir broadcaster.c

/*
** broadcaster.c -- a datagram "client" like talker.c, except
**                  this one can broadcast
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define SERVERPORT 4950    // the port users will be connecting to

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in their_addr; // connector's address information
    struct hostent *he;
    int numbytes;
    int broadcast = 1;
    //char broadcast = '1'; // if that doesn't work, try this

    if (argc != 3) {
        fprintf(stderr,"usage: broadcaster hostname message\n");
        exit(1);
    }

    if ((he=gethostbyname(argv[1])) == NULL) {  // get the host info
        perror("gethostbyname");
        exit(1);
    }

    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // this call is what allows broadcast packets to be sent:
    if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast,
        sizeof broadcast) == -1) {
        perror("setsockopt (SO_BROADCAST)");
        exit(1);
    }

    their_addr.sin_family = AF_INET;     // host byte order
    their_addr.sin_port = htons(SERVERPORT); // short, network byte order
    their_addr.sin_addr = *((struct in_addr *)he->h_addr);
    memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero);

    if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0,
             (struct sockaddr *)&their_addr, sizeof their_addr)) == -1) {
        perror("sendto");
        exit(1);
    }

    printf("sent %d bytes to %s\n", numbytes,
        inet_ntoa(their_addr.sin_addr));

    close(sockfd);

    return 0;
}

Was unterscheidet dieses von einer „normalen“ UDP Client/Server Kommunikation? Nichts! (Okay, der Unterschied ist, dass es dem Client erlaubt ist, Broadcast Pakete zu verschicken – mehr aber auch nicht.) Somit kannst Du also jetzt die Pakete an die Adressen schicken, die vorher nicht erlaubt waren.

$ broadcaster 192.168.1.2 foo
sent 3 bytes to 192.168.1.2
$ broadcaster 192.168.1.255 foo
sent 3 bytes to 192.168.1.255
$ broadcaster 255.255.255.255 foo
sent 3 bytes to 255.255.255.255

Außerdem kann der Listener diese Pakete nun empfangen. Sollte das nicht der Fall sein, könnte es daran liegen, dass es sich um eine IPv6 Adresse handelt. Du kannst versuchen, IPv4 zu erzwingen, indem Du AF_UNSPEC durch AF_INET ersetzt, in der listener.c Datei.

Das ist ja alles schon relativ cool, aber wenn Du listener auf einem anderen Computer, neben Deinem Computer, startest und dann broadcaster mit Deiner Broadcast Adresse startest…. Hey! Beide listener Programme erhalten die Pakete, auch wenn Du sendto() nur einmal aufgerufen hast. Cool!

Falls listener die Daten, die Du ihm direkt zugeschickt hast, bekommt, aber nicht die Broadcast Daten, kann das an einer Firewall liegen, die diese Pakete blockiert. (Danke an Pat und Bapper, die das heraus gefunden haben. Ich habe Euch versprochen, dass ich Euch in diesem Guide erwähnen würde, was dann hiermit geschehen ist)

Ich wiederhole es noch einmal. Sei vorsichtig mit den Broadcast Paketen, da jede Maschine im lokalen Netz diese Daten verwerten muss, egal ob es diese verwerten möchte, oder nicht.


Quelle: http://beej.us//bgnet/output/html/singlepage/bgnet.html

Version 3.0.15
3. Juli 2012

  • TobscoreUndercoverFanboy

    Hello Tobscore :).
    I read this whole thing and was really excited about your german skills.
    I would be pleased if you could explain this line to me

    void packi32(unsigned char *buf, unsigned long i)

    {

    *buf++ = i>>24; *buf++ = i>>16;

    *buf++ = i>>8; *buf++ = i;

    }
    greetings,

    your fanboi

    • Hey,
      this function is used to store a 32 bit integer in a char array.
      Each byte of the integer is stored in the array, by bitshifting the contents byte per byte in the array.

      First element in the array:
      i = >> 24 is used to move the fourth byte to the right: (x—) -> (—x).
      When this integer is cast to a char, only the last byte is used: (—x) -> (x)
      Then store it in the char array.

      Second element in the array:
      i = >> 16 is used to move the third byte to the right: (-x–) -> (—x)
      Cast this again (—x) -> (x)

      I hope I could help you understand this function even better now.

      • TobsCoreUndercoverFanboy

        Hey Tobscore, i don’t know what to say except thanks for replying!
        I’ve never spoken with a famous person before *blush* ^.^
        I love this function now and i will try to use this in all my future programs – it’s just amazing! I love bytes.
        Have a nice day!

  • kssk

    Vielen Dank dir!!! besser erklären könnte man ja nicht! hab dabei noch viel gelacht, super Arbeit hast du gemacht, D A N K E!!!