目标
- 了解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...
- FastCGI进程管理器自身初始化,并启动多个CGI解释器进程
php-cgi
并等待Web服务器的连接。 - 当客户端请求到达Web服务器时,FastCGI进程管理器选择并连接一个CGI解释器,Web服务器将CGI环境变量和标准输入发送到FastCGI子进程PHP-CGI。
- FastCGI子进程完成处理后将标准输出和错误信息,从同一连接返回给Web服务器。当FastCGI子进程关闭连接时请求便处理完毕。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web服务器中)的下一个连接。在CGI模式中,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请求报文的组成结构
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/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();
- 完整的HTTP协议请求会被解析并封装在
swoole_http_request
对象中 - 所有的HTTP协议响应会通过
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服务器,直接返回请求。
使用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 {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream swoole{
server 127.0.0.1:9501;
keepalive 4;
}
server {
listen 80;
server_name www.swoole.com;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://swoole;
proxy_set_header Connection "";
proxy_http_version 1.1;
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
$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)
min mean[+/-sd] median max
Connect: 0 1 3.0 0 12
Processing: 4 44 10.0 45 57
Waiting: 4 44 10.1 45 57
Total: 16 45 7.8 45 57
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
root 16224 1207 0 22:41 ? 00:00:00 php http_server.php
root 16225 16224 0 22:41 ? 00:00:00 php http_server.php
root 16227 16225 0 22:41 ? 00:00:00 php http_server.php
root 16228 16225 0 22:41 ? 00:00:00 php http_server.php
root 16229 16225 0 22:41 ? 00:00:00 php http_server.php
root 16230 16225 0 22:41 ? 00:00:00 php http_server.php
root 16233 2456 0 22:42 pts/0 00:00:00 grep --color=auto http_server.php
查看后台守护进程
$ ps axuf|grep http_server.php
root 16622 0.0 0.0 21536 1044 pts/0 S+ 22:46 0:00 | | \_ grep --color=auto http_server.php
root 16224 0.0 0.3 269036 8104 ? Ssl 22:41 0:00 \_ php http_server.php
root 16225 0.0 0.3 196756 8440 ? S 22:41 0:00 \_ php http_server.php
root 16227 0.0 0.6 195212 14524 ? S 22:41 0:00 \_ php http_server.php
root 16228 0.0 0.6 195212 14524 ? S 22:41 0:00 \_ php http_server.php
root 16229 0.0 0.6 195212 14524 ? S 22:41 0:00 \_ php http_server.php
root 16230 0.0 0.6 195212 14524 ? S 22:41 0: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)
min mean[+/-sd] median max
Connect: 0 1 2.8 0 11
Processing: 4 21 7.2 20 49
Waiting: 1 21 7.2 20 49
Total: 5 22 7.6 20 56
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线程发包更加耗时。
有疑问加站长微信联系(非本文作者)