diff --git a/tcpserver.php b/tcpserver.php index 70b10ea..d925761 100644 --- a/tcpserver.php +++ b/tcpserver.php @@ -18,6 +18,13 @@ */ 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 (); @@ -43,22 +50,35 @@ class tcpserver /** 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; @@ -68,6 +88,7 @@ class tcpserver $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 @@ -77,40 +98,64 @@ class tcpserver * 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 + /** 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)) - $sockServer[$key] = stream_socket_server ("tcp://$address:$port", - $errno, $errstr); + $address = "$address"; elseif (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) - $sockServer[$key] = stream_socket_server ("tcp://[$address]:$port", - $errno, $errstr); + $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"); } - //printf ("Listening on %s:%d...\n", $address, $port); + if ($this->debug) + printf ("[".posix_getpid ()."] Listening on %s:%d...\n", + $address, $port); } - $nbChild = 1; while (1) { - // Watch if a child process is zombie, then destroy it - if (pcntl_wait ($status, WNOHANG) > 0) - $nbChild--; + 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; @@ -137,24 +182,34 @@ class tcpserver { // If the address of the handler doesn't exists, the socket is maybe // waiting on all the interfaces addresses - if (filter_var ($localAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) + $handlerAddress = str_replace (["[", "]"], ["",""], $handlerAddress); + if (filter_var ($handlerAddress, FILTER_VALIDATE_IP, + FILTER_FLAG_IPV4)) $handlerAddress = "0.0.0.0"; - elseif (filter_var ($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) + 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", $localAddress, + printf ("Can't find the handler for %s:%d\n", $handlerAddress, $localPort); continue; } } - //echo "Enter $address:$port > $localAddress:$localPort\n"; + 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 ($nbChild > $this->maxChild) + if ($this->nbChild > $this->maxChild) { printf ("Too much child process %d : Abort the connection !!\n", - $nbChild); + $this->nbChild); @stream_socket_shutdown ($client, STREAM_SHUT_RDWR); continue; } @@ -166,10 +221,16 @@ class tcpserver else if ($pid) { // parent process: return to the main loop - $nbChild++; + $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; @@ -185,15 +246,60 @@ class tcpserver $function = $handler; $function ($this); } - exit; + 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); @@ -201,6 +307,7 @@ class tcpserver 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 @@ -208,6 +315,7 @@ class tcpserver * port) */ public function getInfo () + // {{{ { if ($this->parent === true) throw new \Exception ("Can not get info in parent mode", 500); @@ -221,8 +329,13 @@ class tcpserver $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 @@ -231,6 +344,7 @@ class tcpserver */ public function cryptoEnable ($val, $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_SERVER) + // {{{ { if ($this->socket === null) throw new \Exception ("Can not send to server $this->ipOrName : ". @@ -244,23 +358,27 @@ class tcpserver 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); @@ -272,6 +390,7 @@ class tcpserver throw new \Exception ("Can not send data to client", 500); return $sentLen; } + // }}} /** Read the data from the client. * The connection must be established @@ -281,6 +400,7 @@ class tcpserver * @return The content */ public function read ($maxLength = 1024) + // {{{ { if ($this->parent === true) throw new \Exception ("Can not read data in parent mode", 500); @@ -302,10 +422,12 @@ class tcpserver } return $read; } + // }}} - /** Disconnect the socket + /** Disconnect the socket */ public function disconnect () + // {{{ { if ($this->parent === true) throw new \Exception ("Can not send data in parent mode", 500); @@ -314,4 +436,34 @@ class tcpserver @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 (); + } + // }}} }