/*** patServer* PHP socket server base class* Events that can be handled:** onStart** onConnect** onConnectionRefused** onClose** onShutdown** onReceiveData** @version 1.1* @authorStephan Schmidt <schst@php-tools.de>* @package patServer*/class patServer {/*** information about the project* @var array $systemVars*/var $systemVars= array("appName"=> "patServer","appVersion"=> "1.1","author"=> array("Stephan Schmidt <schst@php-tools.de>", ));/*** port to listen* @var integer$port*/var $port= 10000;/*** domain to bind to* @var string $domain*/var $domain= "localhost";/*** maximum amount of clients* @var integer $maxClients*/var $maxClients = -1;/*** buffer size for socket_read* @var integer $readBufferSize*/var $readBufferSize= 128;/*** end character for socket_read* @var integer $readEndCharacter*/var $readEndCharacter = "
";/*** maximum of backlog in queue* @var integer $maxQueue*/var $maxQueue = 500;/*** debug mode* @var boolean $debug*/var $debug= true;/*** debug mode* @var string $debugMode*/var $debugMode = "text";/*** debug destination (filename or stdout)* @var string $debugDest*/var $debugDest = "stdout";/*** empty array, used for socket_select* @var array $null*/var $null= array();/*** all file descriptors are stored here* @var array $clientFD*/var $clientFD = array();/*** needed to store client information* @var array $clientInfo*/var $clientInfo = array();/*** needed to store server information* @var array $serverInfo*/var $serverInfo = array();/*** amount of clients* @var integer$clients*/var $clients = 0;/*** create a new socket server** @access public* @param string$domaindomain to bind to* @param integer$portport to listen to*/function patServer( $domain = "localhost", $port = 10000 ){$this->domain = $domain;$this->port= $port;$this->serverInfo["domain"] = $domain;$this->serverInfo["port"] = $port;$this->serverInfo["servername"] = $this->systemVars["appName"];$this->serverInfo["serverversion"] = $this->systemVars["appVersion"];set_time_limit( 0 );}/*** set maximum amount of simultaneous connections** @access public* @param int $maxClients*/function setMaxClients( $maxClients ){$this->maxClients = $maxClients;}/*** set debug mode** @access public* @param mixed $debug [text|htmlfalse]* @param string $dest destination of debug message (stdout to output or filename if log should be written)*/function setDebugMode( $debug, $dest = "stdout" ){if( $debug === false ){$this->debug = false;return true;}$this->debug= true;$this->debugMode = $debug;$this->debugDest = $dest;}/*** start the server** @access public* @param int $maxClients*/function start(){$this->initFD = @socket_create( AF_INET, SOCK_STREAM, 0 );if( !$this->initFD )die( "patServer: Could not create socket." );// adress may be reusedsocket_setopt( $this->initFD, SOL_SOCKET, SO_REUSEADDR, 1 );// bind the socketif( !@socket_bind( $this->initFD, $this->domain, $this->port ) ){@socket_close( $this->initFD );die( "patServer: Could not bind socket to ".$this->domain." on port ".$this->port." ( ".$this->getLastSocketError( $this->initFd )." )." );}// listen on selected portif( !@socket_listen( $this->initFD, $this->maxQueue ) )die( "patServer: Could not listen ( ".$this->getLastSocketError( $this->initFd )." )." );$this->sendDebugMessage( "Listening on port ".$this->port.". Server started at ".date( "H:i:s", time() ) );// this allows the shutdown function to check whether the server is already shut down$GLOBALS["_patServerStatus"] = "running";// this ensures that the server will be sutdown correctlyregister_shutdown_function( array( $this, "shutdown" ) );if( method_exists( $this, "onStart" ) )$this->onStart();$this->serverInfo["started"] = time();$this->serverInfo["status"]= "running";while( true ){$readFDs = array();array_push( $readFDs, $this->initFD );// fetch all clients that are awaiting connectionsfor( $i = 0; $i < count( $this->clientFD ); $i++ ) if( isset( $this->clientFD[$i] ) ) array_push( $readFDs, $this->clientFD[$i] );// block and wait for data or new connection$ready = @socket_select( $readFDs, $this->null, $this->null, NULL );if( $ready === false ){ $this->sendDebugMessage( "socket_select failed." ); $this->shutdown();}// check for new connectionif( in_array( $this->initFD, $readFDs ) ){ $newClient = $this->acceptConnection( $this->initFD ); // check for maximum amount of connections if( $this->maxClients > 0 ) { if( $this->clients > $this->maxClients ) {$this->sendDebugMessage( "Too many connections." );if( method_exists( $this, "onConnectionRefused" ) )$this->onConnectionRefused( $newClient );$this->closeConnection( $newClient ); } } if( --$ready <= 0 ) continue;}// check all clients for incoming datafor( $i = 0; $i < count( $this->clientFD ); $i++ ){ if( !isset( $this->clientFD[$i] ) ) continue; if( in_array( $this->clientFD[$i], $readFDs ) ) { $data = $this->readFromSocket( $i ); // empty data => connection was closed if( !$data ) {$this->sendDebugMessage( "Connection closed by peer" );$this->closeConnection( $i ); } else {$this->sendDebugMessage( "Received ".trim( $data )." from ".$i );if( method_exists( $this, "onReceiveData" ) )$this->onReceiveData( $i, $data ); } }}}}/*** read from a socket** @access private* @param integer $clientId internal id of the client to read from* @return string $datadata that was read*/function readFromSocket( $clientId ){// start with empty string$data= "";// read data from socketwhile( $buf = socket_read( $this->clientFD[$clientId], $this->readBufferSize ) ){$data .= $buf;$endString = substr( $buf, - strlen( $this->readEndCharacter ) );if( $endString == $this->readEndCharacter ) break;if( $buf == NULL ) break;}if( $buf === false )$this->sendDebugMessage( "Could not read from client ".$clientId." ( ".$this->getLastSocketError( $this->clientFD[$clientId] )." )." );return $data;}/*** accept a new connection** @access public* @param resource &$socket socket that received the new connection* @return int$clientID internal ID of the client*/function acceptConnection( &$socket ){for( $i = 0 ; $i <= count( $this->clientFD ); $i++ ){if( !isset( $this->clientFD[$i] ) || $this->clientFD[$i] == NULL ){ $this->clientFD[$i] = socket_accept( $socket ); socket_setopt( $this->clientFD[$i], SOL_SOCKET, SO_REUSEADDR, 1 ); $peer_host = ""; $peer_port = ""; socket_getpeername( $this->clientFD[$i], $peer_host, $peer_port ); $this->clientInfo[$i] = array( "host"=> $peer_host, "port"=> $peer_port, "connectOn" => time() ); $this->clients++; $this->sendDebugMessage( "New connection ( ".$i." ) from ".$peer_host." on port ".$peer_port ); if( method_exists( $this, "onConnect" ) ) $this->onConnect( $i ); return $i;}}}/*** check, whether a client is still connected** @access public* @param integer $id client id* @return boolean $connected true if client is connected, false otherwise*/function isConnected( $id ){if( !isset( $this->clientFD[$id] ) )return false;return true;}/*** close connection to a client** @access public* @param int $clientID internal ID of the client*/function closeConnection( $id ){if( !isset( $this->clientFD[$id] ) )return false;if( method_exists( $this, "onClose" ) )$this->onClose( $id );$this->sendDebugMessage( "Closed connection ( ".$id." ) from ".$this->clientInfo[$id]["host"]." on port ".$this->clientInfo[$id]["port"] );@socket_close( $this->clientFD[$id] );$this->clientFD[$id] = NULL;unset( $this->clientInfo[$id] );$this->clients--;}/*** shutdown server** @access public*/function shutDown(){if( $GLOBALS["_patServerStatus"] != "running" )exit;$GLOBALS["_patServerStatus"] = "stopped";if( method_exists( $this, "onShutdown" ) )$this->onShutdown();$maxFD = count( $this->clientFD );for( $i = 0; $i < $maxFD; $i++ )$this->closeConnection( $i );@socket_close( $this->initFD );$this->sendDebugMessage( "Shutdown server." );exit;}/*** get current amount of clients** @access public* @return int $clients amount of clients*/function getClients(){return $this->clients;}/*** send data to a client** @access public* @param int$clientId ID of the client* @param string $datadata to send* @param boolean $debugData flag to indicate whether data that is written to socket should also be sent as debug message*/function sendData( $clientId, $data, $debugData = true ){if( !isset( $this->clientFD[$clientId] ) || $this->clientFD[$clientId] == NULL )return false;if( $debugData )$this->sendDebugMessage( "sending: "" . $data . "" to: $clientId" );if( !@socket_write( $this->clientFD[$clientId], $data ) )$this->sendDebugMessage( "Could not write "".$data."" client ".$clientId." ( ".$this->getLastSocketError( $this->clientFD[$clientId] )." )." );}/*** send data to all clients** @access public* @param string $datadata to send* @param array $exclude client ids to exclude*/function broadcastData( $data, $exclude = array(), $debugData = true ){if( !empty( $exclude ) && !is_array( $exclude ) )$exclude = array( $exclude );for( $i = 0; $i < count( $this->clientFD ); $i++ ){if( isset( $this->clientFD[$i] ) && $this->clientFD[$i] != NULL && !in_array( $i, $exclude ) ){ if( $debugData ) $this->sendDebugMessage( "sending: "" . $data . "" to: $i" ); if( !@socket_write( $this->clientFD[$i], $data ) ) $this->sendDebugMessage( "Could not write "".$data."" client ".$i." ( ".$this->getLastSocketError( $this->clientFD[$i] )." )." );}}}/*** get current information about a client** @access public* @param int$clientId ID of the client* @return array $infoinformation about the client*/function getClientInfo( $clientId ){if( !isset( $this->clientFD[$clientId] ) || $this->clientFD[$clientId] == NULL )return false;return $this->clientInfo[$clientId];}/*** send a debug message** @access private* @param string $msg message to debug*/function sendDebugMessage( $msg ){if( !$this->debug )return false;$msg = date( "Y-m-d H:i:s", time() ) . " " . $msg;switch( $this->debugMode ){case "text": $msg = $msg."
"; break;case "html": $msg = htmlspecialchars( $msg ) . "<br />
"; break;}if( $this->debugDest == "stdout" || empty( $this->debugDest ) ){echo $msg;flush();return true;}error_log( $msg, 3, $this->debugDest );return true;}/*** return string for last socket error** @access public* @return string $error last error*/function getLastSocketError( &$fd ){$lastError = socket_last_error( $fd );return "msg: " . socket_strerror( $lastError ) . " / Code: ".$lastError;}function onReceiveData($ip,$data){$this->broadcastData( $data,array(), true );}}$patServer = new patServer();$patServer->start();