[X] Sockets und das HTTP-Protokoll



  • 1 Vorwort

    1.1 Einleitung

    Willkommen zu meinem ersten Tutorial, in dem ich euch erklären möchte, wie man Sockets unter Linux und Windows benutzt. Am Ende werden wir ein Programm geschaffen haben, welches, auf Eingabe einer URL, über das HTTP-Protokoll die entsprechende Datei herunterlädt und auf der Festplatte speichert. Ihr merkt, es geht in diesem Tutorial nur ums Verbinden, also fangt am besten nicht an mit lesen, wenn ihr eine Einführung in die Programmierung von Netzwerk-Spielen erwartet.

    1.2 Vorrausetzungen

    Unter Windows benutze ich als IDE Code::Blocks (http://www.codeblocks.org) und den GNU-GCC-Compiler in der Version 3.4.4.
    Unter Linux verwende ich gedit (http://www.gnome.org/projects/gedit/) und den GNU-GCC-Compilier in der Version 4.1.2.

    Ihr solltet C++-Kenntnisse in Exceptions, Stringstreams und Filestreams haben. Außerdem ist Erfahrung mit Zeiger in C empfehlenswert.

    1.3 Linken der benötigten Libs

    Wenn ihr unter Linux seid, braucht ihr gar nichts tun. Falls ihr aber unter Windows arbeitet, müsst ihr noch die benötigte Winsock-Library linken. Die Name der Lib lautet libws2_32.a. Falls diese bei euch nicht vorhanden ist, sucht ihr einfach irgendeine raus die nach Sockets klingt und probiert sie mal. Solltet das nicht helfen, fragt am besten im im Compiler- oder Winapi-Forum. Hier 2 Screenshots für das Hinzufügen mit Code::Blocks:


    Abb. 1.3.1 Auswählen der Build-Options


    Abb. 1.3.2 Eingeben der Winsock-Lib.

    2 Das erste Socket-Programm

    Unser ersten Programm soll einfach nur eine Verbindung aufbauen über den Port 80, welches der standardmäßige Port für das HTTP-Protokoll ist.
    Zuerst müsst ihr die nötigen Headerdateien inkludieren:

    #include <iostream>
    #ifdef linux
    #include <sys/socket.h> // socket(), connect()
    #include <arpa/inet.h> // sockaddr_in
    #else
    #include <winsock2.h>
    #endif
    
    int main()
    {
        using namespace std;
    

    Die Konstante linux wird von meinem Compiler automatisch definiert, wenn ich unter Linux bin, ihr können auch einfach nur den einen Code nehmen, falls euch das andere Betriebssystem nicht interessiert.
    Die iostream-Headerdatei ist klar; für Windows müssen wir nur die winsock2.h inkludieren, dort sind alle Funktionen für die zweite Winsock-Version definiert. Bei Linux sind die einzelnen Sachen in verschiedene Dateien aufgeteilt.

    Folgender Code ist nur für Windows wichtig:

    #ifndef linux
        WSADATA w;
        if(WSAStartup(MAKEWORD(2,2), &w) != 0)
        {
            cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << endl;
            return 1;
        }
    #endif
    

    Mit der Funktion WSAStartup wird Windows mitgeteilt, dass man gerne Zugriff auf die Winsock-Library haben will. Die Parameter sind eigentlich unwichtig, falls diese euch dennoch interessieren, könnt ihr auf der MSDN-Seite nachschauen was sie bedeuten. Das einzige was wir uns merken müssen ist, dass jedes Winsock-Programm mit dieser Funktion starten sollte, bevor es irgendwelche anderen Socket-Funktionen aufruft.

    Nun geht es weiter in der main-Funktion:

    int Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(Socket == -1)
        {
            cout << "Socket konnte nicht erstellt werden!" << endl;
            return 1;
        }
    

    Die socket()-Funktion erstellt ein neues Socket, einen „Netzwerkanschluss“, und gibt dessen ID zurück. Mit dieser ID, können wir später auf dem Socket Daten senden, bzw. empfangen. Ein negativer Wert stellt hierbei einen Fehler beim erstellen dar. Weitere Infos zu der socket-Funktion gibts hier.

    Hinweis: Als Windows-User werdet ihr wahrscheinlich auf den Datentyp SOCKET treffen. Dieser ist ein typedef auf unsigned int. Trotzdem könnt ihr ein Socket einfach als int behandeln, da es eine implizite Umwandlung zwischen diesen Typen gibt.

    Nachdem wir ein Socket erstellt haben, müssen wir eine Verbindung aufbauen.
    Jede Verbindung hat verschiedene Parameter, die in einer Struktur namens sockaddr gespeichert. Ihr müsst euch das so vorstellen, dass die sockaddr, eine abstrakte Basisklasse ist, sie wird nie benutzt, und Strukturen wie sockaddr_in sind die abgeleiteten Klassen.
    (Das man nicht gleich ein Klassendesign gewählt hat, liegt daran, dass Sockets auch unter C funktionieren sollen)
    Wir benötigen nur die sockaddr_in, sie ist für die normalen, vierstelligen IP-Adressen zuständig (z.B. 192.168.114.100). Falls Ihr euch mit den neuen IPv6 beschäftigen wollen, benötigt Ihr dann eine andere sockaddr-Struktur. Eine gute Übersicht gibt's hier.
    Der erste Parameter dieser Strukturen ist immer der Typ der sockaddr-Struktur:

    sockaddr_in service; // Normale IPv4 Struktur
        service.sin_family = AF_INET; // AF_INET für IPv4, für IPv6 wäre es AF_INET6
    

    Als nächstes müssen wir einen Port festlegen, auf dem wir connecten wollen, dies wäre beim HTTP-Protokoll der Port 80. Hierbei ist zu beachten, dass die Struktur den Port in umgekehrter Bytereihenfolge speichert. Also zuerst der eigentlich hintere Byte von short und dann der vordere. Klingt komisch, ist aber eigentlich gar nicht so schwer, denn um eine normale Zahl in die umgekehrte Reihenfolge zu bringen, gibt es schon die Funktion htons(). Für genauere Infos sucht Ihr im Internet nach big-endian.

    service.sin_port = htons(80); // Das HTTP-Protokoll benutzt Port 80
    

    Falls ihr noch Probleme habt, das mit der Bytereihenfolge zu verstehen, hier ein kleines Beispiel für eine mögliche Implementierung dieser Funktion:

    unsigned short my_htons(unsigned short h)
    {
        char* p = reinterpret_cast<char*>(&h);
        char n[2];
        n[0] = p[1];
        n[1] = p[0];
        return *reinterpret_cast<unsigned short*>(n);
    }
    

    Falls ihr später htons durch my_htons ersetzt, sollte es genauso funktionieren.

    Jetzt kommen wir zum Interessanten: Der IP. Bei unserem ersten Programm soll der User erstmal nur eine IP eingeben zu der dann verbunden wird.

    string ip;
        cout << "IP: ";
        cin >> ip;
    

    Nun wird es wieder etwas schwieriger. Wie ihr wisst wird die IP in der Form 123.44.32.99 dargestellt. Die einzelnen vier Werte gehen von 0 bis 255, und da muss jedem Programmierer sofort auffallen, dass dies ein unsigned char ist. Also haben wir 4 unsigned chars die durch einen Punkt getrennt sind. Dies ist aber nur eine für Menschen leserlich gemachte Form. In Wirklichkeit speichert der Computer natürlich keinen String sondern die 4 Bytes direkt. Also haben wir ein 4 Byte großes Array aus unsigned char. Klingt logisch; macht euch das nicht misstrauisch? Richtig! Natürlich speichert man kein Array, sondern einen unsigned long. Wäre vielleicht ja noch ganz logisch, wenn man diesen anstelle einer Struktur benutzt, aber ne, packen wir den nochmal in eine Struktur rein. Irgentwie bekloppt, oder?

    struct in_addr
    {
        unsigned long s_addr; // long ist 4 bytes also 4 chars groß
    };
    

    Dass dies irgendwie sinnlos ist, haben sich die Leute von Microsoft wohl auch gedacht. Also haben sie die in_addr bei Winsock neu entworfen. In deren Struktur gibt es vier unsigned chars, zwei unsigned shorts und einen unsigned long. Damit das ganze kompatibel ist hat man diese Variablen in eine union gepackt. Somit lassen sich immernoch Casts von unsigned long* zu einem in_addr* durchführen, dazu aber später mehr. Wichtig ist: Vergesst die Microsoft-Version der in_addr und stellt euch einfach vor, die in_addr-Struktur enthält nur einen unsigned long, der die IP-Adresse in binärer Form speichert.

    Da wir ja wollen, dass der User nicht die IP-Adresse binär eingibt, sondern in der gewohnten Form, müssen wir sie umwandeln. Zum Glück gibt es dafür auch schon eine Funktion namens inet_addr. Diese gibt einen unsigned long zurück, den wir dann einfach an den s_addr Member der in_addr Struktur übergeben. Dieser heißt in der sockaddr_in-Struktur sin_addr (Siehe Übersicht der sockaddr_in-Struktur).

    service.sin_addr.s_addr = inet_addr(ip.c_str());
    

    Nun haben wir alle Informationen für eine Verbindung an die vom User eingegebene IP gespeichert. Es ist an der Zeit nun wirklich zu verbinden. Dazu gibt es die Funktion connect. Diese erwartet als ersten Parameter ein Socket, als zweiten einen Zeiger auf unsere sockaddr-Struktur, den wir natürlich casten müssen, da wir ja eine sockaddr_in Struktur in Wirklichkeit haben. Und als letztes die Länge unserer sockaddr-Struktur. Die Länge muss übergeben werden, weil... äh.. ist eigentlich eine gute Frage, denn theoretisch, kann die connect-Funktion ja durch die sin_family herausfinden wie lang die bestimmte sockaddr-Struktur ist. Aber hier hat man sich anscheinend dazu entschieden, die Länge zur Sicherheit auch noch zu übergeben, vielleicht weil sie nicht plattformunabhängig ist.

    int result = connect(Socket, reinterpret_cast<sockaddr*>(&service), sizeof(service));
    

    Der Rückgabewert von connect ist bei einem Fehler -1. Dies Überprüfen wir und geben bekannt, falls die Verbindung fehlgeschlagen ist:

    if(result == -1)
        {
            cout << "Verbindung fehlgeschlagen!" << endl;
            return 1;
        }
    
        cout << "Verbindung erfolgreich!" << endl;
    

    An dieser Stelle haben wir jetzt eine Verbindung mit dem Server aufgebaut und könnten theoretisch Daten austauschen, das verschieben wir aber mal lieber auf das nächste Kapitel und beenden jetzt schon die Verbindung:

    #ifdef linux
        close(Socket);
    #else
        closesocket(Socket);
    #endif
    }
    

    Diese Funktion schließt das Socket, dessen Identifizierungsnummer hier übergeben wird. Unter Linux heißt sie close() und unter Windows closesocket(). Falls wir hier nach nochmal connect() oder eine andere Funktion aufrufen, die ein Socket erwartet, werden wir eine Fehlermeldung erhalten, denn diese ID zeigt nicht auf ein gültiges Socket. Wir beachten, dass die closesocket-Funktion threadsicher ist und alle blockenden Socket-Funktionen, die dieses Socket benutzen, stoppt. Auch wird ein bestimmter Status gesendet, sodass das Gegenüber weiß, dass die Verbindung geschlossen wurde. Also schreckt nicht ab close zu benutzen.

    Unser Programm sollte nun so aussehen. Nun können wir es compilieren und es sollte keine Fehlermeldung erscheinen. Testet eine IP, hinter der ihr einen Webserver wisst. Falls euch keine einfällt, könnt ihr z.B. mit dem Konsolen-Befehl

    nslookup www.google.de
    

    die IP-Adresse von Google erfahren. (Die Konsole startet ihr unter Windows mit [Windowstaste]+[R] und dann „cmd“ ausführen.)
    Die Verbindung sollte erfolgreich zustande kommen. Probiert auch einfach irgendeinen Dreck aus, nun sollte es eine Fehlermeldung geben. Eine Konsolenausgabe könnte so aussehen (Linux):

    jhasse@jhasse-desktop:~/C++/http$ g++ 01.cpp
    jhasse@jhasse-desktop:~/C++/http$ nslookup www.google.de
    Server:         217.237.149.161
    Address:        217.237.149.161#53
    
    Non-authoritative answer:
    www.google.de   canonical name = www.google.com.
    www.google.com  canonical name = www.l.google.com.
    Name:   www.l.google.com
    Address: 209.85.129.104
    Name:   www.l.google.com
    Address: 209.85.129.147
    Name:   www.l.google.com
    Address: 209.85.129.99
    
    jhasse@jhasse-desktop:~/C++/http$ ./a.out
    IP: 209.85.129.104
    Verbindung erfolgreich!
    jhasse@jhasse-desktop:~/C++/http$ ./a.out
    IP: 209.85.129.147
    Verbindung erfolgreich!
    jhasse@jhasse-desktop:~/C++/http$ ./a.out
    IP: 209.85.129.99
    Verbindung erfolgreich!
    jhasse@jhasse-desktop:~/C++/http$ ./a.out
    IP: 123.123.123.123
    Verbindung fehlgeschlagen!
    jhasse@jhasse-desktop:~/C++/http$
    

    Bis hier hin solltet ihr alles verstanden haben. Falls es dennoch Probleme gibt, schaut noch mal in die Referenzen und anderen Tutorials (siehe Links am Ende).

    3 Unser eigenes nslookup

    Jetzt ist es natürlich sehr nervig, dass man immer eine IP eingeben muss. Es wäre doch viel praktischer, wenn man wie beim Browser einfach einen Namen eingibt, und das Programm diese automatisch auflöst. Also fangen wir an: Programmieren wir uns unser eigenes nslookup.

    Die Funktion die uns interessiert heißt gethostbyname und gibt einen Zeiger auf eine hostent-Struktur zurück. Diese wollen wir uns mal genauer angucken:

    struct hostent
    {
        char* h_name; /* Offizieller Name des Host */
        char** h_aliases; /* Weitere Namen für diesen Host */
        int h_addrtype; /* Adresstyp, meistens AF_INET */
        int h_length; /* Länge einer IP-Adresse in Bytes, meistens 4 */
        char** h_addr_list; /* Die IP-Adressen */
    };
    

    Der erste Parameter, ist ein C-Array, der den Namen des Host speichert. Dieser ist für uns später unwichtig genauso wie die weiteren Namen des Hosts. Da diese mehrere sein können, haben wir eine Liste aus Zeiger, bzw aus C-Arrays. Das wird jetzt sehr kompliziert, deswegen ein kurzes Beispiel:

    char* = C-String
    char** = C-String*
    

    Wir haben also einen Zeiger auf einen C-String. Aber wozu einen Zeiger? Klar, weil es sich um ein dynamisches Array handelt:

    Dynamisches Array in C:     T* p = (T)malloc(size);
    Dynamisches Array in C++:     std::vector<T> v(size);
    

    Wenn also in C ein dynamisches Array ein Zeiger ist, der auf einen Speicherbereich zeigt, dann ist es in C++ ein Vector. Also würde h_aliases in C++ so aussehen:

    std::vector<std::string> h_aliases;
    

    Schade, dass die hostent Struktur in C geschrieben wurde, aber ich hoffe Ihr habt das Prinzip verstanden.

    Der nächste Typ beschreibt den Adresstyp, bei uns also einfach nur AF_INET, IPv6-Adressen sind uns egal. Also ist auch der nächste Parameter nicht relevant und sollte eigentlich immer 4 betragen.

    Wichtig ist nun die Liste der IP-Adressen. Diese sieht am Anfang genau so aus wie die Liste der Aliases, doch passt auf: es ist nicht das gleiche. Hier haben wir keine Strings, denn die IP-Adressen werden, wie in der sockaddr, binär gespeichert.

    Da ein Bild mehr als 1000 Worte sagt, ist hier mal ein Bild:

    Dies ist der grundsätzliche Aufbau der hostent Struktur. Kommen wir zurück zur gethostbyname-Funktion. Diese erwartet einen C-String in dem der Hostname in der Form www.bla.de enthalten ist. Unser neues Programm fängt also so an:

    #include <iostream>
    #ifdef linux
    #include <netdb.h> // gethostbyname(), hostent
    #include <arpa/inet.h> // inet_ntoa()
    #else
    #include <winsock2.h>
    #endif
    
    int main()
    {
        using namespace std;
    
    #ifndef linux
        WSADATA w;
        if(WSAStartup(MAKEWORD(2,2), &w) != 0)
        {
            cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << endl;
            return 1;
        }
    #endif
    
        cout << "Bitte gebe einen Hostnamen ein: ";
        string Hostname;
        cin >> Hostname;
    
        hostent* phe = gethostbyname(Hostname.c_str());
    

    Dass der Zeiger auf die hostent-Struktur wieder freigegeben wird, soll uns nicht kümmern, denn dies wird automatisch erledigt. Erstmal sollten wir checken ob der Hostname überhaupt existiert:

    if(phe == NULL)
        {
            cout << "Host konnte nicht aufgeloest werden!" << endl;
            return 1;
        }
    

    Nun geben wir den Namen sowie die Aliases aus:

    cout << "\nHostname: " << phe->h_name << endl
             << "Aliases: ";
    
        for(char** p = phe->h_aliases; *p != 0; ++p)
        {
            cout << *p << " ";
        }
        cout << endl;
    

    Die Funktion der For-schleife ist nicht ganz einfach: p zeigt auf den ersten C-String und wird nach jedem Schleifendurchlauf um 1 erhöht, bis wir einen Nullzeiger haben. Wichtig: Keinen leeren String der ein '\0' enthält, sondern die Liste von Zeigern auf C-Strings enthält einen Nullzeiger, der nicht auf einen C-String zeigt.

    Als nächstes wird einfach nur überprüft, ob es sich bei den IPs um IPv4-Adressen handelt. IPv6 lassen wir außer Acht.

    if(phe->h_addrtype != AF_INET)
        {
            cout << "Ungueltiger Adresstyp!" << endl;
            return 1;
        }
    
        if(phe->h_length != 4)
        {
            cout << "Ungueltiger IP-Typ!" << endl;
            return 1;
        }
    

    Nun wollen wir diese IP-Adressen ausgeben, doch sie liegen in binärer Form vor. Also müssen wir sie umwandeln, in einen String und hierzu gibt es die Funktion inet_ntoa(). Sie wandelt eine hostent-Struktur in einen String um, in der Form „x.x.x.x“. Leider haben wir keine Zeiger auf hostent-Strukturen sondern Zeiger auf chars. Da die hostent-Struktur die Daten auch nur binär speichert, können wir den Zeiger einfach in einen hostent-Zeiger casten:

    reinterpret_cast<in_addr*>(*phe->h_addr_list);
    

    Dieser neue Zeiger muss nun noch dereferenziert werden, da inet_ntoa() eine Instanz und keinen Zeiger erwartet:

    cout << inet_ntoa(*reinterpret_cast<in_addr*>(*phe->h_addr_list));
    

    Nun müssen wir noch, wie bei den Aliases die Liste wirklich durchgehen, und nicht einfach nur das erste Element nehmen, da ein Host ja auch mehrere IPs haben kann. Der endgültige Code sieht also so aus:

    cout << "IP-Adressen: ";
        for(char** p = phe->h_addr_list; *p != 0; ++p)
        {
            cout << inet_ntoa(*reinterpret_cast<in_addr*>(*p)) << " ";
        }
        cout << endl;
    }
    

    Um die Funktion der inet_ntoa-Funktion etwas klarer zu machen, hier nochmal eine mögliche Implementierung:

    #include <sstream>
    std::string my_inet_ntoa(in_addr& ip)
    {
        unsigned char* p = reinterpret_cast<unsigned char*>(&ip);
        std::stringstream sstream;
        for(int i = 0; i < 4 && (i == 0 || (sstream << ".")); ++i)
        {
            sstream << static_cast<int>(p[i]);
        }
        return sstream.str();
    }
    

    Nun sind wir schon fertig. Das endgültiges Programm könnt ihr nun testen, mit einer Adresse wie www.google.de oder www.microsoft.com (oder www.kernel.org :P). Alles sollte klappen und als nächstes wollen wir diesen Code in das vorherige Programm einfügen.

    4 Integration von nslookup

    Nun wollen wir unser eigenes nslookup in das Programm aus Kapitel 2 einfügen. Danach sollte der Benutzer nur noch den Namen der Internetseite eingeben müssen und das Programm kümmert sich um die Verbindung. Dies klingt einfacher als es ist, da ein Host ja mehrere IP-Adressen haben kann und diese müssen alle ausprobiert werden bevor gesagt wird: "Keine Verbindung möglich!". Denn das wäre ja gelogen 😉

    Fangen wir zuerst an wie beim Programm vom vorherigen Kapitel:

    #include <iostream>
    #ifdef linux
    #include <netdb.h> // gethostbyname(), hostent
    #include <arpa/inet.h> // inet_ntoa()
    #else
    #include <winsock2.h>
    #endif
    
    int main()
    {
        using namespace std;
    
    #ifndef linux
        WSADATA w;
        if(WSAStartup(MAKEWORD(2,2), &w) != 0)
        {
            cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << endl;
            return 1;
        }
    #endif
    
        cout << "Bitte gebe einen Hostnamen ein: ";
        string Hostname;
        cin >> Hostname;
    
        hostent* phe = gethostbyname(Hostname.c_str());
    
        if(phe == NULL)
        {
            cout << "Host konnte nicht aufgeloest werden!" << endl;
            return 1;
        }
    
        cout << "\nHostname: " << phe->h_name << endl
             << "Aliases: ";
    
        for(char** p = phe->h_aliases; *p != 0; ++p)
        {
            cout << *p << " ";
        }
        cout << endl;
    
        if(phe->h_addrtype != AF_INET)
        {
            cout << "Ungueltiger Adresstyp!" << endl;
            return 1;
        }
    
        if(phe->h_length != 4)
        {
            cout << "Ungueltiger IP-Typ!" << endl;
            return 1;
        }
    

    Nun müssen wir jede einzelne IP-Adresse testen ob sie klappt. Dazu gehen wir die Liste vom Anfang durch und sobald wir eine Verbindung gefunden haben, fahren wir fort. Falls das Ende der Liste erreicht wurde und keine IP-Adresse klappte, brechen wir ab. Mit ein bisschen Logik kann man daraus eine Schleife erstellen, ich habe es so gemacht:

    int Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(Socket == -1)
        {
            cout << "Socket konnte nicht erstellt werden!" << endl;
            return 1;
        }
    
        sockaddr_in service;
        service.sin_family = AF_INET;
        service.sin_port = htons(80); // Das HTTP-Protokoll benutzt Port 80
    
        char** p = phe->h_addr_list; // p mit erstem Listenelement initialisieren
        int result; // Ergebnis von connect
        do
        {
            if(*p == NULL) // Ende der Liste
            {
                cout << "Verbindung fehlgschlagen!" << endl;
                return 1;
            }
    
            service.sin_addr.s_addr = *reinterpret_cast<unsigned long*>(*p);
            ++p;
            result = connect(Socket, reinterpret_cast<sockaddr*>(&service), sizeof(service));
        }
        while(result == -1);
    
        cout << "Verbindung erfolgreich!" << endl;
    

    Ich erstelle eine Variable result, die das Ergebnis von connect speichert, eine Variable p die als Iterator dient. Als erstes prüfe ich ob ein Nullzeiger vorliegt, also ob die Liste zu Ende ist. Wenn nicht, erstelle ich meine binäre IP-Adresse und erhöhe danach schonmal den p-Zeiger (irgendwo muss ich's ja tun). Jetzt wird versucht zu verbinden. Falls result -1 ist, also fehlgeschlagen, dann wird das Ganze wiederholt, wenn nicht geht's nach der Schleife weiter.

    Nun müssen wir nur noch die Verbindung beenden und unser Programm ist fertig.

    5 Senden und Empfangen

    5.1 Grundlegender Aufbau des HTTP-Protokolls

    Nun wollen wir uns endlich mit dem Senden und Empfangen von Daten beschäftigen. Zuerst einmal, was bei uns eine Internetadresse ist, besteht aus folgenden Abschnitten:

    http://www.kernel.org/faq/index.html
      1  |       2       |     3
    

    Zuerst kommt das Protokoll (1), danach der Host den wir auflösen (2) und als letztes die Datei bzw. der Pfad den wir an den Webserver schicken müssen. Die Kommunikation zwischen Browser und Webserver geschieht hierbei über das HTTP-Protokoll. Es handelt sich also um eine Sprache mit der der Dateiaustausch über das Internet geregelt werden kann. Ein anderes Protokoll wäre z.B. das FTP-Protokoll.
    Nun müsst ihr wissen, wie das HTTP-Protokoll funktioniert: Ein Webserver horcht auf Port 80. Sobald sich ein Client verbindet wird aufs Empfangen von Daten gewartet. Der Client, meistens der Browser, in diesem Fall aber unser Programm, schickt nun eine Anfrage welche Datei er haben möchte. Bei dieser Anfrage handelt sich um einfachen Text im ASCII-Format (Deswegen auch unter anderem die Probleme beim realisieren von Domainnamen mit Umlauten). Eine Anfrage, die wir dem Server nach dem Verbindungsaufbau schicken, sieht so aus:

    GET /faq/index.html HTTP/1.1
    Host: www.kernel.org
    (hier eine leere Zeile)
    

    Zuerst kommt ein Befehl, in diesem Fall GET, gefolgt von einem Leerzeichen. Nun kommt die Datei, die wir anfordern, wieder ein Leerzeichen und dann die Protokollversion (HTTP/1.0 wäre zum Beispiel die ältere Variante). Nun kommen wir in die nächste Zeile, dabei müssen wir aber eines beachten: Die Zeilenumbrüche sind hier im DOS-Format, das heißt wir haben zuerst ein \r-Zeichen (ASCII-Code 13) und ein \n-Zeichen (ASCII-Code 10). Wollen wir nun also einen String erstellen der unsere Anfrage enthält würde es so aussehen:

    const string request = "GET /faq/index.html HTTP/1.1\r\nHost: www.kernel.org\r\n\r\n";
    

    Am Ende haben wir ein \r\n\r\n, hierbei handelt es sich einfach um die leere Zeile, die am Ende gesendet werden muss, damit der Server erkennt, dass hier die Anfrage zu Ende ist.
    Nach der Anfrage kommt natürlich die Antwort. Diese besitzt auch einen ganz bestimmten Aufbau, den wir uns aber erst im nächsten Kapitel anschauen wollen. Jetzt kommen wir ersteinmal zum allgemeinen Senden und Empfangen von Daten.

    5.2 Die Funktionen send und recv

    Das Senden bzw. Empfangen geschieht mit Hilfe von folgenden Funktionen:

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

    Der Aufbau beider Funktion ist ziemlich gleich. Der erste Parameter ist der Socket auf dem gesendet werden soll, der zweite ein Zeiger auf einen Binärenspeicherbereich mit der Länge len. Der letzte Parameter flags ist für uns unwichtig und es wird einfach 0 übergeben.
    Nicht zu vergessen ist der Rückgabewert: Hierbei handelt es sich um die Anzahl der übertragenen Bytes. Denn auch wenn man versucht einen Buffer von 100 Byte übertragen will kann send damit nicht fertig werden und vorher schon aufhören und dann die geschaffte Anzahl übertragen, z.B. 34. Unsere Aufgabe ist es also den restlichen Buffer von 64 Bytes noch zu senden. Dazu schreiben wir eine Funktion, die uns die Arbeit immer abnehmen soll:

    void SendAll(int socket, const char* const buf, const size_t size)
    {
        size_t bytesSent = 0;
        do
        {
            bytesSent += send(socket, buf + bytesSent, size, 0);
        } while(bytesSent < size);
    }
    

    Diese Funktion sendet den von uns übergebenen Buffer komplett. Doch was passiert nun, wenn während dem Senden, ein Fehler auftritt, z.B. bricht die andere Seite die Verbindung ab oder sie geht verloren. Dies signalisiert uns send(), indem es uns einen Wert gleich 0, für eine normale Beendigung der Verbindung, oder negativ, für einen Fehler zurückgibt. Wir behandeln hier erstmal beide Fehler gleich, da wir ja auch eine Verbindungsbeendigung während des Sendens nicht erwarten. Da sie beide unerwartet sind nehmen wir hierfür Exceptions. Hierzu werden wir die std::runtime_error-Klasse. Jedes mal wenn wir sie werfen wollen, lassen wir sie von einer Funktion erstellen:

    std::runtime_error CreateSocketError()
    {
    

    Die Signatur der Funktion ist selbsterklärend, doch wenn wir zum Inhalt kommen wird's kompliziert. Leider sind die Funktionen zum erhalt von Fehler-Meldungen unter Windows und Linux ziemlich verschieden. Alle Linux-Fans können jetzt aufatmen:

    std::ostringstream temp;
    #ifdef linux
        temp << "Socket-Fehler #" << errno << ": " << strerror(errno);
    

    Da der Konstruktor von std::runtime_error einen std::string erwartet verwenden wir einen std::ostringstream zur einfachen Formatierung. Wir geben eine Fehlernummer aus, gefolgt von einem Text, der den Fehler beschreibt. Natürlich müssen wir uns dies nicht selbst ausdenken. Eine Fehlernummer wird in der globalen Variable errno gespeichert, mit der Funktion strerror(int) erzeugen wir einen Text den wir ausgeben können. Hierzu muss man nur noch die errno.h am Anfang inkludieren.

    Unter Windows sieht das ganze so aus:

    #else
        int error = WSAGetLastError();
        char* msg;
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                      NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                      reinterpret_cast<char*>(&msg), 0, NULL);
        temp << "Socket-Fehler #" << error << ": " << msg;
    #endif
    

    Hier gibt die Funktion WSAGetLastError() eine Fehlernummer zurück und FormatMessage erzeugt daraus einen Text. Die genaue Funktionsweise lässt sich in der MSDN nachlesen.

    Am Ende unserer Funktion erstellen wir eine Instanz der std::runtime_error-Klasse und geben sie zurück:

    return std::runtime_error(temp.str());
    }
    

    Nun wollen wir von unserer neuen Funktion Gebrauch machen und fügen sie in die SendAll-Funktion ein:

    void SendAll(int socket, const char* const buf, const size_t size)
    {
        size_t bytesSent = 0; // Anzahl Bytes die wir bereits vom Buffer gesendet haben
        do
        {
            bytesSent += send(socket, buf + bytesSent, size, 0);
            if(bytesSent <= 0) // Wenn send einen Wert <= 0 zurück gibt deutet dies auf einen Fehler hin.
            {
                throw CreateSocketError();
            }
        } while(bytesSent < size);
    }
    

    Nun entwickeln wir noch eine Funktion für das Empfangen von Daten. Da das HTTP-Protokoll ja zeilenweise sendet, soll unsere Funktion auch so aufgebaut sein:

    // Liest eine Zeile des Sockets in einen stringstream
    void GetLine(int socket, std::stringstream& line)
    {
    

    Wir lesen byteweise von dem Socket bis wir auf einen Zeilenumbruch treffen.

    for(char c; recv(socket, &c, 1, 0) > 0; line << c)
        {
            if(c == '\n')
            {
                return;
            }
        }
    

    Die Schleife wird nur verlassen wenn recv einen Wert kleiner oder gleich 0 zurück gibt, somit werfen wir dahinter unsere Exception.

    throw CreateSocketError();
    }
    

    5.3 Request

    Nun wollen wir ein Programm erstellen, dass all diese Sachen anwendet. Wir schicken einen Request an www.kernel.org und geben dann erstmal einfach die Ausgabe auf der Konsole aus. Unser Programm fängt so an:

    #include <iostream>
    #include <fstream>
    #include <stdexcept> // runtime_error
    #include <sstream>
    #ifdef linux
    #include <sys/socket.h> // socket(), connect()
    #include <arpa/inet.h> // sockaddr_in
    #include <netdb.h> // gethostbyname(), hostent
    #include <errno.h> // errno
    #else
    #include <winsock2.h>
    #endif
    
    // Hier die Funktionen CreateSocketError, SendLine und GetLine einfügen
    
    int main()
    {
        using namespace std;
    
    #ifndef linux
        WSADATA w;
        if(WSAStartup(MAKEWORD(2,2), &w) != 0)
        {
            cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << endl;
            return 1;
        }
    #endif
    
        hostent* phe = gethostbyname("www.kernel.org");
    
        if(phe == NULL)
        {
            cout << "Host konnte nicht aufgeloest werden!" << endl;
            return 1;
        }
    
        cout << "\nHostname: " << phe->h_name << endl
             << "Aliases: ";
    
        for(char** p = phe->h_aliases; *p != 0; ++p)
        {
            cout << *p << " ";
        }
        cout << endl;
    
        if(phe->h_addrtype != AF_INET)
        {
            cout << "Ungueltiger Adresstyp!" << endl;
            return 1;
        }
    
        if(phe->h_length != 4)
        {
            cout << "Ungueltiger IP-Typ!" << endl;
            return 1;
        }
    
        int Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if(Socket == -1)
        {
            cout << "Socket konnte nicht erstellt werden!" << endl;
            return 1;
        }
    
        sockaddr_in service;
        service.sin_family = AF_INET;
        service.sin_port = htons(80); // Das HTTP-Protokoll benutzt Port 80
    
        char** p = phe->h_addr_list; // p mit erstem Listenelement initialisieren
        int result; // Ergebnis von connect
        do
        {
            if(*p == NULL) // Ende der Liste
            {
                cout << "Verbindung fehlgschlagen!" << endl;
                return 1;
            }
    
            service.sin_addr.s_addr = *reinterpret_cast<unsigned long*>(*p);
            ++p;
            result = connect(Socket, reinterpret_cast<sockaddr*>(&service), sizeof(service));
        }
        while(result == -1);
    
        cout << "Verbindung erfolgreich!" << endl;
    

    Wie Sie sehen, nehmen wir als Hostnamen www.kernel.org, denn die Seite www.kernel.org/faq/index.html soll unsere Testseite werden. Nun wollen wir unsere Anfrage senden:

    const string request = "GET /faq/index.html HTTP/1.1\r\nHost: www.kernel.org\r\nConnection: close\r\n\r\n";
    
        SendAll(Socket, request.c_str(), request.size());
    

    Dies ist die gleiche Request bis auf das Hinzufügen von dieser Zeile:

    Connection: close
    

    Sie bewirkt, dass nachdem wir die Antwort erhalten haben, der Server die Verbindung schließt. Die Verbindung könnte z.B. aufrecht erhalten bleiben, wenn wir weitere Request hinterherschicken wollten um z.B. Bilder zu laden.
    Da das Behandeln der Response ins nächste Kapitel kommt, wollen wir jetzt erstmal die Antwort nur in eine Textdatei schreiben:

    ofstream fout("output.txt");
    
        cout << "Empfange und schreibe Antwort in output.txt..." << endl;
        while(true)
        {
            stringstream line;
            try
            {
                GetLine(Socket, line);
            }
            catch(exception& e) // Ein Fehler oder Verbindungsabbruch
            {
                break; // Schleife verlassen
            }
            fout << line.str() << endl; // Zeile in die Datei schreiben.
        }
    

    Am Ende schließen wir gewohnt das Socket und unser Programm sieht so aus.

    Wenn wir es ausführen wird die Antwort die Response vom Server in die output.txt-Datei geschrieben. Diese sollte ungefähr so anfangen:

    HTTP/1.1 200 OK
    Date: Fri, 15 Dec 2006 14:51:19 GMT
    Server: Apache/2.2.2 (Fedora)
    Last-Modified: Wed, 08 Nov 2006 22:06:19 GMT
    ETag: "b643c8-6118-421c3874aacc0"
    Accept-Ranges: bytes
    Content-Length: 24856
    Connection: close
    Content-Type: text/html
    X-Pad: avoid browser bug
    
    <?xml version="1.0" encoding="utf-8"?>
    ...
    

    6 Response

    6.1 Statuszeile

    Die erste Zeile beschreibt einen Statuscode. Sie besteht aus der HTTP-Version des Servers, einer Status-Nr und einem Text der diese beschreibt. Eine Liste aller Codes gibt's unter anderem bei Wikipedia. Für uns wichtig ist zuerst einmal der Code 200: Er steht dafür, dass alles geklappt hat, unsere Anfrage in Ordnung war und die Datei existiert. Ein weiterer wichtiger Status Code ist 100. Er sagt uns, dass der Server noch etwas Zeit braucht die Anfrage zu verarbeiten und uns gleich noch eine Antwort schicken wird. Das könnte z.B. so aussehen:

    HTTP/1.1 100 Continue
    
    HTTP/1.1 200 OK
    Date: Fri, 15 Dec 2006 14:51:19 GMT
    Server: Apache/2.2.2 (Fedora)
    ...
    

    Nach der ersten Antwort die uns nur sagt, dass es gleich weiter geht, kommt eine freie Zeile und dann die richtige Antwort.
    In unserem letzten Programm machen wir jetzt nach dem Verbinden so weiter:

    const string request = "GET /faq/index.html HTTP/1.1\r\nHost: www.kernel.org\r\nConnection: close\r\n\r\n";
        try
        {
            SendAll(Socket, request.c_str(), request.size());
    
            int code = 100; // 100 = Continue
            string Protokoll;
            stringstream firstLine; // Die erste Linie ist anders aufgebaut als der Rest
            while(code == 100)
            {
                GetLine(Socket, firstLine);
                firstLine >> Protokoll;
                firstLine >> code;
                if(code == 100)
                {
                    GetLine(Socket, firstLine); // Leere Zeile nach Continue ignorieren
                }
            }
            cout << "Protokoll: " << Protokoll << endl;
    

    Mit der GetLine-Funktion (siehe oben) lesen wir die erste Zeile in einen Stringstream ein. Nun schreiben wir den Protokoll Typ in einen String ein. Zur Erinnerung: Der >>-operator von streams liest bis zu einem Leerzeichen ein. Als nächstes lesen wir den Statuscode aus, wenn dieser 100 ist müssen wir noch die leere Zeile zwischen der neuen Anfrage ignorieren und beginnen dann wieder von vorne.
    Nun müssen wir nur noch eine Fehlermeldung ausgeben wenn ein Statuscode anders als 200 oder 100 auftrat:

    if(code != 200)
            {
                firstLine.ignore(); // Leerzeichen nach dem Statuscode ignorieren
                string msg;
                getline(firstLine, msg);
                cout << "Error #" << code << " - " << msg << endl;
                return 0;
            }
    

    6.2 Transfer-Arten

    Nun kommen wir zum eigentlich Header. Er besteht aus vielen Argumenten, von denen einige aber nebensächlich sind. Wichtig für uns ist erstmal die allgemeine Übertragunsart:

    Normales Übertragen, Dateigröße nicht mit übergeben

    Dies ist die einfachste Art der Übertragung. Sobald der Header zu Ende ist, lesen wir mit recv solange, bis der Server die Verbindung schließt. Wir erinnern uns: recv gibt beim schließen der Verbindung 0 zurück.

    Normales Übertragen, Dateigröße übergeben

    Nun machen wir alles genauso wie vorher, nur das wir nicht darauf warten müssen, dass recv 0 zurück gibt: Wir hören einfach auf sobald wir die Datei vollständig erhalten haben. Vorteil: Wir können den Fortschritt in % anzeigen.

    Chunked-Encoding

    Chunked-Encoding ist die komplizierteste Art: Die Datei wird nicht als ganzes Übertragen sondern in verschiedenen Abschnitten. Dies ist z.B. notwendig wenn der Server ein PHP-Script ausführt noch während die Seite gesendet wird.

    Die für diese 3 Übertragunstypen wichtige Argumente sind folgende:

    Content-Length: 1234
    Transfer-Encoding: chunked
    

    Content-Length gibt uns die Größe der Datei in Bytes und das Argument Transfer-Encoding existiert nur dann, wenn auch das Chunked-Encoding angewendet wird.
    Wir erstellen uns also eine bool-Variable, die speichert ob das "Tranfer-Encoding: chunked" angegeben wurde, sowie eine Variable, die die übergebene Dateigröße speichert und mit einer Konstanten belegt wird, falls keine Größe angegeben wurde:

    bool chunked = false;
            const int noSizeGiven = -1;
            int size = noSizeGiven;
    

    Nun starten wir mir der zeilenweisen Auslesung des Headers:

    while(true)
            {
                stringstream sstream;
                GetLine(Socket, sstream);
                if(sstream.str() == "\r") // Header zu Ende?
                {
                    break;
                }
    

    Zur Erinnerung: Der Header endet mit einer leeren Zeile, da als Zeilenumbruch allerdings nicht \n verwendet wird sondern \r\n handelt es sich um eine Zeile die nur ein \r enthält:

    HTTP/1.1 200 OK\r\nAsd: 123\r\n[b]\r[/b]\n
    

    Nun lesen wir in einen String ein, wie der Name des Arguments ist. Danach ignorieren wir ein Zeichen (das Leerzeichen) um danach den Wert einzulesen.

    string left; // Das was links steht
                sstream >> left;
                sstream.ignore(); // ignoriert Leerzeichen
    

    Wenn der Name "Content-Size" ist, lesen wir die Größe ein:

    if(left == "Content-Length:")
                {
                    sstream >> size;
                }
    

    Falls wir auf eine Angabe über das Transfer-Encoding treffen, werten wir den Wert aus, ist dieser "chunked", setzen wir die Bool-Variable:

    if(left == "Transfer-Encoding:")
                {
                    string transferEncoding;
                    sstream >> transferEncoding;
                    if(transferEncoding == "chunked")
                    {
                        chunked = true;
                    }
                }
            }
    

    Nun sind wir schon fertig mit der Auswertung des Headers.

    6.3 Die Übertragung

    Nun beginnen wir mit der eigentlichen Übertragung der Datei. Dazu erstellen wir erstmal eine Output-Datei, in die wir binär schreiben werden:

    fstream fout("faq.html", ios::binary | ios::out);
            if(!fout)
            {
                cout << "Could Not Create File!" << endl;
                return 1;
            }
    

    Wir erstellen uns drei Variablen:

    int recvSize = 0; // Empfangene Bytes insgesamt
            char buf[1024];
            int bytesRecv = -1; // Empfangene Bytes des letzten recv
    

    In der ersten Variable speichern wir, wieviel Bytes wir schon emfpangen haben, dies ist z.B. für eine Prozentanzeige notwending. Außerdem erstellen wir ein Array aus 1024 bytes für die Pufferung der Daten.
    Als letztes die temporäre Variable bytesRecv die den letzten Rückgabe wert von recv speichert.

    if(size != noSizeGiven) // Wenn die Größe über Content-length gegeben wurde
            {
                cout << "0%";
    

    Wenn eine Größe übergeben wurde können wir eine Fortschrittsanzeige ausgeben.
    Wir empfangen so lange, bis die gesamte Datei übertragen wurde:

    while(recvSize < size)
                {
    

    Nun empfangen schreiben wir die Daten die wir auf unserem Socket empfangen in den Buffer:

    if((bytesRecv = recv(Socket, buf, sizeof(buf), 0)) <= 0)
                    {
                        throw CreateSocketError();
                    }
    

    Falls recv einen Wert kleiner oder gleich 0 zurück gibt, brechen wir ab und werfen eine Exception.
    Nun addieren wir die aktuell empfange Anzahl an Bytes zu der Gesamtanzahl und schreiben den Buffer in die Datei:

    recvSize += bytesRecv;
                    fout.write(buf, bytesRecv);
    

    Als letztes geben wir noch eine Prozentanzeige aus und sind auch schon fertig:

    cout << "\r" << recvSize * 100 / size << "%" << flush; // Mit \r springen wir an den Anfang der Zeile
                }
            }
    

    Nun müssen wir noch die anderen zwei Transfer-Arten implementieren:

    else
            {
                if(!chunked)
                {
                    cout << "Downloading... (Unknown Filesize)" << endl;
                    while(bytesRecv != 0) // Wenn recv 0 zurück gibt, wurde die Verbindung beendet
                    {
                        if((bytesRecv = recv(Socket, buf, sizeof(buf), 0)) < 0)
                        {
                            throw CreateSocketError();
                        }
                        fout.write(buf, bytesRecv);
                    }
                }
    

    Dies ist die normale Übertragung ohne Angabe von Dateigröße. Der Unterschied besteht darin, dass wir nicht wissen wann die Übertragung zu Ende ist, sondern einfach auf einen Verbindungsabbruch warten müssen.

    6.4 Chunked Transfer-Encoding

    Das letzte was uns nun noch fehlt, ist die Übertragung mit Chunks. Dazu müssen wir uns wieder den Aufbau angucken:

    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    
    <Größe des Chunks in Hex>
    <Daten des Chunks>
    <Größe des zweiten Chunks in Hex>
    <Daten des zweiten Chunks>
    0
    Weiteres Argument: Blabla
    Noch ein Argument: 123
    (hier eine leere Zeile)
    

    Anstelle der Daten folgt bei dieser Übertragungs-Art eine Zeile in hexadezimalen Schreibweise der wir die Größe des Chunks in Bytes entnehmen können. Hiernach kommen die Daten gefolgt von einem Zeilenmumbruch. Nun beginnt entweder ein neuer Chunk, oder es wird die Größe 0 übergeben, das steht für das Ende der Chunks. Hiernach können noch weitere Argumente folgen, die für uns aber unwichtig sind. Hauptsache wir haben die Datei und dann schnell weg 😛

    Machen wir also weiter im Code wo wir vorhin aufgehört haben:

    else
                {
                    cout << "Downloading... (Chunked)" << endl;
                    while(true)
                    {
                        stringstream sstream;
                        GetLine(Socket, sstream);
                        int chunkSize = -1;
                        sstream >> hex >> chunkSize; // Größe des nächsten Parts einlesen
    

    Wir lesen die erste Zeile ein und schreiben die hexadezimale Zahl in size. Hierzu brauchen wir nur das "hex"-Flag setzen damit der Stream auch das Hexadezimalsystem anwendet.
    Wenn die Größe kleiner oder gleich 0 ist, verlassen wir unsere Schleife:

    if(chunkSize <= 0)
                        {
                            break;
                        }
    

    Nun beginnen wir eine Schleife die den aktuellen Chunk runterläd:

    cout << "Downloading Part (" << chunkSize << " Bytes)... " << endl;
                        recvSize = 0; // Vor jeder Schleife wieder auf 0 setzen
                        while(recvSize < chunkSize)
                        {
    

    Nun erstellen wir uns eine Variable, die nur speichert, wieviel Bytes wir diesen Schleifendurchgang noch empfangen müssen:

    int bytesToRecv = chunkSize - recvSize;
    

    Dies ist nämlich notwendig, da wir ja am Ende unseres Chunks, nicht mehr empfangen dürfen als notwendig, sonst könnte wir z.B. schon die größe des nächsten Chunks in unsere Datei schreiben und das wäre fatal. Deswegen übergeben wir an recv als größe entweder sizeof(buf) oder aber wenn wir nur noch ein kleines Stück empfangen müssen bytesToRecv:

    if((bytesRecv = recv(Socket, buf, bytesToRecv > sizeof(buf) ? sizeof(buf) : bytesToRecv, 0)) <= 0)
                            {
                                throw CreateSocketError();
                            }
    

    Nun addieren wir gewohnt die empfangenen Bytes und geben eine Prozentanzeige aus:

    recvSize += bytesRecv;
                            fout.write(buf, bytesRecv);
                            cout << "\r" << recvSize * 100 / chunkSize << "%" << flush;
                        }
                        cout << endl;
    

    Nun haben wir alles empfangen, doch halt: Haben wir nicht noch etwas vergessen? Richtig: Nach den Daten erfolgt ein Zeilenumbruch. Dieser besteht aus einem \r und einem \n, also 2 Bytes. Es ist also am einfachesten wenn wir eben 2 Bytes vom Socket ignorieren:

    for(int i = 0; i < 2; ++i)
                        {
                            char temp;
                            recv(Socket, &temp, 1, 0);
                        }
    

    Nun geben wir noch ein "Finished!" aus und unser Programm ist fertig:

    }
                }
            }
            cout << endl << "Finished!" << endl;
        }
        catch(exception& e)
        {
            cout << endl;
            cerr << e.what() << endl;
        }
    #ifdef linux
        close(Socket); // Verbindung beenden
    #else
        closesocket(Socket); // Windows-Variante
    #endif
    }
    

    Das ganze Programm sollte uns jetzt die faq.html-Datei herrunterladen und auf der Festplatte speichern. Nun sind wir schon fast fertig.

    7 Der letzte Feinschliff

    Als letztes wollen wir unser Programm um ein paar Funktionen ergänzen, die eine Eingabe einer URL ermöglichen. Schließlich will man nicht immer nur eine Datei herrunterladen. Dazu fügen wir am Anfang eine Eingabe hinzu:

    int main()
    {
        using namespace std;
    
        cout << "URL: ";
        string URL;
        cin >> URL; // User gibt URL der Datei ein, die herruntergeladen werden soll
    

    Nun müssen wir ein vor der URL evtl. existierendes http:// entfernen:

    // Entfernt das http:// vor dem URL
    void RemoveHttp(std::string& URL)
    {
        size_t pos = URL.find("http://");
        if(pos != std::string::npos)
        {
            URL.erase(0, 7);
        }
    }
    

    Diese Funktion rufen wir nun nach dem Start von WinSock auf:

    #ifndef linux
        WSADATA w;
        if(WSAStartup(MAKEWORD(2,2), &w) != 0)
        {
            cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << endl;
            return 1;
        }
    #endif
    
        RemoveHttp(URL);
    

    Jetzt extrahieren wir den Hostnamen aus der URL und speichern ihn in einem neuen String ab:

    string hostname = RemoveHostname(URL);
    

    Die RemoveHostname-Funktion sieht so aus:

    // Gibt den Hostnamen zurück und entfernt ihn aus der URL, sodass nur noch der Pfad übrigbleibt
    std::string RemoveHostname(std::string& URL)
    {
        size_t pos = URL.find("/");
        if(pos == std::string::npos)
        {
            std::string temp = URL;
            URL = "/";
            return temp;
        }
        std::string temp = URL.substr(0, pos);
        URL.erase(0, pos);
        return temp;
    }
    

    Nun lösen wir den Hostnamen auf in dem neuen String auf:

    hostent* phe = gethostbyname(hostname.c_str());
    

    Es folgt der Aufbau der Verbindung:

    if(phe == NULL)
        // ...
        cout << "Verbindung erfolgreich!" << endl;
    

    Als nächstes müssen wir eine HTTP-Request erstellen:

    string request = "GET ";
        request += URL;    // z.B. /faq/index.html
        request += " HTTP/1.1\n";
        request += "Host: " + hostname + "\nConnection: close\n\n";
    

    Diesen übergeben wir jetzt an unsere SendAll-Funktion und danach folgt der Code aus 05.cpp bis zur Öffnung des Filestreams:

    try
        {
            SendAll(Socket, request.c_str(), request.size()); // Anfrage an Server senden
            int code = 100; // 100 = Continue
            // ...
            }
    

    Nun muss die Datei erstellt werden und der Inhalt eingelesen werden. Das größte Problem ist hierbei der Dateiname: Bei vielen Fällen lässt er sich extrahieren aber manchmal ist er auch unbekannt (z.B. www.google.de). Die sauberste Methode wäre es, das Response-Argument "Content-Type:" auszuwerten, dies wollen wir uns aber mal sparen, und nennen unsere Datei einfach download und hängen eine Endung an, falls diese im URL enhalten ist:

    // Gibt die Dateiendung im URL zurück
    std::string GetFileEnding(std::string& URL)
    {
        using namespace std;
        size_t pos = URL.rfind(".");
        if(pos == string::npos)
        {
            return "";
        }
        URL.erase(0, pos);
        string ending = ".";
        // Algorithmus um Sachen wie ?index=home nicht zuzulassen
        for(string::iterator it = URL.begin() + 1; it != URL.end(); ++it)
        {
            if(isalpha(*it))
            {
                ending += *it;
            }
            else
            {
                break;
            }
        }
        return ending;
    }
    
    string filename = "download" + GetFileEnding(URL);
            cout << "Filename: " << filename << endl;
            fstream fout(filename.c_str(), ios::binary | ios::out);
            if(!fout)
            {
                cout << "Could Not Create File!" << endl;
                return 1;
            }
            int recvSize = 0; // Empfangene Bytes insgesamt
    
            // ...
    
        }
        catch(exception& e)
        {
            cout << endl;
            cerr << e.what() << endl;
        }
    #ifdef linux
        close(Socket); // Verbindung beenden
    #else
        closesocket(Socket); // Windows-Variante
    #endif
    }
    

    Fertig! Das komplette Programm könnt ihr euch hier herrunterladen.

    8 Nachwort

    Falls ihr eine URL finden solltet, die dieses Programm nicht herrunterladen kann, könnt ihr auf mein Tutorial antworten und mir den Link mitteilen, damit ich es mir angucken kann.
    Bei allgemeinen Fragen zum Thema Sockets könnt ihr in der WinAPI- oder der Linux/Unix-Abteilung nachfragen.

    In meinem Code rufe ich recv auf um nur ein einziges Byte zu empfangen. Dies sollte man in der Praxis möglichst unterlassen und immer eine angemesse Buffergröße wählen, z.B. 4 KB. Hierdurch kann recv dir einzelne TCP-Packete im Ganzen zurück geben. Um trotzdem zeilenweise einzulesen muss man einfach die recv-Funktion in seiner eigenen Funktion kapseln. Dies sollte man sowieso tun, da recv und send sehr low-level sind. Ein auf diese Weise verändertes Programm sähe so aus.

    Das war's von mir, vielen Dank fürs Lesen!

    9 Linkliste

    Socket-Referenz
    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winsock/winsock/wsastartup_2.asp

    Socket-Tutorial
    http://beej.us/guide/bgnet/

    HTTP-Tutorial
    http://www.jmarshall.com/easy/http/

    RFC für HTTP/1.1
    http://tools.ietf.org/html/rfc2616



  • Schonmal was von Kommentaren gehört? 😉



  • Reyx schrieb:

    Schonmal was von Kommentaren gehört? 😉

    Okay, ich hab welche hinzugefügt.

    Außerdem hab ich noch andere Kleinigkeiten geändert. Was mich gerade besonders stört, ist das bearbeiten der 100 - Continue Nachricht, ich find das ziemlich häßlich so wie es jetzt ist. Wenn jemand ne Idee hat, bitte sagen.

    mfg.



  • Die Linux Version ist fertig. Ich hab nicht so viel Ahnung von Linux, hoffe aber mal dass es so in Ordnung ist. Die binäre Datei für Linux hab ich oben im Post ergänzt.

    mfg.



  • joomoo schrieb:

    Die Sache mit den Exceptions, da bin ich mir auch nicht ganz sicher, ist das so in Orndung?

    Wenn du schon <exception> einbindest, solltest du deine Fehlerklasse auch von std::exception ableiten (exception::what() dürfte dann dein rString() zurückgeben oder Text und Fehlernummer zusammenkleben).

    Und als Äquivalent zu strerror() fällt mir nur FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,...) ein - ich weiß allerdings nicht, ob die auch mit WinSock-Fehlern zurechtkommt.



  • CStoll schrieb:

    ich weiß allerdings nicht, ob die auch mit WinSock-Fehlern zurechtkommt.

    Kommt sie 😉



  • Danke für den Tipp mit FormatMessage! Funktioniert einwandfrei, aber ich bin mir nicht sicher ob ich das so richtig gemacht habe (Einen Zeiger auf einen Zeiger in einen Zeiger casten?!??).

    Die exception Headerdatei hab ich jetzt einfach weggelassen.

    mfg.



  • Ich habe jetzt hinzugefügt, dass der Header ausgegeben wird, wenn ein Fehler auftrat. Außerdem habe ich jetzt eine Klasse erstellt, die von std::exception erbt (siehe Code).

    Bitte guckt euch das nochmal an, denn ich habe da ein Problem mit dem Destrutkur, denn wenn ich dessen Definition weglasse, klappt's nicht. Das find ich komisch, denn in Bespielen, die ich gelesen habe, wurde das nie so gemacht.

    Die Binär-Dateien habe ich noch nicht aktualisiert, hat sich ja auch nicht so viel geändert.

    mfg.



  • Joomoo, lade die Dateien doch bitte in das FTP-Verzeichnis des Magazins (in einen nach deinem Artikel benannten Ordner) hoch. Dann ist alles an einem Platz.

    Was das mit dem Konstruktor ist, weiß ich leider nicht.
    Falls du hier zu lange keine Antwort bekommst, frag doch mal im passenden Forum (C++?). 🙂



  • estartu schrieb:

    Joomoo, lade die Dateien doch bitte in das FTP-Verzeichnis des Magazins (in einen nach deinem Artikel benannten Ordner) hoch. Dann ist alles an einem Platz.

    Okay hab ich gemacht.

    Was das mit dem Konstruktor ist, weiß ich leider nicht.
    Falls du hier zu lange keine Antwort bekommst, frag doch mal im passenden Forum (C++?). 🙂

    Jo, könnt ich machen, aber ich glaube so ist's jetzt erstmal in Ordnung mit dem Code und ich arbeite weiter an dem Artikel.

    mfg.



  • Schau dir das mal an:
    Ich hab jetzt erstmal ein Programm geschrieben welches einfach Dateien vom Server runterläd. Hier ist die binäre Windows-Datei und hier die für Linux.

    Oder so ähnlich.
    Das sieht doch viel schöner aus als mit den Monsterlinks. 🙂

    Einfach: [url=http://www.blabla....]Text den man sehen soll[/url]



  • estartu schrieb:

    Schau dir das mal an:
    Ich hab jetzt erstmal ein Programm geschrieben welches einfach Dateien vom Server runterläd. Hier ist die binäre Windows-Datei und hier die für Linux.

    Oder so ähnlich.
    Das sieht doch viel schöner aus als mit den Monsterlinks. 🙂

    Einfach: [url=http://www.blabla....]Text den man sehen soll[/url]

    Danke, hab ich geändert.

    mfg.



  • joomoo schrieb:

    ~socketError() throw() {} // <- Ohne diese Zeile krieg ich:
    // overriding `virtual std::exception::~exception() throw ()'
    // Warum?

    Wenn Du von einer Klasse ableitest muss der Destruktor immer virtuell sein, da sonst das Verhalten deines Programmes beim Zerstören des Exception-Objektes undefiniert ist. Die throw-Spezifikation musst Du angeben, weil die Basis-Klasse den Destruktor mit der throw-Spezifaktion vorgibt. Du garantierst damit übrigens, dass der Destruktor niemals eine Exception wirft (Exception-Garantie).

    Der Typ einer Funktion

    void f(void)

    unterscheidet sich von

    void f(void) throw()

    Du kannst das überprüfen indem Du eine Funktions-Pointer-Variable auf die Funktionen zeigen lässt oder den typeid-Operator auf die Funktionen anwendest.

    Der Konstruktor

    socketError(int errorN, std::string errorS) : errorNumber_(errorN), errorString_(errorS) {}

    sollte besser errorS als const reference auf einen string übergeben bekommen.

    socketError(int errorN, const std::string & errorS) : errorNumber_(errorN), errorString_(errorS) {}
    

    und alternativ (vielleich auch zusätzlich) als char pointer initialisiert werden können

    socketError(int errorN, const char * errorS) : errorNumber_(errorN), errorString_(errorS) {}
    

    da sonst unnötige temporäre Kopien angelegt werden müssen.
    Objekte (auch string-Objekte) sollte man wann immer es möglich ist, als Referenz übergeben oder zurückgeben.



  • rik schrieb:

    joomoo schrieb:

    ~socketError() throw() {} // <- Ohne diese Zeile krieg ich:
    // overriding `virtual std::exception::~exception() throw ()'
    // Warum?

    Wenn Du von einer Klasse ableitest muss der Destruktor immer virtuell sein, da sonst das Verhalten deines Programmes beim Zerstören des Exception-Objektes undefiniert ist. Die throw-Spezifikation musst Du angeben, weil die Basis-Klasse den Destruktor mit der throw-Spezifaktion vorgibt. Du garantierst damit übrigens, dass der Destruktor niemals eine Exception wirft (Exception-Garantie).

    Der Typ einer Funktion

    void f(void)

    unterscheidet sich von

    void f(void) throw()

    Du kannst das überprüfen indem Du eine Funktions-Pointer-Variable auf die Funktionen zeigen lässt oder den typeid-Operator auf die Funktionen anwendest.

    danke für die Erklärung!
    Hat mich nur etwas gewundert da ich in Literatur noch nicht gesehen habe, dass jemand das macht wenn er von std::exception ableitet. Könnte es sein das best. Compiler das übersehen?

    Der Konstruktor

    socketError(int errorN, std::string errorS) : errorNumber_(errorN), errorString_(errorS) {}

    sollte besser errorS als const reference auf einen string übergeben bekommen.

    socketError(int errorN, const std::string & errorS) : errorNumber_(errorN), errorString_(errorS) {}
    

    und alternativ (vielleich auch zusätzlich) als char pointer initialisiert werden können

    socketError(int errorN, const char * errorS) : errorNumber_(errorN), errorString_(errorS) {}
    

    da sonst unnötige temporäre Kopien angelegt werden müssen.
    Objekte (auch string-Objekte) sollte man wann immer es möglich ist, als Referenz übergeben oder zurückgeben.

    danke! hab den tipp sogar noch gelesen bei effektiv C++ programmieren aber irgendwie nicht dran gedacht.

    mfg.



  • Bitte keine Binäries! Wir sind hier je eh ein einem Programmierer-Forum, da sollte man in der Lage sein, selbst zu kompilieren.



  • rüdiger schrieb:

    Bitte keine Binäries! Wir sind hier je eh ein einem Programmierer-Forum, da sollte man in der Lage sein, selbst zu kompilieren.

    Ok das am Anfang hab ich rausgenommen, aber im Artikel kann es doch drinn bleiben, oder?

    mfg.



  • @joomoo
    Schaffst du es, dass dein Artikel zur nächsten Runde fertig wird? Wird bräuchten nämlich noch einen, damit wir auf 2 Stück kommen.



  • GPC schrieb:

    @joomoo
    Schaffst du es, dass dein Artikel zur nächsten Runde fertig wird? Wird bräuchten nämlich noch einen, damit wir auf 2 Stück kommen.

    Momentan hatte ich gerade vor ein paar Wochen mein Praktikum und hab deswegen nicht so viel Zeit, aber ich denke ab Dienstag kann ich wieder weiter arbeiten. Mein Artikel wird dann wahrscheinlich Mitte Dezember fertig werden, dann setze ich ihn auf [T].
    Ihr könnt aber trotzdem jetzt schonmal rübergucken, ich uploade ihn in regelmäßigen Abständen.

    mfg.



  • So, hab den Artikel auf [T] gesetzt, da er bis auf das letzte Kapitel (welches nur noch ein paar Nebensächliche Sachen behandelt die nichts mit dem Thema zu tun haben) und die Linkliste fertig ist.

    An einigen stellen sicher noch einige Grammatikfehler da ich von Du auf Sie/Wir und jetzt auf Ihr/Wir umgeschaltet habe.

    mfg.



  • Mach doch aus

    #ifndef linux 
    // Startet WinSock 2 
    void StartWinsock() 
    { 
        WSADATA w; 
        if(WSAStartup(MAKEWORD(2,2), &w) != 0) 
        { 
            std::cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << std::endl; 
            exit(1); 
        } 
    }
    #endif
    

    ein

    void StartWinsock() 
    { 
    #ifndef linux 
      // Startet WinSock 2 
        WSADATA w; 
        if(WSAStartup(MAKEWORD(2,2), &w) != 0) 
        { 
            std::cout << "Winsock 2 konnte nicht gestartet werden! Error #" << WSAGetLastError() << std::endl; 
            exit(1); 
        } 
    #endif 
    }
    

    Dann kannst Du Dir ein späteres "#ifndef linux" hier sparen:

    int main() { 
        using namespace std; 
    
        StartWinsock();
    

    Auch würde sich dann Dein ersten Beispiel unter Linux übersetzen lassen 😉


Anmelden zum Antworten