25.8 Weitere Anmerkungen zur Netzwerkprogrammierung 

Mit diesem kleinen Kapitel haben Sie sich jetzt den Grundstein zur Netzwerkprogrammierung gelegt. Bedenken Sie aber, dass dieses Thema ein noch viel größeres Spektrum umfasst, als ich hier beschrieben habe. Dazu könnte (will) man ein ganzes Buch schreiben (an mir soll‘s nicht liegen ;-)). Bevor Sie sich jetzt in das Abenteuer stürzen, eigene kleine Programme mit Netzwerkfunktionalität zu schreiben, möchte ich Sie hier noch in ein paar Abschnitten auf einige Dinge hinweisen, auf die Sie besonders achten sollten (bzw. mit denen Sie sich noch intensiver auseinandersetzen sollten) – insbesondere, wenn Ihre Anwendung nicht so funktioniert, wie Sie es gerne hätten.
25.8.1 Das Datenformat 

In den Beispielen, die Sie hier erstellt haben, wurden lediglich Zeichenketten verschickt. Meistens liegen die Daten aber nicht in einem solch bequemen Format vor. Wenn Sie beispielsweise Ganzzahlen oder Gleitpunktzahlen versenden wollen, verwenden Sie am besten sscanf() und snprintf().
Und was ist mit binären Strukturen (struct)? Auch hier empfiehlt es sich, die komplette Struktur in eine Zeichenkette zu konvertieren, bevor Sie diese versenden. Auf der anderen Seite müssen selbstverständlich ebenfalls bestimmte Vorkehrungen getroffen werden.
Sicherlich, letztendlich entscheiden Sie, wie die Daten zwischen Client und Server hin- und hergeschickt werden. Allerdings sollten Sie bedenken, dass Sie nicht immer wissen, auf was für einen Rechner die Daten übertragen werden. Schicken Sie von einem Little-Endian-Rechner einen Integer an ein Big-Endian-System, sind Probleme vorprogrammiert. Oder was ist, wenn Sie von einem 64-Bit-Rechner einen Integer an einen 32-Bit-Rechner verschicken? Sie wissen also nie genau, welche Größe die Datentypen int, long und short auf der Gegenseite haben. Zwar gibt es (seit der Einführung des C99-Standards) die unabhängigen Typen wie beispielsweise int8_t, int16_t etc. und uint8_t etc. in der Headerdatei <stdint.h>, was aber recht wenig nützt, weil dieser Standard von einigen Compiler-Herstellern (insbesondere auf Windows-Systemen) unzureichend bis überhaupt nicht beachtet wird.
Daher die Empfehlung: Senden Sie numerische Daten immer im Textformat an die Gegenseite. Natürlich setzt dies voraus, dass die Gegenseite denselben Zeichensatz verwendet. Wenn Sie einen String an einen Rechner schicken, auf dem sich nur japanische Schriftarten befinden, kommt nichts dabei raus. Ebenso müssen Sie unsere landestypischen Umlaute berücksichtigen, die meist auf einem Rechner, der beispielsweise in der USA steht, auch nicht richtig dargestellt werden können. Der Nachteil: Es wird Bandbreite verschwendet. Eine 64-Bit-Nummer beispielsweise kann nämlich über 20 Zeichen lang sein, während im Binärformat gerade mal 8 Zeichen dafür benötigt werden. Aber es ist Ihre Entscheidung!
25.8.2 Der Puffer 

Bisher mussten Sie sich nie so richtig um die Pufferung der Daten kümmern. Beispielsweise hat Ihnen bei Funktionen wie fgets() oder fputs() das System die Pufferung abgenommen. Sie mussten hierbei nur angeben, wie groß dieser Puffer sein sollte. In der Netzwerkprogrammierung müssen Sie sich nun selbst darum kümmern. Mit den Funktionen send()/sendto() und recv()/recvfrom() können bei den Sockets erst mal weniger Bytes ein- bzw. ausgegeben werden als angenommen. Das Problem ist, dass das System (der Kernel) für das Socket eine bestimmte Puffergröße vorgibt. Das bedeutet: Wenn der Puffer voll ist, liest recv()/recvfrom() bzw. schreibt send()/sendto() aus diesem bzw. in diesen Puffer – selbst dann, wenn noch nicht alle gewünschten Daten ausgelesen bzw. geschrieben wurden.
Wenn Sie die Daten in Form eines char-Arrays mit einfachem Text übertragen, dürften Sie keine Probleme mit der Pufferung bekommen, sofern Sie einen String ordentlich mit \0 abschließen. Sobald allerdings binäre Daten übertragen werden sollen, gibt es Probleme damit. Bei binären Daten können Sie sich nicht darauf verlassen, dass diese mit einem \0 abgeschlossen werden – weshalb Sie sich hierbei selbst um das letzte Zeichen kümmern müssen.
Welche Puffergröße Sie verwenden, bleibt Ihnen überlassen und hängt vom Anwendungsfall ab. Allerdings macht ein byteweiser Puffer genauso wenig Sinn wie ein überdimensional großer Puffer. Es hat sich bewährt, eine Puffergröße von 512 oder 1024 KB zu verwenden.
Hinweis |
Der Puffer ist Ihr wichtigstes Kommunikationswerkzeug, mit dem Sie Daten austauschen können. Wenn Sie etwas in einen Puffer schreiben wollen, sollten Sie immer bedenken, wie es auf der anderen Seite wieder herauskommt, und eventuell auch überprüfen, was herauskommt. Denn wenn etwas häufig nicht klappt, dann ist es die Art und Weise, wie die Daten beim Empfänger ankommen. Nicht selten ist es ein nicht terminierter String, der für Zeichensalat sorgt. |
25.8.3 Portabilität 

Sie haben in diesem Kapitel gesehen, wie man mit einem abstrakten Layer eine portable Anwendung (nicht nur) für die Netzwerkprogrammierung erstellen kann. Mit Linux/UNIX und MS-Windows haben Sie in den Beispielen eine recht große Zielgruppe eingeschlossen. Bedenken Sie allerdings, dass es auch noch andere Systeme gibt. Gemeint sind beispielsweise Systeme wie QNX oder SGI IRIX. Zwar sind die Unterschiede der allgemeinen Socket-Programmierung nicht allzu gravierend, dennoch müssen Sie sich auch diesbezüglich gegebenenfalls schlau machen, welche Differenzen es dabei gibt.
25.8.4 Von IPv4 nach IPv6 

Da IPv6 noch nicht eingeführt wurde (und eine Einführung noch nicht in Sicht ist), wurden seine Eigenheiten in den vorangegangenen Abschnitten nicht näher behandelt. Allerdings gibt es hierzu eigentlich auch gar nicht viel zu berichten. Daher folgt hier eine kurze Zusammenfassung für den Fall der Fälle, die zeigt, wie Sie Ihre Anwendungen von IPv4 nach IPv6 portieren könnten.
Konstanten
Die IPv4-Konstanten AF_INET bzw. PF_INET wurden durch AF_INET6 bzw. PF_INET6 ersetzt. Hierbei muss eigentlich nur die Konstante um eine 6 erweitert werden. Es ist auch kein Fehler, wenn Sie auch bei einer IPv4-Software gleich die neuen Konstanten verwenden, da ein Programm, das auf IPv6 portiert wurde, auch weiterhin auf IPv4-Rechnern läuft (vorausgesetzt, der Rechner ist »dual-stacked«, was in Zukunft bei IPv6-fähigen Rechnern immer der Fall sein sollte).
Was sich auch verändert hat, ist die Konstante INADDR_ANY, die beim Binden von Sockets an einen Port angeben wird. Sie bedeutet, dass Pakete von jedem Interface angenommen werden. Ein wenig ungewöhnlich ist, dass die neue Konstante kleingeschrieben wird – in6addr_any. Der Grund hierfür: Die alte Struktur in_addr bestand nur aus einem unsigned long int s_addr, und somit war die Konstante INADDR_ANY auch nur eine Zahl. Da die Adresse bei IPv6 128 Bit breit ist, ist dies nicht mehr möglich (da kein portabler Datentyp mit dieser Breite existiert), weshalb es sich nun um ein Array handelt:
struct in6_addr { union { uint8_t u6_addr8[16]; uint16_t u6_addr16[8]; uint32_t u6_addr32[4]; } in6_u; #define s6_addr in6_u.u6_addr8 #define s6_addr16 in6_u.u6_addr16 #define s6_addr32 in6_u.u6_addr32 };
Strukturen
Nachdem in_addr durch in6_addr ersetzt wurde (siehe oben), ist es auch nötig, die Struktur sockaddr_in anzupassen:
struct sockaddr_in6 { sa_family_t sin6_family /* Adress-Familie - AF_INET6 */ in_port_t sin6_port; /* Port Transportschicht # */ uint32_t sin6_flowinfo; /* IPv6 Datenfluss-Informationen */ struct in6_addr sin6_addr; /* IPv6 Adresse */ uint32_t sin6_scope_id; /* IPv6 Scope-Kennung */ };
Hier wurde nicht nur die Adressstruktur verändert, es wurden auch noch die zusätzlichen Strukturvariablen sin6_flowinfo und sin6_scope_id hinzugefügt.
Funktionen
Der Großteil der Socket-API-Funktionen ist gleich geblieben. Verändert (hinzugefügt) wurden lediglich die meisten Adressauflösungs- und Konvertierungsfunktionen. So werden die Funktionen inet_aton() bzw. inet_ntoa() durch die Funktionen inet_pton() bzw. inet_ntop() ersetzt. Da diese neuen Funktionen jetzt nicht mehr auf Zahlen operieren, sondern auf den konkreten Adressstrukturen (z. B. in6_addr), unterstützen sie auch beliebige Adressfamilien.
Noch wichtiger sind die neu hinzugekommenen Funktionen getaddrinfo() und getnameinfo(). Diese wurden als Ersatz für die Funktionen gethostbyname()/gethostbyaddr() und getipnodebyname()/getipnodebyaddr() eingeführt und haben den Vorteil, dass sie direkt sockaddr-Strukturen bearbeiten. Des Weiteren wurde noch die Funktion gethostbyname2() hinzugefügt, bei der es sich allerdings nur um eine reine GNU-Extension handelt!
Hinweis |
All diese Funktionen stehen Ihnen übrigens auch schon für IPv4 zur Verfügung, weshalb es nicht falsch sein kann, diese jetzt schon zu verwenden, um eine eventuell spätere Portierung zu erleichtern. |
25.8.5 RFC-Dokumente (Request for Comments) 

Sie wollen einen HTTP-, einen FTP- oder einen SMTP-Server bzw. einen Client erstellen, der damit kommuniziert, und wissen nicht, wo Sie anfangen sollen. Dies ist eine beliebte Frage in den Foren. Wie Sie bereits erfahren haben, findet die Kommunikation zwischen dem Server und Client über Protokolle statt (vergleichbar mit den verschiedenen Sprachen dieser Welt). Ein Webclient und ein Webserver beispielsweise unterhalten sich anders als ein Mailclient und ein Mailserver. All diese Standard-Protokolle werden in den RFCs (Requests for Comments) gesammelt. RFCs sind eine Reihe von technischen Dokumentationen zum Internet, die ihren Ursprung zu ARPANET-Zeiten 1969 hatten. Einen gewaltigen Fundus zur RFC-Sammlung finden Sie im Internet unter http://www.ietf.org/ (The Internet Engineering Task Force).
25.8.6 Sicherheit 

Das Wichtigste kommt zum Schluss. Mit der Netzwerkprogrammierung in C haben Sie auch das gefährlichste Kapitel in C kennengelernt. Die meisten Programme, die angegriffen werden, sind nicht Ihr Editor oder Ihre Entwicklungsumgebung, sondern die Programme, die mit dem Netz verbunden und somit meistens auch für alle erreichbar sind. Beispielsweise kann ein Buffer-Overflow bei einer Netzwerk-Anwendung sehr böse Folgen haben (ein beliebtes Lästerbeispiel ist der Internet Explorer alias »Internet Exploiter«).
Des Weiteren sollten Sie beachten, dass die Daten, die Sie über ein Netzwerk versenden, jederzeit abgefangen werden können. Daher empfiehlt es sich, bei sicherheitsrelevanten Daten (beispielsweise Kundendaten) diese verschlüsselt zu versenden. Die Daten können zwar weiterhin abgefangen werden, aber bei einer guten Verschlüsselung sind diese Daten für den »Sniffer« nicht mehr lesbar – es sei denn, er kann die Verschlüsselung knacken.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.