Galileo Computing < openbook >
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.


Kompendium der Informationstechnik
 von Sascha Kersken
EDV-Grundlagen, Programmierung, Mediengestaltung
Buch: Kompendium der Informationstechnik
gp Kapitel 14 Netzwerkanwendungen
  gp 14.1 Netzwerkkonfiguration unter verschiedenen Betriebssystemen
    gp 14.1.1 Linux
    gp 14.1.2 Mac  OS
    gp 14.1.3 Windows
    gp 14.1.4 TCP/IP-Dienstprogramme
  gp 14.2 Server konfigurieren
    gp 14.2.1 Mac  OS
    gp 14.2.2 Windows
    gp 14.2.3 UNIX/Linux
    gp 14.2.4 Der Webserver Apache
  gp 14.3 Einführung in die Netzwerkprogrammierung
    gp 14.3.1 Die Berkeley Socket API
    gp 14.3.2 Ein einfaches Beispiel
  gp 14.4 Verteilte Anwendungen
    gp 14.4.1 J2EE
    gp 14.4.2 Microsoft .NET
    gp 14.4.3 Web Services
  gp 14.5 Zusammenfassung

gp

Prüfungsfragen zu diesem Kapitel (extern)


Galileo Computing

14.3 Einführung in die Netzwerkprogrammierung  downtop

Die Programmierung von Netzwerkanwendungen ist ein spannendes und sehr umfangreiches Thema. Es gibt zahlreiche gute Bücher, die diese faszinierende Disziplin der Softwareentwicklung ausführlich beschreiben; in Anhang B werden einige von ihnen vorgestellt. In diesem Abschnitt wird speziell die Programmierung von TCP/IP-Netzwerkanwendungen beschrieben, die herausragende Bedeutung dieser Protokollfamilie muss nicht noch einmal betont werden. Die konkrete Programmiersprache, die hier verwendet wird, ist Perl – in kaum einer anderen Sprache sind die Schnittstellen für die Netzwerkprogrammierung so bequem zu benutzen. Allerdings lassen sich die Beispiele leicht in anderen Programmiersprachen nachvollziehen, weil die verwendete Programmierschnittstelle für fast alle Sprachen und Plattformen in ähnlicher Art verfügbar ist.


Galileo Computing

14.3.1 Die Berkeley Socket API  downtop

Die Grundlage für die Programmierung von TCP/IP-Anwendungen ist die Verwendung von Sockets. Die TCP/IP-Kommunikation zwischen zwei Programmen findet stets über zwei Sockets statt, die man als die beiden Enden einer Netzwerkverbindung verstehen kann, vergleichbar mit dem aus Hör- und Sprechmuschel bestehenden Telefonhörer: Wenn eine Netzwerkanwendung einer verbundenen Anwendung etwas mitteilen möchte, schreibt sie in ihr Socket hinein; die Anwendung an der Gegenstelle kann die Information aus ihrem eigenen Socket lesen. Anders als die in Kapitel 6, Konzepte der Programmierung, beschriebenen Pipes ist ein Socket-Paar stets bidirektional, kann also zum Lesen und zum Schreiben gleichzeitig verwendet werden.

Erfreulicherweise ist die Socket-Programmierung schon vor langer Zeit standardisiert worden. An der University of California in Berkeley, wo auch UNIX zu seiner vollen Blüte gereift ist, wurde eine Programmierschnittstelle für die Programmierung von TCP/IP-Netzwerkanwendungen geschrieben, die Berkeley Socket API. Eine API (Application Programming Interface) ist eine standardisierte Sammlung von Funktionen und Schnittstellen für die Programmierung bestimmter Anwendungen.

Die ursprüngliche Fassung der Berkeley Socket API wurde als Sammlung von Bibliotheken und Header-Dateien für die Programmiersprache C unter BSD-UNIX geschrieben. Genau dieser Umstand sorgt dafür, dass C, UNIX und das Internet bis heute eine untrennbare Einheit bilden. Dennoch sind in der Folgezeit Fassungen der Socket API für viele verschiedene Programmiersprachen und Betriebssysteme geschrieben worden. Beispielsweise verfügen alle Windows-Systeme über die leicht angepasste Windows Socket Library (WinSock). In diesem Abschnitt wird die Netzwerkprogrammierung am Beispiel der Programmiersprache Perl vorgestellt, da es eine Reihe interessanter Perl-Module gibt, die die Entwicklung von Netzwerkanwendungen erleichtern.

Das Perl-Modul Socket

Bevor Sie die Funktionen der Berkeley Socket API in Perl effizient verwenden können, sollten Sie mittels use Socket das Modul Socket importieren. Zwar sind die eigentlichen Funktionen in den Sprachkern von Perl eingebaut, aber ohne die zahlreichen Konstanten für bestimmte Einstellungen, die in diesem Modul definiert sind, ist ihre Verwendung undenkbar.

Die im Folgenden genannten Funktionen – die in C genauso lauten und ähnliche Parameter haben – dienen dem Einrichten und Konfigurieren von Sockets.

Sockets erzeugen

Der Befehl zur Erzeugung eines Sockets hat die folgende grundlegende Syntax:

socket (SOCK, $domain, $type, $proto);

Diese Anweisung erzeugt ein neues Socket, dem das Dateihandle SOCK für Lese- und Schreiboperationen zugeordnet ist (natürlich können Sie statt SOCK einen beliebigen Dateihandle-Bezeichner verwenden).

Socket-Eigenschaften

Der Parameter $domain enthält einen Wert für das Einsatzgebiet des Sockets. Die entsprechenden Konstanten werden im Modul Socket definiert: AF_INET bezeichnet ein Socket für eine TCP/IP-Verbindung, während AF_UNIX ein so genanntes UNIX-Domain-Socket erzeugt. UNIX-Domain-Sockets sind ein beliebtes Mittel für die Inter-Prozess-Kommunikation auf UNIX-Systemen. Sie verwenden als Kommunikationsbasis keine Netzwerkverbindung, sondern eine Pipe-ähnliche Datei im Dateisystem. Im Folgenden werden nur AF_INET-Sockets behandelt.

In $type wird eine Konstante für den Socket-Typ angegeben. Es existieren die beiden grundsätzlichen Typen SOCK_STREAM für ein Stream-Socket und SOCK_DGRAM für ein Datagramm-Socket. Stream-Sockets werden in aller Regel für TCP-Verbindungen verwendet, weil sie eine dauerhafte Verbindung zwischen zwei Netzwerkknoten erzeugen. Datagramm-Sockets sind dagegen für die UDP- und ICMP-Netzwerkkommunikation vorgesehen, weil sie verbindungslos sind.

Der vierte Parameter, $proto, erwartet schließlich einen Integer, der das zu benutzende Kommunikationsprotokoll angibt. Zwar existieren zur Angabe des Protokolls keine Konstanten, aber dafür können Sie die Funktion getprotobyname() verwenden. Genau wie die gleichnamige C-Bibliotheksfunktion beziehungsweise der entsprechende UNIX-Systemaufruf wandelt sie ein durch seinen Namen angegebenes Protokoll in die entsprechende Nummer um, indem sie es in der Standarddatei /etc/protocols nachschlägt. Für UNIX-Domain-Sockets wird dagegen die Konstante PF_UNSPEC (»kein bestimmtes Protokoll«) angegeben.

Die folgenden beiden Beispielzeilen erzeugen ein TCP-Stream-Socket beziehungsweise ein UDP-Datagramm-Socket:

socket (TCP_SOCK, AF_INET, SOCK_STREAM, 
     getprotobyname ('tcp'));
socket (UDP_SOCK, AF_INET, SOCK_DGRAM, 
     getprotobyname ('udp'));

Adressen und Ports

Wie die IP-Adressierung funktioniert und was TCP- und UDP-Ports zu bedeuten haben, wurde im vorigen Kapitel ausführlich erläutert. Für die Socket-Programmierung müssen Adressen und Ports in speziellen Strukturen gespeichert werden, die der C-Standardbibliothek entstammen. Zu diesem Zweck werden verschiedene Bibliotheksfunktionen zur Verfügung gestellt, die der Umwandlung von Hostnames, Adressen und Ports in das korrekte Format dienen:

gp  gethostbyname ($hostname)
Diese Funktion wandelt den angegebenen Hostname in die entsprechende IP-Adresse um. Damit dies korrekt funktioniert, benötigen Sie Zugriff auf einen Namensdienst, der in der Lage ist, den entsprechenden Hostname aufzulösen, im Internet also beispielsweise auf einen DNS-Server. Das Ergebnis ist eine Struktur, die den Hostname und die in ASCII-Zeichen gepackte IP-Adresse enthält.
gp  inet_aton ($host_oder_ip)
Mit Hilfe dieser Funktion wird ein Hostname oder eine IP-Adresse in dezimaler Vierergruppenschreibweise (zum Beispiel 192.168.0.9) in vier ASCII-Zeichen verpackt. Viele Socket-Funktionen benötigen diese Schreibweise. inet_ntoa ($pack_adr) erledigt übrigens die umgekehrte Aufgabe und wird mitunter benötigt, um die mit einem Datenpaket empfangene Adresse des Gegenübers zu entschlüsseln.
gp  sockaddr_in ($port, $pack_adr)
Die Funktion sockaddr_in macht aus einer Port-Angabe und einer mittels inet_aton gepackten Adresse eine speziell verpackte Host/Port-Struktur, die Sie für die Angabe der Gegenstelle bei einer Netzwerkverbindung verwenden können. Das folgende Beispiel erstellt eine solche Struktur für den Zugriff auf Port 80 (den HTTP-Server) auf dem Host www.galileo-computing.de:
$webserver = sockaddr_in 
     (80, inet_aton ('www.galileo-computing.de'));
    Interessanterweise ist sockaddr_in gleichzeitig in der Lage, die umgekehrte Umwandlung vorzunehmen: Wenn Sie ein gepacktes Host/Port-Paar aus einem Datenpaket erhalten (siehe unten), können Sie es folgendermaßen entpacken:
       
($port, $host) = sockaddr_in ($pack_port_host);
gp  getservbyname ($service, $proto)
Statt der numerischen Angabe des Ports können Sie die so genannten Well-Known-Ports mit Nummern bis 1023, die bei gängigen Serverdiensten eingesetzt werden, auch durch ihren Namen angeben und per getservbyname umwandeln. Als zweites Argument benötigen Sie dazu das Transportprotokoll ('tcp' oder 'udp'). Zum Beispiel können Sie folgendermaßen eine Verbindung zu dem FTP-Server ftp.uni-koeln.de herstellen:
$port = getservbyname ('ftp', 'tcp');
$ftpserver = sockaddr_in 
     ($port, inet_aton ('ftp.uni-koeln.de'));

Verbindungen herstellen, Daten senden und empfangen

Was als Nächstes mit dem Socket geschieht, hängt zum einen davon ab, ob Sie ein Stream- oder ein Datagramm-Socket erzeugt haben und, falls es ein Stream-Socket ist, ob Sie gerade einen Client oder einen Server schreiben.

UDP-Sockets verwenden

UDP-Datagramm-Sockets können Sie nach dem Erstellen ohne weitere Formalitäten verwenden, um einfache Nachrichten zu versenden oder von anderen UDP-Anwendungen zu empfangen. Dazu werden die Anweisungen send() und recv() verwendet:

gp  send (SOCK, $data, 0, $receiver);
Diese Anweisung sendet den Dateninhalt $data über das Socket SOCK an den Empfänger $receiver. Der Empfänger wird als sockaddr_in-Struktur angegeben (siehe oben). Der dritte Parameter ist normalerweise 0; er kann in sehr seltenen Fällen einen speziellen Wert für besondere Routing-Einstellungen enthalten.
gp  recv (SOCK, $data, $maxlength, 0);
Die Anweisung recv() empfängt ein UDP-Datagramm. Der gelesene Wert wird in der Variablen $data gespeichert, während das Funktionsergebnis den Absender des Datagramms angibt. Von den meisten UDP-Servern können Sie Daten nur dann empfangen, wenn Sie zuvor mittels send() eine Anfrage an sie geschickt haben.

Auf UDP-Anwendungen wird hier nicht weiter eingegangen. Bei TCP-Sockets sieht die Angelegenheit komplizierter aus: Wenn Sie einen Server programmieren, der eine Dienstleistung zur Verfügung stellt, müssen Sie das Socket zum lauschenden (listening) Socket machen. Schreiben Sie dagegen einen Client, müssen Sie als Nächstes eine Verbindung zu einem Server-Socket herstellen.

TCP-Sockets verwenden

Beispielsweise können Sie folgendermaßen eine Verbindung zu dem Webserver www.galileo-design.de herstellen und ihm eine GET-Anfrage schicken, anschließend werden sämtliche zurückgegebenen Zeilen gelesen und am Bildschirm angezeigt:

#!/usr/bin/perl –w
use Socket;
use IO::Handle;
$port = getservbyname ('http', 'tcp');
$dest = sockaddr_in 
     ($port, inet_aton ('www.galileo-design.de'));
socket (SOCK, AF_INET, SOCK_STREAM, 
     getprotobyname ('tcp'));
connect (SOCK, $dest);
# Socket in den Autoflush-Modus schalten:
SOCK->autoflush (1);
# HTTP-Anfrage für Startseite senden:
print SOCK "GET / HTTP/1.0\n\n";
# Antwort des Servers zeilenweise ausgeben:
while ($line = <SOCK>) {
   chomp $line;
   print "$line\n";
}

Dieses Programm übernimmt tatsächlich die Hälfte der Funktionalität eines Browsers: Es richtet eine HTTP-Anfrage an einen Webserver und liest das erhaltene Dokument. Der kompliziertere Teil eines Browsers ist freilich nicht enthalten, nämlich derjenige, der den erhaltenen HTML-Code interpretiert und ein formatiertes Dokument daraus macht. Stattdessen wird die gesamte Ausgabe des Servers einfach angezeigt, und zwar nicht nur das eigentliche HTML-Dokument (die Startseite von Galileo Design), sondern auch sämtliche HTTP-Header.

TCP-Client-Funktionen

In diesem Programm werden einige neue Funktionen verwendet:

gp  connect (SOCK, $dest);
Diese Anweisung stellt eine Verbindung zu einem TCP-Server her, der mit Hilfe der gepackten Adresse $dest angegeben wird. Anschließend können Sie das Socket wie ein Dateihandle verwenden, um Daten daraus zu lesen und hineinzuschreiben.
gp  SOCK->autoflush(1);
Die Funktion autoflush ist im Modul IO::Handle definiert, das aus diesem Grund mittels use eingebunden wird. Sie sorgt dafür, dass Schreiboperationen in das Socket (beziehungsweise ein beliebiges Dateihandle) unmittelbar abgeschickt und nicht gepuffert werden, bis mehr Daten zusammenkommen. Wenn Sie die Zeile weglassen, bleibt das Programm einfach kommentarlos stehen.

Die restlichen Socket-Funktionen wurden bereits weiter oben erläutert. Die eigentliche Kommunikation mit dem Server erfolgt über die Funktion print und den Operator <>; die Verwendung dieser Anweisungen mit Dateihandles wurde bereits in Kapitel 5, Grundlagen der Programmierung, erläutert.

Interessant ist noch die eigentliche GET-Anfrage, die an den Webserver geschickt wird:

GET / HTTP/1.0

Es wird also die Startseite der Website (/) über das Protokoll HTTP 1.0 angefordert. In diesem Zusammenhang können Sie die aktuelle Variante HTTP 1.1 nicht verwenden, weil sie einige zusätzliche Header-Felder erwartet. Die Anfrage muss, wie hier gezeigt, durch zwei Zeilenumbrüche abgeschlossen werden.

TCP-Server-Funktionen

Für einen Server sind zusätzlich die folgenden Anweisungen wichtig:

gp  bind (SOCK, $addr);
Diese Anweisung bindet ein zuvor erzeugtes Socket an eine (lokale) sockaddr_in-Adresse. Sie können jede Ihrer lokalen Schnittstellenadressen, die Loopback-Adresse (127.0.0.1) oder die Konstante INADDR_ANY (für jede beliebige Adresse) verwenden.
    Noch wichtiger als die Adresse selbst ist übrigens der Port, den Sie mittels sockaddr_in in die Struktur einfügen, weil es der spezifische Port ist, auf dem der Server lauschen soll. Beachten Sie, dass Sie unter UNIX root-Rechte benötigen, um einen der Well-Known-Ports unter 1024 anzusprechen. Darüber hinaus ist es ein erhebliches Sicherheitsrisiko, ein selbst geschriebenes und nicht sicherheitsoptimiertes Programm auf einem solchen Port laufen zu lassen, den die Schnüffel-Tools potenzieller Angreifer standardmäßig auf Angriffsmöglichkeiten überprüfen.
       
    Das folgende Beispiel bindet ein Socket an jede beliebige Adresse des Servers und lauscht an Port 11.111:
       
bind (SOCK, sockaddr_in (11111, INADDR_ANY));
gp  listen (SOCK, $max_queue);
Die Anweisung listen benötigt ein mittels bind gebundenes Socket und wandelt es in ein lauschendes Socket um: Dieses Socket wartet auf den Verbindungsversuch eines Clients mittels connect, wie er weiter oben erläutert wurde. Das Argument $max_queue gibt die maximale Größe der Warteschlange dieses Sockets an, bestimmt also, wie viele Client-Verbindungen darauf warten können, akzeptiert zu werden. Normalerweise wird hierfür die Konstante SOMAXCONN verwendet, die den maximalen Wert enthält, den das Betriebssystem zulässt.
gp  $remote_addr = accept (SOCK_CONN, SOCK);
Sobald auf dem lauschenden Socket eine Verbindung eingeht, wird die nächste Programmzeile ausgeführt – normalerweise enthält sie einen solchen accept-Befehl. SOCK_CONN ist ein neues, mit dem Client verbundenes Socket, das durch diese Anweisung erzeugt wird. Die gepackte Adresse des Clients steht im Funktionsergebnis.

Das Modul IO::Socket

Eine große Erleichterung für Perl-Programmierer ist die Verwendung des Moduls IO::Socket statt der bisher behandelten Bibliotheksbefehle. Das Modul stellt bequeme Funktionen zur Verfügung, die die meisten komplexen Umwandlungsbefehle überflüssig machen. Beachten Sie, dass die Anweisungen, die in diesem Unterabschnitt behandelt werden, nicht kompatibel zur C-Standardbibliothek und zur ursprünglichen Berkeley Socket-API sind.

Eingebunden wird das Modul folgendermaßen:

use IO::Socket;

Perl-Objektorientierung

Beachten Sie, dass das Modul von IO::Handle abgeleitet ist und deshalb bereits die oben erwähnte Funktion autoflush beinhaltet. Die IO::*-Module verfolgen alle einen objektorientierten Ansatz, der hier einfach intuitiv verwendet wird. Auf die objektorientierte Programmierung mit Perl geht dieses Buch dagegen aus gutem Grund nicht ein – sie ist ziemlich komplex und umständlich und lässt sich nicht mit der Eleganz von Java-Klassen messen.

Mit Hilfe der IO::Socket-API wird ein neues Socket folgendermaßen eingerichtet:

$sock = IO::Socket::INET->new (@args);

Beachten Sie, dass $sock formal kein Dateihandle ist, sondern eine Referenz auf ein Socket, die von der entsprechenden Funktion zurückgegeben wird. Wenn Sie die strenge Syntaxüberprüfung (use strict) verwenden, müssen Sie diese Variable also mittels my deklarieren. In allen anderen Belangen funktioniert die Referenz wie ein gewöhnliches Socket. Die Funktion IO::Socket::INET->new erzeugt übrigens nur Sockets vom Typ AF_INET; für UNIX-Domain-Sockets müssen Sie dagegen IO::Socket::UNIX->new aufrufen.

Socket-Eigenschaften bei IO::Socket

Die Argumentliste @args gibt die Eigenschaften des Sockets als Name/Wert-Paare an. Die wichtigsten dieser Einstellungen sind folgende:

gp  PeerAddr gibt die Adresse des entfernten Hosts an. Sie können einen Hostname oder eine IP-Adresse verwenden. Wenn Sie PeerAddr angeben, wird nach der Erzeugung des Sockets automatisch connect() aufgerufen, es handelt sich also um eine Eigenschaft für ein Client-Socket.
gp  PeerPort ist der entfernte Port. Alternativ können Sie ihn auch, durch Doppelpunkt getrennt, bei PeerAddr angeben.
gp  LocalAddr gibt die lokale Adresse an.
gp  LocalPort ist der lokale Port. Wenn Sie nur LocalPort, aber nicht LocalAddr angeben, wird für die Adresse automatisch INADDR_ANY eingetragen – in der Regel genau das, was Sie benötigen.
gp  Proto ist der Name oder die Nummer des zu verwendenden Protokolls, also 'tcp' oder 'udp'.
gp  Listen gibt die Größe der Lauschwarteschlange an. Wenn dieses Argument vorhanden ist, wird darüber hinaus automatisch listen() aufgerufen, sodass das Socket zum lauschenden Socket wird.
gp  Type gibt den Socket-Typ an, also eine der Konstanten SOCK_STREAM oder SOCK_DGRAM.

IO::Socket-Objekte erzeugen

Das folgende Beispiel erzeugt ein typisches TCP-Client-Socket, das eine Verbindung zum FTP-Server der Uni Köln herstellt:

my $sock = IO::Socket::INET->new
     (Proto    => 'tcp',
      PeerAddr => 'ftp.uni-koeln.de',
      PeerPort => 'ftp');

Um dagegen ein lauschendes Socket einzurichten, das für einen Server verwendet werden kann, können Sie beispielsweise folgenden Befehl benutzen:

my $listen = IO::Socket::INET->new
     (Proto     => 'tcp',
      LocalPort => 22222,
      Listen    => SOMAXCONN);

Dieses Socket lauscht also auf Port 22.222 an jeder beliebigen Adresse des Hosts und lässt so viele wartende Verbindungsversuche zu, wie das Betriebssystem erlaubt.


Galileo Computing

14.3.2 Ein einfaches Beispiel  toptop

In diesem Abschnitt wird als praktisches Beispiel für die TCP/IP-Programmierung ein einfaches Client-Server-Paar vorgestellt. Der Server ist ein »Würfel-Server«, der das Werfen mit zwei Würfeln simuliert. Er versteht nur zwei Befehle: roll weist ihn an, einen neuen Wurf durchzuführen und das Ergebnis an den Client auszugeben, während exit die Verbindung zum Client beendet. Der Client bietet Ihnen dagegen die Möglichkeit, zu wählen, wie oft gewürfelt werden soll, oder die 0 einzugeben, um die Verbindung zu beenden.

Der Server

Der Würfel-Server ist ein so genannter Forking-Server: Mit Hilfe der in Kapitel 6, Konzepte der Programmierung, vorgestellten Funktion fork() erzeugt er für jede neue Client-Verbindung einen Child-Prozess. Auf diese Weise ist er in der Lage, mehrere Clients zur gleichen Zeit zu bearbeiten. Es gibt viele verschiedene Konzepte für die Implementierung solcher nebenläufigen Server; der Forking-Server ist der absolute Klassiker.

Dieser Server (wserver.pl) lauscht an Port 22.222. Wenn Sie ihn in einem Konsolenfenster starten, ist dieses Terminal blockiert, solange er läuft. Unter UNIX empfiehlt es sich daher, ihn folgendermaßen im Hintergrund zu starten:

$ ./wserver.pl &

Unter Windows existiert diese Option nicht; hier müssen Sie einfach ein weiteres Konsolenfenster öffnen, um normal weiterzuarbeiten.

Listing 14.1 zeigt den vollständigen Code des Servers; darunter folgen einige Erläuterungen.

Listing 14.1   Ein einfacher TCP-Server

#!/usr/bin/perl -w
use strict;
use IO::Socket;
# Lauschendes Socket erzeugen
my $listen = IO::Socket::INET->new
     (Proto     => 'tcp',
      LocalPort => 22222,
      Listen    => SOMAXCONN);
print "Server laeuft...\n";
#Accept-Schleife
while (1) {
   next unless my $conn = $listen->accept();
   my $child = fork;
   if ($child == 0) {
      handle_conn ($conn);
   }
}
 
sub handle_conn
{
   my $sock = shift;
   my $peer_addr = $sock->peerhost;
   print $sock "Hello $peer_addr, nice to meet you!\n";
   my $end = 0;
   while (1) {
      my $line = <$sock>;
      if ($line =~ /^roll/i) {
         my $w1 = int (rand (6)) + 1;
         my $w2 = int (rand (6)) + 1;
         print $sock "$w1 $w2\n";
      } elsif ($line =~ /^exit/i) {
         print $sock "Goodbye.\n";
         $sock->close;
         return;
      } else {
         print $sock "Unknown command: $line\n";
      }
   }
}

Den Server manuell testen

Ob der Server funktioniert, können Sie auch über das Programm telnet ausprobieren. Starten Sie einfach die folgende Verbindung:

$ telnet localhost 22222

Jedes Mal, wenn Sie den Befehl roll eingeben, erhalten Sie ein Paar von Würfelwürfen. Um die Verbindung zu beenden, können Sie exit eintippen.

Die Accept-Schleife versucht in jedem Durchgang, mittels accept eine Verbindung anzunehmen. Damit es nicht zum Forking kommt, wenn keine ankommende Verbindung zum Akzeptieren da ist, wird die Formulierung next unless gewählt: next beginnt unmittelbar mit dem nächsten Schleifendurchlauf, ohne die restlichen Zeilen des Schleifenrumpfes auszuführen. Beachten Sie die geänderte Schreibweise von accept in der objektorientierten IO::Socket-Form:

$conn = $listen->accept;

Wenn eine Verbindung angenommen wird, dann wird mittels fork() ein Child-Prozess erzeugt. Die nächste Zeile überprüft, ob sie im Child-Prozess ausgeführt wird ($child hat den Wert 0), und ruft in diesem Fall die Subroutine handle_conn mit dem verbundenen Socket als Argument auf.

Die einzige neue Socket-Anweisung in handle_conn ist die Methode $sock->peerhost, die die numerische IP-Adresse der Gegenstelle liefert. Interessant ist außerdem der Ausdruck, mit dessen Hilfe die Würfelwürfe berechnet werden: rand(n) ruft den Perl-Zufallsgenerator auf und erzeugt eine pseudozufällige Fließkommazahl, die größer als 0 und kleiner als n ist. Dieser Zahl werden mit int() die Nachkommastellen abgeschnitten, was eine Ganzzahl zwischen 0 und 5 ergibt. Schließlich wird noch 1 dazuaddiert, um einen korrekten Würfelwurf zu erhalten.

Der Client

Der Client für den Würfel-Server ist ein praktisches Beispiel dafür, wie man Clients programmieren kann, wenn die Sprache, also das Anwendungsprotokoll, des Servers bekannt ist: Da man sich auf die Reihenfolge von Ein- und Ausgabe verlassen kann, ist es kein Problem, die entsprechenden Befehle einfach hintereinander zu schreiben. Listing 14.2 zeigt den Code des Clients.

Listing 14.2   Ein einfacher TCP-Client

#!/usr/bin/perl -w
use strict;
use IO::Socket;
# Adresse des Würfel-Servers lesen
my $whost = $ARGV[0] || 'localhost';
print "Verbinde mit $whost\n";
# Socket-Verbindung herstellen
my $conn = IO::Socket::INET->new
     (Proto      => 'tcp',
      PeerAddr   => $whost,
      PeerPort   => 22222);
$conn->autoflush(1);
# Begrüßung des Servers lesen
my $gruss = <$conn>;
chomp $gruss;
print "$gruss\n";
# Würfel-Schleife
while (1) {
   print "Wie viele Wuerfe (0 zum Beenden)? > ";
   my $line = <>;
   chomp $line;
   if ($line !~ /^[0-9]+$/) {
      print "Bitte nur Zahlen eingeben!\n";
   } elsif ($line > 0) {
      for (my $i = 0; $i < $line; $i++) {
         print $conn "roll\n";
         my $wurf = <$conn>;
         chomp $wurf;
         my ($w1, $w2) = split (/\s+/, $wurf);
         print "Gewuerfelt: $w1 und $w2.\n";
      }
   } else {
      print $conn "exit\n";
      my $bye = <$conn>;
      chomp $bye;
      print "$bye\n";
      last;
   }
}

Nach allem, was bisher erläutert wurde, dürfte das Skript eigentlich selbsterklärend sein. Deshalb hier nur einige ganz wichtige Hinweise: Wenn beim Start kein Hostname und keine IP-Adresse auf der Kommandozeile angegeben werden, wählt der Client automatisch localhost – Kommandozeilenparameter liest Perl aus dem Array @ARGV. Die Anweisung last, die am Ende des Programms zum Verlassen der Schleife eingesetzt wird, entspricht dem aus C bekannten break.

Hier sehen Sie beispielhaft eine Ausgabe des Clients:

$ ./wclient.pl
Verbinde mit localhost
Hello 127.0.0.1, nice to meet you!
Wie viele Wuerfe (0 zum Beenden)? > 3
Gewuerfelt: 2 und 6.
Gewuerfelt: 6 und 5.
Gewuerfelt: 5 und 3.
Wie viele Wuerfe (0 zum Beenden)? > 4
Gewuerfelt: 2 und 6.
Gewuerfelt: 1 und 2.
Gewuerfelt: 1 und 6.
Gewuerfelt: 2 und 1.
Wie viele Wuerfe (0 zum Beenden)? >  Goodbye.

Analysieren Sie den Quellcode des Clients und des Servers, um herauszufinden, woher welche Ausgabe stammt. Formal nimmt der Client natürlich alle Ausgaben auf die Konsole selbst vor, aber manche Antworten des Servers gibt er einfach aus, während er den Würfelwurf auseinander nimmt und in eigenen Worten ausgibt.

Wenn Sie überprüfen möchten, ob tatsächlich mehrere Clients zur gleichen Zeit behandelt werden, können Sie einfach zwei Konsolenfenster öffnen und in jedem einen Client starten. Lassen Sie dann den einen Client sehr viele Würfe (zum Beispiel 20.000) durchführen und wechseln Sie zum anderen – obwohl Sie sehen können, dass die Würfe für den anderen Client noch ausgeführt werden, erhalten Sie sofort Antwort.






  

Einstieg in PHP 5

Einstieg in Java

C von A bis Z

Einstieg in C++

Einstieg in Linux

Einstieg in XML

Apache 2




Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de