LaravelS 基于Swoole

LaravelS - 基于 Swoole 加速 Laravel/Lumen

LaravelS是一个胶水项目,用于快速集成SwooleLaravelLumen,然后赋予它们更好的性能、更多可能性。Github
https://learnku.com/articles/8050/laravels-accelerate-laravellumen-based-on-swoole-take-you-fly

特性

  • 内置Http/WebSocket服务器
  • 多端口混合协议
  • 协程
  • 自定义进程
  • 常驻内存
  • 异步的事件监听
  • 异步的任务队列
  • 毫秒级定时任务
  • 平滑Reload
  • 修改代码后自动Reload
  • 同时支持Laravel/Lumen,兼容主流版本
  • 简单,开箱即用

要求

依赖 说明
PHP >=5.5.9 推荐PHP7+
Swoole >=1.7.19 从2.0.12开始不再支持PHP5 推荐4.2.3+
Laravel/Lumen >= 5.1 推荐5.6+

安装

  1. 通过Composer安装(packagist)
1
composer require "hhxsv5/laravel-s:~3.3" -vvv
  1. 注册Service Provider(以下两步二选一)。
  • Laravel: 修改文件config/app.php,Laravel 5.5+支持包自动发现,你应该跳过这步
1
2
3
4
'providers' => [
//...
Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
],
  • Lumen: 修改文件bootstrap/app.php
1
$app->register(Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class);
  1. 发布配置和二进制文件

每次升级LaravelS后,需重新发布

1
2
3
php artisan laravels publish
# 配置文件: config/laravels.php
# 二进制文件: bin/laravels bin/fswatch

使用Lumen时的特别说明: 你不需要手动加载配置laravels.phpLaravelS底层已自动加载。

1
2
// 不必手动加载,但加载了也不会有问题
$app->configure('laravels');
  1. 修改配置config/laravels.php:监听的IP、端口等,请参考配置项。

运行

1
php bin/laravels {start|stop|restart|reload|info|help}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@caoxl laravels]# php bin/laravels info
_ _ _____
| | | |/ ____|
| | __ _ _ __ __ ___ _____| | (___
| | / _` | '__/ _` \ \ / / _ \ |\___ \
| |___| (_| | | | (_| |\ V / __/ |____) |
|______\__,_|_| \__,_| \_/ \___|_|_____/

Speed up your Laravel/Lumen
+---------------------------+---------+
| Component | Version |
+---------------------------+---------+
| PHP | 7.2.7 |
| Swoole | 4.2.7 |
| LaravelS | 3.3.10 |
| Laravel Framework [local] | 5.7.13 |
+---------------------------+---------+

部署

与Nginx配合使用(推荐)

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
gzip on;
gzip_min_length 1024;
gzip_comp_level 2;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
gzip_vary on;
gzip_disable "msie6";
upstream laravels {
# 通过 IP:Port 连接
server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
#server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
#server 192.168.1.2:5200 backup;
keepalive 16;
}
server {
listen 80;
# 别忘了绑Host哟
server_name laravels.com;
root /xxxpath/laravel-s-test/public;
access_log /yyypath/log/nginx/$server_name.access.log main;
autoindex off;
index index.html index.htm;
# Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
location / {
try_files $uri @laravels;
}
# 当请求PHP文件时直接响应404,防止暴露public/*.php
#location ~* \.php$ {
# return 404;
#}
location @laravels {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://laravels;
}
}

与Apache配合使用

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
LoadModule proxy_module /yyypath/modules/mod_deflate.so
<IfModule deflate_module>
SetOutputFilter DEFLATE
DeflateCompressionLevel 2
AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml
</IfModule>

<VirtualHost *:80>
# 别忘了绑Host哟
ServerName www.laravels.com
ServerAdmin hhxsv5@sina.com

DocumentRoot /xxxpath/laravel-s-test/public;
DirectoryIndex index.html index.htm
<Directory "/">
AllowOverride None
Require all granted
</Directory>

LoadModule proxy_module /yyypath/modules/mod_proxy.so
LoadModule proxy_module /yyypath/modules/mod_proxy_balancer.so
LoadModule proxy_module /yyypath/modules/mod_lbmethod_byrequests.so.so
LoadModule proxy_module /yyypath/modules/mod_proxy_http.so.so
LoadModule proxy_module /yyypath/modules/mod_slotmem_shm.so
LoadModule proxy_module /yyypath/modules/mod_rewrite.so

ProxyRequests Off
ProxyPreserveHost On
<Proxy balancer://laravels>
BalancerMember http://192.168.1.1:8011 loadfactor=7
#BalancerMember http://192.168.1.2:8011 loadfactor=3
#BalancerMember http://192.168.1.3:8011 loadfactor=1 status=+H
ProxySet lbmethod=byrequests
</Proxy>
#ProxyPass / balancer://laravels/
#ProxyPassReverse / balancer://laravels/

# Apache处理静态资源,LaravelS处理动态资源。
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ balancer://laravels/%{REQUEST_URI} [P,L]

ErrorLog ${APACHE_LOG_DIR}/www.laravels.com.error.log
CustomLog ${APACHE_LOG_DIR}/www.laravels.com.access.log combined
</VirtualHost>

启用WebSocket服务器

WebSocket服务器监听的IP和端口与Http服务器相同。

  1. 创建WebSocket Handler类,并实现接口WebSocketHandlerInterface。start时会自动实例化,不需要手动创建示例。
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
<?php

namespace App\Services;

use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;

// @see https://wiki.swoole.com/wiki/page/400.html

class WebSocketService implements WebSocketHandlerInterface
{
/**
* 声明没有参数的构造函数
* WebSocketService constructor.
*/
public function __construct()
{
}

public function onOpen(\swoole_websocket_server $server, \swoole_http_request $request)
{
// 在触发onOpen事件之前Laravel的生命周期已经完结,所以Laravel的Request是可读的,Session是可读写的
\Log::info('New WebSocket connection', [
$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])
]);
$server->push($request->fd, 'Welcome to LaravelS');
// throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}

public function onMessage(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
{
\Log::info('Received message', [
$frame->fd, $frame->data, $frame->opcode, $frame->finish
]);
$server->push($frame->fd, date('Y-m-d H:i:s'));
// throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}

public function onClose(\swoole_websocket_server $server, $fd, $reactorId)
{
// throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}
}
  1. 更改配置 config/laravels.php
1
2
3
4
5
6
7
8
9
10
11
12
// ...
'websocket' => [
'enable' => true, // 看清楚,这里是true
'handler' => \App\Services\WebSocketService::class,
],
'swoole' => [
//...
// dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/wiki/page/277.html
'dispatch_mode' => 2,
//...
],
// ...
  1. 使用swoole_table绑定FDUserId,可选的,Swoole Table示例
    也可以用其他全局存储服务,例如Redis/Memcached/MySQL,但需要注意多个Swoole Server实例时FD可能冲突。

  2. 与Nginx配合使用(推荐)

参考 WebSocket代理

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
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream laravels {
# 通过 IP:Port 连接
server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
#server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
#server 192.168.1.2:5200 backup;
keepalive 16;
}
server {
listen 80;
# 别忘了绑Host哟
server_name laravels.com;
root /xxxpath/laravel-s-test/public;
access_log /yyypath/log/nginx/$server_name.access.log main;
autoindex off;
index index.html index.htm;
# Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
location / {
try_files $uri @laravels;
}
# 当请求PHP文件时直接响应404,防止暴露public/*.php
#location ~* \.php$ {
# return 404;
#}
# Http和WebSocket共存,Nginx通过location区分
# !!! WebSocket连接时路径为/ws
# Javascript: var ws = new WebSocket("ws://laravels.com/ws");
location =/ws {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout:如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接;同时,Swoole的心跳设置也会影响连接的关闭
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://laravels;
}
location @laravels {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://laravels;
}
}
  1. 心跳配置
  • Swoole的心跳配置
1
2
3
4
5
6
7
8
// config/laravels.php
'swoole' => [
//...
// 表示每60秒遍历一次,一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭
'heartbeat_idle_time' => 600,
'heartbeat_check_interval' => 60,
//...
],
  • Nginx读取代理服务器超时的配置
1
2
# 如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接
proxy_read_timeout 60s;

监听事件

系统事件

通常,你可以在这些事件中重置或销毁一些全局或静态的变量,也可以修改当前的请求和响应。

  • laravels.received_requestswoole_http_request 转成 Illuminate\Http\Request后,在Laravel内核处理请求前。
1
2
3
4
5
// 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
$req->query->set('get_key', 'hhxsv5'); // 修改query string
$req->request->set('post_key', 'hhxsv5'); // 修改post body
});
  • laravels.generated_responseLaravel内核处理完请求后,将Illuminate\Http\Response转成swoole_http_response之前(下一步将响应给客户端)。
1
2
3
4
5
Event::listen('laravels.generated_response', function (
\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app
) {
$rsp->headers->set('header-key', 'hhxsv5'); // 修改header
});

完整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();

// 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
$req->query->set('get_key', 'hhxsv5'); // 修改query string
$req->request->set('post_key', 'hhxsv5'); // 修改post body
});

Event::listen('laravels.generated_response', function (
\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app
) {
$rsp->headers->set('header-key', 'hhxsv5'); // 修改header
});
}

自定义的异步事件

此特性依赖SwooleAsyncTask,必须先设置config/laravels.phpswoole.task_worker_num
异步事件的处理能力受Task进程数影响,需合理设置task_worker_num

  1. 创建事件类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Tasks;

use Hhxsv5\LaravelS\Swoole\Task\Event;

class TestEvent extends Event
{
private $data;

public function __construct($data)
{
$this->data = $data;
}

public function getData()
{
return $this->data;
}
}
  1. 创建监听器类
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
<?php

namespace App\Tasks;

use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Task\Event;
use Hhxsv5\LaravelS\Swoole\Task\Listener;

class TestListener1 extends Listener
{
/**
* TestListener1 constructor.
* 声明没有参数的构造函数
*/
public function __construct()
{
}

public function handle(Event $event)
{
\Log::info(__CLASS__ . ':handle start', [$event->getData()]);
sleep(2); // 模拟一些慢速的事件处理

// 监听器中也可以投递Task,但不支持Task的finish()回调
// 注意:
// 1.参数2需要传true
// 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
$ret = Task::deliver(new TestTask('task data'), true);
var_dump($ret);

// throw new \Exception('an exception');// handle时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}
}
  1. 绑定事件与监听器
1
2
3
4
5
6
7
8
9
10
11
// 在"config/laravels.php"中绑定事件与监听器,一个事件可以有多个监听器,多个监听器按顺序执行
[
// ...
'events' => [
\App\Tasks\TestEvent::class => [
\App\Tasks\TestListener::class,
//\App\Tasks\TestListener2::class,
],
],
// ...
];
  1. 触发事件
1
2
3
4
5
6
// 实例化TestEvent并通过fire触发,此操作是异步的,触发后立即返回,由Task进程继续处理监听器中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Event;

$success = Event::fire(new TestEvent('event data'));

var_dump($success);//判断是否触发成功

异步的任务队列

此特性依赖SwooleAsyncTask,必须先设置config/laravels.phpswoole.task_worker_num
异步事件的处理能力受Task进程数影响,需合理设置task_worker_num

  1. 创建任务类
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
<?php

namespace App\Tasks;

use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Support\Facades\Log;

class TestTask extends Task
{
private $data;
private $result;

public function __construct($data)
{
$this->data = $data;
}

// 处理任务的逻辑,运行在Task进程中,不能投递任务
public function handle()
{
\Log::info(__CLASS__ . ':handle start', [$this->data]);
sleep(2); // 模拟一些慢速的事件处理
// throw new \Exception('an exception');// handle时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
$this->result = 'the result of' . $this->data;
}

// 可选的,完成事件,任务处理完后的逻辑,运行在Worker进程中,可以投递任务
public function finish()
{
\Log::info(__CLASS__ . ':finish start', [$this->result]);
Task::deliver(new TestTask2('task2')); // 投递其他任务
}
}
  1. 投递任务
1
2
3
4
5
6
7
8
// 实例化TestTask并通过deliver投递,此操作是异步的,投递后立即返回,由Task进程继续处理TestTask中的handle逻辑

use Hhxsv5\LaravelS\Swoole\Task\Task;

$task = new TestTask('task data');
// $task->delay(3);// 延迟3秒投放任务
$ret = Task::deliver($task);
var_dump($ret);//判断是否投递成功

毫秒级定时任务

基于Swoole的毫秒定时器,封装的定时任务,取代LinuxCrontab

  1. 创建定时任务
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
<?php

namespace App\Jobs\Timer;

use App\Tasks\TestTask;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
use Hhxsv5\LaravelS\Swoole\Task\Task;

class TestCronJob extends CronJob
{
protected $i = 0;

// !!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):
// 一是重载对应的方法,二是注册定时任务时传入参数。

// --- 重载对应的方法来返回配置:开始
public function interval()
{
//return parent::interval(); // TODO: Change the autogenerated stub
return 1000; // 每秒运行一次
}

public function isImmediate()
{
//return parent::isImmediate(); // TODO: Change the autogenerated stub
return false; // 是否立即执行第一次,false则等待间隔时间后执行第一次
}
// --- 重载对应的方法来返回配置:结束

public function run()
{
\Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
// do something
$this->i++;
\Log::info(__METHOD__, 'end', $this->i, microtime(true));

if ($this->i >= 10) {
// 运行10次后
\Log::info(__METHOD__, ['stop', $this->i, microtime(true)]);
$this->stop(); // 终止此任务

// CronJob中也可以投递Task,但不支持Task的finish()回调。
// 注意:
// 1.参数2需传true
// 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
$ret = Task::deliver(new TestTask('task data'), true);

var_dump($ret);
}
// throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}
}
  1. 注册定时任务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在"config/laravels.php"注册定时任务类
[
// ...
'timer' => [
'enable' => true, // 启用Timer
'jobs' => [ // 注册的定时任务类列表
// 启用LaravelScheduleJob来执行`php artisan schedule:run`,每分钟一次,替代Linux Crontab
// \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
// 两种配置参数的方式:
// [\App\Jobs\Timer\TestCronJob::class, [1000, true]], // 注册时传入参数
\App\Jobs\Timer\TestCronJob::class, // 重载对应的方法来返回参数
],
],
// ...
];
  1. 注意在构建服务器集群时,会启动多个定时器,要确保只启动一个定期器,避免重复执行定时任务

修改代码后自动Reload

  • 基于inotify,仅支持Linux
  1. 安装inotify扩展
  2. 开启配置项
  3. 注意:inotify只有在Linux内修改文件才能收到文件变更事件,建议使用最新版Docker,Vagrant解决方案。
  • 基于fswatch,支持OS X、Linux、Windows。
  1. 安装fswatch
  2. 在项目根目录下运行命令。
1
2
// 监听当前目录
./bin/fswatch

CentOS下安装fawatch

1
2
3
4
5
// 这里注意 最新的fswatch-1.14.0 需要GCC4.9版本以上
wget https://github.com/emcrisostomo/fswatch/releases/download/1.13.0/fswatch-1.13.0.tar.gz
cd fswatch-1.13.0
./configure
make && make install

在项目中使用swoole_server实例

1
2
3
4
5
6
/**
* 如果启用WebSocket server,$swoole是`swoole_websocket_server`的实例,否则是是`\swoole_http_server`的实例
* @var \swoole_http_server|\swoole_websocket_server $swoole
*/
$swoole = app('swoole');
var_dump($swoole->stats());// 单例

使用swoole_table

  1. 定义swoole_table, 支持定义多个Table

Swoole启动之前会创建定义的所有Table。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
'swoole_tables' => [
// 场景:WebSocket中UserId与FD绑定
'ws' => [ // Key为Table名称,使用时会自动添加Table后缀,避免重名。这里定义名为wsTable的Table
'size' => 102400, //Table的最大行数
'column' => [
[
'name' => 'value',
'type' => \swoole_table::TYPE_INT,
'size' => 8,
],
],
],
// ...继续定义其他Table
],
// ...

2.访问swoole_table:所有的Table实例均绑定在swoole_server上,通过app('swoole')->xxxTable访问。

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
// 场景:WebSocket中UserId与FD绑定
public function onOpen(\swoole_websocket_server $server, \swoole_http_request $request)
{
// var_dump(app('swoole') === $server);// 同一实例
$userId = mt_rand(1000, 10000);
app('swoole')->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// 绑定uid到fd的映射
app('swoole')->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// 绑定fd到uid的映射
$server->push($request->fd, 'Welcome to LaravelS');
}
public function onMessage(\swoole_websocket_server $server, \swoole_websocket_frame $frame)
{
foreach (app('swoole')->wsTable as $key => $row) {
if (strpos($key, 'uid:') === 0 && $server->exist($row['value'])) {
$server->push($row['value'], 'Broadcast: ' . date('Y-m-d H:i:s'));// 广播
}
}
}
public function onClose(\swoole_websocket_server $server, $fd, $reactorId)
{
$uid = app('swoole')->wsTable->get('fd:' . $fd);
if ($uid !== false) {
app('swoole')->wsTable->del('uid:' . $uid['value']);// 解绑uid映射
}
app('swoole')->wsTable->del('fd:' . $fd);// 解绑fd映射
$server->push($fd, 'Goodbye');
}

多端口混合协议

更多的信息,请参考Swoole增加监听的端口与多端口混合协议

为了使我们的主服务器能支持除HTTPWebSocket外的更多协议,我们引入了Swoole的多端口混合协议特性,在LaravelS中称为Socket
现在,可以很方便地在Laravel上被构建TCP/UDP应用。

  1. 创建Socket处理类,继承Hhxsv5\LaravelS\Swoole\Socket\{TcpSocket|UdpSocket|Http|WebSocket}
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
<?php

namespace App\Sockets;

use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket;

class TestTcpSocket extends TcpSocket
{
public function onConnect(\swoole_server $server, $fd, $reactorId)
{
\Log::info('New TCP connection', [$fd]);
$server->send($fd, 'Welcome to LaravelS');
}

public function onReceive(\swoole_server $server, $fd, $reactorId, $data)
{
\Log::info('Received data', [$fd, $data]);
$server->send($fd, 'LaravelS:' . $data);
if ($data === "quit\r\n") {
$server->send($fd, 'LaravelS: bye' . PHP_EOL);
$server->close($fd);
}
}

public function onClose(\swoole_server $server, $fd, $reactorId)
{
\Log::info('Close TCP connection', [$fd]);
$server->send($fd, 'GoodBye');
}
}

这些连接和主服务器上的HTTP/WebSocket连接共享Worker进程,因此可以在这些事件操作中使用LaravelS
提供的异步任务投递swoole_tableLaravel提供的组件如DBEloquent等。同时,
如果需要使用该协议端口的swoole_server_port对象,只需要像如下代码一样访问Socket类的成员swoolePort即可。

1
2
3
4
public function onReceive(\swoole_server $server, $fd, $reactorId, $data)
{
$port = $this->swoolePort; //获得`swoole_server_port`对象
}
  1. 注册套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 修改文件 config/laravels.php
// ...
'sockets' => [
[
'host' => '127.0.0.1',
'port' => 5291,
'type' => SWOOLE_SOCK_TCP,// 支持的嵌套字类型:https://wiki.swoole.com/wiki/page/16.html#entry_h2_0
'settings' => [// Swoole可用的配置项:https://wiki.swoole.com/wiki/page/526.html
'open_eof_check' => true,
'package_eof' => "\r\n",
],
'handler' => \App\Sockets\TestTcpSocket::class,
],
],

关于心跳配置,只能设置在主服务器上,不能配置在套接字上,但套接字继承主服务器的心跳配置。

对于TCP协议,dispatch_mode选项设为1/3时,底层会屏蔽onConnect/onClose事件,原因是这两种模式下无法保证onConnect/onClose/onReceive的顺序。
如果需要用到这两个事件,请将dispatch_mode改为2/4/5,参考。

1
2
3
4
5
'swoole' => [
//...
'dispatch_mode' => 2,
//...
];
  1. 测试
  • TCP: telnet 127.0.0.1 5291
  • UDP: Linux下 echo "Hello LaravelS" > /dev/udp/127.0.0.1/5292
  1. 其他协议的注册示例
  • UDP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => 5292,
    'type' => SWOOLE_SOCK_UDP,
    'settings' => [
    'open_eof_check' => true,
    'package_eof' => "\r\n",
    ],
    'handler' => \App\Sockets\TestUdpSocket::class,
    ],
    ],
  • HTTP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => 5293,
    'type' => SWOOLE_SOCK_TCP,
    'settings' => [
    'open_http_protocol' => true,
    ],
    'handler' => \App\Sockets\TestHttp::class,
    ],
    ],
  • WebSocket

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => 5294,
    'type' => SWOOLE_SOCK_TCP,
    'settings' => [
    'open_http_protocol' => true,
    'open_websocket_protocol' => true,
    ],
    'handler' => \App\Sockets\TestWebSocket::class,
    ],
    ],

协程

Swoole - 协程(Coroutine)

  • 警告:Laravel/Lumen中存在大量单例和静态属性,在协程下是不安全的,不建议打开协程。
  • 启用协程,默认是关闭的
1
2
3
4
5
6
7
8
// 修改文件 `config/laravels.php`
[
//...
'swoole' => [
//...
'enable_coroutine' => true
],
]
1
2
3
4
5
// 修改文件 `config/laravels.php`
[
//...
'enable_coroutine_runtime' => true
]

自定义进程

支持开发者创建一些特殊的工作进程,用于监控、上报或者其他特殊的任务,参考addProcess

  1. 创建Process类,实现CustomProcessInterface接口
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
<?php

namespace App\Processes;

use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface;
use App\Tasks\TestTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;

class TestProcess implements CustomProcessInterface
{
public static function getName()
{
// 进程名称
return 'test';
}

public static function isRedirectStdinStdout()
{
// 是否重定向输入输出
return false;
}

public static function getPipeType()
{
// 管道类型:0:不创建管道,1:创建SOCK_STREAM类型管道,2:创建SOCK_DGRAM类型管道
return 0;
}

public static function callback(\swoole_server $swoole)
{
// 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
\Log::info(__METHOD__, [posix_getpid(), $swoole->stats()]);
while (true) {
\Log::info('Do something');
sleep(1);
// 自定义进程中也可以投递Task,但不支持Task的finish()回调。
// 注意:
// 1.参数2需传true
// 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
$ret = Task::deliver(new TestTask('task data'), true);
var_dump($ret);

// 上层会捕获callback中抛出的异常,并记录到Swoole日志,如果异常数达到10次,此进程会退出,Manager进程会重新创建进程,所以建议开发者自行try/catch捕获,避免创建进程过于频繁。
// throw new \Exception('an exception');
}
}
}
  1. 注册TestProcess
1
2
3
4
5
// 修改文件 config/laravels.php
// ...
'processes' => [
\App\Processes\TestProcess::class,
],
  1. 注意: TestProcess::callback()方法不能退出, 如果退出次数达到10次,Manager进程将会重新创建进程

其他特性

配置Swoole的事件回调函数

支持的事件列表:

事件 需实现的接口 发生时机
WorkerStart Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface 发生在Worker进程/Task进程启动时,并且已经完成Laravel初始化
  1. 创建事件处理类,实现相应的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Events;

use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;

class WorkerStartEvent implements WorkerStartInterface
{
public function __construct()
{
}

public function handle(\swoole_http_server $server, $workerId)
{
// 例如:初始化一个连接池对象,绑定到Swoole Server对象上,其他地方可通过app('swoole')->connectionPool访问
if (!isset($server->connectionPool)) {
$server->connectionPool = new ConnectionPool();
}
}
}
  1. 配置
1
2
3
4
// 修改文件 config/laravels.php
'event_handlers' => [
'WorkerStart' => \App\Events\WorkerStartEvent::class,
],

注意事项

单例问题

  • 传统FPM下,单例模式的对象的生命周期仅在每次请求中请求开始=>实例化单例=>请求结束后=>单例对象资源回收

  • Swoole Server下,所有单例对象会常驻于内存,这个时候单例对象的生命周期与FPM不同,请求开始=>实例化单例=>请求结束=>单例对象依旧保留,需要开发者自己维护单例的状态。

  • 常见的解决方案:

    1. 用一个中间件重置单例对象的状态。
    2. 如果是以ServiceProvider注册的单例对象,可添加该ServiceProviderlaravels.phpregister_providers中,这样每次请求会重新注册该ServiceProvider,重新实例化单例对象。

常见问题

  • 应通过Illuminate\Http\Request对象来获取请求信息,$_ENV是可读取的,$_SERVER是部分可读的,
    不能使用$_GET$_POST$_FILES$_COOKIE$_REQUEST$_SESSION$GLOBALS
1
2
3
4
5
6
7
8
9
10
use Illuminate\Http\Request;
public function test_form(Request $request)
{
$name = $request->input('name');
$all = $request->all();
$sessionId = $request->cookie('sessionId');
$photo = $request->file('photo');
// 调用getContent()来获取原始的POST body,而不能用file_get_contents('php://input')
$rawContent = $request->getContent();
}
  • 推荐通过返回Illuminate\Http\Response对象来响应请求,
    兼容echovar_dump()print_r()
    不能使用函数像 dd()exit()die()header()setcookie()http_response_code()
1
2
3
4
5
6
7
8
public function json()
{
return response()->json([
'time' => time()
])
->header('header1', 'value1')
->withCookie('c1', 'v1');
}
  • 各种单例的连接将被常驻内存,建议开启持久连接

    1. 数据库连接,连接断开后会自动重连

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // config/database.php
      'connections' => [
      'my_conn' => [
      'driver' => 'mysql',
      'host' => env('DB_MY_CONN_HOST', 'localhost'),
      'port' => env('DB_MY_CONN_PORT', 3306),
      'database' => env('DB_MY_CONN_DATABASE', 'forge'),
      'username' => env('DB_MY_CONN_USERNAME', 'forge'),
      'password' => env('DB_MY_CONN_PASSWORD', ''),
      'charset' => 'utf8mb4',
      'collation' => 'utf8mb4_unicode_ci',
      'prefix' => '',
      'strict' => false,
      'options' => [
      // 开启持久连接
      \PDO::ATTR_PERSISTENT => true,
      ],
      ],
      //...
    2. Redis连接,连接断开后不会立即自动重连,会抛出一个关于连接断开的异常,下次会自动重连。需确保每次操作Redis前正确的SELECT DB

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      // config/database.php
      'redis' => [

      'client' => 'predis',

      'default' => [
      'host' => env('REDIS_HOST', '127.0.0.1'),
      'password' => env('REDIS_PASSWORD', null),
      'port' => env('REDIS_PORT', 6379),
      'database' => env('REDIS_DB', 0),
      'persistent' => true, // 开启持久连接
      ],

      'cache' => [
      'host' => env('REDIS_HOST', '127.0.0.1'),
      'password' => env('REDIS_PASSWORD', null),
      'port' => env('REDIS_PORT', 6379),
      'database' => env('REDIS_CACHE_DB', 1),
      ],

      ],
  • 你声明的全局、静态变量必须手动清理或重置。

  • 无限追加元素到静态或全局变量中,将导致内存爆满。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 某类
class Test
{
public static $array = [];
public static $string = '';
}

// 某控制器
public function test(Request $req)
{
// 内存爆满
Test::$array[] = $req->input('param1');
Test::$string .= $req->input('param2');
}

Powered by Hexo and Hexo-theme-hiker

Copyright © 2017 - 2023 Keep It Simple And Stupid All Rights Reserved.

访客数 : | 访问量 :