Maik Görgens: Socketprogrammierung

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"; }
   }
 }

#################################

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

    1. 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);

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

        1. 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);

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

  2. 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);