Статьи

Уведомления в реальном времени с PHP


Общение в реальном времени круто, не правда ли?
То, что невозможно сделать пять лет назад сейчас (или почти невозможно), уже доступно. В настоящее время у нас есть два возможных решения. WebSockets и Comet. WebSockets, вероятно, лучшее решение, но у них есть две проблемы мэра:

  • Не все браузеры поддерживают их.
  • Не все прокси-серверы обеспечивают связь с веб-маркетами.

Из-за этого я предпочитаю использовать комету (по крайней мере, сейчас). Это не так хорошо, как веб-сокеты, но довольно просто и работает (даже в IE). Теперь я собираюсь объяснить небольшой сценарий, который я должен выполнить для связи с кометой, сделанный на PHP. Возможно, это не очень хорошая идея использовать его на сайтах с большим трафиком, но это работает как шарм в маленькой интрасети. Если вы хотите использовать комету на сайте с высоким трафиком, возможно, вам нужно взглянуть на Tornado , twisted , node.js или другие выделенные комет-серверы.

Обычно, когда мы говорим о связи в реальном времени, все люди думают о приложении чата. Я хочу создать более простое приложение. Желание определить, когда кто-то нажимает на ссылку. Из-за этого мне понадобится комбинация HTML, PHP и JavaScript. Давайте начнем:

Для примера я буду использовать библиотеку jquery, поэтому нам нужно включить библиотеку в наш HTML-файл. Это будет смесь JavaScrip и PHP:

<html>
02 <head>
03 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
04 <title>Comet Test</title>
05 </head>
06 <body>
07 <p><a class='customAlert' href="#">publish customAlert</a></p>
08 <p><a class='customAlert2' href="#">publish customAlert2</a></p>
09 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js" type="text/javascript"></script>
10 <script src="NovComet.js" type="text/javascript"></script>
11 <script type="text/javascript">
12 NovComet.subscribe('customAlert', function(data){
13 console.log('customAlert');
14 //console.log(data);
15 }).subscribe('customAlert2', function(data){
16 console.log('customAlert2');
17 //console.log(data);
18 });
19
20 $(document).ready(function() {
21 $("a.customAlert").click(function(event) {
22 NovComet.publish('customAlert');
23 });
24
25 $("a.customAlert2").click(function(event) {
26 NovComet.publish('customAlert2');
27 });
28 NovComet.run();
29 });
30 </script>
31 </body>
32 </html>

Код клиента:

//NovComet.js
02 NovComet = {
03 sleepTime: 1000,
04 _subscribed: {},
05 _timeout: undefined,
06 _baseurl: "comet.php",
07 _args: '',
08 _urlParam: 'subscribed',
09
10 subscribe: function(id, callback) {
11 NovComet._subscribed[id] = {
12 cbk: callback,
13 timestamp: NovComet._getCurrentTimestamp()
14 };
15 return NovComet;
16 },
17
18 _refresh: function() {
19 NovComet._timeout = setTimeout(function() {
20 NovComet.run()
21 }, NovComet.sleepTime);
22 },
23
24 init: function(baseurl) {
25 if (baseurl!=undefined) {
26 NovComet._baseurl = baseurl;
27 }
28 },
29
30 _getCurrentTimestamp: function() {
31 return Math.round(new Date().getTime() / 1000);
32 },
33
34 run: function() {
35 var cometCheckUrl = NovComet._baseurl + '?' + NovComet._args;
36 for (var id in NovComet._subscribed) {
37 var currentTimestamp = NovComet._subscribed[id]['timestamp'];
38
39 cometCheckUrl += '&' + NovComet._urlParam+ '[' + id + ']=' +
40 currentTimestamp;
41 }
42 cometCheckUrl += '&' + NovComet._getCurrentTimestamp();
43 $.getJSON(cometCheckUrl, function(data){
44 switch(data.s) {
45 case 0: // sin cambios
46 NovComet._refresh();
47 break;
48 case 1: // trigger
49 for (var id in data['k']) {
50 NovComet._subscribed[id]['timestamp'] = data['k'][id];
51 NovComet._subscribed[id].cbk(data.k);
52 }
53 NovComet._refresh();
54 break;
55 }
56 });
57
58 },
59
60 publish: function(id) {
61 var cometPublishUrl = NovComet._baseurl + '?' + NovComet._args;
62 cometPublishUrl += '&publish=' + id;
63 $.getJSON(cometPublishUrl);
64 }
65 };

PHP на стороне сервера

// comet.php
02 include('NovComet.php');
03
04 $comet = new NovComet();
05 $publish = filter_input(INPUT_GET, 'publish', FILTER_SANITIZE_STRING);
06 if ($publish != '') {
07 echo $comet->publish($publish);
08 } else {
09 foreach (filter_var_array($_GET['subscribed'], FILTER_SANITIZE_NUMBER_INT) as $key => $value) {
10 $comet->setVar($key, $value);
11 }
12 echo $comet->run();
13 }

и моя реализация библиотеки комет:

// NovComet.php
02 class NovComet {
03 const COMET_OK = 0;
04 const COMET_CHANGED = 1;
05
06 private $_tries;
07 private $_var;
08 private $_sleep;
09 private $_ids = array();
10 private $_callback = null;
11
12 public function __construct($tries = 20, $sleep = 2)
13 {
14 $this->_tries = $tries;
15 $this->_sleep = $sleep;
16 }
17
18 public function setVar($key, $value)
19 {
20 $this->_vars[$key] = $value;
21 }
22
23 public function setTries($tries)
24 {
25 $this->_tries = $tries;
26 }
27
28 public function setSleepTime($sleep)
29 {
30 $this->_sleep = $sleep;
31 }
32
33 public function setCallbackCheck($callback)
34 {
35 $this->_callback = $callback;
36 }
37
38 const DEFAULT_COMET_PATH = "/dev/shm/%s.comet";
39
40 public function run() {
41 if (is_null($callback)) {
42 $defaultCometPAth = self::DEFAULT_COMET_PATH;
43 $callback = function($id) use ($defaultCometPAth) {
44 $cometFile = sprintf($defaultCometPAth, $id);
45 return (is_file($cometFile)) ? filemtime($cometFile) : 0;
46 };
47 } else {
48 $callback = $this->_callback;
49 }
50
51 for ($i = 0; $i < $this->_tries; $i++) {
52 foreach ($this->_vars as $id => $timestamp) {
53 if ((integer) $timestamp == 0) {
54 $timestamp = time();
55 }
56 $fileTimestamp = $callback($id);
57 if ($fileTimestamp > $timestamp) {
58 $out[$id] = $fileTimestamp;
59 }
60 clearstatcache();
61 }
62 if (count($out) > 0) {
63 return json_encode(array('s' => self::COMET_CHANGED, 'k' => $out));
64 }
65 sleep($this->_sleep);
66 }
67 return json_encode(array('s' => self::COMET_OK));
68 }
69
70 public function publish($id)
71 {
72 return json_encode(touch(sprintf(self::DEFAULT_COMET_PATH, $id)));
73 }
74 }

 

Как вы можете видеть в моем примере, я создал персональный протокол для связи между клиентом (js в браузере) и сервером (PHP). Это простой. Если вы ищете «стандартный» протокол, возможно, вам стоит взглянуть на протокол bayeux от людей из Dojo.

Позвольте мне немного объяснить использование скрипта:

  • На странице HTML мы запускаем слушателя (NovComet.subscribe).
  • Мы можем подписаться на столько событий, сколько хотим (ОК, это зависит от наших ресурсов)
  • Когда мы подписываемся на одно событие, мы передаем функцию обратного вызова для запуска.
  • Когда мы подписываемся на событие, мы передаем текущую метку времени на сервер.
  • Клиентский скрипт (js с jquery) будет вызывать серверный скрипт (PHP) с отметкой времени и будет ждать окончания работы сервера.
  • Сценарий на стороне сервера ответит, даже если отметка времени поменяется (кто-то опубликовал событие)
  • Серверная сторона не будет ждать вечно. Если никто не опубликует событие, сервер ответит по истечении заданного времени ожидания
  • клиентский скрипт будет повторять процесс снова и снова.

В этой технике есть что-то действительно важное. Наша проверка событий на стороне сервера должна быть максимально простой. Например, мы не можем выполнить запрос SQL (наш системный администратор убьет нас, если мы это сделаем). Мы должны помнить, что эта проверка будет выполняться снова и снова для каждого пользователя, поэтому она должна быть максимально легкой. В этом примере мы проверяем дату последнего изменения файла ( filemtime ). Другим хорошим решением является использование базы данных memcached и проверка значения.

Для теста я также создал сценарий публикации (NovComet.publish). Это простая часть. Мы вызываем только сценарий на стороне сервера, который касается файла события (изменяя дату последней модификации), вызывая событие.

Теперь я собираюсь объяснить, что мы можем видеть на консоли Firebug:

  1. На первой итерации ничего не происходит. 200 OK Http-код после тайм-аута, установленного в скрипте PHP
  2. Как мы видим здесь, скрипт возвращает JSON с s = 0 (ничего не происходит)
  3. Теперь мы публикуем событие. Скрипт возвращает 200 OK, но теперь JSON другой. s = 1 и отметка времени события
  4. Наш обратный звонок был запущен
  5. И следующая итерация ждет

И это все. Просто и полезно. Но помните, вы должны позаботиться о том, чтобы использовать это решение на сайте с высоким трафиком. Что вы думаете? Вы используете ленивую комету с PHP на производственных серверах или предпочитаете другое решение?

Вы можете получить код на github здесь .