Prima di iniziare leggetevi però gli articoli di fabio sulle socket altrimenti potrebbe mancarvi qualcosa:
http://freephp.html.it/articoli/view_articolo.asp?id=76
http://freephp.html.it/articoli/vie...colo.asp?id=112
---
Cosa sono le socket?
Sono quel sistema di trasmissione dei dati usato ad esempio nelle reti o su internet, tramite di esse potete contattare un'altro pc e inviargli o ricevere dei dati che poi possono essere analizzati.
Le socket sono un mondo immenso dove non finirete MAI di immaginare
Potete fare le cose più strane e divertenti, e sopratutto potete sviluppare POTENTI server scritti in PHP! Certo non potranno essere usati per reggere carichi eccessivi, ma vi voglio fare un'esempio:
Immaginate che nella ditta in cui lavorate si stia sviluppando un protocollo di comunicazione compresso, e che essendo in svuluppo deve essere testato per vedere l'efficenza, il consumo di banda, il consumo di processore e altro ancora, immaginate che per farlo avete poco tempo, quindi non potete ad esempio usare come linguaggio il C o il C++ perché entrerebberò in ballo troppi fattori esterni e quindi lo sviluppo del protocollo sarebbe estremamente lento. La soluzione ideale sarebbe un linguaggio di script, perl? python? ma se sapete programmare con php, perché dover usre un'altro linguaggio? ebbene con php è possibile sviluppare i vostri server e sviluppare codice di un certo tipo!
Per quanto riguarda le socket ci occuperemo soltanto delle socket del modulo esterno del php e non del sistema integrato delle socket in php, perché è molto più lento del modulo.
Per chi programma in C\C++ ed ha già usato le socket sotto i sistemi *nix noterà che sono praticamente uguali
Allora, facciamo un'esempio stupido, mettiamo che dovete scaricare un file dal web. Per farlo ci sono 3 modi, uno tramite le funzioni file di php, uno tramite le funzioni interne per le socket di php (che non tratteremo) ed infine tramite le socket del modulo.
Con il primo metodo molto semplicemente:
<?php ?>
andiamo analizzando questo codice:
- La prima riga si occupa di aprire il file test.html, e di crearlo se non esiste, in modalità di scrittura in modalità binaria con il supporto per la conversione del carattere di fine linea
- La seconda linea si occupa di scrivere il file che abbiamo scaricato dal web (ne riparliamo nello specifico in seguito)
- La terza chiude il file
ok, smontiamo la seconda linea nei vari comandi:
* file: questo comando supporta i cosidetti stream, come le funzioni interne di php. Questo vuol dire che posso mettere http, ftp e gli altri stream supportati internamente da php e in automatico mi restituirà il contenuto apposito. Ovvero a livello pratico oltre ad a leggere i file in locale posso leggere i file in remoto. Restituisce un'array dove per ogni elemento vi sta una linea, quindi molto comodo se dovete analizzare linea per linea, ma nel nostro caso a noi ci interessa "implodere" il file in un'unica riga per poterlo scrivere
* implode: si occupa appunto di implodere un'array in un'unica stringa, e come elemento per unire i vari elementi dell'array e il primo parametro di implode, nel nostro caso specifico implode senza nessun carattere di separazione
* fwrite: semplicemente scrive il contenuto della stringa che implode gli ha restituito.
Questo era il primo metodo, ma ora passiamo al secondo metodo, quello più interessante che vi mostrerà MOLTE vie!
<?php // Il file da scaricare $file = 'index.html'; // Impostiamo dove si deve connettere $host = "www.google.it"; // La porta a cui connettersi $port = 80; // Creiamo la socket (Una socket basata su IPv4, Socket Stream [ovvero un sistema di comunicazione con il controllo dell'errore] ed infine SOL_TCP gli specifica di usare il layer di comunicazione TCP // Dice a php di connettersi a HOST:PORT usando la socket creata in precedenza // Prepariamo il contenuto da inviare $header_send = ''; $header_send .= "GET /{$file} HTTP/1.0\n"; $header_send .= "HOST: {$host}:{$port}\n"; $header_send .= "\n"; // Inviamo i dati // Inzializziamo la variabile che conterra' i dati del buffer $buffer = ''; // Legge i dati dalla sockeet a blocchi di 512 byte mettendoli dentro la variabile $tmpdata. Se socket_read restituisce FALSE, vuol dire che la comunciazione si è chiusa // Aggiungo i dati al buffer $buffer .= $tmpdata; } // Il protocollo HTTP 1.0 è composto da due sezioni separate da un duplice invio. La prima sezione sono le informazioni sul file ricevuto, la seconda è il contenuto // Estraggo gli headers // Il contenuto poteva contenere a sua volta dei duplici invii, quindi per evitare di perdere dati o di averli malformati reimplodiamo l'array da cui abbiamo tolto gli headers // A questo punto mettiamo tutto dentro un file e abbiamo scritto il nostro "downloader" ?>
può sembrare complicato ma in realtà non lo è!
Innanzi tutto all'inizio definiamo 3 variabili:
- file
- host
- port
Ovvero il file da scaricare, l'indirizzo ed infine la porta del server web a cui vogliamo connetterci per scaricare il file. Come esempio li ho definito www.google.it, porta 80 e come file index.html, ovviamente potete mettere qualunque cosa, anche file binari (ovvero eseguibili, compressi, immagini e altro ancora)
Fatto questo dobbiamo creare la risorsa per poter gestire le socket! Ovvero dobbiamo dire al php di prepararci una socket che usi IPv4 (ovvero un sistema di comuncazione basato sul riferimento tramite un IP a 4 cifre), che utilizzi un sistema di comunicazione basato su uno STREAM affidabile, ovvero che controlla che i pacchetti inviati giungono a destinazione correttamente e simili ed infine gli diciamo di usare il TCP! Dalla combinazione IPv4, Stream, TCP deriva appunto il nome TCP/IP.
Fatto questo diciamo al sistema di connettersi al server usando la socket appena creata. Noi qui non stiamo eseguendo alcun tipo di controllo ma potrebbe benissimo succedere che il server sià down e quindi vi sia errore, o che ad esempio il sistema ha allocato troppe socket e non ne possa allocare più.
Dopo che php si è connesso al server che ci interessa prepariamo una semplice richiesta da inviare al server web per poter ricevere il file! Per l'esattezza diciamo al server web:
- Che stiamo eseguendo una richiesta in GET (ovvero niente parametri passati tramite form o affini)
- Che vogliamo leggere il file $file
- Che stiamo usanto il protocollo 1.0 (c'è anche 1.1 ma richiede altri parametri che a noi non interessano)
- Ed infine diciamo al server web che tra i siti che hosta, per quanto riguarda il file, si deve riferire all'host (in questo caso) www.google.it - Per quanto riguarda la comprensione di questo vi rimando al manuale di apache sui Virtual Host, comunque, se vogliamo dare una spiegazione veloce e semplice, i virtual host vi permettono di tenere più siti sulla stessa macchina! Il parametro HOST appunto dice al web server quale host utilizzare, e questo viene inviato dai browser ai server web appunto per poter ricevere le corrette informazioni.
Preparati gli header li inviamo al server web usando socket_write. A questa funzione passiamo la socket da usare e i dati da inviare. Come parametro opzionale c'è anche la lunghezza dei dati da inviare, ma se non specificato invia TUTTI i dati contenuti nel buffer.
Dopo che li abbiamo inviati stiamo in attesa di ricevere i dati fin quando la comunicazione non viene chiusa dall'altra parte.
Nel ciclo while leggiamo i dati dalla socket a blocchi di 512 byte per volta e li aggiungiamo al nostro BUFFER.
Dopo aver ricevuto tutti i dati chiudiamo la socket e passiamo all'analisi di questi
La versione 1.0 (la 1.1 è leggermente diversa nella struttura) è formata da 2 parti principali:
- Headers
- Body
La sezione Headers contiene tutte le informazioni sul file che abbiamo ricevuto! I mime-type, la dimensione, i tipi di file autorizzati, se il file esiste oppure è sotto autenticazione o altro ancora, o ancora i cookie ed altro.
La sezione body contiene il vero e proprio file!
Queste sezioni sono divise da un separatore formato da un doppio invio. Nel codice usiamo array_shift che restituisce il primo elemento di un'array e poi lo elimina dall'array stesso e poi utilizziamo implolde! Perché utilizzare implode? se nel file vi fosse un doppio invio succederebbe che con explode leggeremmo dall'inizio fino a quello spezzone, e non va bene ^^ in questo modo ricomponiamo il tutto senza problemi
Alla fine scriviamo il file tramite le normalissime funzioni per la scrittura
Come potete vedere è tutto abbastanza semplice, basta capire il meccanismo base!
Arrivati a questo punto vi consiglio di interrompere la lettura della pillola e di chiarirvi le idee guardando il manuale e i commenti del manuale!
http://www.php.net/socket_create
http://www.php.net/socket_connect
http://www.php.net/socket_read
http://www.php.net/socket_write
http://www.php.net/socket_close
e di passaggio, dato che li abbiamo utilizzati
http://www.php.net/file
http://www.php.net/array_shift
http://www.php.net/implode
http://www.php.net/explode
Ok, fino a questo punto abbiamo spiegato come utilizzare in maniera BASILARE le socket, ma ora passiamo alle cose più serie.
Mettiamo che dovete sviluppare un server per la chat tramite php e telnet.
Cosa deve fare il server?
1° Inizializzarsi
2° Mettersi in ascolto sulla porta 9999 (che sarà quella che useremo per fare il telnet)
3° Attendere qualcuno che si connetta
4° Autenticare chi si connette
5° Introdurre l'utente appena connesso all'interno della chat
quindi in pratica deve gestire:
- L'autenticazione
- La comunicazione
Occupiamoci della prima.
Innanzi tutto non è possibile entrare usando un nickname di qualcuno che già è dentro con quel nick ed inoltre ci sono i nick registrati.
Quindi se l'utente X inserisce il nick Y il server deve chiedere la pass e controllare che sia valida. Per una questione di semplicità l'accoppiata username|password sarà scritta in un file di testo.
Quindi il nostro file conterrà:
daniele_dll|nunlozo gm|sonomod guidoz|guidoz
Come seconda cosa il server deve appunto permettere la chat, quindi ogni volta che l'utente invia un messaggio questo deve essere inviato a tutti gli altri connessi! Ovviamente al messaggio andra anteposto il nick di chi lo invia
Adesso iniziamo a scrivere il codice.
Dobbiamo come prima cosa creare la configurazione del server stesso...quindi
<?php 'host' => '0.0.0.0', 'port' => 9999, 'max_users' => 15, );
Nella configurazione abbiamo inserito l'ip sul quale bindarsi (un computer può avere più ip, ad esempio può avere l'ip di localhost, quindi 127.0.0.1, l'ip della rete interna, quindi, ad esempio, 10.0.0.1, ed infine l'ip su internet, quindi 80.231.34.51). Usando l'ip 0.0.0.0 gli viene detto di accettare le richieste di connessione da tutti gli ip disponibili nella macchina! Se ad esempio volessimo evitare che dall'esterno si possano connettere persone senza voler usare un firewall possiamo semplicemente inserire l'ip di localhost ovvero 127.0.0.1 e potremo connetterci SOLO noi dal nostro pc!
Il secondo parametro contiene la porta alla quale connettersi, nel nostro caso sarà la 9999. Ovviamente su questa porta non devono essere in ascolto altri server senno riceveremo errori!
Inizializziamo alcune variabili
Queste variabili conterranno le informazioni sui client e le informazioni sulle socket, ed un'array aggiuntivo conterrà la lista dei nickname in modo da velocizzare la ricerca dei nickname in uso quando qualcuno si collega. L'ultimo array contiene la lista degli utenti registrati
Il blocco di define servono a definire gli stati di un utente
Adesso il server deve creare la socket e mettersi in ascolto, quindi
// Creiamo la socket // Controlliamo se ha dato errore o meno if ($socket === FALSE) { // Se si il programma muore restituendo l'errore die("Errore durante la creazione della socket!\n" . socket_strerror(socket_last_error()) . "[" . socket_last_error() . "]\n"); } // Impostiamo la socket come non blocking // Impostiamo la socket come riusabile // Bindiamo la socket if ($ris === FALSE) { die("Impossibile impostare la socket!\n" . socket_strerror(socket_last_error()) . "[" . socket_last_error() . "]\n"); } // Mettiamo in ascolto la socket if ($ris === FALSE) { die("Impossibile mettersi in ascolto sulla porta {$_CONFIG['host']}:{$_CONFIG['port']}!\n" . socket_strerror(socket_last_error()) . "[" . socket_last_error() . "]\n"); } // Leggiamo la lista degli utenti registrati e la inseriamo nell'array 'nick' => $nick, 'pass' => $pass ); }
In questa parte di codice creiamo una socket TCP/IP, la impostiamo in modalità NON-BLOCKING, ovvero il programma continuerà a lavorare e i comandi come socket_read e socket_write restituiranno SEMPRE false perché non attenderanno l'esecuzione del comando che potrebbe riuscire come non potrebbe riuscire, però questo ci permette di eseguire altre operazioni nel frattempo senza bloccare il programma e rallentare pesantemente il sistema operativo.
Dopo di ciò impostiamo la socket in modalità REUSABILE, dopo di che diciamo che IP e PORTA deve usare la socket! Fatto ciò possiamo dire di mettersi in ascolto. In questo codice abbiamo inserito un minimo di gestione degli errori.
Creiamo alcune funzioni che gestiranno l'invio dei dati a tutti gli utenti e l'avvertenza di quando uno si disconnette o entra
// Funzione per scrivere nei socket function sock_write(&$socket, $data, $return = TRUE) { // Inizializza la variabile $num = 0; // Attende che il numero di socket in cui si può scrivere è maggiore di zero while ($num > 0) { // Crea un'array dove inserisce la socket in cui scrivere // Avvia il select sulla socket interessata } // Controlla se alla fine della stringa ci sta il \n // Invia i dati } // Funzione per gestire i nuovi utenti function new_user($nick) { global $_CLIENTS_INFO, $_CLIENTS_SOCK; // Cicla tutte le socket foreach($_CLIENTS_SOCK as $socket) { // Controlla se la socket indicata può chattare if ($_CLIENTS_INFO[(int) $socket]['state'] != STATE_CANCHAT) continue; // Invia il messagio sock_write($socket, "User <{$nick}> connected!"); } } // Gestione per la disconnessione di un utente function quit_user(&$sock, $advise = TRUE) { global $_CLIENTS_NICK, $_CLIENTS_INFO, $_CLIENTS_SOCK; // Recupera il nick $nick = $_CLIENTS_NICK[(int) $sock]; // Controlla se deve avvisare if ($advise) { // Cicla l'array delle socket foreach($_CLIENTS_SOCK as $socket) { // Controlla che l'interessato possa chattare if ($_CLIENTS_INFO[(int) $socket]['state'] != STATE_CANCHAT) continue; // Se si invio il messagio sock_write($socket, "User <{$nick}> exit!"); } } // Chiude la socket // Scarica i vari riferimenti // Elimina la socket } // Gestione della chat function chat_user($from_sock, $msg) { global $_CLIENTS_INFO, $_CLIENTS_NICK, $_CLIENTS_SOCK; // Estrae il nick $from_nick = $_CLIENTS_NICK[(int) $from_sock]; // Si cicla l'array delle socket foreach($_CLIENTS_SOCK as $socket) { // Controlla che la socket selezionata non sia quella di provenienza e che possa chattare if ($from_sock == $socket || $_CLIENTS_INFO[(int) $socket]['state'] != STATE_CANCHAT) continue; // Scrive il messagio sock_write($socket, "<{$from_nick}> {$msg}"); } } // Funzione per recuperare il nick dalla socket function getsockfromnick($nick) { global $_CLIENTS_NICK, $_CLIENTS_SOCK; if ($value == $nick) return $_CLIENTS_SOCK[$key]; } return FALSE; } // Funzione per controllare se un utente è registrato o meno function user_is_regged($nick) { global $_REGLIST; return FALSE; } // Funzione per controllare se user e pass corrispondono function check_user($nick, $pass) { global $_REGLIST; if ($_REGLIST[$nick]['nick'] == $nick && $_REGLIST[$nick]['pass'] == $pass) return TRUE; return FALSE; } // Gestisce i messagi privati function privatechat_user($from_sock, $to_nick, $msg) { global $_CLIENTS_INFO, $_CLIENTS_NICK, $_CLIENTS_SOCK; // Estrae la socket dal nick $socket = getsockfromnick($to_nick); // Estrae il nick dalla socket $from_nick = $_CLIENTS_NICK[(int) $from_sock]; // Controlla che il nick esista if ($socket === FALSE) { // Se non esiste avverte che l'utente non c'è sock_write($from_sock, "Unable to find <{$to_nick}>"); } else { // Esiste, quindi controlla che chi deve ricever il messaggio può chattare if ($_CLIENTS_INFO[(int) $socket]['state'] != STATE_CANCHAT) { sock_write($from_sock, "Unable to find <{$to_nick}>"); } // Invia il messagio sock_write($socket, "PRIVATE: <{$from_nick}> {$msg}"); } }
La prima funzione si occupa di scrivere nella socket le stringe che gli inviamo! E' importante quel while perché attende che sia possibile scrivere nella socket, se non lo facessimo potremmo avere errori a causa di grandi quantità di dati inviati alla socket remota.
La seconda funzione si occupa di avvertire tutti gli utenti connesi che è entrato un nuovo utente, mentre la terza che un utente è uscito. Infine la quarta funzione si occupa di inviare i messaggi della chat ai vari utenti
La quinta funzione controlla se un utente è registrato o meno, la sesta controlla la pass di accesso ed infine la settima gestisce i messaggi privati.
Adesso che il server inizializza il tutto dobbiamo metterci in attesa di ricevere le informazioni
// Crea un'array che conterrà la socket del server // Entra in loop infinito while(1) { // Crea l'array delle socket da controllare // Controlla se nelle socket ci sono dati disponibili // Se si if ($num > 0) { // Cicla la lista delle socket con dati disponibili foreach($tmparray as $sock) { // Controlla se la socket selezionata corrisponde alla socket del server if ($sock === $socket) { // Controlla se il numero di utenti // Se si, accetta la socket // Invia un messaggio sock_write($new_sock, "Numero massimo di utenti raggiunto"); // Chiude la socket // Scarica la socket // Continua il ciclo continue; } // Controlla se riesce ad accettare la socket $tmpnum = (int) $new_sock; // Estrae HOST e PORTA di provenienza // Inserisce le informazioni negli array $_CLIENTS_SOCK[$tmpnum] = &$new_sock; 'host' => $host, 'port' => $port, 'key' => $tmpnum, 'buffer' => '', 'state' => STATE_CONNECTED, 'is_reg' => FALSE ); $_CLIENTS_NICK[$tmpnum] = ''; // Invia il messaggio di entrata alla socket sock_write($new_sock, "Nickname: ", FALSE); // Elimina i riferimenti inutili } else { // Errore, quindi muore die("Errore durante l'accettazione di una socket!\n" . socket_strerror(socket_last_error()) . "[" . socket_last_error() . "]\n"); } } else { // Legge i dati dalla socket // Controlla se la connessione è chiusa o ha dato errore if ($buf == FALSE) { // Essendo chiusa fa disconnettere l'utente quit_user($sock); break; } elseif($buf == '') { // Ha dato errore, quindi esce die("Errore durante la ricezione dei dati\n" . socket_strerror(socket_last_error()) . "[" . socket_last_error() . "]\n"); } $tmpnum = (int) $sock; // Aggiunge i dati appena letti al buffer della socket $_CLIENTS_INFO[$tmpnum]['buffer'] .= $buf; // Controlla se l'ultimo carattere è \n // Se si controlla lo stato in cui si trova l'utente switch($_CLIENTS_INFO[$tmpnum]['state']) { case STATE_CONNECTED: // Elimina gli ultimi caratteri inutili // Riceve il nick, controlla il suo contenuto! Non può contenere spazi. sock_write($sock, "Il nickname non può contenere spazi!\nReinserisci il nickname: ", FALSE); break; } // Controlla se il nick non è già in uso sock_write($sock, "Nickname già in uso, scegline un'altro!\nReinserisci il nickname: ", FALSE); break; } // Se tutto va bene imposta il nick $_CLIENTS_NICK[$tmpnum] = $_CLIENTS_INFO[$tmpnum]['buffer']; // Controlla se l'utente è registrato if (user_is_regged($_CLIENTS_NICK[$tmpnum])) { $_CLIENTS_INFO[$tmpnum]['state'] = STATE_WAITPASS; sock_write($sock, "Questo nickname è registrato, invia la password!\nPassword: ", FALSE); break; } // Non e' registrato quindi annunzia del nuovo utente new_user($_CLIENTS_NICK[$tmpnum]); // Imposta lo stato $_CLIENTS_INFO[$tmpnum]['state'] = STATE_CANCHAT; break; case STATE_WAITPASS: // Elimina i caratteri inutili // Controlla che la password sia corretta if (!check_user($_CLIENTS_NICK[$tmpnum], $_CLIENTS_INFO[$tmpnum]['buffer'])) { sock_write($sock, "Password errata!\nBye Bye\n"); quit_user($sock, FALSE); break; } // Tutto ok, quindi annunzia il nuovo utente new_user($_CLIENTS_NICK[$tmpnum]); // Imposta alcune informazioni $_CLIENTS_INFO[$tmpnum]['state'] = STATE_CANCHAT; $_CLIENTS_INFO[$tmpnum]['is_reg'] = TRUE; break; case STATE_CANCHAT: // Controlla se l'utente sta chiedendo un messaggio privato privatechat_user($sock, $tmparr[1], $tmparr[2]); break; } // Controlla se sta chiedendo la disconnessione quit_user($sock); break; } // Controlla se sta chiedendo di uccidere il server (solo per i registrati) if (preg_match("/^\/QUIT/i", $_CLIENTS_INFO[$tmpnum]['buffer'], $tmparr) && $_CLIENTS_INFO[$tmpnum]['is_reg']) { chat_user($sock, "Chisura del server in corso"); echo "Chiusura del server in corso"; break; } // Dato che non è nessuno di questi, vuol dire che è chat ed invia il messaggio chat_user($sock, $_CLIENTS_INFO[$tmpnum]['buffer']); break; } // Svuota il buffer $_CLIENTS_INFO[$tmpnum]['buffer'] = ''; } } } } } ?>
Credo che qui i commenti parlino da soli
Comunque in parole povere fa questo:
- Controlla le socket per vedere se ci sono eventi (dati in arrivo, richiesta di connessione)
- Appena si verifica un evento controlla se è una richiesta di connessione o dati in arrivo
- Se è una richiesta di connessione, controlla se il numero massimo di utenti non è stato raggiunto e se no aggiunge l'utente alla lista ed invia la richiesta per il nick
- Se sono dati in arrivo, ci possono essere 3 situazioni in cui il server si può trovare!
- Situazione APPENA CONNESSO, se succede questo vuol dire che stiamo aspettando il nickname da parte dell'utente, e lo abbiamo ricevuto! Quindi lo controlliamo e controlliamo se c'è un'utente registrato con quella password
- Situazione ATTESA PASSWORD, dopo aver inviato il nick, solo per gli utenti registrati. Il programma controlla se l'utente ha inviato o meno la password corretta
- Situazione PUO' CHATTARE, l'utente può inviare i comando o la normalissima chat
- Inoltre se l'utente chiude la finestra del telnet in automatico viene detto agli altri utenti che un user si è disconnesso
Il server supporta i comandi base come /EXIT (per disconnettersi) e /PVT user messagio (per mandare messagi privati)
Se si è utenti registrati si ha a disposizione anche il comando /QUIT che serve a killare il server
E' codice molto vecchio, scritto a fine 2003, che non rispecchia la mia attuale metodologia di scrittura del codice, non rispecchia le mie attuali conoscenze e soprattutto potrebbe contenere codice che potrebbe non funzionare correttamente ad oggi.

Commenti recenti
42 settimane 4 giorni fa
42 settimane 4 giorni fa
50 settimane 3 giorni fa
1 anno 5 giorni fa
1 anno 5 giorni fa
1 anno 5 giorni fa
1 anno 21 settimane fa
1 anno 21 settimane fa
1 anno 24 settimane fa
1 anno 24 settimane fa