Simple PHP Websocket Application with Ratchet [Part 1 – Server Side]

11. February 2016 Blog 3

A couple years ago, I wrote a simple chat application. I did some research and found out that Facebook chat and notifications used a technique called long polling. So I wrote the application utilizing that methodology, and it worked pretty well. However, while I was writing the application, I kept seeing references to WebSockets, but I had no experience with them. So I decided to leave that for another day.

I attend a developer Meetup once a month. If you are in the Cincy/Dayton Ohio area, I highly recommend it. Every month, they have giveaways for things like t-shirts, books, etc… In order to pick the winners, some guys at Sparkbox made a simple application called PickMe. Basically the leader of the Meetup gives out a link, and anyone who visits it in their browser is added to the list of people that could win the giveaway, and a colored dot shows up on everyone’s browser for each person connected. Once everyone is ready, the leader from an admin view clicks a pick button, and one person’s browser will get a green background and say they have been picked. Everyone else’s screen turns red and says sorry they weren’t picked. Pretty convenient versus everyone writing their name on a piece of paper and pulling it out of a hat, which is what they used to do a year or two ago.

I figured they were using WebSockets for the project. So I decided this would be a fun application to try to reverse engineer using WebSockets. The first step was to visit packagist.org to see what libraries were already out there for PHP WebSockets and do some research. I found Ratchet which seemed like a pretty good library. So step number one is, of course, create a new directory and initialize a composer.json file:

composer init 

This will help us setup an empty composer.json that will look something like this:

{ 
    "name": "nate/test", 
    "authors": [ 
        { 
            "name": "Nate Denlinger", 
            "email": "test@test.com" 
        } 
    ], 
    "require": {} 
} 

Next, let’s go ahead and get Ratchet installed. Run this from the command line:

composer require cboden/ratchet

This will retrieve Ratchet and its dependencies and place them in a vendor directory and also create an autoloader for us. Also I know I’m going to put all of my server code in a directory called app. So go ahead and modify the composer.json to autoload my code as well using the autoload option. So now my composer.json looks like this:

{
    "name": "nate/test",
    "authors": [
        {
            "name": "Nate Denlinger",
            "email": "test@test.com"
        }
    ],
    "require": {
        "cboden/ratchet": "^0.3.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

Now that we have that done, let’s write some code! There are two things we need to create – the WebSocket server and a frontend to connect to the server. Let’s get started on the server. First, create a new directory called app and put a server.php file in the directory. Now we will setup a simple Ratchet WebSocket server. Here is the code:

namespace App;

use Ratchet\Server\IoServer;
use App\Server;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

require dirname(__DIR__).'/vendor/autoload.php';

$server = IoServer::factory(
new HttpServer(
        new WsServer(
            new Server()
        )
    ),
    8282
);

$server->run();

Ok. What did we just do? I’ll explain each piece since the “why” is more important than the code itself.

First, namespace App;. This is the namespace I created for my code. It is always a good habit to namespace all your code so that you don’t accidentally overwrite a class that was already declared by another package.

Second, all the use statements simple specify the classes we are going to use in our code below. The four classes we will use is Ratchet’s simple IoServer, Ratchet’s HttpServer, Ratchet’s Websocket Server, and a Message class we are going to write next.

Third, we use require to pull in the autoloader that composer generated for us. Now we don’t need to include or require individual files for each of the classes we are using above.

Forth, we actually create our WebSocket server. I’m not going to go into what each of these Ratchet classes are doing, but, in short, we are using their factory to generate a WebSocket server. The only important part for now is to make sure you use a port that is open on your computer, I used port 8282.

Finally, now that we have generated $server we need it to run. So we simply use $server->run(); At this point, we have basically created a blank HTTP based WebSocket server. However, we haven’t programmed it to do anything.

Now we need to start working on our Message class that we used. First let’s create a file name Message.php in the app directory. We need to name it Message so that our autoloader will find the file automatically based off of the class name. So lets start with a basically blank Message class. Here is the code to put in this new file:

namespace App;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Message implements MessageComponentInterface
{
    protected $clients;

    public function __construct()
    {
        $this->clients = [];
    }

    public function onOpen(ConnectionInterface $conn)
    {

    }

    public function onMessage(ConnectionInterface $from, $msg)
    {

    }

    public function onClose(ConnectionInterface $conn)
    {

    }

    public function onError(ConnectionInterface $conn, \Exception $e)
    {

    }
}

Alright! Time to understand the code. There are four basic events that occur when a client interacts with the WebSocket server: a new connection, a message is sent, a connection was closed, and there was some type of error. Based off of the method names, I’m pretty sure you can figure out which function gets called in each circumstance. The only other function we have declared is the __construct() function, and all it is doing is setting up an array $clients for us to store each connection that has been made.

One other thing to note is that our class Message implements MessageComponentInterface, which means it is implementing an interface. Interfaces declare what methods have to be declared on an object. So if we were to leave one of these methods off, we would get a nasty error. If you aren’t familiar with interfaces, I highly recommend you read up on them. They are a great tool.

Right now, we basically have the skeleton setup for our message class. Now let’s add some functionality. First, when a new client connects to the server, we need to store the connection in our clients array. So this is what I made my onOpen() function look like:

public function onOpen(ConnectionInterface $conn)
{
    // Store the new connection so we can send messages to it later
    $this->clients[$conn->resourceId] = [
        'connection' => $conn
    ];
}

The new connection is injected into the method as $conn. I am simply storing it on our clients array using the connections resourceId as a key. In PHP’s WebSocket, server each connection is given a resourceId that identifies the connection. So if we want to send messages to a specific connection, we need to know the resourceId we are going to be sending that data to. Also you will notice I am not just storing the connection itself, I am creating an associative array and storing $conn to connection. This is because once we do some more work, we will want to store more information about each client connected.

I’m going to skip around a little bit so we can get the easy parts out of the way. Let’s go to our onError() function and put some code in there. All we are going to do is echo out the error that we had to the command line for debugging and kill the connection since there was a problem. My method now looks like this:

public function onError(ConnectionInterface $conn, \Exception $e)
{
    echo "An error has occurred: {$e->getMessage()}\n";
    $conn->close();
}

Now we are going to get into more complicated code. We kind of need to think about what our different messages our clients will send to us for this application. Just like writing out a list of API endpoints like this:

GET /user/1
GET /user
POST /user
PUT /user/1

I came up with three basic messages that will be handled by our application to get us started.

pick -> Admin picks a random connection to win
reset -> Admin resets everyone's screen so they can pick again
connect -> Extra info about the user is sent

Also when we send data to and from WebSocket servers, we will be sending them as a string. So we need to utilize JSON. Each message that is sent from the client to the server will be structured like so:

{
    event: 'pick',
    data: {}, //any other data we may need
}

Now that we have this architecture decided on, we can start to write our onMessage() function. Here is what my code looks like:

public function onMessage(ConnectionInterface $from, $msg)
{
    $data = json_decode($msg);
    $valid_functions = ['pick','reset','connect'];
    if(in_array($data->event,$valid_functions)) {
        $functionName = 'event' . $data->event;
        $this->$functionName($from,$data);
    } else {
        $from->send('INVALID REQUEST');
    }
}

The connection that sent the message gets injected at $from, and a $msg, which is our JSON string, is also passed. First I decode the JSON and store it in $data. Next, instead of having a monster onMessage() function that handles all three of these types of events, I setup an array of valid events the client can send and have it call individual private functions on the class and send the $from and $data on to them. If someone tries to send an invalid event type, it will send just that connection a message letting them know it was an invalid message.

Let’s start on our first private method, which would be eventconnect() as it appends event to the front of the event type for the function name. Here is what my method looks like:

private function eventconnect(ConnectionInterface $from, $data)
{
    $avatar = 'http://api.adorable.io/avatars/150/' . rand(100000,999999) . '.png';
    $this->clients[$from->resourceId]['avatar'] = $avatar;
    $this->clients[$from->resourceId]['is_admin'] = $data->is_admin;
    $send_data = [
        'event' => 'connect',
        'clients' => $this->clients,
    ];
    $from->send(json_encode(['event' => 'connected', 'avatar'=> $avatar]));
    $this->sendMessageToAll($send_data);
}

As you can see, I generate a random avatar for each new user. Then I update the clients array and add more data to the connection in the array that represents the connection that the message came from. Since we stored the connections with a key of the resourceId we can easily access that element in the array. So I add the avatar to it and set is_admin to true or false which is from our $data object that was json_decoded() from the message sent by the client.

Next, I create an associative array of data that I want to send back to all the clients which has an event and clients which is array of all the clients connected. This way every client can see a display of all the users/avatars connected and waiting to be picked.

Then I send a message just to the client that sent the connect event letting it know it is connected and letting it know what avatar it was given so the user knows.

Finally, I send a message to all the clients with our $send_data. Now you will notice there isn’t a sendMessageToAll() function on our class. I wrote this to make it easier to send data to all clients as we end up doing this a lot. Here is that method. You can simply add it anywhere you really want in the class:

private function sendMessageToAll($msg)
{
    if(is_object($msg) || is_array($msg)) {
        $msg = json_encode($msg);
    }
    foreach ($this->clients as $client) {
        $client['connection']->send($msg);
    }
}

All it does is check if we are sending a string or an array/object. If it is an array/object, we need to convert it to JSON first. Then simply send out that message to all of the clients currently connected.

Let’s move on to our eventpick() function. This is triggered when an admin wants to pick a winner. Here is my method:

private function eventpick(ConnectionInterface $from, $data)
{
    $users = [];
    foreach($this->clients as $key => $client) {
        if(!$client['is_admin']) $users[] = $key;
    }
    $winning_id = $users[rand(0,(count($users)-1))];
    $winning_avatar = $this->clients[$winning_id]['avatar'];
    foreach($this->clients as $key => $client) {
        $client['connection']->send(json_encode([
            'event'=>'pick',
            'winner'=> ($winning_id == $key ? true : false),
            'winning_avatar' => $winning_avatar
        ]));
    }
}

First, we generate an array of IDs of all clients that are not admins. We don’t want an admin to win the contest. Then we randomly select one of the IDs and get their avatar. Finally, we loop through all the clients and send them a message letting them know if they are the winner or not and send the winning avatar so everyone can see who won.

Alright. Last event function! Let’s write the eventreset() function. This is triggered after the admin has selected a winner and wants to reset everyone’s screen so they can pick another winner. Here is my method:

private function eventreset(ConnectionInterface $from, $data)
{
    $this->sendMessageToAll(['event'=>'reset']);
}

Not very complicated is it? All we are doing is passing the event on to all the clients connected so they know to reset.

Now for our last method that we need to finish up before we move on to the frontend. We need to add some code to the default onClose() function. We need to remove the client that disconnected from our array of clients, and let everyone else know that person disconnected so they are removed from the list of users waiting to be picked. Here is my method:

public function onClose(ConnectionInterface $conn)
{
    unset($this->clients[$conn->resourceId]);
    $send_data = [
        'event' => 'connect',
        'clients' => $this->clients,
    ];
    $this->sendMessageToAll($send_data);
}

We unset() the connection from the array based off of the resourceId of the injected connection $conn. Then I’m simply going to send the exact same message to all the remaining clients that I do when a new connection is made as it already has the code on the frontend to display all the clients sent in that array. May as well just reuse that code.

That is it! We have our WebSocket server ready to go. Once you are ready for it to run, simply open up a terminal/command prompt, go to your projects directory, and enter in the command:

php app/server.php

This will turn the server on. It should be ready to handle connections now. If you would like to deploy this on a server, Ratchet recommends you use supervisor to make sure it is running and will restart automatically if it dies for some reason. View their documentation for more information.

As this post is already really long I decided to break it up into two parts. To continue on with part 2, which covers writing the frontend code, click the link below.

Simple PHP Websocket Application with Ratchet [Part 2 – Frontend]


3 thoughts on “Simple PHP Websocket Application with Ratchet [Part 1 – Server Side]”

  • 1
    Stonez Chen on February 1, 2018 Reply

    When I ran “>php App/server.php”, I have encountered an “PHP Fatal error: Uncaught Error: Class App\Server’ not found “. What could be the problems? I tried to remove the entire section from $server = IoServer….to $server->run(); Then there is no error. So my composer should be setup fine.

    Here are my server.php code. It’s exactly the same from your page above.
    Thanks for helping out!
    run();
    ?>

  • 2
    Vu Ha Nguyen on June 18, 2018 Reply

    Dont forget these in your composer.json

    “autoload”: {
    “psr-4”: {
    “App\\”: “app/”
    }
    },

    And make sure to run composer update to regenerate the autoload files

  • 3
    Vu Ha Nguyen on June 18, 2018 Reply

    And it should be class App\Message instead of App\Server in the file server.php

Leave a Reply

Your email address will not be published. Required fields are marked *