Files
DomFramework/tcpserver.php

470 lines
14 KiB
PHP

<?php
/** DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
*/
/** This class allow to start a TCP server and call a function each time a
* client is connected on it. Each client is separated in a child, so the
* server allow to have multiple simultaneous connections.
*
* The handler method or function will get one parameter : the client from
* socket_accept. It will allow to use socket_getpeername to get the peer
* address and port, socket_read to get the data from the client,
* socket_write to write on the client, socket_shutdown ($client) and
* socket_close ($client) to finish the connection
*
* The server has a child limit set to 500 connections by default
*/
class tcpserver
{
////////////////////
// PROPERTIES //
////////////////////
// {{{
/** Allow to debug with message on screen
*/
private $debug = true;
/** Store the data concerning the sockets and the handlers
*/
private $handlers = array ();
/** Store the addresses
*/
private $addresses = array ();
/** Store the ports
*/
private $ports = array ();
/** The max number of children. The maximum of concurrent connections
*/
private $maxChild = 500;
/** Set to true in parent, and false in child
*/
private $parent = true;
/** The socket in the child
*/
private $socket = null;
/** Read in Text mode or in Binary mode
*/
private $readMode = "text";
/** Stop the new connections
*/
private $loopStop = false;
/** The number of active clients
*/
private $nbChild = 0;
// }}}
////////////////////////
// PUBLIC METHODS //
////////////////////////
/** Set/get the max children, the maximum of concurrent connections
* @param integer|null $val The number of child to get/set
*/
public function maxChild ($val = null)
// {{{
{
if ($val === null)
return $this->maxChild;
$this->maxChild = intval ($val);
return $this;
}
// }}}
/** Set/get the read mode : text or binary
* @param string|null $readMode The mode to set (or get if null)
*/
public function readMode ($readMode = null)
// {{{
{
if ($readMode === null)
return $this->readMode;
if ($readMode !== "text" && $readMode !== "binary")
throw new \Exception ("Invalid readMode provided (nor text nor binary)",
500);
$this->readMode = $readMode;
return $this;
}
// }}}
/** Set the address, port and handler that will be enabled by loop
* @param string $address The server address (can be 0.0.0.0 for all IPv4
* interfaces or :: for all the IPv4 and IPv6 interfaces)
* @param integer $port The port to listen
* @param callable $handler The handler that will be called when a client is
* connected to the address:port
*/
public function init ($address, $port, $handler)
// {{{
{
$this->nbChild = 0;
$this->handlers["$address:$port"] = $handler;
$this->addresses[] = $address;
$this->ports[] = $port;
return $this;
}
// }}}
/** Start the main loop after the init and keep in it until loopStop
*/
public function loop ()
// {{{
{
declare(ticks = 10);
pcntl_async_signals (true);
pcntl_signal (SIGCHLD,[$this, "sigCHLD"]);
pcntl_signal (SIGTERM,[$this, "sigTERMINT"]);
pcntl_signal (SIGINT,[$this, "sigTERMINT"]);
foreach ($this->addresses as $key => $address)
{
$port = $this->ports[$key];
if (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4))
$address = "$address";
elseif (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))
$address = "[$address]";
else
throw new \Exception ("Can't create socket : invalid address provided");
$sockServer[$key] = stream_socket_server ("tcp://$address:$port",
$errno, $errstr);
if ($sockServer[$key] === false)
{
throw new \Exception ("Can't create socket : $errstr");
}
if ($this->debug)
printf ("[".posix_getpid ()."] Listening on %s:%d...\n",
$address, $port);
}
while (1)
{
if ($this->loopStop)
{
if ($this->debug)
echo "[".posix_getpid ()."] Do not accept new connections ".
"($this->nbChild)\n";
foreach ($sockServer as $socket)
stream_socket_shutdown ($socket, STREAM_SHUT_RD);
if ($this->nbChild === 0)
{
if ($this->debug)
echo "[".posix_getpid ()."] No more childs and loopStop requested".
" : end of process\n";
exit;
}
sleep (2);
continue;
}
$read = $sockServer;
$write = null;
$except = null;
// catch the errors, as a kill on a pending stream_select display a PHP
// Warning
if (@stream_select ($read, $write, $except, 0, 200000) < 1)
{
pcntl_signal_dispatch ();
continue;
}
foreach ($read as $sock)
{
$client = stream_socket_accept ($sock);
$name = stream_socket_get_name ($client, false);
$pos = strrpos ($name, ":");
$localPort = substr ($name, $pos+1);
$localAddress = substr ($name, 0, $pos);
$name = stream_socket_get_name ($client, true);
$pos = strrpos ($name, ":");
$port = substr ($name, $pos+1);
$address = substr ($name, 0, $pos);
$handlerAddress = $localAddress;
if (! key_exists ("$handlerAddress:$localPort", $this->handlers))
{
// If the address of the handler doesn't exists, the socket is maybe
// waiting on all the interfaces addresses
$handlerAddress = str_replace (["[", "]"], ["",""], $handlerAddress);
if (filter_var ($handlerAddress, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4))
$handlerAddress = "0.0.0.0";
elseif (filter_var ($handlerAddress, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV6))
$handlerAddress = "::";
if (! key_exists ("$handlerAddress:$localPort", $this->handlers))
{
printf ("Can't find the handler for %s:%d\n", $handlerAddress,
$localPort);
continue;
}
}
if (substr ($address, 0, 8) === "[::ffff:")
$address = substr ($address, 8, -1);
if (substr ($localAddress, 0, 7) === "[::ffff:")
$localAddress = substr ($localAddress, 8, -1);
if ($this->debug)
echo "[".posix_getpid ()."] New connection $address:$port > ".
"$localAddress:$localPort : ".
"use handler $handlerAddress:$localPort\n";
$handler = $this->handlers["$handlerAddress:$localPort"];
// We got a connection fork to manage it
if ($this->nbChild > $this->maxChild)
{
printf ("Too much child process %d : Abort the connection !!\n",
$this->nbChild);
@stream_socket_shutdown ($client, STREAM_SHUT_RDWR);
continue;
}
$pid = pcntl_fork();
if ($pid == -1)
{
throw new \Exception ("TCPServer can not fork", 500);
}
else if ($pid)
{
// parent process: return to the main loop
$this->nbChild++;
continue;
}
if ($this->debug)
echo "[".posix_getpid ()."] Child start ($address:$port) : ".
($this->nbChild+1)." child active\n";
// Do not stop if the parent is requesting a stop from Ctrl+C
pcntl_signal(SIGINT, SIG_IGN);
$sid = posix_setsid();
// In the child. Will call the handler with the actual tcpserver object
// as parameter
$this->parent = false;
$this->socket = $client;
if (is_array ($handler))
{
$object = $handler[0];
$method = $handler[1];
$object->$method ($this);
}
else
{
$function = $handler;
$function ($this);
}
if ($this->debug)
echo "[".posix_getpid ()."] Child ended ($address:$port)\n";
return;
}
}
}
// }}}
/** Request the loop to stop. Will not allow new connections, but wait the
* end of the existing processus
* Block until all is closed
*/
public function loopStop ()
// {{{
{
$this->loopStop = true;
}
// }}}
/** Start the main loop in background and do not wait its end
* @return the PID of the child
*/
public function loopInBackground ()
// {{{
{
$pid = pcntl_fork();
if ($pid == -1)
{
throw new \Exception ("TCPServer can not fork in background", 500);
}
else if ($pid)
{
// parent process: return to the main loop
return $pid;
}
echo "CHILD";
$sid = posix_setsid();
// Will catch all the text messages from the application to not crash if
// there is an "echo"
ob_start ();
@fclose (STDIN);
@fclose (STDOUT);
@fclose (STDERR);
echo "IN";
$this->loop ();
return;
}
// }}}
/** In child, get the socket to direct access
* @return resource The socket with the client
*/
public function getSock ()
// {{{
{
if ($this->parent === true)
throw new \Exception ("Can not return the socket in parent mode", 500);
if ($this->socket === null)
throw new \Exception ("Can not send to client : not connected", 500);
return $this->socket;
}
// }}}
/** Get an array with the peer address, peer port, local address and local
* port
* @return array array ("peer address", peer port, "local address", local
* port)
*/
public function getInfo ()
// {{{
{
if ($this->parent === true)
throw new \Exception ("Can not get info in parent mode", 500);
if ($this->socket === null)
throw new \Exception ("Can not send to client : not connected", 500);
$name = stream_socket_get_name ($this->socket, false);
$pos = strrpos ($name, ":");
$localPort = substr ($name, $pos+1);
$localAddress = substr ($name, 0, $pos);
$name = stream_socket_get_name ($this->socket, true);
$pos = strrpos ($name, ":");
$port = substr ($name, $pos+1);
$address = substr ($name, 0, $pos);
if (substr ($address, 0, 8) === "[::ffff:")
$address = substr ($address, 8, -1);
if (substr ($localAddress, 0, 7) === "[::ffff:")
$localAddress = substr ($localAddress, 8, -1);
return array ($address, $port, $localAddress, $localPort);
}
// }}}
/** Activate the SSL connection
* Put the socket in blocking mode, as it is mandatory to have SSL connection
* @param boolean $val True to activate, false to disable SSL
* @param integer $cryptoMethod The cryptoMethod allowed
*/
public function cryptoEnable ($val,
$cryptoMethod = STREAM_CRYPTO_METHOD_TLS_SERVER)
// {{{
{
if ($this->socket === null)
throw new \Exception ("Can not send to server $this->ipOrName : ".
"The server is not connected", 500);
// Setting the options allow the IP to be decided by the connect and valid
// the certificate of the server by the name
$options = array ("ssl" => array (
"verify_peer_name" => false,
));
stream_set_blocking ($this->socket, true);
stream_context_set_option ($this->socket, $options);
return @stream_socket_enable_crypto ($this->socket, !!$val, $cryptoMethod);
}
// }}}
/** Set context SSL option.
* @param array $options The ssl array to set
*/
public function setSSLOptions ($options)
// {{{
{
if ($this->socket === null)
throw new \Exception ("Can not send to server $this->ipOrName : ".
"The server is not connected", 500);
return stream_context_set_option ($this->socket, array ("ssl" => $options));
}
// }}}
/** Send data to the client
* @param mixed $data The data to send
* @return the length of data sent
*/
public function send ($data)
// {{{
{
if ($this->parent === true)
throw new \Exception ("Can not send data in parent mode", 500);
if ($this->socket === null)
throw new \Exception ("Can not send to client : not connected", 500);
$length = strlen ($data);
$sentLen = @fwrite ($this->socket, $data);
if ($sentLen < $length)
throw new \Exception ("Can not send data to client", 500);
return $sentLen;
}
// }}}
/** Read the data from the client.
* The connection must be established
* Use the readMode in text or binary (text by default)
* In text mode, the read return when found the first \r or the first \n.
* @param integer $maxLength Limit the length of the data from the server
* @return The content
*/
public function read ($maxLength = 1024)
// {{{
{
if ($this->parent === true)
throw new \Exception ("Can not read data in parent mode", 500);
if ($this->socket === null)
throw new \Exception ("Can not read from client : not connected", 500);
if ($this->readMode === "text")
{
$read = stream_get_line ($this->socket, $maxLength, "\r\n");
if ($read === false)
throw new \Exception ("Can not read from client : ".
error_get_last ()["message"], 500);
}
else
{
$read = @fread ($this->socket, $maxLength);
if ($read === false)
throw new \Exception ("Can not read from client : ".
error_get_last ()["message"], 500);
}
return $read;
}
// }}}
/** Disconnect the socket
*/
public function disconnect ()
// {{{
{
if ($this->parent === true)
throw new \Exception ("Can not send data in parent mode", 500);
if ($this->socket === null)
throw new \Exception ("Can not disconnect client : not connected", 500);
@stream_socket_shutdown ($this->socket, STREAM_SHUT_RDWR);
$this->socket = null;
}
// }}}
/////////////////////////
// PRIVATE METHODS //
/////////////////////////
/** Manage the child stop signal
*/
private function sigCHLD ()
// {{{
{
$this->nbChild --;
if ($this->debug)
echo "[".posix_getpid ()."] One child finished : $this->nbChild childs ".
"remain active\n";
pcntl_wait ($status, WNOHANG);
}
// }}}
/** Manage the term / int signals
* Will catch the stop signal, but the real end will be done when the last
* child will be closed
*/
private function sigTERMINT ()
// {{{
{
if ($this->debug)
echo "[".posix_getpid ()."] Request TERM/INT : Wait for last childs\n";
$this->loopStop ();
}
// }}}
}