PHP 非同步後台處理
#PHP 作為後台的介面伺服器已經很常見,在實際應用場景中經常需要非同步後台處理。
推薦:《PHP視訊教學》
PHP 當然具有它能作為後台伺服器的優勢之處,但是,在處理一些客戶端並不關心的結果時,就顯出它的弊端了,沒有非同步執行的機制。
就例如我們想做一些對於某次客戶端存取php的效能記錄(包括開始時間、結束時間、這次結果狀態等)的記錄時,客戶端當然想的是php的本次處理能夠提早返回,拿到結果,如果安裝常規的方案,客戶端就得等php做完效能記錄之後,才能拿到結果。
相當於你去銀行去查你現在的餘額,而櫃員跑過去跟其他人鬧了一會兒的磕,在來告訴你的結果一樣。
所以,很多時候,就需要一種php能執行非同步操作。
PHP 如何實現非同步處理呢?
其中一個方案就是利用php的系統調用,開啟新的進程來實現。
php 提供了fsockopen函數,此函數的功能為初始化一個套接字連接到指定主機,預設會以阻塞模式開啟套接字連線。
當然你可以透過stream_set_blocking()將它轉換到非阻塞模式。這是關鍵。
所以,想法就是:開啟一個非阻塞的套接字連接到本機,本機收到之後作一些耗時處理。
類似這樣的處理程式碼(檔案posttest.php):
$fp = fsockopen($php_Path,80); if (!$fp) { LMLog::error("fsockopen:err" ); } else { $out = "GET /album/action/album_write_friends_thread_record.php?key=&u= HTTP/1.1\r\n"; $out .= "Host: ".$php_Path."\r\n"; $out .= "Connection: Close\r\n\r\n"; stream_set_blocking($fp,true); stream_set_timeout($fp,1); fwrite($fp, $out); usleep(1000); fclose($fp); }
這裡,usleep(1000) 非常關鍵,它能保證這個請求能發出去。
我們在來看處理的程式碼邏輯(檔案album_write_friends_thread_record.php):
<?php /** * Created by PhpStorm. * User: Administrator * Date: 2016-09-23 * Time: 09:26 */ /** * 客户端调用服务器接口页面 * user: guwen */ sleep(20);// 睡眠20s ?>
實際上,我們伺服器在執行fsockopen 那段程式時,就不會再等20s之後才能返回給客戶端,
而是發出這個請求之後,也就是返回客戶端,銷毀進程,而把剩餘的工作交由其他進程慢慢做去,這就實現了php的非同步。
PHP 非同步執行的4種常用方式
客戶端與伺服器端是透過HTTP協定進行連線通訊,客戶端發起請求,伺服器端接收到請求後執行處理,並傳回處理結果。
有時伺服器需要執行很耗時的操作,如處理下載、訊息下發、郵件傳送等,這個操作的結果並不需要回傳給客戶端。
但因為php是同步執行的,所以客戶端需要等待服務處理完才可以進行下一步。
因此,對於耗時的操作適合異步執行,伺服器接收到請求後,處理完客戶端所需的資料就先傳回,剩餘耗時的操作再異步在伺服器後台執行。
PHP非同步執行的常用方式常見的有以下幾種,可以根據各自優缺點進行選擇:
1. ajax 請求
客戶端頁面採用AJAX技術請求伺服器
$.get("doRequest.php", { name: "fdipzone"} ); <img src="doRequest.php?name=fdipzone">
優點:最簡單,也最快,就是在傳回給客戶端的HTML程式碼中,嵌入AJAX調用,或者,嵌入一個img標籤,src指向要執行的耗時腳本。
缺點:一般來說Ajax都應該在onLoad以後觸發,也就是說,用戶點開頁面後,就關閉,那就不會觸發我們的後台腳本了。
而使用img標籤的話,這種方式不能稱為嚴格意義上的非同步執行。使用者瀏覽器會長時間等待php腳本的執行完成,也就是使用者瀏覽器的狀態列一直顯示還在load。
當然,也可以使用其他的類似原理的方法,例如script標籤等等。
2. popen()函數
該函數開啟一個指向進程的管道,該進程由派生給定的command 命令執行而產生。
開啟一個指向進程的管道,該進程由派生給定的 command 命令執行而產生。
所以可以透過呼叫它,但忽略它的輸出。使用程式碼如下:
// popen — 打开进程文件指针 resource popen ( string $command , string $mode ) pclose(popen('php /home/fdipzone/doRequest.php &', 'r'));
優點:避免了第一個方法的缺點,並且執行速度快。
缺點:這種方法不能透過HTTP協定請求另外的一個WebService,只能執行本地的腳本檔案。並且只能單向打開,無法穿著大量參數給被呼叫腳本。而且如果,訪問量很高的時候,會產生大量的進程。如果使用到了外部資源,還要自己考慮競爭。
1)只能在本機執行
2)不能傳遞大量參數
#3)訪問量高時會創建很多進程
3. curl 擴展
CURL是一個強大的HTTP命令列工具,可以模擬POST/GET等HTTP請求,然後得到和提取數據,顯示在"標準輸出"(stdout)上面。
设置curl的超时时间 CURLOPT_TIMEOUT 为1 (最小为1),因此客户端需要等待1秒
代码如下:
<?php $ch = curl_init(); $curl_opt = array( CURLOPT_URL, 'http://www.example.com/doRequest.php' CURLOPT_RETURNTRANSFER,1, CURLOPT_TIMEOUT,1 ); curl_setopt_array($ch, $curl_opt); curl_exec($ch); curl_close($ch); ?>
缺点:如你问题中描述的一样,由于使用CURL需要设置CUROPT_TIMEOUT为1(最小为1,郁闷)。也就是说,客户端至少必须等待1秒钟。
4. fscokopen()函数
fsockopen是最好的,缺点是需要自己拼接header部分。
<?php $url = 'http://www.example.com/doRequest.php'; $param = array( 'name'=>'fdipzone', 'gender'=>'male', 'age'=>30 ); doRequest($url, $param); function doRequest($url, $param=array()){ $urlinfo = parse_url($url); $host = $urlinfo['host']; $path = $urlinfo['path']; $query = isset($param)? http_build_query($param) : ''; $port = 80; $errno = 0; $errstr = ''; $timeout = 10; $fp = fsockopen($host, $port, $errno, $errstr, $timeout); $out = "POST ".$path." HTTP/1.1\r\n"; $out .= "host:".$host."\r\n"; $out .= "content-length:".strlen($query)."\r\n"; $out .= "content-type:application/x-www-form-urlencoded\r\n"; $out .= "connection:close\r\n\r\n"; $out .= $query; fputs($fp, $out); fclose($fp); } ?>
注意:当执行过程中,客户端连接断开或连接超时,都会有可能造成执行不完整,因此需要加上
ignore_user_abort(true); // 忽略客户端断开 set_time_limit(0); // 设置执行不超时
fsockopen支持socket编程,可以使用fsockopen实现邮件发送等socket程序等等,使用fcockopen需要自己手动拼接出header部分
可以参考: http://cn.php.net/fsockopen/
使用示例如下:
$fp = fsockopen("www.34ways.com", 80, $errno, $errstr, 30); if (!$fp) { echo "$errstr ($errno)<br />\n"; } else { $out = "GET /index.php / HTTP/1.1\r\n"; $out .= "Host: www.34ways.com\r\n"; $out .= "Connection: Close\r\n\r\n"; fwrite($fp, $out); /*忽略执行结果 while (!feof($fp)) { echo fgets($fp, 128); }*/ fclose($fp); }
所以总结来说,fscokopen()函数应该可以满足您的要求。可以尝试一下。
fscokopen的问题和popen 一样,并发非常多时会产生很多子进程,当达到apache的连接限制数时,就会挂掉,我问题已经说了这种情况。
PHP 本身没有多线程的东西,但可以曲线的办法来造就出同样的效果,比如多进程的方式来达到异步调用,只限于命令模式。还有一种更简单的方式,可用于 Web 程序中,那就是用fsockopen()、fputs() 来请求一个 URL 而无需等待返回,如果你在那个被请求的页面中做些事情就相当于异步了。
关键代码如下:
$fp=fsockopen('localhost',80,&$errno,&$errstr,5); if(!$fp){ echo "$errstr ($errno)<br />\n"; } fputs($fp,"GET another_page.php?flag=1\r\n"); fclose($fp);
上面的代码向页面 another_page.php 发送完请求就不管了,用不着等待请求页面的响应数据,利用这一点就可以在被请求的页面 another_page.php 中异步的做些事情了。
比如,一个很切实的应用,某个 Blog 在每 Post 了一篇新日志后需要给所有它的订阅者发个邮件通知。如果按照通常的方式就是:
日志写完 -> 点提交按钮 -> 日志插入到数据库 -> 发送邮件通知 ->
告知撰写者发布成功
那么作者在点提交按钮到看到成功提示之间可能会等待很常时间,基本是在等邮件发送的过程,比如连接邮件服务异常、或器缓慢或是订阅者太多。而实际上是不管邮件发送成功与否,保证日志保存成功基本可接受的,所以等待邮件发送的过程是很不经济的,这个过程可异步来执行,并且邮件发送的结果不太关心或以日志形式记录备查。
改进后的流程就是:
日志写完 -> 点提交按钮 -> 日志插入到数据库 --->
告知撰写者发布成功
└ 发送邮件通知 -> [记下日志]
用个实际的程序来测试一下,有两个 php,分别是 write.php 和 sendmail.php,在 sendmail.php 用 sleep(seconds) 来模拟程序执行使用时间。
write.php,执行耗时 1 秒
<?php function asyn_sendmail() { $fp=fsockopen('localhost',80,&$errno,&$errstr,5); if(!$fp){ echo "$errstr ($errno)<br />\n"; } sleep(1); fputs($fp,"GET /sendmail.php?param=1\r\n"); #请求的资源 URL 一定要写对 fclose($fp); } echo time().'<br>'; echo 'call asyn_sendmail<br>'; asyn_sendmail(); echo time().'<br>'; ?>
sendmail.php,执行耗时 10 秒
<?php //sendmail(); //sleep 10 seconds sleep(10); fopen('C:\'.time(),'w'); ?>
通过页面访问 write.php,页面输出:
1272472697 call asyn_sendmail 1272472698
并且在 C:\ 生成文件:
1272472708
从上面的结果可知 sendmail.php 花费至少 10 秒,但不会阻塞到 write.php 的继续往下执行,表明这一过程是异步的。
以上是詳解 PHP 非同步後台處理的詳細內容。更多資訊請關注PHP中文網其他相關文章!