當我們以WEB的方式運行PHP指令碼時,預設情況下,即使你關閉當前頁面,程式也會繼續執行,直接程式結束或逾時。如果我們想在使用者關閉頁面或點擊了停止頁面運行時就中斷程式,我們需要做些什麼呢?上周和小毅同學討論了這個問題,從而也引出了今天我們這篇文章。
我們知道HTTP協議是基於TCP/IP協議,對於一個PHP頁面的請求就是一個HTTP請求(假設我們是Apache伺服器),從而會建立TCP串連,當使用者插斷要求時,會給伺服器一個abort狀態。這個abort狀態就是今天我們要講的關鍵點。
在PHP中有一個函數與abort狀態有關:ignore_user_abort函數
ignore_user_abort() 函數設定與客戶機斷開時是否會終止指令碼的執行。它返回 user-abort 之前設定的布爾值。它的參數可選。如果設定為 true,則忽略與使用者的斷開,如果設定為 false,會導致指令碼停止運行。
PHP 不會檢測到使用者是否已中斷連線,直到嘗試向客戶機發送資訊為止。因此如果我們只是使用echo語句,可能無法如實的看到abort的效果,因為PHP在輸出時會有一個緩衝,如果要重新整理緩衝,則可以使用flush() 函數。
如下代碼t.php:
ignore_user_abort(TRUE);set_time_limit(50); while(1){echo$i++,"\r\n";flush(); $fp=fopen("data.txt",'a');fwrite($fp,$i." \r\n");fclose($fp); sleep(1);}
在瀏覽器中執行這段代碼,過了大概兩秒後,關閉請求的頁面,50秒後,你會發現在data.txt檔案中寫入了至少50個數。這表示我們的中斷操作是無效的。
如果我們改一下,把第一句改為:ignore_user_abort(FALSE);,重複上面的操作,你會發現,唯寫入了極少的數字,這表示我們的中斷操作有效了。
現在通過ignore_user_abort函數,我們實現了使用者中斷就馬上停止程式的操作。這裡有一個問題,即我們需要不停的flush,通過flush函數來更新串連狀態,當狀態為abort時,程式根據ignore_user_abort的設定來判斷是否中斷程式。除此之外,我們也可以使用直接擷取串連狀態來check串連狀態,並對特定的狀態作出處理,如下代碼:
ignore_user_abort(FALSE);set_time_limit(50); while(1){ echo$i++,"\r\n";flush(); if(connection_status()!= CONNECTION_NORMAL){break;} $fp=fopen("data.txt",'a');fwrite($fp,$i.":".connection_status()." \r\n");fclose($fp); sleep(1);}
這裡的connection_status函數的作用是擷取串連的狀態,當串連的狀態非normal時,我們就中斷迴圈,從而也達到了中斷程式的操作。這個樣本與前面的樣本不同之處在於中斷操作是由我們自己控制,而不是通過flush操作直接exit。如果在使用者中斷後還有一些其它的操作,這種方式會更合適一些。當然,這裡的flush操作依舊不可少,我們還是需要通過這個函數做check操作。
ignore_user_abort函數和connection_status函數都實現了我們的目的,這兩個函數的實現有沒有關聯?我們在ext/standard/basic_functions.c檔案中找到這兩個函數的實現如下:
/* {{{ proto int connection_aborted(void) Returns true if client disconnected */PHP_FUNCTION(connection_aborted){ RETURN_LONG(PG(connection_status)& PHP_CONNECTION_ABORTED);}/* }}} */ /* {{{ proto int connection_status(void)Returns the connection status bitfield */PHP_FUNCTION(connection_status){ RETURN_LONG(PG(connection_status));}/* }}} */ /* {{{ proto int ignore_user_abort([string value])Set whether we want to ignore a user abort event or not */PHP_FUNCTION(ignore_user_abort){char*arg = NULL;int arg_len =0;int old_setting; if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"|s",&arg,&arg_len)== FAILURE){return;} old_setting = PG(ignore_user_abort); if(arg){ zend_alter_ini_entry_ex("ignore_user_abort",sizeof("ignore_user_abort"), arg, arg_len, PHP_INI_USER, PHP_INI_STAGE_RUNTIME,0 TSRMLS_CC);} RETURN_LONG(old_setting);}/* }}} */
connection_status函數直接返回PG(connection_status)的值,
ignore_user_abort函數重新設定PG(ignore_user_abort)的值,
不管是因為緩衝滿自動調用或通過flush函數調用的flush操作,其最終都會根據串連狀態判斷是否執行php_handle_aborted_connection函數,如果是abort狀態,則執行。
其代碼如下:
/* {{{ php_handle_aborted_connection*/PHPAPI void php_handle_aborted_connection(void){ TSRMLS_FETCH(); PG(connection_status)= PHP_CONNECTION_ABORTED; php_output_set_status(0 TSRMLS_CC); if(!PG(ignore_user_abort)){ zend_bailout();}}/* }}} */
在PG(ignore_user_abort)為假時,即不忽略使用者的中斷行為時,如果調用了此函數,則使用zend_bailout函數跳出程式直接exit。
在預設情況下ignore_user_abort為0,即不忽略使用者的中斷行為。
如果你是ubuntu的預設apache環境下,可能上面的代碼會無效。這是由於此環境下的apache開啟了zip,在沒有達到預定的大小時,伺服器不會與用戶端通訊,從而也就無法擷取用戶端的狀態,即使使用了flush函數也是一樣。