Chi ha avuto a che fare con un bot di irc si sarà reso sicuramente conto che è lento nello scrivere. Questo perché, sebbene le procedure di calcolo siano estremamente veloci, l'output su di un canale irc è invece estremamente lento. Se si aggiunge il fatto che per evitare di essere kickati per flood occorre attendere prima di ogni messaggio, questo comincia a essere un problema.
Facciamo un esempio:
Due utenti messaggiano un ipotetico bot chiedendogli di stampare l'help. Il buon bottolino reagirà stampando le sue 20 (nemmeno troppe) righe di help per il primo utente, e successivamente le sue 20 righe per il secondo utente. Il buon programmatore per evitare il flood mette uno sleep(1) tra un invio di una riga e l'altro. Quindi per stampare le 20 righe di help ci vorranno 20 secondi (senza contare eventuali altri ritardi del canale irc). Il secondo utente deve quindi aspettare almeno 20 secondi.
Sembra poco, ma se ci fosse un terzo utente? Dovrebbe aspettare 40 secondi! Ma il tempo impiegato dal bot per leggere il file e immagazzinarlo in memoria è stato molto più breve.
Non è un problema nuovo, in realtà da sempre la lentezza delle procedure di output è stata un notevole fastidio. La soluzione nei sistemi operativi è il multitasking. La stampante riceve i suoi dati e stampa mentre il processore può divertirsi a fare qualcos'altro, solitamente qualcosa di estremamente produttivo, tipo giocare a Solitario.
In perl come si può fare? Per il multitasking ci sono i thread, ma sono complicati e a volte non funzionano (a me per esempio). Si possono usare i moduli appositi per IRC, come NET::IRC o POE:IRC, ma è troppo facile :P.
Si può però emulare il multitasking, semplicemente separando il calcolo dall'output.
Normalmente un bot inizia a fare qualcosa quando riceve un messaggio:
while($line = <IRC>) { # fai quel che devi fare }
Quando lo riceve inizia a fare quel che deve fare, stampa l'output, e solo dopo legge la nuova riga.
Ma se noi potessimo modificarlo così:
while(1)
{
# Finché il sole sorge
if($line = <IRC>)
{
# Se c'è qualcosa da leggere leggilo e fai quel che devi fare
}
output(); # Prima di ripetere il controllo stampa un po' delle cose che hai da parte}
}
È chiaro il vantaggio, no? Ad ogni ciclo del while eterno, il bot stampa una o più righe e intanto fa quel che deve fare. Le funzioni non chiameranno più direttamente la funzione di output ma metteranno i propri dati in una struttura dati apposita, globale, che la funzione output andrà a leggere per sapere cosa stampare. Tale e quale al comportamento di un sistema operativo con una stampante.
In linea teorica... Ma nella pratica è un disastro, perché if($line = <IRC>) si blocca ad attendere il dato, invece di passare avanti se non lo trova subito... Questo è il problema più grande, ma... C'è sempre un altro modo per fare le cose...
Il problema più grosso è convincere il bottolino a non aspettare a tempo indefinito i dati, ma di passare oltre se dopo un tot di tempo non sono arrivati. Per fare questo bisogna aprire il file in modalità non bloccante. Il metodo io l'ho trovato qui, in un how-to che spiega come gestire stream multipli: http://snippets.pornosecurity.org/blogs/index.php/perl/2007/03/05/non_blocking_perl_sockets
In pratica si dichiara:
use IO::Select;
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
#### open_socket: open a socket connection with autoflush
sub open_socket
{
my ( $local_addr, $server, $port ) = @_;
socket ( my $sock, PF_INET, SOCK_STREAM, getprotobyname ( 'tcp' ) );
bind ( $sock, sockaddr_in ( '0', inet_aton ( $local_addr )) );
connect( $sock, sockaddr_in ( $port, inet_aton ( $server ) ) );
# autoflushing
select( (select ( $sock ), $|=1)[0] );
return $sock;
}
#### make_nonblock: make a filehandle not blocking
sub make_nonblock
{
my ( sock ) = @_;
# otteniamo le flag correnti associate al filehandle
my $flags = fcntl ( $sock, F_GETFL, 0 );
# aggiungiamo la flag O_NONBLOCK
fcntl ( $sock, F_SETFL, $flags | O_NONBLOCK );
return $sock;
}
Fcntl è il modulo che permette di modificare i permessi ai file. La prima funzione apre un socket, e la seconda lo rende non bloccante. A questo punto nel codice bisognerà inserire le chiamate alle funzioni:
$irc = open_socket ( '0', $server, $port ); make_nonblock ( $irc ); my $select = IO::Select->new (); $select->add ( $irc );
dove $server e $port contengono ovviamente il server e la porta irc cui ci si connette...
while (1)
{
output();
if ( my @ready = $select->can_read (2) )
{
for my $irc ( @ready )
{
my $ret = my $line = '';
my $buf;
# sysread restituisce:
# il numero di bytes letti,
# 0 se alla fine del file,
# undef se ci sono stati errori
while ( defined ($ret = sysread ( $irc, $buf, 512 )) && $ret != 0 )
{
$line = $buf;
}
}
# se $buf e' vuoto il socket e' disconnesso
# oppure c'e' una condizione di errore
if ( $buf eq '' )
{
$select->remove ( $irc );
close ( $irc );
}
# FAI COSE!
}
}
Quasi del tutto uguale a quella proposta dal link citato prima, ma con un while(1) che incapsula il tutto e un if ( my @ready = $select->can_read (2) ) che fa la vera differenza. Il numero passato in argomento alla can_read è infatti il numero di secondi che deve aspettare prima di rinunciare alla lettura dello stream.
Ad ogni ciclo del while infinito viene chiamata la funzione output(). Andiamo a esaminarla:
Mi piace definirla Array di hash di code per far credere che sia una struttura molto complessa, ma in realtà non è così.
È un grosso array, definito come @code, che al suo interno contiene una serie di hash, che contengono il target cui il messaggio è indirizzato, e un array che contiene le righe da inviare a quel target. In questo modo non c'è alcuna possibile ambiguità (si ipotizza che il bot comunichi i risultati in query, ma passando come $target il nome di un canale i dati vengono scritti lì).
Accedere alla struttura dati è semplice:
L'array di code: @code
Un hash all'interno dell'array: $code[$i]
La chiave target: $code[$i]{target}
Un elemento dell'array text: $code[$i]{text}[$j]
#### output: write on the $irc filehandle the buffer content
sub output()
{
my $i = 0;
my ($ncode, $ntext, $target, $line);
$ncode = @code - 1;
for $i (0 .. $ncode)
{
$ntext = @{$code[$i]{text}};
if($ntext <= 0)
{
splice @code, $i, $i;
$i--;
}
else
{
$target = $code[$i]{target};
$line = shift @{$code[$i]{text}};
$line =~ s/\n|\r//;
print "PRIVMSG $target :$line\n";
print $irc "PRIVMSG $target :$line\n";
}
}
return;
}
#### buffer: creates the output buffer
sub buffer()
{
my ($target, @text) = @_;
my (%hash);
$hash{target} = $target;
$hash{text} = \@text;
push @code, \%hash;
}
Partiamo con l'esaminare la funzione output(). Ogni volta che viene chiamata stampa una linea di ogni elemento della coda, e la rimuove dall'array text. Se l'array text è vuoto, rimuove direttamente tutto l'hash corrispondente.
Buffer invece, inserisce semplicemente i dati nella struttura dati.
Quindi una ipotetica funzione che deve stampare dati basta che chiami buffer($target, @text)
That's all, folks!
3ex