Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

salut SocketServer

  • Loading branch information...
commit bbe0956f13356aa7dadb6a1f7a907b06c78f311b 0 parents
@igorw authored
1  .gitignore
@@ -0,0 +1 @@
+vendor
11 .travis.yml
@@ -0,0 +1,11 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+
+before_script:
+ - curl -s http://getcomposer.org/installer | php
+ - php composer.phar install
+
+script: phpunit --coverage-text
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Igor Wiedler
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
82 README.md
@@ -0,0 +1,82 @@
+# SocketServer
+
+Stream-powered library for creating a socket server in PHP.
+
+[![Build Status](https://secure.travis-ci.org/igorw/SocketServer.png)](http://travis-ci.org/igorw/SocketServer)
+
+## Install
+
+The recommended way to install SocketServer is [through composer](http://getcomposer.org).
+
+```JSON
+{
+ "require": {
+ "igorw/socket-server": "dev-master"
+ }
+}
+```
+
+## Usage
+
+### Events
+
+The `Igorw\SocketServer\Server` class extends [événement](https://github.com/igorw/evenement)
+and allows you to bind to events.
+
+* `connection`: Triggered whenever a new client connects to the server. Arguments: $conn.
+* `data`: Triggered whenever a client sends data. Arguments: $data, $conn.
+* `disconnect`: Triggered whenever a client disconnects. Arguments: $conn.
+
+### Input
+
+In order to communicate with the server, you can pass an input stream in
+the constructor and bind to the `input` event. This allows you to trigger
+custom events and run custom code when they happen.
+
+### Running
+
+The `run` method will start the event loop. The server will process connections,
+data and input until it dies.
+
+### Example
+
+Here is an example of a simple HTTP server listening on port 8000:
+
+ use Igorw\SocketServer\Server;
+
+ $server = new Server('localhost', 8000);
+
+ $i = 1;
+
+ $server->on('data', function ($data, $conn) use (&$i) {
+ $lines = explode("\r\n", $data);
+ $requestLine = reset($lines);
+
+ if ('GET /favicon.ico HTTP/1.1' === $requestLine) {
+ $response = '';
+ $length = 0;
+ } else {
+ $response = "This is request number $i.\n";
+ $length = strlen($response);
+ $i++;
+ }
+
+ $conn->write("HTTP 1.1 200 OK\r\n");
+ $conn->write("Content-Type: text/html\r\n");
+ $conn->write("Content-Length: $length\r\n");
+ $conn->write("\r\n");
+ $conn->write($response);
+ $conn->close();
+ });
+
+ $server->run();
+
+## Tests
+
+To run the test suite, you need PHPUnit.
+
+ $ phpunit
+
+## License
+
+MIT, see LICENSE.
15 composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "igorw/socket-server",
+ "description": "Stream-powered library for creating a socket server in PHP.",
+ "keywords": ["socket", "stream", "server"],
+ "license": "MIT",
+ "require": {
+ "php": ">=5.3.2",
+ "evenement/evenement": "dev-master"
+ },
+ "autoload": {
+ "psr-0": {
+ "Igorw\\SocketServer": "src"
+ }
+ }
+}
21 composer.lock
@@ -0,0 +1,21 @@
+{
+ "hash": "9da82ee679c8227d310caacf30874b86",
+ "packages": [
+ {
+ "package": "evenement/evenement",
+ "version": "dev-master",
+ "source-reference": "e9bac9b3cc29bf807c55b26a973090ad7f1eb7e5"
+ },
+ {
+ "package": "igorw/event-source",
+ "version": "dev-master",
+ "source-reference": "55c4f0a20525e834a94c3a277ea794c05d30a4c7"
+ },
+ {
+ "package": "predis/predis",
+ "version": "dev-master",
+ "source-reference": "9426d78b9091a5d6840f25ea7b86ff9bc59bacb0"
+ }
+ ],
+ "aliases": []
+}
25 phpunit.xml.dist
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="tests/bootstrap.php"
+>
+ <testsuites>
+ <testsuite name="SocketServer Test Suite">
+ <directory>./tests/Igorw/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./src/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
25 src/Igorw/SocketServer/Connection.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Igorw\SocketServer;
+
+class Connection
+{
+ private $socket;
+ private $server;
+
+ public function __construct($socket, $server)
+ {
+ $this->socket = $socket;
+ $this->server = $server;
+ }
+
+ public function write($data)
+ {
+ fwrite($this->socket, $data);
+ }
+
+ public function close()
+ {
+ $this->server->close($this->socket);
+ }
+}
7 src/Igorw/SocketServer/ConnectionException.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Igorw\SocketServer;
+
+class ConnectionException extends \ErrorException
+{
+}
136 src/Igorw/SocketServer/Server.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace Igorw\SocketServer;
+
+use Evenement\EventEmitter;
+
+class Server extends EventEmitter
+{
+ private $master;
+ private $input;
+ private $timeout;
+ private $sockets = array();
+ private $clients = array();
+
+ // timeout = microseconds
+ public function __construct($host, $port, $input = null, $timeout = 1000000)
+ {
+ $this->master = stream_socket_server("tcp://$host:$port", $errno, $errstr);
+ if (false === $this->master) {
+ throw new ConnectionException($errstr, $errno);
+ }
+
+ $this->sockets[] = $this->master;
+
+ $this->input = $input;
+ if (null !== $this->input) {
+ $this->sockets[] = $this->input;
+ }
+
+ $this->timeout = $timeout;
+ }
+
+ public function run()
+ {
+ // @codeCoverageIgnoreStart
+ while (true) {
+ $this->tick();
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ public function tick()
+ {
+ $changedSockets = $this->sockets;
+ @stream_select($changedSockets, $write = null, $except = null, 0, $this->timeout);
+ foreach ($changedSockets as $socket) {
+ if ($this->master === $socket) {
+ $newSocket = stream_socket_accept($this->master);
+ if (false === $newSocket) {
+ echo('Socket error');
+ continue;
+ }
+ $this->handleConnection($newSocket);
+ } elseif (null !== $this->input && $this->input === $socket) {
+ $this->handleInput($socket);
+ } else {
+ $data = @stream_socket_recvfrom($socket, 4096);
+ if ($data === '') {
+ $this->handleDisconnect($socket);
+ } else {
+ $this->handleData($socket, $data);
+ }
+ }
+ };
+ }
+
+ private function handleConnection($socket)
+ {
+ $client = new Connection($socket, $this);
+
+ $this->clients[$socket] = $client;
+ $this->sockets[] = $socket;
+
+ $this->emit('connection', array($client));
+ }
+
+ private function handleInput($input)
+ {
+ $this->emit('input', array($input));
+ }
+
+ private function handleDisconnect($socket)
+ {
+ $this->close($socket);
+ }
+
+ private function handleData($socket, $data)
+ {
+ $client = $this->getClient($socket);
+
+ $this->emit('data', array($data, $client));
+ }
+
+ public function getClient($socket)
+ {
+ return $this->clients[$socket];
+ }
+
+ public function getClients()
+ {
+ return $this->clients;
+ }
+
+ public function write($data)
+ {
+ foreach ($this->clients as $conn) {
+ $conn->write($data);
+ }
+ }
+
+ public function close($socket)
+ {
+ $client = $this->getClient($socket);
+
+ $this->emit('disconnect', array($client));
+
+ unset($this->clients[$socket]);
+ unset($client);
+
+ $index = array_search($socket, $this->sockets);
+ unset($this->sockets[$index]);
+
+ fclose($socket);
+ }
+
+ public function getPort()
+ {
+ $name = stream_socket_get_name($this->master, false);
+ return (int) substr(strrchr($name, ':'), 1);
+ }
+
+ public function shutdown()
+ {
+ stream_socket_shutdown($this->master, STREAM_SHUT_RDWR);
+ }
+}
10 tests/Igorw/Tests/SocketServer/CallableMock.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Igorw\Tests\SocketServer;
+
+class CallableMock
+{
+ public function __invoke()
+ {
+ }
+}
59 tests/Igorw/Tests/SocketServer/ConnectionTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Igorw\Tests\SocketServer;
+
+use Igorw\SocketServer\Connection;
+
+class ConnectionTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @covers Igorw\SocketServer\Connection::__construct
+ */
+ public function testConstructor()
+ {
+ $socket = fopen('php://temp', 'r+');
+ $server = $this->createServerMock();
+
+ $conn = new Connection($socket, $server);
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Connection::write
+ */
+ public function testWrite()
+ {
+ $socket = fopen('php://temp', 'r+');
+ $server = $this->createServerMock();
+
+ $conn = new Connection($socket, $server);
+ $conn->write("foo\n");
+
+ rewind($socket);
+ $this->assertSame("foo\n", fgets($socket));
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Connection::close
+ */
+ public function testClose()
+ {
+ $socket = fopen('php://temp', 'r+');
+
+ $server = $this->createServerMock();
+ $server
+ ->expects($this->once())
+ ->method('close');
+
+ $conn = new Connection($socket, $server);
+ $conn->close();
+ }
+
+ private function createServerMock()
+ {
+ $mock = $this->getMockBuilder('Igorw\SocketServer\Server')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ return $mock;
+ }
+}
217 tests/Igorw/Tests/SocketServer/ServerTest.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace Igorw\Tests\SocketServer;
+
+use Igorw\SocketServer\Server;
+
+class ServerTest extends \PHPUnit_Framework_TestCase
+{
+ private $server;
+ private $port;
+
+ /**
+ * @covers Igorw\SocketServer\Server::__construct
+ * @covers Igorw\SocketServer\Server::getPort
+ */
+ public function setUp()
+ {
+ $this->server = new Server('localhost', 0, null, 0);
+
+ $this->port = $this->server->getPort();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleConnection
+ */
+ public function testConnection()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+
+ $this->server->on('connection', $this->expectCallableOnce());
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleConnection
+ */
+ public function testConnectionWithManyClients()
+ {
+ $client1 = stream_socket_client('tcp://localhost:'.$this->port);
+ $client2 = stream_socket_client('tcp://localhost:'.$this->port);
+ $client3 = stream_socket_client('tcp://localhost:'.$this->port);
+
+ $this->server->on('connection', $this->expectCallableExactly(3));
+ $this->server->tick();
+ $this->server->tick();
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleData
+ */
+ public function testDataWithNoData()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ $this->server->on('data', $this->expectCallableNever());
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleData
+ */
+ public function testData()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ fwrite($client, "foo\n");
+
+ $mock = $this->createCallableMock();
+ $mock
+ ->expects($this->once())
+ ->method('__invoke')
+ ->with("foo\n");
+
+ $this->server->on('data', $mock);
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleDisconnect
+ */
+ public function testDisconnectWithoutDisconnect()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ $this->server->on('disconnect', $this->expectCallableNever());
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleDisconnect
+ * @covers Igorw\SocketServer\Server::close
+ */
+ public function testDisconnect()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ fclose($client);
+
+ $this->server->on('disconnect', $this->expectCallableOnce());
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::write
+ */
+ public function testWrite()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ $this->server->write("foo\n");
+ $this->server->tick();
+
+ $this->assertEquals("foo\n", fgets($client));
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::handleInput
+ */
+ public function testInput()
+ {
+ $input = fopen('php://temp', 'r+');
+
+ $this->server = new Server('localhost', 0, $input, 0);
+
+ $this->server->on('input', $this->expectCallableOnce());
+
+ fwrite($input, "foo\n");
+ $this->server->tick();
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::getClients
+ */
+ public function testGetClients()
+ {
+ $this->assertCount(0, $this->server->getClients());
+
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ $this->assertCount(1, $this->server->getClients());
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::tick
+ * @covers Igorw\SocketServer\Server::getClient
+ */
+ public function testGetClient()
+ {
+ $client = stream_socket_client('tcp://localhost:'.$this->port);
+ $this->server->tick();
+
+ $conns = $this->server->getClients();
+ list($key, $conn) = each($conns);
+
+ $this->assertInstanceOf('Igorw\SocketServer\Connection', $conn);
+ $this->assertSame($conn, $this->server->getClient($key));
+ }
+
+ /**
+ * @covers Igorw\SocketServer\Server::shutdown
+ */
+ public function tearDown()
+ {
+ $this->server->shutdown();
+ }
+
+ private function expectCallableExactly($amount)
+ {
+ $mock = $this->createCallableMock();
+ $mock
+ ->expects($this->exactly($amount))
+ ->method('__invoke');
+
+ return $mock;
+ }
+
+ private function expectCallableOnce()
+ {
+ $mock = $this->createCallableMock();
+ $mock
+ ->expects($this->once())
+ ->method('__invoke');
+
+ return $mock;
+ }
+
+ private function expectCallableNever()
+ {
+ $mock = $this->createCallableMock();
+ $mock
+ ->expects($this->never())
+ ->method('__invoke');
+
+ return $mock;
+ }
+
+ private function createCallableMock()
+ {
+ return $this->getMock('Igorw\Tests\SocketServer\CallableMock');
+ }
+}
5 tests/bootstrap.php
@@ -0,0 +1,5 @@
+<?php
+
+$loader = require __DIR__.'/../vendor/.composer/autoload.php';
+$loader->add('Igorw\Tests', __DIR__);
+$loader->register();
Please sign in to comment.
Something went wrong with that request. Please try again.