本文轉載於我的個人部落格,原文地址 www.codeover.cn/php-socket-http/ 歡迎來訪
多路複用
前文 通過原生 socket 實現了簡單的服務端與客戶端通訊,但當有多個客戶端連線時,服務端僅能處理第一個客戶端的請求,而無法對後續客戶端服務
產生這種情況的原因是因為IO模型是阻塞的,同一時刻只能由一個客戶端進行訪問,解決此問題主要有兩種解決方案:
- 多程式,即在服務端啟動多個程式監聽
- IO多路複用機制,簡單來說實現了 N 個客戶端使用一根網線同時訪問
同時多路複用又分為兩個不同的模型,即 select
與 epoll
,常見的軟體中,Apache
使用了 select
模型,nginx
則使用 epoll
模型。在 php 中內建了 select
模型,對應的函式為 socket_select
,多路複用是實現 http 伺服器的基礎
語法
在前文中我們介紹了 php 原生 socket 內建了 socket_select
函式實現了 select
模型,其語法如下:
socket_select(
array &$read,
array &$write,
array &$except,
int $seconds [,
int $microseconds = 0]
): int|false
引數
read
服務端監聽的套接字資源,當他有變化(即收到新的訊息或有客戶端連線、斷開)時,
socket_select
函式才會返回(否則繼續阻塞),同時修改該變數為當前發生事件(收到訊息或有客戶端連線、斷開)的套接字資源列表,並繼續向下執行。write
監聽是否有客戶端寫資料,傳入
null
則代表不關心是否有寫變化except
套接字內要排除的元素,傳入
null
是 「監聽」 全部seconds
秒和微秒一起構成超時引數。如果傳入
null
則會阻塞,為 0 非阻塞,如果是 >0 則為最大阻塞時間microseconds
優化
我們在 上篇文章 簡單實現了 socket 服務端監聽與客戶端的連線,接下來我們在服務端監聽程式碼的基礎上通過多路複用優化程式碼:
<?php
// 建立套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 設定 ip 被釋放後立即可使用
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true);
// 繫結ip與埠
socket_bind($socket, 0, 8888);
// 開始監聽
socket_listen($socket);
$sockets[] = $socket;
while (true) {
$tmp_sockets = $sockets;
socket_select($tmp_sockets, $write, $except, null);
foreach ($tmp_sockets as $sock) {
// 如果當前套接字等於 socket_create 建立的套接字,說明是有新的連線或有新的斷開連線
if ($sock == $socket) {
$conn_sock = socket_accept($socket);
$sockets[] = $conn_sock;
socket_getpeername($conn_sock, $ip, $port);
echo '請求ip: ' . $ip . '埠: ' . $port . PHP_EOL;
} else { // 否則說明是之前連線的客戶端發來訊息
$msg = socket_read($sock, 10240);
socket_write($sock, strtoupper($msg));
echo $msg;
}
}
}
在本示例中 socket_select
函式會阻塞當前程式,當 $tmp_sockets
陣列內的 socket 資源有新的客戶端連線或斷開或收到新訊息時,會將 $tmp_sockets
陣列修改為當前活躍的 socket 資源,隨後通過遍歷該陣列處理業務邏輯
使用socket實現簡易http伺服器
http 協議是在 socket 的基礎上規定了指定的資料格式,所以我們只需在 socket_write
時按照格式傳送資料,瀏覽器就可正常響應請求
<?php
// 建立套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 設定 ip 被釋放後立即可使用
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true);
// 繫結ip與埠
socket_bind($socket, 0, 8888);
// 開始監聽
socket_listen($socket);
$sockets[] = $socket;
while (true) {
$tmp_sockets = $sockets;
socket_select($tmp_sockets, $write, $except, null);
foreach ($tmp_sockets as $sock) {
if ($sock == $socket) {
$conn_sock = socket_accept($socket);
$sockets[] = $conn_sock;
} else {
$msg = socket_read($sock, 10240);
var_dump($msg);
if ($msg == '') return;
$output = '<h1>this is php worker</h1>';
$len = strlen($output);
$response = "HTTP/1.1 200 OK\r\n";
$response .= "content-type: text/html\r\n";
$response .= "server: php socket\r\n";
$response .= "Content-Length: {$len}\r\n\r\n";
$response .= $output;
socket_write($sock, $response);
}
}
}
在服務端執行此示例,隨後在瀏覽器訪問 ip:8888
,可以看到如下:
同時服務端會輸出如下內容:
GET / HTTP/1.1
Host: 124.222.**.**:8888
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: jenkins-timestamper-offset=-28800000; _ga=GA1.1.1403944751.1652010033; _ga_2GM6102E19=GS1.1.1652802985.7.1.1652803014.0
該內容即為使用者端請求原始資料,可解析此資料並根據請求做出響應,比如使用 file_get_content
讀取指定檔案內容返回給瀏覽器
本作品採用《CC 協議》,轉載必須註明作者和本文連結