Location: PHPKode > scripts > Socket Server > socket-server/SocketServer.php
<?php
/**
 * SocketServer
 *
 * This file is part of SocketServer.
 *
 * SocketServer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * SocketServer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with SocketServer. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package SocketServer
 * @copyright Copyright (c) 2009 Aleksey V. Zapparov AKA ixti <http://ixti.ru/>
 * @license http://www.gnu.org/licenses/ GPLv3
 */


/**
 * @package SocketServer
 * @link http://blog.ixti.ru/?p=107 Example usage step by step
 * @copyright Copyright (c) 2009 Aleksey V. Zapparov AKA ixti <http://ixti.ru/>
 * @license http://www.gnu.org/licenses/ GPLv3
 */
class SocketServer
{
    /**
     * Socket resourece created by {@link socket_create()}
     *
     * @see socket_create()
     * @var resource
     */
    private $__socket;


    /**
     * Tells whenever {@link $__socket} is binded or not.
     *
     * @see SocketServer::bind()
     * @var boolean
     */
    private $__isBinded = false;


    /**
     * Handler function of incoming requests. Returned value will be sent client
     * as response message.
     *
     * @see SocketServer::setHandler()
     * @var mixed
     */
    private $__handler = null;


    /**
     * Welome message to be displayed to new clients.
     *
     * @var string|null
     */
    private $__motd = null;



    /**
     * Class constructor.
     *
     * Creates a socket resource. Simple wraper of {@link socket_create()},
     * which creates a resource and keep it as private property.
     *
     * Example:
     * <code>
     * $protocol = getprotobyname('udp');
     * $server   = new SocketServer(AF_INET, SOCK_DGRAM, $protocol);
     * </code>
     *
     * Please reffer to {@link socket_create()} manual for more details, as this
     * is just a wrapper of that function.
     *
     * @see socket_create()
     * @throws Exception If {@link socket_create()} failed
     * @param integer $domain   Protocol family to be used by the socket.
     * @param integer $type     Type of communication to be used by the socket.
     * @param integer $protocol Protocol within the specified $domain.
     */
    public function __construct($domain, $type, $protocol)
    {
        $this->__socket = @socket_create($domain, $type, $protocol);

        if (false === $this->__socket) {
            $this->__raiseError();
        }
    }


    /**
     * Class destructor
     *
     * Close socket if it was created.
     *
     * @see socket_close()
     */
    public function  __destruct()
    {
        @socket_close($this->__socket);
    }


    /**
     * Set welcome message for new clients.
     *
     * @param string|null $msg
     * @return SocketServer self-reference
     */
    public function setMotd($msg)
    {
        $msg = trim($msg);
        $this->__motd = (0 !== $msg) ? PHP_EOL . $msg . PHP_EOL : null;
        return $this;
    }


    /**
     * Throws {@link Exception} with last socket error or specified message.
     *
     * Close socket, if it was opened and then throws {@link Exception}. If
     * $msg is not specified or NULL, last sockt error will be used as message.
     *
     * @throws Exception
     * @param string $msg (optional)
     */
    private function __raiseError($msg = null)
    {
        if (null === $msg) {
            $msg = socket_strerror(socket_last_error());
        }
        
        throw new Exception($msg);
    }


    /**
     * Binds a name to a socket.
     *
     * Binds the name given in $address to the socket. This has to be done
     * before starting server with {@link SocketServer::run()}.
     *
     * @link socket_bind()
     * @throws Exception If {@link socket_bind()} failed
     * @param string $address Address name to be binded to socket.
     *        - If the socket is of the AF_INET family, the address is an IP in
     *          dotted-quad notation (e.g. 127.0.0.1).
     *        - If the socket is of the AF_UNIX family, the address is the path
     *          of a Unix-domain socket (e.g. /tmp/my.sock).
     * @param integer $port (optional) The port parameter is only used when
     *        connecting to an AF_INET socket, and designates the port on the
     *        remote host to which a connection should be made.
     * @return SocketServer self-reference
     */
    public function bind($address, $port = null)
    {
        if (false === @socket_bind($this->__socket, $address, $port)) {
            $this->__raiseError();
        }

        $this->__isBinded = true;
        return $this;
    }


    /**
     * Registers request handler.
     *
     * $handler will be called with passing request as the only argument.
     *
     * - Client will be disconnected upon $handler will return NULL.
     * - Server will be stopped upon $handler will return boolean false.
     * - Else returned value will be sent as a response.
     *
     * @throws Exception If specified $handler can't be called
     * @param mixed $handler Request handler function or method. Can be either
     *        the name of a function stored in a string variable, or an object
     *        and the name of a method within the object, like this:
     *         array($SomeObject, 'MethodName')
     * @return SocketServer self-reference
     */
    public function setHandler($handler)
    {
        if ( ! is_callable($handler)) {
            $this->__raiseError('Handler is not callable.');
        }

        $this->__handler = $handler;
        return $this;
    }


    /**
     * Run server.
     *
     * Calls {@link socket_listen()} and then run main daemon loop. Please refer
     * to {@link socket_listen()} about $backlog argument.
     *
     * Bind socket with {@link SocketServer::bind()} method and set request's
     * handler with {@link SocketServer::setHandler()} before running a server.
     *
     * @see SocketServer::bind()
     * @see SocketServer::setHandler()
     * @throws Exception If {@link $__handler} was not set
     * @throws Exception If socket was not binded
     * @throws Exception If {@link socket_listen()} failed
     * @param integer $backlog (optional) A maximum of incoming connections.
     * @return void
     */
    public function run($backlog = null)
    {
        if (null === $this->__handler) {
            $this->__raiseError('Handler must be set first');
        }

        if (false === $this->__isBinded) {
            $this->__raiseError('Socket must be binded first');
        }

        if (false === @socket_listen($this->__socket, $backlog)) {
            $this->__raiseError();
        }

        $this->__run();
    }


    /**
     * Server's main loop.
     *
     * Taken from first version as it was described on my blog and leaved almost
     * untouched :))
     *
     * @link http://blog.ixti.ru/?p=105 Socket reader in PHP
     */
    private function __run()
    {
        // Client connections' pool
        $pool = array($this->__socket);

        // Main cycle
        while (is_resource($this->__socket)) {
            // Clean-up pool
            foreach ($pool as $conn_id => $conn) {
                if ( ! is_resource($conn)) {
                    unset($pool[$conn_id]);
                }
            }

            // Create a copy of pool for socket_select()
            $active = $pool;

            // Halt execution if socket_select() failed
            if (false === socket_select($active, $w = null, $e = null, null)) {
                $this->__raiseError();
            }

            // Register new client in the pool
            if (in_array($this->__socket, $active)) {
                $conn = socket_accept($this->__socket);
                if (is_resource($conn)) {
                    if (null !== $this->__motd) {
                        // Send welcome message
                        socket_write($conn, $this->__motd, strlen($this->__motd));
                    }
                    $pool[] = $conn;
                }
                unset($active[array_search($sock, $active)]);
            }

            // Handle every active client
            foreach ($active as $conn) {
                $request = socket_read($conn, 2048, PHP_NORMAL_READ);

                // If connection is closed, remove it from pool and skip
                if (false === $request) {
                    unset($pool[array_search($conn, $pool)]);
                    continue;
                }

                $request  = trim($request);

                // Skip to next if client tells nothing
                if (0 == strlen($request)) {
                    continue;
                }

                $response = call_user_func($this->__handler, $request);
                
                if (null === $response) {
                    socket_close($conn);
                    unset($pool[array_search($conn, $pool)]);
                    continue;
                }

                if (false === $response) {
                    $this->__destruct();
                }
                
                socket_write($conn, $response, strlen($response));
            }
        }
    }
}
Return current item: Socket Server