<?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));
}
}
}
}