其實會想用fsockopen做非同步幕後執行,主要是因為客戶要開發自動批次下載Google雲端硬碟及自動建立檔案功能,如果用以往的活人撞針方法,觸發執行的那個ip會因為自動執行畫面整個延遲卡死(UX死當),必須改用非同步方式來解決(瀏覽跟執行層分開),經過測試fsockopen確實能開一個新的HTTP請求至目標腳本然後斷線不理(非同步),腳本自動以Web Server中獨立request執行程序,完全不影響前端頁面瀏覽不延遲,能優化UX體驗,不需要用活人獻祭了(改幕後執行),以下是框架及心得分享
要建立fsockopen前需要先使用Xoops的preload機制,preload是XOOPS提供的全域事件機制,可在每次request的特定生命週期節點(如 footer)插入自訂邏輯,使所有頁面都能觸發指定行為,換句話說在模組中執行preload的腳本,Xoops全體頁面都能被觸發事件(全域事件機制),所以拿來當作fsockopen的執行footer非常適合。
preload = XOOPS 全域事件鉤子(Hook)
XOOPS 啟動流程中→ 每個 request 都會觸發 preload
→ 依事件執行(你用的是 Footer)
建立preload方法
先在xoops模組根目錄建立一個preloads,在置入一個core.php檔案例如:my_modules/preloads/core.php
然後加入以下代碼
<?php
class NeilalbumCorePreload extends XoopsPreloadItem {
public static function eventCoreFooterStart($args) {
//eventCoreFooterStart 所有頁面 / 在輸出footer前 /都會觸發
async_trigger();
}
}
function async_trigger() {
global $xoopsModuleConfig,$xoopsModule,$isAdmin;
//做一個$token
$secret_id="rEDBzCyHEJ7Yv32ie5JPyB0WmKP9caeDPbkpfc1L"; //可改成自己要的雜奏碼
$ts = time();
$token = hash_hmac('sha256', 'my_modules_script'.$ts, $secret_id);
$url = XOOPS_URL . '/modules/my_modules/script.php?ts='.$ts.'&token='.$token; //script.php是要非同步的執行腳本檔案
$parts = parse_url($url);
$host = $parts['host'];
$path = $parts['path'] . (isset($parts['query']) ? '?' . $parts['query'] : '');
$port = ($parts['scheme'] == 'https') ? 443 : 80;
//透過 fsockopen 主動發起一個 HTTP 請求至目標腳本,並在送出後立即關閉連線,使該腳本由 Web Server 以獨立 request 執行,達到非同步效果。
$fp = @fsockopen(
($parts['scheme'] == 'https' ? 'ssl://' : '') . $host,
$port,
$errno,
$errstr,
1
);
if ($fp) {
//以下是非同步成立的關鍵
stream_set_blocking($fp, false);
$out = "GET $path HTTP/1.1\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n\r\n"; //不等回應送出後直接斷線(腳本於Web Server獨立執行)
fwrite($fp, $out);
fclose($fp);
}
}
運作原理
PHP A(主頁)→ fsockopen 打 HTTP 請求
→ 不等回應
→ 直接結束
PHP B(script.php)
→ 由 Web Server 接手
→ 獨立執行
到這裡完成請求端的部分,每當xoops執行preload就會發一個request執行程序到script.php且立即斷線,再來就是執行端部分,在模組根目錄建一個腳本檔,例如:my_modules/script.php
然後加入以下代碼
<?php
include "header.php";
ignore_user_abort(true); //使用者斷線繼續跑
set_time_limit(0); //避免PHP執行超時
global $xoopsModuleConfig,$xoopsModule,$isAdmin;
//log輸出-重要因為在腳本中無法ceho也不會有錯誤輸出,透過log可以查看事件進行回應
function cron_log($msg) {
file_put_contents(
XOOPS_ROOT_PATH . '/uploads/my_modules_script.log',
date('H:i:s') . ' ' . $msg . "\n",
FILE_APPEND
);
}
//LOG使用方法如下
/*
cron_log("進入script");
cron_log("ts=".$ts ?? 'NULL');
cron_log("token=".$token ?? 'NULL');
cron_log("secret_id=".$secret_id ?? 'NULL');
*/
//腳本位置 XOOPS_ROOT_PATH . '/uploads/my_modules_script.log',
//token 驗證(重要)
$secret_id="rEDBzCyHEJ7Yv32ie5JPyB0WmKP9caeDPbkpfc1L"; //跟來源端一樣
$ts = isset($_GET['ts']) ? (int)$_GET['ts'] : 0;
$token = isset($_GET['token']) ? $_GET['token'] : '';
if (empty($ts) || empty($token)) exit;
//時間限制(5分鐘)
if (abs(time() - (int)$ts) > 300) exit;
// 驗證
//$check = hash_hmac('sha256', $ts, $secret_id);
$check = hash_hmac('sha256', 'my_modules_script'.$ts, $secret_id);
if (!hash_equals($check, $token)) exit;
//這裡要放執行的腳本代碼(批次下載寄信或是自動化排程功能都可)
exit;
這樣就完成幕後執行機制建立,觸發者完全沒任何延遲,全部都由腳本在Web Server以獨立request執行,但有一點還是要注意,既使是幕後執行,仍會吃主機的I/O 資源,所以腳本最好是作分流批次處理或是上執行鎖,避免Web Server負載過重當掉。
以上工作心得分享,有需要的朋友參考看看!!
工作心得撰寫:徐嘉裕 Neil hsu
留言
張貼留言