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:
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.
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. |