WebSocket с помощью PHP

Обычно 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

Оцените статью
Procodings.ru
Добавить комментарий