Первые шаги в мир веба в реальном времени

a743f907ad2244899868b3fa01a7087c.png

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

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

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


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


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

5e37bd690a9943d3815b54225ba8a8bc.gif

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

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


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

Дебют


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

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


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


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

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

4510538f382b49a696314bd5eda1bbe1.png

Если кому интересно — конечный вариант наброска и демо. В демо точки уничтожаются каждые 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 для показа табличек. И данные обновляются, приходят с центрифуги, но мы находимся на третьей странице таблицы с фильтрами/сортировками. И вопрос в том, как синхронизировать это.


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

© Habrahabr.ru