Today, realtime is used more and more frequently on the web: instant messaging, notifications, collaborative edition, etc. These technologies allow new features and meet new needs. WebSockets have arrived to overcome the constraints of polling by making bidirectional exchanges between a client and a server possible. They are frequently implemented using the NodeJS and JavaScript ecosystem. Did you know that it is possible to integrate WebSockets using just PHP?
WebSocket ?
WebSocket is a protocol that was standardized in RFC 6455 in 2011. It allows real-time communications between a client and a server. The defining feature of its operation is at the server level, since it does not only send responses but can send a message spontaneously to the client.
This protocol is also full-duplex: data can pass simultaneously in both directions. It thus allows a strong interactivity which makes it its main strength.
To set up a WebSocket server, you have to create a program that will be executed directly from the server (through a service for example). It will therefore not be called via a browser as you might get used to with PHP.
If you write this program in JavaScript when you are already using PHP, you will need to embed two different technologies on the server side. This is an impactful choice for the project. Once implemented, each part will have to be brought to life and therefore ensure the stability of the code, make software updates on the server or even manage monitoring. To be comfortable, it is therefore important to have a good knowledge of the tools you use. The greater the number of tools, the more complex it is to maintain. It is on this point that we can ask ourselves the question “Could we have a WebSocket server directly in PHP if we are already using PHP elsewhere?”. The simple fact of asking this question already allows you to make an informed choice and not to jump headlong into the first solution available!
Most languages have at least one implementation of this protocol. You have to take the time to discover them, then the most appropriate solution is required as a compromise between the skills of the team, the lifespan of the application and the kind of features to develop.
Where to start ?
We will see how to use WebSockets in PHP. Knowing how to use Composer is highly recommended to manage your dependencies. This tool has established itself as a standard today and it is important to know its general behaviour and usage.
There are a large number of packages out there and sometimes it is difficult to sort them out. Here are two reference packages that I am used to using: Ratchet (cboden/ratchet + ratchet/pawl) and hoa/websocket. Ratchet is the most used today and is well maintained. My preference goes towards Hoa for the simplicity of the code but it is sorely lacking in contributors to continue to develop the project.
These are two solid and well-documented implementations that are interesting to discover to fully understand how the protocol works.
Instant messaging with Ratchet
To show you the overall logic to implement, I am going to rely on a chat system. This example, intentionally simplified, will contain the following functionalities:
- The client connects to the server and is added to the list of users,
- He/she can start sending messages,
- Messages are exchanged between users,
- He can define his name which will be transmitted with the message.
I’ll use Ratchet for the example but the approach is similar with other tools. Under the hood, Ratchet uses the ReactPHP ecosystem and asynchronous programming which enables very efficient use of network transfers. In addition, ReactPHP is based on the Event Loop pattern which is used to propagate messages through application code. At each event on a connection (opening, closing, receiving a message), part of the code is executed.
This relationship between the two tools is interesting because it simplifies the use of other ReactPHP features such as non-blocking read streams.
Create the server
Before managing real-time interactions, the server should be created. The current version of Ratchet works with PHP 7.4 although it can be used from PHP 5.4.2. To install it with Composer, just run the following command:
composer require cboden/ratchet
Next, let’s create a server.php
PHP file to declare the server, not forgetting to load the dependencies. This server will be accessible locally on port 8080:
$app = new Ratchet\App('127.0.0.1', 8080);
$app->route('/echo', new Ratchet\Server\EchoServer, ['*']);
$app->run();
You can start the server by running the PHP script in your terminal. It will be available on the network at “ws: //127.0.0.1: 8080 /”. Then you have to define its behavior. Within a single server, it is possible to process messages in different ways thanks to the integrated routing system.
The EchoServer
object integrated into Ratchet returns all transmitted messages to the client as its name suggests. It allows to quickly have an operational version of the server with which the first exchanges are possible.
Client implementation
Now that the server is operational, we will be able to create the client. For this part, another library is to be installed with Composer:
composer require ratchet/pawl
Separating the client and the server into two separate packages can seem complex, but in reality you will rarely have a client and a server within the same application. You can therefore integrate only the part that interests you and thus limit the amount of dependencies and facilitate maintenance.
Now let’s create a client.php
file to declare the client. It must connect to the “/echo” URL of the server listening on port 8080:
use Ratchet\RFC6455\Messaging\MessageInterface;
$loop = React\EventLoop\Factory::create();
$connector = new Ratchet\Client\Connector($loop);
$connection = $connector('ws://127.0.0.1:8080/echo')->then(
function (Ratchet\Client\WebSocket $conn) {
$conn->on('message', function (MessageInterface $msg) use ($conn) {
echo "{$msg}\n";
$conn->close();
});
$conn->send('Hello world !');
}, function (Throwable $e) {
echo "Could not connect: {$e->getMessage()}\n";
}
);
$loop->run();
For the client to work, it must be attached to an event loop that we create in the $loop
variable. All interactions on network streams will take place in this loop.
The last line of the script is essential since it starts this loop. It will run while waiting to receive events until the user decides to stop it.
Ratchet\Client\Connector
is initialized and, since it is an invokable object, it can be called as a function. This call returns a React\Promise\PromiseInterface
that we configure with two anonymous functions:
- The first will be executed if the connection is successful,
- The second will deal with the different cases of errors.
We can see in the first function the beginning of the client logic implementation. We ask to display the message received in the console when a message
event occurs. For the moment, when the first message is received from the server, the connection is terminated and the client stopped.
Just below, a message is sent to the server using the send
method. The messages’ content is a character string. It is therefore possible to use a structured language such as JSON to go further in the exchanges (define the author of the message, embed additional information, etc.). Some have even gone further by specifying an exchange protocol through WebSocket: Web Application Messaging Protocol (WAMP).
The client will therefore, once the connection is open, send the message “Hello world!” and receive that same message back because the server sends everything back to the sender.
To draw a parallel with the protocol, here’s what happens when opening a connection. The client sends an HTTP request asking a protocol upgrade (change from HTTP to WebSocket). A response from the server authenticates and starts the new connection. From this moment, exchanges are no longer in the form of HTTP requests but through messages encoded in frames (a packet of structured bits). Ratchet fully manages the reading and writing of frames which allows you to focus on the text to be sent.
Implementing server logic
At the moment the server is only able to echo messages. We will now see how to create a specific implementation.
For this, Ratchet provides us with the Ratchet\MessageComponentInterface
interface which asks us to define four methods: onOpen
, onMessage
, onClose
, onError
. These methods refer directly to the events that will be triggered by the internal event loop.
Usually PHP is used in request / response mode. For each request, a script is invoked, calculates a response, and returns a response. A new request means a new invocation of the script. In a sense, this language can be considered stateless in this context. However, external tools make it possible to maintain a state between the different requests: session, database, etc.
Here the paradigm is different. The server.php script will run continuously and will therefore handle several “requests” without stopping. This is interesting because the memory used within the script can contain a state (list of active users for example) but it has downsides. Great care must be taken to free up used resources and limit data stored in internal memory to remain efficient. There is nothing that prevents you from using a third-party system (database, cache system, etc.) within a WebSocket server to facilitate data management.
As part of an instant messaging system, the server must be “aware” of all the clients connected to communicate with them. Also, when a client sends a message, you have to send that same message to everyone else. Our server will therefore maintain a list of active clients to act accordingly.
We are going to implement this new server in a class (src/ChatServer.php) that we are going to connect to another route:
namespace CHStudio\IPC\WebSocket;
class ChatServer implements Ratchet\MessageComponentInterface
{
private $clients;
public function __construct()
{
$this->clients = new \SplObjectStorage;
}
public function onOpen(Ratchet\ConnectionInterface $conn)
{
$this->clients->attach($conn);
}
public function onMessage(Ratchet\ConnectionInterface $from, $message)
{
foreach ($this->clients as $client) {
if ($client !== $from) {
$client->send($message);
}
}
}
public function onClose(Ratchet\ConnectionInterface $conn)
{
$this->clients->detach($conn);
}
public function onError(Ratchet\ConnectionInterface $conn, \Exception $e)
{
$conn->close();
echo “Error: {$e->getMessage()}\n“;
}
}
Let’s detail the content of this new object. Note that there is a property, $clients
, which is initialized in the constructor as a SplObjectStorage
. This “Standard PHP Library” data structure is interesting because, unlike a simple array, it can use objects as a storage key.
The onOpen()
et onClose()
methods will run when a client connects or disconnects. This is where the list of active clients is managed. A call to the attach()
and detach()
methods of SplObjectStorage
allows us to maintain a reliable client list.
Then the onMessage()
method is the most central one because it is executed when a message arrives. This is where the logic of exchange between the different customers is defined. A loop allows us to cycle through active connections and send the message to all clients except to the one that just sent it.
The last method, onError()
, is used to handle the different errors. As the server is running continuously it should not stop as soon as an exception is thrown. This last listener is therefore very important, because it allows to judge the level of the error and decide whether everything should be stopped or not.
Now that this object is defined, you need to configure the server to use it. Let’s modify the server.php script accordingly :
$app = new Ratchet\App('127.0.0.1', 8080);
$app->route('/echo', new Ratchet\Server\EchoServer, [‘*']);
$app->route('/chat', new CHStudio\IPC\WebSocket\ChatServer, ['*']);
$app->run();
A new route is therefore added: “/chat”. Our new object must of course be loaded to be used (using Composer, an autoload or a simple include
).
Sending messages by the client
The server is now operational but the client does not yet have the ability to send user-written messages. You must allow text to be entered on the keyboard without blocking the arrival of messages, i.e. the script continues to run until nothing has been entered on the keyboard.
Non-blocking and asynchronous processing logic is at the heart of ReactPHP. As we use this ecosystem, we have at our disposal a suitable object able to read a stream asynchronously: React\Stream\ReadableResourceStream
.
In PHP, most of the file manipulation or network processing functions are blocking. That is, the code waits for a response before continuing to execute. This wait is problematic in our case for two reasons:
- Very inefficient, the program does nothing during this wait;
- Prohibits concurrent interactions.
It is this last point that is the most important. To type on the keyboard while continuing to display messages from the server, the program should not be put on hold. ReactPHP uses native PHP functions, mainly stream_select()
to handle this concurrency.
All information sources must be initialized as a stream (file, socket) and connected to the same Event Loop instance. Then, at each iteration of the loop, the streams are reviewed: if there is information to transmit, it is read and interpreted, otherwise we move on to the next stream. Message processing can be blocking, but it mostly happens during wait times.
Here’s how to modify the client to read keyboard input and pass it to the server :
use Ratchet\Client\WebSocket;
use Ratchet\RFC6455\Messaging\MessageInterface;
$loop = React\EventLoop\Factory::create();
$stdin = new React\Stream\ReadableResourceStream(STDIN, $loop);
$connector = new Ratchet\Client\Connector($loop);
$connection = $connector('ws://127.0.0.1:8080/chat')->then(
function (WebSocket $conn) use ($stdin) {
$conn->on('message', function (MessageInterface $msg) {
echo "{$msg}\n";
});
$stdin->on('data', function (string $data) use ($conn) {
$conn->send(rtrim($data, "\n"));
});
}, function (Throwable $e) {
echo "Could not connect: {$e->getMessage()}\n";
}
);
$loop->run();
There is little difference from the previous file. The $stdin
variable is used to read the input stream. A listener is declared to send the entered data whenever the “data” event is raised on $stdin
.
The client and the server are now able to communicate and, if several clients are connected at the same time, they can exchange messages.
Enrich the server to personalize the client
We previously saw that the server maintains a list of active connections in memory. We can use this list to store, for example, the username.
To do this, we will define a command in the form of structured text that the user can enter and which will be interpreted by the server: /name Username
Then you must update the onOpen()
and onMessage()
methods from ChatServer.php file :
public function onOpen(ConnectionInterface $conn)
{
$this->clients->attach($conn, new \stdClass);
}
public function onMessage(ConnectionInterface $from, $message)
{
if (0 === strpos($message, '/name')) {
$this->clients[$from]->name = trim(substr($message, 5));
$from->send(sprintf(
'[server] your name is now: %s',
$this->clients[$from]->name
));
return;
}
if (isset($this->clients[$from]->name)) {
$message = sprintf('%s: %s', $this->clients[$from]->name, $message);
}
foreach ($this->clients as $client) {
if ($client !== $from) {
$client->send($message);
}
}
}
When a client connects, an empty stdClass
object is stored in the list and bound to the connection. This is the advantage of going through SplObjectStorage
: it allows you to link two objects together. This empty object will therefore be present for the entire duration of the client’s connection and since it is tied to this connection, we will be able to access it easily.
Then, for each message received by the server, the content must be checked. If it starts with “/name”, the command entered will allow extracting the new username. This name is stored in the list of customers inside the previously initialized empty object.
As this message is considered as a command, it should not be sent to other connected clients. The client having transmitted this message is informed in return that the name has been taken into account and the method is stopped thanks to the return
instruction.
Finally, in the case of a standard message, the username is appended to the message before being sent to other clients. Every customer can then know who just wrote the message.
We can therefore see that it is possible to enrich the experience by storing additional information about a connection. Then it is not very complex to prohibit sending messages if the name is not yet defined or to notify connected clients when a new one joins or leaves the session.
Conclusion
You have seen that it is possible to use WebSocket in PHP thanks to solid and accessible libraries. The advantage is that you can add this protocol to an existing application and even share a few bits of logic (model, business rules, etc.) between your existing application and your client/server implementation.
Running the server as a stand-alone service is a bit more complex than the usual Request / Response model. Tools like Supervisor or SystemD are also used with PHP and will help you having a reliable and controlled environment.
You can find the server and client code described in this article on GitHub: https://github.com/shulard/ipc-websocket-sample
Going further
In parallel with the WebSocket protocol, the W3C has also specified an API implemented in browsers in JavaScript. It is very well supported by browsers today and allows you to define a client within a web page to interact with a server.
As part of a web application, creating a server in PHP and a client in the browser is an efficient solution.
The use of WebSockets is only relevant when there is a two-way exchange. Otherwise, there are less complex solutions that are more suitable for web applications:
- From client to server: API and HTTP calls are sufficient to respond to most use-cases,
- From the server to the client: remember to use the Server-Sent Events which are made especially for this purpose and very well supported in browsers.
This article was written for the PHP Magazin August issue in which its translated into German.
Thanks to my buddy Sylvain Adam who helped a lot by reviewing the written english 😉.
https://kiosk.entwickler.de/php-magazin/php-magazin-6-2020/vom-protokoll-zur-loesung-in-purem-php/