Grundlegende Netzwerkprogrammierung mit WinSock

Autor: Georg 'Black' Wicherski

In diesem Tutorial lernt ihr, wie ihr die WinSock API, die Windows-Schnittstelle zur Netzwerkporgammierung, initialisiert und benutzt. Wir werden einen Client programmieren, der sich zu einem Server verbindet und eine Zahl sendet, eine Zahl empfängt und diese ausgibt.

Als erstes binden wir die windows.h und die winsock.h ein. Die windows.h sollte schon hinlänglich bekannt sein, die winsock2.h enthält die Definitionen zu den WinSock Funktionen und Datentypen, wird manchmal allerdings auch schon durch die windows.h eingebunden. Die stdio.h wird lediglich für die Ausgabe benötigt, die time.h nur für die Initialisierung des Zufallszahlengenerators.
Dann teilen wir dem Linker mittels der #pragma-direktive mit, wo er die nötigen Implementationen der WinSock Funktionen findet (schließlich liegen diese in einer nicht-standard DLL). Wer aus Gründen des Styles oder sonstige tollen Begründungen gegen die #pragma-direktive ist, kann die ws2_32.lib auch über die Projektoptionen hinzufügen.

#include <windows.h>
#include <winsock.h>
#include <stdio.h>
#include <time.h>
 
#pragma comment(lib, "ws2_32.lib")

Die Initialisierung von WinSock gestaltet sich recht einfach, sie besteht lediglich aus der Funktion WSAStartup(…). Der erste Parameter ist die angeforderte Version der WinSock API (aktuell v2.2), der zweite Parameter ist ein Pointer auf eine struct, die nach der Initialisierung einige Informationen über die verwendete API enthält. Wir werden diese Informationen hier nicht weiter verwenden, da sie in 90% der Fälle vollkommen irrelevant sind.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, char * szCommandLine, int nShowMode)
{
  srand(time(0));   // initialize random number generator
 
  {
    WORD wVersionRequested = MAKEWORD(2, 2);
    WSADATA wdData;
 
    WSAStartup(wVersionRequested, &wdData);
  }

Das war auch schon der Initialisierungsteil, die WinSock API ist nun bereit benutzt zu werden. Im folgenden werden wir einige Variablen erstellen, die wir später noch brauchen werden.

  {
    SOCKET sckSocket;
    SOCKADDR_IN addrServer;
    float flNumber;

Die Variable sckSocket dient dem Speichern eines Handles zu unserem Socket. Die WinAPI Netzwerkprogrammierung wird über sogenannte Sockets realisiert, ein Socket stellt also quasi ein Interface zum Netzwerk-Layer da.
Im folgenden werde ich mich auf TCP Protokoll beziehen, ein Sub-Protokoll von IP. Für die genaue Struktur eines Netzwerkpaketes verweise ich hier auf das Raw-Socket Tutorial. Das TCP Protokoll liegt einer Client-Server Struktur zu grunde, d.h. es gibt einen Server der auf Verbindungen wartet und einen oder mehrer Clients die eine Verbindungsanfrage an den Server senden. Ist dieses dann geschehen können beide asynchron Daten versenden. Wenn das TCP Protokoll benutzt wird, werden automatisch von den Netzwerk-Zwischenlayern (hier WinSock) Checksums generiert, d.h. es gehen normalerweise keine Daten verloren; dieses Protokoll ist verlustfrei.
Da wir hier im Einführungstutorial einen Client programmieren werden, ist addrServer eine Struct in der wir die Adresse des Server, zu dem wir uns verbinden wollen, speichern.
Die Variable flNumber ist unser Puffer, in dem wir die Daten vor dem Senden schreiben und nach dem Empfangen schreiben lassen.

Im folgendem Abschnitt werden wir einen Socket mit der Funktion socket(…) erstellen. Der erste Parameter gibt die 'Address Family' an, wir benutzen AF_INET, welche das IP Protokoll symbolisiert. Als zweiten Parameter geben wir SOCK_STREAM an, dass das TCP Protokoll charakterisiert. Der dritte Parameter gibt bei Raw-Sockets das Protokoll an, wir geben hier einfach 0 an.
Nach dem Erzeugen vergleichen wir sckSocket mit INVALID_SOCKET, stellen also fest, ob das Erstellen gescheitert ist. Sollte dies der Fall sein, brechen wir mit Fehlercode 256 ab.

    if((sckSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
      return 256;

Bis jetzt lässt sich der Code auch für Server einsetzen, doch jetzt werden wir Client spezifisch. Wir setzten im folgendem Code-Abschnitt die addrServer Struct.
Die Funktion inet_addr(…) verwandelt einen IP String (also z.B. „192.168.0.100“) in eine 4-byte Nummer. Dabei gehen deshalb keine Daten verloren, weil eine IP eigentlich nur die String-Repräsentation dieser Nummer ist, vier Blöcke von jeweils 0 bis 255 entsprechen genau vier Bytes, der Punkt ist ein überflüßiges Trennzeichen.
Mittels htons wandeln wir einen short (TCP Ports sind immer shorts) von host byte order (höchstes byte links) in network byte order um (höchstes byte rechts). Dies ist notwendig, damit der Port korrekt verarbeitet wird, die Umkehrfunktion ist analog ntohs(…).
Die IP „127.0.0.1“ bezeichnet das Loopback-Interface, d.h. eine Verbindung mit dem Computer, auf dem das Programm läuft, soll aufgebaut werden. Wir benutzen den Port 61473, dieser ist unwillkürlich gewählt.

    addrServer.sin_family = AF_INET;
    addrServer.sin_addr.s_addr = inet_addr("127.0.0.1");
    addrServer.sin_port = htons(61473);

Nun sind wir bereits in der Lage eine Verbindung zu unserem Server herzustellen, dies geschieht mittels der Funktion connect(…). Diese erhält als ersten Parameter unseren Socket, als zweiten Parameter einen SOCKADDR * der den Server repräsentiert und als letzten Parameter die Größe der Serveradress-Struct. Dies ist erforderlich, um die Funktion so global wie möglich zu halten, so dass auch andere Protokolle als IP wie z.B. IPX genutzt werden können, die andere Adress-Structs benutzen, ohne eine andere Funktion benutzen zu müssen.
Die Funktion gibt bei Fehlschlag SOCKET_ERROR, wie die meisten andere WinSock Funktionen, zurück. Sollte dies geschehen, brechen wir mit der Meldung „Could not connect!“ ab.

    if(connect(sckServer, (SOCKADDR *) &addrServer, sizeof(addrServer)) == SOCKET_ERROR)
            return (MessageBox(0, "Could not connect!", "WinSock Tutorial Series", MB_ICONEXCLAMATION) == IDOK) ? 254 : 254; // evil construction

Da wir nun verbunden sind, können wir auch schon unsere Variable flNumber mit einem Wert initialisieren und versenden. Die erste Zeile im folgendem Listing generiert eine Fließkommazahl zwischen 0.0f und 100.0f. Dies geschieht mit der Funktion send(…) die als ersten Parameter mal wieder unseren Socket bekommt, als zweiten Parameter einen Pointer zu dem Buffer der die zu versendenden Daten enthält. Als dritter Parameter erhält die Funktion die Länge des Buffers, so darf dieser auch binäre Nullen enthalten. Der letzte Parameter bezeichnet besondere Flags, in der Regel übergeben wir hier 0. Die Funktion gibt die Anzahl der übertragenen bytes, 0 bei bereits geschloßener Verbindung oder eine negative Zahl bei einem Fehler zurück.

    flNumber = (float) rand() / (float) RAND_MAX * 100.0f;
 
    if(send(sckSocket, (char *) &flNumber, sizeof(flNumber), 0) <= 0)
      return 253;

Das Empfangen von Daten ist recht ähnlich zum Senden und geschieht mit der Funktion recv(…), als ersten Parameter den Socket, als zweiten einen Pointer zum Empfangsbuffer, als dritten Parameter wieviel Bytes (maximal) empfangen werden sollen und als letzten Parameter wieder unsere Flags.

    if(recv(sckSocket, &flNumber, sizeof(flNumber), 0) <= 0)
      return 252;

Zu guter Letzt wird mit der Funktion closesocket(…) die Verbindung geschlossen. Darauf wird noch die Nummer ausgegeben und das Programm beendet.

    closesocket(sckSocket);
 
    {
      char szBuffer[256];
 
      sprintf(szBuffer, "Received Number: %f", flNumber);
      MessageBox(0, szBuffer, "WinSock Tutorial Series", MB_ICONINFORMATION);
    }
  }
 
  return 0;
}

Wenn man das Programm nun kompiliert und ausführt, wird natürlich die Verbindungsfehler-Meldung ausgegeben, dies ist jedoch nicht weiter verwunderlich, da noch kein Server auf diesem Port bereitsteht.
Zu guter Letzt sei noch gesagt, dass alle erdenkliche Datentypen auf diese Art und Weise übertragen werden können, ein String ließe sich beispielsweise so übertragen:

{
  char szBuffer[256];
 
  strcpy(szBuffer, "Hello Network!");
  send(sckSocket, szBuffer, strlen(szBuffer), 0);
}

Anmerkung

Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.

Die Verwendung aller Dokumente einschließlich der Abbildungen ausschließlich zu nichtkommerziellen Zwecken. Verbreitung des Dokuments auf Speichermedien, (insbesondere auf CD-ROMs als Beilage zu Zeitschriften und Magazinen oder sog. "Mission-Packs" etc.) ist untersagt.
 
coding/cpp_windows/networkprogramming_basics.txt · Zuletzt geändert: 2008/10/29 22:06 von TomMe