Статьи

Веб-сокеты на вашем синхронном сайте

Эта статья была рецензирована Верной Анчетой . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Я всегда говорю о написании асинхронного PHP-кода, и по какой-то причине. Я думаю, что это полезно, чтобы получить новые перспективы — быть открытыми для новых парадигм программирования.

Асинхронная архитектура распространена в других языках программирования, но она только находит свое место в PHP. Проблема в том, что эта новая архитектура имеет свою цену.

Я не говорю об этой стоимости достаточно.

Когда я рекомендую такие фреймворки, как Icicle , ReactPHP и AMPHP , очевидным местом для начала является создание чего-то нового. Если у вас есть существующий сайт (возможно, работающий через Apache или Nginx), добавить демонизированные службы PHP в ваше приложение, вероятно, не так просто, как просто начать заново.

Абстрактное изображение потоковой передачи данных параллельно

Требуется большая работа по интеграции новых асинхронных функций в существующие приложения. Часто есть веские причины и большие преимущества, но переписывание всегда сложно продать. Возможно, вы сможете получить некоторые преимущества параллельного выполнения без использования цикла обработки событий. Возможно, вы сможете получить некоторые преимущества веб-сокетов без новой архитектуры.

Я собираюсь показать вам сервис Sockets-as-a-Service, который называется Socketize . Попробуйте сказать это несколько раз, вслух …

Примечание. В веб-сокетах используется довольно много JavaScript. К счастью, нам не нужно создавать сложные цепочки сборки. Вы можете найти пример кода для этого урока здесь .

Настроить!

Давайте настроим простой пример CRUD. Загрузите скрипт SQL и импортируйте его в локальную базу данных. Тогда давайте создадим конечную точку JSON:

$action = "/get";
$actions = ["/get"];

if (isset($_SERVER["PATH_INFO"])) {
    if (in_array($_SERVER["PATH_INFO"], $actions)) {
        $action = $_SERVER["PATH_INFO"];
    }
}

$db = new PDO(
    "mysql:host=localhost;dbname=[your_database_name];charset=utf8",
    "[your_database_username]",
    "[your_database_password]"
);

function respond($data) {
    header("Content-type: application/json");
    print json_encode($data) and exit;
}

Этот код будет определять, будет ли сделан запрос к действительной конечной точке (в настоящее время поддерживается только /get Мы также устанавливаем соединение с базой данных и определяем метод, позволяющий нам реагировать на браузер с минимальными усилиями. Затем нам нужно определить конечную точку для /get

 if ($action == "/get") {
    $statement = $db->prepare("SELECT * FROM cards");
    $statement->execute();

    $rows = $statement->fetchAll(PDO::FETCH_ASSOC);

    $rows = array_map(function($row) {
        return [
            "id" => (int) $row["id"],
            "name" => (string) $row["name"],
        ];
    }, $rows);

    respond($rows);
}

/get Это то, что мы хотим, чтобы этот PHP-скрипт делал, если больше ничего не требуется. Это на самом деле станет более полезным, когда мы добавим функциональность. Мы также определяем список поддерживаемых действий. Вы можете думать об этом как о белом списке, чтобы защитить скрипт от странных запросов.

Затем мы проверяем свойство сервера PATH_INFO Посмотрим (чуть-чуть) откуда взялась эта информация. Просто представьте, что он содержит путь URL. Так, для http: // localhost: 8000 / foo / bar эта переменная будет содержать /foo/bar Если оно установлено и в массиве разрешенных действий мы переопределяем действие по умолчанию.

Затем мы определяем функцию для ответа браузеру. Он установит соответствующий заголовок типа контента JSON и напечатает ответ в кодировке JSON, который мы можем использовать в браузере.

Затем мы подключаемся к базе данных. Детали вашей базы данных, вероятно, будут другими. Обратите особое внимание на имя вашей базы данных, имя пользователя и пароль базы данных. Остальное должно быть хорошо как есть.

Для действия /getcards Затем для каждой карты мы приводим столбцы к соответствующему типу данных (используя функцию array_map Наконец, мы передаем их функции respond

Чтобы запустить это и получить соответствующие данные PATH_INFO

 $ php -S localhost:8000 server.php

Встроенный сервер разработки PHP не подходит для рабочих приложений. Используйте правильный веб-сервер. Здесь его удобно использовать, потому что нам не нужно настраивать рабочий сервер на нашей локальной машине, чтобы иметь возможность тестировать этот код.

На этом этапе вы должны увидеть следующее в браузере:

Начальный вывод JSON

Если вы этого не видите, вам, вероятно, следует попытаться решить эту проблему, прежде чем продолжить. Вы можете использовать альтернативные структуры базы данных или данные, но вам нужно быть осторожным с остальными примерами.

Теперь давайте сделаем клиента для этих данных:

 <!doctype html>
<html lang="en">
    <head>
        <title>Graterock</title>
    </head>
    <body>
        <ol class="cards"></ol>
        <script type="application/javascript">
            fetch("http://localhost:8000/get")
                .then(function(response) {
                    return response.json();
                })
                .then(function(json) {
                    var cards = document.querySelector(".cards");

                    for (var i = 0; i < json.length; i++) {
                        var card = document.createElement("li");
                        card.innerHTML = json[i].name;
                        cards.appendChild(card);
                    }
                });
        </script>
    </body>
</html>

Мы создаем пустой список .cards Для этого мы используем функцию fetch Он возвращает обещания, которые являются композиционным шаблоном для обратных вызовов.

Это почти самая техническая часть всего этого. Я обещаю, что это станет легче, но если вы хотите узнать больше о fetchhttps://developer.mozilla.org/en/docs/Web/API/Fetch_API . Он работает в последних версиях Chrome и Firefox, поэтому используйте их, чтобы эти примеры работали!

Мы можем использовать этот клиент в нашем скрипте PHP-сервера:

 $action = "/index";
$actions = ["/index", "/get"];

// ...

if ($action == "/index") {
    header("Content-type: text/html");
    print file_get_contents("index.html") and exit;
}

Теперь мы можем загрузить страницу клиента и получить список карточек, все по одному и тому же сценарию. Нам даже не нужно менять способ запуска сервера разработки PHP — это должно сработать!

Чехол для веб-розеток

Это все довольно стандартные вещи PHP / JavaScript. Ну, мы используем новую функцию JavaScript, но она почти такая же, как $.ajax

Если мы хотим добавить к этому функциональность в реальном времени, мы можем попробовать несколько хитростей. Скажем, мы хотели добавить удаление карты в реальном времени; мы могли бы использовать запросы Ajax для удаления карточек и опрос Ajax для автоматического обновления других клиентов.

Это будет работать, но это также добавит значительный трафик на наш HTTP-сервер (даже для дублированных ответов).

В качестве альтернативы, мы можем открыть постоянные соединения между браузерами и сервером. Затем мы могли бы выталкивать новые данные с сервера и избегать непрерывного опроса. Эти постоянные соединения могут быть веб-сокетами, но есть проблема …

PHP был создан и в основном используется для быстрого обслуживания запросов. Большинство сценариев и приложений не предназначены для длительных процессов. Из-за этого обычные PHP-скрипты и приложения действительно неэффективны для одновременного удержания многих открытых соединений.

Это ограничение часто подталкивает разработчиков к другим платформам (например, NodeJS) или к новым архитектурам (например, предоставляемым Icicle, ReactPHP и AMPHP). Это одна из задач, которые Socketize стремится решить.

Socketize на помощь!

Для краткости мы собираемся использовать версию Socketize без аутентификации. Это означает, что мы сможем читать и записывать данные без аутентификации каждого пользователя. Socketize поддерживает аутентификацию, и это рекомендуемый (и даже предпочтительный) метод.

Создайте учетную запись Socketize, перейдя по адресу https://socketize.com/register :

Socketize создание аккаунта

Затем перейдите на https://socketize.com/dashboard/member/payload/generate и сгенерируйте ключ для admin Запишите это вниз. Перейдите на https://socketize.com/dashboard/account/application для ключа своей учетной записи («Открытый ключ»). Теперь нам нужно добавить новый JavaScript на нашу HTML-страницу:

 <script src="https://socketize.com/v1/socketize.min.js"></script>
<script type="application/javascript">
    var params = {
        "public_key": "[your_public_key]",
        "auth_params": {
            "username": "admin",
            "payload": "[your_admin_key]"
        }
    };

    var socketize = new Socketize.client(params);

    socketize.on("login", function(user) {
        var user = socketize.getUser();
        console.log(user);
    });
</script>

Если вы замените [your_public_key][your_admin_key] Вы также должны увидеть объект, описывающий учетную запись пользователя Socketize, которая вошла в систему. Обратите внимание на id

Что это значит? Наша HTML-страница теперь подключена к базе данных Socketize (для нашей учетной записи). Мы можем читать и писать из именованных списков сообщений. Давайте изменим наш серверный скрипт, чтобы записать начальный список карт в именованный список Socketize. Все запросы API Socketize имеют вид:

 curl "https://socketize.com/api/[method]?[parameters]"
    -u [your_public_key]:[your_private_key]
    -H "Accept: application/vnd.socketize.v1+json"

Мы можем создать функцию request

 function request($method, $endpoint, $parameters = "")
{
    $public = "[your_public_key]";
    $private = "[your_private_key]";

    $auth = base64_encode("{$public}:{$private}");

    $context = stream_context_create([
        "http" => [
            "method" => $method,
            "header" => [
                "Authorization: Basic {$auth}",
                "Accept: application/vnd.socketize.v1+json",
            ]
        ]
    ]);

    $url = "https://socketize.com/api/{$endpoint}?{$parameters}";
    $response = file_get_contents($url, false, $context);

    return json_decode($response, true);
}

Этот метод абстрагирует общий код для использования конечных точек, которые мы настроили. Мы можем увидеть это в действии со следующими запросами:

 $json = json_encode([
    "id" => 1,
    "name" => "Mysterious Challenger",
]);

request(
    "PUT",
    "push_on_list",
    "key=[your_user_id]:cards&value={$json}"
);

request(
    "GET",
    "get_list_items",
    "key=[your_user_id]:cards"
);

Вероятно, лучше всего использовать надежную библиотеку HTTP-запросов, такую ​​как GuzzlePHP , для отправки запросов сторонним сервисам. Также есть официальный PHP SDK по адресу: https://github.com/socketize/rest-php , но я предпочитаю просто использовать эти лаконичные методы против JSON API.

Функция request Вам нужно заменить [your_public_key][your_private_key][your_user_id] Два примера вызова должны тогда работать для вас. cards Вы можете думать о Socketize как о HTTP-версии хранилища объектов.

Мы можем настроить нашу HTML-страницу, чтобы вытащить элементы из этого списка. Пока мы на этом, мы можем добавить кнопку для удаления ненужных карт:

 var socketize = new Socketize.client(params);
var cards = document.querySelector(".cards");

cards.addEventListener("click", function(e){
    if (e.target.matches(".card .remove")) {
        e.stopPropagation();
        e.preventDefault();

        socketize.publish("removed", e.target.dataset._id);
        socketize.updateListItem("cards", null, e.target.dataset._id);
    }
});

socketize.subscribe("removed", function(_id) {
    var items = document.querySelectorAll(".card");

    for (var i = 0; i < items.length; i++) {
        if (items[i].dataset._id == _id) {
            items[i].remove();
        }
    }
});

socketize.on("login", function(user) {
    var user = socketize.getUser();

    socketize.getListItems("cards")
        .then(function(json) {
            for (var i = 0; i < json.length; i++) {
                var name = document.createElement("span");
                name.className = "name";
                name.innerHTML = json[i].name;

                var remove = document.createElement("a");
                remove.dataset._id = json[i]._id;
                remove.innerHTML = "remove";
                remove.className = "remove";
                remove.href = "#";

                var card = document.createElement("li");
                card.dataset._id = json[i]._id;
                card.className = "card";
                card.appendChild(name);
                card.appendChild(remove);

                cards.appendChild(card);
            }
        });
});

Метод match Это не единственный способ сделать это, но он намного чище, чем альтернатива.

В этом примере мы добавляем слушатель события click, перехватывая щелчки на элементах .remove Это называется делегированием событий, и это очень эффективно! Таким образом, нам не нужно беспокоиться об удалении прослушивателей событий для каждого элемента .remove

При каждом .removeremoval В то же время мы публикуем событие removal Мы также прислушиваемся к событиям dataset

Мы можем обновить свойства data-* Это JavaScript-представление атрибутов _id Таким образом, мы можем хранить Socketize _id каждой карты. Наконец, мы корректируем код для создания новых элементов и добавляем сгенерированные элементы к элементу .cards

Куда мы отправимся отсюда?

Этот крошечный пример должен показать, как начать делать полезные вещи с помощью Socketize. Мы добавили связь через веб-сокет (чтение и запись в именованные списки и публикация специальных типов событий). Что удивительно, так это то, что мы сделали это без существенного изменения нашего серверного кода. Нам не нужно было переписывать цикл событий. Мы просто добавили одну или две функции и могли передавать события в браузер.

Подумайте об интересных вещах, которые вы могли бы сделать с этим. Каждый раз, когда вы сохраняете запись в базе данных, вы можете отправить событие в браузер. Вы можете подключить геймеров друг к другу. Вы можете предупреждать участников конференции, когда они находятся рядом с посетителями, вопросами. Все это в традиционной кодовой базе PHP.

Если вы хотите улучшить свои навыки, подумайте о создании интерфейса для добавления и изменения порядка карт. Если у вас есть вопросы или вы можете подумать о других интересных применениях для этого вида постоянного общения; оставьте комментарий ниже.