WebSockets and Ratchet

Spread the news

WebSockets

Before we get into how to use WebSockets, let’s take a look at what the technology is and why it is important to Web app development. WebSockets are defined by IETF RFC 6455, which was standardized in 2011.  Today, the technology is rather well developed and available in most web browsers (Over 94% of global web users at the time of writing) via JavaScript. In truth, Sockets have been around since the beginning and WebSockets is just a newer way of using them in a standard way on the web – specifically as an HTTP-compatible TCP protocol.

If you want to dig a little deeper into WebSockets and like having a book in hand, I recommend the book to the right. It is short, sweet, and to the point! Clicking the image will take you to Amazon (and a purchase will help support this site without costing you anything extra).

The WebSocket protocol has three things going for it (besides being pretty universal):

  • Simple (it’s just messages)
  • Full-Duplex (messages travel in either direction)
  • Persistent (connection stays open)

I call this technology simple because it is really straightforward in implementation – it is a technology that allows messages to be sent from the client to the sever or from the server to the client.  These messages can travel in any direction at any time.  The client initiates a standard HTTP connection to the server and then “upgrades” it to a WebSocket. Once this connection is established there is very little overhead and either side can send a message to the other at any time. This persistence means that systems become very responsive and have reduced overhead when the app requires multiple messages going back and forth.

Let’s consider a simple chat application.  Without WebSockets, the client has to poll the server constantly to see if there are any new messages. Each of these polls contains a full set of headers, which, though small singularly, add up very quickly the faster you are polling.  For a chat system to be very responsive you would need to poll every couple seconds.  However, with WebSockets, a single connection is made, with all its headers, and then a simple heartbeat is implemented. This heartbeat is very small in comparison and does not happen very often (and is taken care of by the browser and most server libraries). Furthermore, since the server can initiate a message at any time, the chat becomes near real-time communication!

JavaScript

Gaining access to WebSockets in JavaScript is fairly simple.  A WebSocket is instantiated by passing in the URL (and port) that it should connect to.

var exampleSocket = new WebSocket("ws://www.example.com:8100");

Once you have this exampleSocket created you can send messages with the .send method, and receive messages using the .onmessage callback method.

exampleSocket.send("message for server");
exampleSocket.onmessage = function(event) {console.log(event.data)};

The PHP

PHP is not primarily designed to work with WebSockets and does not support it out of the box. However, there are two or three primary packages that you can use to gain WebSocket functionality:

In this article I’m going to be looking specifically at React. However, Aerys and Wrench are also very well documented and used.

A change in thinking…

The first thing you will notice as you look at code for Ratchet is that it doesn’t look like the code you are used to seeing! This is because it is fundamentally different.  All three of the packages listed above will require you to change your way of thinking and turn your code into something that is asynchronous.  They all implement a “server” which is a loop that switches between tasks while waiting for things. This means that your PHP application is not used via Apache or Nginx (unless you’re just using nginx as a proxy). The PHP script itself will have to handle all the communication with the browser directly.   This causes its own set of difficulties that a PHP developer doesn’t typically have to understand, but all of them can be overcome with a little work.

The Ratchet documentation is very well developed and is the first place you should turn when setting up your environment. It covers all the components of the package as well as things such as setting up a proxy, or keeping your PHP script running in the background or on restart.

To install Ratchet you create a new project and then run composer require cboden/ratchet from the command line. Then you instantiate a basic server with something like:

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\MyAppRouter;

//load composer
require 'vendor/autoload.php';

$server = IoServer::factory(
	new HttpServer(
		new WsServer(
			new MyAppRouter()
		)
	),
	8100
);
$server->start();

where MyAppRouter is your own class that has all the logic for your app, and 8100 is the port that the server will listen on (You can use any port you want for WebSockets, including 80).

Looking at Line 10, you find the main building blocks of your server. The IoServer Factory builds a PSR-4-compliant server app, passing the connection/data down into the contained class. Using this logic, when a web browser connects to your server, the connection will enter the IoServer first, the HttpServer, the WsServer, and finally your application logic. Whatever your Application returns, travels out to the client in the opposite manner.

The IoServer component takes care of the basic socket communications that are used in TCP. WebSockets are technically not dependent on HTTP, but are compatible with HTTP, and even use HTTP specs to initiate the WebSocket. Therefore, the IoServer passes the connection into the HttpServer which implements the HTTP Specifications, in turn, passing WebSocket connections into the WsServer. The WsServer implements the heartbeat and sending/receiving of WebSocket messages, passing those messages into your app – in this case a Router. With that it mind all you need to worry about is keeping track of the connections and proxying messages to and from all of them.

Let’s build a simple chat system

With some pretty simple Router code you can set up your app to easily be extensible.  Obviously you could use a switch statement for a super small project… but we all know that even small projects have a tendency to grow. Before long you’re adding public chat and private chat, typing notifications, read notifications, user registration and profiles, image upload, and all kinds of fun stuff. For now we’ll stick to the basics – a single chat room, but let’s do it in a way that will be easier to maintain in the future.

<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class MyAppRouter implements MessageComponentInterface {
	
	public $controller;
	
	public function __construct(){
		$this->controller = new \MyApp\Controller();
		$this->controller->clients = new \SplObjectStorage();
	}
	
	/**
	 * When a new connection is opened it will be passed to this method
	 *
	 * @param  ConnectionInterface $conn The socket/connection that just connected to your application
	 * @throws \Exception
	 */
	function onOpen(ConnectionInterface $conn)
	{
		$this->controller->clients->attach($conn);
		$conn->send(json_encode(['action'=>'notification', 'message'=>'Welcome!', 'error'=>false]));
	}
	
	/**
	 * This is called before or after a socket is closed (depends on how it's closed).
	 * SendMessage to $conn will not result in an error if it has already been closed.
	 *
	 * @param  ConnectionInterface $conn The socket/connection that is closing/closed
	 * @throws \Exception
	 */
	function onClose(ConnectionInterface $conn)
	{
		$this->controller->clients->detach($conn);
	}
	
	/**
	 * If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
	 * the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
	 * @param  ConnectionInterface $conn
	 * @param  \Exception $e
	 * @throws \Exception
	 */
	function onError(ConnectionInterface $conn, \Exception $e)
	{
		try {
			$conn->send(json_encode(['action' => 'notification', 'message' => $e->getMessage(), 'error' => true]));
		} catch (\Exception $e){
			//should probably log this...
		}
	}
	
	/**
	 * Triggered when a client sends data through the socket
	 *
	 * @param  \Ratchet\ConnectionInterface $from The socket/connection that sent the message to your application
	 * @param  string $msg The message received
	 * @throws \Exception
	 */
	function onMessage(ConnectionInterface $from, $msg)
	{
		try {
			$msgJSON = json_decode($msg);
			if (
				!empty($msgJSON->action)
				&& is_string($msgJSON->action)
				&& $msgJSON->action !== '__construct'
				&& method_exists($this->controller, $msgJSON->action)
			){
				$reflection = new \ReflectionMethod($this->controller, $msgJSON->action);
				if ($reflection->isPublic()) {
					$this->controller->{$msgJSON->action}($from, $msgJSON);
				} else {
					throw new \Exception('You are not allowed to do that.');
				}
			} else {
				throw new \Exception('Invalid Operation');
			}
		} catch (\Exception $e){
			//there was an error
			$from->send(json_encode(['action'=>'notification', 'message'=>$e->getMessage(), 'error'=>true]));
		}
	}

}

This is your basic Router that would maintain a single controller. You can always extend it to dynamically load the corresponding Controller, but that is something I’ll leave that to be part of your homework!

This Router class has a few methods to note:

  • onOpen (line 20) – this gets called by the WsServer class whenever a new client connects.  This is where you would want to register the connection so you can get to it later.  \SplObjectStorage is the recommended structure to use for this as you can safely store an “array” or objects. Later, you can go through those objects to find connections you want to send something to.
  • onClose (line 33) – this gets called whenever a client disconnects – either by choice or accident.  There is no way to know whether or not the user is technically going to receive any messages you send them at this point as this could be a client-initiated “I’m going to disconnect now” kind of message or it could be a network problem that caused the connection to be lost. Here you want to get rid of the saved instance of the object with a call to detach.
  • onError (line 45) – this is a catch-all that the WsServer will call if an uncaught error bubbles up.  It will catch the error and push the error to this method. I have used this as a way to send a notification to the client that has the error message included.
  • onMessage (line 61) – this is the method that gets called every time a message is received from the client. This is where you want to determine what kind of data has been passed to you and do something with it.

You’ll notice that I have been using json_encode and json_decode for the messages.  This is not necessary as the messages come through as plain text. However, I find it to be the simplest way to pass multiple parameters back and forth between the front end and the WebSocket server.  JSON is easily parsed and created in both JavaScript and PHP.

In this router I have chosen to have an “action” parameter that would be the method to call in the controller. First I check this field to make sure it has something in it, is not the constructor, and exists as a method in the Controller.  I then took this up a step and use PHP’s ReflectionMethod to get some additional information about the Controller’s method on the fly.  Here we need to make sure that the method is public.  If you are certain that your controller will not have any private methods, this would be OK to skip for the sake of speeding things up (ReflectionMethod is somewhat expensive in server resources), but otherwise is crucial to ensuring your server does not crash when a malicious user tries to send their own actions across the wire. Such Fatal Errors would cause your WebSocket server to exit the loop, dropping all connections.

Once all these things have been checked, the Router calls the method in the controller. These public methods are where you would do the rest of the programming for your server! We have set out to make a super-simple chat application, so we really only need one public method.

<?php
namespace MyApp;
use Ratchet\ConnectionInterface;
class Controller {
	
	/* @var \SplObjectStorage */
	public $clients;
	
	public function chat(ConnectionInterface $conn, $msgJson) {
		$this->sendToAll(['action'=>'chat', 'message'=>$msgJson->message, 'error'=>false]);
	}
	
	/**
	 * Sends a JSON-encoded message to the client
	 *
	 * @param ConnectionInterface $conn
	 * @param array|object $msgArray
	 */
	private function send(ConnectionInterface $conn, $msgArray){
		$conn->send(json_encode($msgArray));
	}

	/**
	 * Sends a JSON-encoded message to all the clients online
	 *
	 * @param ConnectionInterface $conn
	 * @param array|object $msgArray
	 */
	private function sendToAll($msgArray){
		$msgJson = json_encode($msgArray);
		foreach ($this->clients as $conn){
			$conn->send($msgJson);
		}
	}
	
}

This controller gives us a chat method that accepts a message and broadcasts it to all the other users logged in. Granted, in this example you won’t know who is chatting, but you are able to send and receive messages.  We can extend it slightly by changing the message passed back in line 10 to prepend messages with the ip address or the connection number of the chatter, but it would probably be better to start extending our little application to have users, or at least allow the chatters to send some sort of command to set their nickname.  That I will also leave for you to explore on your own – and recommend that you set it up with a keyed array.  Use the connection ID as the key.  If you want to persist users across servers or instances, you would need to have a login/logout function available in the methods as well.

Difficulties

Some things in Ratchet can get a little tricky if you are used to standard PHP development.

  • Sessions.  These are set by PHP when the script runs, but in these WebSocket applications, the script runs once and handles multiple connections with multiple people. The use of the global Session data is strictly impossible.  You will need to either implement some sort of session data based on connection ID or use some a Symphony2 Session with Ratchet’s SessionProvider. It wraps a WampServer, so you’ll have to get a little deeper in the Ratchet docs for that.
  • Asynchronous. If you want your server to be able to handle multiple people at the same time, then you will have to keep in mind that the App needs to be quick about returning control to the server.  This means that you need to only make non-blocking calls.
  • Non-blocking calls causes a lot of trouble if you want to talk to a Database.  Typically PHP is only used with Synchronous SQL calls.  You send a query to the database and then wait for the database to give you back the data.  All of this waiting has to be mitigated. Hint: PHP does support Asynchronous database calls under the hood.  Learn more in my article Asynchronous SQL, and check out the documentation for mysqli_poll.
  • Client Lists in multiple controllers – If you do extend this model to have multiple Controllers, you will need to move the SplObjectStorage up into the Router.  You will want to pass it in to the controllers as a pointer so that information contained therein updates across the Router and all the Controllers. Check out my article Getting Advanced with Variables or Pass By Reference in the PHP docs for more info on this advanced topic.

I would love to hear your comments on this topic and am always happy to answer any questions you might have! Please reach out using the comments section below, through the Contact Me page, or through my personal website! Happy Coding!


Spread the news

Leave a Reply

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