PHP Conference Japan 2024

連線處理

在 PHP 內部,會維護一個連線狀態。有 4 種可能的狀態:

  • 0 - NORMAL(正常)
  • 1 - ABORTED(已中止)
  • 2 - TIMEOUT(逾時)
  • 3 - ABORTED 和 TIMEOUT(已中止且逾時)

當 PHP 腳本正常執行時,會啟用 NORMAL 狀態。如果遠端客戶端斷線,則會開啟 ABORTED 狀態旗標。遠端客戶端斷線通常是因為使用者按下停止按鈕所致。如果達到 PHP 設定的時間限制(請參閱 set_time_limit()),則會開啟 TIMEOUT 狀態旗標。

您可以決定是否要讓客戶端斷線導致腳本中止。有時候,即使沒有遠端瀏覽器接收輸出,也最好讓腳本始終執行到完成。但是,預設行為是當遠端客戶端斷線時,腳本會中止。此行為可以透過 php.ini 指令中的 ignore_user_abort 設定,以及透過相應的 php_value ignore_user_abort Apache httpd.conf 指令或 ignore_user_abort() 函式來設定。如果您沒有告訴 PHP 忽略使用者中止,而且使用者中止連線,則您的腳本將會終止。唯一的例外是,如果您已使用 register_shutdown_function() 註冊關機函式。使用關機函式時,當遠端使用者按下停止按鈕時,PHP 會偵測到連線已中止,並且會呼叫關機函式。此關機函式也會在腳本正常終止時呼叫,因此若要在客戶端斷線時執行不同的動作,您可以使用 connection_aborted() 函式。如果連線已中止,此函式會傳回 true

您的腳本也可能會因內建的腳本計時器而終止。預設逾時為 30 秒。可以使用 max_execution_time php.ini 指令或對應的 php_value max_execution_time Apache httpd.conf 指令,以及 set_time_limit() 函式來變更此設定。當計時器過期時,腳本將會中止,並且如上述客戶端斷線的情況,如果已註冊關機函式,則會呼叫該函式。在此關機函式中,您可以檢查是否因為逾時而呼叫關機函式,方法是呼叫 connection_status() 函式。如果因為逾時而呼叫關機函式,此函式會傳回 2。

要注意的一件事是,ABORTED 和 TIMEOUT 狀態可以同時處於啟用狀態。如果您告訴 PHP 忽略使用者中止,則可能會發生這種情況。PHP 仍然會注意到使用者可能已中斷連線,但是腳本將會繼續執行。如果接著達到時間限制,則將會中止,並且會呼叫您的關機函式 (如果有的話)。此時,您會發現 connection_status() 傳回 3。

新增註解

使用者提供的註解 11 則註解

65
tom lgold2003 at gmail dot com
15 年前
嘿,感謝 arr1,當我需要快速返回給使用者然後執行其他操作時,這對我非常有用。

使用這些程式碼時,幾乎讓我瘋掉,我發現另一個可能會影響程式碼的東西

Content-Encoding: gzip

這是因為啟用了 zlib,而且內容將會壓縮。但是,這將不會輸出緩衝區,直到所有輸出完成。

因此,可能需要傳送標頭來防止這個問題。

現在,程式碼變成

<?php
ob_end_clean
();
header("Connection: close\r\n");
header("Content-Encoding: none\r\n");
ignore_user_abort(true); // 選項
ob_start();
echo (
'使用者會看到的文字');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // 奇怪的行為,無法運作
flush(); // 除非兩者都呼叫!
ob_end_clean();

// 在這裡進行處理
sleep(5);

echo(
'使用者永遠不會看到的文字');
// 進行一些處理
?>
45
arr1 at hotmail dot co dot uk
18 年前
自從 4.1 版以來,當 register_shutdown_function() 的行為被修改為不會自動關閉使用者的連線時,在保持 PHP 腳本運行的同時關閉使用者的瀏覽器連線一直是個問題。

sts at mail dot xubion dot hu
發佈原始解決方案

<?php
header
("Connection: close");
ob_start();
phpinfo();
$size=ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();
sleep(13);
error_log("在背景中執行某些操作");
?>

這可以正常運作,直到您將 phpinfo() 替換為
echo ('我希望使用者看到的文字'); 在這種情況下,標頭永遠不會傳送!

解決方案是在傳送標頭資訊之前,明確關閉輸出緩衝並清除緩衝區。

範例

<?php
ob_end_clean
();
header("Connection: close");
ignore_user_abort(); // 可選
ob_start();
echo (
'使用者將看到的文字');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // 奇怪的行為,不會生效
flush(); // 除非兩個都呼叫!
// 在這裡進行處理
sleep(30);
echo(
'使用者永遠不會看到的文字');
?>

我花了 3 個小時才弄清楚這個,希望對其他人有幫助 :)

測試環境
IE 7.5730.11
Mozilla Firefox 1.81
28
Lee
20 年前
最後一個評論中提到的點並不總是如此。

如果使用者在訂單處理腳本確認使用者的信用卡/將他們新增到資料庫等過程中(由於他們的 ISP 故障、網路問題...等等)失去連線,並且您的腳本嘗試傳回輸出(例如,「預先處理訂單」或任何其他類型的確認),那麼您的腳本將會中止 -- 這可能會為您的流程帶來問題。

我有一個訂單腳本會將資料新增到 InnoDB 資料庫(透過 MySQL),並且只有在成功完成時才會提交交易。如果沒有 ignore_user_abort(),我曾經發生過使用者在處理階段中斷連線的情況...他們的卡被扣款了,但他們沒有被新增到我的本機資料庫中。

因此,如果您正在處理應該繼續進行的敏感交易,那麼忽略任何中止都是安全的,無論您的使用者是否在另一端「觀看」。
31
mheumann at comciencia dot cl
11 年前
我遇到了很多問題,導致重新導向無法運作,之後我的腳本原本應該在背景中繼續運作。重新導向到我網站的另一個頁面,只有在原始頁面完成處理後才會運作。

我終於發現了問題所在
Session 只有在腳本的最後才會被 PHP 關閉,由於對 Session 資料的存取會被鎖定,以防止多個頁面同時寫入,因此新的頁面在原始處理完成之前無法載入。

解決方案
使用 session_write_close() 重新導向時手動關閉 Session

<?php
ignore_user_abort
(true);
set_time_limit(0);

$strURL = "在此處放置您的重新導向連結";
header("Location: $strURL", true);
header("Connection: close", true);
header("Content-Encoding: none\r\n");
header("Content-Length: 0", true);

flush();
ob_flush();

session_write_close();

// 繼續處理...

sleep(100);
exit;
?>

但要小心
請確保您的腳本在 session_write_close() 之後不會寫入 Session,例如在您的背景處理程式碼中。這將無法運作。還要避免讀取,請記住,下一個腳本可能已經修改了資料。

因此,請嘗試在重新導向前讀取您需要的資料。
20
Marco
7 年前
由於某些原因,此處未列出的 CONNECTION_XXX 常數為

0 = CONNECTION_NORMAL
1 = CONNECTION_ABORTED
2 = CONNECTION_TIMEOUT
3 = CONNECTION_ABORTED & CONNECTION_TIMEOUT

數字 3 的實際測試方式如下
if (CONNECTION_ABORTED & CONNECTION_TIMEOUT)
echo '連線中止且逾時';
26
a1n2ton at gmail dot com
14 年前
PHP 會在連線中止時變更目錄,因此像這樣的程式碼將無法達到您想要的效果

<?php
function abort()
{
if(
connection_aborted())
unlink('file.ini');
}
register_shutdown_function('abort');
?>

實際上,它會刪除 Apache 根目錄中的檔案,因此如果您想在中止時取消連結腳本目錄中的檔案或寫入檔案,您必須儲存目錄
<?php
function abort()
{
global
$dsd;
if(
connection_aborted())
unlink($dsd.'/file.ini');
}
register_shutdown_function('abort');
$dsd=getcwd();
?>
24
Ilya Penyaev
12 年前
當我嘗試讓我的腳本將客戶端重新導向到另一個 URL,然後繼續處理時,我遇到了很大的困難。原因在於 php-fpm。除非我呼叫 fastcgi_finish_request();,否則所有可能的緩衝區排清都無效。

例如

<?php
// 重新導向...
ignore_user_abort(true);
header("Location: ".$redirectUrl, true);
header("Connection: close", true);
header("Content-Length: 0", true);
ob_end_flush();
flush();
fastcgi_finish_request(); // 使用 php-fpm 時很重要!

sleep (5); // 使用者不會感受到此休眠,因為他已經離開了

// 在使用者被重新導向後進行一些工作
?>
20
Anonymous
12 年前
這個簡單的函數會輸出字串並關閉連線。它考慮使用「ob_gzhandler」進行壓縮

我花了一點時間才將所有這些整合在一起,主要是因為將編碼設定為 none(如同這裡的一些人所指出)並未生效。

<?php
function outputStringAndCloseConnection2($stringToOutput)
{
set_time_limit(0);
ignore_user_abort(true);
// 緩衝所有即將發生的輸出 - 確保我們關心壓縮:
if(!ob_start("ob_gzhandler"))
ob_start();
echo
$stringToOutput;
// 取得輸出的尺寸
$size = ob_get_length();
// 傳送標頭,告知瀏覽器關閉連線
header("Content-Length: $size");
header('Connection: close');
// 排清所有輸出
ob_end_flush();
ob_flush();
flush();
// 關閉目前的 Session
if (session_id()) session_write_close();
}
?>
20
Anonymous
17 年前
關於發文者
arr1 at hotmail dot co dot uk

如果您使用/寫入 Session,則需要在之前執行此操作
(否則它將無法運作)

session_write_close();

如果想要的話

ignore_user_abort(TRUE);
而不是 ignore_user_abort();
18
pulstar at mail dot com
21 年前
這些函數非常有用,例如,如果您需要控制您的網站訪客何時下訂單,並且您需要檢查他/她是否沒有按兩次提交按鈕或在按一下提交按鈕後立即取消提交。
如果您的訪客在提交後立即按一下停止按鈕,您的腳本可能會在註冊產品的過程中停止,並且無法完成清單,導致您的資料庫中出現不一致的情況。
透過 ignore_user_abort() 函數,您可以讓您的腳本順利完成所有工作,之後您可以使用 register_shutdown_function() 和 connection_aborted() 來檢查訪客是否取消了提交或失去連線。如果他/她取消了,您可以將訂單設定為未確認,當訪客回來時,您可以再次顯示舊訂單。
為了防止重複點擊送出按鈕,您可以使用 JavaScript 來禁用它,或者在您的腳本中為該訂單設置一個標記,該標記將被記錄到資料庫中。在接受新的提交之前,腳本會檢查是否之前沒有下過相同的訂單,並拒絕它。這將正常運作,因為腳本會在之前完成工作。
請注意,如果您在腳本的開頭使用 ob_start("callback_function"),您可以指定一個回呼函數,該函數將在腳本結束時像關閉函數一樣執行,並允許您在將產生的頁面發送給訪客之前對其進行處理。
12
Jean Charles MAMMANA
16 年前
只有當客戶端優雅地斷開連線(使用停止按鈕)時,connection_status() 才會返回 ABORTED 狀態。在這種情況下,瀏覽器會發送 RST TCP 封包,通知 PHP 連線已關閉。
但是...如果連線因網路問題(例如 Wi-Fi 連線中斷)而停止,腳本不知道客戶端已斷開連線 :(

我嘗試使用 fopen("php://output") 和 stream_select() 來寫入以檢測寫入鎖定(由於緩衝區已滿),但 PHP 給我這個錯誤:「cannot represent a stream of type Output as a select()able descriptor」(無法將 Output 類型的串流表示為可使用 select() 的描述符)。

所以我不知道如何正確檢測網路連線問題...
To Top