Пишем реал-тайм приложение с использование "Centrifugo"

#Технологии — Опубликовано 2 месяца назад

Если вы работали с "Laravel" и читали документацию, то создатель фреймворка предлагает в ней два решения для написания "real-time" приложении: "Pusher" — платный сервис от 100 подключении (>50$) и socket.io (node js).

Мне эти два решения не нравятся. Первое является платным, хотя и хорошо поддерживаемым, второе же требуется установки nodejs и разворачивания nodejs сервера. Оба решения ужасны. Если вы выбрали одно из двух, то посоветую вам использовать "Centrifugo".

Если хотите ознакомиться с решением, то здесь есть хорошая документация по которой можно найти ответы на все вопросы.

Итак, давайте попробуем написать небольшое "Laravel" приложение, которое будет показывать в реал-тайме твиты людей, которые живут в Нью-Йорке. Причем будем брать именно те твиты, которые имеют аватарки и указанную геолокацию.

Для этого мы воспользуемся данным пакетом для "Laravel": https://github.com/spatie/laravel-twitter-streaming-api , который является оберткой для работы со стримингом твитов, а также удобным "broadcaster" по данной ссылке: https://github.com/LaraComponents/centrifuge-broadcaster.

Надеюсь, вы установили "Laravel" и имеете базовое представление о работе с консолью. 

Устанавливаем первый пакет:

composer require spatie/laravel-twitter-streaming-api

Добавляем в "providers" config/app.php приложения:

'providers' => [
    ...
    Spatie\LaravelTwitterStreamingApi\TwitterStreamingApiServiceProvider::class,
];

Добавляем также в этом файле "alias":

'aliases' => [
    ...
    'TwitterStreamingApi' => Spatie\LaravelTwitterStreamingApi\TwitterStreamingApiFacade::class,
];

Публикуем конфиги для работы с пакетом командой в консоли:

php artisan vendor:publish --provider="Spatie\LaravelTwitterStreamingApi\TwitterStreamingApiServiceProvider" --tag="config"

После данной команды в конфигах вы найдете файл "config/laravel-twitter-streaming-api.php" с данным содержанием:

return [

    /**
     * To work with Twitter's Streaming API you'll need some credentials.
     *
     * If you don't have credentials yet, head over to https://apps.twitter.com/
     */

    'access_token' => env('TWITTER_ACCESS_TOKEN'),

    'access_token_secret' => env('TWITTER_ACCESS_TOKEN_SECRET'),

    'consumer_key' => env('TWITTER_CONSUMER_KEY'),

    'consumer_secret' => env('TWITTER_CONSUMER_SECRET'),
];

Надеюсь, у вас есть приложения в твиттере, потому что в последнее время они стали требовать очень много информации для созданиях оных. Если у вас есть ключи, то смело публикуйте их в ".env" файле "Laravel":

TWITTER_ACCESS_TOKEN="Ваш токен"
TWITTER_ACCESS_TOKEN_SECRET="Ваш секретный токен"
TWITTER_CONSUMER_KEY="Ваш клиентский ключ"
TWITTER_CONSUMER_SECRET="Ваш секретный ключ"

После ввода ключей и правильной установки, мы установили обертку для работы со стримингом твиттера. Чтобы продолжить работу с "real-time" составляющей, устанавливаем "broadcaster" для "centrifugo" командой:

composer require laracomponents/centrifuge-broadcaster

Опять добавляем в "config/app.php" данные строки и выводим из комментарием "BroadcastServiceProvider":

'providers' => [
    // ...
    LaraComponents\Centrifuge\CentrifugeServiceProvider::class,

    // Раскомментируем BroadcastServiceProvider
    App\Providers\BroadcastServiceProvider::class,
],

В документации на "github" указан немного ошибочный конфиг и ниже он уже исправленный. Его нужно добавить в "config/broadcasting":

'centrifuge' => [
            'driver' => 'centrifuge',
            'secret' => env('CENTRIFUGE_SECRET'), // you secret key
            'url' => env('CENTRIFUGE_URL', 'http://localhost:8000'), // centrifuge api url
            'redis_api' => env('CENTRIFUGE_REDIS_API', false), // enable or disable Redis API
            'redis_connection' => env('CENTRIFUGE_REDIS_CONNECTION', 'default'), // name of redis connection
            'redis_prefix' => env('CENTRIFUGE_REDIS_PREFIX', 'centrifugo'), // prefix name for queue in Redis
            'redis_num_shards' => env('CENTRIFUGE_REDIS_NUM_SHARDS', 0), // number of shards for redis API queue
            'verify' => env('CENTRIFUGE_VERIFY', false), // Verify host ssl if centrifuge uses this
            'ssl_key' => env('CENTRIFUGE_SSL_KEY', null), // Self-Signed SSl Key for Host (require verify=true)
        ],

Для конфигурации redis вы можете добавить данный конфиг в массивы "redis" внутри "config/database.php":

        'centrifuge' => [
            'host' => env('REDIS_HOST', '127.0.0.1'),
            'password' => env('REDIS_PASSWORD', null),
            'port' =>  env('REDIS_PORT', 6379),
            'database' => 0,
        ],

После нужно добавить данные для взаимодействия данного "broadcaster" и центрифуги:

CENTRIFUGE_SECRET=very-long-secret-key
CENTRIFUGE_URL=http://localhost:8000
CENTRIFUGE_REDIS_API=false
CENTRIFUGE_REDIS_CONNECTION=centrifuge
CENTRIFUGE_REDIS_PREFIX=centrifugo
CENTRIFUGE_REDIS_NUM_SHARDS=0
CENTRIFUGE_SSL_KEY=/etc/ssl/some.pem
CENTRIFUGE_VERIFY=false

Стандартный "broadcaster" в ".env" фреймворка меняем на "centrifugo":

BROADCAST_DRIVER=centrifuge

Все, теперь у нас есть стриминг твиттера и пакет для работы с "centrifuge". Давайте теперь установим само "centrifugo". Для этого мы переходит на страницу проекта на https://github.com/centrifugal/centrifugo, где в папке релизов берем нужный нам архив под нашу систему. 


Скачиваем нужный архив и распаковываем в папку:

Попробуем завести без конфига:


Запускается и работает. Вот только для нормальной работы и сохранности в будущем, нам нужно будет создать типовой "config.json", в котором будут храниться настройки "Centrifugo". Вот пример типового конфига для локальной работы:

{
  "secret": "Ваш секретный ключ",
  "namespaces": [
    {
      "name": "public",
      "publish": true,
      "watch": true,
      "presence": true,
      "join_leave": true,
      "history_size": 10,
      "history_lifetime": 30
    }
  ],
  "log_level": "debug",
  "admin": true,
  "admin_password": "Ваш секретный ключ",
"admin_secret": "Ваш секретный ключ",
"web": true, "port": "8000" }

Конфиг можно установить возле "centrifugo" или указать командой при запуске в виде:

./centrifugo --config=config.json

После запуска "centrifugo" с конфигом, где есть секретный ключ и номер порта, добавляем эти данные в ".env" настроек "broadcaster" который мы установили ранее:

CENTRIFUGE_SECRET="Ваш секретный ключ"
CENTRIFUGE_URL=http://localhost:8000
CENTRIFUGE_REDIS_API=false
CENTRIFUGE_REDIS_CONNECTION=centrifuge
CENTRIFUGE_REDIS_PREFIX=centrifugo
CENTRIFUGE_REDIS_NUM_SHARDS=0
CENTRIFUGE_SSL_KEY=/etc/ssl/some.pem
CENTRIFUGE_VERIFY=false

Все. На бэкенд стороне мы установили все нужные нам пакеты и прописали все конфиги. 

Можем протестировать сначала работу "twitter streaming api", создав команду в "Laravel" под названием "ListenForHashTags":

php artisan make:command ListenForHashTags

После чего чего созданный в папке "app/Console/Commands" файл "ListenForHashTags.php" приведем к следующему виду:


namespace App\Console\Commands;

use Illuminate\Console\Command;
use TwitterStreamingApi;

class ListenForHashTags extends Command
{
     /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'twitter:listen-for-hash-tags';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Listen for hashtags being used on Twitter';
    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        TwitterStreamingApi::publicStream()
            ->whenHears('#laravel', function (array $tweet) {
                dump("{$tweet['user']['screen_name']} tweeted {$tweet['text']}");
            })
            ->startListening();
    }
}

Если ввести команду в консоли "php artisan list", то мы найдем её и найдем описание к ней. Ну а если ввести:

php artisan twitter:listen-for-hash-tags

То мы запустим эту команду и в консоли будем получать все твиты с хештегом "laravel". Вот только, нам это не надо.

Для этого приложения создаем миграцию:

php artisan make:migration create_users_table

Заполняем её данными полями:

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTweetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->increments('id');
            $table->bigInteger('tweet_id');
            $table->string('name');
            $table->text('text');
            $table->string('image');
            $table->text('coordinates');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tweets');
    }
}

Создаем пустую модель под названием "Tweet.php" (лучше в папке Model).

namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class Tweet extends Model
{


}

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

namespace App\Services;

use App\Model\Tweet;
use LaraComponents\Centrifuge\Centrifuge;

class TwitterService
{
    public function __construct(Centrifuge $centrifuge)
    {
        $this->centrifuge = $centrifuge;
    }

    public function create(array $array)
    {
    	$tweet = new Tweet;
    	$tweet->tweet_id = $array['id'];
    	$tweet->name = $array['user']['name'];
        $tweet->text = $array['text'];
    	$tweet->image = $array['user']['profile_image_url_https'];
    	$tweet->coordinates = serialize([$array['geo']['coordinates'][0], $array['geo']['coordinates'][1]]);
    	$tweet->save();

        $this->centrifuge($tweet);
    }

    public function get_array()
    {
        $tweets = Tweet::get();

        $tweets = $this->decorate($tweets);

        return $tweets;
    }

    private function centrifuge(Tweet $tweet)
    {
        $this->centrifuge->publish('twitter', [
            'tweet' => [
                'id' => $tweet->id,
                'tweet_id' => $tweet->tweet_id,
                'text' => $tweet->text,
                'name' => $tweet->name,
                'image' => $tweet->image,
                'coordinates' => unserialize($tweet->coordinates)
            ]
        ]);
    }

    private function decorate($collection)
    {
        $array = [];

        foreach ($collection as $tweet) {
            $array[] = [
                'id' => $tweet->id,
                'tweet_id' => $tweet->tweet_id,
                'text' => $tweet->text,
                'name' => $tweet->name,
                'image' => $tweet->image,
                'coordinates' => unserialize($tweet->coordinates)
            ];
        }

        return $array;
    }


}

Создаем контроллер, который будет работать с классом выше:

namespace App\Http\Controllers\Twitter;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Services\TwitterService;

class TwitterController extends Controller
{
    protected $tweetService;

    public function __construct(TwitterService $tweetService)
    {
        $this->tweetService = $tweetService;
    }

    public function twitter()
    {
        return view('pages.twitter');
    }

    public function get()
    {
        $tweets = $this->tweetService->get_array();

        return response()->json($tweets);
    }
}

В нем всего два метода, один отдает нужную нас страницу представления, тогда как второй отдает нам твиты в формате "json". Добавляем "роуты" к данным контроллерам в "web.php":

Route::get('twitter', ['as' => 'twitter', 'uses' => 'Twitter\[email protected]']);
Route::get('twitter/get', ['as' => 'twitter.get', 'uses' => 'Twitter\[email protected]']);

Так как задача состоит в том, чтобы находить свежие твиты из города Нью-Йорк, то наша команда "ListenForHashTags" изменяется следующим образом:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use TwitterStreamingApi;
use App\Services\TwitterService;

class ListenForHashTags extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'twitter:listen-for-hash-tags';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Listen for New York tweets';

    protected $twitterService;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(TwitterService $twitterService)
    {
        parent::__construct();

        $this->twitterService = $twitterService;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        TwitterStreamingApi::publicStream()
            ->whenFrom([[-74, 40, -73, 41]], function (array $tweet) {
                if (isset($tweet['geo']) && isset($tweet['user']['profile_background_image_url_https'])) {
                    $this->twitterService->create($tweet);
                }
            })
            ->startListening();
    }
}

Где "->whenFrom([[-74, 40, -73, 41]]" это координаты самого города, а данные из массива "$tweet['geo']" и "$tweet['user']['profile_background_image_url_https']" это нужные нам данные геолокации и изображения профиля.

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

Но посмотреть мы их не сможем. Поэтому надо надо создать "view" и компонент для представлениях данных.

Вьюшка здесь.

Компонент (посмотреть полностью):

export default {
        mounted() {
          var self = this;
          ymaps.ready(yandexInit);
          self.getTweets();

          centrifuge.subscribe('twitter', function(message) {
             self.addTweet(message.data.tweet);
          });

          function yandexInit() {
              window.yandex_map = new ymaps.Map("map", {
                  center: [40.7143528, -74.0059731],
                  zoom: 9
              }, {
                  searchControlProvider: 'yandex#search'
              });
          }
        },
        methods: {
            addTweet: function (value) {
              let placemark = {
                  coords: value.coordinates,
                  options: {
                    iconLayout: 'default#image',
                    iconImageHref: value.image,
                    iconImageSize: [48, 48]
                  },
                  properties: {
                    hintContent: value.name,
                    balloonContent: '
' + value.text + '
' } }; let myPlacemark; myPlacemark = new ymaps.Placemark(placemark.coords); myPlacemark.properties.set(placemark.properties); myPlacemark.options.set(placemark.options); window.yandex_map.geoObjects.add(myPlacemark); }, getTweets: function() { var self = this; axios.get('/twitter/get').then(function (response) { $.each(response.data, function(index, value ){ self.addTweet(value); }); }).catch(error => { if (error.response) { $.each(error.response.data, function(index, value ){ alert(value); }); } }); } } }

Как вы могли заметить. Мы используем подписку через код:

          centrifuge.subscribe('twitter', function(message) {
             self.addTweet(message.data.tweet);
             console.log('Real-time update');
          });

Мы его устанавливаем командой:

npm install centrifuge

А потом инициируем в том месте, где обычно находится инициация "echo", то есть, в файле "bootstrap.js" что в папке "resources/js" (Laravel 5.7):


// window._ = require('lodash');
window.Popper = require('popper.js').default;

/**
 * We'll load jQuery and the Bootstrap jQuery plugin which provides support
 * for JavaScript based Bootstrap features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

try {
    window.$ = window.jQuery = require('jquery');

    require('bootstrap');
} catch (e) {}

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

/**
 * Next we will register the CSRF Token as a common header with Axios so that
 * all outgoing HTTP requests automatically have it attached. This is just
 * a simple convenience so we don't have to attach every token manually.
 */

let token = document.head.querySelector('meta[name="csrf-token"]');

if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

import Centrifuge from 'centrifuge';

window.centrifuge = new Centrifuge({
    url: 'Ваш url',
    user: 'Id пользователя',
    timestamp: 'Временная метка',
    token: 'token';
});


/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

// import Echo from 'laravel-echo'

// window.Pusher = require('pusher-js');

// window.Echo = new Echo({
//     broadcaster: 'pusher',
//     key: process.env.MIX_PUSHER_APP_KEY,
//     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
//     encrypted: true
// });

Ну и чтобы подключиться, нужно добавить эту строчку в конце вашего "app.js":

const app = new Vue({
    el: '#app'
});

centrifuge.connect();

Вот теперь, после запуска "npm run watch", компонент соберется и вы сможете взглянуть на твиты нью-йоркцев на yandex карте:

Если вы не поняли, откуда брать метку и токен, то данный демонстрационный код даст вам это понять:

    public function token()
    {
        $time = time();
        $token = $this->centrifuge->generateToken(1, $time);

        return ['user' => 1, 'time' => $time, 'token' => $token];
    }

Демонстрационная работа приложения доступна здесь: https://ginkida.ru/twitter

Код приложения можете посмотреть здесь: https://bitbucket.org/Ginkida/ginkida

Чтобы видеть и оставлять комменты зайдите через:
Вконтакте Facebook Twitter Google+