Socketprogrammierung
Maik Görgens
- perl
Hallo!
Icher beschäftige mich seit geraumer Zeit (seit gestern ;) damit, ein pop3-Server in Perl zu programmieren. Das funktioniert in Ansätzen auch schon ganz gut (ich kann bereits mit meinem eMail-Prog - The Bat! - eMails abrufen).
Allerdings gibts nun ein Problem:
Wenn ich mit Telnet den Server blockiere kann ich nicht mehr mit einem anderen Programm drauf zugreifen. Es müßte doch aber möglich sein, das der einer eingehenden Verbindung eine Subroutine zuweist, und dann weiter auf eingehende Verbindungen wartet, während der andere Client abgefertigt wird.
Kennt sich da jemand aus?
Viele Grüße
Maik Görgens
P.S.: ich poste mal mein bisheriges Skript hierhin:
#################################
use IO::Socket;
$server = new IO::Socket::INET (
LocalPort => 110,
Listen => $SOMAXCONN,
Proto => 'tcp',
Reuse => 1);
while ($client = $server->accept())
{
select($client);
$|=1;
select(STDOUT);
print $client "+OK\r\n";
$user = <$client>;
print $client "+OK\r\n";
$pass = substr(<$client>,5,length($pass)-2);
if($pass eq "passwort") { print $client "+OK\r\n"; }
else { print $client "+ERR\r\n"; next; }
while(<$client>)
{
$comm = $_;
$comm = substr($comm,0,length($comm)-2);
if($comm =~ m/STAT/) { print $client "+OK 2 500\r\n"; }
elsif($comm =~ m/LIST/) { print $client "+OK\r\n1 50\r\n2 450\r\n.\r\n"; }
elsif($comm =~ m/RETR/) { print $client "+OK\r\nFrom: me@home\r\nTo: you@home\r\nSubject: hi\r\n\r\nhihihi.\n\n\nhihihi\r\n.\r\n"; }
elsif($comm =~ m/DELE/) { print $client "+OK\r\n"; }
elsif($comm =~ m/QUIT/) { print $client "+OK\r\n"; }
}
}
#################################
Hoi,
Allerdings gibts nun ein Problem:
Wenn ich mit Telnet den Server blockiere kann ich nicht mehr mit
einem anderen Programm drauf zugreifen. Es müßte doch aber möglich
sein, das der einer eingehenden Verbindung eine Subroutine
zuweist, und dann weiter auf eingehende Verbindungen wartet,
während der andere Client abgefertigt wird.
Kennt sich da jemand aus?
Dazu gibt es mehrere Ansaetze. Der erste und einfachste ist der
folgende: du forkst mit jeder Verbindung einen neuen Prozess auf. Das
Child beendet sich dann nach einer abgeschlossenen Session und der
Vater-Prozess horcht weiter auf dem Listener-Socket. Das ist allerdings
keine optimale Loesung: fuer jeden Verbindungs-Request muss eine
Prozess-Kopie erstellt werden, was unter Umstaenden sehr lange dauern
kann. Und wenn viele Anfragen gleichzeitig kommen, dann kann es auch
durchaus sein, dass dein Server in die Knie geht, weil zu viele
Prozesse laufen. Ausserdem koennen verschiedene Childs nur sehr
schwer miteinander kommunizieren.
Die zweite Moeglichkeit ist multiplexing I/O. Das ist relativ
kompliziert zu handhaben, aber letztenendes laeuft es darauf hinaus,
dass man einen array of sockets hat, die in der Haupt-Schleife
jedesmal durchlaufen werden und denen bei jedem Schleifendurchlauf
ein Teil eines Buffers gesendet wird. Die Sockets werden vorher
mittels 'fcntl()' in den nonblocking Modus gesetzt, so dass sie
bei Lese- oder Schreibzugriffen nicht blockieren.
Die Methode hat jedoch mehrere Nachteile: sie ist kompliziert zu
implementieren und zu handhaben, und man muss aufpassen, dass man
keinen buffer overflow fabriziert. Dafuer ist sie jedoch, bei
vernuenftiger Programmierung, optimal schnell.
Die dritte Moeglichkeit ist ein threaded server. Threads sind IMHO
eine Weiterentwicklung von 'fork()' und ein threaded server ist
ein logischer Konsens aus fork()-Servern und multiplexing I/O.
Bei einem threaded server wird fuer jede Verbindung ein neuer
Thread aufgemacht. Ein threaded program kann mehrere Dinge scheinbar
parallel ausfuehren (auf Multi-Prozessor-Systemen *geht* das) und
arbeitet asynchron, es verhaelt sich also wie zwei fork()-Prozesse.
Aber die verschiedenen Threads benutzen alle denselben
Speicherbereich, so dass auch keine Kopie angefertigt werden muss,
und auch keine neuen Prozesse erstellt werden muessen. Wenn du mehr
ueber Threads erfahren willst, kann ich dir nur 'Programming with
posix threads' empfehlen, aus der Addison Wesley Professional
Computing Series.
Wenn du mehr ueber Netzwerk-Programmierung lernen willst, kann ich
dir 'Programming UNIX Networks Vol. 1 und 2' von W. Richard Stevens
und 'TCP/IP Illustrated 1, 2, 3' empfehlen.
Gruesse,
CK
use Mosche;
Die zweite Moeglichkeit ist multiplexing I/O. Das ist relativ
kompliziert zu handhaben [...] Dafuer ist sie jedoch, bei
vernuenftiger Programmierung, optimal schnell.
Du willst eigentlich IO::Select anwenden, was genau für diese Zwecke geschrieben wurde.
perldoc IO::Select
liefert (fast) kopierbaren Beispielcode.
use Tschoe qw(Matti);
Hoi,
Die zweite Moeglichkeit ist multiplexing I/O. Das ist relativ
kompliziert zu handhaben [...] Dafuer ist sie jedoch, bei
vernuenftiger Programmierung, optimal schnell.
Du willst eigentlich IO::Select anwenden, was genau für diese Zwecke
geschrieben wurde.
Das denke ich nicht. IMHO ist multiplexing I/O nicht besonders fuer
gutes Software-Design geeignet. perldoc perlthrtut ist viel
interessanter und intuitiver.
Gruesse,
CK
use Mosche;
Du willst eigentlich IO::Select anwenden, was genau für diese Zwecke
geschrieben wurde.
Das denke ich nicht. IMHO ist multiplexing I/O nicht besonders fuer
gutes Software-Design geeignet. perldoc perlthrtut ist viel
interessanter und intuitiver.
Ohne das jetzt in einen Flamewar ausarten zu lassen:
Warum denn Threads. Ich verwende seit längerem IO::Select und es erfüllt (gerade für sehr kurze Verbindungszeiten wie bei POP-Servern (wo für jedes zweite Kommando doch neu angemeldet wird)) seinen Zweck sehr gut.
Was ist beim Gebrauch von IO::Select denn das schlechte Design?
use Tschoe qw(Matti);
Hoi,
Ohne das jetzt in einen Flamewar ausarten zu lassen:
Was ist bei einer Diskussion ein Flamewar?
Warum denn Threads.
Sie sind intuitiv, einfach zu handhaben und erfuellen ihren Zweck sehr
gut.
Ich verwende seit längerem IO::Select und es erfüllt
(gerade für sehr kurze Verbindungszeiten wie bei POP-Servern
(wo für jedes zweite Kommando doch neu angemeldet wird))
seinen Zweck sehr gut.
Ansichtssache. Erstens koennen die Verbindungszeiten auch sehr lang
werden (grosse EMails) und zweitens sind sie eine zu grosse
Fehlerquelle. Man muss mit dem Puffer aufpassen, kann nicht direkt
in die Sockets schreiben, usw. Die Haupt-Schleife kann, wenn man
nicht aufpasst, auf einmal zu einem busy wait werden oder zum
Gegenteil: die Hauptschleife kann zu lange fuer einen Durchlauf
brauchen. All solche Sachen muss man beachten.
Was ist beim Gebrauch von IO::Select denn das schlechte Design?
Ich habe nicht gesagt, dass der Gebrauch von IO::Select automatisch
schlechtes Software-Design bedeutet, sondern ich sagte, dass es
nicht gut geeignet sei dafuer -- diese Methode ist IMHO wenig
intuitiv und kompliziert. Nicht umsonst geht der Apache von dieser
Methode weiter weg.
Aber du hast schon so halb recht. Um einen optimalen Server zu
schreiben, sollte man beides kombinieren: ein Thread handelt mehrere
Requests ab. Die Haupt-Schleife darf einfach nicht zu sehr
ausgelastet sein, gerade bei sehr vielen Verbindungen ist
multiplexing i/o nicht gut geeignet (zu lange Schleifendurchlaeufe,
die response time wird zu gross). Deshalb ist eine Mischung aus
beidem IMHO die wirklich optimale Loesung -- zumindest von der
Performance her gesehen. Ueber das Konzept laesst sich streiten,
das ist Geschmacksache.
Gruesse,
CK
use Mosche;
Wenn ich mit Telnet den Server blockiere kann ich nicht mehr mit einem anderen Programm drauf zugreifen. Es müßte doch aber möglich sein, das der einer eingehenden Verbindung eine Subroutine zuweist, und dann weiter auf eingehende Verbindungen wartet, während der andere Client abgefertigt wird.
Wie an meiner Antwort an CK zu sehen ist, empfehle ich das Modul IO::Select. Damit geht das dann _wirklich_ einfach.
if($comm =~ m/STAT/) { print $client "+OK 2 500\r\n"; }
elsif($comm =~ m/LIST/) { print $client "+OK\r\n1 50\r\n2 450\r\n.\r\n"; }
elsif($comm =~ m/RETR/) { print $client "+OK\r\nFrom: me@home\r\nTo: you@home\r\nSubject: hi\r\n\r\nhihihi.\n\n\nhihihi\r\n.\r\n"; }
elsif($comm =~ m/DELE/) { print $client "+OK\r\n"; }
elsif($comm =~ m/QUIT/) { print $client "+OK\r\n"; }
}
}
Für diesen Codeschnippsel ampfehle ich dir etwas anders zu machen.
Und zwar nimmst du für jedes POP-Kommando eine anonyme Subroutine und packst diese in einen Hash, mit dem Kommando als Schlüssel.
my %POP;
$POP{'STAT'} = sub {
#...
}
$POP{'RETR'} = sub {
#...
}
#...
dann wird nämlich deine if..elsif Abfrage wesentlich kürzer:
if ($POP{$comm}) {
$POP{$comm}->($client); ## ggf. noch weiter Argumente
} else {
# falsches Kommando oder nich nicht implementiert
}
Die POP Kommandos kannst du dann auch ganz einfach in ein Modul packen. Das ist wesentlich einfacher zu erweitern.
Da ich momentan gerade an einem kleinen NNTP-Server arbeite, kann ich dir meinen Code mit dem implementierten IO::Select und dem anonymen Subroutinen für die Pop- (bei mir NNTP-) -Kommandos geben. Schreib mir diesbezüglich ne E-mail.
use Tschoe qw(Matti);