DEV Community

Cover image for WebSocket with PHP
Roberto B.
Roberto B.

Posted on • Updated on

WebSocket with PHP

The Web Socket scenario

PHP is typically used as a server-side language for responding to HTTP requests. Requests of this type are not persistent and are stateless. This scenario is straightforward: the client (the browser) makes a request (GET POST or PUT...) for a specific resource. The server, once provided with the resource, closes the connection. If it needs a further resource, the client starts a request again. A flow of this type is not persistent and 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; if a client wants to communicate with other clients, it can do it by sending messages to the server. The server will forward or 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 secure WebSocket (network communication 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 particular 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 the management of multiple concurrent threads that want to add, remove, and 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 clients connected (stored in the Swoole Table data structure) and deliver the message to all clients (via push method).

Overview of the Frontend Side Logic

The front-end logic is straightforward. We will create a simple HTML file (with no style, sorry for that) with an input text to allow the user to fill in the message and 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 the 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 the 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
Enter fullscreen mode Exit fullscreen mode

With -D option, you can specify the option to enable. If you want to allow Secure WebSocket, I suggest enabling “enable-openssl”.

The -f option is to force reinstalling the package if you have already installed it 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.5
Enter fullscreen mode Exit fullscreen mode

Or, if you want to install the latest stable version, you can run:

composer require openswoole/core
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 instance the WebSocket server class.

The server-side code

Create a new file named websocket.php.

Let’s start by 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;
Enter fullscreen mode Exit fullscreen mode

Create the new Server instance on port 9501, listening on 0.0.0.0 (accepting all incoming requests) 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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

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

Before you 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";
});
Enter fullscreen mode Exit fullscreen mode

Listening the Open event.

The "Open" event 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");
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Listening the Message event.

The "Message" event is triggered once a client sends a message to the 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);
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Listening the Close event.

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

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

Listening the Disconnect event.

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

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

Starting the service

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

$server->start();
Enter fullscreen mode Exit fullscreen mode

Launching the Web Socket service

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

php websocket.php
Enter fullscreen mode Exit fullscreen mode

Launching WebSocket PHP Service

Frontend code

Create a new index.html file.

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

When you instance the WebSocket object, be sure to use the correct IP address (or hostname) and the correct port (the same port that you defined in the 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>
Enter fullscreen mode Exit fullscreen mode

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: https://github.com/roberto-butti/websocket-php

Troubleshooting

Sometimes, during installation or compilation, you may encounter an error indicating that the 'pcre2.h' file was not found.

This typically happens when the PCRE (Perl Compatible Regular Expressions) library is missing or improperly configured on your system. Here's how you can solve this issue with your MacOS.

The first check you can perform is to check if you have installed the pcre2 library.
If you are using Homebrew you can check:

brew info pcre2
Enter fullscreen mode Exit fullscreen mode

If the Pcre2 package is installed, one of the most common reasons for the issue is the pcre2.h (the C source header file for pcre2) is not in the expected directory.

The compiling process requests the file of the OpenSwoole module.
The file is probably in the Pcre2 directories, but the OpenSwoole compiling process will look for that file between the PHP files. So one of the ways is to create a symbolic link from the PHP directory to the Pcre2 directory:

ln -s /opt/homebrew/Cellar/pcre2/10.42/include/pcre2.h /opt/homebrew/Cellar/php/8.3.3/include/php/ext/pcre/pcre2.h
Enter fullscreen mode Exit fullscreen mode

In the command, I'm using the link tool ln, the -s flag creates a symbolic link, and I'm using the 10.42 version of pcre2 and the 8.3.3 version of PHP. You have to replace the current versions of Pcre2 and PHP.

After executing the command, you will find a symbolic link in the PHP directory /opt/homebrew/Cellar/php/8.3.3/include/php/ext/pcre/pcre2.h, so that you can execute the pecl command for installing the OpenSwoole module.

Top comments (4)

Collapse
 
abiodun_sam profile image
Abiodun Aworeni

Thank you for this well explained tutorial.

Collapse
 
robertobutti profile image
Roberto B.

Added the Troubleshooting section dev.to/robertobutti/websocket-with... for the "pcre2.h not found issue" with MacOS.

Collapse
 
bobbyiliev profile image
Bobby Iliev

Great tutorial Roberto! Well done!

Collapse
 
cortesluis profile image
Luis Fabian Cortes Perez

Hello, thanks for the guide, it has been very useful to me, it works very well with ws, but with wss it connects and disconnects without any error, is there an Open Swoole log, or how can I know why it doesn't work.

Best regards