diff --git a/lib/Ratchet/Protocol/WebSocket.php b/lib/Ratchet/Protocol/WebSocket.php index 2d41df4..ddb80bf 100644 --- a/lib/Ratchet/Protocol/WebSocket.php +++ b/lib/Ratchet/Protocol/WebSocket.php @@ -1,6 +1,13 @@ <?php namespace Ratchet\Protocol; +use Ratchet\Server; +use Ratchet\Server\Client; +use Ratchet\Server\Message; +use Ratchet\Socket; +/** + * @link http://ca.php.net/manual/en/ref.http.php + */ class WebSocket implements ProtocolInterface { /** * @return Array @@ -20,15 +27,18 @@ class WebSocket implements ProtocolInterface { * @return string */ function getName() { - return __CLASS__; + return 'WebSocket'; } - function handleConnect() { + public function setUp(Server $server) { } - function handleMessage() { + function handleConnect(Socket $client) { } - function handleClose() { + function handleMessage($message, Socket $from) { + } + + function handleClose(Socket $client) { } } \ No newline at end of file diff --git a/lib/Ratchet/ReceiverInterface.php b/lib/Ratchet/ReceiverInterface.php index feadd46..8f2d3b3 100644 --- a/lib/Ratchet/ReceiverInterface.php +++ b/lib/Ratchet/ReceiverInterface.php @@ -1,5 +1,8 @@ <?php namespace Ratchet; +use Ratchet\Server; +use Ratchet\Server\Client; +use Ratchet\Server\Message; interface ReceiverInterface { /** @@ -7,9 +10,11 @@ interface ReceiverInterface { */ function getName(); - function handleConnect(); + function setUp(Server $server); - function handleMessage(); + function handleConnect(Socket $client); - function handleClose(); + function handleMessage($message, Socket $from); + + function handleClose(Socket $client); } \ No newline at end of file diff --git a/lib/Ratchet/Server.php b/lib/Ratchet/Server.php index 570f4da..c8de64b 100644 --- a/lib/Ratchet/Server.php +++ b/lib/Ratchet/Server.php @@ -1,56 +1,124 @@ <?php namespace Ratchet; +use Ratchet\Server\Aggregator; use Ratchet\Protocol\ProtocolInterface; class Server implements ServerInterface { + /** + * The master socket, receives all connections + * @type Socket + */ protected $_master = null; - protected $_debug = false; - protected $_receivers = Array(); - protected $_connections = Array(); + /** + * @todo This needs to implement the composite pattern + * @var array of ReceiverInterface + */ + protected $_receivers = array(); + + protected $_resources = array(); + + /** + * @type array of Ratchet\Server\Client + */ + protected $_connections = array(); /** * @param Ratchet\Socket * @param boolean True, enables debug mode and the server doesn't infiniate loop */ - public function __construct(Socket $socket, $debug = false) { - $this->_master = $socket; - $this->_debug = (boolean)$debug; + public function __construct(Socket $host) { + $this->_master = $host; + + $socket = $host->getResource(); + + $this->_resources[] = $socket; + $this->_connections[$socket] = $host; + } + + /** + * @todo Receive an interface that creates clients based on interface, decorator pattern for Socket + */ + public function setClientFactory($s) { } public function attatchReceiver(ReceiverInterface $receiver) { + $receiver->setUp($this); $this->_receivers[spl_object_hash($receiver)] = $receiver; } + public function getMaster() { + return $this->_master; + } + + public function getClients() { + return $this->_connections; + } + /* * @param mixed * @param int - * @throws Ratchet\Exception + * @throws Exception + * @todo Validate address. Use socket_get_option, if AF_INET must be IP, if AF_UNIX must be path */ public function run($address = '127.0.0.1', $port = 1025) { if (count($this->_receivers) == 0) { - throw new \RuntimeException("No receiver has been attatched to the server"); + throw new \RuntimeException("No receiver has been attached to the server"); } set_time_limit(0); ob_implicit_flush(); +// socket_create_listen($port); instead of create, bind, listen + if (false === ($this->_master->bind($address, (int)$port))) { // perhaps I should do some checks here... - throw new Exception(); + throw new Exception; } if (false === ($this->_master->listen())) { - throw new Exception(); + throw new Exception; } do { - $changed = $this->_connections; - $num_changed = socket_select($changed_sockets, $write = NULL, $except = NULL, NULL); -// foreach($changed as $) + $changed = $this->_resources; + $num_changed = @socket_select($changed, $write = NULL, $except = NULL, NULL); + foreach($changed as $resource) { + if ($this->_master->getResource() == $resource) { + $new_connection = clone $this->_master; + $this->_resources[] = $new_connection->getResource(); + $this->_connections[$new_connection->getResource()] = $new_connection; - } while (!$this->_debug); + // /here $this->_receivers->handleConnection($new_connection); + $this->tmpRIterator('handleConnect', $new_connection); + } else { + $conn = $this->_connections[$resource]; + $data = null; + $bytes = $conn->recv($data, 4096, 0); + + if ($bytes == 0) { + $this->tmpRIterator('handleClose', $conn); + // $this->_receivers->handleDisconnect($conn); + + unset($this->_connections[$resource]); + unset($this->_resources[array_search($resource, $this->_resources)]); + } else { + $this->tmpRIterator('handleMessage', $data, $conn); + // new Message + // $this->_receivers->handleMessage($msg, $conn); + } + } + } + } while (true); // $this->_master->set_nonblock(); // declare(ticks = 1); } + + protected function tmpRIterator() { + $args = func_get_args(); + $fn = array_shift($args); + foreach ($this->_receivers as $app) { + call_user_func_array(array($app, $fn), $args); + } + } } \ No newline at end of file diff --git a/lib/Ratchet/Server/Aggregator.php b/lib/Ratchet/Server/Aggregator.php new file mode 100644 index 0000000..1396701 --- /dev/null +++ b/lib/Ratchet/Server/Aggregator.php @@ -0,0 +1,69 @@ +<?php +namespace Ratchet\Server; +use Ratchet\Socket; +use Ratchet\Exception; + +class Aggregator implements \IteratorAggregator { + /** + * @var Ratchet\Socket + */ + protected $_master; + + /** + * @var SplObjectStorage + */ + protected $_sockets; + + protected $_resources = array(); + + /** + * @param Ratchet\Socket + * @throws Ratchet\Exception + */ + public function __construct(Socket $master) { + $this->_sockets = new \SplObjectStorage; + + $this->_master = $master; + $this->insert($this->_master); + } + + /** + * @return Socket + */ + public function getMaster() { + return $this->_master; + } + + /** + * @param resource + * @return Socket + */ + public function getClientByResource($resource) { + if ($this->_sockets->contains($resource)) { + return $this->_sockets[$resource]; + } + + throw new Exception("Resource not found"); + } + + protected function insert(Socket $socket) { + $resource = $socket->getResource(); + + $this->_sockets[$socket] = $resource; + $this->_resources[] = $resource; + } + + /** + * @return SplObjectStorage + */ + public function getIterator() { + return $this->_sockets; + } + + /** + * @return array + */ + public function asArray() { + return $this->_resources; + } +} \ No newline at end of file diff --git a/lib/Ratchet/Server/Client.php b/lib/Ratchet/Server/Client.php new file mode 100644 index 0000000..2bfcc6e --- /dev/null +++ b/lib/Ratchet/Server/Client.php @@ -0,0 +1,13 @@ +<?php +namespace Ratchet\Server; +use Ratchet\Socket; + +class Client { + protected $_socket; + + public function __construct(Socket $socket) { + $this->_socket = $socket; + } + + +} \ No newline at end of file diff --git a/lib/Ratchet/Socket.php b/lib/Ratchet/Socket.php index 263bf9a..9b01e89 100644 --- a/lib/Ratchet/Socket.php +++ b/lib/Ratchet/Socket.php @@ -10,7 +10,7 @@ class Socket { /** * @type resource */ - public $_resource; + protected $_resource; public static $_defaults = Array( 'domain' => AF_INET @@ -34,6 +34,17 @@ class Socket { } } + /** + * @return resource (Socket) + */ + public function getResource() { + return $this->_resource; + } + + public function __clone() { + $this->_resource = @socket_accept($this->_resource); + } + /** * @param Ratchet\Protocol\ProtocolInterface * @return Ratchet\Socket @@ -70,7 +81,7 @@ class Socket { } } - return Array($domain, $type, $protocol); + return array($domain, $type, $protocol); } /** @@ -88,7 +99,17 @@ class Socket { $write = static::mungForSelect($write); $except = static::mungForSelect($except); - socket_select($read, $write, $except, $tv_sec, $tv_usec); + return socket_select($read, $write, $except, $tv_sec, $tv_usec); + } + + /** + * @param string + * @param int + * @param int + * @return int + */ + public function recv(&$buf, $len, $flags) { + return socket_recv($this->_resource, $buf, $len, $flags); } /** @@ -105,9 +126,9 @@ class Socket { throw new \InvalidArgumentException('Object pass is not traversable'); } - $return = Array(); + $return = array(); foreach ($collection as $key => $socket) { - $return[$key] = ($socket instanceof \Ratchet\Socket ? $socket->_resource : $socket); + $return[$key] = ($socket instanceof \Ratchet\Socket ? $socket->getResource() : $socket); } return $return; @@ -118,12 +139,19 @@ class Socket { * @param string * @param Array * @return mixed + * @throws Exception * @throws \BadMethodCallException */ public function __call($method, $arguments) { if (function_exists('socket_' . $method)) { array_unshift($arguments, $this->_resource); - return call_user_func_array('socket_' . $method, $arguments); + $result = @call_user_func_array('socket_' . $method, $arguments); + + if (false === $result) { + throw new Exception; + } + + return $result; } throw new \BadMethodCallException("{$method} is not a valid socket function"); diff --git a/tests/Ratchet/Tests/Mock/Application.php b/tests/Ratchet/Tests/Mock/Application.php index 08e8c79..7848b37 100644 --- a/tests/Ratchet/Tests/Mock/Application.php +++ b/tests/Ratchet/Tests/Mock/Application.php @@ -1,18 +1,24 @@ <?php namespace Ratchet\Tests\Mock; use Ratchet\ReceiverInterface; +use Ratchet\Server; +use Ratchet\Tests\Mock\Socket as MockSocket; +use Ratchet\Socket; class Application implements ReceiverInterface { public function getName() { return 'mock_application'; } - public function handleConnect() { + public function setUp(Server $server) { } - public function handleMessage() { + public function handleConnect(Socket $client) { } - public function handleClose() { + public function handleMessage($msg, Socket $from) { + } + + public function handleClose(Socket $client) { } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/Mock/Socket.php b/tests/Ratchet/Tests/Mock/FakeSocket.php similarity index 96% rename from tests/Ratchet/Tests/Mock/Socket.php rename to tests/Ratchet/Tests/Mock/FakeSocket.php index 9cf8eea..51aa973 100644 --- a/tests/Ratchet/Tests/Mock/Socket.php +++ b/tests/Ratchet/Tests/Mock/FakeSocket.php @@ -2,7 +2,7 @@ namespace Ratchet\Tests\Mock; use Ratchet\Socket as RealSocket; -class Socket extends RealSocket { +class FakeSocket extends RealSocket { protected $_arguments = Array(); protected $_options = Array(); diff --git a/tests/Ratchet/Tests/Mock/Protocol.php b/tests/Ratchet/Tests/Mock/Protocol.php index 636ee0b..5c7a673 100644 --- a/tests/Ratchet/Tests/Mock/Protocol.php +++ b/tests/Ratchet/Tests/Mock/Protocol.php @@ -1,6 +1,8 @@ <?php namespace Ratchet\Tests\Mock; use Ratchet\Protocol\ProtocolInterface; +use Ratchet\Server; +use Ratchet\Socket; class Protocol implements ProtocolInterface { public static function getDefaultConfig() { @@ -18,12 +20,15 @@ class Protocol implements ProtocolInterface { return 'mock_protocol'; } - public function handleConnect() { + public function setUp(Server $server) { } - public function handleMessage() { + public function handleConnect(Socket $client) { } - public function handleClose() { + public function handleMessage($msg, Socket $client) { + } + + public function handleClose(Socket $client) { } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/Mock/SocketAggregator.php b/tests/Ratchet/Tests/Mock/SocketAggregator.php new file mode 100644 index 0000000..c9488f1 --- /dev/null +++ b/tests/Ratchet/Tests/Mock/SocketAggregator.php @@ -0,0 +1,42 @@ +<?php +namespace Ratchet\Tests\Mock; +use Ratchet\SocketAggregator as RealSocketAggregator; + +class SocketAggregator extends RealSocketAggregator { + protected $_arguments = Array(); + protected $_options = Array(); + + public function __construct($domain = null, $type = null, $protocol = null) { + list($this->_arguments['domain'], $this->_arguments['type'], $this->_arguments['protocol']) = static::getConfig($domain, $type, $protocol); + } + + public function accept() { + } + + public function bind($address, $port) { + } + + public function close() { + } + + public function get_option($level, $optname) { + return $this->_options[$level][$optname]; + } + + public function listen($backlog) { + } + + public function recv($buf, $len, $flags) { + } + + public function set_option($level, $optname, $optval) { + if (!isset($this->_options[$level])) { + $this->_options[$level] = Array(); + } + + $this->_options[$level][$optname] = $optval; + } + + public function write($buffer, $length = 0) { + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/ServerTest.php b/tests/Ratchet/Tests/ServerTest.php index b7d193e..d1fe6a5 100644 --- a/tests/Ratchet/Tests/ServerTest.php +++ b/tests/Ratchet/Tests/ServerTest.php @@ -1,7 +1,7 @@ <?php namespace Ratchet\Tests; use Ratchet\Server; -use Ratchet\Tests\Mock\Socket; +use Ratchet\Tests\Mock\FakeSocket as Socket; use Ratchet\Tests\Mock\Application as TestApp; /** diff --git a/tests/Ratchet/Tests/SocketAggregatorTest.php b/tests/Ratchet/Tests/SocketAggregatorTest.php new file mode 100644 index 0000000..2f3fce0 --- /dev/null +++ b/tests/Ratchet/Tests/SocketAggregatorTest.php @@ -0,0 +1,94 @@ +<?php +namespace Ratchet\Tests; +use Ratchet\Tests\Mock\FakeSocket as Socket; +use Ratchet\Socket as RealSocket; +use Ratchet\Tests\Mock\Protocol; + +/** + * @covers Ratchet\SocketAggregator + */ +class SocketAggregatorTest extends \PHPUnit_Framework_TestCase { + protected $_socket; + + protected static function getMethod($name) { + $class = new \ReflectionClass('\\Ratchet\\Tests\\Mock\\Socket'); + $method = $class->getMethod($name); + $method->setAccessible(true); + + return $method; + } + + public function setUp() { + $this->_socket = new Socket(); + } + +/* + public function testWhatGoesInConstructComesOut() { + $this->assertTrue(false); + } +*/ + + public function testGetDefaultConfigForConstruct() { + $ref_conf = static::getMethod('getConfig'); + $config = $ref_conf->invokeArgs($this->_socket, Array()); + + $this->assertEquals(array_values(Socket::$_defaults), $config); + } + + public function testInvalidConstructorArguments() { + $this->setExpectedException('\\Ratchet\\Exception'); + $socket = new RealSocket('invalid', 'param', 'derp'); + } + + public function testConstructAndCallByOpenAndClose() { + $socket = new RealSocket(); + $socket->close(); + } + + public function testInvalidSocketCall() { + $this->setExpectedException('\\BadMethodCallException'); + $this->_socket->fake_method(); + } + + public function testConstructionFromProtocolInterfaceConfig() { + $protocol = new Protocol(); + $socket = Socket::createFromConfig($protocol); + + $this->assertInstanceOf('\\Ratchet\\Socket', $socket); + } + + public function testCreationFromConfigOutputMatchesInput() { + $protocol = new Protocol(); + $socket = Socket::createFromConfig($protocol); + $config = $protocol::getDefaultConfig(); + + // change this to array_filter late + unset($config['options']); + + $this->assertAttributeEquals($config, '_arguments', $socket); + } + + public function asArrayProvider() { + return Array( + Array(Array('hello' => 'world'), Array('hello' => 'world')) + , Array(null, null) + , Array(Array('hello' => 'world'), new \ArrayObject(Array('hello' => 'world'))) + ); + } + + /** + * @dataProvider asArrayProvider + */ + public function testMethodMungforselectReturnsExpectedValues($output, $input) { + $method = static::getMethod('mungForSelect'); + $return = $method->invokeArgs($this->_socket, Array($input)); + + $this->assertEquals($return, $output); + } + + public function testMethodMungforselectRejectsNonTraversable() { + $this->setExpectedException('\\InvalidArgumentException'); + $method = static::getMethod('mungForSelect'); + $method->invokeArgs($this->_socket, Array('I am upset with PHP ATM')); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/SocketTest.php b/tests/Ratchet/Tests/SocketTest.php index e9f9e9e..cce6619 100644 --- a/tests/Ratchet/Tests/SocketTest.php +++ b/tests/Ratchet/Tests/SocketTest.php @@ -1,6 +1,6 @@ <?php namespace Ratchet\Tests; -use Ratchet\Tests\Mock\Socket; +use Ratchet\Tests\Mock\FakeSocket as Socket; use Ratchet\Socket as RealSocket; use Ratchet\Tests\Mock\Protocol;