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?
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).
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();
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.
Soweit, so einfach. Doch wie wird aus diesem recht normalen Skript ein netzwerkfähiger Server? Drei Möglichkeiten gibt's:
STDIN und STDOUT, sondern
nutzt Netzwerk-Sockets zum Lesen und Schreiben.
Die Perl-Funktionen bind()
und accept() erlauben es dem Server, anfragende Clients abzufangen, deren
Wünsche entgegenzunehmen und Antworten über bereitgestellte
Sockets, die wie Datei-Handles aussehen, zurückzuschicken.
Das Skript verwendet weiterhin STDIN und STDOUT.
inetd übernimmt per Konfiguration die Netzwerkfunktionen.
Er läuft seit dem Systemstart im Hintergrund,
lauscht unter anderem auf dem für unser Skript konfigurierten Port,
leitet eingehende Requests an
das Skript weiter und sendet dessen STDOUT-Ausgabe an den
angedockten Client zurück.
Das Modul NetServer::Generic übernimmt in einem kleinen Skript
wie japfork.pl die Serverfunktionen. Nach dem Start lauscht es
auf einem ausgewählten Port auf
eingehende Requests, leitet deren ankommende Daten in
STDIN des Skripts
weiter und schickt die Skript-STDOUT-Ausgaben wieder durch die
Leitung zurück an den Client.
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:
inetdSchon 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.
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).
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 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!
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!
xinetd: förder man mit man xinetd
und man xinetd.conf zutage.
![]() |
Michael Schilliarbeitet 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. |