在使用PHP 進行Socket 編程時,很多開發者初次接觸socket_accept()時都會遇到一個問題:。這在某些需要高並發或者多個連接同時處理的場景中會成為瓶頸。
那麼,為什麼會出現這種情況?我們又該如何使用socket_select()來避免阻塞並優雅地解決這個問題呢?
socket_accept()是用來接受一個來自監聽套接字的連接請求的函數。其原型如下:
resource socket_accept(resource $socket);
這個函數的本質是:當沒有客戶端連接時,它會阻塞程序執行,直到有連接到來。這是典型的阻塞式I/O 行為。
這種行為對於單線程服務器來說,是不可取的。如果服務器因為等待一個連接而阻塞,其他任務將完全無法執行。
為了解決這個問題,PHP 提供了socket_select()函數,可以在多個套接字上進行“監聽”,判斷哪些套接字已經準備好進行讀/寫操作,從而避免阻塞等待。
int socket_select(
array &$read,
array &$write,
array &$except,
int $tv_sec,
int $tv_usec = 0
);
你只需要將要監聽的socket 放入$read數組,然後調用socket_select() 。當該socket 可讀時(即有客戶端連接請求時), socket_select()就會返回,這樣你再調用socket_accept()就不會阻塞了。
下面是一個使用socket_select()的服務器端完整示例:
<?php
set_time_limit(0);
// 創建 socket
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '0.0.0.0', 8080);
socket_listen($server);
// 設置非阻塞模式(可選,但推薦)
socket_set_nonblock($server);
$clients = [];
while (true) {
$read_sockets = [$server] + $clients;
$write = $except = [];
// 監聽所有 sockets,超時為 1 秒
$changed = socket_select($read_sockets, $write, $except, 1);
if ($changed === false) {
echo "socket_select() 發生錯誤\n";
break;
}
// 有新的連接請求
if (in_array($server, $read_sockets)) {
$client = socket_accept($server);
if ($client !== false) {
socket_getpeername($client, $ip, $port);
echo "新連接來自 {$ip}:{$port}\n";
$clients[] = $client;
}
// 從 read 列表中移除監聽 socket
$key = array_search($server, $read_sockets);
unset($read_sockets[$key]);
}
// 處理已有連接的數據
foreach ($read_sockets as $sock) {
$data = @socket_read($sock, 1024, PHP_NORMAL_READ);
if ($data === false || trim($data) === '') {
// 客戶端斷開連接
$key = array_search($sock, $clients);
unset($clients[$key]);
socket_close($sock);
continue;
}
$data = trim($data);
echo "接收到數據:{$data}\n";
$response = "你發送了:{$data}\n";
socket_write($sock, $response);
}
}
socket_close($server);
socket_accept()是阻塞的,適合簡單的、同步的單連接場景;
要構建一個支持並發的服務器,應當使用socket_select()來監聽socket 的狀態;
socket_select()可以讓你在多個socket 上“等待”事件,並避免因為一個阻塞操作導致整個程序掛起;
建議配合socket_set_nonblock()使用,提高靈活性。
通過以上方式,你可以實現一個響應迅速、可同時處理多個客戶端連接的PHP Socket 服務器,為更複雜的網絡編程打下基礎。
在實際開發中,域名或者地址替換為自己的服務地址,例如: