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
.
Variables principales :
- 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
. 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->sock, SOL_SOCKET, SO_REUSEADDR, 1 );
//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 
if((socket_listen( $this->sock, SocketServer::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 
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 = 0 ; $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]->link, SOL_SOCKET, SO_REUSEADDR, 1 );
$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
<?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 