Tour 1: "hello"

Überblick

Zu Beginn wollen wir sicherstellen, dass unsere Umgebung richtig konfiguriert ist und wir in der Lage sind Socket-Programme zu erstellen. Das einfachste Programm hierzu legt einen neuen Socket an und prüft, dass dies geglückt ist. Wir wollen sogar noch einen Schritt weiter gehen und gleich mehrere Sockets anlegen, um deren Eigenschaften zu betrachten.

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

int main(void)
{
        int i;

        for (i = 0; i < 5; i++)
        {
                int sock = socket(AF_INET, SOCK_STREAM, 0);
                printf("%i: %i\n", i, sock);
        }

        return 0;
}

Um dieses Programm unter Linux zu übersetzen bemühen wir gcc. Früher stand dies für GNU C Compiler, später dann für GNU Compiler Collection, und ist nach wie vor der Standard-Compiler in diesem Umfeld. Sollte der Befehl nicht gefunden werden, so muss das zumeist gleichnamige Paket noch installiert werden. Wir rufen den gcc wie folgt auf:

gcc -Wall -o hello 01-hello.c

Wenn dabei alles geklappt hat, dann gibt es keinerlei Textausgabe. Das Argument -Wall ist optional und teilt dem Compiler mit, dass er alle Warnungen aktivieren soll. Das ist für gewöhnlich eine gute Idee und hilft viele subtile Programmierfehler zu erkennen.

Wenn wir das Programm nun ausführen erhalten wir folgende Ausgabe:

$ ./hello 
0: 3
1: 4
2: 5
3: 6
4: 7

Wir sehen, dass ein Socket einer kleinen ganzen Zahl entspricht. Um genau zu sein der nächsten Ganzzahl die noch keiner geöffneten Datei zugeordnet ist, denn unter UNIX sind Sockets in erster Näherung das Gleiche wie Dateideskriptoren wie man sie durch Aufrufe von z.B. open() erhält. Mehr dazu im Abschnitt Hintergrundwissen.

Wenn wir das Programm ein zweites Mal ausführen dann erhalten wir die exakt identische Ausgabe, und das obwohl wir die Sockets nicht geschlossen haben. Das könnte natürlich daran liegen, dass jeder neue Prozess wieder von vorne zu zählen beginnt. Es sei aber verraten, dass in diesem Fall die Sockets tatsächlich vom Betriebssystem geschlossen und die damit verbundenen Ressourcen wieder freigegeben werden.

Warum aber fangen die Sockets mit 3 an zu zählen? Das liegt daran, dass die Dateideskriptoren 0, 1 und 2 bereits geöffnet sind, nämlich für die Kanäle stdin, stdout und stderr. Dies passiert automatisch wenn ein Prozess gestartet wird.

Eine typische Windows-Installation bringt keinen Compiler mit. Aber auch hier steht mit MinGW eine Version von gcc zur Verfügung. Ich benutze momentan einen MinGW der Teil von Strawberry Perl ist, aber das hat sich eher zufällig so ergeben. Des weiteren gibt es mit Visual Studio Community eine kostenlose Version von Microsoft Visual Studio, mit der ebenfalls Socket-Programme erstellt werden können.

Um das Beispiel-Programm unter Windows zu übersetzen müssen wir ein paar Modifikationen durchführen. Zunächst einmal wollen wir nur den Header winsock.h anstelle von sys/types.h und sys/socket.h benutzen:

#include <stdio.h>
#include <winsock.h>

int main(void)
{
        int i;

        for (i = 0; i < 5; i++)
        {
                int sock = socket(AF_INET, SOCK_STREAM, 0);
                printf("%i: %i\n", i, sock);
        }

        return 0;
}

Das Übersetzen schlägt fehl:

$ gcc -Wall -o hello.exe 01-hello.c
C:\Users\Felix\AppData\Local\Temp\ccLgJHjY.o:01-hello.c:(.text+0x29): undefined reference to `__imp_socket'
collect2.exe: error: ld returned 1 exit status

Anders als unter UNIX sind unter Windows die Socket-Funktionen nicht Teil der Standardbibliotheken die beim Übersetzen automatisch hinzugelinkt werden. Stattdessen muss dies mit einem zusätzlichen Argument angeordnet werden:

$ gcc -Wall -o hello.exe 01-hello.c -lwsock32

Hiermit sagen wir dem Linker, dass er die Bibliothek wsock32.lib hinzufügen soll. Dies ist die Import-Bibliothek für die dynamische Bibliothek wsock32.dll, die von nun an im System existieren muss, damit unser Programm gestartet werden kann. Zum Glück ist sie das bei allen Windows-Versionen seit Windows 95.

Führen wir dieses Programm nun aus, so erleben wir die nächste Überraschung:

C:\>hello.exe
0: -1
1: -1
2: -1
3: -1
4: -1

Die Sockets sehen alle gleich aus!? Nicht ganz, -1 ist der Rückkagbewert von socket() wenn der Aufruf fehlgeschlagen ist. In diesem Fall schlägt der Aufruf fehl, weil die Winsock-Implementierung initialisiert werden muss, bevor eine der Socket-Funktionen benutzt werden kann. Der korrigierte Quellcode sieht so aus:

#include <stdio.h>
#include <winsock.h>

int main(void)
{
        int i;
        WSADATA wsa;

        WSAStartup(MAKEWORD(1, 1), &wsa);

        for (i = 0; i < 5; i++)
        {
                int sock = socket(AF_INET, SOCK_STREAM, 0);
                printf("%i: %i\n", i, sock);
        }

        WSACleanup();

        return 0;
}

Rufen wir das hieraus entstehende Programm auf, so sehen wir z.B. die folgende Ausgabe:

C:\>hello.exe
0: 172
1: 176
2: 180
3: 184
4: 188

C:\>hello.exe
0: 188
1: 124
2: 128
3: 132
4: 136

Offenbar haben die Sockets unter Windows eine andere Bedeutung: zwei aufeinander folgende Aufrufe erzeugen nicht die gleiche Ausgabe. Dennoch wiederholen sich die Werte teilweise (in diesem Fall die 188, dem Leser sei aber versichert, dass bei mehreren Aufrufen die Überschneidung teilweise deutlich größer ist). Die Werte sind nicht ganz zufällig, aber man sollte trotzdem nicht zu viel hinein interpretieren und sie einfach als eine magische Zahl sehen, die man bei einem Aufruf von socket() genannt bekommt und die man in genau der Form für spätere Aufrufe von Socket-Funktionen angeben muss.

Was hat es nun mit WSAStartup() und WSACleanup() auf sich? Diese Funktionen werden benötigt um das Winsock-Subsystem zu starten bzw. aufzuräumen. Am Besten nimmt man das einfach so hin. Von Bedeutung ist noch der erste Parameter von WSAStartup(): die Winsock-Version die benutzt werden soll. Zur Zeit der Erstellung dieses Tutorials sind die gültigen Werte 1.0, 1.1, 2.0, 2.1 und 2.2. Um Funktionen von Winsock 2 zu nutzen muss anstelle von wsock32.lib die Bibliothek ws2_32.lib benutzt werden. Da aber die Teile der BSD-Socket-API, die wir benutzen wollen, alle in Winsock 1 enthalten sind, Version 1.1 die höchste "1er-Version" ist, und nochdazu bei MAKEWORD(1, 1) die Reihenfolge der Parameter keine Rolle spielt, werden wir in diesem Tutorial immer Winsock 1.1 benutzen.

Da die Winsock-Bibliothek Teil der Windows-API ist, und die Windows-API gerne eigene Datentypen definiert, gibt es natürlich auch für Sockets einen speziellen Datentyp: SOCKET. Für den Wert eines ungültigen Sockets gibt es die Konstante INVALID_SOCKET. Der Typ SOCKET ist vorzeichenlos (auf meinem System: unsigned long long int), die Konstante INVALID_SOCKET ist auf (SOCKET)(~0) definiert. Es ist bestimmt kein Zufall, dass dieser Wert auf int gecastet genau -1 ergibt und damit kompatibel zur UNIX-Version der Socket-API ist. Man kommt daher üblicherweise ohne Probleme davon, wenn man Code von UNIX auf Windows übernimmt (von WSAStartup() und WSACleanup() abgesehen, die müssen natürlich sein!). Trotzdem werde ich in den Windows-spezifischen Beispielprogrammen die Windows-Datentypen benutzen.


Zurück