Как связать Socket.io с Yii2

Введение

В даной статье я буду рассматривать конкретную задачу и ее решение.

Мы рассмотрим следующие вопросы:

  1. Как связать Socket.io с Yii2;
  2. График в реальном времени с использованием Yii2, Socket.io и Redis;
  3. Настройка Socket.io под Yii2 через Redis;

Задача

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

{
    "userId": "134256", 
    "currencyFrom": "EUR", 
    "currencyTo": "GBP", 
    "amountSell": 1000, 
    "amountBuy": 747.10, 
    "rate": 0.7471, 
    "timePlaced" : "24-JAN-15 10:27:44", 
    "originatingCountry" : "FR"
}

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

Подход

Я пропущу принятие и обработку даных и сразу перейду к графику в реальном времени. Кому интересно, то постановку и решение этой задачи можна найти здесь. Для работы с графиком в реальном времени я буду использовать socket.io. Когда кто-то будут отсылать сообщения на Message Consumer (ConsumerController.php::index()) url, то мы будем отображать страну, с которой это сообщение было отослано. К примеру, если Вы послали сообщение с "originatingCounty": "CA", тогда мы должны увидеть 'CA' над Канадой на карте и сообщение должно появится в таблице сообщений. CA должно исчезнуть через несколько секунд после обработки. Вот пример: Real-time world map

socket.io клиент находится в web/js/socket-client.js файле.

Технологии

  1. Apache v2.4(website)
  2. PHP 5.4.0.(website)
  3. Redis v3(website)
  4. NodeJS v6(website)
  5. Socket.io(website)
  6. Composer(website)
  7. Codeception test framework(website)
  8. JQuery(website)
  9. Yii2 Framework(website)
  10. Bootstrap(website)

Настройки и решение

Установка Yii2

Для установки Yii2 фреймворка с базовыми возможностями, запустите следующую команду:

php composer.phar create-project yiisoft/yii2-app-basic basic 2.0.6

Установка NodeJS

Инструкция по установке здесь: (https://nodejs.org/en/download/)[https://nodejs.org/en/download/]

Установка и настройка Redis DB

Скачайте, розпакуйте и скомпилируйте Redis:

$ wget http://download.redis.io/releases/redis-3.0.4.tar.gz
$ tar xzf redis-3.0.4.tar.gz
$ cd redis-3.0.4
$ make

Бинарники, которые были скомпилированы тепер доступны в папке src. Запустите Redis:

$ src/redis-server

Redis server

Для мониторинга сообщений, которые будут посланы на Socket.io откройте новую вкладку в консоли и запустите:

$ src/redis-cli monitor

Установка yii2-redis

yii2-redis - это модуль, предоставляющий функционал, для работы с базой даных Redis. Для установки модуля перейдите в корневую директорию проекта и запустите:

$ php composer.phar require --prefer-dist yiisoft/yii2-redis

Теперь добавте в файл config/web.php следующие настройки:

'redis' => [
    'class' => 'yii\redis\Connection',
    'hostname' => 'localhost',
    'port' => 6379,
    'database' => 0,
],

Создание сервера NodeJS

Создайте папку nodejs в корневой папке проекта. Перейдите в новую папку и запустите следующую команду, чтобы установить redis.io и socket.io.

$ npm install socket.io
$ npm install redis

После установки даных пакетов создайте новый файл /nodejs/server.js со следующим содержанием:

var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
var redis = require('redis');

server.listen(8890);

io.on('connection', function (socket) {

    console.log("new client connected");

    var redisClient = redis.createClient();

    redisClient.subscribe('notification');
    redisClient.subscribe('rate');

    redisClient.on("message", function(channel, message) {
        console.log("New message: " + message + ". In channel: " + channel);
        socket.emit(channel, message);
    });

    socket.on('disconnect', function() {
        redisClient.quit();
    });

});

Установка VMap JQuery

Скачайте VMap JQuery библиотеку, для отображения карты и добавте ее в файл assets/AppAsset.php:

...
    public $jsOptions = ['position' => \yii\web\View::POS_HEAD];
    public $css = [
        'css/site.css',
        'js/jqvmap/dist/jqvmap.css',
    ];
    public $js = [
        'js/jqvmap/dist/jquery.vmap.js',
        'js/jqvmap/dist/maps/jquery.vmap.world.js',
        'js/socket-client.js',
    ];
...

Настройка Socket.io

Для испльзования socket.io вам нужно отредактировать файл views/layouts/main.php следующим образом:

<head>
    <meta charset="<?= Yii::$app->charset ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <?= Html::csrfMetaTags() ?>
    <title><?= Html::encode($this->title) ?></title>
    <script src="https://cdn.socket.io/socket.io-1.3.5.js"></script>
    <?php $this->head() ?>
</head>

После этого создайте новый файл web/js/socket-client.js и добавте в него следующие строки:

/**
 * Created by oleksii on 17/09/2016.
 */
$( document ).ready(function() {
    $('#vmap').vectorMap({
        map: 'world_en',
        enableZoom: false,
        pins: {
        },
        pinMode: 'content',
    });

    var socket = io.connect('http://example.com:8890/');
    var counter = 0;
    socket.on('notification', function (data) {
        var message = JSON.parse(data);
        counter = $('.messages > table > tbody > tr').length;
        if (counter == 0) {
            $('.messages > table > tbody > tr').remove();
        }
        if(counter > 18) {
            $('.messages > table > tbody > tr').last().remove();
        }
        counter++;
        var html = '<tr><td>'+message.userId+'</td><td>'+message.originatingCountry+'</td><td>'+message.currencyFrom+'/'+message.currencyTo+'</td><td>'+message.amountSell+'/'+message.amountBuy+'</td><td>'+parseFloat(message.rate).toFixed(2)+'</td><td>'+message.timePlaced+'</td></tr>';
        $('.messages > table > tbody').prepend(html);
        var pins = new Object();
        pins[message.originatingCountry.toLowerCase()] = message.originatingCountry;
        jQuery('#vmap').vectorMap('placePins', pins, 'content');
        setTimeout(function(){
            jQuery('#vmap').vectorMap('removePin', message.originatingCountry.toLowerCase());
        }, 1000);
    });
});

Обратите внимание на эту строку:

 io.connect('http://example.com:8890/');

Здесь замените example.com на Ваш сервер IP или домен. Так же добавте этот файл в файл assets/AppAsset.php

public $js = [
   ...
   'js/socket-client.js'
   ...
];

Обновите ваш файл views/site/index.php:

<div class="site-index">
    <h3>CurrencyFair - Task!</h3>
    <div class="body-content">
        <div class="row">
            <div class="col-lg-6">
                <div id="vmap" style="width: 550px; height: 422px;"></div>
            </div>
            <div class="col-lg-6">
                <div class="messages">
                    <table class="table-bordered table-responsive" style="width: 100%; text-align: center; max-height: 400px; overflow-y: auto;">
                        <thead>
                            <th>UserID</th>
                            <th>Country</th>
                            <th>Currency From/To</th>
                            <th>Amount Sell/Buy</th>
                            <th>Rate</th>
                            <th>Date/Time</th>
                        </thead>
                        <tbody>
                            <?php if(empty($data)):?>
                                <tr><td colspan="6">No activity</td></tr>
                            <?php else:?>
                                <?php for($i = count($data)-1; $i >= count($data)-19 && $i >= 0; $i--):?>
                                    <tr>
                                        <td><?=$data[$i]['userId']?></td>
                                        <td><?=$data[$i]['originatingCountry']?></td>
                                        <td><?=$data[$i]['currencyFrom'].'/'.$data[$i]['currencyTo']?></td>
                                        <td><?=$data[$i]['amountSell'].'/'.$data[$i]['amountBuy']?></td>
                                        <td><?=number_format($data[$i]['rate'], 2)?></td>
                                        <td><?=$data[$i]['timePlaced']?></td>
                                    </tr>
                                <?php endfor;?>
                            <?php endif;?>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

Так же обновите controllers/SiteController::index() метод:

        $redis = Yii::$app->redis;
        $id = $redis->get('id');
        $data = [];
        for($i = $id - 200; $i < $id; $i++) {
            $data[] = json_decode($redis->hget('messages', 'message'.$i), 1);
        }
        return $this->render('index', [
            'data' => $data,
        ]);

Но для того, чтобы мы видели с каких стран приходят сообщения обновите метод, который будет принимать сообщения controllers/ConsumerController.php::index():

/**
     * Message consumer action.
     *
     * @return json
     */
    public function actionIndex($token = null)
    {
        if (!$token || !in_array($token, Yii::$app->params['tokens'])) {
            throw new ErrorException('You have no access to this page.');
        }
        $jsonString = file_get_contents('php://input');
        if (strlen($jsonString) <= 0) {
            return false;
        }
        $data = json_decode($jsonString, 1);
        try {
            //Save message in database
            $message = new TradeMessage($data);
            //Validate accepted data
            if(!$message->validate()) {
                $errors = array_values($message->getErrors());
                if (isset($errors[0]) && isset($errors[0][0])) {
                    throw new ErrorException($errors[0][0]);
                } else {
                    throw new ErrorException('Some of TradeMessage attributes are not valid.');
                }
            }
            //Save message
            if (!$message->save()) {
                throw new ErrorException('An error occurs when saving trade message.');
            }
            //Run observer to trigger socket.io and update frontend global map
            $observer = new TradeObserver($message);
            $observer->notify();
            return json_encode([
                'status' => 200,
                'code' => 0,
                'message' => 'Success',
            ]);
        } catch (\Exception $e) {
                return json_encode([
                    'status' => 500,
                    'code' => $e->getCode(),
                    'message' => $e->getMessage(),
                ]);
        }
    }

Для того, чтобы посылать сообщения на socket.io мы используем клас TradeObserver. Вот его содержание:

<?php
/**
 * Created by PhpStorm.
 * Author: Oleksii Danylevskyi <aleksey@danilevsky.com>
 * Date: 17/09/2016
 * Time: 17:42
 */
namespace app\models;
use app\models\interfaces\Observer;
use Yii;
use yii\helpers\Json;
use app\models\TradeMessage;
class TradeObserver implements Observer
{
    private $channel;
    private $message;
    public function __construct(TradeMessage $message, $channel = 'notification')
    {
        $this->message = $message;
        $this->channel = $channel;
    }
    public function notify()
    {
        Yii::$app->redis->executeCommand('PUBLISH', [
            'channel' => $this->channel,
            'message' => Json::encode($this->message->data),
        ]);
    }
}

Как Вы можете видеть, мы посылаем сообщение на Socket.io через Redis канал:

Yii::$app->redis->executeCommand('PUBLISH', [
       'channel' => $this->channel,
       'message' => Json::encode($this->message->data),
]);

Запускаем приложение

Во-первых, мы должны запустить Redis сервер:

$ src/redis-server

Потом запустить (в новом терминале) монитор Redis CLI:

$ src/redis-cli monitor

Потом запустить nodejs сервер (перейдите в корневую папку проекта):

$ node nodejs/server.js

Теперь когда вы будете посылать JSON сообщения на example.com/consumer/index на главной страничке example.com/site/index вы будете видеть, как на карте появляются инициалы стран, а таблица сообщений обновляется: Real-time world map

Полная постановка и решение лежат здесь.

Спасибо за внимание. Коментируйте)

LikeMe: