Die Bibliothek SDL-Net implementiert Funktionen, um netzwerkbasierte Programme zu schreiben. Client/Server-Spiele und Chat-Programme sind angesichts der einfachen API innerhalb weniger Stunden zu schreiben.

Um wirklich zu verstehen, wie Netzwerkprogrammierung funktioniert, verweisen wir Sie auf die Bücherei ihrer Wahl. WikiBooks kann Ihnen da zum gegenwärtigen Zeitpunkt (April 2006) leider noch kein Buch anbieten.

Initialisierung

Bearbeiten

Die Programme werden mit der Funktion

 SDLNet_Init ()

für den Netzwerkzugriff vorbereitet.

Programme, die mit der SDL-Net Bibliothek geschrieben wurden, können mit

 gcc query.c -Wall -W `sdl-config --libs --cflags` -lSDL_net -o query

unter GNU/Linux übersetzt werden.

Netzwerk / Host

Bearbeiten

Versuchen zwei verschiedene Computersysteme sich miteinander zu verständigen, so kann es dazu kommen, dass der eine Computer die Daten rückwärts schreibt und der andere Computer sie aber gerne vorwärts lesen möchte. Aus diesem Grund gibt es Network Byte Order (netzwerkbasierte Reihenfolge der Daten) und die Host Byte Order (Reihenfolge der Daten auf Ihrem oder dem Zielcomputer, diese können unterschiedlich sein). Man einigt sich also einmal auf eine Reihenfolge der Daten, wie sie im Netzwerk zu übertragen sind und kann dann für jedes Zielcomputersystem einen Konvertierer schreiben. Einen solchen Konvertierer bieten die folgenden Funktionen:

void SDLNet_Write16 (Uint16 value, void *net_value);
void SDLNet_Write32 (Uint32 value, void *net_value);
Uint16 SDLNet_Read16 (void *net_value);
Uint32 SDLNet_Read32 (void *net_value);

Die Write*-Funktionen konvertieren einen Wert von Ihrem Computer (value) in die netzwerkbasierte Datenreihenfolge (net_value). Die Read*-Funktionen hingegen lesen einen Wert in Network Byte Order und konvertieren diesen in ein für Sie lesbares Host-Format.

Computer unterhalten sich über IP-Adressen. Über IP-Adressen gibt es viele Missverständnisse, die wir an dieser Stelle nur schwerlich ausräumen können. Um es kurz zu machen: Jeder Computer hat eine, aber nicht jeder Computer hat eine andere Adresse als der Nachbar. Viele Computer im Netz haben einen Namen, wie zum Beispiel de.wikibooks.org. Die IP-Adresse von de.wikibooks.org lautet 145.97.39.155, was sie leicht mit dem host-Programm (alternativ nslookup) nachprüfen können. Irgendwo in den Weiten des Netzes muss es also einen Verzeichnisdienst geben, der eine Tabelle speichert mit einem für uns lesbaren Namen und einer dazu passenden IP-Adresse. Diesen Verzeichnisdienst, den so genannten Domain Name Server erreichen Sie jedes Mal, wenn sie host aufrufen. Solche Abfragen können wir auch mit der SDL-Net stellen:

 int SDLNet_ResolveHost (IPaddress *address, char *host, Uint16 port);
 char *SDLNet_ResolveIP(IPaddress *address);

SDLNet_ResolveHost ermittelt zu einem Hostnamen (wie de.wikibooks.org) und einem Port die IP-Adresse. Die Funktion gibt 0 zurück, wenn sie erfolgreich war. SDLNet_ResolveIP gibt Ihnen von einer gegebenen IP-Adresse den Hostnamen zurück. Die Struktur IP-Adresse ist in der SDL-Net etwas anders, als es Programmierer von POSIX-Betriebssystemen sonst gewohnt sind:

typedef struct
{
  Uint32 host;
  Uint16 port;
} IPaddress;

Host und Port sind hier in Network-Byte-Order.

Beispiel DNS-Lookup

Bearbeiten

Mit dem bisher angehäuften Wissen können wir unser eigenes host-Programm schreiben:

 #include <SDL/SDL.h>
 #include <SDL/SDL_net.h>
 #include <stdlib.h>

 void init_sdl (void)
 {
   if (SDL_Init (SDL_INIT_VIDEO) < 0)
     exit (-1);
   atexit (SDL_Quit);
 }

 void do_net (void)
 {
   IPaddress addr;
   if (SDLNet_Init () < 0)
     {
       printf ("ERR Net: %s\n", SDLNet_GetError ());
       exit (-1);
     }
   if (SDLNet_ResolveHost (&addr, "de.wikibooks.org", 0) < 0)
     {
       printf ("ERR ResolveHost: %s\n", SDLNet_GetError ());
       SDLNet_Quit ();
       exit (-1);
     }
   else
     {
       Uint32 number;
       number = addr.host;
       printf ("Netzwerk-Reihenfolge: %d.%d.%d.%d\n",
         (number & 0xFF000000)>>24, (number & 0x00FF0000)>>16,
         (number & 0x0000FF00)>>8, (number & 0x000000FF));
       number = SDLNet_Read32 (&addr.host);
       printf ("Host-Reihenfolge:     %d.%d.%d.%d\n",
         (number & 0xFF000000)>>24, (number & 0x00FF0000)>>16,
         (number & 0x0000FF00)>>8, (number & 0x000000FF));
     }
 }

 int main (void)
 {
   init_sdl ();
   do_net ();
   return 0;
 }

Die Ausgabe des Programmes ist

Netzwerk-Reihenfolge: 155.39.97.145
Host-Reihenfolge:     145.97.39.155

wobei die letzte Zeile unserer Gewohnheit entspricht.

Verbinden

Bearbeiten

Mit Hilfe der TCP-Funktionen von SDL-Net können wir verschiedene Computer miteinander dauerhaft verbinden. Funktionen, um Rechner miteinander zu verbinden sind:

TCPsocket SDLNet_TCP_Open (IPaddress *Ip);
void SDLNet_TCP_Close (TCPsocket sock);

Ein TCPsocket ist in der Netzwerkprogrammierung üblicherweise ein einfacher Dateihandle. Bei SDL-Net ist es eine Datenstruktur, deren Innerstes uns nicht weiter interessieren sollte. Wichtig ist nur, daß wir auf einen TCPsocket schreiben und von ihm lesen können, als sei er eine Datei. SDLNet_TCP_Open öffnet eine Verbindung mit einem anderen Rechner, dessen Ip-Adresse wir schon kennen. SDLNet_TCP_Close beendet diese Verbindung wieder.

Wird ein Computer als Server auserkoren, darf er auswählen, ob er ankommende Verbindungen aus dem Netz annehmen möchte oder nicht. Falls der Server mit

 TCPsocket SDLNet_TCP_Accept (TCPsocket server);

die Verbindung annimmt, erhalten wir eine Möglichkeit, dem Client Nachrichten zu schreiben, indem wir einfach seinen Socket benutzen. Diese Funktion erwartet unseren mit SDLNet_TCP_Open erstellten socket als Argument. Diese Funktion wartet nicht darauf, daß Verbindungen ankommen. Liegt gerade keine an, so beendet sich die Funktion und gibt NULL zurück. Um zu ermitteln, welche IP-Adresse die ankommende Verbindung hat, dient die Funktion

 IPaddress *SDLNet_TCP_GetPeerAddress (TCPsocket socket);

Senden/Empfangen

Bearbeiten

Um nun wirklich auf den Socket zu schreiben oder von ihm zu lesen sind die folgenden Funktionen zu nutzen:

 int SDLNet_TCP_Send (TCPsocket socket, void *daten, int laenge);
 int SDLNet_TCP_Recv (TCPsocket socket, void *daten, int maxlaenge);

Die Parameter socket sind diejeweils verbundenen Sockets, daten ist ein Speicherbereich, den Sie übermitteln wollen, beispielsweise ein String. laenge bzw maxlaenge ist die Größe des zu übermittelnden Speicherbereiches, z. B. die Länge des Strings. SDLNet_TCP_Send gibt die Anzahl der geschriebenen Bytes zurück. Ist diese weniger als laenge, so handelt es sich um einen Fehler oder die Gegenstelle hat aufgelegt. SDLNet_TCP_Recv gibt die Anzahl der gelesenen Bytes zurück. Ist dies 0, so hat die Gegenstelle aufgelegt, bei -1 kam es zu einem allgemeinen Netzwerkfehler.

Server-Beispiel

Bearbeiten

Das folgende Beispiel zeigt, wie ein Server aufgebaut ist. Dieser Server übermittelt lediglich ein Rechteck vom Typ SDL_Rect und nimmt ein modifiziertes Rechteck entgegen.

 #include <SDL/SDL.h>
 #include <SDL/SDL_net.h>
 #include <stdlib.h>

 SDL_Surface *screen;
 TCPsocket server;

 void init_sdl (void)
 {
   if (SDL_Init (SDL_INIT_VIDEO) < 0)
     exit (-1);
   atexit (SDL_Quit);
 }

 void init_net (void)
 {
   IPaddress addr;
   if (SDLNet_Init () < 0)
     {
       printf ("ERR Net: %s\n", SDLNet_GetError ());
       exit (-1);
     }
   if (SDLNet_ResolveHost (&addr, NULL, 1234) < 0)
     {
       printf ("ERR ResolveHost: %s\n", SDLNet_GetError ());
       SDLNet_Quit ();
       exit (-1);
     }
   server = SDLNet_TCP_Open (&addr);
   if (server == NULL)
     {
       printf ("ERR TCP_Open: %s\n", SDLNet_GetError ());
       SDLNet_Quit ();
       exit (-1);
     }
 }

 int main (void)
 {
   TCPsocket client = NULL;
   SDL_Rect *rect;
   int maxread;
   init_sdl ();
   init_net ();
   printf ("Server-Socket ist offen\n");
   client = SDLNet_TCP_Accept (server);
   while (client == NULL)
     {
       /* eine Sekunde warten */
       SDL_Delay (1000);
       client = SDLNet_TCP_Accept (server);
     }
   printf ("Client wurde akzeptiert\n");
   /* Schicke erstes Rechteck */
   rect = (SDL_Rect *)malloc (sizeof (SDL_Rect));
   rect->x = 10;
   rect->y = 20;
   rect->w = 100;
   rect->h = 200;
   SDLNet_TCP_Send (client, rect, sizeof (SDL_Rect));

   maxread = SDLNet_TCP_Recv (client, rect, sizeof (SDL_Rect));
   printf ("I am Server. \n Read = %d\n", maxread);
   printf ("x = %d, y = %d, w = %d, h = %d\n",
     rect->x, rect->y, rect->w, rect->h);
   SDLNet_TCP_Close (client);
   SDLNet_TCP_Close (server);
   return 0;
 }

Client-Beispiel

Bearbeiten

Der Client empfängt ein Rechteck, modifiziert es und schickt es wieder zurück.

 #include <SDL/SDL.h>
 #include <SDL/SDL_net.h>
 #include <stdlib.h>

 SDL_Surface *screen;
 TCPsocket client;

 void init_sdl (void)
 {
   if (SDL_Init (SDL_INIT_VIDEO) < 0)
     exit (-1);
   atexit (SDL_Quit);
 }

 void init_net (void)
 {
   IPaddress addr;
   if (SDLNet_Init () < 0)
     {
       printf ("ERR Net: %s\n", SDLNet_GetError ());
       exit (-1);
     }
   if (SDLNet_ResolveHost (&addr, "localhost", 1234) < 0)
     {
       printf ("ERR ResolveHost: %s\n", SDLNet_GetError ());
       SDLNet_Quit ();
       exit (-1);
     }
   client = SDLNet_TCP_Open (&addr);
   if (client == NULL)
     {
       printf ("ERR TCP_Open: %s\n", SDLNet_GetError ());
       SDLNet_Quit ();
       exit (-1);
     }
 }

 int main (void)
 {
   SDL_Rect *rect;
   int maxread;
   init_sdl ();
   init_net ();
   printf ("Client-Socket ist offen\n");
   rect = (SDL_Rect *) malloc (sizeof (SDL_Rect));
   maxread = SDLNet_TCP_Recv (client, rect, sizeof (SDL_Rect));
   printf ("Read = %d\n", maxread);
   printf ("x = %d, y = %d, w = %d, h = %d\n",
     rect->x, rect->y, rect->w, rect->h);
    
   /* zurückschicken */
   printf ("send back\n");
   rect->x = 1;
   rect->y = 2;
   rect->w = 100;
   rect->h = 200;
   SDLNet_TCP_Send (client, rect, sizeof (SDL_Rect));
   SDLNet_TCP_Close (client);
   return 0;
 }

Viele Clients

Bearbeiten

Hat man viele Clients, die sich an einem Server anmelden, beispielsweise, weil mehrere Personen zusammen ein Spiel spielen, so kann man eine bestimmte Zeit auf eine bestimmte Anzahl an Clients warten. Folgender Programmausschnitt verdeutlich das:

 wait = 10;
 num_clients = 0;
 do
   {
     SDL_Delay (1000);
     sock_client[num_clients] = SDLNet_TCP_Accept (sock_server);
     if (sock_client[num_clients])
       {
         printf ("accepted %d\n", num_clients + 1);
         num_clients++;
       }
     wait--;
   } while ((num_clients < 2) && (wait > 0));

Hier wartet der Server maximal 10 Sekunden auf genau 2 Clients. Diese Clients können nun nach und nach mit Daten versorgt

 SDLNet_TCP_Send (sock_client[0], &data, sizeof (int));
 SDLNet_TCP_Send (sock_client[1], &data, sizeof (int));

oder abgefragt

SDLNet_TCP_Recv (sock_client[0], &data, sizeof (int));
SDLNet_TCP_Recv (sock_client[1], &data, sizeof (int));

werden.

Grundvorraussetzung dafür ist, daß sich alle Clients ständig in einem empfangsbereiten oder sendebereiten Zustand befinden. Ansonsten blockiert das Warten auf Client-1 den Datenaustausch mit Client-2. Dies ist im Allgemeinen wenig wünschenswert, denn die Clients selbst sollen natürlich ihre Aufgaben erledigen können. Kommt es bei einzelnen Clients zu Verzögerungen, so könnte doch schon ein anderer Client bedient werden.

Die Lösung für dieses Problem heißt Socket Sets, also Mengen von Sockets. Ein SDLNet_SocketSet ist letztlich nur eine Sammlung von Sockets, bei der Sie mittels

 int SDLNet_TCP_AddSocket (SDLNet_SocketSet set, TCPsocket socket);
 int SDLNet_TCP_DelSocket (SDLNet_SocketSet set, TCPsocket socket);

einen Socket hinzufügen oder entfernen können. Die Funktionen geben die Anzahl der Sockets in der Menge zurück oder -1 im Fehlerfall. Ein Socket Set wird mit

 SDLNet_SocketSet SDLNet_AllocSocketSet (int max_anzahl_sockets);

erzeugt und mit

 void SDLNet_FreeSocketSet (SDLNet_SocketSet set);

wieder freigegeben. Um herauszufinden, ob an den Netzwerkverbindungen "etwas los ist", dient

 int SDLNet_CheckSockets (SDLNet_SocketSet set, Uint32 zeit);

Diese Funktion wartet zeit (in Millisekunden) lang auf Aktivität, 0 bedeutet gar nicht zu warten, -1 wartet einige Wochen. SDLNet_CheckSockets gibt die Anzahl der aktiven Sockets zurück, die Sie dann mit

 int SDLNet_SocketReady (TCPsocket socket);

einzeln überprüfen müssen. Die Funktion gibt 0 zurück, wenn der Socket inaktiv ist.

Socket-Set-Server

Bearbeiten

Folgendes Beispiel implementiert einen Server, der ein Socket-Set für zwei Clients implementiert:

#include <SDL/SDL.h>
#include <SDL/SDL_net.h>
#include <stdlib.h>
#include <stdio.h>

int main (void)
{ 
  IPaddress address;
  TCPsocket sock_server, sock_client[2];
  SDLNet_SocketSet set;
  int wait, num_clients, data, ready;


  if(SDL_Init (0) ==-1)
    {
      printf ("SDL_Init: %s\n", SDL_GetError ());
      exit (1);
    }
  if(SDLNet_Init () ==-1)
    {
      printf ("SDLNet_Init: %s\n", SDLNet_GetError ());
      exit(1);
    }
  if (SDLNet_ResolveHost (&address, NULL, 10000) == -1)
    {
      printf ("SDLNet_ResolveHost: %s\n", SDLNet_GetError ());
      SDLNet_Quit ();
      exit (1);
    }
  sock_server = SDLNet_TCP_Open (&address);
  if (!sock_server)
    {
      printf ("SDLNet_TCP_Open: %s\n", SDLNet_GetError ());
      SDLNet_Quit ();
      exit (1);
    }

  printf ("Init finished. Wait for clients...\n");

  wait = 10;
  num_clients = 0;
  do
    {
      SDL_Delay (1000);
      sock_client[num_clients] = SDLNet_TCP_Accept (sock_server);
      if (sock_client[num_clients])
        {
          printf ("accepted %d\n", num_clients + 1);
          num_clients++;
        }
      wait--;
    } while ((num_clients < 2) && (wait > 0));

  if (wait > 0)
    {
      /* Jetzt haben wir 2 Clients */

      /* Platz für 2 Sockets */
      set = SDLNet_AllocSocketSet (2);
      if (!set)
        {
          printf ("SDLNet_AllocSocketSet: %s\n", SDLNet_GetError ());
          exit (1);
        }
      SDLNet_TCP_AddSocket (set, sock_client[0]);
      if (SDLNet_TCP_AddSocket (set, sock_client[1]) != 2)
        {
          printf ("SDLNet_TCP_AddSocket: %s\n", SDLNet_GetError ());
          exit (1);
        }

      num_clients = 2;
      while (num_clients > 0)
        {
          ready = SDLNet_CheckSockets (set, 1000 * 10);
          if (ready < 1)
            {
              printf ("SDLNet_CheckSockets: %s\n", SDLNet_GetError ());
              exit (1);
            }
          else
            {
              printf ("There are %d sockets ready\n", ready);
            }

          if (SDLNet_SocketReady (sock_client[0]))
            {
              SDLNet_TCP_Recv (sock_client[0], &data, sizeof (int));
              printf ("\tReceive-0  %d\n", data);
              data = 2 * data;
              printf ("\tSend-0  %d\n", data);
              SDLNet_TCP_Send (sock_client[0], &data, sizeof (int));
              num_clients = SDLNet_TCP_DelSocket (set, sock_client[0]);
            }

          if (SDLNet_SocketReady (sock_client[1]))
            {
              SDLNet_TCP_Recv (sock_client[1], &data, sizeof (int));
              printf ("\tReceive-1  %d\n", data);
              data = 2 * data;
              printf ("\tSend-1  %d\n", data);
              SDLNet_TCP_Send (sock_client[1], &data, sizeof (int));
              num_clients = SDLNet_TCP_DelSocket (set, sock_client[1]);
            }
        } /* while */

      /* Aufräumen */
      SDLNet_FreeSocketSet (set);
      SDLNet_TCP_Close (sock_server);
      SDLNet_TCP_Close (sock_client[0]);
      SDLNet_TCP_Close (sock_client[1]);
    }
  else
    {
      printf ("...sorry!\n");
    }
  return 0;
}

Socket-Set-Client

Bearbeiten

Clients brauchen überhaupt nichts zu beachten, sie merken nichts von den Socket Sets des Servers. Damit Sie jedoch die Auswirkungen testen können, dürfen Sie folgendes Beispiel verwenden. Aktivieren Sie in einem Client die im Code grünfarben gezeichnete Pause, im anderen nicht:

#include <SDL/SDL.h>
#include <SDL/SDL_net.h>
#include <stdlib.h>
#include <stdio.h>

int main (void)
{ 
  IPaddress address;
  TCPsocket sock_server;
  int data;

  if(SDL_Init (0) ==-1)
    {
      printf ("SDL_Init: %s\n", SDL_GetError ());
      exit (1);
    }
  if(SDLNet_Init () ==-1)
    {
      printf ("SDLNet_Init: %s\n", SDLNet_GetError ());
     exit(1);
    }
  if (SDLNet_ResolveHost (&address, "localhost", 10000) == -1)
    {
      printf ("SDLNet_ResolveHost: %s\n", SDLNet_GetError ());
      SDLNet_Quit ();
      exit (1);
    }
  sock_server = SDLNet_TCP_Open (&address);
  if (!sock_server)
    {
      printf ("SDLNet_TCP_Open: %s\n", SDLNet_GetError ());
      SDLNet_Quit ();
      exit (1);
    }

  printf ("Init finished. Connected to server. Send Data after 5 Seconds...\n");
  SDL_Delay (5000);

  data = 2048;
  printf ("\tSend %d\n", data);
  SDLNet_TCP_Send (sock_server, &data, sizeof (int));
  SDLNet_TCP_Recv (sock_server, &data, sizeof (int));
  printf ("\tReceive %d\n", data);

  SDLNet_TCP_Close (sock_server);
  return 0;
}