Или как мы использовали Ratchet и веб-сокеты за пределами мира узлов.

Если вы собираетесь работать в асинхронной парадигме, node.js — очевидный выбор. из коробки.

Один из наших клиентов хотел создать веб-приложение с невероятно быстрой связью между клиентом и сервером, и эта задача хорошо решалась с помощью технологии WebSocket. Поскольку WebSockets открывает соединение между браузером и сервером и поддерживает его открытым, ему не нужно устанавливать новое соединение каждый раз, когда ему нужно отправить данные. Это также позволяет серверу передавать информацию клиенту всякий раз, когда появляется новая релевантная информация, без необходимости опроса пользователя снова и снова.

Итак, почему бы не создать решение в мире узлов? Это было бы очевидным и наиболее легко реализуемым выбором.
У нашего клиента был большой опыт работы с PHP, а его библиотеки и инфраструктура уже были созданы. Перенос всего потребует значительных усилий.

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

Для решения потребуется способ создания модели цикла событий в PHP. Рэтчет был лучшим местом для начала.

Добро

Построенный на React (еще один проект с открытым исходным кодом), Ratchet позволил нам создать наш сервер WebSocket с использованием PHP. Он быстрый, хорошо работает и хорошо спроектирован, так что вы можете реализовать свои собственные компоненты и интегрировать их в свою экосистему. Это довольно модульная конструкция, разделенная на отсеки, поэтому вы можете вставлять свои собственные элементы в то, что уже построено. Ratchet также предлагает реализацию для других протоколов, которые мы сочли очень полезными, таких как WAMP.

Соревнование

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

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

Обработка блокирующих вызовов

Хотя блокирующие вызовы (такие как файловый ввод-вывод, запросы к базе данных, разрешение DNS и т. д.) вполне приемлемы при написании «обычного» PHP, они губительны в асинхронной среде. Если бы вы сделали вызов базы данных в цикле событий React, он заблокировался бы, и ничего больше не обрабатывалось бы в цикле событий, пока база данных не завершит запрос и не вернется. Это означает, что все ваши другие пользовательские запросы будут сидеть там и ничего не делать! Мы можем сделать лучше, чем это.

Чтобы решить эту проблему, мы создали рабочие процессы в PHP, которые будут работать как отдельные процессы. Эти рабочие процессы будут обрабатывать все наши блокирующие вызовы базы данных, а поскольку они работают как отдельный процесс, цикл обработки событий React может продолжать обрабатывать запросы пользователей.

Здорово! Это решило проблему с блокировкой. Теперь нам просто нужно было подключить наших воркеров к нашему процессу Ratchet, чтобы Ratchet мог сообщить воркерам, какие запросы к базе данных выполнять, а наши воркеры могли отправлять результаты обратно в Ratchet. Для этого мы решили использовать брокер сообщений. В частности, мы обратились к RabbitMQ, быстрой реализации брокера сообщений с открытым исходным кодом.

Мы также решили использовать STOMP в качестве нашего протокола для связи через RabbitMQ (используя React/STOMP), и мы «обещали» большую часть нашего кода, используя React/Promise, чтобы сделать его более дружественным к асинхронности.

Запись в сеансы PHP

По умолчанию Ratchet позволяет загружать данные сеанса пользователя и иметь доступ только для чтения. Мы хотели иметь возможность не только читать данные сеанса, но и изменять их, а также записывать обратно. Было много странных, нетривиальных пограничных случаев, с которыми нам приходилось справляться, но двумя самыми большими вопросами были:

Как вы можете заблокировать сеанс пользователя, не блокируя цикл событий React? и

Когда вы хотите изменить и записать данные обратно в сеанс пользователя, как убедиться, что вы не испортите данные?

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

Как мы знаем, такое блокирующее поведение неприемлемо в нашем цикле обработки событий React, поэтому нам нужен был способ получше.

Поскольку мы использовали Redis в качестве хранилища сеансов, мы сначала искали библиотеку сеансов, в которой уже была какая-то реализация блокировки и которая работала с Redis. В итоге остановились на SncRedisBundle. У него была реализация блокировки Spin Lock, которая неплохо работала для традиционного варианта использования PHP. Мы расширили класс RedisSessionHandler и вместо использования PHP-функции usleep (которая является блокирующей) вызвали цикл обработки событий React, чтобы попытаться снова заблокировать сеанс через определенный период времени (используя функцию addTimer).

Затем нам предстояло решить вопрос о том, чтобы не испортить пользовательские данные. (Оказывается, это очень важно!) В нашем решении всякий раз, когда мы хотели изменить данные, мы блокировали сеанс, считывали все данные (обновляли имеющиеся у нас данные), изменяли значения, которые мы хотели изменить, и затем сохраните его и разблокируйте сеанс. Обычно это была бы блокировка, но благодаря описанному выше механизму неблокирующей блокировки и использованию проекта Predis\Async мы смогли сделать это асинхронным способом.

Отлично! Теперь, когда наша серверная реализация была завершена, мы обратились к клиентскому коду, который оказался намного проще. После некоторых поисков мы остановились на использовании Autobahn и протокола WAMP для связи с нашим сервером WebSocket.

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