Реализация Comet в среде php+javascript

Ну что ж, сказано — сделано. В этой статье я расскажу, что из себя представляет Comet и рассмотрю реализацию простой схемы с помощью PHP и JavaScript.

Для начала, Comet это что-то вроде теории о создания постоянного соединения между клиентом и сервером в универсальной среде, в общем без привязки к какому-то определённому языку программирования.

При инициализации клиент открывает соединение с сервером и повисает в таком состоянии, когда на сервере появляются какие-то новые данные он отправляет их по этому соединению клиенту.

Для того, чтобы реализовать подобную вещь на JavaScript нужно учесть, что работа ведётся, через AJAX по протоколу HTTP. На сервере же предположительно установлен веб-сервер Apache.

В условиях AJAX-запросов, после отправки данных сервер закрывает соединение, клиент обрабатывает данные, открывает соединение заново и ждёт новую порцию данных.

Особенности

На стороне клиента необходимо открыть соединение с достаточно большим таймаутом, после срабатыания этого таймаута необходимо открыть соединение повторно и так каждый раз при обрыве соединения.

// javascript class, connection manager
this.connection = function() {
        self.comet = $.ajax({
            type: "GET",
            url:  "backend.php",
            data: {'id':self.last},
            dataType: "text",
            timeout: self.timeout*1000,
            success: self.parseData,
            error: function(){
                // something wrong. but setInterval will set up connection automatically
                setTimeout(self.connection,1000);
            }
        });
    }
this.init = function() {
        //setInterval(self.connection,self.timeout*1000);
        self.connection();
    }
this.init();

В JavaScript все таймауты указываются в миллисекундах, поэтому мы их умножаем на тысячу.

На сервере рекомендую поставить max_execution_time в значение равное таймауту клиента. В самом скрипте нужно организовать вечный цикл, с регулярной проверкой данных для отправки.

// simple infinite loop in php
while (time()-$time<$limit) {
    // checking if something new was added to my test table
    $res = mysql_query('SELECT * FROM `comet` WHERE `id`>"'.$last_id.'" ORDER BY `id` ASC');
    if (mysql_num_rows($res)) {
        while ($item=mysql_fetch_array($res)) {
            echo 'self.putMessage("'.$item['id'].'","'.escape($item['name']).'","'.escape($item['text']).'");';
        }
        flush();
        exit;
    }
    sleep(5);
}

Реализация

Я напишу всё с использованием библиотеки jQuery. Итак, javascript:

function Messanger() {
    this.last = 0;
    this.timeout = 360;
    this.comet = 0;
    var self = this;
    this.putMessage = function(id,name,text) {
        // callback, добавляет сообщения на страницу, вызывается из полученных с сервера данных
        self.last = id;
        var b = document.createElement('div');
        b.innerHTML = '<span style="color: red;">'+name+'</span> '+text;
        $('#messages').append(b);
    }
    this.parseData = function(message) {
        // простая обработка данных полученных с сервера, разбиваем строки и выполняет функции
        var items = message.split(';');
        if (items.length<1) return false;
        for (var i=0;i<items.length;i++) {
            eval(items[i]);
        }
        setTimeout(self.connection,1000);
    }
    this.connection = function() {
        // здесь открывается соединение с сервером
        self.comet = $.ajax({
                type: "GET",
                url:  "backend.php",
                data: {'id':self.last},
                dataType: "text",
                timeout: self.timeout*1000,
                success: self.parseData,
                error: function(){
                    // something wrong. but setInterval will set up connection automatically
                    setTimeout(self.connection,1000);
               }
            });
    }
    this.init = function() {
        //setInterval(self.connection,self.timeout*1000);
        self.connection();
    }
    this.init();
}
function sendMessage() {
    // callback формы, для отправки сообщений на сервер
    if ($('#name').val()&&$('#text').val()) {
        var data = {
                name: $('#name').val(),
                text: $('#text').val()
            }
        $.post('add_new.php',data);
    } else {
        alert('Please fill the fields');
    }
}
$(document).ready(function(){
    // инициализация
    var msg = new Messanger();
});

И PHP скрипт, отправляющий данные:

<?
// number of second the script allowed to run. setting to 6 minutes
$limit = 360;
$time = time();

// getting last loaded value
$last_id = (int)$_GET['id'];

// just to be sure that script will be killed
set_time_limit($limit+5);

mysql_connect('localhost','user','password');
mysql_select_db('database');

function escape($str) {
    return str_replace('"','\"',$str);
}

// цикл, проверяющий новые сообщения каждые 5 секунд
while (time()-$time<$limit) {
    // checking if something new was added to my test table
    $res = mysql_query('SELECT * FROM `comet` WHERE `id`>"'.$last_id.'" ORDER BY `id` ASC');
    if (mysql_num_rows($res)) {
        while ($item=mysql_fetch_array($res)) {
            // пишем js-скрипт, который выполнится у клиента
            echo 'self.putMessage("'.$item['id'].'","'.escape($item['name']).'","'.escape($item['text']).'");';
        }
        // выбрасываем все данные и выходим, чтобы клиент смог их обработать
        flush();
        exit;
    }
    // если данных нет - ждём 5 секунд
    sleep(5);
}

mysql_close();
?>

Мы получили фронтенд на JavaScript, который отправляет запросы на сервер в ожидании новых данных, сервер каждые 5 секунд проверяет не появились ли данные, когда появляются — он их отправляет клиенту и закрывает соединение. После этого клиент снова открывает соединение и всё повторяется. Если данных не поступило и сработал таймаут — клиент просто открывает соединение заново.

Обратите внимание, что и в классе на JS и в скрипте PHP задан таймаут, это сделано, чтобы избежать создание мёртвых процессов, одинаковые таймауты в теории должны поддерживать нагрузку сервера на минимуме.