Создание виджета «Счет Live» использую PHP Web Sockets

в 20:27, , рубрики: javascript, php, web-сокет

Внедрение веб-сокетов позволяет веб-приложения обрабатывать данные в режиме реального времени, не прибегая к «хакам», таким как long-polling.
Одним из примеров применения, является отображение результатов спортивного матча. Даже сейчас, много сайтов, которые показывают эти данные используют Flash-приложения, т.к. Action Script позволяет общаться с сервером через сокет-соединения. Тем не менее, вев-сокеты позволяют сделать тоже самое используя только HTML и JavaScript. Это, мы постараемся сделать в данном руководстве, используя php-сервер.
image

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

Мы будем использовать библиотеку Ratchet, позволяющую PHP использовать web-сокеты.
Создайте следующий composer.json, который устанавливает как эту зависимость, так и автозагрузку для кода, который мы напишем далее:

{
    "require": {
        "cboden/Ratchet": "0.2.*"
    },
    "autoload": {
        "psr-0": {
            "LiveScores": "src"
        }
    }    
}

Сейчас создайте следующую структуру каталогов:

[root]
    bin
    src
        LiveScores
    public
        assets
            css
                vendor
            js
                vendor
    vendor

Возможно, вы захотите взять все из репозитория, который содержит ряд нужных css, js и изображений, а так же весь код из этого руководства. Если же вы хотите писать все с нуля, параллельно с руководством, то скопируйте только public/assets/*/vendor.
Естественно, не забудьте запустить php composer.phar update. Если у вас не установлен composer, установите его выполнив curl -sS getcomposer.org/installer | php.
Мы начнем с создания класса, который будет принимать подключения и отправлять сообщения. Позже, мы будем его использовать для обновления данных о идущий играх. Это базис класса, что бы показать как работает брокер сообщений:

// src/LiveScores/Scores.php

<?php namespace LiveScores;

use RatchetMessageComponentInterface;
use RatchetConnectionInterface;

class Scores implements MessageComponentInterface {

    private $clients;    

    public function __construct() 
    {    
        $this->clients = new SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) 
    {
        $this->clients->attach($conn);
    }

    public function onMessage(ConnectionInterface $from, $msg) 
    {            
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // The sender is not the receiver, send to each client connected
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) 
    {
        $this->clients->detach($conn);
    }

    public function onError(ConnectionInterface $conn, Exception $e) 
    {     
        $conn->close();
    }


}

Важно заметить:

  • Класс должен реализовывать MessageComponentInterface для того, что бы выступать в качестве брокера сообщений
  • Мы храним список всех подключенных клиентов в виде коллекции
  • Когда клиент добавляется, вызывается событие onOpen, и клиент добавляется в коллекцию
  • Метод onClose делает противоположное
  • Интерфейс также требует от нас создания обработчика ошибок

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

// bin/server.php

<?php
use RatchetServerIoServer;
use RatchetWebSocketWsServer;
use LiveScoresScores;

require dirname(__DIR__) . '/vendor/autoload.php';

$server = IoServer::factory(
    new WsServer(
        new Scores()
    )
    , 8080
);

$server->run();

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

Поддержание состояния

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

// src/LiveScores/Fixtures.php
<?php namespace LiveScores;

class Fixtures {

    public static function random()
    {
        $teams = array("Arsenal", "Aston Villa", "Cardiff", "Chelsea", "Crystal Palace", "Everton", "Fulham", "Hull", "Liverpool", "Man City", "Man Utd", "Newcastle", "Norwich", "Southampton", "Stoke", "Sunderland", "Swansea", "Tottenham", "West Brom", "West Ham");

        shuffle($teams);

        for ($i = 0; $i <= count($teams); $i++) {
            $id = uniqid();
            $games[$id] = array(
                'id' => $id,
                'home' => array(
                    'team' => array_pop($teams),
                    'score' => 0,
                ),
                'away' => array(
                    'team' => array_pop($teams),
                    'score' => 0,
                ),
            );
        }

        return $games;
    }


}  

Обратите внимание, что мы присваиваем каждой игре уникальный id, который мы будем использовать дальше, чтобы указать в какой игре произошло событие. Вернёмся к нашем Score классу:

// src/LiveScores/Scores.php

public function __construct() {

    // Create a collection of clients
    $this->clients = new SplObjectStorage;

    $this->games = Fixtures::random();
}

Так как клиент может подключить наш виджет в любой момент игры, нам важно, что бы он получил актуальный счет. Один из способов сделать это — это ответить на новый запрос, отправив текущее состоянии игры, затем отобразить список игр и их счет на клиенте.
Вот реализация метода onOpen, который делает это:

// src/LiveScores/Scores.php

public function onOpen(ConnectionInterface $conn) {
    // Store the new connection to send messages to later
    $this->clients->attach($conn);

    // New connection, send it the current set of matches
    $conn->send(json_encode(array('type' => 'init', 'games' => $this->games)));

    echo "New connection! ({$conn->resourceId})n";
}

Отмечу, что сообщение мы шлем как JSON-объект, где тип события — это свойство. Не обязательно использовать JSON, вы может использовать любой другой формат, но так, чтобы мы могли слать структурированные данные.

HTML

Раз мы шлем данные через web-сокет, а отображать их будем используя JavaScript, html-код страницы очень прост:

<div id="scoreboard">

    <table>

    </table>

</div>

Строка в таблице с результатами будет выглядеть так:

<tr data-game-id="SOME-IDENTIFIER">
    <td class="team home">
        <h3>HOME TEAM NAME</h3>
    </td>
    <td class="score home">
        <div id="counter-0-home"></div>
    </td>
    <td class="divider">
        <p>:</p>
    </td>
    <td class="score away">
        <div id="counter-0-away"></div>
    </td>
    <td class="team away">
        <h3>AWAY TEAM NAME</h3>
    </td>
</tr>

id элементов Counter-* — * мы будем использовать далее в JS-плагине.

JavaScript

Приступим к JavaScript. Первое что нужно сделать — это открыть сокет:

var conn = new WebSocket('ws://localhost:8080');

Возможно вам придётся заменить адрес хоста или порт, в зависимости от указанных настроек сервера/демона.
Далее необходим обработчик события, который будет отрабатывать при получении сообщения:

conn.onmessage = function(e) {  

Сообщение находится в свойстве data объекта событие e. Так как мы шлём сообщения в JSON, сначала мы должны разобрать его:

var message = $.parseJSON(e.data);

Теперь мы можем проверить тип сообщения и вызвать соответствующую функцию:

switch (message.type) {
    case 'init':
        setupScoreboard(message);
        break;
    case 'goal':
        goal(message);
        break;
}

Функция setupScoreboard довольно проста:

function setupScoreboard(message) {

    // Create a global reference to the list of games
    games = message.games;

    var template = '<tr data-game-id="{{ game.id }}"><td class="team home"><h3>{{game.home.team}}</h3></td><td class="score home"><div id="counter-{{game.id}}-home" class="flip-counter"></div></td><td class="divider"><p>:</p></td><td class="score away"><div id="counter-{{game.id}}-away" class="flip-counter"></div></td><td class="team away"><h3>{{game.away.team}}</h3></td></tr>';

    $.each(games, function(id){        
        var game = games[id];                
        $('#scoreboard table').append(Mustache.render(template, {game:game} ));        
        game.counter_home = new flipCounter("counter-"+id+"-home", {value: game.home.score, auto: false});
        game.counter_away = new flipCounter("counter-"+id+"-away", {value: game.away.score, auto: false});
    });

}

В этой функции мы просто «пробегаем» по массиву игр, использую Mustache для рендера новой строки в таблицу счета и реализации пары анимированых счетчиков для каждой игры. Массив games будет хранить текущее состояние игр и ссылки на счетчики, чтобы мы могли их обновлять по мере необходимости.
Далее идет функция goal. Мы получаем сообщение через web-сокет, которое сигнализирует нам об изменении состояния, имеет следующую структуру:

{
    type: 'goal',
    game: 'UNIQUE-ID',
    team: 'home'
}

Свойство game — уникальный ID, team — либо «home», либо «away». Используя эти данные мы можем обновить счет в массиве games, найти нужный нам объект счетчика и увеличить его.

function goal(message) {    
    games[message.game][message.team]['score']++;
    var counter = games[message.game]['counter_'+message.team];
    counter.incrementTo(games[message.game][message.team]['score']);
}

Теперь, всё что нам осталось это запустить сервер из командной строки:

php bin/server.php

Заключение

В данной статье я показал как легко можно создать виджет «Live счета» использую JS, HTML и web-сокеты. Конечно, обычно охота увидеть значительно больше информации, чем просто счет, но раз мы используем JSON, мы сможем без проблем добавить и другие данные.
Демо.

Автор: WildZero

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js