不用三方包 给Laravel开启Swoole

Laravel Swoole

Swoole 是一款优秀的 PHP 扩展,利用其可以实现原生 PHP 很难做到的常驻服务和异步。正好我有个 Laravel 项目可以折腾,就研究了下。

Laravel 项目是基于 composer 的,所以我先帖下我的 composer.json 中的 require 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"require": {
"php": "^7.1.3",
"cybercog/laravel-love": "^5.1",
"dingo/api": "~v2.0.0-alpha2",
"doctrine/dbal": "^2.8",
"fideloper/proxy": "^4.0",
"guzzlehttp/guzzle": "^6.3",
"infyomlabs/adminlte-templates": "5.6.x-dev",
"infyomlabs/laravel-generator": "5.6.x-dev",
"jeroennoten/laravel-adminlte": "^1.23",
"laravel/framework": "5.6.*",
"laravel/tinker": "^1.0",
"laravelcollective/html": "^5.6.0",
"lshorz/luocaptcha": "^1.0",
"overtrue/laravel-lang": "v3.0.08",
"overtrue/laravel-wechat": "^4.0",
"predis/predis": "^1.1",
"spatie/laravel-permission": "^2.17",
"tymon/jwt-auth": "~1.0.0-rc.2",
"yajra/laravel-datatables-buttons": "^4.0",
"yajra/laravel-datatables-oracle": "^8.7"
}
}

如果我们要开启swoole,我们可选的包有这些:

但一般来说,项目中需要常驻容器的服务与每次均需重新构建的服务并不一样,所以我才剑走偏锋。

起步

我们需要将 public/index.php 替换成如下

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<?php

use Illuminate\Http\Request;
use Illuminate\Http\Response;

define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';

class Laravel
{
/**
* Illuminate\Foundation\Application
*
* @var \Illuminate\Foundation\Application
*/
public $app;


/**
* App\Http\Kernel
*
* @var \App\Http\Kernel
*/
public $kernel;

/**
* App\Http\Requests\Request
*
* @var \App\Http\Requests\Request
*/
public $request;

/**
* Illuminate\Http\JsonResponse
*
* @var \Illuminate\Http\JsonResponse
*/
public $response;

/**
* 构造
*
* @param \Illuminate\Foundation\Application $app
*/
public function __construct(\Illuminate\Foundation\Application $app)
{
$this->app = $app;
}

/**
* RUN
*
* @return void
*/
public function run()
{
\Swoole\Runtime::enableCoroutine(true);

$http = new swoole_http_server('127.0.0.1', '80');

$http->set([
'document_root' => public_path('/'),
'enable_static_handler' => true,
]);

$http->on('request', function ($req, $res) {
try {
$kernel = $this->app->make(Illuminate\Contracts\Http\Kernel::class);

$get = $req->get ?? [];
$post = $req->post ?? [];
$input = array_merge($get, $post);
$cookie = $req->cookie ?? [];
$files = $req->files ?? [];
$server = $req->server ?? [];

$request = Request::create($req->server['request_uri'], $req->server['request_method'], $input, $cookie, $files, $server);

if (isset($req->header['x-requested-with']) && $req->header['x-requested-with'] == "XMLHttpRequest") {
$request->headers->set('X-Requested-With', "XMLHttpRequest", true);
}
if (isset($req->header['accept']) && $req->header['accept']) {
$request->headers->set('Accept', $req->header['accept'], true);
}

$response = $kernel->handle($request);

$res->status($response->getStatusCode());

foreach ($response->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($values as $value) {
$res->header($name, $value, false);
}
}

foreach ($response->headers->getCookies() as $cookie) {
$res->header('Set-Cookie', $cookie->getName() . strstr($cookie, '='), false);
}
dump(time());

$res->end($response->getContent());
$this->app->forgetInstance('request');
} catch (\throwable $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getFile();
echo PHP_EOL;
echo $e->getLine();
echo PHP_EOL;
}
});
$http->start();
}
}

(new Laravel($app))->run();

运行时发现大多数页面均没有问题,只有几个用了 infyomlabs/laravel-generator 产生的列表页,AJAX拉取JSON时却返回了HTML。


排查

在有问题页面的 controller 代码中,找到如下

1
2
3
4
5
6
7
8
9
10
/**
* Display a listing of the Star.
*
* @param StarDataTable $starDataTable
* @return Response
*/
public function index(StarDataTable $starDataTable)
{
return $starDataTable->render('stars.index');
}

定位 StarDataTable::render() 到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Process dataTables needed render output.
*
* @param string $view
* @param array $data
* @param array $mergeData
* @return mixed
*/
public function render($view, $data = [], $mergeData = [])
{
if ($this->request()->ajax() && $this->request()->wantsJson()) {
return app()->call([$this, 'ajax']);
}
...
}

这是判断 $this->request() 是不是 XHR 请求,且 Accept 请求头声明了 application/json

$this->request() 实现如下

1
2
3
4
5
6
7
8
9
10

/**
* Get DataTables Request instance.
*
* @return \Yajra\DataTables\Utilities\Request
*/
public function request()
{
return $this->request ?: $this->request = resolve('datatables.request');
}

不难看出,如果第一次构建,会走到

1
$this->request = resolve('datatables.request');

而 resolve 的实现是啥?

1
2
3
4
5
6
7
8
9
10
11
12
if (! function_exists('resolve')) {
/**
* Resolve a service from the container.
*
* @param string $name
* @return mixed
*/
function resolve($name)
{
return app($name);
}
}

就是从容器中取出 datatables.request 的过程。

所以我们只需让每次请求结束,$app 容器忘掉 datatables.request 就好了


改进

增加遗忘 datatables.request

1
2
3
4
$res->end($response->getContent());
$this->app->forgetInstance('request');
$this->app->forgetInstance('datatables.request');
$this->app->forgetInstance(\Dingo\Api\Http\Middleware\Request::class);

完整最终版:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php

use Illuminate\Http\Request;
use Illuminate\Http\Response;

define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';

class Laravel
{
/**
* Illuminate\Foundation\Application
*
* @var \Illuminate\Foundation\Application
*/
public $app;


/**
* App\Http\Kernel
*
* @var \App\Http\Kernel
*/
public $kernel;

/**
* App\Http\Requests\Request
*
* @var \App\Http\Requests\Request
*/
public $request;

/**
* Illuminate\Http\JsonResponse
*
* @var \Illuminate\Http\JsonResponse
*/
public $response;

/**
* 构造
*
* @param \Illuminate\Foundation\Application $app
*/
public function __construct(\Illuminate\Foundation\Application $app)
{
$this->app = $app;
}

/**
* RUN
*
* @return void
*/
public function run()
{
\Swoole\Runtime::enableCoroutine(true);

$http = new swoole_http_server('127.0.0.1', '80');

$http->set([
'document_root' => public_path('/'),
'enable_static_handler' => true,
]);

$http->on('request', function ($req, $res) {
try {
$kernel = $this->app->make(Illuminate\Contracts\Http\Kernel::class);

$get = $req->get ?? [];
$post = $req->post ?? [];
$input = array_merge($get, $post);
$cookie = $req->cookie ?? [];
$files = $req->files ?? [];
$server = $req->server ?? [];

$request = Request::create($req->server['request_uri'], $req->server['request_method'], $input, $cookie, $files, $server);

if (isset($req->header['x-requested-with']) && $req->header['x-requested-with'] == "XMLHttpRequest") {
$request->headers->set('X-Requested-With', "XMLHttpRequest", true);
}
if (isset($req->header['accept']) && $req->header['accept']) {
$request->headers->set('Accept', $req->header['accept'], true);
}

$response = $kernel->handle($request);

$res->status($response->getStatusCode());

foreach ($response->headers->allPreserveCaseWithoutCookies() as $name => $values) {
foreach ($values as $value) {
$res->header($name, $value, false);
}
}

foreach ($response->headers->getCookies() as $cookie) {
$res->header('Set-Cookie', $cookie->getName() . strstr($cookie, '='), false);
}
dump(time());

$res->end($response->getContent());
$this->app->forgetInstance('request');
//$this->app->forgetInstance('session');
//$this->app->forgetInstance('session.store');
//$this->app->forgetInstance('cookie');
$this->app->forgetInstance('datatables.request');
$this->app->forgetInstance(\Dingo\Api\Http\Middleware\Request::class);
//$kernel->terminate($request, $response);
} catch (\throwable $e) {
echo $e->getMessage();
echo PHP_EOL;
echo $e->getFile();
echo PHP_EOL;
echo $e->getLine();
echo PHP_EOL;
}
});
$http->start();
}
}

(new Laravel($app))->run();


测试

比原生 laravel 确实快不少(这还有4句SQL查询)

测试截图


  • 注,此处给出的代码可以借鉴,但未经长期验证。且不同项目实际用到的包不同,需要在调试过程中 debug 容器中的服务提供者,和追踪代码来调优。