Skip to content

WebSocket server with tags management, forward your messages on the right clients with ease !

License

Notifications You must be signed in to change notification settings

gomoob/php-websocket-server

Repository files navigation

Gomoob WebSocket server

WebSocket server with tags management, forward messages on the right clients with ease !

Total Downloads Latest Stable Version Build Status Coverage Code Climate License

Introduction

The Gomoob WebSocket server is a simple Ratchet server which works with custom tags to easily forward messages to clients depending on custom tags.

As an example let's suppose we have a Web Application with English and French users. English users should receive English messages, French users should receive French messages.

Each application opens a Web Socket with a particular language tag.

// Web Application in English mode
var enWebSocket = new WebSocket('ws://localhost:8080?tags={"language":"EN}');

...

// Web Application in French mode
var frWebSocket = new WebSocket('ws://localhost:8080?tags={"language":"FR}');

...

On server side the Gomoob WebSocket server keeps track of the associations between tags and WebSocket connections. For example this simple PHP peace of code allows to easily forward a message to all clients connected with the language=FR tag.

// PHP Server (in most cases a Web Server) to Web Socket server client, allows to send one message which is forwared to
// several opened WebSocket connections
$phpClient = new WebSocketClient('ws://localhost:8080');
$phpClient->send(WebSocketRequest::create($message, ['language' => 'FR']);

Installation

Server side (run the server)

Running a server requires only one line of code.

require __DIR__ . '/vendor/autoload.php';

echo "WebSocket server started, enter Ctrl+C to stop server." . PHP_EOL;
\Gomoob\WebSocket\Server\WebSocketServer::factory()->run();

Client side (PHP)

First pull the project with composer using the following dependency.

{
    "require": {
        "gomoob/php-websocket-server": "^1.0.0"
    }
}

Then simply use the \Gomoob\WebSocket\Client\WebSocketClient class to send your messages.

// Open a Server / Server WebSocket connection
$phpClient = new WebSocketClient('ws://localhost:8080');

// Forward a message to all the WebSocket client connections associated to 'tag1' and 'tag2'
$response = $phpClient->send(
    WebSocketRequest::create(
        $message, 
        [
            'tag1' => 'tag1Value',
            'tag2' => 'tag2Value'
        ]
    )
);

If you want to write solid unit tests we also provide the \Gomoob\WebSocket\Client\WebSocketClientMock class. This class is a utility mock which is very easy to use.

// Somewhere in our code we use a \Gomoob\WebSocket\IWebSocketClient ...
// We suppose this code is implemented in MyPowerfulService->serviceMethod();
$phpClient->send(WebSocketRequest::create('Message 0.')->setTags(['tag0' => 'tag0Value']));
$phpClient->send(WebSocketRequest::create('Message 1.')->setTags(['tag1' => 'tag0Value']));
$phpClient->send(WebSocketRequest::create('Message 2.')->setTags(['tag0' => 'tag0Value', 'tag1' => 'tag1Value']));
		
// Then we write a test case by replacing the real WebSocket client implementation with the mock one
class SampleTestCase extends TestCase
{
    public function setUp() {
        $this->webSocketClient = new WebSocketClientMock();
        $this->myPowerfulService->setWebSocketClient($this->webSocketClient);
    }

    public function testServiceMethod() {
    
        // Calls the method to test
        $this->myPowerfulService->serviceMethod();
    
        // Checks the right requests were sent
        $webSocketRequests = $this->webSocketClient->findByTags(['tag0' => 'tag0Value']);
		$this->assertCount(2, $webSocketRequests);
		$this->assertContains($webSocketRequest0, $webSocketRequests);
		$this->assertNotContains($webSocketRequest1, $webSocketRequests);
		$this->assertContains($webSocketRequest2, $webSocketRequests);
    }
}

Advanced configuration

The default behavior of the Gomoob WebSocket server is the following !

  • Expose port 8080 and authorize connections from all IP addresses (i.e 0.0.0.0) ;
  • Accept only plain string messages (exceptions are encountered if JSON messages are sent / received) ;
  • Use a default PSR logger which output messages on the terminal ;
  • Do not manage any authorization checks.

If one of those behaviors does not fit your need please read the following sub sections. You can also read the src/test/server.php file which shows how to start a server with custom message parsing and authorizations.

Message parser

By default the WebSocket server will accept plain string messages, if you try to send a JSON object then you'll encounter the following exception.

The 'message' property is not a string, you must configure a message parser to parse messages !

This is the expected behavior, if you want the server to manage custom PHP object messages then you have to :

  • Make your PHP object messages extends \JsonSerializable and implement the jsonSerialize() method correctly ;
  • Implement a custom message parser to create your custom PHP object messages when a plain JSON object is received.

A sample message object is provided in the \Gomoob\WebSocket\Message\Message class, feel free to read the associated source code to understand how it works. You'll also found a sample message parser in the \Gomoob\WebSocket\Message\MessageParser.

To explain how to manage custom PHP object messages let's suppose we have the following message object to send.

class MyMessage {
    private $messageProperty1;
    public function __construct($messageProperty1) {
        $this->messageProperty1 = $messageProperty1; 
    }
    public function getMessageProperty1() {
        return $this->messageProperty1;
    }
}

Sending such a message in a browser on Javascript would require the following code.

var socket = new WebSocket('ws://localhost:8080');
socket.send(
    JSON.stringify(
        {
            message : {
                messageProperty1 : "Hello !"
            },
            tags : {
                tag1 : 'tag1Value'
            }
        }
    )
);

Or in PHP with the client we provide.

WebSocketClient::factory('ws://localhost:8080')->send(WebSocketRequest::create(new MyMessage('Hello !')));

As this this will not work because on server side the Gomoob WebSocket server will not know how to parse the messages and how to re-create those messages to forward them to clients who opened WebSocket connections.

The first thing to do is to implement the \JsonSerializable class and the jsonSerializeMethod() in our MyMessage class.

class MyMessage implements \JsonSerializable {
   ...
   public function jsonSerialize() {
       return [
           'messageProperty1' => $this->messageProperty1;
        ];
   }
}

Then we have to implement a message parser by extending the \Gomoob\WebSocket\IMessageParser class.

use Gomoob\WebSocket\IMessageParser;

class MyMessageParser implement IMessageParser {
    public function parse(array $arrayMessage)
    {
        // Ensure the array contains only valid key names
        foreach (array_keys($arrayMessage) as $key) {
            if (!is_string($key) || !in_array($key, ['messageProperty1'])) {
                throw new \InvalidArgumentException('Unexpected property \'' . $key . '\' !');
            }
        }
        
        // The 'messageProperty1' property is mandatory
        if (!array_key_exists('messageProperty1', $arrayMessage)) {
            throw new \InvalidArgumentException('No \'messageProperty1\' property found !');
        }
        
        return new MyMessage($arrayMessage['messageProperty1']);
    }
}

Finally we have to provide our parser when we create our WebSocket server.

WebSocketServer::factory(
    [
    	'messageParser' => new MyMessageParser()
    ]
)->run();

Authorization Manager

By default the WebSocket server will accept all connections and message sendings, in most cases this behavior is not expected because anybody could open a WebSocket on your server and try to forward messages to all connected clients without authorization.

You can implement a custom authorization manager by implementing the \Gomoob\WebSocket\IAuthManager interface, this interface has the following signature.

/**
 * Interface which defines an authorization manager. An authorization manager allows to control authorization while
 * opening Web Socket connections and sending messages over Web Sockets.
 *
 * @author Baptiste Gaillard (baptiste.gaillard@gomoob.com)
 */
interface IAuthManager
{
    /**
     * Function used to indicate if connection opening is authorized.
     *
     * @param \Ratchet\ConnectionInterface $connection the current Ratchet connection.
     *
     * @return boolean `true` if the connection opening is authorized, `false` otherwise.
     */
    public function authorizeOpen(ConnectionInterface $connection);

    /**
     * Function used to indicate if message sending is authorized.
     *
     * @param \Ratchet\ConnectionInterface $connection the current Ratchet connection.
     * @param \Gomoob\WebSocket\IWebSocketRequest $webSocketRequest the current Gomoob WebSocket request.
     */
    public function authorizeSend(ConnectionInterface $connection, IWebSocketRequest $webSocketRequest);
}

So its very easy to manage authorizations, just return true or false with the authorizeOpen(...) or authorizeSend(...) functions.

The ApplicationsAuthManager

To easier authorization we provide an authorization manager which allows to declare several applications with key and secret properties.

This authorization manager is available in the \Gomoob\WebSocket\Auth\ApplicationsAuthManager class, it works with a very simple YAML configuration file.

Here is a sample instanciation of the manager with a WebSocket server.

WebSocketServer::factory(
    [
    	'authManager' => ApplicationsAuthManager::factory(
    	    [
    	        'authorizeOpen' => false,
    	    	'configurationFile' => __DIR__ . '/auth.yml'
    	    ]
    	)
    ]
)->run();

The content of the auth.yml file could be the following.

applications:
  - 
    key: application1
    secret: B4ajW3P7jfWEYPZsQV8mnteHg97G67uW
    authorizeOpen: true
  - key: application2
    secret: 33yLWdynhaqm9tYjDFKf8gB8zmAPKdDP
    authorizeOpen: false

Then the followig Javascript peace of code will apply.

// Does not work because required 'key' and 'secret' URL parameters are not provided
var socket1 = new WebSocket('wss://myserver.org:8080'); 

// Works because the 'key' and 'secret' URL parameters provided are valid
var socket2 = new WebSocket('wss://myserver.ord:8080?key=application1&secret=B4ajW3P7jfWEYPZsQV8mnteHg97G67uW');

// Does not work because the request does not provide the 'key' and 'secret' properties
socket2.send(
    JSON.stringify(
        {
            message : {
                messageProperty1 : "Hello !"
            }
        }
    )
);

// Works because the request provides valid 'key' and 'secret' properties
socket2.send(
    JSON.stringify(
        {
            message : {
                messageProperty1 : "Hello !"
            },
            metadata : {
                key : 'application2',
                secret : '33yLWdynhaqm9tYjDFKf8gB8zmAPKdDP'
            }
        }
    )
);

The same rules are also applicable with the PHP client we provide.

WebSocketClient::factory('ws://localhost:8080')->send(
    WebSocketRequest::create(
        new MyMessage('Hello !')
    )->setMetadata(
        [
            'key' => 'application2',
            'secret' => '33yLWdynhaqm9tYjDFKf8gB8zmAPKdDP'
        ]
    )
);

Docker container

To help you start quickly we also provide a Docker container here https://hub.docker.com/r/gomoob/php-websocket-server.

Release history

1.2.0 (2016-08-23)

  • Moves the TagsTree class to \Gomoob\WebSocket\Util\TagsTree ;
  • Add a new TagsTree->reset() method ;
  • Add a new \Gomoob\WebSocket\Client\WebSocketClientMock class to easier unit testing ;
  • Update composer dependencies.

1.1.0 (2016-08-18)

  • Add more PHP Documentor documentation about the goals of metadata in the \Gomoob\WebSocket\IWebSocketRequest interface and the \Gomoob\WebSocket\Request\WebSocketRequest class ;
  • Add management of defaultMetadata in the \Gomoob\WebSoscket\IWebSocketClient interface and the \Gomoob\WebSocket\Client\WebSocketClient class ;
  • Add management of defaultTags in the \Gomoob\WebSocket\IWebSocketClient interface and the \Gomoob\WebSocket\Client\WebSocketClient class ;
  • Improve \Gomoob\WebSocket\Message\Message serialization ;
  • Improve \Gomoob\WebSocket\Request\WebSocketRequest serialization ;
  • Now all the factory methods can be calls with a factory(...) method or an alias create(...) method.

1.0.3 (2016-08-17)

  • Fix port and address options problems while creating a WebSocketServer, the parameter were not transmitted to the Ratchet server ;
  • Now the default port number is 80 which is the default Ratchet server port.

1.0.2 (2016-08-17)

  • Add missing symfony/yaml composer dependency, otherwise problems was encountered while running composer update --no-dev ;
  • Add missing monolog/monolog composer dependency, , otherwise problems was encountered while running composer update --no-dev.

1.0.1 (2016-08-17)

  • Configure specific Eclipse validator rules ;
  • Add MIT license.

1.0.0 (2016-08-17)

  • First release.

About Gomoob

At Gomoob we build high quality software with awesome Open Source frameworks everyday. Would you like to start your next project with us? That's great! Give us a call or send us an email and we will get back to you as soon as possible !

You can contact us by email at contact@gomoob.com or by phone number (+33) 6 85 12 81 26 or (+33) 6 28 35 04 49.

Visit also http://gomoob.github.io to discover more Open Source softwares we develop.