Hin und wieder steht man vor dem Problem, dass man parallel mehrere File- bzw. Socket-Handles lesend und oder schreibend bedienen möchte. Diese Handles können aber möglicherweise nicht alle zu lesenden Daten auf einen Schlag liefern oder alle zu schreibenden auf einmal aufnehmen. In diesen Fällen erweist sich die Kenntnis von select() oft als hilfreich.
Dieser Beitrag erklärt die Funktionsweise der Funktion select() und demonstriert deren praktischen Einsatz anhand von zwei Beispielen.
Zur Vermeidung von Missverständnissen sei darauf hingewiesen, dass es in Perl zwei select()-Funktionen gibt, die jedoch mit Ausnahme ihres Namens nichts gemein haben, vgl. "perldoc -f select
" bzw. <http://www.perldoc.com/perl5.8.0/pod/func/select.html>:
|
Im Folgenden wird ausschließlich von der letztgenannten Variante "select RBITS,WBITS,EBITS,TIMEOUT
" die Rede sein.
Da die Perl-Standard-Dokumentation zu select() neben dem obigen Zitat effektiv nur noch ein kurzes, m. E. nicht sonderlich repräsentatives Beispiel in perlipc(1) bietet, die Funktion aber ein wenig eigen ist und sich in ihrem Umfeld noch mehrere andere ansonsten eher ungebräuchliche Funktionen wie fileno() oder vec() tummeln, halte ich es für angebracht, darüber ein paar Worte in Form dieses Beitrags zu verlieren.
select() wird in Situationen aufgerufen, in denen ein oder mehrere File- und/oder Socket-Handles zu Lesen und/oder Schreiben geöffnet, jedoch nicht alle Handles dazu bereit sind, z. B. weil ein Programm seine Ausgabe erst noch "errechnen" muss oder bei einem TCP-Socket die Daten durchs Internet einige Zeit unterwegs sind. select() wartet dann, bis mindestens eines der Handles zur gewünschten Verwendung bereit ist, und teilt mit, um welche(s) es sich handelt. Optional kann eine Zeitspanne (Timeout) angegeben werden, nach der select() spätestens zurückkehren soll, d. h. auch dann, wenn zwischenzeitlich keine der angegeben Handles bereit wurden.
Die Rest der Arbeit, d. h. das eigentliche Lesen und/oder Schreiben, wird dem Anwender bzw. der Anwenderin überlassen. Daher wird select() meist im Rahmen von Schleifen verwendet.
select() erwartet vier Argumente:
$rbits
$wbits
$ebits
$timeout
Näherer Erläuterungen bedürfen hierbei insbesondere die ersten drei Argumente, die ich bislang etwas salopp als "Liste" bezeichnet habe.
Tatsächlich handelt es sich um Bit-Vektoren; aus Perls Sicht sind das im Prinzip Strings.
Dabei macht man es sich zunutze, dass intern jedem Handle ein File-Deskriptor mit eindeutigen Nummer zugeordnet ist; normalerweise entspricht 0 STDIN
, 1 STDOUT
und 2 STDERR
; die weiteren Nummern werden an die vom Programm geöffneten Handles vergeben.
Die Nummer eines bestimmten Handles lässt sich mittels der Funktion fileno() ermitteln, z. B. "fileno(STDIN)
", "fileno($filehandle)
".
Um select() nun mitzuteilen, dass man von Handle Nummer n lesen möchte, setze man das n-te Bit des Vektors $rbits
.
Dazu wiederum bietet sich die Funktion vec() an: vec($rbits, $n, 1) = 1;
Möchte man von gar keinen Handles lesen (sondern z. B. nur auf welche schreiben), gibt man für $rbits
einfach undef
oder einen leeren String an; Gleiches gilt freilich für die anderen beiden Vektoren.
$timeout
wird in Sekunden angegeben.
Anzumerken ist dabei, dass auch Sekundenbruchteile möglich sind.
Zusammen mit dem oben Geschriebenen ergibt sich so ein netter Nebennutzen: Man kann select() auch gut als Ersatz für sleep() missbrauchen, wenn man die Ausführung eines Scripts vorübergehend anhalten möchte, ganze Sekunden aber zu ungenau wären: select(undef, undef, undef, 0.42);
Möchte man keinen Timeout setzen, kann auch hier undef
angegeben werden.
Die Art und Weise, in der select() mitteilt, was nun Sache ist, steht der Aufrufsyntax in puncto Gewöhnungsbedürftigkeit keineswegs nach.
Zuerst wären da einmal die (potenziell) beiden Rückgabewerte:
$nfound
$timeout
zurückgekehrt ist)$!
finden)$timeleft
$timeout
verbleibende Zeit an.Wird select() in einem skalaren Kontext aufgerufen, wird nur $nfound
zurückgeliefert.
So weit, so gut. Die im Regelfall jedoch interessanteste Information, nämlich welche Handles denn nun zum Lesen bzw. Schreiben bereit sind, enthalten diese Rückgabewerte offensichtlich nicht.
Des Rätsels Lösung:
Diese Information ist in den übergebenen Bit-Vektoren enthalten, die von select() nämlich dreisterweise verändert werden.
Genauer gesagt bleiben nach Durchlaufen der Funktion in jedem Vektor nur jeweils die Bits gesetzt, die zu den lese- ($rbits
) bzw. schreibbereiten ($wbits
) Handles gehören.
Also gilt es erneut, diese Vektoren mittels vec() zu analysieren, doch dazu mehr im Rahmen des (ersten) Anwendungsbeispiels.
Der select()-Aufruf wird also üblicherweise in folgenden Kontext eingebettet sein:
$nfound
:
< 0
$!
== 0
> 0
Da all das auch bei ausführlicher Erläuterung noch recht abstrakt sein dürfte, einige nicht ganz unwichtige Details bislang unterschlagen wurden und der tiefere Sinn des Ganzen sich erst im Kontext erschließt, wird es nun höchste Zeit für das bereits mehrfach erwähnte
Ein Script soll Daten von Standardeingabe lesen und auf Standardausgabe ausgeben.
Falls nötig, d. h. falls die Standardausgabe die Daten nicht so schnell entgegennehmen kann wie sie eingeliefert werden, soll bis zu einem Megabyte Daten zwischengepuffert werden.
Können über einen Zeitraum von einer Minute weder Daten gelesen noch geschrieben werden, soll das Script abbrechen.
Auf STDERR
sollen Debug-Meldungen ausgegeben werden.
Zunächst einige allgemeine Definitionen und Hilfsroutinen, die (hoffentlich) keiner weiteren Erläuterung bedürfen:
01| #!/usr/bin/perl -Tw 02| 03| use 5.006; 04| use strict; 05| use warnings; 06| 07| use Errno 'EINTR'; 08| use IO::Handle (); # stellt ->blocking (vgl. Z. 26) zur Verfuegung 09| 10| use constant BUFFSIZE => 2**20; # 1 MB 11| use constant TIMEOUT => 60; # 60 Sekunden = 1 Minute 12| 13| sub bytes($) { 14| my $bytes = shift; 15| "$bytes byte" . ( $bytes != 1 && 's' ); 16| } 17| 18| sub print_status($$$) { 19| my ( $mode, $bytes, $bufflength ) = @_; 20| printf STDERR " \u%s %s; buffer now contains %s (%.1f %% full).\n", 21| $mode, 22| bytes $bytes, 23| bytes $bufflength, 24| 100 * $bufflength / BUFFSIZE; 25| } 26| defined $_->blocking(0) or die "Cannot set $_ to non-blocking mode: $!\n" 27| for \*STDIN, \*STDOUT; # Erlaeuterungen folgen 28| 29| my $buffer = ''; # unser Zwischenspeicher 30| my $done_reading; # Flag, ob es auf STDIN noch was zu lesen gibt 31| my $Read = my $Written = 0; # Counter fuer die Statistik
Damit sind alle notwendigen Vorbereitungen getroffen, und wir kommen zur Haupt-Schleife des Programms:
33| while ( !$done_reading || length $buffer ) { 34| 35| my $rbits = my $wbits = ''; 36| vec( $rbits, fileno STDIN , 1 ) = 1 37| unless $done_reading || length $buffer == BUFFSIZE; 38| vec( $wbits, fileno STDOUT, 1 ) = 1 if length $buffer;
Diese soll grundsätzlich durchlaufen werden, solange es auf STDIN etwas zu lesen gibt oder sich noch nicht ausgegebene Daten im Puffer befinden (Zeile 33).
In Zeilen 35 bis 38 werden die Bit-Vektoren für select() vorbereitet:
$rbits
; hier wird das Bit für STDIN
gesetzt, sofern das Lesen nicht bereits abgeschlossen wurde (Details dazu weiter unten) oder der Puffer gerade voll ist.STDOUT
in $wbits
zum Schreiben vor, sofern der Puffer Daten enthält.Nun ist die Zeit für den lang ersehnten select()-Aufruf (Zeile 48) gekommen:
40| print STDERR "\nselect(" 41| . join ( ',', $rbits ? 'R' : "''", 42| $wbits ? 'W' : "''", 43| 'undef', 44| TIMEOUT 45| ) 46| . ')...'; 47| 48| if ( ( my $nfound = select $rbits, $wbits, undef, TIMEOUT ) < 0 ) { 49| if ( $! == EINTR ) { print STDERR " got signal.\n" } 50| else { die "select(): $!\n" } 51| } 52| 53| elsif ( !$nfound ) { 54| print STDERR " timed out.\n"; 55| last; 56| } 57| 58| else { 59| print STDERR " returned $nfound:\n";
Anhand des Rückgabewertes $nfound
sind drei Fälle zu unterscheiden:
$nfound < 0
(Zeilen 48 bis 51):$!
enthaltene Fehlercode der aus Errno.pm importierten Konstante EINTR
entspricht; falls ja, besteht kein Handlungsbedarf.$nfound == 0
(Zeilen 53 bis 56):TIMEOUT
angegebene Zeitspanne, ohne dass STDIN
oder STDOUT
bereit geworden wären; die Schleife wird abgebrochen.$nfound > 0
(Zeilen 58 ff.)STDIN
und/oder STDOUT
sind bereit; Zeit, sich das genauer anzusehen:60| if ( vec $wbits, fileno STDOUT, 1 ) { # STDOUT bereit? 61| unless ( defined( my $written = syswrite STDOUT, $buffer ) ) 62| { 63| die "syswrite(): $!\n"; 64| } 65| elsif ( !$written ) { die " Right side has closed its STDIN.\n" } 66| else { 67| substr( $buffer, 0, $written ) = ''; 68| $Written += $written; 69| print_status wrote => $written, length $buffer; 70| } 71| }
In Zeile 60 wird vec() mit den gleichen Argumenten aufgerufen, mit denen in Zeile 38 das Bit für STDOUT
in diesem Vektor gesetzt wurde, nur dieses Mal, ohne einen Wert zuzuweisen.
Somit liefert die Funktion (nur dann) einen wahren Wert zurück, wenn dieses Bit immer noch gesetzt ist, STDOUT
also bereit ist, Daten aufzunehmen, so dass die weiteren Befehle in diesem if
-Block zum Tragen kommen:
Um die Daten zu senden, kommt in Zeilen 63 bis 65 syswrite() zur Anwendung.
Dabei zwar versucht, alle in $buffer
enthaltenen Daten zu schreiben, durch Schalten von STDOUT
in den nicht-blockierenden Modus in Zeilen 26 f. ist aber sichergestellt, dass die Funktion in jedem Fall sofort zurückkehrt und ggf. nur einen Teil der Daten sendet.
Anhand des Rückgabewertes in $written
sind dabei wiederum drei Fälle zu differenzieren:
undef
(Zeilen 62 bis 64)0
(Zeile 65)> 0
(Zeilen 66 bis 70)$written
enthält die Anzahl der geschriebenen Bytes, die daraufhin in Zeile 67 aus dem Puffer entfernt werden.Nun zum lesenden Teil:
73| if ( vec $rbits, fileno STDIN, 1 ) { # STDIN bereit? 74| unless ( 75| defined( 76| my $read = sysread STDIN, 77| $buffer, 78| BUFFSIZE - length $buffer, 79| length $buffer 80| ) 81| ) 82| { 83| die "sysread(): $!\n"; 84| } 85| elsif ( !$read ) { 86| print STDERR " Left side has closed its STDOUT.\n"; 87| $done_reading = 1; 88| } 89| else { 90| $Read += $read; 91| print_status read => $read, length $buffer; 92| } 93| }
Wie man sieht, weist der Code große strukturelle Ähnlichkeiten zum schreibenden auf, nur dass an Stelle von syswrite() sysread() zum Einsatz kommt und hierbei die maximal zu lesende Datenmenge in Zeile 78 auf den noch freien Teil des Puffers beschränkt sowie durch das Offset in Zeile 79 sichergestellt wird, dass die Daten an den Puffer angehängt werden. Wiederum wird durch den nicht-blockierenden Modus sichergestellt, dass die Funktion sofort und damit ggf. auch nach weniger gelesenen Daten zurückkehrt.
Sofern der Prozess, von dem das Script gelesen hat, seine Ausgabe beendet und sysread() entsprechend 0 zurückgegeben hat (Zeile 85), wird in Zeile 87 das Flag $done_reading
gesetzt.
Das war's:
94| 95| } 96| } 97| 98| printf "\nDone: read %s, wrote %s.\n", bytes $Read, bytes $Written;
Eine der größten Herausforderungen bei der Verwendung von select() innerhalb einer Schleife und mit mehreren gleichzeitigen Ein-/Ausgabekanälen besteht oft darin, sich über die einzelnen Durchläufe dieser Schleife hinweg und für jeden Kanal einzeln den aktuellen Status zu merken und entsprechend unterschiedliche Aktionen auszuführen. Dies soll im Folgenden anhand eines weiteren, komplexeren Anwendungsfalles demonstriert werden, der außerdem einige Kniffe im Zusammenhang mit TCP-Verbindungen beschreibt. (Beim im Folgenden besprochenen Script handelt es sich um eine vereinfachte Version des unter <https://www.noris.net/cgi-bin/whois> installierten.)
Ein Script soll nähere Informationen (Inhaber(innen)daten etc.) zu einem Internet-Domain-Namen, z. B. checkts.net, ermitteln. Als Quellen der Weisheit stehen mehrere whois-Server zur Verfügung, die alle befragt werden sollen, da sie möglicherweise unterschiedliche Detailinformationen beisteuern können. Um den Benutzer oder die Benutzerin des Scripts nicht unnötig warten zu lassen, sollen die Anfragen dabei parallel durchgeführt werden. Da es weiterhin vorkommen kann, dass ein beteiligter Server gar nicht bzw. nur sehr schleppend antwortet, soll der Versuch nach maximal 42 Sekunden abgebrochen werden.
Exkurs: whois-AbfragenDie Durchführung von whois-Abfragen ist recht simpel (vgl. auch RFC 954); hier ein Rezept:
|
Erstmal ein paar grundsätzliche Dinge:
001| #!/usr/bin/perl -Tw 002| 003| use 5.006; 004| use strict; 005| use warnings; 006| 007| use Errno; 008| use Fcntl; 009| use FindBin; 010| use IO::Handle; 011| use Socket; 012| 013| my ( $domain, @whoisserver ) = @ARGV; 014| 015| my $timeout = $^T + 42; # Timeout 42 Sekunden nach Scriptbeginn
Zentrale Datenstruktur sei ein Hash of Hashes mit einem Element pro abzufragenden whois-Server; Schlüssel sei dabei der Servername (FQDN):
016| my %whoisserver = map +( $_ => {} ), @whoisserver;
Die einzelnen Unter-Hashes werden nach und nach folgende Elemente bekommen:
sockaddr_in
-Strukturen) dieses whois-Servers im gepackten Format (wie es etwa Socket::inet_aton() liefert)Aus Effizienzgründen werden außerdem zwei eigenständige Hashes %read
und %write
angelegt.
Die Schlüssel dieser Hashes sind ebenfalls die FQDNs der whois-Server, die Werte die Nummer des File-Deskriptors des TCP-Sockets zum jeweiligen Server.
%write
enthält dabei (nur) die Server, bei der unsere whois-Abfrage noch nicht (vollständig) gesendet wurde (Phasen 1 und 2 im obigen Rezept), %read
diejenigen, bei denen die Abfrage bereits gesendet wurde, die Antwort aber noch nicht vollständig eingetroffen ist.
Server, bei denen die Abfrage bereits vollständig abgeschlossen ist, sind in keinem der beiden Hashes enthalten.
Beginnen wir nun mit den Verbindungsaufbauten. Normalerweise könnte der Code dazu etwa folgendermaßen aussehen:
while ( my ( $fqdn, $w ) = each %whoisserver ) { if ( $w->{socket} = new IO::Socket::INET PeerAddr => $fqdn, PeerPort => 'whois' ) { $write{$fqdn} = fileno $w->{socket} } else { $w->{error} .= "Cannot connect: $!\n" } }
Dieses Vorgehen hätte aber einen erheblichen Nachteil: Bereits ein TCP-Verbindungsaufbau via connect() kann (relativ) lange dauern. Um keine Zeit zu verlieren, sollte also nach Möglichkeit nicht gewartet werden, bis dieser abgeschlossen ist, sondern der Verbindungsaufbau nur initiiert und anschließend gleich zum nächsten übergegangen werden.
Dazu muss das Socket vor dem connect() mittels fcntl() auf (die aus Fcntl.pm importierte Konstante) O_NDELAY
gesetzt werden.
connect()
kehrt dann sofort mit Status false zurück, und $!
meldet ein EINPROGRESS
(aka "Operation in progress"); diese Konstante kann aus Errno.pm importiert werden.
Leider bietet IO::Socket::INET meines Wissens keine Möglichkeit, diesen Mechanismus zu nutzen; wir werden das oben Geschilderte sowie all die Dinge, die dieses Modul ansonsten freundlicherweise automagisch tun würde, daher wohl oder übel zu Fuß erledigen müssen:
017| { 018| my %write; 019| { 020| defined( my $proto = getprotobyname 'tcp' ) 021| or die "Cannot get protocol number for TCP.\n"; 022| 023| defined( my $port = getservbyname 'whois', 'tcp' ) 024| or die "Cannot get port number for whois (TCP).\n"; 025| 026| while ( my ( $fqdn, $w ) = each %whoisserver ) { 027| 028| socket $w->{socket}, PF_INET, SOCK_STREAM, $proto 029| or die "socket(PF_INET, SOCK_STREAM, 'tcp'): $!\n"; 030| 031| fcntl $w->{socket}, 032| F_SETFL, 033| ( fcntl $w->{socket}, F_GETFL, 0 034| or die "Cannot get flags for socket: $!\n" ) 035| | O_NDELAY | O_NONBLOCK 036| or die "Cannot set socket to O_NDELAY and O_NONBLOCK: $!\n"; 037| 038| { 039| ( undef, undef, undef, undef, my @addrs ) = 040| gethostbyname $fqdn; 041| @{ $w->{addrs} } = map scalar sockaddr_in( $port, $_ ), 042| @addrs; 043| } 044| 045| while ( defined( my $peer = shift @{ $w->{addrs} } ) ) { 046| if ( connect( $w->{socket}, $peer ) || $!{EINPROGRESS} ) 047| { 048| $write{$fqdn} = fileno $w->{socket}; 049| last; 050| } 051| $w->{error} .= "connect(): $!\n"; 052| } 053| 054| } 055| }
In Zeilen 31 bis 36 wird hier zunächst das erzeugte TCP-Socket auf O_NDELAY
(vgl. oben) und außerdem O_NONBLOCK
(mehr dazu später) gesetzt.
Das etwas längere Konstrukt verbindet dabei das Auslesen der bisherigen Einstellungen des Sockets (Zeilen 33 f.) mit dem Setzen der zusätzlichen Flags, die dazu mit dem ermittelten Wert bitweise oder-verknüpft werden müssen.
In Zeilen 38 bis 43 werden dann alle im DNS eingetragenen IP-Adressen des jeweiligen whois-Servers ermittelt und mittels der aus Socket.pm importierten Funktion sockaddr_in() für connect() mundgerecht vorbereitet.
In Zeilen 45 bis 52 werden schließlich diese IP-Adressen durchprobiert, bis zu einer entweder ein connect() direkt klappt (was aufgrund des O_NDELAY
-Modus eigentlich nicht sein kann) oder zumindest mit Status EINPROGRESS
(vgl. oben) "fehlschlägt".
Sobald dies der Fall ist, wird die Deskriptoren-Nummer des Sockets in %write
vermerkt.
Damit sind die Vorbereitungsarbeiten abgeschlossen, und wir kommen zum eigentlich interessanten Teil, der select() (enthaltenden) Schleife:
056| my %read; 057| 058| while ( keys %write || keys %read ) { 059| 060| my $rbits = my $wbits = ''; 061| vec( $rbits, $_, 1 ) = 1 for values %read; 062| vec( $wbits, $_, 1 ) = 1 for values %write; 063| 064| unless ( 065| my $nfound = select $rbits, $wbits, undef, $timeout - time 066| ) { 067| $whoisserver{$_}{error} .= 068| 'Timeout before receiving any data.' for keys %write; 069| $whoisserver{$_}{error} .= 070| 'Timeout while receiving data.' for keys %read; 071| last; 072| } 073| elsif ( $nfound < 0 && !$!{EINTR} ) { die "select(): $!\n" }
Zunächst werden in Zeilen 60 bis 62 die beiden Bit-Vektoren für select() vorbereitet.
In Zeile 65 schließlich ist es endlich soweit: select() kann aufgerufen werden.
Beim ersten Durchlauf der while-Schleife wird %read
noch leer, d. h. $rbits
entsprechend ein leerer String sein, so dass zunächst nur auf Sockets in $wbits
gewartet wird, deren connect()s abgeschlossen sind.
Da select() als Timeout eine relative Zeitangabe erwartet, muss vom in $timeout
enthaltenen absoluten Zeitpunkt (vgl. weiter oben Zeile 15) die aktuelle Systemzeit (ermittelt durch einen Aufruf von time()) abgezogen werden.
Liefert der select()-Aufruf in $nfound
nun 0 zurück, kam es tatsächlich zu einem Timeout, der in Zeilen 67 bis 71 behandelt wird; die umgebende while-Schleife wird dabei durch das last
in Zeile 71 verlassen.
Andernfalls wird in Zeile 73 geprüft, ob select() einen Fehler in Form eines negativen Werts zurückgeliefert hat; falls ja, wird die Ausführung des Scripts hier abgebrochen.
Eine Ausnahme hiervon sollte gemacht werden, wenn select() nur aufgrund eines für diesen Prozess eintreffenden System-Signals unterbrochen wurde; in diesem Falle wäre $!{EINTR}
(vgl. Errno.pm) gesetzt.
In allen anderen Fällen können wir uns nun mit der eigentlichen Kommunikation via TCP beschäftigen.
Betrachten wir dabei zunächst den sendenden Teil, also dem, der in Phasen eins und zwei des obigen whois-Kochrezepts zum Zuge kommt:
074| WRITE: for ( keys %write ) { 075| 076| next unless vec $wbits, $write{$_}, 1; 077| 078| unless ( defined $whoisserver{$_}{write} ) 079| { # noch Phase 1: TCP-Verbindungsaufbau 080| unless ( defined( getpeername $whoisserver{$_}{socket} ) ) 081| { # Fehler beim TCP-Verbindungsaufbau 082| $whoisserver{$_}{error} .= "connect(): $!\n"; 083| while ( 084| defined( 085| my $peer = shift @{ $whoisserver{$_}{addrs} } 086| ) 087| ) 088| { 089| next WRITE 090| if connect $whoisserver{$_}{socket}, $peer 091| or $!{EINPROGRESS}; 092| $whoisserver{$_}{error} .= "connect(): $!\n"; 093| } 094| delete $whoisserver{$_}{socket}; 095| delete $write{$_}; 096| next WRITE; 097| } 098| else { # TCP-Verbindungsaufbau war erfolgreich 099| $whoisserver{$_}{write} = $domain . $Socket::CRLF 100| } 101| } 102| 103| unless ( 104| defined( 105| my $sent = send $whoisserver{$_}{socket}, 106| $whoisserver{$_}{write}, 107| 0 108| ) 109| ) 110| { 111| die "Error send()ing data to $_: $!\n"; 112| } 113| elsif ( not $sent ) { die "Sent 0 bytes to $_!?\n" } 114| else { 115| substr( $whoisserver{$_}{write}, 0, $sent ) = ''; 116| $read{$_} = delete $write{$_} 117| unless length $whoisserver{$_}{write}; 118| } 119| }
In der umschließenden while-Schleife (Zeilen 74 bis 119) durchlaufen wir hier all diejenigen Sockets, die oben in $wbits
vorkamen.
Zunächst wird nun in Zeile 76 geprüft, ob beim jeweiligen Socket tatsächlich eine Statusänderung eingetreten, also das korrespondierende Bit in $wbits
(jetzt) nach dem select()-Aufruf immer noch gesetzt ist.
Ist dies nicht der Fall, gibt es insofern nichts zu tun, und der Rest der Schleife kann übersprungen werden.
Sodann muss unterschieden werden, ob wir uns beim zu behandelnden Socket noch in Phase 1 (Verbindungsaufbau) oder bereits in Phase 2 (Anfrage senden) befinden.
Das Kriterium ist dabei, ob das Hash-Element {write}
bereits benutzt wurde (Zeile 78), um darin den Text der zu sendenden whois-Anfrage zwischenzuspeichern; falls nicht, befinden wir uns noch in der Phase des Verbindungsaufbaus, d. h. haben es mit einem Socket zu tun, bei dem sich seit dem vom connect()-Versuch in Zeile 46 (der mutmaßlich mit einem EINPROGRESS
endete) etwas getan haben muss: Entweder er scheiterte, oder es besteht nun eine TCP-Verbindung.
Eine Unterscheidung dieser beiden Fälle ermöglicht uns getpeername() (Zeile 80):
undef
.
Ist der fragliche whois-Server noch unter weiteren IP-Adressen erreichbar, versuchen wir nun in Zeilen 83 bis 93 (nicht anders als oben in Zeilen 45 bis 52) unser Glück; sobald es zu einem EINPROGRESS
kommt, gibt es für dieses Socket erstmal nichts weiter zu tun, und wir können via "next WRITE
" (Zeilen 89 bis 91) zum nächsten Socket übergehen.
Erst wenn alle IP-Adressen erfolglos durchprobiert wurden, geben wir in Zeilen 94 bis 96 für diesen Server endgültig auf.In Zeilen 103 bis 118 erfolgt nun das eigentliche Senden der Daten; aufgrund der next
-Statements in Zeilen 89 und 96 wird dieser Teil nur für Sockets erreicht, die sich bereits in Phase 2 befinden, also grundsätzlich zum Senden von Daten bereit sind.
Das Senden der Daten erledigt die Funktion send() in Zeilen 105 bis 107. Je nach deren Rückgabewert sind nun wiederum drei Fälle zu unterscheiden:
undef
0
%write
gelöscht und entsprechend in %read
eingetragen wird.Nun zum lesenden Teil, der diejenigen Verbindungen behandelt, die sich bereits in Phase 3 befinden:
120| for ( keys %read ) { 121| 122| next unless vec $rbits, $read{$_}, 1; 123| 124| $whoisserver{$_}{data} = '' 125| unless defined $whoisserver{$_}{data}; 126| 127| unless ( 128| defined( 129| my $read = sysread $whoisserver{$_}{socket}, 130| $whoisserver{$_}{data}, 131| 4096, 132| length $whoisserver{$_}{data} 133| ) 134| ) 135| { 136| die "Error read()ing data from $_: $!\n"; 137| } 138| elsif ( not $read ) { 139| close delete $whoisserver{$_}{socket} 140| or push @{ $whoisserver{$_}{error} }, "close(): $!"; 141| delete $read{$_}; 142| } 143| }
Auch hier werden wieder alle Verbindungen geprüft, bei denen wir darauf warten, dass Daten zum Lesen bereit sind (Zeile 120) und dabei diejenigen gleich wieder übersprungen, bei denen dies nicht der Fall ist (Zeile 122).
Die Initialisierung des Lese-Puffers in Zeilen 124 f. dient lediglich zur Vermeidung von Warnungen im Rahmen des folgenden sysread() in Zeilen 129 ff. Bei diesem sind — im Grunde analog zum send() weiter oben — wiederum drei Fälle zu unterscheiden:
undef
{error}
zu vermerken und mit den übrigen Verbindungen fortzufahren, aber das hätte das Beispiel noch weiter verkompliziert.)0
{data}
gelesen wurden und die Verbindung noch nicht "fertig" ist, gibt es hier erstmal nichts weiter zu tun.Schleife zu...
144| } # Ende der in Zeile 58 beginnenden while-Schleife 145| }
... und fertig:
146| print "\n$_ - ", 147| defined $whoisserver{$_}{error} ? "FEHLER:\n$whoisserver{$_}{error}" 148| : "OK:\n$whoisserver{$_}{data}" 149| for sort keys %whoisserver;
STDIN
, andere Pipes, irgendwelche Unix-, TCP- und UDP-Sockets zu lauschen, wenn man das denn möchte.$timeout
verändert; ich konnte allerdings nicht feststellen, dass dies auch die Perl-Funktion täte.$ebits
in der Praxis verwenden möchte, und diesen Punkt daher nicht weiter vertieft.