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.
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
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
Or, if you want to install the latest stable version, you can run:
composer require openswoole/core
Installing the IDE Helper (optional)
If your editor like VS Code or Zed doesn't recognize the syntax of OpenSwoole classes or methods you should consider to install the OpenSwoole IDE-Helper package.
The OpenSwoole IDE Helper package provides comprehensive IDE help files, enabling accurate autocompletion and enhancing your overall development experience.
The OpenSwoole IDE-Helper package: https://packagist.org/packages/openswoole/ide-helper
To install the OpenSwoole IDE Helper package, you can use Composer. Run the following command in your project directory:
composer require openswoole/ide-helper --dev
After installing the IDE Helper, your editor will recognize the syntac of OpenSwoole elements.
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 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\{Frame, Server};
use OpenSwoole\Constant;
use OpenSwoole\Http\Request;
use OpenSwoole\Table;
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);
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 server to use the SSL certificates:
$server->set([
'ssl_cert_file' => __DIR__ . '/localhost+2.pem',
'ssl_key_file' => __DIR__ . '/localhost+2-key.pem'
]);
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";
});
Listening the Open event.
The "Open" event is triggered once a client is connected.
$server->on('Open', function (Server $server, 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 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);
}
}
});
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";
});
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";
});
Starting the service
Start the WebSocket server with the start()
method. Once the server 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
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>
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 ;)
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
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
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 (8)
Thank you for this well explained tutorial.
Great tutorial Roberto! Well done!
Added the Troubleshooting section dev.to/robertobutti/websocket-with... for the "pcre2.h not found issue" with MacOS.
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
How to install openswoole on windows?
I tried, but failed. Not possible without docker, or WSL.
Exactly, if you have a Windows machine for local development, my suggestion is to use WSL or Docker. On production, i suggest a Linux environment (or in general *nix)
Good