<?php
/**
* Class Qserv is used to create a simple php server (TCP only for now)
* You can extend it in two way the first one (recommanded) is to extends the Qserv class and implement
* your own methods for onincomming,onaccept,onclose,onstart,onshutdown,onstartlisten as needed.
* The second way is to simply register some callback functions with the Qserv::set_event_callback method.
* @package Qserv
* @since 2006-01-03
* @author <jonathan dot gotti at free dot fr> for Qaleo
* @exemple
* @code
* function cmd_handler(&$server,$clientid,$cmd){
* if($cmd=='stop')
* $server->shutdown();
* }
* $server = &new Qserv(45000);
* $server->set_event_callback('onincomming','cmd_handler');
* $server->start();
* @endcode
* @changelog 2006-04-19 - better error handling on read method + bug correction to correctly close disconnected clients connection
* 2006-04-18 - new parameter Qserv::_shutdown_onclose to enabled socket_shutdown() before socket_close when needed (i really need it)
* - Qserv::read() use a loop now so will handle longer response than before
* 2006-03-15 - no more error when reading from a disconnected client
* 2006-02-08 - no more error when writing to a disconnected client
* 2006-01-17 - changed prompstr usage
* 2006-01-12 - now simply ignore writing to null client (!= unknown)
* - new event onstartlisten
* 2006-01-06 - new ask and confirm methods + disconnect on client CTRl+D
* 2006-01-05 - better error handling in read and write methods
* - now keep some infos about connected clients (not in single_listen mode)
* - don't use socket_shutdown any more (give more trouble than help)
* 2006-01-04 - add some event support such as onStart/onShutdown/onAccept/onClose each one can be handle in 2 ways:
* by registering a callback function or by implementing a corresponding method in your derived Qserv class.
* - removed all 'cmd_handler' related method and properties
* @TODO regler le probleme de lecture sur message trop long -> attente de reponse de hide@address.com
*/
class Qserv{
var $adress = '0.0.0.0';
var $port = 10000;
var $sock = null;
var $AF = AF_INET;
var $maxqueue = 50;
var $start_time = 0;
var $watched = array();
var $clients = array();
var $clients_info = array();
var $greetings = "Welcome to a QServ server.";
var $listen_method = 'listen';
var $callbacks = array();
var $supported_events = array();
var $promptstr = '>';
var $_shutdown_onclose = FALSE;
/**
* constructor method just set server adress and port
*/
function Qserv($port='10000',$adress='0.0.0.0'){
$this->adress = $adress;
$this->port = (int) $port;
$this->supported_events = array('onstart','onshutdown','onaccept','onclose','onincomming','onstartlisten');
}
###--- SETTING METHODS ---###
/**
* Set the method used to lisen on port.
* @param string $lmethod single|normal
*/
function set_listen_method($lmethod){
switch(strtolower($lmethod)){
case 'single':
case 'single_listen':
$this->listen_method = 'single_listen'; break;
case 'listen':
case 'normal':
default:
$this->listen_method = 'listen'; break;
}
}
/**
* Set the greets phrase
* @param str $greets
*/
function set_greetings($greets){
$this->greetings = $greets;
}
/**
* set the prompt string
* @param string $promptstr
*/
function set_prompt($prompt='>'){
$this->promptstr = $prompt;
}
/** set prompt string to '' */
function set_noprompt(){
$this->promptstr = '';
}
/** return the prompt string */
function get_prompt(){
return $this->promptstr;
}
###--- RUNNING METHODS ---###
/**
* start the server
*/
function start(){
echo "Init server\n";
if(! $this->init())
return FALSE;
$this->_do_event('onstart');
$this->start_time = time();
$this->{$this->listen_method}();
}
/**
* shutdown the server and optionnaly close_connection all clients before
*/
function shutdown($kick_clients_first=TRUE){
if(! $this->sock )
return $this->error("Can't shutdown a not started server");
$this->_do_event('onshutdown');
# close_connection all clients
if($kick_clients_first)
foreach($this->clients as $cid=>$csock)
$this->close_connection($cid);
# close the main socket
# socket_shutdown($this->sock,2);
socket_close($this->sock);
$this->sock = $this->watched[-1] = null;
unset($this->watched[-1]);
}
/**
* init the server main socket
*/
function init(){
# re-init clients infos in case we restart the server
$this->clients = $this->clients_info = $this->watched = array();
if(! $this->sock = @socket_create( $this->AF, SOCK_STREAM, SOL_TCP ) )
return $this->error("Create main socket failed",$this->sock,1);
# set adress to be reusable
socket_set_option( $this->sock, SOL_SOCKET, SO_REUSEADDR, 1 );
if( ! @socket_bind( $this->sock, $this->adress, $this->port ) )
return $this->error("BIND main socket failed",$this->sock,1);
# start listening
if( @socket_listen($this->sock,$this->maxqueue) === FALSE )
return $this->error('Listening problem',$this->sock,1);
$this->watched[-1] = $this->sock;
return TRUE;
}
/**
* Mode d'écoute simple, un client à la fois deconnecté après chaque commande.
* Toute tentative de connection alors qu'un client est déjà connecté sera mise en attente.
* dans ce mode les infos sur les clients ne sont pas renseigné (inutile)
*/
function single_listen(){
echo "Server start listening in single mode at ".date("Y-m-d H:i:s")."\n";
while($this->sock){
# wait for a connection
$cid = $this->accept_connection();
# read datas from remote connection
if(! $data = $this->read($cid))
continue;
$this->_do_event('onincomming',$cid,$data);# do something with datas
$this->close_connection($cid); # then kick the client
}
}
/**
* start to wait client connection
* peut discuter avec plusieurs clients à la fois et ne les déconnecte que sur demande
*/
function listen(){
echo "Server start listening at ".date("Y-m-d H:i:s")."\n";
while($this->sock){
# init watched array (must do this)
$r = $this->watched;
$w = $e = null;
if(!isset($listening)){
$listening = TRUE;
$this->_do_event('onstartlisten');
}
# look for any change on any open sockets
$nb = @socket_select($r,$w,$e,null);
if($nb===0)
continue;
elseif(! $this->sock ) # server probably received a kill signal
break;
elseif($nb===FALSE)
return $this->error("Socket Select failed");
foreach($r as $sock){ # all changed socket
if($sock === $this->sock){ # this case is new connection
$cid = $this->accept_connection();
continue;
}
$cid = array_search($sock,$this->clients);
if(! $data = $this->read($cid))
continue;
$this->_do_event('onincomming',$cid,$data);
}
}
}
###--- CLIENTS RELATED METHODS ---###
/**
* accept a new connection add the new socket to the watched socket array
* @private
* @return str internal client id
*/
function accept_connection(){
$id = $this->_new_id();
if(! ($sock = socket_accept($this->sock)) )
return $this->error("Can't accept new client on main socket",$this->sock);
$this->watched[$id] = $this->clients[$id] = $sock;
# get some info from client if usefull (not in single_listen mode)
if( $this->listen_method == 'listen' ){
list($ip,$port) = $this->get_client_infos($id);
$this->clients_info[$id] = array('ip'=>$ip,'port'=>$port,'connection_time'=>time());
}
if( $this->greetings )
$this->write($id,$this->greetings." [$id]");
$this->_do_event('onaccept',$id);
return $id;
}
/**
* return the first free client id or give a new one
* added just for readability of accept_connection
* @private
*/
function _new_id(){
return md5(uniqid(null,TRUE));
}
/**
* close_connection the client with id: $id
* @param str $id client internal id
* @return bool
*/
function close_connection($id){
if( (! isset($this->clients[$id])) || is_null($this->clients[$id]) )
return $this->error("Can't close not existing connection to client[$id]!");
$this->_do_event('onclose',$id);
if($this->_shutdown_onclose)
@socket_shutdown($this->clients[$id],2);
socket_close($this->clients[$id]);
$this->clients_info[$id] = $this->clients[$id] = $this->watched[$id] = null;
unset($this->clients[$id], $this->watched[$id], $this->clients_info[$id]);
return TRUE;
}
/**
* verifie si un client à toujours l'air connecté ou non
* @param str $id internal client id
*/
function is_connected($id){
return (bool) ( @$this->clients[$id] && @$this->watched[$id] );
}
/**
* get some info about the client sucha as IP
* @param str $cid client internal id
* @return array
*/
function get_client_infos($cid){
if( (! isset($this->clients[$cid])) || is_null($this->clients[$cid]) )
return $this->error("client[$cid] doesn't seem to be connected!");
socket_getpeername( $this->clients[$cid], $host, $port );
return array($host,$port);
}
###--- CONVERSATION METHODS ---###
/**
* read the client $cid socket
* @param str $cid client internal id
* @return string
* /
function read($cid){
if(! @$this->clients[$cid])
return $this->error('read from a non existant client socket',null,1);
$datas = @socket_read($this->clients[$cid],2048);
if($datas === FALSE && socket_last_error($this->clients[$cid])!=104) # no more error on client disconnect
$this->error("Can't read from client[$cid], closing connection... ",$this->clients[$cid]);
if( (! $datas) || trim($datas)===chr(4) ){ # disconnected client,empty response or CTRL+d we kick him
$this->close_connection($cid);
return FALSE;
}
return trim($datas);
}*/
function read($cid){
if(! (isset($this->clients[$cid]) && $this->clients[$cid]) )
return $this->error('read from a non existant client socket',null,1);
$data = '';
# echo "START READING $cid\n";
while (true) {
$buf = @socket_read($this->clients[$cid], 512);
$data .= $buf;
if(strlen($buf) < 512)
break;
}
# echo "END READING $cid\n";
if($buf === FALSE && socket_last_error($this->clients[$cid])!=104){ # no more error on client disconnect
$this->error("Can't read from client[$cid], closing connection... ",$this->clients[$cid]);
$this->close_connection($cid);
return FALSE;
}
# if( (! $data) || trim($data)===chr(4) ){ # disconnected client,empty response or CTRL+d we kick him
if( (! $data) || trim($data)===chr(4) ){ # disconnected client,empty response or CTRL+d we kick him
$this->close_connection($cid);
return FALSE;
}
return trim($data);
}
/**
* write to client $cid socket
* @param int $cid client internal id
* @param string $msg the message to send to the client
* @param string $noprompt set this to true to avoid prompt
* @return bool
*/
function write($cid,$msg,$noprompt=FALSE){
if( ! $cid ) # ignore msgs for null client
return FALSE;
if(! @$this->clients[$cid])
return $this->error("write \"$msg\" to a non existant client socket",null,1);
$_msg = $msg; # used for later error
$msg .= "\n".($noprompt?'':$this->promptstr);
$done = 0;
$todo = strlen($msg);
while($done<$todo){
if(! $n = @socket_write($this->clients[$cid],substr($msg,$done)))
break;
$done += $n;
}
if( $done == $todo ){
return TRUE;
}else{
if(socket_last_error($this->clients[$cid])!=104) # no more error on client disconnect
$this->error("error while writing \"$_msg\" to client[$cid], closing connection...",$this->clients[$cid]);
$this->close_connection($cid);
return FALSE;
}
}
/**
* send a confirmation request to client $cid.
* will return true if the response match this reg_exp: '!^[yo](?:[eu][si])?!i'
* else return FALSE
* @param int $cid client internal id
* @param string $msg confirmation message
* @return bool
*/
function confirm($cid,$msg){
if(! $this->write($cid,$msg." (y[es]|N[o])") )
return FALSE;
return preg_match('!^[yo](?:[eu][si])?!i',$this->read($cid));
}
/**
* ask a question to the client and return the response
* return FALSE on connection problem and that is != from an empty answer ''
* @param int $cid client internal id
* @param string $msg question message
* @return string
*/
function ask($cid,$msg){
if(! $this->write($cid,$msg) )
return FALSE;
return $this->read($cid);
}
/**
* send a message to all connected clients
* @param string $msg message to send
* @param string $noprompt set this to true to avoid prompt
*/
function broadcast($msg,$noprompt=FALSE){
foreach($this->clients as $cid=>$sock)
$this->write($cid,$msg,$noprompt);
}
###--- EVENT HANDLING METHODS ---###
/**
* Qserv support some events, when those events occurs it will first check for a callback function to execute.
* If no callback is registered or if the callback function return TRUE, Qserv will then look for a method with the same name as the event,
* if defined Qserv then will call it. At least if no callback and no method are defined it will simply do nothing!
*
* Callback prototypes: callback function / extended Qserv method
* - onstart(&$server) / onstart()
* - onshutdown(&$server) / onshutdown()
* - onstartlisten(&$server) / onstartlisten()
* - onaccept(&$server,$clientid) / onaccept($clientid)
* - onclose(&$server,$clientid) / onclose($clientid)
* - onincomming(&$server,$clientid,$datas) / onincomming($clientid,$datas)
*
* @param string $event the name of the event (onstart|onshutdown|onaccept|onclose|onincomming|onstartlisten)
* @param mixed $callback the function to execute as a callback ('mycallbackfunc' | array(&$myobject,'mycallbackmethod'))
* @param array $params an array of additionnal params to pass to the callback function.
*/
function set_event_callback($event,$callback,$params=null){
$events = $this->supported_events;
$event = strtolower($event);
echo "SET SERVER EVENT $event\n";
if(! in_array($event,$events) )
return $this->error("event $event is not supported (supported events: ".implode('|',$events).')');
$this->callbacks[$event] = array($callback,(array) $params);
}
/**
* unregister an event callback registered with Qserv::set_event_callback
* @param string $event event name
*/
function unset_event_callback($event){
$events = $this->supported_events;
$event = strtolower($event);
if(! in_array($event,$events) )
return $this->error("event $event is not supported (supported events: ".implode('|',$events).')');
$this->callbacks[$event] = null;
unset($this->callbacks[$event]);
}
/**
* internal method to manag ecallback functions / methods
* @see Qserv::set_event_callback for more infos
* @private
* @param string $event name of the event
* @param int $cid the client id concerned by the event
* @param string $datas the command passed by the client
*/
function _do_event($event,$cid=null,$datas=null){
# echo "DO SERVER EVENT $event\n";
$check4method = TRUE;
$event = strtolower($event);
$std_param = (in_array($event,array('onstart','onshutdown'))? array(&$this): array(&$this,$cid,$datas) );
# first check for the event registered callback
if( isset($this->callbacks[$event]) ){ # on a un callback enregistré
list($cb,$params) = $this->callbacks[$event];
$check4method = call_user_func_array( $cb, array_merge($std_param,(array)$params) );
}
# check for method if needed
if( $check4method === TRUE && method_exists($this,$event) ){
if( count($std_param)>1 )
$this->{$event}($cid,$datas);
else
$this->{$event}();
}
}
###--- ERROR HANDLING ---###
/**
* trhow an error
* @param str $msg
* @param resource $sock the socket on wich the error happened
* @param int $fatal giving a value to this parameter will cause the script to stop and return the $fatal status code
* @return FALSE for convenience
*/
function error($msg,$sock=null,$fatal=FALSE){
if(! is_null($sock) ){
$errno = socket_last_error($sock);
$msg .= " ($errno:".socket_strerror($errno).')';
}
$dbg = debug_backtrace();
# print_r($dbg);
$msg = (isset($dbg[1]['class'])?$dbg[1]['class'].'::':'').$dbg[1]['function']." => $msg";
if(class_exists('console_app') ){
console_app::msg_error($msg);
}else{
echo "$msg\n";
error_log($msg);
}
if($fatal!==FALSE){
if($this->sock)
$this->shutdown();
exit($fatal);
}
return FALSE;
}
}
/**
* SAMPLE USAGE
*
class myserv extends Qserv{
function myserv(){
$this->Qserv();
$this->set_listen_method('single');
}
function onincomming($cid,$data){
switch($data){
case 'uptime':
$this->write($cid,"UPTIME ". (time() - $this->start_time));
break;
case 'nb':
$this->write($cid,"#".count($this->clients)." clients connected");
break;
case 'pid':
$this->write($cid,"current server pid: ".getmypid());
break;
case 'kill':
$this->write($cid,"Server will shutdown now");
echo "Server shutdown by peer[$cid] ".implode(':',$this->clients_info[$cid])."\n";
$this->shutdown();
break;
case 'info':
$this->write($cid,print_r($this->get_client_infos($cid),1));
break;
case 'broad':
$this->write($cid,"BROADCAST msg");
$this->broadcast("BROADCAST MSG by peer $cid");
break;
case 'show':
$this->write($cid,print_r($this->clients_info,1));
break;
case 'quit':
# default:
$this->write($cid,"Good Bye");
$this->close_connection($cid);
break;
}
}
}
$s = &new myserv();
$s->start();
*/
?>