用Swoole HTTP服务器运行Lumen项目的实现方法

LNMP虽是传统的Web应用架构组合,无奈NginX + PHP-FPM的搭配运行效率实在太低;而Swoole HTTP服务器具有NginX级别的性能,且本身嵌入在PHP中,完全可以替代NginX + PHP-FPM。于是一直在探索用Swoole HTTP服务器运行传统PHP应用的途径。这里主要介绍一下Swoole HTTP服务器运行Lumen项目的实现方法:

Swoole HTTP服务器

Swoole1.7.7增加了内置HTTP服务器的支持,使用了跟NginX一样的IO多路复用机制epoll,可以达到NginX级别的性能,并且只需几行代码即可写出一个异步非阻塞多进程的HTTP服务器(epoll属于同步非阻塞,这里说“异步非阻塞”主要是因为“多进程”,单个进程内部依然是同步非阻塞)。

开启一个Swoole HTTP服务器的面向对象编程代码样板大致如下(具体参考Swoole官方文档HttpServer):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

class Service {
public $app;
public $server;

public function __construct($host, $port) {
$this->server = new \swoole_http_server($host, $port);
}

public function start()
{
// $this->server->on('start', array($this, 'onStart'));
// $this->server->on('shutdown', array($this, 'onShutdown'));
// $this->server->on('workerStop', array($this, 'onWorkerStop'));
$this->server->on('workerStart', array($this, 'onWorkerStart'));
$this->server->on('request', array($this, 'onRequest'));
$this->server->start();
}

public function onWorkerStart($serv, $worker_id) {
// 应用初始化
$this->app = 1;
}

public function onRequest(\swoole_http_request $request,\swoole_http_response $response) {
$app = $this->app;
// 处理用户请求
$get = json_encode($request->get);
// 响应用户请求
$response->end("App is {$app}. Get {$get}");
}
}

$s = new Service("127.0.0.1", 9080);
$s->start();

直接用php命令执行以上代码,浏览器访问http://127.0.0.1:9080,会得到:

1
App is 1. Get null

浏览器访问http://127.0.0.1:9080/?user=guest,会得到:

1
App is 1. Get {"user":"guest"}

就是这么简单。

加载Lumen项目

Lumen项目的入口文件是项目路径下的public/index.php,里面只有简单的两行代码:

1
2
3
<?php
$app = require __DIR__.'/../bootstrap/app.php';
$app->run();

第一行代码就是加载整个Lumen框架代码,第二行代码则是处理请求,生成响应。

所以,在Swoole HTTP服务器中加载Lumen项目,也很简单,只需修改onWorkerStart方法:

1
2
3
4
5
<?php
public function onWorkerStart($serv, $worker_id) {
// 应用初始化
$this->app = require '/THE/FULL/PATH/TO/bootstrap/app.php';
}

处理用户请求

虽然可以加载Lumen框架,但是要怎样才能用Lumen框架来处理用户请求呢?

由于Lumen框架的解耦程度非常高,我们可以很轻松地将swoole_http_request对象$request,转换成Lumen框架熟悉的Illuminate\Http\Request对象,这样Lumen框架就能处理用户请求了。
这里实现一个parseRequest方法,接收swoole_http_request类参数,返回\Illuminate\Http\Request类结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
protected function parseRequest(\swoole_http_request $request)
{
$get = isset($request->get) ? $request->get : array();
$post = isset($request->post) ? $request->post : array();
$cookie = isset($request->cookie) ? $request->cookie : array();
$server = isset($request->server) ? $request->server : array();
$header = isset($request->header) ? $request->header : array();
$files = isset($request->files) ? $request->files : array();
$fastcgi = array();

$new_server = array();
foreach ($server as $key => $value) {
$new_server[strtoupper($key)] = $value;
}
foreach ($header as $key => $value) {
$new_server['HTTP_' . strtoupper($key)] = $value;
}

$content = $request->rawContent() ?: null;

$http_request = new \Illuminate\Http\Request($get, $post, $fastcgi, $cookie, $files, $new_server, $content);

return $http_request;
}

生成请求响应

Lumen框架处理用户请求,十分方便,只需调用应用的dispatch方法即可。不过dispatch方法返回结果一般是Symfony\Component\HttpFoundation\Response对象,需要做一些转化才能交由swoole_http_response对象给用户输出响应。
这里实现一个makeResponse方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
protected function parseRequest(\swoole_http_response $request, \Symfony\Component\HttpFoundation\Response $http_response)
{
// status
$response->status($http_response->getStatusCode());
// headers
foreach ($http_response->headers->allPreserveCase() as $name => $values) {
foreach ($values as $value) {
$response->header($name, $value);
}
}
// cookies
foreach ($http_response->headers->getCookies() as $cookie) {
$response->rawcookie(
$cookie->getName(),
$cookie->getValue(),
$cookie->getExpiresTime(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->isSecure(),
$cookie->isHttpOnly()
);
}
// content
$content = $http_response->getContent();
// send content
$response->end($content);
}

有了parseRequest方法和makeResponse方法,要实现以Swoole HTTP服务器运行Lumen项目来处理用户请求只要几行代码。onRequest方法这样修改:

1
2
3
4
5
6
7
8
9
<?php
public function onRequest(\swoole_http_request $request,\swoole_http_response $response) {
$app = $this->app;
// 处理用户请求
$http_request = $this->parseRequest($request);
$http_response = $app->dispatch($http_request);
// 响应用户请求
$this->makeResponse($response, $http_response);
}

最后

就这样,不需要修改Lumen项目原本任何代码,就能让其运行在Swoole HTTP服务器上。
当然,用户请求各种各样,有请求文件上传,有请求静态资源等等;响应类型也是各种各样,有Gzip压缩,有JSON格式,有设置Cookie等待。要实现一个健全的Swoole+Lumen网关服务,以上代码还须更多优化,具体可以参考Service.php

按照以上思路,要实现以Swoole HTTP服务器运行Laravel项目也不难,不过,为了效率选择Swoole,自然会为了效率选择Lumen而不是Laravel。

下一篇文章介绍如何在Lumen中使用异步MySQL客户端,减少IO阻塞。