Location: PHPKode > scripts > Qserv > qserv/Qserv.php
<?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();
*/

?>
Return current item: Qserv