Lab : Socket Server en PHP - Principe & démos (telnet & xml)

Principes de développement

Structure de l'objet

Vu qu'un serveur est un service en attente de connexion de clients, je vais le développer sur un modèle événementiel : l'objet une fois instancié broadcastera des events qui pourront être récupérés par plusieurs EventListener. L'objet sera alors plus facile à utiliser pour les développeurs moins avancé, pas besoin d'étendre la classe, de devoir le modifier, on passe juste la config et on met en place les listeners que l'on veut gérer.

Les classes seront chargées automatiquement d'où l'absence de include ou require dans mes codes smiley myconcept: actionscript & php developments.

Variables principales :

Pour notre recette nous n'avons pas grand chose à définir en fait :
  • Le backlog : le nombre max d'entrées qu'il faut écouter
  • Le buffer size : la taille du buffer qu'il faut obtenir (accumulation de paquets)
  • Le data breaker : la plupart du temps c'est soit un byte 0 (\0) soit un simple retour à la ligne (\n)
  • L'adresse : le ou les ip ou dns sur le(s)quel(s) on va écouter
  • Le port : port d'écoute -> sous windows il n'y a pas trop de souci si ce n'est qu'il ne faut pas rentrer en conflit avec un port déjà ouvert (mais bon ça ça vaut pour tous les OS c'est logique ). Sous linux un simple user ne pourra ouvrir un port en dessous du port 1024; Faire tourner un serveur de socket à usage publique entant que root (linux) ou compte admin (windows) serait un peu dangereux donc ben si vous absolument faire tourner le service de socket sous linux sur par exemple le port 80 pour par exemple éviter les problèmes de firewall avec vos clients : soit vous trouvez un hack pour autoriser votre user à ouvrir des ports en dessous de 1024 (sudo fonctionne mais ça revient pareil que de le faire tourner comme root). Soit si vous en avez la possibilité de faire du virtuel et pas le temps d'essayer les pistes éventuelles de hack ben vous faites tourner ça en root sur une machine virtuelle qui ne fait que ça et qui n'a accès à aucune données réseaux.

Principe du serveur de socket en php :

Pour le principe c'est assez simple : il suffit de créer le socket en tcp ou udp (perso je préfère le tcp), de lui dire d'écouter sur telle ip et tel port et ensuite on boucle pour détecter les nouveaux users & nouveaux messages qui arrivent. Ensuite chaque Event va être dispatché et géré par les listeners c'est tout smiley myconcept:actionscript & php développement et management en Belgique. Bon j'ai aussi développé une méthode broadcast afin de renvoyer un message aux clients et une autre pour gérer une extinction "propre" de notre serveur de socket, mais sinon c'est pas plus compliquer que ça sur le principe.

Pour rendre les choses plus propres & faciliter les interactions server/client que l'on voudrait développer plus tard j'ai aussi isoler chaque socket client dans un objet "SocketServerClient". Mon but était d'avoir un code super proche de FMS pour ce qui est de la gestion des events. Donc voilà à quoi ressemblerait par exemple un serveur de chat style telnet (démo à la va vite il faudrait bien évidemment implémenter le tout dans une classe du style TelnetServer) : <?php
/***
 *  Version simplifiée d'une utilisation du serveur de socket
 *  Définition des fonctions qui vont Ãªtre enregistrées comme EventListener
 *  Définition du message d'accueil (par défaut le data breaker est défini comme \n dans la classe SocketServer)
 *  Démarrage du service
 **/
    /***
     * EventListener : Appelé lors du démarrage du service
     * @param   SocketServerEvent  $e   Objet contenant les infos de l'event
     * @void             
     **/         
    
function onAppStart($e){
        
SocketServer::debug("Application is running");
    }
    
/***
     * EventListener : Appelé lors de l'arrêt  du service
     * @param   SocketServerEvent  $e   Objet contenant les infos de l'event
     * @void             
     **/    
    
function onAppStop($e){
        
SocketServer::debug("Application is stopped");
    }
    
/***
     * EventListener : Appelé lors de la connexion d'un nouveau user
     * @param   SocketServerEvent  $e   Objet contenant les infos de l'event
     * @void             
     **/ 
    
function onConnect($e){
       global 
$svr;
       
SocketServer::debug("A new client is online ".$e->datas);
       
$svr->broadCast($e->_target,PHP_EOL."@SERVER : ".$e->_target->link_infos["host"]." is in the place... Make some noise !".PHP_EOL);
       
$e->_target->send(str_replace("%USER%",$e->_target->link_infos["host"],$svr->wMsg));
    }
    
/***
     * EventListener : Appelé lors de la déconnexion d'un user
     * @param   SocketServerEvent  $e   Objet contenant les infos de l'event
     * @void             
     **/ 
    
function onDisconnect($e){
        global 
$svr;
        
$svr->broadCast(false,PHP_EOL."@SERVER : ".$e->datas["link_infos"]["host"]." is gone !".PHP_EOL.">",true);
    }
    
/***
     * EventListener : Appelé lors de la réception d'un nouveau message user; Dans ce cas renvoi le msg Ã  tous les users
     * @param   SocketServerEvent  $e   Objet contenant les infos de l'event
     * @void             
     **/ 
    
function onCall($e){
        global 
$svr;
        
$svr->broadCast($e->_target,PHP_EOL.$e->_target->link_infos["host"]." > ".$e->datas.">",true);
        
$e->_target->send(">");
    }
//On instancie le serveur et on y plug les EventListeners. Une fois que c'est fait
try {
    
$svr=new SocketServer("telnet.myconcept.local",8080);
    
$svr->addEventListener(SocketServerEvent::APP_START ,"onAppStart");
    
$svr->addEventListener(SocketServerEvent::APP_STOP  ,"onAppStop");
    
$svr->addEventListener(SocketServerEvent::CONNECT   ,"onConnect");
    
$svr->addEventListener(SocketServerEvent::DISCONNECT,"onDisconnect");
    
$svr->addEventListener(SocketServerEvent::CALL      ,"onCall");
    
SocketServer::debug("Server Ready");
    
//définition du msg d"accueil que chaque client verra lors de sa connexion au serveur
    
$message=array();
    
$message[]="######################################################";
    
$message[]="##  MY PHP TELNET CHAT SERVER";
    
$message[]="######################################################";
    
$message[]="";
    
$message[]="Welcome %USER%";
    
$message[]=">";
    
$svr->wMsg=implode(PHP_EOL,$message);
    
$svr->start();
}
catch(
Exception $e){
    echo 
"Erreur lors du lancement du service";
}

L'objet SocketServer

Ze code

<?php
/**
 * @category   net
 * @package    be.myconcept.phplab.net
 * @subpackage be.myconcept.phplab.net.SocketServer
 * @see        be.myconcept.phplab.net.SocketServer.SocketServerClient
 * @see        be.myconcept.phplab.events.EventDispatcher
 * @see        be.myconcept.phplab.events.SocketServerEvent 
 * @author     Yannick Molitor - http://myconcept.be
 * @version    1.0
 * @link       http://www.myconcept.be
 * @Modified   feb 06
 * @since      1.1
 * @copyright  Copyright Â© 2006 - Yannick Molitor 
 * @license    MIT Licence > http://fr.wikipedia.org/wiki/Licence_X11 
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE. 
 *  
 **/
class SocketServer extends EventDispatcher{
    
/**
    * Max entries to listen to
    * @var int
    **/
    
const LISTEN_BACKLOG  500;
    
/**
    * Max buffer size to read packets
    * @var int
    **/
    
const BUFFER_SIZE  500;
    
/**
    * String the split packets
    * @var string
    **/
    
public $data_breaker "\n";
    
/**
    * Address to listen to (ip or dns)
    * @var string
    **/
    
private $address;
    
/**
    * Port to listen to
    * @var int
    **/
    
private $port;
    
/**
    * Master Socket
    * @var socket
    **/
    
private $sock;
    
/**
    * UserList -> array of objects (SocketServerClient)
    * @var array
    **/
    
public $userList=array();
    
/**
    * Info broadcasted to all user on killSignal
    * @var string
    **/
    
public $killMsg="The Server will shutdown in 10 seconds";
    
/**
    * Welcome Message
    * @var string
    **/
    
public $wMsg="Mess with the best or die with the rest (hackers the movie - old school baby  )\n\r";
    
/**
    * Delay after killSignal to kill the masterLink (stop the server);
    **/
    
public $killDelay=10;
    
/**
    * Constructor
    * @param    string      $add        the address to listen to
    * @param    int         $port       the port to listen to
    **/
    
public function __construct($add="127.0.0.1",$port=3){
        
$this->address $add;
        
$this->port    $port;
    }
    
/**
    * Main Part of this object... load the socket and start listening
    **/
    
public function start(){
        
//We create the socket
        
if(($this->sock=socket_create(AF_INET,SOCK_STREAM,SOL_TCP))===false){
            throw new 
Exception("The socket cannot be created : ".socket_strerror(socket_last_error($this->sock)),1);
        }
        
socket_setopt($this->sockSOL_SOCKETSO_REUSEADDR);
        
//Then we attribute this socket an address & a port
        
if((socket_bind($this->sock,$this->address$this->port ))===false){
            
socket_close($this->sock);
            throw new 
Exception(
                
"The socket cannot be binded to ".$this->address.":
                "
.$this->port.PHP_EOL.socket_strerror(socket_last_error($this->sock))
                ,
4);
        }
        
//End finally we start listening smiley myconcept:actionscript & php développement et management pour applications RIA (internet, intranet et extranet)
        
if((socket_listen$this->sockSocketServer::LISTEN_BACKLOG))===false){
            
socket_close($this->sock);
            throw new 
Exception("The socket can\"t listen".PHP_EOL.socket_strerror(socket_last_error($this->sock)),3);
        }
        
//To ensure a proper end to this story smiley myconcept:actionscript & php développement et management pour applications RIA (internet, intranet et extranet)
        
register_shutdown_function( array( $this"stop" ) );
        
$this->dispatchEvent(new SocketServerEvent(SocketServerEvent::APP_START,false));
        
//CPU"s READY...  SET...  GO !!!
        
while(true){
            
//Array which will contain active connections
            
$actives=array();
            
//We put it into the actives to detect a new connection
            
$actives[]=$this->sock
            
//We loop the get Clients Link
            
foreach($this->userList as $i=>$valeur){
                
$actives[]=$this->userList[$i]->link;
            }
            
$null=null;
            if(
$cleaned=socket_select($actives,$null,$null,$null)===false){
                continue;
            }
            
//New client Detection
            
if(in_array($this->sock,$actives)){
                
$newClientID=$this->getNewClient($this->sock);
                if(
$newClientID!==false){
                    
$this->dispatchEvent(
                        new 
SocketServerEvent(
                            
SocketServerEvent::CONNECT,
                            array(
$newClientID,"_target"=>$this->userList[$newClientID])
                        )
                    );
                }
            }
            
//Disconnections & New Messages Detection
            
for( $i 0$i count$this->userList ); $i++ ){
                if(!isset( 
$this->userList[$i])) {continue;}
                if(!
in_array($this->userList[$i]->link,$actives)){continue;};
                
$data    =    $this->userList[$i]->getDatas($this->data_breaker);
                
//    empty data => connection was closed
                
if($data===false){
                        
$this->userList[$i]->kill();
                        
$this->dispatchEvent(
                            new 
SocketServerEvent(
                                
SocketServerEvent::DISCONNECT,
                                array(
                                    
"id"=>$this->userList[$i]->id,
                                    
"user_datas"=>$this->userList[$i]->datas,
                                    
"link_infos"=>$this->userList[$i]->link_infos
                                
)
                            )
                        );
                        unset(
$this->userList[$i]);
                }
                else{
                    if(
trim(str_replace("\n","",$data))!=""){
                        
$this->dispatchEvent(new SocketServerEvent(SocketServerEvent::CALL,array($data,"_target"=>$this->userList[$i])));
                     }
                    else {
                        
$this->userList[$i]->send("");
                    }
                }
            }
        }
    }
    
/**
    * Detect new client on a link
    * @param    socket  $link   Link to detect
    * @return   int             The Index of the userList where new connection datas were stored
    **/
    
private function getNewClient(&$link){
       
$vreturn=false;
       
//We loop to fill in the userList and we put equal to detect new connection if nobody was disconnected
        
for( $i $i <= count$this->userList ); $i++ ){
            if( !isset( 
$this->userList[$i] )){
                
//SocketServer::debug("New client : ".$i);
                
$this->userList[$i]    =new SocketServerClient(socket_accept($this->sock),$i);
                
socket_setopt$this->userList[$i]->linkSOL_SOCKETSO_REUSEADDR);
                
$vreturn=$i;
                break;
            }
        }
        return 
$vreturn;
    }
    
/**
    * BroadCast a string to all connected clients
    * @param    SocketServerClient  $from       Client :: sender
    * @param    string              $msg        Message
    * @param    boolean             $system     If it"s a system message or not
    * @void
    **/
    
public function broadCast($from,$msg,$showBuffered=false){
        for(
$i=0;$i<count($this->userList);$i++){
            if(
$this->userList[$i]!==$from){
                
$this->userList[$i]->send($msg,$showBuffered);
            }
        }
    }
    
/**
    * Close the server
    * @param    boolean     $direct     ByPass the delay or not
    * @void
    **/
    
public function stop($direct=false){
        if(!
$direct){
            
$this->broadCast(false,$this->killMsg,true);
            
sleep($this->killDelay);
        }
        
socket_close($this->sock);
        
$this->dispatchEvent(new SocketServerEvent(SocketServerEvent::APP_STOP,false));
    }
    
#
    # STATIC TOOLS
    #
    /**
    * Write & Flush Buffer
    * @param    mixed      $msg     array or string : message to display
    **/
    
public static function debug($msg) {
        if(
is_array($msg)){
            
print_r($msg);
        }
        else {
            echo 
$msg.PHP_EOL;
        }
        
ob_flush();
    }
}

L'objet SocketServerClient

Principe & extrait de code

L'objet SocketServerClient est tout con : stocker les infos propres à un socket client (données à sa construction par le SocketServer, écouter les messages et éventuellement écrire un message.

<?php
/**
 * @category   net
 * @package    be.myconcept.phplab.net
 * @subpackage be.myconcept.phplab.net.SocketServer
 * @see        be.myconcept.phplab.net.SocketServer.SocketServer
 * @see        be.myconcept.phplab.events.EventDispatcher
 * @see        be.myconcept.phplab.events.SocketServerEvent 
 * @author     Yannick Molitor - http://myconcept.be
 * @version    1.0
 * @link       http://www.myconcept.be
 * @Modified   feb 06
 **/ 
class SocketServerClient {
...
    
/**
    * Retrieves Sent Datas from this client
    * @return   mixed   a string if there is something in the buffer, false if not
    **/
    
public function getDatas($byteZero){
        
$data=null;
        
//I do it like this to avoid blocking situations like someone who never press return...
        
if(!($local_buffer=socket_read($this->link,SocketServer::BUFFER_SIZE))){
           
$data=false;
        }
        else {
            
$this->buffer[]=$local_buffer;
            if(
substr($local_buffer,-strlen($byteZero))===$byteZero){    
                
$data=implode("",$this->buffer);
                
$this->buffer=array();
            }
        }
        return 
$data;
    }
    
/**
    * Send string to the client
    * @param    string         msg      the message to send
    * @void
    **/
    
public function send($msg,$showBuffered=false){
        if(
$showBuffered){
            
$msg.=implode("",$this->buffer);
        }
        
socket_write($this->link,$msg,strlen($msg));
    }
...

L'objet XMLSocketServer

Principe & code

L'objet du serveur xml se gère de la même façon que le Socket Server Telnet de la démo qui se trouve supra. Il y a juste le format des msgs (xml) et le data breaker qui changent (byte zero)

<?php
/**
 * @category   net
 * @package    be.myconcept.phplab.net
 * @subpackage be.myconcept.phplab.net.SocketServer
 * @see        be.myconcept.phplab.net.SocketServer.SocketServer
 * @see        be.myconcept.phplab.net.SocketServer.SocketServer.Client
 * @see        be.myconcept.phplab.events.EventDispatcher
 * @see        be.myconcept.phplab.events.SocketServerEvent 
 * @author     Yannick Molitor - http://myconcept.be
 * @version    1.0
 * @link       http://www.myconcept.be
 * @Modified   feb 06
 * @license    MIT Licence > http://fr.wikipedia.org/wiki/Licence_X11 
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE. 
 *   
 **/ 
class XMLSocketServer extends SocketServer{
    
/**
    * Byte Zero to extract messages in  the buffer
    * @var string
    **/
    
public $data_breaker"\0";
    
/**
    * Kill Message sent before the server shutdown
    * @var string
    **/   
    
public $killMsg="<xml><ConnectionStatus>Closed</ConnectionStatus></xml>";
    
/**
    * Welcome message
    * @var string
    **/
    
public $wMsg="<xml>My XML Server</xml>\0";
    
/**
    * Delay between kill msg and the shutdown
    * @var string
    **/
    
public $killDelay="1";
    
/**
    * Buffer Max Size
    * @var int
    **/
    
const  BUFFER_SIZE=100;
}

XML Policy Server from Flash (Adobe) CrossDomain Policy / Security Policy

Ce n'est que le serveur xml socket décrit ci-dessus avec un controle sur le message qui va délivrer le fichier de régulation (policy file) puis fermer la connexion : <?php
    
//Gestionnaire de tous les messages arrivant et sortant
    
function onCall($e){
       
SocketServer::debug("Call at  ".date("H:i:s"));
       
$xml=new SimpleXMLElement($e->datas);
       if(
trim($e->datas)=="<policy-file-request/>"){
            
SocketServer::debug("Cross Domain Policy File Request");
            
//Master Policy
            
if($port==843){
                
$policy =
                    
"<"."?xml version="1.0" encoding="UTF-8"?".">".
                        
"<cross-domain-policy 
                            xmlns:xsi="
http:/ /www.w3.org/2001/XMLSchema-instance
                            xsi:noNamespaceSchemaLocation="
http:/ /www.adobe.com/xml/schemas/PolicyFileSocket.xsd">".
                        
"<allow-access-from domain="xml-policy.myconcept.be" to-ports="80" />".
                        
"<site-control permitted-cross-domain-policies="master-only" />".
                    
"</cross-domain-policy>".$svr->data_breaker;
            }
            
//Domain Policy 
            
else{
                
$policy=array();
                
$policy[]="<"."?xml version="1.0" encoding="UTF-8"?".">".
                
$policy[]="<cross-domain-policy>";
                
$policy[]="<allow-access-from domain="xml-policy.myconcept.be" to-ports="80" />";
                
$policy[]="</cross-domain-policy>";
                
$policy=implode("",$policy).$svr->data_breaker;
            }
            
$e->_target->send($policy);
       }

Et voilà un Policy Server tout simple smiley myconcept:actionscript & php développement et management en Belgique

- MY Concept FlashPlayer Loader for actionscript demo's and movie/animation player-