WebSocket with PHP

WebSocket with PHP

Building a WebSocket service with PHP using Open Swoole.

·

7 min read

The Web Socket scenario

The typical usage of PHP is as a server-side language for responding to HTTP requests. Requests of this type are not persistent and are stateless. This scenario is very simple: the client (the browser) makes a request (GET or POST or PUT...) for a specific resource. The server, once provided the resource, closes the connection. The client, if it needs a further resource, starts a request again. A flow of this type is not persistent and strongly controlled by the client.

In a WebSocket "scenario", you have a server-side part and multiple clients. A client can connect with the server side part, and if a client wants to communicate with other clients, it can do it by sending messages to the server. The server will forward or will send messages to the client. The connection between the client and the server is persistent and allows for a two-way conversation for multiple messages.

WebSocket with PHP schema architecture

Overview of the Server Side Logic

Before we get to the code, let me explain the example application we will implement. We have a server side service implemented with PHP and Open Swoole. We will use "Swoole WebSocket Server" based on TCP protocol and with secure WebSocket (communication over network is encrypted, a bit like HTTP and HTTPS). On the server side, we are going to implement a WebSocket service on TCP protocol that listens and reacts to some very specific events:

  • Start: when the server side WebSocket part is started;
  • Open: when a new connection request from a new client arrives
  • Message: when a message arrives from one of the active connections. Connections initialized with "Open" remain active and are persistent;
  • Close: when a client closes the connection. The closing of the connection by the client, implies the invalidation of its specific connection;
  • Disconnect: when a client disconnects.

The server will keep track of the connected client via the Swoole Table. Swoole Table is a data structure similar to a collection (bi dimensional array), that allows to manage multiple concurrent thread that want to add, remove, retrieve items. In a typical PHP application you don’t need that because a single thread accesses the collection or the data structure.

Once the Server receives a new message from a connected client (via “Message” event), the server will loop all the client connected (stored in Swoole Table data structure) and delivery the message to all client (via push method).

Overview of the Frontend Side Logic

The frontend logic is very simple. We will create a very simple HTML files (with no style, sorry for that) with an input text for allowing the user to fill the message, a button to send the message through the WebSocket. This part is covered by a JS script inline in the HTML file.

We will open a connection with WebSocket class, and we will implement some callback:

  • onmessage : for managing when the message is received on the client;
  • onopen : form managing when the WebSocket connection (with the server) is established;
  • onclose: for managing when the WebSocket connection is closed;

When the user clicks the button, a function sendMessage() will be called. The function will be called send() method from the WebSocket object to push the message to the server.

Building a Web Socket example

Installing Open Swoole

To implement WebSocket with PHP, you must install an additional module like Swoole. You have more than one module that allows you to implement WebSocket service in PHP. In this tutorial, I will use Open Swoole implementation by SwooleLabs, considering that Open Swoole includes support for WebSocket.

Open Swoole is released via PECL package, so you can install it with PECL install.

pecl install -f -D 'enable-openssl="no" enable-sockets="no" enable-http2="no" enable-mysqlnd="no" enable-hook-curl="no" with-postgres="no"' openswoole

With -D option, you can specify the option to enable. If you want to enable Secure WebSocket, my suggestion is to enable “enable-openssl”.

The -f option is to force reinstalling the package if you have already installed in the past.

Installing composer package

With the latest version (22) of Open Swoole you can use the standard packages (PSR) provided by Open Swoole. So you have to install the openswoole/core package:

composer require openswoole/core:22.1.2

Using the SSL certificates (optional)

If you want to establish a secure WebSocket connection and during the installation you use the enable-sockets="yes" option, you need:

  • create private and public certificates
  • set up correctly the WebSocket service

Let me start with creating the new certificates

mkcert localhost 127.0.0.1 ::1

Two files are created:

  • localhost+2-key.pem the SSL key file
  • localhost+2.pem the SSL cert file

These two files will be loaded while you will instance the WebSocket server class.

The server side code

Create a new file named websocket.php.

Let’s start including the right classes. From the version 22 of OpenSwoole, you can use the OpenSwoole namespace:

<?php

use OpenSwoole\WebSocket\{Server, Frame};
use OpenSwoole\Constant;
use OpenSwoole\Table;

Create the new Server instance on port 9501, listening on 0.0.0.0 (accepting all incoming request) using TCP protocol (Constant::SOCK_TCP). If you want to enable Secure WebSocket, you should use Constant::SSL as forth parameter Constant::SOCK_TCP || Constant::SSL )

$server = new Server("0.0.0.0", 9501, Server::SIMPLE_MODE, Constant::SOCK_TCP);

For storing the list of the client connected to the WebSocket, create a Table (a two dimensions memory table) with fd (file descriptor) and name fields

$fds = new Table(1024);
$fds->column('fd', Table::TYPE_INT, 4);
$fds->column('name', Table::TYPE_STRING, 16);
$fds->create();

If you are going to create a secure WebSocket connection, you have to configure the seerver to use the SSL certificates:

$server->set([
    'ssl_cert_file' => __DIR__ . '/localhost+2.pem',
    'ssl_key_file' => __DIR__ . '/localhost+2-key.pem'
]);

Now, before tho start the Server, you have to define the handler functions for the events dispatched by the WebSocket server.

Listening the Start event.

The "Start" event is triggered once the WebSocket service is started.

$server->on("Start", function (Server $server) {
    echo "Swoole WebSocket Server is started at " . $server->host . ":" . $server->port . "\n";
});

Listening the Open event.

The "Open" even is triggered once a client is connected.

$server->on('Open', function (Server $server, Swoole\Http\Request $request) use ($fds) {
    $fd = $request->fd;
    $clientName = sprintf("Client-%'.06d\n", $request->fd);
    $fds->set($request->fd, [
        'fd' => $fd,
        'name' => sprintf($clientName)
    ]);
    echo "Connection <{$fd}> open by {$clientName}. Total connections: " . $fds->count() . "\n";
    foreach ($fds as $key => $value) {
        if ($key == $fd) {
            $server->push($request->fd, "Welcome {$clientName}, there are " . $fds->count() . " connections");
        } else {
            $server->push($key, "A new client ({$clientName}) is joining to the party");
        }
    }
});

Listening the Message event.

The "Message" event is triggered once a client sends a message to WebSocket service.

$server->on('Message', function (Server $server, Frame $frame) use ($fds) {
    $sender = $fds->get(strval($frame->fd), "name");
    echo "Received from " . $sender . ", message: {$frame->data}" . PHP_EOL;
    foreach ($fds as $key => $value) {
        if ($key == $frame->fd) {
            $server->push($frame->fd, "Message sent");
        } else {
            $server->push($key,  "FROM: {$sender} - MESSAGE: " . $frame->data);
        }
    }
});

Listening the Close event.

The "Close" event is triggered once a client close the connection.

$server->on('Close', function (Server $server, int $fd) use ($fds) {
    $fds->del($fd);
    echo "Connection close: {$fd}, total connections: " . $fds->count() . "\n";
});

Listening the Disconnect event.

The "Disconnect" event is triggered once a client loose the connection.

$server->on('Disconnect', function (Server $server, int $fd) use ($fds) {
    $fds->del($fd);
    echo "Disconnect: {$fd}, total connections: " . $fds->count() . "\n";
});

Starting the service

Start the WebSocket server with the start() method. Once the sever is started, the "Start" event is triggered.

$server->start();

Launching the Web Socket service

From the command line, you can launch the new service:

php websocket.php

Launching WebSocket PHP Service

Frontend code

Create a new index.html file.

The code is quite simple. If you set up on the server side and yuo want to use the Secure Web socket connection, you have to use wss:// protocol instead of ws:// protocol.

When you will instance the WebSocket object, be sure to use the right IP address (or hostname) and the right port (the same port that you defined in PHP script)

<!doctype html>
<html>
<head>
    <title> WebSocket with PHP and Open Swoole </title>
    <script>
        let echo_service;
        append = function (text) {
            document.getElementById("websocket_events").insertAdjacentHTML('beforeend',
                "<li>" + text + ";</li>"
            );
        }
        window.onload = function () {
            echo_service = new WebSocket('ws://127.0.0.1:9501');
            echo_service.onmessage = function (event) {
                append(event.data)
            }
            echo_service.onopen = function () {
                append("Connected to WebSocket!");
            }
            echo_service.onclose = function () {
                append("Connection closed");
            }
            echo_service.onerror = function () {
                append("Error happens");
            }
        }

        function sendMessage(event) {
            console.log(event)
            let message = document.getElementById("message").value;
            echo_service.send(message);
        }

    </script>
</head>

<body>
    <div>
        Message: <input value="Hello!" type="text" id="message" /><br><br>
        <input type="button" value="Submit" onclick="sendMessage(event)" /><br>
        <ul id="websocket_events">
        </ul>
    </div>
</body>

</html>

If you open or access to HTML file with your browser (on multiple tab to emulate multiple clients), you can start to chat with yourself ;)

Realtime Chat with PHP and WebSocket

Source Code

I published the code used in this post on GitHub: github.com/roberto-butti/websocket-php