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

http://www.sitepoint.com/building-live-score-widget-using-php-web-sockets/
  • Перевод
  • Tutorial
Внедрение веб-сокетов позволяет веб-приложения обрабатывать данные в режиме реального времени, не прибегая к «хакам», таким как 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 Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

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 Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;
use LiveScores\Scores;

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, мы сможем без проблем добавить и другие данные.
Демо.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 14
  • 0
    Я немного отстал от web технологий. Скажите, а не будет проще для сервера, если клиент будет опрашивать статичный файл и загружать его только в случае изменения даты создания? В таком варианте мы обмениваемся только заголовками и не держим открытую сессию для каждого клиента.
    • +2
      вы не учитываете того факта, что создание соединения — это тоже нагрузка. в теории намного проще держать открытое соединение и обмениваться изредка небольшими сообщениями, нежели периодически бомбить запросами сервак.
      • +2
        Может все зависит от количества передаваемой информации и количества подключенных клиентов?
        Что легче, держать 10 000 одновременных подключений в минуту с помощью php демона для передачи 1кб данных, или с помощью nginx отвечать на запросы тех же 10 000 клиентов раз в 10 секунд заголовками целую минуту, передав каждому один раз 1кб данных?
        Кстати, порт 8080 открыт не у 100% клиентов.
        • 0
          В JQueryAJAX так же есть функция ifModified. Можно дергать статичный файл, а не php-скрипт. Можно открыть кучу аналогов сокета на 80-м порту на одном ip. Если не требуется моментальная отправка данных клиенту, а данные обновляются не часто, думаю, ajax будет легче. Вот в каких-нибудь онлайн играх web-сокет более приемлемый. Вот так я сам себе ответил на вопрос. Нахожусь в ожидании критики :)
          • 0
            Вы в любом случае отправляете запрос на сервер. Да и флаг ifModified вроде как никак не поможет если кеширование не настроено на уровне nginx-а. Да и это позволит вам просто обмениваться только заголовками. Да, этот метод работает и он прост в реализации, но он не оптимален. Хотя и у автора статьи вышло так себе.
            p.s. Если бы web sockets поддерживали udp, было бы вообще круто для игрушек.
            • 0
              В дынный момент ведущий разработчик %CompanyName% прикручивает к web сокетам udp over tcp что бы передавать XML.
              • 0
                что значит udp over tcp? это вообще как? В чем смысл?
          • +1
            Да, проще будет держать 10 000 подключений. Правда лучше для этого использовать node.js, который уже имеет встроенную обертку с мультиплексированием соединений и не нужно дополнительно писать алгоритмы оптимизирующие работу сервера. Профит тут такой:
            10К соединений одновременных особо не грузят систему, просто у вас будет 10К дескрипторов, слежением за состояниями которых будет заниматься операционная система (при использовании select/epoll). В этом случае вам нужно отправлять данные только если это нужно. Что-то поменялось — шлете данные клиенту. Ничего не поменялось — ничего и не делаем.

            А так вы предлагаете поддерживать порядка 1000 запросов в секунду и + порядка 1000 запросов к файловой системе в секунду. Проще уже взять и написать оптимизированный сервер под этим задачи. Благо сейчас это не так сложно.
            • 0
              node.js, который уже имеет встроенную обертку с мультиплексированием соединений

              Мультиплексирование в данном случае ознчает разделение информационного канала (ip + port = socket) на логические потоки. Оба node.js http server и Ratchet IoServer поддерживают это.
            • 0
              Кстати, порт 8080 открыт не у 100% клиентов.

              Клиент подключается к порту 8080, так что он должен быть открыт на сервере но никак не на клиенте.
              • –1
                Все верно. Я и пишу не «на клиенте» а «у клиентов».
          • 0
            Я думаю тут есть другая проблема: то, что WebSocket до сих пор в разработке и имеет туманное будущее.
        • 0
          Извините, я только не понял, как приходит к клиенту сообщение с типом «goal»? В коде нашел только тип «init».

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.