Я работаю над несколько большим веб-приложением, а бэкэнд – в основном на PHP. В коде есть несколько мест, где мне нужно выполнить некоторую задачу, но я не хочу, чтобы пользователь дождался результата. Например, при создании новой учетной записи мне нужно отправить им приветственное письмо. Но когда они нажимают кнопку «Финальная регистрация», я не хочу, чтобы они дождались отправки сообщения электронной почты, я просто хочу запустить процесс и сразу же отправить сообщение пользователю.
До сих пор, в некоторых местах, я использовал то, что было похоже на взлом с exec (). В основном делайте такие вещи, как:
exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
Кажется, что работает, но мне интересно, есть ли лучший способ. Я рассматриваю возможность написания системы, которая ставит задачи в таблице MySQL, и отдельный длинный PHP-скрипт, который запрашивает эту таблицу один раз в секунду и выполняет любые новые задачи, которые он находит. Это также имеет преимущество, позволяя мне разделить задачи между несколькими рабочими машинами в будущем, если понадобится.
Я заново изобретаю колесо? Есть ли лучшее решение, чем exec () взломать или очередь MySQL?
Я использовал подход к очередности, и он работает хорошо, так как вы можете отложить эту обработку до тех пор, пока ваша загрузка на сервере не будет работать, позволяя вам эффективно управлять своей загрузкой, если вы можете легко отделить «задачи, которые не являются срочными».
Роллинг не слишком сложный, вот еще несколько вариантов:
Другим, возможно, более простым подходом является использование ignore_user_abort – после того, как вы отправили страницу пользователю, вы можете выполнить свою окончательную обработку, не опасаясь преждевременного прекращения, хотя это действительно приводит к увеличению загрузки страницы пользователем перспектива.
Когда вы просто хотите выполнить один или несколько HTTP-запросов, не дожидаясь ответа, есть и простое PHP-решение.
В вызывающем скрипте:
$socketcon = fsockopen($host, 80, $errno, $errstr, 10); if($socketcon) { $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n"; fwrite($socketcon, $socketdata); fclose($socketcon); } // repeat this with different parameters as often as you like
На вызванном скрипте.php вы можете вызвать эти функции PHP в первых строках:
ignore_user_abort(true); set_time_limit(0);
Это приводит к тому, что скрипт продолжает работать без ограничения по времени, когда соединение HTTP закрывается.
Другой способ для процессов вилки – через завиток. Вы можете настроить свои внутренние задачи как веб-сервис. Например:
Затем в вашем пользовательском скрипте выполняются вызовы службы:
$service->addTask('t1', $data); // post data to URL via curl
Ваш сервис может отслеживать очередь задач с помощью mysql или что вам нравится: это все обернуто внутри службы, а ваш скрипт просто потребляет URL-адреса. Это освобождает вас от необходимости переместить службу на другой компьютер / сервер (то есть легко масштабируется).
Добавление http-авторизации или пользовательской схемы авторизации (например, веб-служб Amazon) позволяет вам открывать ваши задачи, которые будут потребляться другими людьми / службами (если хотите), и вы можете принять их дальше и добавить службу мониторинга сверху, чтобы отслеживать очереди и состояния задачи.
Это требует немного работы по настройке, но есть много преимуществ.
Я использовал Beanstalkd для одного проекта и планировал снова. Я нашел, что это отличный способ запускать асинхронные процессы.
Несколько вещей, которые я сделал с этим:
Я написал систему на основе Zend-Framework, чтобы декодировать «хороший» URL-адрес, например, чтобы изменить размер изображения, которое он вызовет QueueTask('/image/resize/filename/example.jpg')
. URL-адрес сначала был декодирован в массив (модуль, контроллер, действие, параметры), а затем преобразован в JSON для вставки в очередь.
Последовательный скрипт cli затем выполнил задание из очереди, запустил его (через Zend_Router_Simple), и, если необходимо, поместите информацию в memcached для веб-сайта PHP, чтобы поднять его по мере необходимости, когда это было сделано.
Одна морщина, которую я также вложил, заключалась в том, что cli-script запускался только для 50 циклов перед перезапуском, но если бы он захотел перезапустить, как планировалось, он немедленно выполнил бы (запускается через bash-скрипт). Если возникла проблема, и я сделал exit(0)
(значение по умолчанию для exit;
или die();
), он сначала остановится на пару секунд.
Если это просто вопрос предоставления дорогостоящих задач, в случае поддержки php-fpm, почему бы не использовать fastcgi_finish_request()
?
Эта функция удаляет все данные ответа клиенту и завершает запрос. Это позволяет выполнять трудоемкие задачи, не оставляя открытое соединение с клиентом.
Вы не используете асинхронность таким образом:
fastcgi_finish_request()
. Еще раз нужно php-fpm.
Вот простой класс, который я закодировал для своего веб-приложения. Это позволяет использовать PHP-скрипты и другие скрипты. Работает в UNIX и Windows.
class BackgroundProcess { static function open($exec, $cwd = null) { if (!is_string($cwd)) { $cwd = @getcwd(); } @chdir($cwd); if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { $WshShell = new COM("WScript.Shell"); $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd); $WshShell->Run($exec, 0, false); } else { exec($exec . " > /dev/null 2>&1 &"); } } static function fork($phpScript, $phpExec = null) { $cwd = dirname($phpScript); @putenv("PHP_FORCECLI=true"); if (!is_string($phpExec) || !file_exists($phpExec)) { if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe'; if (@file_exists($phpExec)) { BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd); } } else { $phpExec = exec("which php-cli"); if ($phpExec[0] != '/') { $phpExec = exec("which php"); } if ($phpExec[0] == '/') { BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd); } } } else { if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { $phpExec = str_replace('/', '\\', $phpExec); } BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd); } } }
Это тот же метод, который я использовал уже пару лет, и я не видел и не нашел ничего лучшего. Как говорили люди, PHP однопоточный, так что вы ничего не можете сделать.
На самом деле я добавил к этому дополнительный уровень, и это позволяет получать и хранить идентификатор процесса. Это позволяет мне перенаправить на другую страницу и заставить пользователя сидеть на этой странице, используя AJAX, чтобы проверить, завершен ли процесс (идентификатор процесса больше не существует). Это полезно для случаев, когда длина сценария приведет к таймауту браузера, но пользователю нужно дождаться завершения этого сценария до следующего шага. (В моем случае он обрабатывал большие ZIP-файлы с CSV-подобными файлами, которые добавляют до 30 000 записей в базу данных, после чего пользователь должен подтвердить некоторую информацию.)
Я также использовал аналогичный процесс для создания отчетов. Я не уверен, что буду использовать «обработку фона» для чего-то вроде электронной почты, если нет реальной проблемы с медленным SMTP. Вместо этого я мог бы использовать таблицу в качестве очереди, а затем процесс, который запускается каждую минуту, чтобы отправлять электронные письма в очередь. Вам нужно будет быть осторожным при отправке писем дважды или других подобных проблем. Я бы рассмотрел аналогичный процесс очередности и для других задач.
PHP HAS многопоточность, ее просто не включен по умолчанию, существует расширение, называемое pthreads, которое делает именно это. Вам понадобится php, скомпилированный с ZTS. (Thread Safe) Ссылки:
Примеры
Другой учебник
pthreads Расширение PECL
Отличная идея использовать cURL, как было предложено rojoca.
Вот пример. Вы можете контролировать text.txt во время работы сценария в фоновом режиме:
<?php function doCurl($begin) { echo "Do curl<br />\n"; $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']; $url = preg_replace('/\?.*/', '', $url); $url .= '?begin='.$begin; echo 'URL: '.$url.'<br>'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); echo 'Result: '.$result.'<br>'; curl_close($ch); } if (empty($_GET['begin'])) { doCurl(1); } else { while (ob_get_level()) ob_end_clean(); header('Connection: close'); ignore_user_abort(); ob_start(); echo 'Connection Closed'; $size = ob_get_length(); header("Content-Length: $size"); ob_end_flush(); flush(); $begin = $_GET['begin']; $fp = fopen("text.txt", "w"); fprintf($fp, "begin: %d\n", $begin); for ($i = 0; $i < 15; $i++) { sleep(1); fprintf($fp, "i: %d\n", $i); } fclose($fp); if ($begin < 10) doCurl($begin + 1); } ?>
К сожалению, PHP не имеет каких-либо собственных возможностей потоковой передачи. Поэтому я думаю, что в этом случае у вас нет выбора, кроме как использовать какой-то пользовательский код, чтобы делать то, что вы хотите сделать.
Если вы просматриваете сеть для потоковой передачи PHP, некоторые люди придумали способы имитации потоков на PHP.
Если вы установите HTTP-заголовок Content-Length в ответе «Спасибо за регистрацию», браузер должен закрыть соединение после получения указанного количества байтов. Это приводит к запуску процесса на стороне сервера (при условии, что параметр ignore_user_abort установлен), поэтому он может завершить работу, не дожидаясь конечного пользователя.
Конечно, вам нужно будет рассчитать размер вашего содержимого ответа перед рендерингом заголовков, но это довольно легко для коротких ответов (запись вывода в строку, вызов strlen (), call header (), строка рендеринга).
Преимущество такого подхода заключается в том, что вы не вынуждаете вас управлять очередью «переднего конца», и, хотя вам может понадобиться выполнить некоторую работу на заднем конце, чтобы предотвратить переход от нежелательных ходовых HTTP-процессов, это то, что вам нужно было сделать уже , так или иначе.
Если вы не хотите полностью раздутый ActiveMQ, я рекомендую рассмотреть RabbitMQ . RabbitMQ – это легкий обмен сообщениями, в котором используется стандарт AMQP .
Я рекомендую также посмотреть в php-amqplib – популярную клиентскую библиотеку AMQP для доступа к брокерам сообщений на AMQP.
я думаю, вы должны попробовать эту технику, это поможет назвать столько страниц, сколько вам понравится, все страницы будут запускаться сразу независимо, не ожидая, что каждый ответ на страницу будет асинхронным.
cornjobpage.php // главная страница
<?php post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue"); //post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2"); //post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue"); //call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous. ?> <?php /* * Executes a PHP page asynchronously so the current page does not have to wait for it to finish running. * */ function post_async($url,$params) { $post_string = $params; $parts=parse_url($url); $fp = fsockopen($parts['host'], isset($parts['port'])?$parts['port']:80, $errno, $errstr, 30); $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like $out.= "Host: ".$parts['host']."\r\n"; $out.= "Content-Type: application/x-www-form-urlencoded\r\n"; $out.= "Content-Length: ".strlen($post_string)."\r\n"; $out.= "Connection: Close\r\n\r\n"; fwrite($fp, $out); fclose($fp); } ?>
testpage.php
<? echo $_REQUEST["Keywordname"];//case1 Output > testValue ?>
PS: если вы хотите отправить параметры URL-адреса в качестве цикла, выполните следующий ответ: https://stackoverflow.com/a/41225209/6295712
PHP – это однопоточный язык, поэтому нет официального способа запуска асинхронного процесса с ним, кроме использования exec
или popen
. Об этом здесь есть сообщение в блоге. Идея для очереди в MySQL тоже хорошая идея.
Ваше конкретное требование здесь для отправки электронной почты пользователю. Мне любопытно, почему вы пытаетесь сделать это асинхронно, так как отправка электронной почты – довольно простая и простая задача для выполнения. Я полагаю, что если вы отправляете тонны электронной почты, а ваш интернет-провайдер блокирует вас по подозрению в рассылке спама, это может быть одной из причин очереди, но кроме этого я не могу придумать никаких причин для этого.