Abbiamo tutto quello che ti server

Tradotto dal tutorial Writing Socket Servers in PHP, la cui versione originale è consultabile qui

Realizzare Server basati sui Socket in PHP

A quale tipo di lettori è destinato
Prerequisiti
Panoramica - Cos'è un server basato sui socket?
•  Tipi di Socket
Funzioni Socket del PHP
•  Creare un Socket in PHP
•  La realizzazione di un server reale
•  Utilizzazioni pratiche
•  Sicurezza
•  Uno alla volta, per carità
Aggiunte e miglioramenti possibili
Notizie sull'autore

A quale tipo di lettori è destinato

Questa lezione è destinata al programmatore PHP interessato ad esplorare l'uso delle funzioni Socket del PHP per creare un Server Internet basata sui Socket.


Prerequisiti

La lezione utilizza:
  • la PHP Sockets library. L'estensione è abilitata a compile time usando l'opzione di configurazione --enable-sockets.
  • la versione CLI (Command Line Interface) del PHP: consente di attivare il socket server dalla linea di comando.

    A partire dal PHP 4.3.0 l'eseguibile CLI viene compilato ed installato di default (ad ogni modo si può installare esplicitamente la versione CLI specificando --enable-cli a compile time).
  • il sistema operativo Linux
Nonostante questa lezione usi Linux, la libreria sockets funziona ugualmente bene negli ambienti Windows e Unix-like.

Sotto Windows, i PHP Sockets possono essere attivati levando il commento alla linea extension=php_sockets.dll nel file di configurazione php.ini


Panoramica - Cos'è un server basato sui socket?

Un server basato sui socket è un servizio assegnato ad una porta particolare che ascolta le richieste in arrivo e fornisce delle risposte ad esse.

I server di posta elettronica (POP3, SMTP) ed i server web sono buoni esempi di server basati sui socket. Un server HTTP (web) ascolta sulla porta 80 le richieste in entrata ed invia al richiedente l'HTML ed altri file (immagini, documenti exc).

I Socket Server normalmente sono continuamente in esecuzione come un service o un daemon.


Tipi di Socket

Quando si invia l'informazione attraverso la Internet, di solito la si spezzetta in pacchetti. Questo ci consente l'invio di file di grandi dimensioni in molti pezzetti più piccoli, che verranno riassemblati successivamente una volta giunti a destinazione.

Esistono due protocolli differenti per spezzettare l'informazione in pacchetti, a seconda del tipo di informazione che si sta inviando e dei requisiti di consegna.
  • TCP (Transmission Control Protocol) – i pacchetti trasmessi sono numerati e vengono poi riassemblati nel giusto ordine dal lato del destinatario per ricostituire l'intero messaggio. TCP normalmente si appoggia su IP (Internet Protocol), perciò si usa il termine TCP/IP.

    TCP garantisce che nessun dato vada perso (se un pacchetto è perduto, verrà ritrasmesso), e quindi è l'ideale per inviare immagini, file ed altre informazioni che devono giungere a destinazione intere ed intatte (come, ad esempio, un tuo messaggio di posta elettronica).
  • UDP (User Datagram Protocol) – questo è un protocollo senza connessione. Come TCP, può essere eseguito appoggiandosi sulle funzionalità del protocollo IP. La differenza è che UDP fornisce pochi servizi per il recupero degli errori di trasmissione e che non è garantito che un particolare pacchetto sarà senz'altro ricevuto dall'altro lato, né in quale ordine i pacchetti saranno ricevuti.

    UDP è particolarmente adatto nella trasmissione di dati in modalità stream come si fa ad esempio per la musica (se perdiamo qualche informazione, non è detto che cercheremo di ottenerla daccapo, perché potrebbe darsi che il pezzo perduto sia già stato riprodotto).
In questa lezione, useremo i socket TCP per essere sicuri che tutti i dati siano ricevuti integri. Gli esempi utilizzati in questa lezione possono essere convertiti con facilità per usare i socket UDP.


Funzioni Socket del PHP


Torna su

Il PHP è ben equipaggiato per gestire i socket al livello più basso. A partire dal PHP3, il PHP ha introdotto la gestione dei socket attraverso l'uso della funzione fsockopen() e di altre funzioni ad essa associate (confronta la sezione Network del manuale PHP su http://www.php.net/network). A partire dal PHP4, la funzionalità socket del PHP è stata assai migliorata con l'introduzione dell'interfaccia a basso livello dei socket stile BSD.

Nota: le funzioni socket in PHP sono tuttora considerate sperimentali e quindi potrebbero cambiare in versioni future del PHP. Il testing mostra che sono abbastanza stabili quando le si utilizza in applicazioni ben scritte.


Creare un Socket in PHP

Creare un socket a basso livello in PHP è molto simile ad usare le funzioni socket del linguaggio C ed alla programmazione dei socket che si fa in Unix. Il manuale su http://www.php.net/sockets descrive a grandi linee le funzioni disponibili e fa anche riferimento alle Unix Socket FAQ, che sono una grande risorsa per la programmazione dei socket (anche se ci vuole un po' di tempo per leggersele tutte).

Iniziamo con un esempio semplice: un socket server che ascolta una connessione sulla porta 9000, accetta una stringa in input, e la restituisce con tutti gli spazi eliminati.

Nota veloce sui numeri delle porte. I numeri di porta sotto 1024 possono essere aperti solo se si è in possesso dei privilegi di root (o dei privilegi di amministratore in Windows NT/2000). Essi sono anche per lo più riservati per servizi rinomati (come HTTP, POP3, SMTP, FTP, ecc). Per facilità d'uso e per la sicurezza, la maggior parte dei servizi programmati dall'utente ascoltano numeri di porta sopra 1024 (il massimo numero di porta disponibile è 65535, perché IPV4 lo esprime in 16 bit).
#!/usr/local/bin/php –q

<?php
// Set time limit to indefinite execution
set_time_limit (0);

// Set the ip and port we will listen on
$address = '192.168.0.100';
$port = 9000;

// Create a TCP Stream socket
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to an address/port
socket_bind($sock, $address, $port) or die('Could not bind to address');
// Start listening for connections
socket_listen($sock);

/* Accept incoming requests and handle them as child processes */
$client = socket_accept($sock);

// Read the input from the client &#8211; 1024 bytes
$input = socket_read($client, 1024);

// Strip all white spaces from input
$output = ereg_replace("[ \t\n\r]","",$input).chr(0);

// Display output back to client
socket_write($client, $output);

// Close the client (child) socket
socket_close($client);

// Close the master sockets
socket_close($sock);
?>

Per poter mandare in esecuzione il programma bisogna assicurarsi che la prima linea #!/usr/local/bin/php –q sia la locazione dell'eseguibile del PHP CLI (o CGI). Potrebbe essere necessario modificarne i permessi per renderlo eseguibile (chmod 755 socket_server.php – dove socket_server.php è il nome del file) e lanciarlo usando ./socket_server.php dalla linea di comando.

Ora osserviamo in dettaglio ciascuna linea:
  • #!/usr/local/bin/php –q Esegue il binario CLI del con la quiet options per evitare che faccia l'output degli headers HTTP.
  • $sock = socket_create(AF_INET, SOCK_STREAM, 0) – Crea il "master" socket. Questo socket non si occuperà di servire i client, ma piuttosto ascolterà le richieste in arrivo e genererà nuovi socket per quei client. Si comporta come il socket "ascoltatore" principale.

    Dal manuale PHP (http://www.php.net/socket_create): AF_INET è il tipo di dominio del protocollo IPV4 – usato perTCP and UDP. SOCK_STREAM fornisce stream di byte in sequenza, affidabili, basati sulla connessione. Il protocollo TCP è fondato su questo tipo di connessione.
Note: per ottenere un socket di tipo UDP, basta sostituire SOCK_STREAM con SOCK_DGRAM.
  • socket_bind($sock, $address, $port) or die('Could not bind to address') – Collega il socket con l'indirizzo IP e la porta specifici.
  • socket_listen($sock) – Ascolta sulla specifica porta le connessioni in ingresso; una volta instaurata la connessione, sarà usata per creare il socket figlio.
  • $client = socket_accept($sock) – Accetta la connessione sul master socket.
  • $input = socket_read($client, 1024) – Legge dal socket accettato, 1024 byte alla volta (oppure finché non si riceve un \r, \n or \0 – a seconda del valore del terzo parametro opzionale – vedi la nota sotto).
Note: Il terzo parametro di socket_read() può essere o PHP_BINARY_READ, che usa la funzione di sistema read() ed è affidabile per leggere dati binari (il tipo di default per PHP >= 4.1.0) oppure PHP_NORMAL_READ in cui la lettura si ferma alla ricezione di \n o \r (default in PHP <= 4.0.6)
  • $output = ereg_replace("[ \t\n\r]","",$input).chr(0) – Rimuove tutti i blank, le tabulazioni e le andate a capo usando una regular expression.
L'unico punto di interesse è chr(0), che rappresenta il carattere null. Questo viene posto in coda alla stringa che sarà inviata indietro al richiedente. Il motivo per cui si termina l'output con il carattere null è che parecchie socket API dei client usano questo carattere per notificare al client la fine della trasmissione (ciò è vero con la Macromedia Flash XMLSocket API).
  • socket_write($client, $output) - Scrive l'output per il client..
  • Infine, socket_close($client) e socket_close($sock) Chiude il client socket ed il master socket.

La realizzazione di un server reale

Ora che conosci i passi fondamentali necessari per metter su un socket ed ascoltare le richieste in arrivo, sei pronto per creare un server "adulto".

Devi notare che nel codice riportato qui sopra il programma viene lanciato una sola volta, attende una connessione in entrata e poi termina. Questo può andar bene per spiegare i passi necessari a creare un server, ma risulta inadeguato per affrontare le situazioni reali. Una volta che il tuo programma è in esecuzione e risponde alle richieste in arrivo, certo non vuoi che esso termini (dato che questo comporterebbe la necessità di farlo ripartire manualmente).

Ci serve quindi un meccanismo per eseguire il programma in modo continuo – per farlo andare in loop.

Possiamo usare un while(true) { /* do something */ } per costringere il programma a rimanere continuamente in esecuzione, finchè non lo mandiamo ad un exit statement esplicito.

Espandiamo l'esempio di prima, aggiungendo le seguenti funzionalità:
  • consentire al programma di rimanere in esecuzione indefinitamente
  • predisporre la gestione della sua terminazione
  • fornire la gestione simultanea di client multipli
#!/usr/local/bin/php –q

<?php
// Set time limit to indefinite execution
set_time_limit (0);

// Set the ip and port we will listen on
$address = '192.168.0.100';
$port = 9000;
$max_clients = 10;

// Array that will hold client information
$clients = Array();

// Create a TCP Stream socket
$sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Bind the socket to an address/port
socket_bind($sock, $address, $port) or die('Could not bind to address');
// Start listening for connections
socket_listen($sock);

// Loop continuously
while (true) {
    
// Setup clients listen socket for reading
    
$read[0] = $sock;
    for (
$i = 0; $i < $max_clients; $i++)
    {
        if (
$client[$i]['sock']  != null)
            
$read[$i + 1] = $client[$i]['sock'] ;
    }
    
// Set up a blocking call to socket_select()
    
$ready = socket_select($read,null,null,null);
    
/* if a new connection is being made add it to the client array */
    
if (in_array($sock, $read)) {
        for (
$i = 0; $i < $max_clients; $i++)
        {
            if (
$client[$i]['sock'] == null) {
                
$client[$i]['sock'] = socket_accept($sock);
                break;
            }
            elseif (
$i == $max_clients - 1)
                print (
"too many clients")
        }
        if (--
$ready <= 0)
            continue;
    }
// end if in_array
    
    // If a client is trying to write - handle it now
    
for ($i = 0; $i < $max_clients; $i++) // for each client
    
{
        if (
in_array($client[$i]['sock'] , $read))
        {
            
$input = socket_read($client[$i]['sock'] , 1024);
            if (
$input == null) {
                
// Zero length string meaning disconnected
                
unset($client[$i]);
            }
            
$n = trim($input);
            if (
$input == 'exit') {
                
// requested disconnect
                
socket_close($client[$i]['sock']);
            } elseif (
$input) {
                
// strip white spaces and write back to user
                
$output = ereg_replace("[ \t\n\r]","",$input).chr(0);
                
socket_write($client[$i]['sock'],$output);
            }
        } else {
            
// Close the socket
            
socket_close($client[$i]['sock']);
            unset(
$client[$i]);
        }
    }
}
// end while
// Close the master sockets
socket_close($sock);
?>

La funzionalità di base è la stessa del primo esempio, con una prestazione aggiuntiva – quando un utente passa la stringa 'exit' il programma termina la connessione con l'utente.

Questo programma è molto simile al primo, fatta eccezione che all'interno del loop abbiamo quattro blocchi fondamentali di codice.

  1. Metter su i socket per la lettura.
  2. Mettersi in ascolto di nuovi client e predisporli compilando l'array $client.
  3. Ascoltare ciò che i client ci scrivono e registrarne l'input.
  4. Gestire l'input del client.
Una nuova funzione che utilizziamo è socket_select($read,null,null,null); questa invoca la system call select() sull'array di socket che le viene fornito ed attende che essi modifichino il loro stato. Questo provocherà il blocco di tutti i socket finché non si verifica un cambiamento di stato, che a questo punto verrà gestito.

Ciliegina sulla torta, potresti trovarti in una situazione in cui ti serve trasmettere qualche informazione a tutti gli altri client connessi (cose che capitano ad esempio in un ambiente di chat molti-a-molti). Lo si può ottenere con il codice seguente:
$output = 'This is my broadcast message'.chr(0);
for (
$j = 0; $j < MAX_CLIENTS; $j++) // for each client
{
    if (
$client[$j]['sock']) {
        
socket_write($client[$j]['sock'], $output);
    }
}


Utilizzazioni pratiche

Ora che conosci gli aspetti fondamentali della creazione di un socket server, l'unico limite sta nella tua immaginazione. Ecco alcune idee:
  • Server di chat (utilizzando un'interfaccia grafica o basata sul testo). Lo si può realizzare per divertimento o per scopi più seri (ad es. per gestire il supporto on line della clientela).
  • Streaming di informazione in tempo reale (notizie, quotazioni di borsa, ecc.)
  • Streaming di dati multimediali (immagini, video e musica)
  • Server di autenticazione
  • Semplici server web, POP3, SMTP ed FTP.
Inoltre la libreria socket può essere utilizzata per creare non solo server, ma anche client.


Sicurezza

Quando si realizzano programmi che saranno accessibili online, bisogna prendere in considerazione l'aspetto della sicurezza. Questo è vero per normali script PHP, così come per programmi che sono continuamente in esecuzione come i socket server.

Ci sono molti aspetti da prendere in considerazione per realizzare una politica di sicurezza completa, dalla programmazione, al controllo degli accessi e tante altre cose.

Quando si pianifica una politica di sicurezza bisogna considerare molti punti. Qui consideriamo soltanto alcuni da cui si può incominciare:
  • Accesso ai file – bisogna porre dei limiti all'accesso ai file. Se il server consente l'accesso ai file (ad es. un web server), assicurati che vengano forniti all'esterno soltanto i files contenuti in una certa cartella. Sarebbe un erroraccio rendere accessibili /etc/passwd o /etc/shadow.

    Si può limitare l'accesso ai file definendo una cartella padre o una cartella root e consentendo al server solamente l'accesso alle sottocartelle. Un'altra buona idea è quella di "disinfettare" i dati inviati dall'utente, rimuovendo da essi caratteri estranei e pericolosi (come il "../" usato per accedere file contenuti in cartelle di un livello superiore).
  • Cadere in piedi – nei casi in cui il server si ferma, dovresti organizzarti perché si fermi in maniera sicura. Cioè, se il tuo server non può più funzionare come desiderato, dovrebbe essere portato in un stato in cui non può causare danni – terminare il programma e bloccare l'esecuzione.
  • Autenticazione – per servizi sensibili, è raccomandato che si usi l'autenticazione come parte delle specifiche di comunicazione. Persino se si usa un'interfaccia personalizzata in flash o in Visual Basic, non è garantito che qualcuno non sia capace di "annusare" la connessione di rete e di decifrare il protocollo che si sta utilizzando (e tutti i dati che si trasferiscono).

    Un ottimo modo di realizzare l'autenticazione è quello di non consentire che avvenga nessuna azione finché l'utente non si è autenticato con successo (nell'esempio di prima questo può essere realizzato settando $client[$i]['authenticated'] = true dopo una autenticazione riuscita).
  • Crittografia – la crittografia è un ottimo metodo per proteggere le informazioni sensibili che passano atttraverso il server. I sistemi di crittografia possono essere utili specialmente se combinati con le procedure suggerite prima (specialmente l'autenticazione su canali criptati). Per fortuna, il PHP possiede un'eccellente libreria crittografica (la Mcrypt library – vedi http://www.php.net/mcrypt per ulteriori dettagli).

Uno alla volta, per carità

Di solito si vuole che ci sia una sola istanza del server in esecuzione in un dato momento. Nell'esempio proposto abbiamo usato socket_bind() or die('Could not bind to address'). Questo significa che se il programma prova a collegare una porta già in uso esso andrà in abort visualizzando un messaggio di errore.

Si può ottenere un controllo più sofisticato del programma usando il programma pidof che è attivabile dalla riga di comando (/sbin/pidof). Questo programma visualizza la lista dei process ID (pid) dei processi in esecuzione. Nell'usare pidof, ricordatevi di attivarlo con l'argomento -x per visualizzare anche i process id degli shell script.


Aggiunte e miglioramenti possibili

  • Aggiungere controllo dei processi e dei thread (si può fare usando PCNTL).
  • Aggiungere funzioni di controllo dei processi (di cui abbiamo parlato prima) – per essere sicuri che soltanto una singola istanza del programma è in esecuzione in un dato momento (usando /sbin/pidof –x program name).
  • Mandare in esecuzione periodicamente un script per controllare che il servizio è in esecuzione. Lo si può fare con un cron job in *nix (digita man crontab nella shell *nix per ulteriori informazioni) o sotto Windows come un task programmato (sotto Pannello di Controllo).
  • L'interfaccia di front-end può essere implementata in C++, VB, Flash (usando gli XMLSockets), Java o qualsiasi software che supporti i socket TCP/IP o UDP.
  • Se il server sarà in esecuzione senza controllo umano, potrebbe essere auspicabile la creazione di una funzione personalizzata di controllo degli errori che li memorizzi su di un file di testo o in un database invece di visualizzarli.

Notizie sull'autore

Ori Staub è un analista di sistemi senior, uno sviluppatore ed un consulente specializzato nelle soluzioni basate sul web. Ha sviluppato molte soluzioni web ed e-commerce and web per farle lavorare in stretta collaborazione con modelli di client gestionale pre-esistenti.

Commenti

Post più popolari