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

Итак, стоит задача синхронизации того, что видит пользователь и того, что есть в бд. Ранее для подобных задач использовал сервис Pusher, но, в последнее время, предпочитаю использовать Centrifuge. Предвосхищу вопрос о том, чем лучше это обычной связки redis/socket.io/node.js. Из коробки приватные каналы, простая интеграция, масштабирование, api, история сообщения в канале, события отписки и подписки пользователей на канал и много другое, что позволяет крайне быстро построить нужный прототип приложения, без раздувания стека технологий. Это работает для меня, у каждого свой путь. Кстати, язык на бекенде — php, и, соответственно, на фронтенде — js.

Что из этого получилось, некоторые нюансы — можете посмотреть ниже.

Здесь буду складывать ссылки на следующие заметки.
1. Собираем простенький прототип
2. Вводим приватные каналы
3. Пишем простенькую crm в качестве примера

Собираем простенький прототип


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



Для простоты брокер на данном этапе будет работать в insecure mode:

  • Отключение проверки токена и временных меток;
  • Разрешение анонимного доступа ко всем каналам;
  • Клиентам разрешено публиковать сообщения во все каналы;
  • Убрана проверка соединения.

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

Дебют


Для начала необходимо развернуть центрифугу на сервере. Делается это в несколько простейших шагов:

  1. Выбираем необходимый исполняемый файл под свою платформу в репозитории;
  2. В качестве транспорта предпочитаю использовать Redis. Но, если хочется, то можно и по http.
  3. Пишем небольшой конфиг для нашего проекта;
  4. Стартуем центрифугу на 8002 порту и загоняем все это дело под nginx.

Миттельшпиль


Центрифуга работает, готова к подсоединению клиентов и отправки им сообщений. Теперь дело за бекендом.

Генерируем composer.json, делаем composer require predis/predis, пишем пару классов. Собственно сам модуль отправки сообщений в брокер и транспорт, через редис. Т.к. центрифуга на данном этапе работает в insecure mode, токены генерировать нет нужды и задача еще более упрощается.



Если кому интересно — конечный вариант наброска и демо. В демо точки уничтожаются каждые 240 секунд, просто можно пощелкать кнопку на добавление новых в историю и обновить страницу. В реальном мире, конечно, данные берутся с api, в следующих заметках мы вернемся к этому.

Как итог, теперь мы можем отправлять с бекенда пользователям сообщения. Примерно следующим образом:

...
require __DIR__.'/vendor/autoload.php';

use Push\Push;
use RedisCommunucate\Communicate;
use Predis\Client;

$redisClient = new Communicate(new Client());
$push = new Push('development', 'secret', $redisClient);
//Точку в канал "chart"
$push->publish('chart', ['point' => rand(0,10)]);
...

...
//Отправляем в центрифугу командой "publish"
public function publish($channel, $data = [])
    {
        return $this->send("publish", ["channel" => $channel, "data" => $data]);
    }
public function send($method, $params = [])
    {
        if (empty($params)) {
            $params = new \StdClass();
        }
        $data = json_encode(["method" => $method, "params" => $params]);
        return
            $this->communicate
                ->communicate(
                    $this->projectKey,
                    ["data" => $data]
                );
    }
...

...
//Через redis rpush
public function communicate($projectId, $data)
    {
        $toSend = array(
            "project" => $projectId
        );
        $data['data'] = json_decode($data['data']);
        $toSend = array_merge($toSend, $data);
        $toSend['data'] = array($toSend['data']);
        $this->redisClient->rpush("centrifugo.api", json_encode($toSend));
    }
...

И на этом первую часть задачи можно считать выполненной. Теперь дело за фронтендом.

Эндшпиль


В качестве библиотеки для графика возьмем c3.js. Подключаем c3, sockjs, centrifuge.js.

...
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.js" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/sockjs/1.0/sockjs.min.js" type="text/javascript"></script>
<script src="https://rawgit.com/centrifugal/centrifuge-js/master/centrifuge.js" type="text/javascript"></script>
...

Соединяемся с центрифугой, ловим точки. И сделаем кнопку, чтобы дергать сервер на генерацию новых точек.

...
    var chart;
    // Конфигурируем.
    var centrifuge = new Centrifuge({
        url: 'http://socket.logistics.app/connection',
        project: 'development',
        insecure: true
    });
    // Когда центрифуга подключилась,
    centrifuge.on('connect', function() {
        // просто подписываемся на канал,
        var subscription = centrifuge.subscribe('chart', function(message) {
            // ловим точки и добавляем в график,
            chart.flow({
                columns: [
                    ['sample', message.data.point]
                ],
                duration: 100
            });
        });
        // не забывая взять предыдущие. В реальном мире - берем с api.
        subscription.on('ready', function() {
            subscription.history(function(message) {
                var data = message.data.map(function(point){
                    return point.data.point
                });
                data.reverse();
                data.unshift('sample');
                chart = c3.generate({
                    bindto: '#chart',
                    data: {
                        columns: [
                            data
                        ]
                    }
                });
            });
        });
    });
    // Запускаем коннект
    centrifuge.connect();
    // Дергаем сервер, чтобы отправлял в центрифугу сообщения через редис
    $('.random').click(function(){
        $.get('/random.php');
    });
...

Задача выполнена, открываем страницу с демо на разных устройствах или просто в разных браузерах. Когда появляется новая точка — график обновляется. Да, на данном этапе это все работает крайне небезопасно. Но для понимания концепции — в самый раз. Из известных проблем, решение которых я бы хотел описать в следующих заметках:

  • Если между получением хистори и подпиской на канал придут новые точки, то они потеряются.
  • Сортировка точек. Если они будут приходить крайне часто, то будут добавляться в неправильном порядке.
  • Мы рассмотрели только добавление данных в конец. Но есть еще списочные данные. И, допустим, мы используем datatables для показа табличек. И данные обновляются, приходят с центрифуги, но мы находимся на третьей странице таблицы с фильтрами/сортировками. И вопрос в том, как синхронизировать это.

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

Комментарии (10)


  1. ketrin7
    13.10.2015 11:54
    +2

    Valeriy_tw3eX вы действительно правы, кому задача тривиальна, а кому нет, в том числе и мне ;)


    1. Valeriy_tw3eX
      13.10.2015 12:00

      Есть такой нюанс, я решил оформить в виде заметки после того, когда мне колега скинул наброски таск менеджера, где нужно обновлять страницу, чтобы просмотреть новые) И это решение легко переложить на ror/python. Ну и на angularjs/backbone, вместо ванильного джс.


      1. ketrin7
        13.10.2015 13:45
        -3

        наброски таск менеджера, где нужно обновлять страницу, чтобы просмотреть новые
        :)))


  1. auine
    13.10.2015 14:05
    -2

    Первые шаги PHP, в мире реального времени, вы ввиду имели ))?

    Создавая подобного рода новые проекты «рич» фронт приложений, я не вижу смысла включать в стек PHP в принципе.
    Особенно если вы не юзает такие недокопии node рантайма, как симфони, фалькон или хотя бы кеши которые подвешивают ваше приложение в памяти.
    Данный пример это скорей — как прибить гвоздями то все хорошее, что нет в мире PHP разработки.


    1. Valeriy_tw3eX
      13.10.2015 14:10

      Там из php только коннектор, по факту. А мир php разработки — не сказал бы, что все настолько плохо. Я и сам обычно не пишу большие проекты на нем, но для того, чтобы сделать mvp — разработчиков куча. Если грамотно ими оперировать и ставить четко задачи, то проблем не возникает.

      Обычно новорожденный проект может быть написан хоть в одном файле) А после запуска уже могут появиться деньги на хороших ребят и на хорошие решения. Это по моему опыту если судить.


    1. SerafimArts
      19.10.2015 18:36

      У вас очень большие стереотипы по поводу php. Он работает быстрее, нежели нода, кушает в 2-3 раза меньше, что проверено на практике. Раньше я тоже как вы считал, что реалтайм на пыхе — это не сильно решение под продакшн, пока на практике не увидел как приложение на Laravel 5 стеке кушает 10 метров, а такое же приложение на ноде в 5 раз больше под приложение + монитор forever.


      1. auine
        19.10.2015 18:56

        Каким таким образом php работает быстрей ?)) Дайте линк на бенчмарк, где хотя бы какой-то кейс с php быстрей ноды.
        И по памяти особенно


        1. SerafimArts
          19.10.2015 19:34

          Бенчмарков нет, есть лишь визуальное воприятие. Либо с алгоритмом в js версии беда, либо нода притормаживала. А с памятью — можно убедиться на практике: https://github.com/LaravelRUS/GitterBot master ветка на L5, devel на ноде.


          1. auine
            19.10.2015 19:42

            Ну это не серъезный халивар) Я могу вам сказать почему, php всегда будет медленей основываясь не на «восприятии». Хотя учитывая, мой позапрошлый коммент, это без толку, всеравно не дойдет))


  1. akubintsev
    13.10.2015 21:37

    Есть еще более тонкий стек технологий под описанную задачу http://socketo.me/docs/push и даже без Redis