2012年9月27日 星期四

PHP 也可以寫排程

一般我們在寫網頁腳本相關的程式時,都會因為HTTP 給我們一種,連線完取得資料後「即斷線」的觀念,因此很直覺的就認為 PHP 不可能來做一些排程的工作。

錯~

在參考了 Allen J 大的「用 PHP 寫的排程 (ignore_user_abort)」這篇之後,我對PHP所能處理的事只僅限於一般HTTP所能處理的觀念就此改觀了。

底下將為您介紹透過 ignore_user_abort 來寫排程,並且避開 PHP 的垃圾處理機制的缺點。





首先,PHP 提供了一個 ignore_user_abort 的設定供開發者使用。

ignore_user_abort(true)
set_time_limit(0);



while(true){
    // 要處理的工作

    sleep(60); // sleep 1分鐘 (以秒為單位)
}

開啟 ignore_user_abort 這個機制後,使用者在連線執行程式後,如馬上關掉視窗(HTTP斷線),雖然使用者已經看不到執行結果,其實在伺服器端的這支程式仍然在執行。直到結束為止…

一般Web程式執行時,都會有 timeout 預設 30秒的限制,這時候我們再加上 set_time_limit(0); 則可將這個 timeout 調到「無限制」。這樣一來這支程式將可以無後顧之憂的「永久」的「一輩子」的執行下去了!

呃,有木有發現哪裡怪怪的?在使用者斷線後,程式將永遠進入「無窮迴圈」。

解決的辦法只有兩個:
1.關掉 Apache 服務
2.關機

如果是在自己的電腦上執行就算了,如果把這支程式放在虛擬主機上 (自己沒有任何權限),那就等著去麻煩虛擬主機的工程師了。你忍心、你忍心嗎?(一直拍桌)


建議可以在將程式上傳到主機之前,先在自己的本機端做好測試。而在 while 迴圈的判斷式裡面也不要真的像範例那樣放一個「true」,這樣風險太大了!這邊建議可以在每次判斷的時候透過讀寫檔的方式來判斷是不是該跳出迴圈了!


ignore_user_abort(true)
set_time_limit(0);

while(file_get_content("gate.cfg")=="Y"){

    // 要處理的工作

    sleep(60); // sleep 1分鐘 (以秒為單位)
}

file_get_content 在這邊是讀取檔案內容的意思。而gate.cfg整個檔案裡面就只有一個字「Y」或「N」,只要讀到的內容是Y,則這個程式將永遠的執行下去;反之,N則離開回圈。這麼做的話基本上也不用擔心程式有什麼難以控制的問題而跳不出回圈了!只要手動去gate.cfg裡改一下值,就可以結束程式了。


不過,就在這一切都很美好的狀況下,有件事情發生了。

在while 回圈裡,所做的一切(宣告變數),都會留下足跡的。來看看以下的程式:

ignore_user_abort(true)
set_time_limit(0);

while(file_get_content("gate.cfg")=="Y"){
    
    $var = "我們要長大";
    
    sleep(60); // sleep 1分鐘 (以秒為單位)
}

這個程式應該不難懂,其下場就是隨時迴圈一圈一圈的執行,您的記憶體就會越來越肥大!
因為宣告好的變數使用完沒消毀掉。一般我們都會使用 unset($var); 來處理這個問題!但經由我努力向 Google 大神請教了一些問題後發現,一般的變數可以 unset,但如果您宣告的是「陣列(array)」就完蛋了!



ignore_user_abort(true)
set_time_limit(0);

while(file_get_content("gate.cfg")=="Y"){
    
    $var = array("我們要長大");
    
    unset($var);

    sleep(60); // sleep 1分鐘 (以秒為單位)
}


因為 unset 這個指令對於消毀陣列的效果是有限的! ( 參考:风雪之隅 )。就在我苦思無策之時,突然想到了一個「爛」方法!由於PHP在整個程式結束後,將會有個垃圾回收機制,將這支程式所使用的記憶體空間都清掉。

咦…但程式結束了,我的排程呢?看好囉,我要放大絕了…


ignore_user_abort(true)
set_time_limit(0);

if(file_get_content("gate.cfg")=="Y"){
    
    $var = array("我們要長大");
    
    unset($var);

    sleep(60); // sleep 1分鐘 (以秒為單位)

    $ch = curl_init();

    $options = array( 
      CURLOPT_URL => "http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']
      CURLOPT_HEADER => false, 
      CURLOPT_RETURNTRANSFER => true, 
      CURLOPT_TIMEOUT=>1,
      CURLOPT_USERAGENT => "Google Bot", 
      CURLOPT_FOLLOWLOCATION => true
    );

    curl_setopt_array($ch, $options);
    curl_exec($ch);

}


CURL 是執行指定網域並取得 HTML 執始碼的指令,這邊就不多介紹了。這邊的做法是,只要讀到檔案裡的值是Y,代表我仍要繼續執行這支程式。

在執行完排程工作後,Sleep個1分鐘,接著再「執行自己」!這邊有個小技巧,在執行網址(自己)的時候,Timeout要設定為 1 秒(盡量短一點)。

否則,這個CURL將會等你執行完你剛發送給自己的請求,下場是…
執行發送給自己的請求 → 睡個 60 秒 → 執行發送的請求 → 再睡個60秒…(跑不完了)
這支程式永遠會卡在等待請求結束的時候 ( 不會有這一天了= =)

因此 Timeout 是絕對重要的!在發出請求後,立刻斷開,結束程式。此時另一個接到請求的網址(自己),將會開始他的排程。而原本發送請求的程式將在結束後,該資源由記憶體做了消毀,進而達到「節能減碳」的功效。

2 則留言: