Статьи

Создание виджета Live-Score с использованием веб-сокетов PHP

Внедрение веб-сокетов позволяет веб-приложениям обрабатывать данные почти в реальном времени, не прибегая к таким «взломам», как длинный опрос.

Одним из примеров приложения, требующего самых последних данных, являются спортивные результаты. Даже сейчас многие веб-сайты, которые отображают эту информацию, используют Flash-приложения, поскольку Actionscript предоставляет средства для связи через соединения на основе сокетов. Однако веб-сокеты позволяют нам копировать эту функциональность, используя только HTML и Javascript. Это то, что мы собираемся построить в этом уроке вместе с облегченным «сервером» в PHP.

образ

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

Мы будем основывать пример на библиотеке Ratchet , которая обеспечивает реализацию веб-сокетов на PHP.

Создайте следующий файл 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 / image, а также весь код из этого урока. Если вы хотите создать его с нуля вместе с этой статьей, все, что вам нужно сделать, это скопировать public/assets/*/vendor папки public/assets/*/vendor из клонированного / загруженного пакета в свои собственные в соответствующих местах.

Естественно, не забудьте запустить php composer.phar update , которому предшествует curl -sS https://getcomposer.org/installer | php curl -sS https://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 событие onOpen , когда мы добавляем клиента в нашу коллекцию
  • Когда клиент отключается ( onClose ), мы делаем наоборот
  • Интерфейс также требует от нас реализации простого обработчика ошибок ( onError )

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

 // 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 который взаимодействует с использованием веб-сокетов, и мы IoServer его на прослушивание через порт 8080. Конечно, вы можете выбрать другой порт – при условии, что он не заблокирован вашим брандмауэром – но 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; } } 

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

 // 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

Поскольку мы собираемся загрузить текущие результаты через веб-сокет и отобразить их с помощью 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> 

Элементы counter-*-* являются местозаполнителями для плагина JS, который мы собираемся использовать для визуализации фантастического виджета позже.

JavaScript

Теперь давайте начнем строить JS. Первое, что нужно сделать, это открыть веб-сокет:

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

Возможно, вам придется заменить имя хоста и / или номер порта, в зависимости от того, где работает ваш «сервер».

Затем подключите обработчик событий к соединению, которое срабатывает при получении сообщения:

 conn.onmessage = function(e) { 

Само сообщение предоставляется как свойство data для события e . Поскольку мы отправляем сообщения в формате JSON, нам нужно сначала проанализировать его:

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

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

 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}); }); } 

В этой функции мы просто перебираем массив игр, используя Усы для рендеринга новой строки, которая будет добавлена ​​в таблицу табло, и создаем пару анимированных счетчиков для каждой. Массив games будет хранить текущее состояние игровой стороны клиента и содержит ссылки на эти счетчики, чтобы мы могли обновлять их по мере необходимости.

Далее goal функция. Сообщение, которое мы получаем через веб-сокет для указания цели, будет объектом JSON со следующей структурой:

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

Свойство game содержит уникальный идентификатор, а team находится либо «дома», либо «в гостях». Используя эти биты информации, мы можем обновить соответствующий счет в массиве 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']); } 

Все, что остается, – это какой-то способ указать, что гол был забит. Для простоты мы просто добавим это клиенту; щелчок по названию команды покажет, что она забила. На практике у вас есть отдельное приложение или страница, но принцип тот же. Мы просто добавим обработчик кликов следующим образом, который отправляет простое сообщение JSON через веб-сокет:

 $(function () { $(document).on('click', '.team h3', function(e){ var game = $(this).parent().parent().attr('data-game-id'); var team = ($(this).parent().hasClass('home')) ? 'home' : 'away'; conn.send(JSON.stringify({ type: 'goal', team: team, game: game })); }); }); 

Сервер «слушает» эти сообщения, и если он получает слово цели, он обновляет свою запись. Все полученные сообщения немедленно передаются всем подключенным клиентам.

 // src/LiveScores/Scores.php public function onMessage(ConnectionInterface $from, $msg) { foreach ($this->clients as $client) { $client->send($msg); } $message = json_decode($msg); switch ($message->type) { case 'goal': $this->games[$message->game][$message->team]['score']++; break; } } 

Наконец, чтобы запустить его, вам нужно запустить сервер из командной строки:

 php bin/server.php 

Вот и все – попробуйте открыть несколько окон бок о бок и нажать на название команды, чтобы указать цель. Вы должны увидеть обновление табло прямо сейчас!

Вывод

В этой статье я продемонстрировал простой HTML и Javascript виджет «живых результатов» с использованием веб-сокетов. Это имеет свои ограничения; как правило, вы ожидаете увидеть забивающего гол и время, когда был забит каждый гол, а также дополнительную информацию, такую ​​как бронирование и отсылка. Однако, поскольку мы используем объект JSON для представления события, такие функции добавить относительно просто. Демонстрационная версия этого урока доступна.

(Примечание: Javascript и стили для счетчиков спасибо Крису Нэнни и взяты из этого поста .)