Обычно PHP используется в качестве серверного языка для ответа на HTTP-запросы. Запросы такого типа не являются постоянными и не имеют состояния. Этот сценарий очень прост: клиент (браузер) делает запрос (GET или POST или PUT…) на определенный ресурс. Сервер, предоставив ресурс, закрывает соединение. Клиент, если ему нужен другой ресурс, снова начинает запрос. Поток такого типа не является постоянным и в значительной степени контролируется клиентом.
В «сценарии» WebSocket есть серверная часть и несколько клиентов. Клиент может соединяться с серверной частью, и если клиент хочет общаться с другими клиентами, он может делать это, посылая сообщения на сервер. Сервер будет пересылать или отправлять сообщения клиенту.
Соединение между клиентом и сервером является постоянным и позволяет вести двусторонний диалог для нескольких сообщений.
Логика на стороне сервера
Прежде чем мы перейдем к коду, позвольте мне объяснить пример приложения, которое мы собираемся реализовать.
У нас есть служба на стороне сервера, реализованная с помощью PHP и Open Swoole. Для этого мы будем использовать «Swoole WebSocket Server», основанный на протоколе TCP и защищенный WebSocket (связь по сети шифруется, немного похоже на HTTP и HTTPS).
На стороне сервера мы собираемся реализовать WebSocket сервис на протоколе TCP, который слушает и реагирует на некоторые очень специфические события:
- Start: когда запускается часть WebSocket на стороне сервера;
- Open: когда приходит новый запрос на соединение от нового клиента.
- Сообщение: когда приходит сообщение от одного из активных соединений. Соединения, инициализированные с помощью «Open», остаются активными и постоянными;
- Close: когда клиент закрывает соединение. Закрытие соединения клиентом влечет за собой аннулирование его конкретного соединения;
- Disconnect: когда клиент отсоединяется.
Сервер будет отслеживать подключенных клиентов с помощью таблицы Swoole Table. Swoole Table — это структура данных, похожая на коллекцию (двумерный массив), которая позволяет управлять несколькими параллельными потоками, которые хотят добавлять, удалять, извлекать элементы. В обычном PHP-приложении это не нужно, поскольку доступ к коллекции или структуре данных осуществляется одним потоком.
Как только сервер получит новое сообщение от подключенного клиента (через событие «Message»), сервер переберет все подключенные клиенты (хранящиеся в структуре данных Swoole Table) и доставит сообщение всем клиентам (через метод push).
Логика фронтенда
Логика фронтенда очень проста, мы создадим очень простой HTML файл (без стилей, извините за это) с текстом для ввода, чтобы пользователь мог заполнить сообщение, кнопкой для отправки сообщения через WebSocket. Эта часть покрывается JS-скриптом, встроенным в HTML-файл.
Мы откроем соединение с классом WebSocket и реализуем несколько обратных вызовов:
- onmessage : для управления получением сообщения на клиенте;
- onopen : для управления при установлении WebSocket-соединения (с сервером);
- onclose: для управления, когда WebSocket соединение закрывается;
Когда пользователь нажмет на кнопку, будет вызвана функция sendMessage(). В функции будет вызван метод send() из объекта WebSocket для отправки сообщения на сервер.
Установите Open Swoole
Чтобы реализовать WebSocket с помощью PHP, необходимо установить дополнительный модуль, например Swoole. У вас есть несколько модулей, позволяющих реализовать WebSocket сервис в PHP, в этом руководстве я буду использовать реализацию Open Swoole от SwooleLabs, учитывая, что Open Swoole включает поддержку WebSocket.
Open Swoole выпускается в пакете PECL, поэтому вы можете установить его с помощью PECL install.
pecl install -f -D 'enable-sockets="no"
enable-openssl="yes"
enable-http2="yes"
enable-mysqlnd="yes"
enable-swoole-json="no"
enable-swoole-curl="yes"
enable-cares="yes"
with-postgres="no"' openswoole-4.11.1
С помощью опции -D вы можете указать опцию, которую нужно включить. Если вы хотите включить Secure WebSocket, я бы посоветовал включить опцию «enable-openssl».
Опция -f предназначена для принудительной переустановки пакета, если он уже был установлен ранее.
Установка сертификатов
Если вы хотите установить безопасный WebSocket, вам необходимо:
- создать частный и публичный сертификаты
- правильно настроить службу WebSocket
Позвольте начать с создания новых сертификатов
mkcert localhost 127.0.0.1 ::1
Создаются два файла:
- localhost+2-key.pem файл SSL ключа
- localhost+2.pem файл SSL cert
Эти два файла будут загружены при создании экземпляра класса сервера WebSocket.
Код на стороне сервера
Создайте новый файл с именем websocket.php.
Начнем включать нужные классы
<?php
use SwooleWebSocket{Server, Frame};
Создайте сервер на порту 9501, слушающий 0.0.0.0 (принимающий все входящие запросы).
По протоколу TCP (SWOOLE_SOCK_TCP) и Secure WebSocket (SWOOLE_SSL)
$server = new Server("0.0.0.0", 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
Создание таблицы (двухмерной таблицы памяти) с полями fd (дескриптор файла) и name
$fds = new SwooleTable(1024);
$fds->column('fd', SwooleTable::TYPE_INT, 4);
$fds->column('name', SwooleTable::TYPE_STRING, 16);
$fds->create();
Установите только что созданные SSL-сертификаты
$server->set([
'ssl_cert_file' => __DIR__ . '/localhost+2.pem',
'ssl_key_file' => __DIR__ . '/localhost+2-key.pem'
]);
Прослушать событие «Старт».
«Старт» срабатывает, как только запускается служба WebSocket
$server->on("Start", function (Server $server) {
echo "Swoole WebSocket Server is started at " . $server->host . ":" . $server->port . "n";
});
Прослушать событие Open.
«Open» срабатывает при подключении клиента.
$server->on('Open', function (Server $server, SwooleHttpRequest $request) use ($fds) {
$fd = $request->fd;
$clientName = sprintf("Client-%'.06dn", $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");
}
}
});
Прослушать событие Message.
«Сообщение» срабатывает, когда клиент отправляет сообщение на сервис WebSocket
$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);
}
}
});
Прослушать событие Close.
«Close» срабатывает, когда клиент закрывает соединение.
$server->on('Close', function (Server $server, int $fd) use ($fds) {
$fds->del($fd);
echo "Connection close: {$fd}, total connections: " . $fds->count() . "n";
});
Прослушать событие Disconnect.
«Отключить» срабатывает, когда клиент разрывает соединение
$server->on('Disconnect', function (Server $server, int $fd) use ($fds) {
$fds->del($fd);
echo "Disconnect: {$fd}, total connections: " . $fds->count() . "n";
});
Запустите сервер WebSocket, чтобы сработало событие «Start».
$server->start();
Запуск службы
Из командной строки вы можете запустить новую службу:
php websocket.php
Код фронтенда
Создайте новый файл index.html.
Код довольно прост. Если вы настраиваете на стороне сервера, вам нужно использовать протокол wss:// вместо ws://.
Когда вы будете создавать экземпляр объекта WebSocket, убедитесь, что используете правильный IP-адрес (или имя хоста) и правильный порт (тот же порт, который вы определили в PHP-скрипте).
<!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('wss://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>
Если вы откроете HTML-файл в браузере (на нескольких вкладках, для эмуляции нескольких клиентов), вы можете начать общаться с самим собой 😉
Исходный код
Я опубликовал код, использованный в этом посте, на GitHub: https://github.com/roberto-butti/websocket-php