Вопрос про фоновую работу ПХП

Эгея написана на ПХП как обычное веб-приложение, отвечающее на запросы браузера к серверу. Браузер просит страницу — Эгея её генерирует и отдаёт.

Часть работы, которую делает Эгея — медленная по своей природе, например создание бекапа или индексация большого блога для поиска. Но Эгея отдаёт страницы быстро.

Это потому что в Эгее реализован механизм фоновой работы через запрос к себе. Когда нужно сделать что-то долгое, Эгея устанавливает ХТТП-соединение сама с собой, как бы делая вид, что она браузер, отправляет запрос по специальному урлу, который означает «сделай бекап» или «поиндексируй поиск», и тут же обрывает соединение. В результате выполнение скрипта заканчивается быстро, а вся долгая работа делается незаметно, как раз в ответ на этот запрос к себе. О таком методе я узнал лет двенадцать назад от Романа Иванова, и с тех пор пользуюсь.

Асинхронное выполнение на ПХП

К сожалению, с этим методом возникла проблема, когда я стал поддерживать ХТТПС. Если просто отправлять запрос и сразу закрывать соединение, как я всегда делал, не происходит вообще ничего — с точки зрения сервера всё выглядит так, как будто к нему и не обращались. Если же попробовать прочитать ответ, то приходит 400 Bad Request, потому что я как бы пытаюсь говорить с ХТТПС-сервером на простом ХТТП. К сожалению, мне почему-то так и не удалось отправить самому себе запрос по ХТТПС с помощью функции fsockopen () и её родственников, хотя я вроде бы исчитал документацию со всех сторон.

Вместо запроса к себе можно использовать register_shutdown_function (). Но я когда-то пользовался ей, и у меня осталось ощущение ненадёжности — кажется, она выполнялась не всегда, и там были какие-то особенности внутри странные, например, что все пути к файлам должны быть указаны абсолютно. Короче, чтобы ей воспользоваться, нужно переструктурировать код. Женя Степанищев ещё рассказал про fastcgi_finish_request () — похожий вариант.

Эти схемы мне не нравятся непрозрачностью: часть скрипта будет выполняться в каком-то мире, который не виден никому. В варианте с запросом к себе мне ничто не мешает зайти браузером по моему служебному урлу и посмотреть, что происходит. А тут придётся что-то специальное изобретать для отладки. Кроме того, запрос к себе, разумеется, запускает новую копию ПХП, в которой заново начинается отсчёт времени. Выполняя такой запрос, я могу теоретически захотеть сделать что-то ещё так же, и инициировать запрос третьго уровня. И все эти запросы — одинаковой природы, просто заход по урлу. Мне нравится эта однородность.

Есть радикальный вариант — переделать всё так, чтобы по служебным урлам ходил аджаксом клиент, пока ни о чём не подозревающий пользователь смотрит на уже загрузившуюся страницу. Такая конфигурация мне не нравится уже эстетически. Что это за беспомощный сервер, что ему надо, чтобы его клиент приводил в чувство? Возможно, это бред, но я бы хотел, чтобы сервер мог работать самостоятельно.

Ну и пока умники в твиттере не начали писать «расскажите кто-нибудь Бирману про крон», объясню, что моя задача — сделать максимально автономное решение, не требующее от пользователя никакой специальной настройки сервера. Закачал на сервер папку — всё работает. Если вы матёрый программист в свитере, вам такое не понять.

Короче, в идеале я бы хотел заставить работать свою исходную конфигурацию. Для этого мне нужно научиться делать запрос к себе по ХТТПС. Причём мне нужно просто «потрогать» нужный урл и отвалиться — мне не нужно по нему передавать никаких данных, не нужно читать ответ. Как это сделать?

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

Дальше
15 комментариев
Павел 2017

Использовать транспорт ssl:// или подобный пробовали?
http://fi2.php.net/manual/en/transports.inet.php

Илья Бирман 2017

Да. К сожалению, не помогло: fsockopen () возвращает false, при этом код ошибки 0, текст ошибки пустой.

Миша Крайнов 2017

Илья, мне кажется, что пробелы перед скобками лишние в данном тексте:
fsockopen ()

Илья Бирман 2017

Мне так не кажется :-)

Анатолий Березняк 2017

Curl пробовал?

https://stackoverflow.com/a/4372730

Илья Бирман 2017

Теперь да, и вроде это работает! Спасибо :-)

Миша Крайнов 2017

хм, на вкус и цвет. а как же экономия байтов?

ну и да, не мне тебе показывать http://php.net/manual/ru/function.fsockopen.php

Александр Денисюк 2017

Существует замечательная функция int ignore_user_abort ([ bool $value ]). Если ей передать true, то скрипт отработает до конца в фоновом режиме при отключении клиента: закрыть окно браузера, оборвать HTTP-запрос на первой секунде и всё в таком духе. Можно пойти дальше и сделать нативный менеджер очередей: добавить set_time_limit(0), чтобы скрипт работал постоянно в фоне и через while(true) ловить из сессии какие-нибудь параметры для обработки. Нужен Cron? Добавь sleep(60 * 15) перед завершением while(true) — цикл будет отрабатывать один раз в 15 минут. Пример: http://php.net/manual/ru/function.ignore-user-abort.php#68114

bormotov 2017

а вот тот fsockopen, у которого ssl:// он был на 443 порт?

Илья Бирман 2017

Да.

Миша Вырцев 2017

Можно вызвать скрипт бэкапа с помощью exec не ожидая его конца. Только я не знаю как сейчас с разрешением на exec, а то многие шаред хостинги запрещают его или открывают при челобитной в офисе у царя.
https://gist.github.com/anonymous/39a201cea7e7f99e2ecb297908ee53f1

Могу объяснить подробнее в почте, если устраивает суть, но по сути это замена вызова по тисипи на внутренний вызов.

Илья Бирман 2017

Мне нельзя зависеть от воли царя :-)

Дмитрий 2017

Погуглил чуть, похоже опенссл в пхп не правильно настроен http://php.net/manual/ru/openssl.installation.php

Лёша Захарченко 2017

Примерно так:
file_get_contents(’https://ya.ru/', false, stream_context_create([’http’ => [’timeout’ => 10]]), -1, 1);
таймаут 10 что бы +- точно запрос успел дойти, 1 в четвертом параметре, что бы как-то только что-то придет от сервера, сразу закрыть соединение.

Но по хорошему лучше такие вещи через cron запускать, т. к. на сервере скорее всего max_execution_time может стоять какой-то, а в safe mode его и поменять/отменить нельзя будет. Кстати, ты его меняешь? Можно посмотреть как wordpress при установки в крон прописывает себя, если не ошибаюсь, кажется он так делал.

bormotov 2017

вот, уже высказали про yfcnhjqre openssl — с ним точно всё хорошо в том php, которое используется?

у меня хоть и нет опыта с php, но судя по документации и многочисленным ответам на вопросы вида «как сделать запрос по https» — fsockopen — это то, что должно работать. И раз оно не работает — то есть смысл посмотреть глубже, чего там с ssl реально происходит. Конечно, при условии, что это не сильно сложно сделать (можно включить дебаг на такой глубине?).

Еще из популярных ответов — таки curl, в виде подключаемого модуля, и вот еще модуль HTTP_Request2.

Миша Вырцев 2017

И еще подумалось: а почему надо обязательно дергать урл через хттпс? Почему нельзя дергать по хттп? Редирект на хттп-хттпс стоит? Убрать, и жить по старинке.

Илья Бирман 2017

Да, это я уже сделал пока, что это идёт по ХТТП. Но да, у кого-то из пользователей может стоять редирект.

Максим Гашков 2017

Илья,

код из официальной документации вполне работоспособен:

$fp = fsockopen(«tls://<hostname>», 443, $errno, $errstr, 30);
if (!$fp) {
echo «$errstr ($errno)<br />\n»;
} else {
$out = «GET /wait.php?time=5 HTTP/1.1\r\n»;
$out .= «Host: <hostname>\r\n»;
$out .= «Connection: Close\r\n\r\n»;
fwrite($fp, $out);
fclose($fp);
// while (!feof($fp)) { //закомментированная часть ждет ответа от сервера
// echo fgets($fp, 128);
// }
// fclose($fp);
}

Я подозреваю, что вы уже пробовали такой код. Что идет не так? Как сконфигурирован сервер? Можете написать на почту.

Но выше и воображаемые умники в твиттере пишут вам правильно: это иллюзия решения проблемы, которая может легко сломаться или не заработать вообще. У многих хостеров скрипты не могут создавать внешние TCP-соединения (даже если это запрос к этому же сайту), у многих стоят жесткие лимиты по времени выполнения — поэтому, если до какого-то времени бэкап укладывался в 30 секунд, а потом перестал — сайт остался без бэкапов (и хорошо, если вы об этом сообщаете как-то пользователю).

deadem 2017
Константин Барышников 2017

Если делать fsockopen по tls, надо не забыть про заголовок Connection: close. И, кстати, можно вообще HEAD-запрос делать.

Впрочем, на многих хостингах не установлены корневые сертификаты, а fsockopen с tls-враппером — это фактически вызов openssl s_client -connect, который сертификаты по умолчанию обычно проверяет. Более-менее универсальный способ — реализовать два варианта (curl и fopen + stream context) и в явном виде попросить не проверять сертификаты. Одно из двух — либо curl, либо отсутствие запрета на использование http fopen wrappers — есть у подавляющего большинства хостеров. Проще всего взять готовую библиотеку, если, конечно, не смущает необходимости таскать десяток файлов только ради этого. В любом случае, в библиотеках можно подсмотреть код.

Отдельный вопрос — буферизация на стороне реверс-прокси хостера, но тут в целом без разницы, http или https. Хотя, конечно, где-то может встретиться такая конфигурация, где проблемы будут только с https. Тут ничего не сделаешь, придется ставить таймаут и надеяться, что дернуться оно успело.

Сергей Рогожкин 2017

Колхоз «Червоно Дышло».
Предлагаю радикальный вариант: exec(«nohup wget —delete-after $url»);

Что касается основного вопроса, похожие симптомы, когда обертка fopen не может проверить хост.
Надо вот так проверить отправку POST из кода:

$arrQuery = [
’form_name’ => ’form_value’,
];

$context = stream_context_create( [
’http’ => [
«method» => «POST»,
«header» => «Content-Type: application/x-www-form-urlencoded» . PHP_EOL,
«content» => http_build_query( $arrQuery )
],
’ssl’ => [
’verify_peer’ => FALSE,
’verify_peer_name’ => FALSE,
’allow_self_signed’ => TRUE
]
] );

$result = file_get_contents( $url, FALSE, $context );

Мои книги