Внедрение веб-сокетов позволяет веб-приложениям обрабатывать данные почти в реальном времени, не прибегая к таким «взломам», как длинный опрос.
Одним из примеров приложения, требующего самых последних данных, являются спортивные результаты. Даже сейчас многие веб-сайты, которые отображают эту информацию, используют 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 и стили для счетчиков спасибо Крису Нэнни и взяты из этого поста .)