概述
眾所周知,我們一般使用 PHP 開發Web程序時需要使用到比如Apache或Nginx等Web服務器來支持,那么有沒有辦法直接使用PHP開發HTTP服務器,答案當然是可以的,最近看了一遍Workerman框架的源碼,於是自己仿照寫了一個簡易的HTTP服務器,學習為主。本文涉及到知識點包括:
- PHP Socket編程
- 網絡 IO 模型
- PHP libevent
- PHP 多進程
- PHP 擴展信號
如何編寫 HTTP 服務器
下面是一個簡易版HTTP服務器,HTTP 是應用層,其實底層用的是 TCP,在TCP 的基礎上包了一層 HTTP的協議。代碼如下:
require_once 'Http.php'; $socket = stream_socket_server("0.0.0.0:2345", $errno, $errstr); if (!$socket) { echo "$errstr ($errno)<br />\n"; } else { while (true) { $conn = @stream_socket_accept($socket); if ($conn) { $data = Http::encode('Hi world'); fwrite($conn, $data); fclose($conn); } else { echo "no newSocket\n"; } } }
幾行代碼就可以實現一個簡單的 web 服務器,在 shell 下面執行下面命令,在瀏覽器輸入:http://127.0.0.1:2345/ 即可看到 Hi world。
php simple_http_server.php
上面那那種構架,阻塞模式,要等前一個處理完了,才能處理下一個。所以流量稍微大一點,就會處理不過來。那我們可以改進一下,變成多進程模式。
require_once 'Http.php'; $socket = stream_socket_server("0.0.0.0:2345", $errno, $errstr); if (!$socket) { echo "$errstr ($errno)<br />\n"; } else { while (true) { if (pcntl_fork() == 0) { $conn = @stream_socket_accept($socket); if ($conn) { $data = Http::encode('Hi world'); fwrite($conn, $data); fclose($conn); } else { echo "no newSocket\n"; } } } }
這種模式最大的問題是,進程/線程創建和銷毀的開銷很大。所以上面的模式沒辦法應用於非常繁忙的服務器程序
高性能的服務器
其實IO復用的歷史和多進程一樣長,Linux很早就提供了 select 系統調用,可以在一個進程內維持1024個連接。后來又加入了poll系統調用,poll做了一些改進,解決了 1024 限制的問題,可以維持任意數量的連接。但select/poll還有一個問題就是,它需要循環檢測連接是否有事件。這樣問題就來了,如果服務器有100萬個連接,在某一時間只有一個連接向服務器發送了數據,select/poll需要做循環100萬次,其中只有1次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。
直到Linux 2.6內核提供了新的epoll系統調用,可以維持無限數量的連接,而且無需輪詢,這才真正解決了 C10K 問題。現在各種高並發異步IO的服務器程序都是基於epoll實現的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 這樣單進程單線程的程序,都可以維持超過1百萬TCP連接,全部歸功於epoll技術。
libevent是一個輕量級的基於事件驅動的高性能的開源網絡庫,並且支持多個平台,依據系統提供的select,poll和epoll方法來進行I/O復用,但是針對於多個系統平台上的不同的I/O復用實現方式,libevent進行了重新的封裝,並提供了統一的API接口。libevent在實現上使用了事件驅動這種機制。
我們通過 多進程 + libevent 來構架 web 服務器,結構圖如下:
具體的代碼可以到 demo, 執行 php demo.php start 即可。
壓力測試
硬件是自己 Mac pro,依據 1000 並發重復 100次進行測試:
先測試一個 Nginx + fpm ,siege -c 1000 -r 100 http://yii2.localhost/ 結果如下:
再測試自己寫的服務器 siege -c 1000 -r 100 http://127.0.0.0:2345
自己寫的服務器成功率幾乎是 Nginx + fpm 的 2 倍 。