<?PHP
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP Version 4 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2002 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available at through the world-wide-web at |
// | http://www.php.net/license/2_02.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | hide@address.com so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Authors: Stephan Schmidt <hide@address.com> |
// +----------------------------------------------------------------------+
//
// $Id: Sequential.php,v 1.12 2006/05/27 07:37:56 cweiske Exp $
/**
* Sequential server class.
*
* @category Networking
* @package Net_Server
* @author Stephan Schmidt <hide@address.com>
* @author Christian Weiske <hide@address.com>
*/
/**
* needs the driver base class
*/
require_once 'Net/Server/Driver.php';
/**
* Sequential server class.
*
* This class will handles all connection in one server process.
* This allows you to build servers, where communication between
* the clients is easy. The drawback is that clients are served
* sequentially (hence the name). If you send large blocks of data
* to a client, the others will have to wait.
* For servers where communication between clients is not needed,
* use Net_Server_Fork instead.
*
* Events that can be handled:
* - onStart
* - onConnect
* - onConnectionRefused
* - onClose
* - onReceiveData
* - onShutdown
*
* @category Networking
* @package Net_Server
* @author Stephan Schmidt <hide@address.com>
*/
class Net_Server_Driver_Sequential extends Net_Server_Driver
{
/**
* amount of clients
* @var integer $clients
*/
var $clients = 0;
/**
* Seconds until the idle handler is called.
* If set to NULL, the idle handler is deactivated.
* @var integer
*/
var $idleTimeout = null;
/**
* set maximum amount of simultaneous connections
*
* @access public
* @param int $maxClients
*/
function setMaxClients($maxClients)
{
$this->maxClients = $maxClients;
}
/**
* Set the number of seconds until the idle handler is called (if defined).
* If the timeout is set to NULL (or 0), the timeout is deactivated.
*
* The idle handler function is "onIdle" and takes no parameters.
*
* Please take care when using timeout handlers, as the PHP manual states:
* You should always try to use socket_select() without timeout. Your program
* should have nothing to do if there is no data available. Code that depends
* on timeouts is not usually portable and difficult to debug.
*
* @access public
* @param int $idleTimeout Number of seconds until the timeout handler
* is called.
*/
function setIdleTimeout($idleTimeout = null)
{
if ($idleTimeout === 0) {
$idleTimeout = null;
}
$this->idleTimeout = $idleTimeout;
}
/**
* start the server
*
* @access public
*/
function start()
{
$this->initFD = @socket_create($this->protocol, SOCK_STREAM, 0);
if (!$this->initFD) {
return $this->raiseError('Could not create socket.');
}
// adress may be reused
socket_setopt($this->initFD, SOL_SOCKET, SO_REUSEADDR, 1);
// bind the socket
if (!@socket_bind($this->initFD, $this->domain, $this->port)) {
$error = $this->getLastSocketError($this->initFD);
@socket_close($this->initFD);
return $this->raiseError('Could not bind socket to ' . $this->domain
. ' on port ' . $this->port . ' (' . $error .').');
}
// listen on selected port
if (!@socket_listen($this->initFD, $this->maxQueue)) {
$error = $this->getLastSocketError($this->initFd);
@socket_close($this->initFD);
return $this->raiseError('Could not listen (' . $error . ').');
}
$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['_Net_Server_Status'] = 'running';
if (method_exists($this->callbackObj, 'onStart')) {
$this->callbackObj->onStart();
}
if ($this->idleTimeout !== null) {
if (method_exists($this->callbackObj, 'onIdle')) {
$idleLast = time();
} else {
$this->_sendDebugMessage('Disabling idle handler because onIdle() is not defined in callback handler.');
$this->idleTimeout = null;
}
}
while (true) {
$readFDs = array();
array_push($readFDs, $this->initFD);
// fetch all clients that are awaiting connections
for ($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, $this->idleTimeout);
if ($ready === false) {
$this->_sendDebugMessage('socket_select failed.');
$this->shutdown();
}
//Idling and no data
if ($ready == 0 && $this->idleTimeout !== null && ($idleLast + $this->idleTimeout) <= time()) {
$idleLast = time();
$this->_sendDebugMessage('Calling onIdle handler.');
$this->callbackObj->onIdle();
continue;
}
// check for new connection
if (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->callbackObj, 'onConnectionRefused')) {
$this->callbackObj->onConnectionRefused($newClient);
}
$this->closeConnection($newClient);
}
}
if (--$ready <= 0) {
continue;
}
}
// check all clients for incoming data
for ($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 === false) {
$this->_sendDebugMessage('Connection closed by peer');
$this->closeConnection($i);
}
else {
$this->_sendDebugMessage('Received ' . trim($data) . ' from ' . $i);
if (method_exists($this->callbackObj, 'onReceiveData')) {
$this->callbackObj->onReceiveData($i, $data);
}
}
}
}
}
}
/**
* accept a new connection
*
* @access private
* @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->callbackObj, 'onConnect')) {
$this->callbackObj->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;
}
/**
* 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 $data data 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 $this->raiseError('Client does not exist.');
}
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]) . ').');
}
return true;
}
/**
* send data to all clients
*
* @access public
* @param string $data data to send
* @param array $exclude client ids to exclude
*/
function broadcastData($data, $exclude = array())
{
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 (!@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 $info information about the client
*/
function getClientInfo($clientId)
{
if (!isset($this->clientFD[$clientId]) || $this->clientFD[$clientId] == NULL) {
return $this->raiseError('Client does not exist.');
}
return $this->clientInfo[$clientId];
}
/**
* close connection to a client
*
* @access public
* @param int $clientID internal ID of the client
*/
function closeConnection($id = 0)
{
if (!isset($this->clientFD[$id])) {
return $this->raiseError('Connection already has been closed.');
}
if (method_exists($this->callbackObj, 'onClose')) {
$this->callbackObj->onClose($id);
}
$this->_sendDebugMessage('Closed connection (' . $id . ') from ' . $this->clientInfo[$id]['host']
. ' on port ' . $this->clientInfo[$id]['port']);
@socket_shutdown($this->clientFD[$id], 2);
@socket_close($this->clientFD[$id]);
$this->clientFD[$id] = NULL;
unset($this->clientInfo[$id]);
$this->clients--;
}
/**
* shutdown server
*
* @access public
*/
function shutDown()
{
if (method_exists($this->callbackObj, 'onShutdown')) {
$this->callbackObj->onShutdown();
}
$maxFD = count($this->clientFD);
for ($i = 0; $i < $maxFD; $i++) {
$this->closeConnection($i);
}
@socket_shutdown($this->initFD, 2);
@socket_close($this->initFD);
$this->_sendDebugMessage('Shutdown server.');
exit();
}
}
?>