Japanisch-Paukerin (Linux-Magazin, Dezember 2001)

Eigene Applikationsserver und Clients in Perl entstehen ganz einfach -- ohne mit technischen Details der Netzwerkprogrammierung zu kämpfen.

Habt ihr euch schon mal gewundert, was passiert, wenn ihr euch mit ftp auf einem FTP-Server einloggt:

    $ ftp ftp.microsoft.com
    Connected to ftp.microsoft.com.
    220 Microsoft FTP Service ...
    Name (ftp.microsoft.com:ich):

Das Clientprogramm ftp öffnet auf der lokalen Maschine einen Netzwerk-Socket, kontaktiert übers Netzwerk den angesprochenen FTP-Server und fängt eine Konversation mit ihm an. Der FTP-Dämon auf dem FTP-Server sendet Fragen (z.B. Was ist Ihr Username?), nimmt Antworten oder neue Wünsche des Clients entgegen und reagiert darauf wieder mit neuen Ausgaben.

Eine derartige Client-Server-Applikation benötigt zweierlei:

Um mit einem solchen Service zu kommunizieren, braucht man keineswegs einen spezialisierten Client. Es reicht auch ein generisches Programm wie telnet, das, mit Hostname und Portnummer aufgerufen, lediglich Tastatureingaben an einen Server weiterleitet und dessen Antworten ausdruckt:

    telnet ftp.microsoft.com 21
    Trying 207.46.133.140...
    Connected to ftp.microsoft.com.
    Escape character is '^]'.
    220 ... Microsoft FTP Service
    LIST
    530 Please login with USER and PASS.
    QUIT
    221  Thank You for using Microsoft Products!
    Connection closed by foreign host.

Unser Kommando LIST wollte der FTP-Server nicht ausführen und blaffte statt dessen, dass wir uns erstmal mit den Kommandos USER und PASS identifizieren sollten. Ganz wie der ftp-Client unter der Motorhaube könnten wir USER bill hinschicken und mit dem Kommando PASS das entsprechende Passwort nachreichen -- schon wären wir als bill eingeloggt. Statt dessen verabschieden wir uns mit QUIT, worauf sich der Server anständig bedankt und die Verbindung abbricht.

So funktioniert das überall -- sogar Microsoft muss sich an die Spielregeln halten. Heute wollen wir das Internet mal mit einem neuen, simplen Service bereichern und sowohl Server als auch Client in Perl schreiben. Wie wär's mit einem Japanisch-Pauker?

Japanisch pauken

Das Skript jap.pl zeigt offline, wie später der Server funktionieren wird:

    *** Welcome to jap 1.0 ***
    add Vater - chichi
    add Mutter - haha
    quiz
    Mutter? (Hit Enter)
    haha
    Vater? (Hit Enter)
    chichi
    End of quiz.
    exit
    *** Thanks for using jap.

Mit dem add-Kommando lernt das Programm neue Begriffe, der deutsche und der japanische Ausdruck werden durch einen Gedankenstrich getrennt angefügt. Die gelernten Begriffe bleiben persistent in einer Datenbank, sodass sie auch beim nächsten Aufruf noch bestehen. Das Kommando quiz geht in zufälliger Reihenfolge durch alle Begriffe in der Datenbank, gibt jeweils den deutschen Begriff mit einem Fragezeichen aus, wartet, bis der Benutzer die Eingabetaste drückt und gibt dann die Lösung in japanisch aus, gefolgt von der nächsten Quizfrage in deutsch. Auf das Kommando exit hin bricht der Pauker die Verbindung ab. Weitere, oben nicht gezeigte Funktionen sind help (zeigt alle verfügbaren Kommandos) und dump (gibt alles bisher gelernte zeilenweise aus).

Listing 1: jap.pl

    01 #!/usr/bin/perl
    02 ##################################################
    03 # jap.pl -- Mike Schilli, 2001 (m@perlmeister.com)
    04 ##################################################
    05 use warnings;
    06 use strict;
    07 
    08 use lib '/etc/scripts';
    09 use Jap;
    10 
    11 Jap::shell();

Listing 2: Jap.pm

    01 #!/usr/bin/perl
    02 ##################################################
    03 # Jap.pm - Mike Schilli, 2001 (m@perlmeister.com)
    04 ##################################################
    05 use warnings;
    06 use strict;
    07 
    08 package Jap;
    09 
    10 use DB_File;
    11 
    12 my  %DB;
    13 my  $VERSION  = "1.0";
    14 my  $DBF  = "/etc/scripts/jap.dat";
    15 my  %CMDS = map { $_ => \&$_ } 
    16                            qw(help quiz add dump);
    17 ##################################################
    18 sub shell {
    19 ##################################################
    20     $|++;
    21     print "*** Welcome to jap $VERSION ***\n";
    22 
    23     tie %DB, "DB_File", $DBF, O_CREAT|O_RDWR, 0666 
    24         or die "Cannot open $DBF";
    25 
    26     while(<STDIN>) {
    27         chop; 
    28         my($cmd, $params) = split ' ', $_, 2;
    29 
    30         next unless defined $cmd;
    31         last if $cmd eq "exit";      # Ende?
    32 
    33         if(exists $CMDS{$cmd}) {
    34             $CMDS{$cmd}->($params);  # Kommando
    35         } else {
    36             print "Unknown command '$cmd'\n";
    37         }
    38     }
    39 
    40     untie %DB;      # Hash und DB synchronisieren
    41     print "*** Thanks for using jap.\n";
    42 }
    43 
    44 ##################################################
    45 sub help {                        # Hilfe ausgeben
    46 ##################################################
    47     print <<EOT;
    48 Commands:
    49     ADD german - japanese (add a new expression)
    50     QUIZ (run a quiz)
    51     EXIT (exit)
    52 EOT
    53 }
    54 
    55 ##################################################
    56 sub add {      # Ausdruck zur Datenbank hinzufügen
    57 ##################################################
    58     my $params = shift or (help(), return);
    59     
    60     my($ger, $jap) = split /-/, $params, 2;
    61     if(!defined $ger or ! defined $jap) {
    62         help();  # Falsch aufgerufen => Hilfe
    63         return;
    64     }
    65     $ger =~ s/^\s+|\s+$//g; # Leerräume am Anfang
    66     $jap =~ s/^\s+|\s+$//g; # und Ende entfernen
    67     $DB{$ger} = $jap;       # => in die Datenbank
    68 }
    69 
    70 ##################################################
    71 sub quiz {      # Japanisch pauken - Abfragestunde
    72 ##################################################
    73     my @keys = keys %DB;
    74 
    75     while(@keys) {
    76         my $key = splice(@keys, rand @keys, 1);
    77         print "$key? (Hit Enter)\n";   # Frage
    78         my $in = <STDIN>;       # Benutzereingabe
    79         last unless $in =~ /^\s*$/;    # Abbruch?
    80         print "$DB{$key}\n\n";  # Lösung ausgeben
    81     }
    82 
    83     print "End of quiz.\n";
    84 }
    85 
    86 ##################################################
    87 sub dump {              # Datenbankinhalt ausgeben
    88 ##################################################
    89     for my $key (sort keys %DB) {
    90         print "$key - $DB{$key}\n";
    91     }
    92 }
    93 
    94 1;

jap.pl selbst tut nichts, außer die Funktion shell() im Modul Jap.pm aufzurufen, welches den kleinen Pauker implementiert. jap.pl sucht nicht nur in den Standardpfaden nach Jap.pm, sondern wegen der Anweisung use libs auch in /etc/scripts. Dort installieren wir Jap.pm später hin.

Die Funktion shell() im Paket Jap im Modul Jap.pm entpuffert zunächst mit $|++ die Standardausgabe. Das wird später lebenswichtig, wenn das Skript nicht mehr lokal läuft -- denn dann müssen die Daten sofort über den Socket raus.

Sie gibt zuerst eine Meldung aus (*** Welcome ...) und nimmt dann Benutzereingaben über die Standardeingabe (STDIN) entgegen. Ausgaben erfolgen auf der Standardausgabe (STDOUT). Tippt der Benutzer exit ein, beendet sich das Skript mit freundlichem Gruß (*** Thanks ...).

Für die Datenbank nimmt Jap.pm mit DB_File die Berkeley-DB her und verbindet mittels tie den Hash %DB damit. Zeile 15 definiert die gültigen Jap-Shell-Kommandos im Hash %CMDS und verbindet jeweils eine Referenz auf eine gleichlautende Funktion mit den Einträgen. So ist der Key help in %CMDS mit einer Referenz auf die Funktion help() weiter unten im Skript verbandelt. Existiert zu einem eingegebenen Kommando eine Funktion, ruft Zeile 34 diese auf und übergibt ihr auch eventuell ans Kommando angehängte Parameter zusammengefasst als einen einzigen String.

untie in Zeile 40 synchronisiert die Datenbank mit dem Hash %DB. Bricht man das Programm also vor dem exit-Kommando etwa mit CTRL-C ab, gehen alle mit add angefügten Änderungen verloren.

Die Funktion quiz iteriert in zufälliger Reihenfolge über die Einträge des Hashs, indem sie diese in einem Array @keys ablegt und daraus immer wieder mit splice() einen einzelnen, zufälligen extrahiert. Die Lösung wird jeweils nachgereicht, sobald der Benutzer einen leeren String eingibt, also nur die Enter-Taste drückt. Steht noch etwas anderes dabei, bricht Zeile 79 das Quiz vorzeitig ab.

Vom Skript zum Server

Soweit, so einfach. Doch wie wird aus diesem recht normalen Skript ein netzwerkfähiger Server? Drei Möglichkeiten gibt's:

Die erste Methode verlangt viel Handarbeit (Details in [3]), die letzten zwei abstrahieren schön viele technische Details und sind daher sehr geeignet, schnell eigene Client-Server-Applikationen auf die Beine zu stellen. Hier kommen sie:

Der Super-Dämon inetd

Schon bei den ersten Unix-Systemen vor 25 Jahren stellte sich immer wieder die Aufgabe, aus einem einfachen Programm einen netzwerkfähigen Server zu basteln. Es entstand inetd, der Super-Dämon, der üblicherweise zum Zeitpunkt des Systemstarts hochfährt und gemäß den Einträgen in den Konfigurationsdateien /etc/services und /etc/inetd.conf auf vielen Ports gleichzeitig lauscht. Kommt auf einem von ihnen etwas an, leitet inetd den Request an das in inetd.conf zugeordnete Programm oder Skript weiter. Jedes Skript, dass aus STDIN liest und nach STDOUT schreibt, wird so flugs zum Dämon.

In /etc/services steht hierzu etwa

    jap 9000/tcp  # Der Japanisch-Server

und in /etc/inetd.conf entsprechend:

    jap stream tcp nowait nobody /etc/scripts/jap.pl

Änderungen an diesen beiden Dateien erfordern root-Rechte. 9000 ist hierbei die Portnummer, auf der der neue Service lauscht. tcp und stream bezeichnen das verwendete Protokoll. nowait (im Gegensatz zu wait) bewirkt, dass inetd für jeden neuen andockenden Client einen neuen Prozess startet, also verschiedene Clients gleichzeitig bedienen kann. nobody ist der Benutzer, in dessen Namen das Skript abläuft. Er braucht Schreibrechte für die Datenbank, in unserem Beispiel /etc/scripts/jap.dat. Installiert man jap und Jap.pm in /etc/scripts, braucht man nur inetd zu reinitialisieren. Dies geht sogar, ohne inetd neu zu starten, indem man mittels ps -ef | grep inetd seine Prozessnummer <pid> ausfindig macht und dieser dann ein HUP-Signal schickt:

    kill -HUP <pid>

Wer übrigens statt inetd den neuen xinetd-Dämon fährt (zum Beispiel mit Redhat 7.1), muss statt des Eintrags in /etc/inetd.conf die Datei /etc/xinetd.d/jap anlegen und folgendes hineinschreiben:


    service jap 
    {
      socket_type = stream
      wait        = yes
      user        = benutzername
      server      = /etc/scripts/jap.pl
      disable     = no
    }

Außerdem kann man mit dem Parameter only_from einschränken, für welche Rechner (per IP oder Hostname) der Service angeboten wird. Genaueres steht in [2]. Für xinetd heisst das Rekonfigurierungs-Signal außerdem -USR2 und nicht -HUP:

    kill -USR2 <pid>

Und das war's schon. Sobald inetd oder xinetd sich neu initialisieren, ist unser neuer Server auf Port 9000 verfügbar, wie wir leicht mit telnet als Testclient ausprobieren können:

    telnet localhost 9000
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    *** Welcome to jap 1.0 ***
    dump
    Mutter - haha
    Vater - chichi
    exit
    *** Thanks for using jap.
    Connection closed by foreign host.

Ohne Hilfe vom großen Bruder

Auch einen selbstverwalteten Server können wir mit Perl schnellstens zaubern: Die haarigen Seiten der Netzwerkprogrammierung erledigt das Modul NetServer::Generic von Charlie Stross, das wir wie immer kostenlos vom CPAN holen:

    perl -MCPAN -e'install NetServer::Generic'

Listing japfork.pl zeigt die Implementierung. Ein neu erzeugtes Objekt vom Typ NetServer::Generic erhält über die Methode port() den Port zugewiesen, auf dem es auf Clientanfragen warten soll. Die Methode callback() nimmt eine Referenz auf eine Funktion entgegen, die der Server für einen Request aufruft, und die aus STDIN liest und nach STDOUT schreibt. Ganz wie beim Ansatz mit inetd gibt's die Möglichkeit, parallele Prozesse abzufeuern, was die Methode mode() mit dem Argument "forking" einstellt. Die allowed()-Methode nimmt eine Referenz auf einen Array mit erlaubten IP-Addressen oder Hostnamen entgegen, auf die der Server den Service beschränken soll -- alle anderen Clients lässt er dann prompt abblitzen. Die Methode run() startet schließlich den Server -- fertig. Für Portnummern unter 1024 sind root-Rechte erforderlich, darüber geht's auch ohne. Soll der Server immer laufen, muss er beim Systemstart hochfahren, am einfachsten durch einen Eintrag in einer Datei wie /etc/rc.d/rc.local (RedHat).

Listing 3: japfork.pl

    01 #!/usr/bin/perl
    02 ##################################################
    03 # calcfork - Mike Schilli, 2001(m@perlmeister.com)
    04 ##################################################
    05 use warnings;
    06 use strict;
    07 
    08 use Jap;
    09 use NetServer::Generic;
    10 
    11 my $PORT = 9002;
    12 
    13 my ($server) = new NetServer::Generic;
    14 $server->port($PORT);
    15 
    16     # Für jetzt nur den lokalen Host zulassen
    17 $server->allowed(["127.0.0.1"]);
    18 
    19 $server->callback(\&Jap::shell);
    20 $server->mode("forking");
    21 print "Starting server on port $PORT\n";
    22 $server->run();

Ein eigenes Clientprogramm

Ein Client, der die Dienste unseres neuen Servers nutzen möchte, schnappt sich am besten das Modul IO::Socket, das neueren Perl-Distributionen von Haus aus beiliegt. Es abstrahiert sehr schön die unschönen Szenen, die sich abspielen, wenn man direkt mit Sockets und Funktionen aus der C-Welt wie inet_aton() herumorgelt. Der Konstruktor der Klasse IO::Socket::INET nimmt einfach einen String der Form "Rechnername:Port" entgegen, schon kann man Daten mit $socket->print("...") an den fremden Rechner senden und mit $socket->getline() die Ergebnisse abholen.

Statt unsere Datenbank von Hand zu füllen, steht mit japc.pl ein kleines Skript zur Verfügung, das den Server über einen Socket kontaktiert und ihm anschließend mit dem add-Kommando der Jap-Shell alle deutsch-japanischen Übersetzungen schickt, die im DATA-Bereich von japc.pl stehen. Zeile 23 sendet dem Server nach Abschluß der Arbeit dann das exit-Kommando, worauf dieser alle gesendeten Einträge in die Datenbank übernimmt -- bereit für die nächste Abfragerunde!

Listing 4: japc.pl

    01 #!/usr/bin/perl
    02 ##################################################
    03 # japc.pl --Mike Schilli, 2001 (m@perlmeister.com)
    04 ##################################################
    05 use warnings;
    06 use strict;
    07 
    08 my $H = "localhost";
    09 my $P = 9000;
    10 
    11 use IO::Socket;
    12 
    13 my $socket = IO::Socket::INET->new("$H:$P") or
    14     die "Cannot open $H:$P";
    15 
    16 my $intro = $socket->getline();
    17 
    18 while(<DATA>) {
    19     print "... adding $_";
    20     $socket->print("add $_");
    21 }
    22 
    23 $socket->print("exit\n");
    24 my $r = $socket->getline();
    25 print "$r\n";
    26 
    27 __DATA__
    28 Hallo, wie geht's? - Hajimemashite!
    29 Mein Name ist Mike - Mike to mooshimasu.
    30 Schönes Wetter, nicht wahr? - Ii ten'ki desu nee.
    31 Bis nächste Woche! - Mata raishuu.

Netzwerkprogrammierung ist gar nicht so schwer, oder? Wer Appetit auf mehr entwickelt hat, dem sei [3] empfohlen. Hervorragendes Buch, bisschen teuer, aber das kann man beim Essen wieder einsparen. Sofort kaufen und lesen! Besten Dank an Holger Wirtz, der wertvolle Anregungen zum Thema und speziell zu NetServer::Generic gab. Schreibt fleißig eigene Server!

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2001/11/Perl

[2]
Manualseiten zu xinetd: förder man mit man xinetd und man xinetd.conf zutage.

[3]
Lincoln Stein, ``Network Programming with Perl'', Addison-Wesley 2000, http://www.amazon.de/exec/obidos/ASIN/0201615711/perlmeistercom04

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.