PHP 原生不支持多線程,但在高併發場景下(如同時檢測多台服務器狀態),逐個發起阻塞式 fsockopen() 連接會導致總耗時等於所有單次連接耗時之和,嚴重影響性能。本文介紹如何利用非阻塞 I/O 實現「偽多線程」並發請求。
$hosts = ["host1.sample.com", "host2.sample.com", "host3.sample.com"];
$timeout = 15;
$status = [];
foreach ($hosts as $host) {
$s = fsockopen($host, 80, $errno, $errstr, $timeout);
// ... 發送請求、讀取響應 ...
}
此代碼需等待每個連接建立或超時才能進行下一個,對於大量主機極其低效。
PHP 5 引入 stream_socket_client() 支持 STREAM_CLIENT_ASYNC_CONNECT 標誌,配合 stream_select() 實現事件驅動的非阻塞並發:
$hosts = ["host1.sample.com", "host2.sample.com", "host3.sample.com"];
$timeout = 15;
$status = [];
$sockets = [];
// 1. 發起所有非阻塞連接
foreach ($hosts as $id => $host) {
$s = stream_socket_client("$host:80", $errno, $errstr, $timeout, STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_CONNECT);
if ($s) {
$sockets[$id] = $s;
$status[$id] = "in progress";
} else {
$status[$id] = "failed, $errno $errstr";
}
}
// 2. 事件循環:等待可讀/可寫事件
while (count($sockets)) {
$read = $write = $sockets;
$n = stream_select($read, $write, $e = null, $timeout);
if ($n > 0) {
// 處理可讀 socket(連接成功或收到數據)
foreach ($read as $r) {
$id = array_search($r, $sockets);
$data = fread($r, 8192);
if (strlen($data) === 0) {
if ($status[$id] === "in progress") $status[$id] = "failed to connect";
fclose($r);
unset($sockets[$id]);
} else {
$status[$id] .= $data;
}
}
// 處理可寫 socket(連接已建立,發送請求)
foreach ($write as $w) {
$id = array_search($w, $sockets);
fwrite($w, "HEAD / HTTP/1.0\r\nHost: " . $hosts[$id] . "\r\n\r\n");
$status[$id] = "waiting for response";
}
} else {
// 超時處理
foreach ($sockets as $id => $s) {
$status[$id] = "timed out " . $status[$id];
}
break;
}
}
// 3. 輸出結果
foreach ($hosts as $id => $host) {
echo "Host: $host\nStatus: " . $status[$id] . "\n\n";
}
核心原理: stream_select() 監聽多個流的讀/寫就緒事件,類似系統調用 select(2),實現單進程多路複用。
若環境不支持 streams,可使用 socket_create()、socket_set_nonblock()、socket_connect() 手動構建非阻塞 socket,並用 socket_select() 替換 stream_select(),邏輯完全一致。
stream_socket_client(),代碼更簡潔且支持所有流封裝協議(HTTP、SSL、UNIX domain socket 等)。