*/ /** 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 messages on screen */ private $debug = false; /** 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 (); } // }}} }