Источник событий PHP продолжает выполняться

Я начал использовать push в HTML5, используя объект JavaScript EventSource. Я был полностью доволен рабочим решением на PHP:

$time = 0;
while(true) {
    if(connection_status() != CONNECTION_NORMAL) {
        mysql_close()
        break;
    }
    $result = mysql_query("SELECT `id` FROM `table` WHERE UNIX_TIMESTAMP(`lastUpdate`) > '".$time."'");
    while($row = mysql_fetch_array($result)) {
        echo "data:".$row["id"].PHP_EOL;
        echo PHP_EOL;
        ob_flush();
        flush();
    }
    $time = time();
    sleep(1);
}

Но внезапно мое веб-приложение стало недоступным из-за ошибки MySQL «слишком много подключений».

Оказалось, что соединение MySQL не закрывается после закрытия источника события в JavaScript:

window.onload = function() {
    sse = new EventSource("push.php");
    sse.onmessage = function(event) {
        console.log(event.data.split(":")[1]);
    }
}
window.onbeforeunload = function() {
    sse.close();
}

Итак, я предполагаю, что PHP-скрипт не перестает выполняться. Есть ли способ вызвать функцию (например, die();) до разрыва соединения с клиентом? Почему мой скрипт не завершается после вызова .close(); в EventSource?!

Спасибо за помощь! —


person Julian F. Weinert    schedule 16.10.2012    source источник
comment
Кажется, что каждый сеанс клиента запускает новый бесконечный цикл на стороне сервера со своим собственным соединением mysql, не так ли? Я вижу, что в приведенном вами примере не начинается соединение, поэтому я не уверен. Во всяком случае, поэтому появляется слишком много ошибок соединений. Служба на стороне сервера, которая обслуживает данные, может использовать (разделять) одно соединение с БД (и один бесконечный цикл). Я не публикую это как ответ, потому что у меня нет примера под рукой. Не использую PHP уже пару лет. Хотя я вижу на php.net mysql_query немного устарел. Вместо этого они советуют использовать MySQLi или PDO_MySQL.   -  person topr    schedule 16.10.2012
comment
Что-то вроде. Каждая клиентская сессия имеет собственное соединение с MySQL. Соединение начинается до бесконечного цикла. Настоящая проблема заключается в том, что вызов EventSource.close() не закрывает и не прерывает само соединение!   -  person Julian F. Weinert    schedule 16.10.2012
comment
Насколько я понимаю, контент, предоставляемый через push.php, одинаков для каждого клиента, не так ли? Зачем тогда подключаться к базе данных и запрашивать для каждого клиентского сеанса вместо того, чтобы делиться одними и теми же результатами SQL между всеми клиентскими сеансами? Несмотря на то, что это более оптимальное решение, оно также решит вашу проблему.   -  person topr    schedule 17.10.2012
comment
Похоже, я должен попробовать это... Мне нужен мой скрипт для отправки информации клиенту, когда другой клиент изменяет базу данных. Как я могу поделиться результатами со всеми клиентами, не устанавливая соединение с БД для каждого клиента?! Мне нужно немедленно отправить информацию.   -  person Julian F. Weinert    schedule 18.10.2012
comment
Я не знаю точного ответа без дополнительных исследований. Я использовал PHP несколько лет назад. Но попробуйте поискать такие вещи, как «область приложения», «служба с отслеживанием состояния» и «задания cron», добавив ключевое слово PHP. Может что-нибудь склеишь.   -  person topr    schedule 18.10.2012


Ответы (5)


Прежде всего: когда вы заключаете свой код в огромный цикл while(true), ваш скрипт никогда не завершится. Соединение с БД было бы разорвано, когда у вашего скрипта "исчерпался код для выполнения", но поскольку вы написали блокировку, этого не произойдет... никогда.
Это не проблема EventSource, она просто делает то, что должна делать. Это честно, искренне и, к сожалению, ваша вина.

Дело в том, что пользователь подключается к вашему сайту, и создается экземпляр объекта EventSource. Устанавливается соединение с сервером и запрашивается возвращаемое значение push.php. Ваш сервер выполняет запрос и запускает скрипт, что, опять же, не что иное, как тупик. Ошибок нет, поэтому он может просто продолжать работать, пока php.ini позволяет ему работать. Метод .close() действительно отменяет поток вывода (или, скорее, должен), но ваш скрипт слишком занят либо выполнением своего бесконечного цикла, либо спящим режимом. Предполагать, что сценарий будет остановлен из-за отключения клиента, все равно что предположить, что любой клиент может мешать тому, что делает сервер. С точки зрения безопасности это было бы худшим из того, что могло произойти. Теперь, чтобы ответить на ваш актуальный вопрос: что вызывает проблему, как ее исправить?
Ответ: HEADERS.

Посмотрите на этот небольшой PHP-пример: скрипт не поддерживается сервером. стороны (бесконечными циклами), а путем установки правильных заголовков.

Просто как примечание: пожалуйста, выбросьте mysql_* как можно скорее, это устарело (наконец-то), вместо этого используйте PDO или mysqli_*. Это не так сложно, честно.

person Elias Van Ootegem    schedule 16.10.2012
comment
Чтобы следовать вашим аргументам: вызов close(); не изменяет состояние соединения, возвращаемое connection_status();? С каких это пор вы можете редактировать заголовки из JavaScript?! Когда я завершаю скрипт из PHP, новые данные никогда не будут переданы клиенту. При установке заголовка в text/event-stream (как я) никогда не скажу mysql проверять наличие обновлений, верно? - person Julian F. Weinert; 16.10.2012
comment
@Julian: вызов .close() действительно меняет connecion_status. Вы можете установить заголовки запроса в JS (просто посмотрите на вызовы ajax: setRequestHeader), но я имел в виду PHP header('some header');. Вам также не нужно явно завершать PHP-скрипт. Просто посмотрите на ссылку, которую я предоставил, она содержит более подробную информацию и дает довольно четкие примеры того, как заставить это работать. - person Elias Van Ootegem; 17.10.2012
comment
Я знаю ссылку. К какому абзацу относится? Мне нужен PHP-скрипт для постоянной проверки моей базы данных. В моем PHP я каждую секунду записывал статус подключения в текстовый файл. Когда я вызываю .colse(), статус НЕ меняется. К сожалению. Я не могу изменить заголовок в PHP, потому что скрипт должен работать непрерывно, пока клиент не отключится! while(connection_staus() == CONNECTION_NORMAL) {} тоже не работает... - person Julian F. Weinert; 18.10.2012
comment
@Julian: Ну, в ссылке, которую я разместил, есть образец PHP. Поскольку для заголовков будут установлены заголовки keep-alive и event-stream, соединение с вашей БД будет сохраняться до тех пор, пока не будет вызван метод .close()... Вот и все. Вам не нужно никакого волшебства циклов, а не непрерывных проверок. Его поведение сравнимо с node.js: отправка фрагментированных данных с сервера, который имеет слушателей и проводит большую часть своего времени в режиме ожидания... Просто скопируйте и вставьте пример кода из ссылки (первый фрагмент под заголовком Server Samples) и использовать это для проверки - person Elias Van Ootegem; 18.10.2012
comment
Ну... Это не работает. Я использую текст/поток событий с тех пор, как начал работать над push. Когда я удаляю цикл while, объект Event Source возвращает функцию on error и закрывает соединение. Когда у меня нет цикла, скрипт завершает выполнение и завершается. Тогда поток закрывается. Швы заголовка не беспокоят его. Вызов .close() по-прежнему НЕ меняет connection_status. Не могу представить, почему... - person Julian F. Weinert; 18.10.2012
comment
@Julian: Что такое readyState в обратном вызове ошибки? это CLOSED? в этом случае: убедитесь, что у вас нет mysql_close или $pdo = null; где-то в вашем php-коде, также проверьте исходный код JS этого примера, возможно, есть некоторые вдохновение черпать оттуда - person Elias Van Ootegem; 18.10.2012
comment
Состояние готовности равно 2 (он же CLOSED). У меня нет mysql_close или чего-то еще в моем коде. Я хотел реализовать это после цикла... Я тоже знаю этот пример. Они ничем не отличаются от того, что делаю я. Это то же самое - person Julian F. Weinert; 18.10.2012
comment
@Julian: Я только что кое-что понял: вы очищаете буфер внутри цикла, EventSource — это не то же самое, что поток сокета, вы должны либо вернуть весь вывод одним куском, либо использовать веб-сокет для непрерывной передачи данных - person Elias Van Ootegem; 18.10.2012
comment
давайте продолжим обсуждение в чате - person Julian F. Weinert; 18.10.2012
comment
@EliasVanOotegem, пример, на который вы ссылаетесь в своей ссылке, не поддерживается заголовками. На самом деле он фактически умирает, и клиент повторно открывает соединение через три секунды. - person Ulad Kasach; 22.01.2016

У меня была точно такая же проблема, и, насколько я понял, причина заключалась в том, что apache не завершал php-скрипт, пока я снова не попытался записать данные через закрытое соединение сокета.

У меня не было никаких изменений в моей базе данных, поэтому не было никакого вывода.

Чтобы исправить это, я просто повторяю комментарий источника события в цикле while, например:

echo ": heartbeat\n\n";
ob_flush();
flush();

Это приводит к завершению сценария, если соединение сокета закрыто.

person Marcel Gwerder    schedule 22.03.2015
comment
Да, это сработает. Но по сравнению с этим это приводит к огромным сетевым накладным расходам. Это не намного лучше, чем постоянное голосование... - person Julian F. Weinert; 23.03.2015
comment
Зависит от того, как часто вы его отправляете. Вам не нужно делать это на каждом цикле. Вы можете делать это каждые 15 минут или около того... Важно то, что скрипт завершается, а не сразу. - person Marcel Gwerder; 23.03.2015
comment
Согласитесь, вы совершенно правы. Но я всегда стараюсь сделать все идеально ;) Итак, вы понимаете, почему это не так вкусно :D Но да, спасибо за ваше предложение, это определенно делает его выполнимым, даже если не идеальным. Я просто думаю, что PHP не подходит для того, чтобы сделать это идеальным. - person Julian F. Weinert; 23.03.2015
comment
Я тоже люблю делать все идеально, я понимаю, что ты имеешь в виду. Однако возможность сделать это с помощью php, как и во всем остальном моем приложении, делает это намного проще. Если бы мне пришлось делать часть источника событий с чем-то другим, я бы, возможно, переключился на WebSockets. - person Marcel Gwerder; 23.03.2015

вы можете попробовать добавить это в цикл:

if(connection_status() != CONNECTION_NORMAL)
{
    break;
}

но php должен остановиться, когда клиент отключится.

person Bastian    schedule 16.10.2012
comment
Я предполагал, что PHP тоже должен остановиться. Я попробовал ваше решение с оператором if, затем mysql_close();, затем break;. Это не помогает. Когда я захожу в свой клиент MySQL и набираю SHOW PROCESSLIST, мой пользователь WebApps все еще подключен. Каждый раз, когда я перезагружаю страницу, она подключается еще раз… Может быть, это проблема JS EventSource.clos();??? - person Julian F. Weinert; 16.10.2012
comment
Будет ли if(connection_aborted()) лучшей проверкой? - person Nathan; 27.12.2014

1.

window.onbeforeunload = function() {
    sse.close();
}

это не требуется, EventSource будет закрыт во время выгрузки страницы.

  1. даже если вы не можете найти способ обнаружения отключенного клиента на PHP, вы можете просто остановить выполнение через N секунд, EventSource автоматически переподключится.

http://www.php.net/manual/ru/function.connection-aborted.php в комментариях на этой странице говорится, что в любом случае вы должны использовать «flush» перед connection_status, вы должны сбросить соединение через N секунд, потому что вы не можете обнаружить «плохие» разрывы соединения.

3. эхо "повторить:1000\n"; // используйте это, чтобы сообщить EventSource о задержке переподключения в мс

person 4esn0k    schedule 19.10.2012
comment
Спасибо, это своего рода обходной путь. Но это тоже не кажется правильным, поскольку EventStream получает ошибку, когда выполнение PHP-скрипта прекращается. - person Julian F. Weinert; 19.10.2012
comment
а также? это нормально, чтобы получить эту ошибку, вы должны проверить readyState в обработчике ошибок, если это EventSource.CONNECTED - тогда ES попытается переподключиться - person 4esn0k; 21.10.2012

Как указал Элиас Ван Отегем, ваш скрипт никогда не завершается, и, следовательно, у вас есть несколько активных подключений к MySQL. Что еще более важно, у вас есть куча ресурсов, используемых в виде циклов PHP, работающих без конца!

К сожалению, решение для хранения не является заголовком. В примере по этой ссылке, на которую ссылается Элиас, PHP-скрипт завершается, и клиентский javascript фактически должен повторно открыть соединение. Вы можете проверить это, используя следующий код

source.addEventListener('open', function(e) {
  // Connection was opened.
    console.log("Opening new connection");
}, false);

Если вы последуете его примеру на github, в реализации автор фактически использует цикл, который завершается через X секунд. см. сделать цикл и комментарии внизу скрипта.

Решение состоит в том, чтобы продлить жизнь вашей петле.

Определив ограничение на цикл, количество итераций IE X или по истечении X времени, чтобы вызвать конец цикла или die();, вы сделаете так, чтобы в случае отключения клиента существовало ограничение на то, как долго скрипт будет оставаться активным. Если клиент все еще находится там по прошествии X времени, он просто переподключится.

person Ulad Kasach    schedule 22.01.2016