在PHP 中使用socket進行網絡編程時,非阻塞模式能夠提升並發處理能力,是實現事件驅動或多路復用服務器的關鍵。然而,某些時候我們又需要臨時地將套接字設置為阻塞狀態,以完成某些“必須等待”的操作,例如完整讀取一個大數據包,或者等待客戶端發送完整的握手數據。
幸運的是,PHP 提供了socket_set_block()和socket_set_nonblock()兩個函數,可以非常方便地在阻塞與非阻塞模式之間進行切換。
本文將結合一個具體的TCP 服務端示例,演示如何在非阻塞監聽中臨時切換套接字為阻塞狀態,並在完成操作後恢復非阻塞狀態。
我們構建一個非阻塞TCP 服務器,監聽客戶端連接,並在接收到客戶端連接之後,臨時將該連接切換為阻塞模式,從客戶端讀取一段數據,完成後再切回非阻塞模式。
以下是完整代碼:
<code> <?php $host = '0.0.0.0';
$port = 12345;
// 創建socket
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
// 綁定和監聽
socket_bind($server, $host, $port);
socket_listen($server);
// 設置為非阻塞
socket_set_nonblock($server);
echo "Server listening on {$host}:{$port} ...\n";
$clients = [];
while (true) {
// 接受連接(非阻塞)
$client = @socket_accept($server);
if ($client !== false) {
echo "New client connected.\n";
// 添加到客戶端列表
$clients[] = $client;
// 設置客戶端為非阻塞
socket_set_nonblock($client);
}
foreach ($clients as $index => $sock) {
$buffer = '';
// 臨時設置為阻塞以確保完整讀取(例如讀取固定長度或直到某個標誌)
socket_set_block($sock);
$bytes = @socket_recv($sock, $buffer, 1024, MSG_DONTWAIT);
// 恢復為非阻塞
socket_set_nonblock($sock);
if ($bytes === false || $bytes === 0) {
// 客戶端斷開
echo "Client disconnected.\n";
socket_close($sock);
unset($clients[$index]);
continue;
}
// 輸出讀取數據
echo "Received: " . trim($buffer) . "\n";
// 示例響應
$response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello from server!\n";
socket_write($sock, $response);
}
// 輕微延遲避免 CPU 佔滿
usleep(100000);
}
socket_close($server);
?>
</code>
在上述代碼中,我們的監聽socket($server)是非阻塞的,因此不會在socket_accept()上阻塞。每當接受到新連接後,我們將新客戶端也設置為非阻塞。然而,在我們處理接收數據的部分,我們主動將客戶端socket 切換為阻塞模式,這是因為我們希望保證socket_recv()能完整接收到我們期望的數據,避免數據殘缺。
這是非常常見的混合使用方式,在很多場景(如協議握手階段)尤其重要。例如你設計了一個簡單協議,規定客戶端第一次發送必須包含固定頭信息,服務端需要完整接收後才能繼續處理,此時使用阻塞讀取就變得非常有意義。
切換模式要謹慎:在非阻塞流程中臨時使用阻塞讀取是一種“強制等待”的手段,雖然方便,但如果客戶端沒有發送任何數據,服務端會被卡住,建議搭配超時機製或預設數據長度。
適用於短期阻塞:這類模式切換適用於某些操作必須阻塞完成的情況,不建議在整個通信過程中都採用阻塞方式。
使用socket_set_block()和socket_set_nonblock()可以讓你的PHP 套接字編程更具彈性。你可以根據處理邏輯靈活選擇合適的阻塞模式,從而兼顧效率與完整性。
更多socket 編程的實踐與示例可以參考文檔或訪問我們的資源平台: