Merge branch 'refs/heads/master' into unittests

This commit is contained in:
Chris Boden 2011-11-24 21:03:19 -05:00
commit 4129036356
16 changed files with 154 additions and 77 deletions

View File

@ -1,13 +1,13 @@
#Ratchet
A PHP 5.3 (PSR-0 compliant) application for serving and consuming sockets.
A PHP 5.3 (PSR-0 compliant) component library for serving/consuming sockets and building socket based applications.
Build up your application (like Lego!) through simple interfaces using the decorator and command patterns.
Re-use your application without changing any of its code just by wrapping it in a different protocol.
##WebSockets
* Supports the HyBi-10 and Hixie76 protocol versions (at the same time)
* Tested on Chrome 14, Firefox 7, Safari 5, iOS 4.2
* Tested on Chrome 13 - 15, Firefox 6 - 8, Safari 5, iOS 4.2, iOS 5
##Requirements
@ -15,8 +15,8 @@ Shell access is required and a dedicated (virtual) machine with root access is r
To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access.
Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine.
Cookies from your Apache/Nginx/IIS server will be passed to the socket server, allowing you to identify users.
It's recommended using a database/cache solution to store session data, so it's accessible on both servers.
Cookies from your domain will be passed to the socket server, allowing you to identify users.
It's recommended using a database/cache solution to store session data, so it's accessible on both web and socket servers.
A demonstration of this will be posted (eventually).
See https://github.com/cboden/socket-demos for some out-of-the-box working demos using Ratchet.

View File

@ -2,42 +2,49 @@
namespace Ratchet\Application;
use Ratchet\Resource\Connection;
/**
* This is the interface to build a Ratchet application with
* It impelemtns the decorator and command pattern to build an application stack
*/
interface ApplicationInterface {
/**
* Decorator pattern
* @param Ratchet\ObserverInterface Application to wrap in protocol
* @throws UnexpectedValueException
* @param Ratchet\ApplicationInterface Application to wrap in protocol
*/
public function __construct(ApplicationInterface $app = null);
/**
* When a new connection is opened it will be passed to this method
* @param SocketInterface The socket/connection that just connected to your application
* @param Ratchet\Resource\Connection The socket/connection that just connected to your application
* @return Ratchet\Resource\Command\CommandInterface|null
* @throws Exception
*/
function onOpen(Connection $conn);
/**
* Triggered when a client sends data through the socket
* @param SocketInterface The socket/connection that sent the message to your application
* @param Ratchet\Resource\Connection The socket/connection that sent the message to your application
* @param string The message received
* @return Ratchet\Resource\Command\CommandInterface|null
* @throws Exception
*/
function onMessage(Connection $from, $msg);
/**
* This is called before or after a socket is closed (depends on how it's closed). SendMessage to $conn will not result in an error if it has already been closed.
* @param SocketInterface The socket/connection that is closing/closed
* @param Ratchet\Resource\Connection The socket/connection that is closing/closed
* @return Ratchet\Resource\Command\CommandInterface|null
* @throws Exception
*/
function onClose(Connection $conn);
/**
* If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
* the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
* @param SocketInterface
* @param Ratchet\Resource\Connection
* @param \Exception
* @return Ratchet\Resource\Command\CommandInterface|null
* @throws Exception
*/
function onError(Connection $conn, \Exception $e);
}

View File

@ -21,8 +21,8 @@ class App implements ApplicationInterface {
protected $_connections = array();
/**
* The decorated application to send events to
* @var Ratchet\Application\ApplicationInterface
* Maybe temporary?
*/
protected $_app;
@ -41,18 +41,27 @@ class App implements ApplicationInterface {
$this->_app = $application;
}
/**
* Set the incoming buffer size in bytes
* @param int
* @return App
* @throws InvalidArgumentException If the parameter is less than 1
*/
public function setBufferSize($recv_bytes) {
if ((int)$recv_bytes < 1) {
throw new \InvalidArgumentException('Invalid number of bytes set, must be more than 0');
}
$this->_buffer_size = (int)$recv_bytes;
return $this;
}
/*
* Run the server infinitely
* @param Ratchet\SocketInterface
* @param mixed The address to listen for incoming connections on. "0.0.0.0" to listen from anywhere
* @param int The port to listen to connections on
* @param int The port to listen to connections on (make sure to run as root if < 1000)
* @throws Ratchet\Exception
* @todo Validate address. Use socket_get_option, if AF_INET must be IP, if AF_UNIX must be path
* @todo Consider making the 4kb listener changable
@ -134,6 +143,7 @@ class App implements ApplicationInterface {
public function onOpen(Connection $conn) {
$new_socket = clone $conn->getSocket();
$new_socket->set_nonblock();
$new_connection = new Connection($new_socket);
$this->_resources[] = $new_connection->getSocket()->getResource();

View File

@ -7,6 +7,7 @@ use Ratchet\Resource\Command\Factory;
use Ratchet\Resource\Command\CommandInterface;
use Ratchet\Resource\Command\Action\SendMessage;
use Ratchet\Application\WebSocket\Util\HTTP;
use Ratchet\Application\WebSocket\Version;
/**
* The adapter to handle WebSocket requests/responses
@ -15,6 +16,7 @@ use Ratchet\Application\WebSocket\Util\HTTP;
* @todo Make sure this works both ways (client/server) as stack needs to exist on client for framing
* @todo Learn about closing the socket. A message has to be sent prior to closing - does the message get sent onClose event or CloseConnection command?
* @todo Consider chaning this class to a State Pattern. If a WS App interface is passed use different state for additional methods used
* @todo I think I need to overhaul the architecture of this...more onus should be on the VersionInterfaces in case of changes...let them handle more decisions, not just parsing
*/
class App implements ApplicationInterface, ConfiguratorInterface {
/**
@ -24,6 +26,7 @@ class App implements ApplicationInterface, ConfiguratorInterface {
protected $_app;
/**
* Creates commands/composites instead of calling several classes manually
* @var Ratchet\Resource\Command\Factory
*/
protected $_factory;
@ -47,6 +50,8 @@ class App implements ApplicationInterface, ConfiguratorInterface {
}
/**
* Return the desired socket configuration if hosting a WebSocket server
* This method may be removed
* @return array
*/
public static function getDefaultConfig() {
@ -66,6 +71,10 @@ class App implements ApplicationInterface, ConfiguratorInterface {
$conn->WebSocket->headers = '';
}
/**
* Do handshake, frame/unframe messages coming/going in stack
* @todo This needs some major refactoring
*/
public function onMessage(Connection $from, $msg) {
if (true !== $from->WebSocket->handshake) {
if (!isset($from->WebSocket->version)) {
@ -136,12 +145,17 @@ class App implements ApplicationInterface, ConfiguratorInterface {
return $this->prepareCommand($this->_app->onClose($conn));
}
/**
* @todo Shouldn't I be using prepareCommand() on the return? look into this
*/
public function onError(Connection $conn, \Exception $e) {
return $this->_app->onError($conn, $e);
}
/**
* Incomplete, WebSocket protocol allows client to ask to use a sub-protocol, I'm thinking/wanting to somehow implement this in an application decorated class
* @param string
* @todo Implement or delete...
*/
public function setSubProtocol($name) {
}
@ -153,6 +167,10 @@ class App implements ApplicationInterface, ConfiguratorInterface {
*/
protected function prepareCommand(CommandInterface $command = null) {
if ($command instanceof SendMessage) {
if (!isset($command->getConnection()->WebSocket->version)) { // Client could close connection before handshake complete or invalid handshake
return $command;
}
$version = $command->getConnection()->WebSocket->version;
return $command->setMessage($version->frame($command->getMessage()));
}
@ -167,11 +185,11 @@ class App implements ApplicationInterface, ConfiguratorInterface {
}
/**
* @param array of HTTP headers
* Detect the WebSocket protocol version a client is using based on the HTTP header request
* @param string HTTP handshake request
* @return Version\VersionInterface
* @throws UnderFlowException If we think the entire header message hasn't been buffered yet
* @throws InvalidArgumentException If we can't understand protocol version request
* @todo Can/will add more versions later, but perhaps a chain of responsibility, ask each version if they want to handle the request
*/
protected function getVersion($message) {
if (false === strstr($message, "\r\n\r\n")) { // This CAN fail with Hixie, depending on the TCP buffer in between
@ -180,26 +198,33 @@ class App implements ApplicationInterface, ConfiguratorInterface {
$headers = HTTP::getHeaders($message);
if (isset($headers['Sec-Websocket-Version'])) { // HyBi
if ((int)$headers['Sec-Websocket-Version'] >= 6) {
return $this->versionFactory('HyBi10');
foreach ($this->_versions as $name => $instance) {
if (null !== $instance) {
if ($instance::isProtocol($headers)) {
return $instance;
}
} else {
$ns = __NAMESPACE__ . "\\Version\\{$name}";
if ($ns::isProtocol($headers)) {
$this->_version[$name] = new $ns;
return $this->_version[$name];
}
}
} elseif (isset($headers['Sec-Websocket-Key2'])) { // Hixie
return $this->versionFactory('Hixie76');
}
throw new \InvalidArgumentException('Could not identify WebSocket protocol');
}
/**
* @return Version\VersionInterface
* Disable a version of the WebSocket protocol *cough*Hixie76*cough*
* @param string The name of the version to disable
* @throws InvalidArgumentException If the given version does not exist
*/
protected function versionFactory($version) {
if (null === $this->_versions[$version]) {
$ns = __NAMESPACE__ . "\\Version\\{$version}";
$this->_version[$version] = new $ns;
public function disableVersion($name) {
if (!array_key_exists($name, $this->_versions)) {
throw new \InvalidArgumentException("Version {$name} not found");
}
return $this->_version[$version];
unset($this->_versions[$name]);
}
}

View File

@ -3,6 +3,9 @@ namespace Ratchet\Application\WebSocket\Command\Action;
use Ratchet\Resource\Command\Action\SendMessage;
use Ratchet\Application\ApplicationInterface;
/**
* Not yet implemented/completed
*/
class Disconnect extends SendMessage {
protected $_code = 1000;

View File

@ -3,6 +3,9 @@ namespace Ratchet\Application\WebSocket\Command\Action;
use Ratchet\Resource\Command\ActionTemplate;
use Ratchet\Application\ApplicationInterface;
/**
* Not yet implemented/completed
*/
class Ping extends ActionTemplate {
public function execute(ApplicationInterface $scope = null) {
}

View File

@ -3,6 +3,9 @@ namespace Ratchet\Application\WebSocket\Command\Action;
use Ratchet\Resource\Command\ActionTemplate;
use Ratchet\Application\ApplicationInterface;
/**
* Not yet implemented/completed
*/
class Pong extends ActionTemplate {
public function execute(ApplicationInterface $scope = null) {
}

View File

@ -3,6 +3,7 @@ namespace Ratchet\Application\WebSocket\Util;
/**
* A helper class for handling HTTP requests
* @todo Needs re-write...http_parse_headers is a PECL extension that changes the case to unexpected values
*/
class HTTP {
/**

View File

@ -14,6 +14,10 @@ namespace Ratchet\Application\WebSocket\Version;
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/
class Hixie76 implements VersionInterface {
public static function isProtocol(array $headers) {
return isset($headers['Sec-Websocket-Key2']);
}
/**
* @param string
* @return string

View File

@ -10,6 +10,19 @@ use Ratchet\Application\WebSocket\Util\HTTP;
class HyBi10 implements VersionInterface {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
/**
* @todo When I support later version (that implement extension) change >= 6 to 6 through 10 (or w/e #)
*/
public static function isProtocol(array $headers) {
if (isset($headers['Sec-Websocket-Version'])) {
if ((int)$headers['Sec-Websocket-Version'] >= 6) {
return true;
}
}
return false;
}
/**
* @return array
* I kept this as an array and combined in App for future considerations...easier to add a subprotol as a key value than edit a string

View File

@ -8,6 +8,14 @@ namespace Ratchet\Application\WebSocket\Version;
* The current method names suggest you could create a new message/frame to send, which they can not do
*/
interface VersionInterface {
/**
* Given an HTTP header, determine if this version should handle the protocol
* @param array
* @return bool
* @throws UnderflowException If the protocol thinks the headers are still fragmented
*/
static function isProtocol(array $headers);
/**
* Perform the handshake and return the response headers
* @param string
@ -28,6 +36,7 @@ interface VersionInterface {
/**
* @param string
* @return string
* @todo Change to use other classes, this will be removed eventually
*/
function frame($message);
}

View File

@ -9,6 +9,7 @@ use Ratchet\Application\ApplicationInterface;
*/
interface WebSocketAppInterface extends ApplicationInterface {
/**
* Currently instead of this, I'm setting header in the Connection object passed around...not sure which I like more
* @param string
*/
function setHeaders($headers);

View File

@ -1,41 +0,0 @@
<?php
namespace Ratchet;
/**
* Observable/Observer design pattern interface for handing events on a socket
* @todo Consider an onDisconnect method for a server-side close()'ing of a connection - onClose would be client side close()
* @todo Is this interface needed anymore?
* @deprecated
*/
interface ObserverInterface {
/**
* When a new connection is opened it will be passed to this method
* @param SocketInterface The socket/connection that just connected to your application
* @return Ratchet\Resource\Command\CommandInterface|null
*/
function onOpen(SocketInterface $conn);
/**
* Triggered when a client sends data through the socket
* @param SocketInterface The socket/connection that sent the message to your application
* @param string The message received
* @return Ratchet\Resource\Command\CommandInterface|null
*/
function onMessage(SocketInterface $from, $msg);
/**
* This is called before or after a socket is closed (depends on how it's closed). SendMessage to $conn will not result in an error if it has already been closed.
* @param SocketInterface The socket/connection that is closing/closed
* @return Ratchet\Resource\Command\CommandInterface|null
*/
function onClose(SocketInterface $conn);
/**
* If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
* the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
* @param SocketInterface
* @param \Exception
* @return Ratchet\Resource\Command\CommandInterface|null
*/
function onError(SocketInterface $conn, \Exception $e);
}

View File

@ -9,7 +9,7 @@ use Ratchet\Application\ApplicationInterface;
interface CommandInterface {
/**
* The Server class will call the execution
* @param Ratchet\ObserverInterface Scope to execute the command under
* @param Ratchet\ApplicationInterface Scope to execute the command under
* @return CommandInterface|NULL
*/
function execute(ApplicationInterface $scope = null);

View File

@ -64,7 +64,7 @@ class Socket implements SocketInterface {
$len = strlen($message);
do {
$sent = $this->write($message, 4);
$sent = $this->write($message, $len);
$len -= $sent;
$message = substr($message, $sent);
} while ($len > 0);
@ -72,7 +72,7 @@ class Socket implements SocketInterface {
public function bind($address, $port = 0) {
if (false === @socket_bind($this->getResource(), $address, $port)) {
throw new Exception;
throw new Exception($this);
}
return $this;
@ -85,15 +85,24 @@ class Socket implements SocketInterface {
public function connect($address, $port = 0) {
if (false === @socket_connect($this->getResource(), $address, $port)) {
throw new Exception;
throw new Exception($this);
}
return $this;
}
public function getRemoteAddress() {
$address = $port = '';
if (false === @socket_getpeername($this->getResource(), $address, $port)) {
throw new Exception($this);
}
return $address;
}
public function get_option($level, $optname) {
if (false === ($res = @socket_get_option($this->getResource(), $level, $optname))) {
throw new Exception;
throw new Exception($this);
}
return $res;
@ -101,12 +110,20 @@ class Socket implements SocketInterface {
public function listen($backlog = 0) {
if (false === @socket_listen($this->getResource(), $backlog)) {
throw new Exception;
throw new Exception($this);
}
return $this;
}
public function read($length, $type = PHP_BINARY_READ) {
if (false === ($res = @socket_read($this->getResource(), $length, $type))) {
throw new Exception($this);
}
return $res;
}
/**
* @see http://ca3.php.net/manual/en/function.socket-recv.php
* @param string Variable to write data to
@ -150,7 +167,7 @@ class Socket implements SocketInterface {
public function set_block() {
if (false === @socket_set_block($this->getResource())) {
throw new Exception;
throw new Exception($this);
}
return $this;
@ -158,7 +175,7 @@ class Socket implements SocketInterface {
public function set_nonblock() {
if (false === @socket_set_nonblock($this->getResource())) {
throw new Exception;
throw new Exception($this);
}
return $this;
@ -166,7 +183,7 @@ class Socket implements SocketInterface {
public function set_option($level, $optname, $optval) {
if (false === @socket_set_option($this->getResource(), $level, $optname, $optval)) {
throw new Exception;
throw new Exception($this);
}
return $this;
@ -174,7 +191,7 @@ class Socket implements SocketInterface {
public function shutdown($how = 2) {
if (false === @socket_shutdown($this->getResource(), $how)) {
throw new Exception;
throw new Exception($this);
}
return $this;
@ -182,7 +199,7 @@ class Socket implements SocketInterface {
public function write($buffer, $length = 0) {
if (false === ($res = @socket_write($this->getResource(), $buffer, $length))) {
throw new Exception;
throw new Exception($this);
}
return $res;

View File

@ -34,6 +34,7 @@ interface SocketInterface {
// function accept();
/**
* Bind the socket instance to an address/port
* @param string
* @param int
* @return SocketInterface
@ -47,6 +48,7 @@ interface SocketInterface {
function close();
/**
* Initiates a connection to a socket
* @param string
* @param int
* @return SocketInterface
@ -54,6 +56,13 @@ interface SocketInterface {
*/
function connect($address, $port = 0);
/**
* Get the address the socket connected from
* @return string
* @throws Exception
*/
function getRemoteAddress();
/**
* @param int
* @param int
@ -63,12 +72,22 @@ interface SocketInterface {
function get_option($level, $optname);
/**
* Listen for incoming connections on this socket
* @param int
* @return SocketInterface
* @throws Exception
*/
function listen($backlog = 0);
/**
* Read a maximum of length bytes from a socket
* @param int Number of bytes to read
* @param int Flags
* @return string Data read from the socket
* @throws Exception
*/
function read($length, $type = PHP_BINARY_READ);
/**
* Called when the client sends data to the server through the socket
* @param string Variable to write data to
@ -84,12 +103,15 @@ interface SocketInterface {
// function select(array &$read, array &$write, array &$except, $tv_sec, $tv_usec = 0);
/**
* Sets the blocking mode on the socket resource
* Wen an operation (receive, send, connect, accept, etc) is performed after set_block() the script will pause execution until the operation is completed
* @return SocketInterface
* @throws Exception
*/
function set_block();
/**
* Sets nonblocking mode for socket resource
* @return SocketInterface
* @throws Exception
*/