Winsock Tutorial von c-worker.ch (Teil 1: Grundlagen und TCP Sockets)

Dieses Tutorial stammt von www.c-worker.ch.
Es darf auf einer eigenen Seite veröffentlicht werden sofern es unverändert bleibt, und die Seite www.c-worker.ch als Quelle erwähnt wird.
Für die Richtigkeit aller in diesem Dokument beschriebenen Angaben und für eventuelle Schäden die durch dieses Dokument entstehen könnten wird nicht gehaftet.

Hinweise

Falls beim kompilieren einige "Neudefinition" Fehler kommen entfernt die "#include <winsock2.h>" Zeile (wurde in diesem Fall schon in windows.h includiert)
Manche Leute berichten auch, dass Sie den Fehler beheben können indem sie winsock2.h vor windows.h includieren.

Falls der Compiler INADDR_ANY nicht finden kann verwendet ADDR_ANY (oder umgekert, eins von beiden geht schon)
Das Progamm muss gegen ws2_32.lib  gelinkt werden. Falls man Visual Studio verwendet muss man bei den Projekteigenschaften unter Linker ws2_32.lib zu den Libraries hinzufügen.
Eventuell schafft aber auch folgende Zeile am Anfang des Quellcodes Abhilfe: #pragma comment( lib, "ws2_32.lib" )

 

1. Einleitung
2. Was sind Sockets ?
3. Winsock starten
4. Typen von Sockets
5. Client Socket erstellen
6. Verbindung zu einem Server herstellen
7. Server Socket erstellen
8. Daten austauschen
9. Aufräumen

 

1. Einleitung

Dieses Dokument soll ein Tutorial darstellen wie man eine Winsock Anwendung programmiert. Dabei werden nicht irgendwelche MFC Klassen benutzt, sondern die Windows Socket API. Ebenfalls wird hier nicht auf alle Dinge der Socketprogrammierung eingegangen. Vorallem werden keine WSAxx Funktionen verwendet (nur die nötigen), das hat jedoch auch den Vorteil das man gleich die Standard Socketfunktionen lernt, und das Wissen fast 1:1 unter Linux, etc. einsetzten kann. Das Tutorial ist in einige Schritte unterteilt und während des Tutorials werden 2 kleine Konsolenanwendungen geschrieben. Ein Client Programm und ein Server Programm. Ebenfalls wird hier Winsock 2 verwendet, falls du Windows 95 installiert hasst kann es sein, dass du nicht über die Version 2 verfügst. In diesem Fall findest du Informationen und ein entsprechendes Update hier.
Dieses Tutorial ist für die Programmiersprache C (geht natürlich auch mit C++) bestimmt, um das Tutorial einigermassen zu verstehen solltest du zumindest eine Ahnung von C haben. Kenntnisse der Windows API sind keine Voraussetzung denn wir werden hier sowieso nur Konsolenanwendungen entwickeln. Weiterhin ist natürlich ein Compiler erforderlich um die Beispiele zu kompilieren. Wie ihr an so einen kommt sieht ihr unter http://www.c-worker.ch/compiler/index.php. Ich empfehle den von Borland, welcher gratis ist. (falls du mit der Installation mühe hasst lies das Readme und falls es immer noch nicht geht: http://www.c-worker.ch/tuts/bcchowto.txt).

Noch ein wichtiger Hinweis: einige Socket Funktionen sind sogenannte "Blocking Calls". Das heisst eine Funktion wird nicht zurückkehren bis ihre Aufgabe erfüllt ist. So bleibt das Programm zB bei einem recv() oder accept() stehen bist Daten empfangen wurden (recv()) bzw. eine Verbindung akzeptiert wurde (accept()). Das sorgt vorallem am Anfang für Verwirrung. Häufig arbeitet man mit Threads um mit diesen Blocking Calls gut arbeiten zu können.

 

2. Was sind Sockets ?

Eins mal vorweg: Der Inhalt dieses Abschnittes wurde erst kürzlich eingefügt. Wenn du gerade erst mit der Winsock Programmierung anfängst, ist es eventuell besser du überspringst ihn gleich mal, weil er vielleicht mehr Verwirrung als Klarheit schaft. Den Inhalt dieses Abschnittes zu begreiffen ist relativ unwichtig für den Anfang.
Theoretisch gesehen ist ein Socket der Endpunkt einer Verbindung. Zwei Sockets definieren somit eine Verbindung. Ein Socket kann durch eine IP UND eine Portnummer eindeutig identifiziert werden.
In der Praxis ist ein Socket ein File Desriptor (eine Nummer, also ganz einfach ein Integer). Wie man normale Files lesen und schreiben kann, ermöglicht es einem das Betriebssystem auch Sockets wie eine Datei zu lesen (Daten empfangen) und zu schreiben (Daten senden). Das sieht man u.A. auch daran das man unter Unix mit read() und write() Daten über Sockets senden und empfangen kann. Unter Windows kann man nur mit Windows NT/2000/XP per ReadFile() und WriteFile() Daten über Sockets senden und empfangen. Wie auch immer, es ist sowieso empfehlenswert send() und recv() zum senden und empfangen der Daten zu verwenden (das geht dann auch unter allen Windows Versionen), aber mehr dazu später. Also wenn du jetzt verwirrt bist, vergiss das mit den Dateien gleich wieder.

 

3. Winsock starten

Damit die Winsock Funktionen uns überhaupt bekannt sind, müssen wir erst mal die benötigten Header einbinden:

#include <windows.h>
#include <winsock2.h>

und damit wir noch etwas in die Konsole ausgeben können fügen wir noch ein

#include <stdio.h> 

ein.

Bevor eine Anwendung überhaupt Windows Socket Funktionen verwenden kann muss man als allererstes die Funktion WSAStartup aufrufen. So lange die Funktion WSAStartup nicht erfolgreich aufgerufen wurde, kann die entsprechende Anwendung keine Socket Funktionen verwenden und jede Funktion wird den Fehler WSANOTINITIALISED zurückgeben. Aber nun zu der Funktion WSAStartup, diese ist folgendermassen definiert:

int WSAStartup (
   WORD wVersionRequested, 
   LPWSADATA lpWSAData 
   );
   


Zu den beiden Parametern:

-wVersionRequested: Mit diesem Parameter legt man die Winsock Version fest die man verwenden möchte. Dabei muss im High Order Byte die Minor Version und im Low Order Byte die Major Versionsnummer angegeben werden. Das klingt eventuell für einige recht verwirrend.
Erstmal zu WORD, WORD ist ein 2 Byte Datentyp und nichts anderes als ein unsigned short. Definiert ist WORD in windef.h, und zwar folgendermassen:

typedef unsigned short WORD; 
Eben, wie schon gesagt hat ein WORD 2 Bytes, dabei ist das High Order das linke und das Low Order das Rechte Byte:
[ 7 6 5 4 3 2 1 0 ] [ 7 6 5 4 3 2 1 0 ]

[                 WORD                ]

[ High Order Byte ] [ Low Order Byte  ]

Allerdings sind "rechts" und "links" keine besonders gute Beschreibungen. Einfach gesagt ist das High Order Byte das Byte mit dem grössten Wert (Ziffern die mehr Rechts sind haben ja einen höherern Wert) und das Low Order Byte das Byte mit dem niedrigsten Wert.
Also wenn wir zB Version 1.2 wollen, müssen wir ins Hight Order Byte 2 und ins Low Order Byte 1 schreiben.
Da könnte man z.B. so machen:

WORD version; 
version=(2<<8)+1; 

Das wäre eine Möglichkeit, wir verwenden jedoch ein Makro der Win32 Api welches genau dasselbe macht: MAKEWORD.
MAKEWORD übernimmt 2 Parameter: Als ersten Parameter das Low Order Byte als zweiten das Hight order Byte:

WORD MAKEWORD(
   BYTE bLow, // low-order byte 
   BYTE bHigh // high-order byte 
   );

Hier noch schnell einige Beispiele:
für Version 1.2: MAKEWORD(1,2);
für Version 2.0: MAKEWORD(2,0);
etc..

Aber nun zurück zur Funktion WSAStartup. Als zweiten Parameter (lpWSAData) muss man einen Pointer auf eine Struktur vom Typ WSADATA übergeben, diese Struktur wird dann mit Informationen zu der Winsock Version gefüllt, diese sind für uns aber nicht wichtig, und wird übergeben einfach einen Pointer und belassen es dabei.

Nun schreiben wir uns eine kleine Funktion die Winsock startet:

int startWinsock()
{
  WSADATA wsa;
  return WSAStartup(MAKEWORD(2,0),&wsa);
}

WSAStartup gibt 0 zurück wenn kein Fehler auftauchte. Unsere Funktion gibt einfach den Rückgabewert von WSAStartup zurück, diesen werden wird dann in main prüfen, wir erweitern also unsere Datei folgendermassen:

#include <windows.h>
#include <winsock2.h>
#include <stdio.h>

//Prototypen
int startWinsock(void);
    
int main()
{
  long rc;
  rc=startWinsock();
  if(rc!=0)
  {
    printf("Fehler: startWinsock, fehler code: %d\n",rc);
    return 1;
  }
  else
  {
    printf("Winsock gestartet!\n");
  }
  return 0;
}
    
int startWinsock(void)
{
  WSADATA wsa;
  return WSAStartup(MAKEWORD(2,0),&wsa);
}
Ich habe die Datei mal sock.c genannt und habe sie folgendermassen kompiliert:
C:\borland\Bin>bcc32 C:\sock.c 

Es sollte eigentlich 0 Fehler und 0 Warnungen geben.
Hinweis: Falls man Visual Studio verwendet muss man bei den Projekteigenschaften unter Linker ws2_32.lib zu den Libraries hinzufügen.

Wenn man das Programm dann von der Konsole aus startet hat man hoffentlich folgende Ausgabe:

Winsock gestartet!

Falls nicht ist eventuell die angeforderte Winsock Version nicht korrekt installiert oder bei lpWSAData wurde ein ungültiger Pointer übergeben..

 

4. Typen von Sockets

Ok, Winsock ist nun für unsere Anwendung gestartet und wir können alle Socket Funktionen verwenden. Bevor wird aber einen Socket erstellen ist es eventuell noch hilfreich zu wissen was es alles für Socket Typen gibt. Dabei wird hier nur auf die zwei wichtigsten kurz eingegangen: TCP Sockets und UDP Sockets:
TCP Sockets werden wohl am häufigsten verwendet. TCP Sockets arbeiten mit Verbindungen: Es muss eine Verbindung aufgebaut werden, dann können Daten hin und her übertragen werden, und wenn man damit fertig ist trennt man die Verbindung wieder. Hier arbeitet man auch mit dem sogenannten Client-Server Modell.
UDP Sockets dagegen sind nicht verbindungsorientiert. Sie senden einfach Daten an einen anderen Rechner und können Daten von anderen Rechnern empfangen, ohne eine Verbindung aufbauen zu müssen. In diesem Beispiel wird ein TCP Socket verwendet.

 

5. Client Socket erstellen

Da TCP Sockets wie gesagt verbindungsorientiert sind, müssen wir nun entscheiden ob unser Socket ein Client Socket (Also ein Socket der Verbindung zu einem Server Socket aufbaut) oder ein Server Socket (ein Socket der auf Verbindungen von Client Sockets wartet und diese ggf. annimmt) sein soll. Als erstes erstellen wir mal einen Client Socket, da das wesentlich einfacher ist. Nun bleibt die Frage zu welchem Server wir eine Verbindung aufbauen wollen ? Vorläufig noch zu gar keinem, wir werden nachher einen entwickeln.
Als erstes definieren wir mal einen Socket:

SOCKET s;

Mit diesem Socket können wir natürlich noch gar nichts anfangen, wir haben erst eine Variable s vom Typ SOCKET kreiert, nun müssen wir dein eigentlichen Socket noch erstellen. Dies geschieht mit der Funktion socket(), die folgendermassen definiert ist:

SOCKET socket (
int af,
int type,
int protocol
);

-af: Hier muss die Adressfamilie übergeben werden, da wird TCP/IP benutzten verwenden wir hier die Konstante AF_INET
-type: hier muss der Socket Typ angegeben werden (siehe Kapitel 4). Da wir einen TCP Socket wollen geben wir hier SOCK_STREAM an.
-protocol: hier muss man noch das Protkoll angeben, momentan sollte man hier einfach 0 verwenden

Falls der Socket nicht erstellt werden konnte, gibt die Funktion INVALID_SOCKET zurück, und mit WSAGetLastError() kann der Fehlercode abgefragt werden. Die meisten Socket Funktionen (ausser WSAStartup) geben nicht einen Fehlercode zurück, sondern SOCKET_ERROR oder einen anderen Wert. Der eigentliche Fehlercode muss dann immer mit der Funktion WSAGetLastError() abgefragt werden.

Gut, nun ergänzen wir main() um folgenden Code (fett) um einen Socket zu erstellen:

long rc;
SOCKET s;

rc=startWinsock();
.....
s=socket(AF_INET,SOCK_STREAM,0);
    
if(s==INVALID_SOCKET)
{
  printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
  return 1;
}
else
{
  printf("Socket erstellt!\n");
}
return 0;
Nun kann man das ganze wieder kompilieren und ausführen, wenn alles glatt ging sollte man folgende Ausgabe sehen:
Winsock gestartet!
Socket erstellt! 

 

6. Verbindung zu einem Server herstellen

Mit unserem erstellten Socket wollen wir nun auch eine Verbindung zu einem anderen Server Socket herstellen.
Dazu verwendet man die Funktion connect:

int connect (
   SOCKET s, 
   SOCKADDR* name, 
   int namelen 
   );

-s: hier muss man den Socket angeben den man verbinden möchte, logischerweise nehmen wir den vorhin erstellten Socket
-name: hier muss ein Pointer auf eine gefüllte SOCKADDR Struktor gegeben werden, diese wird unten erklärt
-namelen: hier muss die Grösse der SOCKADDR Struktur angegeben werden, also sizeof(SOCKADDR);

Der Rückgabewert von connect ist SOCKET_ERROR falls ein Fehler auftrat.

Nun zu der SOCKADDR Struktur: Da wir TCP/IP benutzten, verwenden wir nicht die SOCKADDR Struktur sondern die SOCKADDR_IN Struktur, welche mit SOCKADDR kompatibel ist. _IN steht dabei wohl für Internet. SOCKADDR_IN ist folgendermassen definiert (SOCKADDR und SOCKADDR_IN sind einfach typedef's für struct sockaddr, bzw. struct sockaddr_in):

struct sockaddr_in{
   short sin_family;
   unsigned short sin_port;
   struct in_addr sin_addr;
   char sin_zero[8];
   };


- sin_family: Hier müssen wir erneut die Adressfamilie angeben, da wir diese Struktur mit einem Socket mit der Adressfamilie AF_INET verwenden, müssen wir hier natürlich wieder AF_INET angeben.
- sin_port: Hier müssen wir den Port angeben zu dem wir ein Verbindung herstellen wollen (HTTP ist 80, FTP 21, etc..), allerdings muss dieser in Network Byte Order angegeben werden. Im Netzwerk sind alle Bytes von Zahlen verkehrt (Im Vergleich zu Windows) angeordnet. Das umordnen übernimmt die Funktion

 u_short htons (u_short hostshort); 

glücklicherweise für uns. Man übergibt einfach eine Nummer und htons gibt die selbe Nummer in Network Byte Order zurück. htons steht für Host To Network Short. Es gibt auch noch htonl wenn man einen long (also 4 Byte) in Network Byte Order bringen möchte. Da sin_port jedoch ein short ist, verwenden wir logischerweise htons.

- sin_addr: Das ist wohl der komplizierteste Teil der Struktur, hier muss die IP Adresse des Servers angegeben werden. sin_addr selbst ist vom Typ in_addr, welcher so aussieht:

struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long s_addr;
} S_un;

Zum Glück gibt es aber auch hierfür eine Hilfsfunktion um diese Struktur zu füllen:

unsigned long inet_addr(char* cp );

Diese übernimmt einen String der eine IP Adresse enthält und gibt einen long zurück. Diesen Wert schreiben wir dann einfach in das s_addr Mitglied der in_addr Struktur.

- sin_zero[8]: Wird nicht verwendet und sollte mit 0 gefüllt sein

Nun ergänzen wir main() wiefolgt (Ergänzungen sind fett):

long rc;
SOCKET s;
SOCKADDR_IN addr;
rc=startWinsock();
.....

memset(&addr,0,sizeof(SOCKADDR_IN)); // zuerst alles auf 0 setzten 
addr.sin_family=AF_INET;
addr.sin_port=htons(12345); // wir verwenden mal port 12345
addr.sin_addr.s_addr=inet_addr("127.0.0.1"); // zielrechner ist unser eigener

rc=connect(s,(SOCKADDR*)&addr,sizeof(SOCKADDR));
if(rc==SOCKET_ERROR)
{
  printf("Fehler: connect gescheitert, fehler code: %d\n",WSAGetLastError());
  return 1;
}
else
{
  printf("Verbunden mit 127.0.0.1..\n");
}

return 0;

Die Ausgabe sollte nun etwa so aussehen:

Fehler: connect gescheitert, fehler code: 10061

Natürlich gibt es einen Fehler beim verbinden, da wahrscheinlich auf unserem eigenen Rechner kein Server auf port 12345 läuft.
(Aber soweit ich weiss benutzt Backorifice Port 12345, wenn also eine Verbindung zustande kommt seit ihr ev. infisziert :-)

 

 

7. Server Socket erstellen

Im Gegensatz zu einem Client der Verbindungen aufbaut, ist die Aufgabe des Servers eine oder mehrere Verbindungen anzunehmen, und diese zu verwalten.
Dabei ist zu beachten: Für jede Verbindung wird ein Socket benötigt und das auf jeder Seite, also einer beim Client als auch einer beim Server. Also ist es nicht möglich, dass über einen Socket mehrere Verbindungen laufen. Ein Server benötigt zusätzlich noch einen Socket über den er Verbindungen annimmt. Dieser Socket macht nichts anderes als die ganze Zeit auf Verbindungen zu warten und diese anzunehmen. Die eigentliche Verbindung zum Client besteht dann über einen anderen Socket. Das sieht etwa so aus:

                                                                           -------
                                                                           |     |
                                                                           v     |
 Client Socket   ----------------------->    Server Socket: listen() -> accept() -
            \         connect()                                            |    
             \                                                             |
              \	                                                           v
               \------------------------------------------------------>  Socket 
                    eigentliche Verbindung zwischen den beiden

 

Der eigentliche Server Socket ist also dauernd in der Funktion accept. Sobald ein Client ein connect() zu ihm macht gibt accept() als Rückgabewert einen Socket, der die eigentliche Verbindung zum Client darstellt, zurück. Der Socket der Verbindungen akzeptiert wird nun erneut mit einem accept()-Aufruf in den accept Modus gebracht. Darin bleibt er auch bis wieder eine neue Verbindung kommt, usw...

Ok, nun erstellen wir eine neue Datei socksrv.c welche den Server darstellt. Die ersten Schritte sind gleich wie beim Client Socket, ausser den Name des Sockets habe ich zur besseren Übersicht in acceptSocket geändert:

#include <windows.h>
#include <winsock2.h>
#include <stdio.h>
//Prototypen
int startWinsock(void);
int main()
{
  long rc;
  SOCKET acceptSocket;
  SOCKADDR_IN addr;  // Winsock starten
  rc=startWinsock();
  if(rc!=0)
  {
    printf("Fehler: startWinsock, fehler code: %d\n",rc);
    return 1;
  }
  else
  {
    printf("Winsock gestartet!\n");
  }  // Socket erstellen
  acceptSocket=socket(AF_INET,SOCK_STREAM,0);
  if(acceptSocket==INVALID_SOCKET)
  {
    printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
    return 1;
  }
  else
  {
    printf("Socket erstellt!\n");
  }  return 0;
}

int startWinsock(void)
{
  WSADATA wsa;
  return WSAStartup(MAKEWORD(2,0),&wsa);
}

 

Ok, ab nun unterscheidet sich der Server vom Client. Welchen Port der Client von vorhin wirklich benutzt hat wissen wir eigentlich gar nicht. Wir wissen nur das der Client eine Verbindung zum Port 12345 herstellt. Der Socket selbst benötigt jedoch auch einen Port, und dieser wurde durch Windows für ihn gewählt. Also eine zufällige Portnummer. Bei einem Server geht das natürlich nicht, wir wollen wissen welchen Port unser Socket verwendet um Verbindungen anzunehmen. Deshalb müssen wir den Server Socket der die Verbindungen annimmt erst mal an einen Port "binden". Dies geschieht mit der Funktion bind():

int bind (
   SOCKET s, 
   const struct sockaddr FAR* name, 
   int namelen 
   );

ev. hat jemand gemerkt, dass es sich um die selben Parameter wie bei connect() handelt. Hier werden sie jedoch zT. etwas anders verwendet.
-s: Den socket den wir an einen Port binden wollen
-name: Hier muss die Adressfamilie natürlich wieder auf AF_INET sein, sin_port wird verwendet um den Port anzugeben an den wir den Socket binden wollen, und die IP Adresse können wir auf ADDR_ANY setzten.
-namelen: Grösse der Struktur im 2. Parameter.

Auch bind() gibt im Fehlerfalle SOCKET_ERROR zurück

Wir ergänzen main wiefolgt:

...

memset(&addr,0,sizeof(SOCKADDR_IN));
addr.sin_family=AF_INET;
addr.sin_port=htons(12345);
addr.sin_addr.s_addr=ADDR_ANY;
rc=bind(acceptSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN));
if(rc==SOCKET_ERROR)
{
  printf("Fehler: bind, fehler code: %d\n",WSAGetLastError());
  return 1;
}
else
{
  printf("Socket an port 12345 gebunden\n");
}
return 0;

Auch das sollte wieder fehlerfrei kompilierbar sein. Als nächsten Schritt ist nun listen() aufzurufen damit der Socket auf Verbindungen wartet:

int listen (
SOCKET s,
int backlog
);

-s: Der Socket welcher auf Verbindungen warten soll
-backlog: nicht von grosser Bedeutung, legt fest wieviele Verbindungen maximal ausstehend sein dürfen. Wir verwenden mal 10

Listen liefert SOCKET_ERROR zurück falls etwas schief ging.

Wir ergänzen also den Quellcode:

....

rc=listen(acceptSocket,10);
if(rc==SOCKET_ERROR)
{
  printf("Fehler: listen, fehler code: %d\n",WSAGetLastError());
  return 1;
}
else
{
  printf("acceptSocket ist im listen Modus....\n"); 
}

 

Und nun fehlt nur noch accept(), damit unser Socket auch Verbindungen akzeptiert. accept() ist folgendermassen definiert:

SOCKET accept (
SOCKET s,
struct sockaddr FAR* addr,
int FAR* addrlen
);


-s: Der Socket der Verbindungen akzeptieren soll (muss vorher mit listen() in den entsprechenden Modus gebracht werden), in unserem Fall acceptSocket
-addr: optional, ein Pointer auf eine SOCKADDR Strucktur in welchem die IP Adresse, etc. des Clients, der eine Verbindung hergestellt hatt, gespeichert werden, verwenden wir hier nicht.
-addrlen: optional, Pointer in welchem die Länge von addr gespeichert wird, verwenden wir hier nicht.

Falls accept fehlschlägt wird INVALID_SOCKET zurückgegeben.

accept() ist nun ein sogenannter "Blocking Call" (siehe Einleitung), und wird nicht zurückkehren bevor nicht eine Verbindung akzeptiert wurde oder sonst ein Fehler auftrat.

Wir ergänzen also den Code wiefolgt (fett):

long rc;
SOCKET acceptSocket;
SOCKET connectedSocket;
SOCKADDR_IN addr; .... connectedSocket=accept(acceptSocket,NULL,NULL); if(connectedSocket==INVALID_SOCKET) { printf("Fehler: accept, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("Neue Verbindung wurde akzeptiert!\n"); }

Wenn man den Server nun kompiliert und ausführt so wird das Pogramm bei der Zeile

acceptSocket ist im listen Modus....

stehen bleiben. Das ist auch logisch da, accept() ja ein "Blocking Call" ist.
Unterdessen kann man nun in einer zweiten Konsole das Programm von vorhin, also den Client, starten und wenn ihr alles richtig gemacht habt sollten folgende Ausgaben erscheinen (immer zuerst den Server starten!!):

Client:

Winsock gestartet!
Socket erstellt!
Verbunden mit 127.0.0.1..

Server:

Winsock gestartet!
Socket erstellt!
Socket an port 12345 gebunden
acceptSocket wartet auf Verbindungen.....
Neue Verbindung wurde akzeptiert!

 

Nun das war soeben der erste Erfolg, wir haben mit unserem Client Programm eine Verbindung zum Server hergestellt. Nur leider werden beide Programme danach beendet und die Verbindung ist wieder weg. Deshalb werden wir im nächsten Kapitel Daten zwischen den Beiden austauschen.

 

8. Daten austauschen

Das Senden und Empfangen von Daten ist beim Server und Client wieder identisch. Dazu stehen die folgenden beiden Funktionen zur Verfügung:

send:

int send (
SOCKET s,
const char FAR * buf,
int len,
int flags
);

-s: Socket über den wir die Daten senden wollen
-buf: Pointer auf einen Buffer in dem die zu sendenden Daten enthalten sind
-len: Wieviele Bytes von buf gesendet werden sollen
-flags: Brauchen wir nicht

Rückgabewert: Anzahl der gesendeten Bytes oder SOCKET_ERROR bei einem Fehler.

recv:

int recv (
SOCKET s,
char FAR* buf,
int len,
int flags
);


-s: Socket über den wir Daten empfangen wollen
-buf: Pointer zu einem Buffer in dem die empfangenen Daten gespeichert werden sollen
-len: Grösse des Buffers (oder wieviele Daten im Buffer gespeichert werden sollen)
-flags: benötigen wir nicht

Rückgabewert: Anzahl der empfangenen Bytes, 0 falls die Verbindung vom Partner getrennt wurde oder SOCKET_ERROR bei einem Fehler.

Hier noch schnell je ein Beispiel (gehört nicht zu sock.c oder socksrv.c, Annahme: s ist ein verbundener Socket):

char buf[256];
SOCKET s;
long rc;

...

strcpy(buf,"Hallo wie gehts?");
rc=send(s,buf,9,0);

Das würde die ersten 9 Zeichen von buf senden, in diesem Falle "Hallo wie", rc enthält die Anzahl der gesendeten Zeichen, falls alles glatt ging sollte das auch wieder 9 sein.

 

char buf[256];
SOCKET s;
long rc;
....

rc=recv(s,buf,256,0);

Hier werden maximal 256 Zeichen empfangen, es können auch weniger empfangen werden, bei recv dient der dritte Parameter nur dazu die grösse des Buffers im zweiten Parameter anzugeben. Wieviele Zeichen wirklich empfangen wurden sieht man im Rückgabewert (in diesem Falle rc).

 

Wie man sieht sind sich die beiden Funktionen recht ähnlich. Nur dass buf bei send() zum lesen der zu sendenden Daten und bei recv() zum speichern der empfangenen Daten verwendet wird. Aber in beiden Fällen muss buf auf einen gültigen Buffer im Speicher zeigen, und len darf nicht grösser als die Grösse des Buffers sein. Es sind auch beides Blocking Calls. Nur wird man das bei send() nicht gross merken, weil die Daten relativ schnell gesendet sind und die Funktion dann zurückkehrt. recv() jedoch wartet bis Daten ankommen, und falls der Partner nichts sendet, wartet er ewig.

Wir erweitern nun beide Programme folgendermassen: Der Client sendet einen vom Benutzer eingegebenen String an den Server, und dieser macht nicht anderes als mit "Du mich auch " + der Nachricht die er empfangen hat, zu antworten. Unten sind beide Programme nochmals vollständig aufgeführt.

Noch ein kleiner Hinweis: Wenn man mit Strings arbeitet hat man mehrere Möglichkeiten:
- Entweder sendet man das terminierende \0 gleich mit, und so kann der andere den String gleich ausgeben
oder die Variante die ich gewählt habe (ist etwas sicherer)
- Man prüft zuerst den Rückgabewert von recv, ist es kein Fehler macht man folgendes:

buf[rc]='\0'; 

(Annahme: buf ist der Buffer der die Daten speichert, und rc der Rückgabewert)
Da recv() ja die Anzahl der empfangenen Bytes zurück gibt wird so automatisch hinter dem letzten empfangenen Zeichen ein \0 angefügt.

Hier die beiden Dateien: sock.c, socksrv.c
Diese beiden Beispiele sind etwas umfänglich geschrieben und fangen auch keine fehlerhaften Eingaben ab, aber das war auch nie die Absicht, es soll ja nur ein kleines Beispiel sein.

9. Aufräumen

Falls ihr die beiden fertigen Dateien angesehen habt ist wohl schon klar wie man am Schluss jeder Winsock Anwendung noch aufräumt:
Jeder Socket muss mit closesocket() geschlossen werden, und ganz am Ende muss ein WSACleanup() Aufruf erfolgen. Natürlich müsste auch nach jedem "return 1;" im Quellcode alles aufgeräumt werden, aber das wurde hier bewusst weggelassen um Platz zu sparen.