Git Product home page Git Product logo

php-websocket-server's Introduction

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 ([email protected])
 */
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 [email protected] 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.

php-websocket-server's People

Contributors

bgaillard avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

php-websocket-server's Issues

Allow configuration of default metadata on WebSocketClient

It is very frequent to always use the same metadata properties on requests, it would be useful to allow configuration of default metadata properties on the WebSocketClient to merge those default metadata with the request metadata before sending messages.

Here is a sample configuration.

$webSocketClient = new WebSocketClient(
    'wss://websockets.myserver.com, 
    [
        'key' => 'XXXXXXXX',
        'secret' => 'XXXXXXXX'
    ]
);

// Or

$webSocketClient->setDefaultRequestMetadata(
    [
        'key' => 'XXXXXXXX',
        'secret' => 'XXXXXXXX'
    ]
);

Note We could also implement defautl request tags.

Create metadata and tags validators

We have several set*Metadata(...) and set*Tags(...) method in the source code.

We should improve those methods by calling a validator for the metadata and tags property, for example.

public function setMetadata(array $metadata = [])
{
    // Validates the metadata
    $this->metadataValidator->validate($metadata);

    // Sets the metadata
    $this->metadata = $metadata;

    // Returns this instance
    return $this;
}

So we should create a \Gomoob\WebSocket\Validator\MetadataValidator class and a \Gomoob\WebSocket\Validator\TagsValidator class.

Add mandatoryTags to YAML authorization file

The ApplicationsAuthManager works as expected but the current implementation allows to forward messages everywhere using any credentials defined in the YAML authorization file.

For example its currently possible to forward messages to clients connected with the credentials of application1 with the credentials of application2.

applications:
  - 
    key: application1
    secret: 7UxuWw3ZcFBW85U2rdtjKZeStMHKVAzf8jpqkb5eAPBkd37F2sz4x3WS3GnMk7gq
    authorizeOpen: true  
 - key: application2
    secret: Kmrw5apmzmQMseAttckp6e7APeCDVtL58QzSPaKqqdHUF469hfhWyue3ns363kn5
    authorizeOpen: false

To prevent credentials of application2 to allows message sendings to clients connected with the credentials of application1 we propose a new mandatoryTags property.

Here is an example.

applications:
  - 
    key: application1
    secret: 7UxuWw3ZcFBW85U2rdtjKZeStMHKVAzf8jpqkb5eAPBkd37F2sz4x3WS3GnMk7gq
    authorizeOpen: true  
    mandatoryTags:
      -
          applicationName: application1
 - key: application2
    secret: Kmrw5apmzmQMseAttckp6e7APeCDVtL58QzSPaKqqdHUF469hfhWyue3ns363kn5
    authorizeOpen: false

This will force clients using the first credentials to specify an applicationName tag with the application1 value, otherwise connection and message sending will fail.

Also the consequence will be that all the clients will be forced to use the same applicationName tag value and we can be sure the messages will always be forwarded only to the application1 clients.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.