In diesem Tutorial wird gezeigt, wie man sich mit einem Server verbindet, Daten sendet und empfängt. Das Programm entspricht im wesentlichen einem einfachen telnet-Client.

Fehlerprüfung ist wichtig. Deshalb wird von jeder Funktion der Rückgabewert kontrolliert. Das Programm wird so grösser doch Fehler lassen sich einfacher aufspüren. Auch im fertigen Programm sollten Fehlerprüfungen niemals entfernt werden!

## Argumente

Um sich mit einem Server zu verbinden, braucht es einen Domain-Namen und einen Port. Das geben wir dem Programm über zwei Argumente beim Aufruf.

#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    const char *host;
    const char *service;

    if (argc != 3) {
        printf("usage: %s host service\n", argv[0]);
        return 1;
    }

    host = argv[1];
    service = argv[2];

...

Wie verbinden?

Das wichtigste hier ist getaddrinfo. Diese Funktion gibt verfügbare Verbindungsarten anhand festgelegten Einschränkungen als verkettet Liste zurück. Ausserdem löst sie Domain- und Service-Namen auf. So bekommt man auch ohne sich gross darüber Gedanken zu machen welche Adressfamilien (IPv4 oder IPv6) das Betriebssystem unterstützt, die richtigen Verbindungsarten. In diesem Fall soll eine Verbindung über TCP aufgebaut werden.

Der Service-Name kann z.B. «http» sein. getaddrinfo löst diesen dann zu Port 80 auf. Natürlich kann auch ein numerischer Port angegeben werden. Dieser muss jedoch als String vorliegen.

Wichtig ist, das hints mit memset vor dem befüllen geleert werden muss, weil sonst implementierungs-abbängige Felder nicht leer sind und unerwartete Fehler auftreten können. Ausserdem muss addr0 mit freeaddrinfo freigegeben werden, wenn es nicht mehr benötigt wird.

gai_strerror liefert den Fehlerstring falls ein Fehler aufgetreten ist.

...

    int error;
    struct addrinfo hints;
    struct addrinfo *addr0;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = 0;

    printf("Get resources...\n");

    if ((error = getaddrinfo(host, service, &hints, &addr0)) < 0) {
        perror(gai_strerror(error));
        return 1;
    }

...

Verbinden …

Diese Schleife probiert alle Verbindungsarten durch und endet, wenn eine Verbindung geklappt hat. Zuerst wird mit socket ein Socket mit der entsprechenden Adressfamilie, Socket-Typ und Protokoll erstellt. Dann wird versucht sich mit connect mit dem Server zu verbinden. Falls das nicht geklappt hat (z.B. wenn der Server kein IPv6 unterstützt), wird das Socket mit close wieder geschlossen und die nächste Verbindungsart ausprobiert.

Falls gar keine funktioniert hat, ist addr am Ende der Schleife NULL und es konnte keine Verbindung hergestellt werden. Ein nicht abgefangener Fall ist, wenn der Server nicht erreichbar ist. Dann blockiert connect womöglich endlos und das Programm läuft nicht weiter. Ein sauberes Connection-Timeout würde jedoch das Grundlagenthema sprengen. Darum gibts dazu ein eigenes Connection-Timeout-Tutorial.

...

    int sockno;
    struct addrinfo *addr;

    for (addr = addr0; addr; addr = addr->ai_next) {
        printf("Create socket (ai_family=%d, ai_socktype=%d, ai_protocol=%d)...\n"),
            addr->ai_family,
            addr->ai_socktype,
            addr->ai_protocol);

        if ((sockno = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol)) < 0) {
            perror("socket");
            continue;
        }

        printf("Connecting...\n");

        if ((connect(sockno, addr->ai_addr, addr->ai_addrlen)) < 0) {
            perror("connect");
            close(sockno);
            continue;
        }

        break;
    }

    if (!addr) {
        printf("No connection found\n");
        return 1;
    }

    freeaddrinfo(addr0);

...

Senden

Dieser Teil läuft in einer Endlosschleife ab und erlaubt ein kontinuierliches Schreiben und Lesen. Erst wenn der Server die Verbindung beendet oder wie hier der Text «exit» eingegeben wird, wird auch das Programm beendet.

Der Datentyp fd_set enthält eine Reihe von File-Descriptoren, die mit select überwacht werden sollen. Eine ausführliche Erklärung zu select würde das Grundlagenthema sprengen. Deshalb gibts auch hier wieder ein eigenes Select-Tutorial. Doch grob gesagt wartet select solange bis an einem der zu überwachenden File-Descriptoren Daten liegen. In diesem Fall hier wird der Standard-Input und das Server-Socket überwacht.

Wird also etwas in die Kommandozeile geschrieben oder Daten vom Server gesendet, kehrt select zurück. Dann wird geprüft welcher File-Descriptor betroffen ist. Sind Daten von der Kommandozeile vorhanden, werden diese eingelesen und direkt durch das Socket an den Server geschickt.

...

    char buffer[1024];
    fd_set read_set0;

    FD_ZERO(&read_set0);
    FD_SET(STDIN_FILENO, &read_set0);
    FD_SET(sockno, &read_set0);

    do {
        int res;
        ssize_t length;
        fd_set read_set = read_set0;

        printf("Select...\n");

        if ((res = select(sockno + 1, &read_set, NULL, NULL, NULL)) > 0) {
            if (FD_ISSET(STDIN_FILENO, &read_set)) {
                printf("Read STDIN...\n");

                if ((length = read(STDIN_FILENO, buffer, sizeof(buffer))) < 0) {
                    perror("read");
                    continue;
                }

                if (strcmp(buffer, "exit\n") == 0) {
                    printf("Exiting client\n");
                    break;
                }

                printf("Write socket...\n");

                if ((length = write(sockno, buffer, length)) < 0) {
                    perror("write");
                    continue;
                }
            }

...

EmpfangenSind Daten am Socket vorhanden, werden diese eingelesen und auf der Kommandozeile ausgegeben. Ein spezieller Fall tritt ein wenn 0 Bytes gelesen wurden, denn dann hat der Server die Verbindung beendet, also das Socket auf seiner Seite geschlossen. In diesem Fall wird die Schleife beendet und das Socket auf unserer Seite auch geschlossen.

...

            if (FD_ISSET(sockno, &read_set)) {
                printf("Read socket...\n");

                if ((length = read(sockno, buffer, sizeof(buffer))) < 0) {
                    perror("read");
                    continue;
                } else if (length == 0) {
                    printf("Server closed connection\n");
                    break;
                }

                printf("Write STDOUT...\n");

                if ((length = write(STDOUT_FILENO, buffer, length)) < 0) {
                    perror("write");
                    continue;
                }
            }
        } else if (res < 0 && errno == EINTR) {
            continue;
        } else {
            perror("select");
            return 1;
        }
    } while (1);

    close(sockno);

    return 0;
}

Ausführen

Das Programm wird mit zwei Parametern gestartet. Der Erste gibt den Host, der Zweite den Service an.

$ ./client example.com 80

Wenn jetzt keine Fehlermeldung erscheint, hat die Verbindung geklappt und es kann zum Beispiel durch die Eingabe von

GET /

eine Seite des Webservers abgefragt werden. Da Service-Namen durch getaddrinfo aufgelöst werden, kann auch folgende Eingabe gemacht werden. Sie hat den selben Effekt.

$ ./client example.com http

Obwohl das ganze noch ziemlich rudimetär ist, zeigt es doch in etwa die Grundlagen der Netzwerk-Programmierung. Im Tutorial habe ich meine eigenen Methoden verwendet, so wie ich vorgehen würde. Natürlich ist das nicht die ultimative Lösung. Denn wie schon erwähnt, ist keinerlei Timeout-Funktion vorhanden, was in einem richtigen Programm unentbehrlich wäre.