First steps with Ratchet

In this section, you will learn how to install and use the Ratchet framework. It's important to note that Ratchet applications work differently than regular PHP applications that are deployed in a web server and work on a per-request basis. This will require you to adopt a new way of thinking of how PHP applications are run and deployed.

Architectural considerations

Implementing a WebSocket server with PHP is not trivial. Traditionally, PHP's architecture revolves around the classical request/reply paradigm: the web server receives a request, passes it to the PHP interpreter (which is typically built into the web server or managed by a process manager such as PHP-FPM), which processes the request and returns a response to the web server who in turn responds to the client. The lifetime of data in a PHP script is limited to a single request (a principle that is called Shared Nothing).

This works well for classical web applications; especially the Shared Nothing principle as it is one of the reasons that PHP applications usually scale very well. However, for WebSocket support, we need a different paradigm. Client connections need to stay open for a very long time (hours, potentially days) and servers need to react to client messages at any time during the connection lifetime.

One library that implements this new paradigm is the Ratchet library that we'll be working with in this chapter. In contrast to regular PHP runtimes that live within a web server, Ratchet will start its own web server that can serve long-running WebSocket connections. As you'll be dealing with PHP processes with extremely long run times (a server process may run for days, weeks, or months), you will need to pay special attention to things such as memory consumption.

Getting started

Ratchet can be easily installed usingComposer. It requires PHP in at least version 5.3.9 and also works well with PHP 7. Start by initializing a new project with the composer init command on a command line in your project directory:

$ composer init .

Next, add Ratchet as a dependency to your project:

$ composer require cboden/ratchet

Also, configure Composer's autoloader by adding the following section to the generated composer.json file:

'autoload': { 
  'PSR-4': { 
    'PacktChp6Example': 'src/' 
  } 
} 

As usual, PSR-4 autoloading means that the Composer class loader will look for classes of the PacktChp6Example namespace within the src/ folder of your project directory. A (hypothetical) PacktChp6ExampleFooBar class would need to be defined in the file src/Foo/Bar.php file.

As Ratchet implements its own web server, you will not need a dedicated web server such as Apache or Nginx (for now). Start by creating a file called server.php, in which you initialize and run the Ratchet web server:

$app = new RatchetApp('localhost', 8080, '0.0.0.0'); 
$app->run() 

You can then start your web server (it will listen on the port that you have specified as the second parameter of the RatchetApp constructor) using the following command:

$ php server.php

If you do not have a PHP 7 installation ready on your machine, you can get started quickly withDocker, using the following command:

$ docker run --rm -v $PWD:/opt/app -p 8080:8080 php:7 php /opt/app/server.php

Both of these commands will start a long-running PHP process that can directly handle HTTP requests on your command line. In a later section, you will learn how to deploy your application to production servers. Of course, this server does not really do much, yet. However, you can still test it using a CLI command or your browser, as shown in the following screenshot:

Getting started

Testing the example application with HTTPie

Let's continue by adding some business logic to our server. WebSocket applications served by Ratchet need to be PHP classes that implement RatchetMessageComponentInterface. This interface defines the following four methods:

  • onOpen(RatchetConnectionInterface $c) will be called whenever a new client connects to the WebSocket server
  • onClose(RatchetConnectionInterface $c) will be called when a client disconnects from the server
  • onMessage(RatchetConnectionInterface $sender, $msg) will be called when a client sends a message to the server
  • onError(RatchetConnectionInterface $c, Exception $e) will be called when an exception occurred at some point while handling a message

Let's start with a simple example: a WebSocket service that clients can send messages to, and it will respond to the same client with the same message, but reversed. Let's call this class PacktChp6ExampleReverseEchoComponent; the code is as follows:

namespace PacktChp6Example; 
 
use RatchetConnectionInterface; 
use RatchetMessageComponentInterface; 
 
class ReverseEchoComponent implements MessageComponentInterface 
{ 
    public function onOpen(ConnectionInterface $conn) 
    {} 
 
    public function onClose(ConnectionInterface $conn) 
    {} 
 
    public function onMessage(ConnectionInterface $sender, $msg) 
    {} 
 
    public function onError(ConnectionInterface $conn, 
                            Exception $e) 
    {} 
} 

Note that although we do not need all of the methods specified by the MessageComponentInterface, we need to implement all of them nonetheless in order to satisfy the interface. For example, if you do not need anything special to happen when a client connects or disconnects, implement the onOpen and onClose methods, but just leave them empty.

In order to better understand what's happening in this application, add some simple debug messages to the onOpen and onClose methods, as follows:

public function onOpen(ConnectionInterface $conn) 
{ 
    echo "new connection from " . $conn->remoteAddress . "
"; 
} 
 
public function onClose(ConnectionInterface $conn) 
{ 
    echo "connection closed by " . $conn->remoteAddress . "
"; 
} 

Next, implement the onMessage method. The $msg parameter will contain the message that was sent by the client as string, and you can use the ConnectionInterface class' send() method to send messages back to the client, as shown in the following code snippet:

public function onMessage(ConnectionInterface $sender, $msg) 
{ 
    echo "received message '$msg' from {$conn->remoteAddress}
"; 
    $response = strrev($msg); 
    $sender->send($response); 
} 

Tip

You might be inclined to use PHP 7's new type hinting feature to hint the $msg parameter as string. This does not work in this case, because it would change the method's interface that is prescribed by the RatchetMessageComponentInterface and cause a fatal error.

You can then register your WebSocket application at the RatchetApp instance in your server.php file using the following code:

$app = new RatchetApp('localhost', 8080, '0.0.0.0'); 
$app->route('/reverse', new PacktChp6ExampleReverseEchoComponent); 
$app->run(); 

Testing WebSocket applications

To test WebSocket applications, I can recommend the wscat tool. It is a command-line tool written in JavaScript (and thus requires Node.js to be running on your machine) and can be installed using npm, as follows:

$ npm install -g wscat

With the WebSocket server listening at port 8080, you can use wscat to open a new WebSocket connection using the following CLI command:

$ wscat -o localhost --connect localhost:8080/reverse

This will open a command-line prompt in which you can enter messages that are sent to the WebSocket server. Messages received from the server will also be displayed. See the following screenshot for an example output of both the WebSocket server and wscat:

Testing WebSocket applications

Testing WebSocket applications using wscat

Playing with the event loop

In the preceding example, you have only sent messages to clients after having received a message from the same client. This is the traditional request/reply communication pattern that works well in most scenarios. However, it is important to understand that when using WebSockets, you are not forced to follow this pattern, but can send messages to connected clients at any time you like.

In order to gain a better understanding of the possibilities you have in a Ratchet application, let's have a look at the architecture of Ratchet. Ratchet is built on ReactPHP; an event-driven framework for network applications. The central component of a React application is the event loop. Each event that is triggered in the application (for example, when a new user connects or sends a message to the server) is stored in a queue, and the event loop processes all events stored in this queue.

ReactPHP offers different implementations of event loops. Some of these require additional PHP extensions such as libevent or ev to be installed (and typically, the event loops based on libevent, ev, or similar extensions offer the best performance). Usually, applications like Ratchet will automatically choose which event loop implementation to use so that you do not need to concern yourself with the inner workings of ReactPHP if you do not want to.

By default, a Ratchet application creates its own event loop; however, you can also inject your own event loop into the RatchetApp class that you've created yourself.

All ReactPHP event loops must implement the interface ReactEventLoopLoopInterface. You can use the class ReactEventLoopFactory to automatically create an implementation of this interface that is supported by your environment:

$loop = ReactEventLoopFactory::create(); 

You can then pass this $loop variable into your Ratchet application:

$app = new RatchetApp('localhost', 8080, '0.0.0.0', $loop) 
$app->run(); 

Having direct access to the event loop allows you to implement some interesting features. For example, you can use the event loop's addPeriodicTimer function to register a callback that will be executed by the event loop in a periodic interval. Let's use this feature in a short example by building a new WebSocket component called PacktChp6ExamplePingComponent:

namespace PacktChp6Example; 
 
use RatchetMessageComponentInterface; 
use ReactEventLoopLoopInterface; 
 
class PingCompoment extends MessageComponentInterface 
{ 
    private $loop; 
    private $users; 
 
    public function __construct(LoopInterface $loop) 
    { 
        $this->loop  = $loop; 
        $this->users = new SplObjectStorage(); 
    } 
 
    // ... 
} 

In this example, the $users property will help us to keep track of connected users. Each time a new client connects, we can use the onOpen event to store the connection in the $users property, and use the onClose event to remove the connection:

public function onOpen(ConnectionInterface $conn) 
{ 
    $this->users->attach($conn); 
} 
 
public function onClose(ConnectionInterface $conn) 
{ 
    $this->users->detach($conn); 
} 

As our WebSocket component now knows the connected users, we can use the event loop to register a timer that periodically broadcasts messages to all connected users. This can be easily done in the constructor:

public function __construct(LoopInterface $loop) 
{ 
    $this->loop  = $loop; 
    $this->users = new SplObjectStorage(); 
 
    $i = 0;

    $this->loop->addPeriodicTimer(5, function() use (&$i) {

        foreach ($this->users as $user) {

            $user->send('Ping ' . $i);

        }

        $i ++;

    }); 
} 

The function passed to addPeriodicTimer will be called every five seconds and will send a message with an incrementing counter to each connected user. Modify your server.php file to add this new component to your Ratchet application:

$loop = ReactEventLoopFactory::create(); 
$app = new RatchetApp('localhost', 8080, '0.0.0.0', $loop) 
$app->route('/ping', new PingCompoment($loop)); 
$app->run(); 

You can again test this WebSocket handler using wscat, as shown in the following screenshot:

Playing with the event loop

Periodic messages cast by a periodic event loop timer

This is a good example of a scenario in which a WebSocket client receives updates from a server without having explicitly requested them. This offers efficient ways to push new data to connected clients in near real-time, without the need to repeatedly poll for information.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.246.148