php原生socket之IO多路複用以及實現web伺服器

荒街!發表於2022-06-22

本文轉載於我的個人部落格,原文地址 www.codeover.cn/php-socket-http/ 歡迎來訪

多路複用

前文 通過原生 socket 實現了簡單的服務端與客戶端通訊,但當有多個客戶端連線時,服務端僅能處理第一個客戶端的請求,而無法對後續客戶端服務

服務端未正確處理截圖

產生這種情況的原因是因為IO模型是阻塞的,同一時刻只能由一個客戶端進行訪問,解決此問題主要有兩種解決方案:

  1. 多程式,即在服務端啟動多個程式監聽
  2. IO多路複用機制,簡單來說實現了 N 個客戶端使用一根網線同時訪問

同時多路複用又分為兩個不同的模型,即 selectepoll,常見的軟體中,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 協議》,轉載必須註明作者和本文連結

相關文章