Swoole HTTP Server

JunChow520 · · 2638 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

目标

  • 了解swoole的http_server的使用
  • 了解swoole的tcp服务开发
  • 实际项目中问题如粘包处理、代理热更新、用户验证等。
  • swoole与现有框架结合

风格

  • 偏基础重代码

环境

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...

  1. FastCGI进程管理器自身初始化,并启动多个CGI解释器进程php-cgi并等待Web服务器的连接。
  2. 当客户端请求到达Web服务器时,FastCGI进程管理器选择并连接一个CGI解释器,Web服务器将CGI环境变量和标准输入发送到FastCGI子进程PHP-CGI。
  3. 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请求报文的组成结构

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模式下很多rootshell是无法执行的,而使用Swoole服务器就能很好的控制rsyncgitsvn等。

$ vim http_server.php

使用Swoole的API,构建HTTP服务器需要4个步骤

  1. 创建Server对象
  2. 设置运行时参数
  3. 注册事件回调函数
  4. 启动服务器
<?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();
  • echovar_dumpprint_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协议,再加上requestresponse类库去实现请求数据和获取数据。与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 {
    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_handletrue后,底层收到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线程发包更加耗时。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:JunChow520

查看原文:Swoole HTTP Server

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2638 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传