WebSocket server with tags management, forward messages on the right clients with ease !
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']);
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();
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);
}
}
The default behavior of the Gomoob WebSocket server is the following !
- Expose port
8080
and authorize connections from all IP addresses (i.e0.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.
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 thejsonSerialize()
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();
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.
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'
]
)
);
To help you start quickly we also provide a Docker container here https://hub.docker.com/r/gomoob/php-websocket-server.
- 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.
- 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 aliascreate(...)
method.
- Fix
port
andaddress
options problems while creating aWebSocketServer
, the parameter were not transmitted to the Ratchet server ; - Now the default port number is
80
which is the default Ratchet server port.
- Add missing
symfony/yaml
composer dependency, otherwise problems was encountered while runningcomposer update --no-dev
; - Add missing
monolog/monolog
composer dependency, , otherwise problems was encountered while runningcomposer update --no-dev
.
- Configure specific Eclipse validator rules ;
- Add MIT license.
- First release.
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.