Winsock Tutorial von c-worker.ch (Teil 3: UDP)

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. Unterschied zwischen TCP und UDP Sockets
3. Einen UDP Socket erstellen
4. Erstellen eines UDP Clients
5. Daten austauschen mit sendto und recvfrom
6. Erstellen eines UDP Servers
7. Verwendung von connect() bei UDP

 

1. Einleitung

Im ersten Tutorial wurden ausschliesslich TCP Sockets behandelt. Dieses Tutorial soll nun auf UDP Sockets eingehen. Es ist empfehlenswert zuerst das erste Tutorial zu lesen, weil dieses am ersten anknüpft und Kenntnisse aus dem ersten Tutorial voraussetzt.

 

2. Unterschied zwischen TCP und UDP Sockets

Im ersten Tutorial wurden diese beiden Socket Typen bereits einmal kurz vorgestellt.
Wie dort erwähnt wird sind TCP Sockets verbindungsorientiert und UDP Sockets nicht. TCP Sockets stellen erst eine Verbindung zu einem bestimmten Server her, tauschen dann Daten aus, und schliessen die Verbindung am Schluss wieder. Das ist bei UDP Sockets alles nicht nötig. Nun werden sich einige vielleicht fragen: "Wohin sendet er dann die Daten, wenn er nicht zu einem bestimmten Server verbunden ist ?". Ganz einfach, bei UDP Sockets verwendet man nicht send() sondern eine Funktion sendto(). Bei dieser Funktion kann man als Parameter übergeben wohin die Daten gesendet werden sollen.
Auch würde bei UDP Sockets ein recv() nicht viel Sinn machen. Man würde zwar die Daten bekommen, jedoch möchte man meistens noch darauf antworten (zumindest im Falle des Servers). Wie weiss man nun aber wem man antworten möchte ? Hier steht für UDP auch wieder eine spezielle Funktion zu verfügung: recvfrom(). recvfrom() übernimmt zusätzlich 2 Pointer welche dann mit den Informationen über den ursprünglichen Sender der soeben empfangenen Daten gefüllt werden.

Doch eines bleibt auch bei UDP Applikationen: Es braucht immer noch einen Server und einen Client. Zwar unterscheiden diese sich nicht so stark in den aufrufen der Winsock API so wie bei TCP, aber dennoch von der Logik. Es braucht ja immer noch einen Client der erst mal eine Anfrage sendet, und einen Server der dann darauf antwortet. Deshalb werden wir auch hier wieder zwei kleine Programme entwickeln: einen Client und einen Server.

 

3. Einen UDP Socket erstellen

Natürlich müssen wir erst wieder Winsock starten, dies wurde aber bereits im ersten Tutorial behandelt, und wird deshalb hier kommentarlos aufgeführt.

Zum erstellen eines UDP Sockets verwendet man gleich wie bei TCP Sockets die Funktion socket(), welcher hier nochmal aufgeführt ist:

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

Dabei verwenden wir alle Parameter gleich wie wenn wir einen TCP Socket erstellen wollten, ausser der Parameter type unterscheidet sich natürlich. Anstelle der Konstante SOCK_STREAM welche für TCP Sockets ist verwenden wir hier SOCK_DGRAM für UDP.
Hier mal ein kurzes Beispiel:

SOCKET s;
s=socket(AF_INET,SOCK_DGRAM,0);
if(s==INVALID_SOCKET)
{
  printf("Fehler: Der UDP Socket konnte nicht erstellt werden\n");
}

Natürlich läuft das so alleine noch nicht, da noch ein WSAStartup() erforderlich ist.

 

4. Erstellen eines UDP Clients

Wir schreiben nun einen UDP Client. Erst muss einmal Winsock gestartet werden, und ein UDP Socket (wie oben gezeigt) erstellt werden. Die Funktion startWinsock() stammt aus dem ersten Tutorial und wird hier kommentarlos aufgeführt. Hier der entsprechende Code:

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

//Prototypen
int startWinsock(void);
int main()
{
  long rc;
  SOCKET s;

  rc=startWinsock();
  if(rc!=0) 
  {
    printf("Fehler: startWinsock, fehler code: %d\n",rc);
    return 1;
  }
  else
  {
    printf("Winsock gestartet!\n");
  }
  // UDP Socket erstellen
  s=socket(AF_INET,SOCK_DGRAM,0);
  if(s==INVALID_SOCKET)
  {
    printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
    return 1;
  }
  else
  {
    printf("UDP Socket erstellt!\n");
  }
  return 0;
}

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

Ich habe die Datei mal udpcl.c gennant und mit

bcc32 C:\udpcl.c   

kompiliert. Wenn man das Beispiel nun ausführt sollte es folgende Ausgabe liefern:

Winsock gestartet!

UDP Socket erstellt!

Das war es auch schon, denn von jetzt an können wir mit sendto() und recvfrom() Daten austauschen.

 

5. Daten austauschen mit sendto und recvfrom

Wie oben bereits erwähnt benutzten wir für den Datentransfer bei UDP sendto() und recvfrom(). Die beiden Funktionen werden hier kurz vorgestellt.

sendto() sendet die Daten an einen bestimmen Host, diesen müssen wir mit der bereits bekannten SOCKADDR_IN Struktur angeben. Die Funktionsdefinition:

int sendto (
SOCKET s,
const char FAR * buf,
int len,
int flags,
const struct sockaddr FAR * to,
int tolen
);

-s: Socket über den wir die Daten senden wollen
-buf: Pointer auf einen Buffer der die zu sendenden Daten enthält
-len: Wieviele Zeichen von buf gesendet werden sollen
-flags: benötigen wir nicht, auf 0 setzten
-to: in unserem Falle ein Pointer auf eine SOCKADDR_IN Struktur die Informationen über den Zielrechner enthält
-tolen: länge von to, in userem Fall sizeof(SOCKADDR_IN)

Die Funktion liefert wie bei send() die Anzahl der gesendeten Zeichen zurück oder SOCKET_ERROR bei einem Fehler.

Hier ein kleines Beispiel für die verwendung von sendto() (Annahme: s ist ein UDP Socket):

SOCKET s;
SOCKADDR_IN addr;
char buf[256];

...

// addr vorbereiten
addr.sin_family=AF_INET;
addr.sin_port=htons(1234);
addr.sin_addr=inet_addr("127.0.0.1");

strcpy(buf,"Hallo Welt!");
rc=sendto(s,buf,strlen(buf),0,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN));
if(rc==SOCKET_ERROR)
{
printf("Fehler: sendto, fehler code: %d\n",WSAGetLastError());
return 1;
}
else
{
printf("%d Bytes gesendet!\n", rc);
}

 

Und nun zum empfangen von Daten: recvfrom() hat wie sendto() 2 zusätzliche Parameter. Diese haben jedoch nicht die gleiche Verwendung. Der erste zusätzliche Parameter ist ein Pointer auf eine SOCKADDR Struktur, in welche Informationen über den Rechner gespeichert werden von welchem wir die Daten empfangen haben. Wir müssen diese Stuktur nicht initialisieren, sondern sie wird von recvfrom() für uns abgefüllt. Der zweite zusätzliche Parameter ist ein Pointer auf ein int, welcher die grösse des ersten zusätzlichen Parameters, also die grösse einer SOCKADDR_IN Struktur in unserem Falle, enthält. Dieser int muss von uns initialisiert werden, muss jedoch als Pointer übergeben werden damit recvfrom() die benötigte grösse rein schreiben kann, falls sie zu klein ist.

Aber nun zur Funktionsdefinition:

int recvfrom (
SOCKET s,
char FAR* buf,
int len,
int flags,
struct sockaddr FAR* from,
int FAR* fromlen
);

-s: Socket über welchen wir Daten empfangen wollen
-buf: Pointer auf einen Buffer in dem die Daten gespeichert werden
-len: Grösse von buf (oder die maximale anzahl Zeichen die in buf gespeichert werden sollen)
-flags: benötigen wir nicht, auf 0 setzten
-from: Optionaler Pointer in welchem die Informationen über den sender der Daten für uns gespeichert werden
-fromlen: Optionaler Pointer in welchen wir die grösse von from speichern müssen

Die Funktion liefert wie recv() die anzahl der empfangenen Zeichen zurück oder 0 falls die Verbindung geschlossen wurde oder SOCKET_ERROR bei einem Fehler.

Hier wieder ein kleines Beispiel, es wird wieder davon ausgegangen das s ein bereits erstellter UDP Socket ist:

SOCKET s;
SOCKADDR_IN remoteAddr; int remoteAddrLen; char buf[256]; ... remoteAddrLen=sizeof(SOCKADDR_IN); rc=recvfrom(s,buf,256,0,&remoteAddr,&remoteAddrLen);
if(rc==SOCKET_ERROR)
{
printf("Fehler: recvfrom, fehler code: %d\n",WSAGetLastError());
return 1;
}
else
{
printf("%d Bytes empfangen!\n", rc);
buf[rc]='\0';
}

 

Wir erweitern unser Beispiel udpcl.c nun so, das wir einen Text eingeben können und diesen dann nach 127.0.0.1 an den Port 1234 senden. Nachdem wir die Nachricht gesendet haben warten wir bis wir eine Antwort empfangen und dann das ganze wieder von vorne. Änderungen sind fett dargestellt:

int main()
{
long rc;
SOCKET s;
char buf[256];
SOCKADDR_IN addr;
SOCKADDR_IN remoteAddr;
int remoteAddrLen=sizeof(SOCKADDR_IN);
rc=startWinsock(); if(rc!=0) { printf("Fehler: startWinsock, fehler code: %d\n",rc); return 1; } else { printf("Winsock gestartet!\n"); } //UDP Socket erstellen s=socket(AF_INET,SOCK_DGRAM,0); if(s==INVALID_SOCKET) { printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("UDP Socket erstellt!\n"); } // addr vorbereiten addr.sin_family=AF_INET; addr.sin_port=htons(1234); addr.sin_addr.s_addr=inet_addr("127.0.0.1"); while(1) { printf("Text eingeben: "); gets(buf); rc=sendto (s,buf,strlen(buf),0,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN)); if(rc==SOCKET_ERROR) { printf("Fehler: sendto, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("%d Bytes gesendet!\n", rc); } rc=recvfrom(s,buf,256,0,(SOCKADDR*)&remoteAddr,&remoteAddrLen); if(rc==SOCKET_ERROR) { printf("Fehler: recvfrom, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("%d Bytes empfangen!\n", rc); buf[rc]='\0'; printf("Empfangene Daten: %s\n",buf); } } return 0; }

Das ist eigentlich schon der fertige Client.

 

6. Erstellen eines UDP Servers

Wie bereits erwähnt unterscheiden sich Client und Server von den Winsock API aufrufen her nur sehr gering. Der einzige Unterschied des Server ist, das er bind() aufrufen muss, um nicht einen zufälligen Port zu bekommen, sondern genau den den wir wollen. Und da unser Client seine Daten zu Port 1234 sendet, ist das logischerweise der Port 1234. bind() wurde bereits im ersten Tutorial behandelt. Auch sendto() und recvfrom() wurden oben bereits durchgenommen. Der einzige Unterschied beim Server: beim Client haben wir beim recvfrom() aufruf die letzten beiden Parameter (die ja optional sind) theoretisch umsonst übergeben, da wir sie gar nicht verwendeten. Beim Server werden diese jedoch verwendet, sie sind sogar sehr wichtig, damit wir dem richtigen Rechner antworten können.

Der Server Unterscheidet sich auch noch dadurch das er erst recvfrom() aufruft und erst dann sendto(), er muss ja erst etwas empfangen um antworten zu können. Das Beispiel ist unten ohne weitere Kommentare aufgefürt, da ja ansich nichts neues darin ist. Er macht übrigens das gleiche wie im ersten Tutorial: er setzt einfach "Du mich auch " vorne hin. Ich habe die Datei mal udpsrv.c genannt:

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

//Prototypen
int startWinsock(void);
int main()
{
  long rc;
  SOCKET s;
  char buf[256];
  char buf2[300];
  SOCKADDR_IN addr;
  SOCKADDR_IN remoteAddr;
  int remoteAddrLen=sizeof(SOCKADDR_IN);
  rc=startWinsock();
  if(rc!=0)
  {
    printf("Fehler: startWinsock, fehler code: %d\n",rc);
    return 1;
  }
  else
  {
    printf("Winsock gestartet!\n");
  }

  //UDP Socket erstellen
  s=socket(AF_INET,SOCK_DGRAM,0);
  if(s==INVALID_SOCKET)
  {
    printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
    return 1;
  }
  else
  {
    printf("UDP Socket erstellt!\n");
  }

  addr.sin_family=AF_INET;
  addr.sin_port=htons(1234);
  addr.sin_addr.s_addr=ADDR_ANY;
  rc=bind(s,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN));
  if(rc==SOCKET_ERROR)
  {
    printf("Fehler: bind, fehler code: %d\n",WSAGetLastError());
    return 1;
  }
  else
  {
    printf("Socket an Port 1234 gebunden\n");
  }
  while(1)
  {
    rc=recvfrom(s,buf,256,0,(SOCKADDR*)&remoteAddr,&remoteAddrLen);
    if(rc==SOCKET_ERROR)
    {
      printf("Fehler: recvfrom, fehler code: %d\n",WSAGetLastError());
      return 1;
    }
    else
    {
      printf("%d Bytes empfangen!\n", rc);
      buf[rc]='\0';
    }
    printf("Empfangene Daten: %s\n",buf);
    //Antworten
    sprintf(buf2,"Du mich auch %s",buf);
    rc=sendto(s,buf2,strlen(buf2),0,(SOCKADDR*)&remoteAddr,remoteAddrLen);
    if(rc==SOCKET_ERROR)
    {
      printf("Fehler: sendto, fehler code: %d\n",WSAGetLastError());
      return 1;
    }
    else
    {
      printf("%d Bytes gesendet!\n", rc);
    }
  }
  return 0;
}

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

 

Nun kann man in der einen Konsole den Server starten und in der anderen den Client (den Server zuerst!), und die beiden sollten problemlos ihre Daten austauschen.

 

7. Verwendung von connect() bei UDP

Ich habe vorhin nur die halbe Wahrheit erzählt: Man muss bei UDP nicht zwingend sendto() und recvfrom() verwenden, man kann auch einfach send() und recv() nehmen. Das setzt jedoch voraus das wir vorher einen connect() aufruf auf unseren UDP Socket gemacht haben. Einige werden sich nun Fragen: "Aber heisst es nicht, UDP sei NICHT verbindungsorientiert ?". Ja ! Das ist auch weiterhin so ! UDP ist NICHT verbindungsorientiert. Und wenn man nun connect() auf einen UDP Socket aufruft stellt er auch KEINE Verbindung her ! In wirklichkeit wird damit einfach der Standard-Zielrechner angegeben. Somit wird bei UDP mit recv() und send() einfach der mit connect() festgelegte Standardrechner als Ziel verwendet. Wenn man nun aber nicht an den Standardrechner senden bzw. von ihm empfangen möchte kann man immernoch sendto() und recvfrom() verwenden.

Damit wir bei unserem Client nicht immer sendto() und recvfrom() verwenden müssen, werden wir ihn nun so umschreiben, das er per connect() das Standardziel festlegt und immer dorthin sendet. Änderungen sind fett hervorgehoben, und gelöschte Zeile sind fett auskommentiert:

  ...

  // addr vorbereiten
addr.sin_family=AF_INET;
addr.sin_port=htons(1234);
addr.sin_addr.s_addr=inet_addr("127.0.0.1"); rc=connect(s,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN)); while(1)
{
printf("Text eingeben: ");
gets(buf);
//rc=sendto (s,buf,strlen(buf),0,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN));
rc=send(s,buf,strlen(buf),0);
if(rc==SOCKET_ERROR)
{
//printf("Fehler: sendto, fehler code: %d\n",WSAGetLastError());
printf("Fehler: send, fehler code: %d\n",WSAGetLastError());
return 1;
}
else
{
printf("%d Bytes gesendet!\n", rc);
} //rc=recvfrom(s,buf,256,0,(SOCKADDR*)&remoteAddr,&remoteAddrLen); rc=recv(s,buf,256,0); if(rc==SOCKET_ERROR) { //printf("Fehler: recvfrom, fehler code: %d\n",WSAGetLastError()); printf("Fehler: recv, fehler code: %d\n",WSAGetLastError()); return 1; } else { printf("%d Bytes empfangen!\n", rc); buf[rc]='\0'; printf("Empfangene Daten: %s\n",buf); } } return 0; }

 

Auch dieses Tutorial ist hiermit zu Ende. Die beiden Dateien können hier heruntergeladen werden: udpcl.c, udpsrv.c
Mir ist klar das der Quellcode recht unsauber ist, WSACleantup(), closesocket(), etc. wurden extra weggelassen um Platz zu sparen, und das while(1) ist auch etwas unschön, aber es soll ja nur ein kleines Beispiel sein.