Eigene Clients und eigene Server sind in Perl schnell programmiert – ohne mit den Details der Netzwerkprotokolle zu kämpfen. (Michael Schilli)
Haben Sie schon einmal den Wunsch verspürt, einen eigenen Server-Dienst zu schreiben? Mit Perl ist das kein Problem, das Modul »NetServer::Generic« hilft Ihnen. Bevor wir loslegen, sehen wir uns aber erst einmal an, was bei etablierten Diensten unter der Haube passiert:
»ftp ftp.microsoft.com Connected to ftp.microsoft.com. 220 Microsoft FTP Service ... Name (ftp.microsoft.com:ich):
Das Client-Programm »ftp« öffnet auf der lokalen Maschine einen Netzwerk-Socket, kontaktiert den gewünschten FTP-Server und fängt eine Konversation mit ihm an. Der FTP-Daemon sendet Fragen (etwa: 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:
- Ein Client-Programm, das die Verbindung zum Server aufbaut, Eingaben vom Benutzer entgegennimmt und diese an den Server weiterleitet. Es gibt die Antworten des Servers für den Benutzer aus.
- Ein Server-Programm, das ständig auf anfragende Clients wartet. Kommt einer an, startet es eine private Session, liest seine Wünsche und sendet die Antworten zurück. Ein Server bedient unter Umständen mehrere Clients gleichzeitig.
FTP unter der Haube
Um mit einem solchen Service zu kommunizieren, ist nicht immer ein spezialisierter Client nötig. Oft genügt ein generisches Programm wie Telnet. Mit Hostname und Portnummer aufgerufen leitet es die Eingaben an den Server weiter und gibt dessen Antworten aus:
»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.
Das Kommando »LIST« wollte der FTP-Server nicht ausführen. Er verlangte, dass wir uns vorher mit den Kommandos »USER« und »PASS« authentifizieren. Ein FTP-Client würde jetzt den Benutzernamen abfragen und ihn mit »USER bill« an den Server senden, danach das Passwort mit »PASS …« nachreichen – schon wäre er als »bill« eingeloggt. Beide Kommandos könnten wir auch mit Telnet absenden, stattdessen verabschieden wir uns: »QUIT«. Der Server bedankt sich freundlich und beendet die Verbindung. So funktioniert das bei vielen Protokollen – und sogar Microsoft muss sich an die Spielregeln halten.
Wir wollen uns aber nicht auf vorhandene Dienste beschränken, sondern das Internet um einen neuen simplen Service bereichern. Wie wäre es mit einem Japanisch-Pauker? Der Pauker wird mit Deutsch-Japanisch-Wortlisten gefüttert und soll die Begriffe in Form eines Quiz abfragen. Aufgeteilt ist die Lernhilfe in einen Server und einen Client, beide in Perl geschrieben.
Japanisch pauken
Das Skript »jap.pl« (Listing 1) simuliert 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 Kombinationen, den deutschen und den japanischen Ausdruck erwartet es in einer Zeile, durch einen Gedankenstrich voneinander getrennt. Die gelernten Begriffe speichert es persistent in einer Datenbank, so dass es sie auch beim nächsten Aufruf noch kennt.
Das Kommando »quiz« geht in zufälliger Reihenfolge durch alle Einträge in der Datenbank und gibt jeweils den deutschen Begriff aus, gefolgt von einem Fragezeichen. Es wartet dann, bis der Benutzer die Eingabetaste drückt, und gibt schließlich die japanische Lösung aus. Darauf folgt die nächste 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(); |
Ein kleines Modul dient als Pauker
Das Skript »jap.pl« selbst tut nichts, außer die Funktion »shell()« im Modul »Jap.pm« aufzurufen (Listing 2). Diese Funktion implementiert die Kernfunktion des Paukers. Das Skript sucht nicht nur in den Standardpfaden nach seinem Modul, sondern wegen der Anweisung »use libs« auch in »/etc/scripts«. Dorthin installieren wir »Jap.pm« später.
Die Funktion »shell()« in dem Paket »Jap« (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.
Als Nächstes gibt die Funktion einen Gruß aus (»Welcome«) und nimmt Benutzereingaben über die Standardeingabe (»STDIN«) entgegen. Die Ausgaben landen wie üblich auf der Standardausgabe (»STDOUT«). Tippt der Benutzer »exit« ein, dann bedankt sich das Skript (»Thanks«) und beendet sich.
Zeile 15 definiert die gültigen Jap-Kommandos im Hash »%CMDS«. Durch den »map«-Aufruf speichert sie zu jedem Eintrag eine Referenz auf eine gleich lautende Funktion. So ist der Key »help« mit einer Referenz auf die Funktion »help()« verbandelt.
Existiert zu einem eingegebenen Kommando eine Funktion, dann ruft Zeile 34 diese Prozedur auf und übergibt ihr auch eventuell ans Kommando angehängte Parameter zusammengefasst als einen einzigen String.
Listing 2: »Jap.pm« |
01 #!/usr/bin/perl
02 #############################################
03 # Jap.pm - Mike Schilli (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 new words)
50 QUIZ (run a quiz)
51 EXIT (exit)
52 EOT
53 }
54
55 #############################################
56 sub add { # Wörter 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; # Space am Anfang
66 $jap =~ s/^s+|s+$//g; # und Ende raus
67 $DB{$ger} = $jap; # => in die DB
68 }
69
70 #############################################
71 sub quiz { # Japanisch pauken
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>; # Eingabe
79 last unless $in =~ /^s*$/; # Ende?
80 print "$DB{$key}nn"; # Lösung
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;â
|
Datei als Datenbank
Als Datenbank nimmt unser Modul mit »DB_File« die Berkeley-DB: Die Daten landen dabei in einfachen Files, es ist kein eigener Datenbankserver erforderlich. Das Modul verbindet die Datenbank über »tie« (Zeile 23) mit dem Hash »%DB«. »untie« in Zeile 40 synchronisiert sie am Ende wieder mit dem Hash. Bricht man das Programm 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 alle Einträge des Hashs. Sie legt die Schlüssel in einem Array »@keys« ab und nimmt immer wieder mit »splice()« einen einzelnen, zufällig ausgewählten Eintrag heraus. Splice entfernt diesen Eintrag gleichzeitig aus dem Array.
Die Lösung reicht der Pauker jeweils nach, sobald der Benutzer einen leeren String eingibt, also nur die Eingabetaste drückt. Steht noch etwas anderes dabei, bricht Zeile 79 das Quiz vorzeitig ab.
Vom einfachen Skript zur Client-Server-Applikation
Soweit, so einfach. Doch wie wird aus diesem recht normalen Skript ein netzwerkfähiger Server? Drei Möglichkeiten stehen zur Auswahl:
- Das Skript kommuniziert nicht mehr über »STDIN« und »STDOUT«, sondern nutzt Netzwerk-Sockets zum Lesen und Schreiben. Die Perl-Funktionen »bind()« und »accept()« erlauben es dem Server, Verbindungen von Clients anzunehmen, deren Wünsche entgegenzunehmen und Antworten zurückzuschicken. Die Ein- und Ausgabe läuft dabei über Sockets, die wie Datei-Handles aussehen.
- Das Skript verwendet weiterhin »STDIN« und »STDOUT« und der Internet-Superserver »inetd« übernimmt die Netzwerkfunktionen. Dieser Daemon läuft seit dem Systemstart im Hintergrund und lauscht auf verschiedenen Ports. Über seine Konfiguration erfährt er den für unser Skript zuständigen Port, leitet eingehende Requests an das Skript weiter und sendet dessen 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 das Modul auf einem ausgewählten Port auf eingehende Requests, leitet deren ankommende Daten an »STDIN« weiter und schickt die »STDOUT«-Ausgaben wieder durch die Leitung zurück an den Client.
Die erste Methode verlangt viel Handarbeit (siehe beispielsweise[3]). Die anderen beiden Varianten abstrahieren viele technische Details und sind daher sehr gut geeignet, um schnell eigene Client-Server-Applikationen auf die Beine zu stellen. Beginnen wir mit dem Superserver.
Der Super-Daemon Inetd
Schon bei den ersten Unix-Systemen vor etwa 25 Jahren stellte sich immer wieder die Aufgabe, aus einem einfachen Programm einen netzwerkfähigen Server zu basteln. Um diese Aufgabe zu vereinfachen entstand »inetd«, der Internet-Super-Daemon. Dieser Server fährt üblicherweise zum Zeitpunkt des Systemstarts hoch und lauscht gemäß den Einträgen in den Konfigurationsdateien »/etc/services« und »/etc/inetd.conf« auf vielen Ports gleichzeitig.
Kommt auf einem seiner Ports eine Anfrage an, dann leitet der Daemon diesen Request an das in »inetd.conf« zugeordnete Programm oder Skript weiter. Die Ausgaben des Skripts reicht Inetd entsprechend wieder an den Client durch. Jedes Skript, das aus »STDIN« liest und nach »STDOUT« schreibt, wird so flugs zum Server. Die Datei »/etc/services« gibt der Portnummer einen verständlichen Namen, hier »jap«:
jap 9000/tcp # Der Japanisch-Server
Die Datei »/etc/inetd.conf« sagt dann, dass für diesen Port das Programm »/etc/scripts/jap.pl« zuständig ist:
jap stream tcp nowait nobody /etc/scripts/jap.pl
Änderungen an diesen beiden Dateien erfordern Root-Rechte. »9000« ist die Portnummer, auf der der neue Service lauscht, »tcp« und »stream« bezeichnen das verwendete Protokoll. »nowait« bewirkt (im Gegensatz zu »wait«), dass »inetd« für jeden neu andockenden Client einen neuen Prozess startet, also verschiedene Clients gleichzeitig bedienen kann. »nobody« ist der Benutzer, in dessen Namen das Skript abläuft. Dieser User braucht Schreibrechte für die Datenbank, in unserem Beispiel die Datei »/etc/scripts/jap.dat«.
Sind »jap« und »Jap.pm« im Verzeichnis »/etc/scripts« installiert, ist nur noch der Inetd zu reinitialisieren. Das geht sogar, ohne ihn neu zu starten: Die Shell-Anweisung »ps -ef | grep inetd« sucht nach seiner Prozessnummer (PID) und das »kill«-Kommando schickt ihm dann ein »HUP«-Signal: »kill -HUP PID«
Der alternative Superserver Xinetd
Der bekannte Inetd ist aber nicht der einzige Internet-Superserver. Mit einer einfacheren Konfiguration (besser lesbare Syntax) um mit besseren Sicherheitsmechansimen ausgestattet (Zugriffsschutz anhand der IP-Adressen) bietet sich der neue »xinetd« an (beispielsweise bei Red Hat 7.1). Statt des Eintrags in »/etc/inetd.conf« ist hier die Datei »/etc/xinetd.d/jap« anzulegen mit folgendem Inhalt:
service jap
{
socket_type = stream
wait = yes
user = nobody
server = /etc/scripts/jap.pl
disable = no
}
Darüber hinaus kann der Parameter »only_from« einschränken, von welchen Rechnern aus (IP oder Hostnamen) der Service genutzt werden darf[2]. Für »xinetd« heißt das Rekonfigurierungs-Signal USR2: »kill -USR2 PID«.
Das war’s schon: Sobald »inetd« oder »xinetd« sich neu initialisieren, ist unser neuer Server auf Port 9000 verfügbar. Mit »telnet localhost 9000« als Testclient können wir das leicht ausprobieren:
*** Welcome to jap 1.0 *** »dump Mutter - haha Vater - chichi »exit *** Thanks for using jap. Connection closed by foreign host.
Auch ohne die Dienste eines Super-Daemons können wir mit Perl schnell und einfach einen eigenen Server zaubern.
Ohne Hilfe vom großen Bruder
Die haarigen Seiten der Netzwerkprogrammierung erledigt das Modul »NetServer::Generic« von Charlie Stross, das wir wie immer vom CPAN holen:
perl -MCPAN -e'install NetServer::Generic'
Listing 3 zeigt die Implementierung als »japfork.pl«: Einem neu erzeugten Objekt vom Typ »NetServer::Generic« weist die Methode »port()« den Port zu, auf dem es die Client-Anfragen erwarten soll. Die Methode »callback()« nimmt eine Referenz auf eine Funktion entgegen, die der Server für jeden Request aufruft. Diese Funktion kann aus »STDIN« lesen und nach »STDOUT« schreiben, ganz wie beim Ansatz mit »inetd«. Ähnlich wie dort gibt es hier auch die Möglichkeit, parallele Prozesse abzufeuern. Dies stellt die Methode »mode(“forking”)« ein.
Die »allowed()«-Methode nimmt eine Referenz auf ein Array mit erlaubten IP-Adressen oder Hostnamen entgegen, die den Service nutzen dürfen, alle anderen Clients lässt der Server dann prompt abblitzen. Die Methode »run()« startet schließlich den Server – fertig.
Für Portnummern unter 1024 sind Root-Rechte erforderlich, darüber dürfen auch normale User einen Dienst starten. Soll der Server immer laufen, muss er beim Systemstart hochfahren, am einfachsten durch einen Eintrag in einer Datei wie »/etc/rc.d/rc.local« (Red Hat).
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 verhindert 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 dem Kommando »$socket->getline()« die Ergebnisse abholen.
Listing 3: »japfork.pl« |
01 #!/usr/bin/perl
02 ##############################################
03 # calcfork - Mike Schilli (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 $PORTn";
22 $server->run();
|
Ein eigener Client
Statt unsere Datenbank von Hand zu füllen, kontaktiert »japc.pl« den Server und schickt ihm anschließend mit dem »add«-Kommando der Jap-Shell alle Deutsch-Japanisch-Übersetzungen, die im »DATA«-Bereich stehen (Listing 4). Zeile 23 sendet dem Server nach Abschluss der Arbeit dann das »exit«-Kommando, worauf dieser alle gesendeten Einträge in die Datenbank übernimmt – bereit für die nächste Abfragerunde.
Netzwerkprogrammierung ist mit Perl gar nicht so schwer, oder? Wer Appetit auf mehr entwickelt hat, dem sei[3] empfohlen. Ein hervorragendes Buch, vielleicht etwas teuer, aber das kann man beim Essen wieder einsparen.
Listing 4: »japc.pl« |
01 #!/usr/bin/perl
02 ##############################################
03 # japc.pl --Mike Schilli (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("exitn");
24 my $r = $socket->getline();
25 print "$rn";
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.
|
Besten Dank an Holger Wirtz, der wertvolle Anregungen zum Thema und speziell zu »NetServer::Generic« gab. Also: Schreibt fleißig eigene Server! (fjl)
Infos |
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2001/12/Perl] [2] Manualseiten zu Xinetd: »man xinetd« und »man xinetd.conf« [3] Lincoln Stein, “Network Programming with Perl”, Addison-Wesley 2000 |
Der Autor |
|
Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen. Seine Homepage ist [http://perlmeister.com]. |






