Swoole HTTP Server
目標
- 瞭解swoole的http_server的使用
- 瞭解swoole的tcp服務開發
- 實際專案中問題如粘包處理、代理熱更新、使用者驗證等。
- swoole與現有框架結合
風格
- 偏基礎重程式碼
環境
- PHP版本:
- Swoole版本: https://github.com/swoole/swoole-src
- zphp開發框架: https://github.com/shenzhe/zphp
HTTP Server
- 靜態檔案處理
- 動態請求與框架結合
# 檢視SWOOLE版本 $ php -r 'echo SWOOLE_VERSION;' 4.3.1
基礎概念
CGI
CGI(Common Gateway Interface, 通用閘道器介面)是HTTP伺服器和一個獨立的程序之間的協議,它把HTTP請求Request的Header頭設定成程序的環境變數,HTT請求的正文設定成程序的標準輸入,程序的標準輸出設定為HTTP響應Response,包含Header頭和Body正文。
CGI在2000年以及之前使用的比較多,早期的Web伺服器一般只用來處理靜態的請求,Web伺服器會根據請求的內容,Fork建立一個新程序來執行外部C程式或Perl指令碼等,這個程序會把處理完的資料返回給Web伺服器,然後Web伺服器把內容傳送給使用者,Fork創建出來的程序也會隨之退出。如果下次使用者請求為動態指令碼,那麼Web伺服器會再次Fork建立一個新程序,如此周而復始的執行。
FastCGI
FastCGI是Web伺服器與處理程式之間通訊的一種協議,是CGI的改進版本。由於CGI程式反覆載入CGI而造成效能低下,如果CGI程式保持在記憶體中並接收FastCGI程序管理器排程,則可以提供良好的效能、伸縮性、Fail-Over特性等。
FastCGI就是常駐型的CGI,可以一直執行。在請求到達時不會耗費時間去Fork建立一個程序來處理。FastCGI是語言無關的、可伸縮架構的CGI開放擴充套件,它將CGI直譯器程序保持在記憶體中,因此獲得較高的效能。
FastCGI的工作流程
1.Web伺服器啟動時載入FastCGI程序管理,如IIS的ISAPI、Apache的Module...
php-cgi
PHP-FPM
PHP的直譯器PHP-CGI只是一個CGI程式,它本身只能解析請求並返回結果,不會對程序進行管理,所以就出現了一些能夠排程PHP-CGI程序的程式。PHP-FPM是PHP對FastCGI的一種具體實現,是fast-cgi程序管理工具。PHP-FPM啟動後會建立多個CGI子程序,然後主程序負責管理子程序,同時對外提供一個socket,那麼Web伺服器當要轉發一個動態請求時,只需要按照FastCGI協議要求的格式將資料發往socket即可。PHP-FPM建立的子程序去爭奪socket連線,誰搶到誰處理並將結果返回給Web伺服器。當其中一個子程序異常退出時,PHP-FPM主程序會去監控,一旦發現CGI子程序就會又啟動一個。
HTTP報文
關於HTTP請求報文的組成結構

HTTP請求報文結構
POST /search HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */* Referer: http://www.google.cn/ Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; TheWorld) Host: www.google.cn Connection: Keep-Alive Cookie: PREF=ID=80a06da87be9ae3c:U=f7167333e2c3b714:NW=1:TM=1261551909:LM=1261551917:S=ybYcq2wpfefs4V9g; NID=31=ojj8d-IygaEtSxLgaJmqSjVhCspkviJrB6omjamNrSm8lZhKy_yMfO2M4QMRKcH1g0iQv9u-2hfBW7bUFwVh7pGaRUb0RnHcJU37y- FxlRugatx63JLv7CWMD6UB_O_r hl=zh-CN&source=hp&q=domety
關於HTTP響應報文的組成結構

HTTP響應報文結構
HTTP/1.1 200 OK Date: Mon, 23 May 2005 22:38:34 GMT Content-Type: text/html; charset=UTF-8 Content-Encoding: UTF-8 Content-Length: 138 Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) ETag: "3f80f-1b6-3e1cb03b" Accept-Ranges: bytes Connection: close
建立HTTP伺服器
建立應用
$ mkdir test && cd test
Swoole在1.7.7版本後內建HTTP伺服器,可建立一個非同步非阻塞多程序的HTTP伺服器。
因為Swoole是在CLI命令列中執行的,在傳統的NGINX+FastCGI模式下很多 root
的 shell
是無法執行的,而使用Swoole伺服器就能很好的控制 rsync
、 git
、 svn
等。
$ vim http_server.php
使用Swoole的API,構建HTTP伺服器需要4個步驟
- 建立Server物件
- 設定執行時引數
- 註冊事件回撥函式
- 啟動伺服器
<?php // 建立伺服器物件 $addr = "0.0.0.0";//swoole主機埠 $port = 9501; //swoole主機埠 $svr = new swoole_http_server($addr, $port); // 設定和執行時引數 $cfg = []; $cfg["woker_num"] = 1; $svr->set($cfg); // 註冊事件回撥函式,此處是監聽request請求。 $svr->on("request", function(swoole_http_request $rq, swoole_http_response $rp){ var_dump($rq); }); // 啟動伺服器 $svr->start();
-
echo
、var_dump
、print_r
的內容是在伺服器中輸出的 - 瀏覽器中輸出需要使用
$rp->end(string $contents)
,end()
方法只能呼叫一次。 - 如果需要多次先客戶端傳送訊息可使用
$rp->write(string $content)
方法
<?php //建立HTTP伺服器 $addr = "0.0.0.0"; $port = 9501; $srv = new swoole_http_server($addr, $port); //設定HTTP伺服器引數 $cfg = []; $cfg["worker_num"] = 4;//設定工作程序數量 $cfg["daemonize"] = 0;//守護程序化,程式轉入後臺。 $srv->set($cfg); $srv->on("request", function(swoole_http_request $rq, swoole_http_response $rp) use($srv){ $rp->write("hello"); $rp->end(); end(); }); //啟動服務 $srv->start();
swoole_http_request swoole_http_response
由於 swoole_http_server
是基於 swoole_server
的,所以 swoole_server
下的方法在 swoole_http_server
中都可以使用,只是 swoole_http_server
只能被客戶端喚起。簡單來說, swoole_http_server
是基於 swoole_server
加上HTTP協議,再加上 request
和 response
類庫去實現請求資料和獲取資料。與PHP-FPM不同的是,Web伺服器收到請求後會傳遞給Swoole的HTTP伺服器,直接返回請求。

swoole_http_server
使用PHP-CLI執行指令碼
$ php http_server.php
使用CURL向HTTP伺服器傳送請求測試
$ curl 127.0.0.1:9501
由於Swoole的 swoole_http_server
對HTTP協議支援的並不完整,建議僅僅作為應用伺服器,並在前端增加NGINX作為反向代理。
設定Nginx反向代理 127.0.0.1:9501
$ vim /usr/local/nginx/conf/nginx.conf
http { includemime.types; default_typeapplication/octet-stream; #log_formatmain'$remote_addr - $remote_user [$time_local] "$request" ' #'$status $body_bytes_sent "$http_referer" ' #'"$http_user_agent" "$http_x_forwarded_for"'; #access_loglogs/access.logmain; sendfileon; #tcp_nopushon; #keepalive_timeout0; keepalive_timeout65; #gzipon; upstream swoole{ server 127.0.0.1:9501; keepalive 4; } server { listen80; server_namewww.swoole.com; #charset koi8-r; #access_loglogs/host.access.logmain; location / { proxy_pass http://swoole; proxy_set_header Connection ""; proxy_http_version 1.1; roothtml; indexindex.html index.htm; } #error_page404/404.html; # redirect server error pages to the static page /50x.html error_page500 502 503 504/50x.html; location = /50x.html { roothtml; } }
$cfg = []; $cfg["enable_static_handler"] =true; $cfg["document_root"] = "/test"; $svr->set($cfg);
設定 enable_static_handle
為 true
後,底層收到HTTP請求會像判斷 document_root
路徑下是否存在目標檔案,若存在則會直接傳送檔案給客戶端,不再觸發 onRequest
回撥。
處理請求
$ vim http_server.php
<?php $addr = "0.0.0.0"; $port = 9501; $svr = new swoole_http_server($addr, $port); $svr->on("request", function(swoole_http_request $rq, swoole_http_response $rp){ //處理動態請求 $path_info = $rq->server["path_info"]; $file = __DIR__.$path_info; echo "\nfile:{$file}"; if(is_file($file) && file_exists($file)){ $ext = pathinfo($path_info, PATHINFO_EXTENSION); echo "\next:{$ext}"; if($ext == "php"){ ob_start(); include($file); $contents = ob_get_contents(); ob_end_clean(); }else{ $contents = file_get_contents($file); } echo "\ncontents:{$contents}"; $rp->end($contents); }else{ $rp->status(404); $rp->end("404 not found"); } }); $svr->start();
建立靜態檔案
$ vim index.html index.html
測試靜態檔案
$ curl 127.0.0.1:9501/index.html
觀察http_server日誌輸出
file:/home/jc/projects/swoole/chat/index.html ext:html contents:index.html
測試動態檔案
$ vim index.php <?php echo "index.php";
觀察http_server日誌輸出
file:/home/jc/projects/swoole/chat/index.php ext:php contents:index.php
獲取動態請求的引數
$ vim http_server.php
<?php $addr = "0.0.0.0"; $port = 9501; $svr = new swoole_http_server($addr, $port); $svr->on("request", function(swoole_http_request $rq, swoole_http_response $rp){ //獲取請求引數 $params = $rq->get; echo "\nparams:".json_encode($params); //處理動態請求 $path_info = $rq->server["path_info"]; $file = __DIR__.$path_info; echo "\nfile:{$file}"; if(is_file($file) && file_exists($file)){ $ext = pathinfo($path_info, PATHINFO_EXTENSION); echo "\next:{$ext}"; if($ext == "php"){ ob_start(); include($file); $contents = ob_get_contents(); ob_end_clean(); }else{ $contents = file_get_contents($file); } echo "\ncontents:{$contents}"; $rp->end($contents); }else{ $rp->status(404); $rp->end("404 not found"); } }); $svr->start();
測試帶引數的請求
$ curl 127.0.0.1:9501?k=v
觀察請求引數的輸出
params:{"k":"v"} file:/home/jc/projects/swoole/chat/index.html ext:html contents:index.html
跨域處理
//Access-Control-Allow-Origin 不能使用 *,這樣修改是不支援php版本低於7.0的。 //$rp->header('Access-Control-Allow-Origin', '*'); $rp->header('Access-Control-Allow-Origin', $rq->header['origin'] ?? ''); $rp->header('Access-Control-Allow-Methods', 'OPTIONS'); $rp->header('Access-Control-Allow-Headers', 'x-requested-with,session_id,Content-Type,token,Origin'); $rp->header('Access-Control-Max-Age', '86400'); $rp->header('Access-Control-Allow-Credentials', 'true'); if ($rq->server['request_method'] == 'OPTIONS') { $rp->status(200); $rp->end(); return; };
壓力測試
使用Apache Bench工具進行壓力測試可以發現, swoole_http_server
遠超過PHP-FPM、Golang自帶的HTTP伺服器、Node.js自帶的HTTP伺服器,效能接近Nginx的靜態檔案處理。
Swoole的http server與PHP-FPM的效能對比
安裝Apache的壓測工作ab
$ sudo apt install apache2-util
使用100個客戶端跑1000次,平均每個客戶端10個請求。
$ ab -c 100 -n 1000 127.0.0.1:9501/index.php Concurrency Level:100 Time taken for tests:0.480 seconds Complete requests:1000 Failed requests:0 Total transferred:156000 bytes HTML transferred:9000 bytes Requests per second:2084.98 [#/sec] (mean) Time per request:47.962 [ms] (mean) Time per request:0.480 [ms] (mean, across all concurrent requests) Transfer rate:317.63 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] medianmax Connect:013.0012 Processing:44410.04557 Waiting:44410.14557 Total:16457.84557 Percentage of the requests served within a certain time (ms) 50%45 66%49 75%51 80%52 90%54 95%55 98%55 99%56 100%57 (longest request)
觀察可以發現QPS可以達到 Requests per second: 2084.98 [#/sec] (mean)
。
HTTP SERVER 配置選項
swoole_server::set()
用於設定 swoole_server
執行時的各項引數化。
$cfg = []; // 處理請求的程序數量 $cfg["worker_num"] = 4; // 守護程序化 $cfg["daemonize"] = 1; // 設定工作程序的最大任務數量 $cfg["max_request"] = 0; $cfg["backlog"] = 128; $cfg["max_request"] = 50; $cfg["dispatch_mode"] = 1; $srv->set($cfg);
配置HTTP SERVER引數後測試併發
$ vim http_server.php
<?php //建立HTTP伺服器 $addr = "0.0.0.0"; $port = 9501; $srv = new swoole_http_server($addr, $port); //設定HTTP伺服器引數 $cfg = []; $cfg["worker_num"] = 4;//設定工作程序數量 $cfg["daemonize"] = 1;//守護程序化,程式轉入後臺。 $srv->set($cfg); $srv->on("request", function(swoole_http_request $rq, swoole_http_response $rp){ //獲取請求引數 $params = $rq->get; echo "\nparams:".json_encode($params); //處理動態請求 $path_info = $rq->server["path_info"]; $file = __DIR__.$path_info; echo "\nfile:{$file}"; if(is_file($file) && file_exists($file)){ $ext = pathinfo($path_info, PATHINFO_EXTENSION); echo "\next:{$ext}"; if($ext == "php"){ ob_start(); include($file); $contents = ob_get_contents(); ob_end_clean(); }else{ $contents = file_get_contents($file); } echo "\ncontents:{$contents}"; $rp->end($contents); }else{ $rp->status(404); $rp->end("404 not found"); } }); //啟動服務 $srv->start();
檢視程序
$ ps -ef|grep http_server.php root1622412070 22:41 ?00:00:00 php http_server.php root16225 162240 22:41 ?00:00:00 php http_server.php root16227 162250 22:41 ?00:00:00 php http_server.php root16228 162250 22:41 ?00:00:00 php http_server.php root16229 162250 22:41 ?00:00:00 php http_server.php root16230 162250 22:41 ?00:00:00 php http_server.php root1623324560 22:42 pts/000:00:00 grep --color=auto http_server.php
檢視後臺守護程序
$ ps axuf|grep http_server.php root166220.00.0215361044 pts/0S+22:460:00||\_ grep --color=auto http_server.php root162240.00.3 2690368104 ?Ssl22:410:00\_ php http_server.php root162250.00.3 1967568440 ?S22:410:00\_ php http_server.php root162270.00.6 195212 14524 ?S22:410:00\_ php http_server.php root162280.00.6 195212 14524 ?S22:410:00\_ php http_server.php root162290.00.6 195212 14524 ?S22:410:00\_ php http_server.php root162300.00.6 195212 14524 ?S22:410:00\_ php http_server.php $ ps auxf|grep http_server.php|wc -l 7
殺死後臺程序
$ kill -9 16224 $ kill -9 16225 $ kill -9 16227 $ kill -9 16228 $ kill -9 16229 $ kill -9 16230
壓測
$ ab -c 100 -n 1000 127.0.0.1:9501/index.php Server Software:swoole-http-server Server Hostname:127.0.0.1 Server Port:9501 Document Path:/index.php Document Length:9 bytes Concurrency Level:100 Time taken for tests:0.226 seconds Complete requests:1000 Failed requests:0 Total transferred:156000 bytes HTML transferred:9000 bytes Requests per second:4417.72 [#/sec] (mean) Time per request:22.636 [ms] (mean) Time per request:0.226 [ms] (mean, across all concurrent requests) Transfer rate:673.01 [Kbytes/sec] received Connection Times (ms) minmean[+/-sd] medianmax Connect:012.8011 Processing:4217.22049 Waiting:1217.22049 Total:5227.62056 Percentage of the requests served within a certain time (ms) 50%20 66%23 75%25 80%26 90%30 95%38 98%45 99%53 100%56 (longest request)
觀察可以發現QPC為 Requests per second: 4417.72 [#/sec] (mean)
。
效能優化
使用 swoole_http_server
服務後,若發現服務的請求耗時監控毛刺十分嚴重,介面耗時波動較大的情況,可以觀察下服務的響應包 response
的大小,若響應包超過1~2M甚至更大,則可判斷是由於包太多而且很大導致服務響應波動較大。
為什麼響應包惠導致相應的時間波動呢?主要有兩個方面的影響,第一是響應包太大導致Swoole之間程序通訊更加耗時並佔用更多資源。第二是響應包太大導致Swoole的Reactor執行緒發包更加耗時。