NIO、BIO、AIO 與 PHP 實現

前言

最近看到NIO,AIO,Netty,Promise話題很熱,我做爲一個phper也想來湊湊熱鬧,湊着湊着發現周圍怎麼都是javaer,jser。那麼PHP能作NIOAIO麼?php

什麼BIO、NIO、AIO

BIO 同步阻塞I/O。html

有小夥伴又要問了啥叫 同步,啥叫阻塞啊?java

同步/異步 阻塞/非阻塞

同步: 兩個同步任務相互依賴,而且一個任務必須以依賴於另外一任務的某種方式執行。 好比在A->B事件模型中,你須要先完成 A 才能執行B。 再換句話說,同步調用種被調用者未處理完請求以前,調用不返回,調用者會一直等待結果的返回。react

異步: 兩個異步的任務徹底獨立的,一方的執行不須要等待另一方的執行。再換句話說,異步調用種一調用就返回結果不須要等待結果返回,當結果返回的時候經過回調函數或者其餘方式拿着結果再作相關事情,nginx

阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,沒法從事其餘任務,只有當條件就緒才能繼續。git

非阻塞: 非阻塞就是發起一個請求,調用者不用一直等着結果返回,能夠先去幹其餘事情。github

以上就是這四個詞彙的解釋,那麼放到計算機IO上,比較接地氣的解釋shell

BIO (Blocking I/O)

那麼咱們拿快遞攬件來舉例,一個快遞公司,有一部分工做是攬件,它的工做模式是隻能一個一個的攬件,你要寄快遞,必須排隊,一個一個的來,這就是 同步 。好不容易輪到你了,你把快遞一扔給他,他還讓給你等着,快遞工做人員說,咱們這後面還有些信息要錄入,快遞要檢查,必須等咱們快遞公司檢查完畢後,你才能離開,這叫 阻塞編程

NIO (No-Blocking I/O)

同步非阻塞的I/Osegmentfault

繼續啊,拿快遞公司舉例。這個快遞公司發現有些用戶在後面排隊,排着排着,過久了就去隔壁快遞公司了,怎麼辦呢?快遞公司想了個辦法,置辦了一個發號器和一批收納盒。來一個客戶,就把快遞放在一個收納盒裏,再給用戶一個編號,此時再來一個用戶,不論前面一個的快遞是否檢查完畢,仍是給他一個收納盒,發一個編號。不一樣客戶之間不排隊,一來就被受理了,這就是 非阻塞。 咱們再來看看內部,快遞呢仍是一個個地錄入信息,X光檢查,這樣就是 同步 運行的,等待快遞人員檢查完畢叫號,客戶拿到回執才能離開快遞點。

AIO (Asynchronous I/O)

異步非阻塞IO

也有Javaer叫他 NIO2,快遞公司攬件又升級了,作了一個快遞櫃,客戶又寄件需求,來了就放入快遞櫃,而後經過手機掃碼關注這個櫃子的動態,客戶就能夠離開了,此時服務被受理,並能立刻離開。這就是 非阻塞 。等到快遞人員來攬件時,會將櫃子裏面的寄件一併取走,快遞點集中一塊兒處理這些快遞件,發現有問題的件,不是當即停下手中的活等待客戶來出來,而是放一旁通知客戶來,而後繼續處理下一個快遞,這就是 異步

異步 阻塞 IO

同步/異步 阻塞/非阻塞,這4個名詞,兩兩組和,還有一個就是 異步/阻塞

那麼咱們仍是先把例子舉出來吧,仍是這個快遞點,來了一批客戶來寄口罩到國外,因爲有很大的可能會通不過檢查,因此,快遞點把你們都留了下來。等全部的 寄件 都檢查完了在統一給你們發送回執單,這就是 阻塞 。快遞人員檢查寄件時,發現問題不是立馬通知客戶來處理,而已放到一邊,繼續處理下一個。 這就是 異步

僞異步 IO

這種模式,底層實現是多個 同步阻塞的BIO, 同時運行。

最後總結一下:

阻塞與非阻塞指的的是當不能進行讀寫(網卡滿時的寫/網卡空的時候的讀)的時候, I/ O操做當即返回仍是阻塞;同步異步指的是,當數據已經 ready的時候,讀寫操做是同步讀仍是異步讀,階段不一樣而已。

區別

異步/同步在計算機區別

以上是一些舉例,只是幫助你們理解記憶,接下來咱們看看計算上的實現。

計算機提供的Web服務,剛開始的 CGI 模式,就是純正的 BIO 模式。一個cgi進程監聽一個端口,處理完一個請求,才能接收下一個http請求。這就是同步

實際使用體驗式異步的,那是由於後來優化了, CGI可以自我fork進程的達到同時響應多個 http請求的效果。

注意,咱們這裏討論的基礎是 單進程 ,上的 異步/同步

阻塞/非阻塞在計算機區別

這裏拿購物流程舉例,用戶的下單,須要作以下操做:

  • 商品可售否
  • 庫存數量
  • 用戶餘額
  • 觸發哪些優惠規則
  • 獎券有效性衆

按照通常作法就是一步步驗證,上一個檢查完成再進行下一個檢查,這就是 阻塞 的方式。

那麼非阻塞方式如何作呢,假設在微服務環境中,商品,庫存,獎券,促銷都是獨立的系統,調用商品服務,發起商品可售檢查請求;不等商品服務回覆,繼續調用庫存服務,發起商品可售庫存請求;緊接着依次發出...檢查請求,這樣5個檢查項目同時發起,最後,我等他們全部的請求都回復我,再來校驗是否全部的檢查都經過了。就這種發起請求不等響應,就繼續作下一件事的叫 非阻塞

轉載著名來源sifou

PHP 能作什麼

PHP 與 BIO 實現

PHP已經實現啦,這是最基本的好麼。但咱們平時測試時卻感受是不阻塞啊,你能夠試試吧nginx和php-fpm的進程限制爲1個試試。其實php-fpm就是 多進程的BIO。

  • 調整Nginx配置

調整 /etc/nginx/nginx.conf 文件:

## 把nginx worker數量設置爲1
worker_processes 1;

好了以後咱們經過ps命令檢查下
image.png

  • 調整PHP配置

調整 /etc/php/php-fpm/conf.d/www.conf 文件:

pm = static

pm.max_children = 1

pm.start_servers = 1

pm.min_spare_servers = 1

pm.max_spare_servers = 1

找到這幾個配置都改成如上數值。

最後的結果以下

image.png

我在index.php代碼裏面加入第一行就加入了sleep。

<?php
sleep(5);

咱們同時打開兩個網頁,一塊兒訪問試試
image.png

經過Firefox 抓包能夠發現,其中一個耗時5s,另外一個頁面耗時9.3s,(0.7s偏差是我手速慢了) 這就是 BIO。

image.png

好的,咱們在作一個實驗。把以上nginx,php-fpm配置中1改爲2.而後咱們打開三個網頁,同時訪問試試看。

image.png

結果是有兩個網頁耗時5s一個是9s,也就是說服務器同時處理了2個請求,第三個請求等待了4s才被處理。這就是 多線程-BIO,一個服務同時接待的客戶數量取決與worker的數量。

PHP 與 NIO 實現

咱們寫的大部分php-fpm代碼都是非阻塞的。其實PHP是支持非阻塞IO編程的。

咱們來看看PHP原生代碼實現NIO編程: PHP回顧之socket編程。這段代碼爲了實現併發使用了stream_select()

I/O 多路複用

這裏,PHP 實現 NIO 核心就是 stream_select()
經過以上源碼,發現原生的NIO實現仍是比較繁瑣,不易讀的。那麼 NIO 就是爲了實現一個socket server麼,咱們來看看Netty 官網。打開Netty首頁,它是這樣描述本身的

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

第一句話:Netty是一個 NIO 客戶端 服務框架, 能快速輕鬆地開發協程客戶端。第二句話:簡化了網絡編程,如建立TCP和UDP套接字服務。

好,重點是什麼?第一句話就是重點——開發 協程客戶端!回到咱們業務上,剛剛舉了一個例子,購物到下單,有不少個流程須要作檢查,按照通常的BIO那麼程序時序圖以下:

nio購物流程檢查

從上能夠看到,三個檢查依次分開執行。那麼客戶的等待時間是大於,庫存檢查時間加上,產品檢查時間加上,促銷檢查時間 的。

假設, 庫存,產品,促銷是三個微服務,而後購物車服務用 NIO客戶端,與這三個微服務交互,那麼會是怎樣的效果呢:

nio請求時序圖

這裏,咱們發起檢查請求時,是按照順序發起的,但不等第一個服務返回檢查結果就開始發起下一個檢查請求。最後三個服務都返回後,綜合結果,返回給用戶。那麼這三個檢查的耗時,就等於一個服務(耗時最長的那個服務)的檢查耗時。大大減小得了購物車服務響應時間。

NIO 客戶端

看到以上兩個時序圖,仍是給你們演示一下用PHP原生代碼實現一個 PHP-BIOPHP Simple NIO Server

建議你們點擊連接,把源碼 git clone https://gitee.com/xupaul/php-nio-server 到本地運行一下,再來看截圖更容易理解。

image.png

這三個所依賴的服務響應耗時,我設置爲:inventory: 4s, product: 2s, promo:6s

藍色框和黃色框標註了兩個請求,咱們主要看參數 noBlocking: true/false 的不一樣, 第一個是非阻塞方式請求, 能夠看到共耗時6s,第二個共耗時12s! (第三個爲啥和第二個耗時不同——6s這個留給你們去研究)。顯而易見得非阻塞IO的優點。不過這代碼結構就不那麼友好了,看到代碼 nio_server.php 中,有兩種請求方式,阻塞代碼流程還能看懂檢查完成後就綜合結果返回,而非阻塞方式中,發起三個檢查後程序流程就開始進入到handleMessage,代碼進入哪一個分支,取決於 socket_read 的消息,不運行起程序來,沒有文檔,很難搞懂整個程序流程。

那麼,有沒有什麼什麼方便的php類庫,讓咱們編碼更友好一點呢,這裏介紹下 ReactPHP

這裏我用ReactPHP從新實現 nio_server, 代碼在這裏

這個回調代碼寫起來有點 NodeJS 的味道呢,當你的PHP沒啓用 libev 之類的拓展時, ReactPHP內部Loop依然用的 stream_select(), 能夠看代碼 ~/react/event-loop/src/StreamSelectLoop.php@290 .

執行效果以下:
image.png

這個同時發起請求這個業務邏輯,就還得提一下 curl_multi, 它能同時發起多個curl請求,最後不斷檢查是否全部的curl請求已完成。這只是在發起多個curl請求階段作到 非阻塞 運行。

還有個拓展pThreads,可以實現多線程,不過對PHP編譯參數有限制,須要在線程安全的模式下運行。

pThreads 如今已不是PHP官方所推薦使用的拓展了,固然了這種就屬於 僞異步IO範疇了

PHP 與 AIO

PHP 異步&非阻塞 編碼。

此處, 非阻塞I/O 系統調用( nonblocking system call ) 和 異步I/O系統調用 (asychronous system call)的區別是:

  • 一個非阻塞I/O 系統調用 read() 操做當即返回的是任何能夠當即拿到的數據, 能夠是完整的結果, 也能夠是不完整的結果, 還能夠是一個空值。
  • 而異步I/O系統調用 read() 結果必須是完整的, 可是這個操做完成的通知能夠延遲到未來的一個時間點。
<?php

/**
 * 消息處理
 */
function handleMessage() {
    global $changed, $clients, $cartCheck;
    foreach ($changed as $key => $client) {
        while (true) {
            // read socket data
            $msg = @fread($client, 1024);
//            $msg = 1;
            if ($msg) {
                // application process
            } else {
                if (feof($client)) {
                    // TODO check data eof
                }
                break;
            }

能夠看到,在文件~/nio_server.php 中, 雖然設置了 stream_set_blocking false, 可是在209行的 fread() , 這是在一個循環裏讀,這是一個阻塞讀取。這的系統函數的響應速度是受系統IO影響的。

而異步調用中,當有I/O事件時,系統會將數據複製到用戶內存中,也就是準備好數據,再通知到用戶程序。

那麼原生PHP顯然是不支持的,這裏呢就要引入PHP拓展,就是 Event,或者 Ev 拓展。這篇博客主要講 Event

Event 拓展是基於 libevent 庫封裝而來,而 Ev 拓展是基於 libev 庫封裝而來。 經過PHP接口,和C庫的接口就能看到他們之間的聯繫,因此,若是經過PHP文檔找不到相關資料能夠去,看看C庫的文檔。

這裏放上用Event實現的Tcp Server demo

在用Event作這個demo中,我用到了EventBuffer ,讀、寫都和Buffer交互, Buffer數據是用戶態數據,不會等待系統I/O或被阻塞,避免了程序耗時在I/O數據拷貝上。由此PHP 也能實現 AIO 程式,提升CPU利用率。

講到這裏,就會感受這個PHP的AIO有些牽強了,我這找了其餘博主的論點來幫助你們理解,先放兩張圖。

image.png

上面是非阻塞IO,下面是異步IO。中間的區別就是非阻塞IO的應用,須要不斷的去訪問內核獲取數據(固然了,每一次訪問都是有求必應,能取到數據),但不必定能取完; 而異步IO的特色就是,你告訴內核取數據,取完整了,我再一塊兒發給應用程序。這就是Linux對異步IO的定義。

image.png

那麼再看到咱們的Demo,這是一個簡單TCP server,一個TCP請求系統是能知道一個數據的包大小的,是否接收完畢,這是傳輸層要作的。而咱們的應用層面,是接收到數據還要作合併,分包,以及數據轉碼。 這就和 AIO 數據結果必須是完整的,機率有些出入,(在系統層面顯然是完整的) . 在應用層面呢,一次性收到的不必定是完整的數據,那麼就還須要作額外代碼來解決合包,分包,沾包。這就是AIO實現Tcp Server的須要問題。

爲了解決以上問題,就須要自定義TCP通信協議。至關於本身開發RPC框架了。

那咱們來看看Http呢,在應用層面有明確公開的協議(協議有頭無尾,標明瞭每次請求具體長度),並有豐富的實現。這就是一個很是適合採用AIO編程協議。而PHP的Event拓展,剛好有EventHttp實現。

話很少說,先上 Demo

<?php
...

/**
 * event http 請求回調函數
 * 
 * @param   \EventHttpRequest   $req    Http請求對象
 */
function _http_about($req) {
    echo __METHOD__, PHP_EOL;
    // print request URL
    echo "URI: ", $req->getUri(), PHP_EOL;
    // print request's headers
    echo "Input headers:"; var_dump($req->getInputHeaders());
    echo "\n >> Sending reply ...";
    /**
     * @var \EventBuffer    $buf
     */
    $buf = $req->getOutputBuffer();
    $buf->add("It's about Event http server");
    $req->sendReply(200, "OK", $buf);
    echo "OK\n";
}

這裏是一個回調函數,入參數就是一個由 EventHttp 封裝的http請求對象。這就知足了以上 調用時非阻塞,數據徹底準備好後,再通知回調——異步I/O。好,藉助Event,PHP就實現了AIO.

結語

關於性能提高,這就不作壓測了,主要論證PHP實現NIOAIO 的可行性。也實際給你們展現了幾個Demo, 簡單展現瞭如何寫異步,非阻塞程序。

以上,但願你們經過文章能瞭解 異步/同步阻塞/非阻塞區別,以及對PHP異步非阻塞編程。

有問題歡迎提問~

參考

  1. PHP實現非阻塞
  2. PHP回顧之socket編程
  3. Cooperative multitasking using coroutines (in PHP!)
  4. IO - 同步,異步,阻塞,非阻塞
  5. 同步/異步,阻塞/非阻塞概念深度解析
  6. PHP之高性能I/O框架:Libevent
  7. 網絡編程(三):從libevent到事件通知機制
相關文章
相關標籤/搜索