LaravelS

LaravelS是一个胶水项目,用于快速集成SwooleLaravelLumen,然后赋予它们更好的性能、更多可能性。Github

特性

要求

依赖 说明
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)。有可能找不到3.0版本,解决方案移步#81

composer require "hhxsv5/laravel-s:~3.5.0" -vvv
# 确保你的composer.lock文件是在版本控制中

2.注册Service Provider(以下两步二选一)。

  • Laravel: 修改文件config/app.phpLaravel 5.5+支持包自动发现,你应该跳过这步

    'providers' => [
    //...
    Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
    ],
  • Lumen: 修改文件bootstrap/app.php

    $app->register(Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class);

3.发布配置和二进制文件。

每次升级LaravelS后,需重新publish;点击Release去了解各个版本的变更记录。

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

4.修改配置config/laravels.php:监听的IP、端口等,请参考配置项

运行

php bin/laravels {start|stop|restart|reload|info|help}

在运行之前,请先仔细阅读:注意事项(非常重要)。

命令 说明
start 启动LaravelS,展示已启动的进程列表 "ps -ef|grep laravels"。支持选项 "-d|--daemonize" 以守护进程的方式运行,此选项将覆盖laravels.phpswoole.daemonize设置;支持选项 "-e|--env" 用来指定运行的环境,如--env=testing将会优先使用配置文件.env.testing,这个特性要求Laravel 5.2+
stop 停止LaravelS
restart 重启LaravelS,支持选项 "-d|--daemonize" 和 "-e|--env"
reload 平滑重启所有Task/Worker/Timer进程(这些进程内包含了你的业务代码),并触发自定义进程的onReload方法,不会重启Master/Manger进程;修改config/laravels.php后,你只能调用restart来实现重启
info 显示组件的版本信息
help 显示帮助信息

部署

建议通过Supervisord监管主进程,前提是不能加-d选项并且设置swoole.daemonizefalse

[program:laravel-s-test]
command=/user/local/bin/php /opt/www/laravel-s-test/bin/laravels start -i
numprocs=
autostart=true
autorestart=true
startretries=
user=www-data
redirect_stderr=true
stdout_logfile=/opt/www/laravel-s-test/storage/logs/supervisord-stdout.log

与Nginx配合使用(推荐)

示例

gzip on;
gzip_min_length ;
gzip_comp_level ;
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 swoole {
# 通过 IP:Port 连接
server 127.0.0.1: weight= max_fails= fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight= max_fails= fail_timeout=30s;
#server 192.168.1.1: weight= max_fails= fail_timeout=30s;
#server 192.168.1.2: backup;
keepalive ;
}
server {
listen ;
# 别忘了绑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://swoole;
}
}

与Apache配合使用

 LoadModule proxy_module /yyypath/modules/mod_deflate.so
<IfModule deflate_module>
SetOutputFilter DEFLATE
DeflateCompressionLevel
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 *:>
# 别忘了绑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:5200 loadfactor=7
#BalancerMember http://192.168.1.2:5200 loadfactor=3
#BalancerMember http://192.168.1.3:5200 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时会自动实例化,不需要手动创建实例。

 namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
* @see https://wiki.swoole.com/wiki/page/400.html
*/
class WebSocketService implements WebSocketHandlerInterface
{
// 声明没有参数的构造函数
public function __construct()
{
}
public function onOpen(Server $server, Request $request)
{
// 在触发onOpen事件之前,建立WebSocket的HTTP请求已经经过了Laravel的路由,
// 所以Laravel的Request、Auth等信息是可读的,Session是可读写的,但仅限在onOpen事件中。
// \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(Server $server, 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(Server $server, $fd, $reactorId)
{
// throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
}
}

2.更改配置config/laravels.php

 // ...
'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' => ,
//...
],
// ...

3.使用SwooleTable绑定FD与UserId,可选的,Swoole Table示例。也可以用其他全局存储服务,例如Redis/Memcached/MySQL,但需要注意多个Swoole Server实例时FD可能冲突。

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

参考 WebSocket代理

 map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream swoole {
# 通过 IP:Port 连接
server 127.0.0.1: weight= max_fails= fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight= max_fails= fail_timeout=30s;
#server 192.168.1.1: weight= max_fails= fail_timeout=30s;
#server 192.168.1.2: backup;
keepalive ;
}
server {
listen ;
# 别忘了绑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://swoole;
}
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://swoole;
}
}

5.心跳配置

  • Swoole的心跳配置

     // config/laravels.php
    'swoole' => [
    //...
    // 表示每60秒遍历一次,一个连接如果600秒内未向服务器发送任何数据,此连接将被强制关闭
    'heartbeat_idle_time' => ,
    'heartbeat_check_interval' => ,
    //...
    ],
  • Nginx读取代理服务器超时的配置

    # 如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接
    proxy_read_timeout 60s;

监听事件

系统事件

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

  • laravels.received_request 将Swoole\Http\Request转成Illuminate\Http\Request后,在Laravel内核处理请求前。

     // 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
    // 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
    $events->listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
    $req->query->set('get_key', 'hhxsv5');// 修改querystring
    $req->request->set('post_key', 'hhxsv5'); // 修改post body
    });
  • laravels.generated_response 在Laravel内核处理完请求后,将Illuminate\Http\Response转成Swoole\Http\Response之前(下一步将响应给客户端)。

     // 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
    // 如果变量$events不存在,你也可以通过Facade调用\Event::listen()。
    $events->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.创建事件类。

 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;
}
}

2.创建监听器类。

 use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Task\Event;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
class TestListener1 extends Listener
{
// 声明没有参数的构造函数
public function __construct()
{
}
public function handle(Event $event)
{
\Log::info(__CLASS__ . ':handle start', [$event->getData()]);
sleep();// 模拟一些慢速的事件处理
// 监听器中也可以投递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捕获处理
}
}

3.绑定事件与监听器。

 // 在"config/laravels.php"中绑定事件与监听器,一个事件可以有多个监听器,多个监听器按顺序执行
[
// ...
'events' => [
\App\Tasks\TestEvent::class => [
\App\Tasks\TestListener1::class,
//\App\Tasks\TestListener2::class,
],
],
// ...
];

4.触发事件。

 // 实例化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.创建任务类。

 use Hhxsv5\LaravelS\Swoole\Task\Task;
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();// 模拟一些慢速的事件处理
// 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')); // 投递其他任务
}
}

2.投递任务。

 // 实例化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.创建定时任务类。

 namespace App\Jobs\Timer;
use App\Tasks\TestTask;
use Swoole\Coroutine;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
class TestCronJob extends CronJob
{
protected $i = ;
// !!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):一是重载对应的方法,二是注册定时任务时传入参数。
// --- 重载对应的方法来返回配置:开始
public function interval()
{
return ;// 每1秒运行一次
}
public function isImmediate()
{
return false;// 是否立即执行第一次,false则等待间隔时间后执行第一次
}
// --- 重载对应的方法来返回配置:结束
public function run()
{
\Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
// do something
// sleep(1); // Swoole < 2.1
Coroutine::sleep(); // Swoole>=2.1 run()方法已自动创建了协程。
$this->i++;
\Log::info(__METHOD__, ['end', $this->i, microtime(true)]); if ($this->i >= ) { // 运行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捕获处理
}
}

2.注册定时任务类。

 // 在"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, // 重载对应的方法来返回参数
],
'max_wait_time' => , // Reload时最大等待时间
],
// ...
];

3.注意在构建服务器集群时,会启动多个定时器,要确保只启动一个定期器,避免重复执行定时任务。

4.LaravelS v3.4.0开始支持热重启[Reload]定时器进程,LaravelS 在收到SIGUSR1信号后会等待max_wait_time(默认5)秒再结束进程,然后Manager进程会重新拉起定时器进程。

修改代码后自动Reload

  • 基于inotify,仅支持Linux。

    1.安装inotify扩展。

    2.开启配置项

    3.注意:inotify只有在Linux内修改文件才能收到文件变更事件,建议使用最新版Docker,Vagrant解决方案

  • 基于fswatch,支持OS X、Linux、Windows。

    1.安装fswatch

    2.在项目根目录下运行命令。

    # 监听当前目录
    ./bin/fswatch
    # 监听app目录
    ./bin/fswatch ./app
  • 基于inotifywait,仅支持Linux。

    1.安装inotify-tools

    2.在项目根目录下运行命令。

    # 监听当前目录
    ./bin/inotify
    # 监听app目录
    ./bin/inotify ./app

在你的项目中使用SwooleServer实例

/**
* 如果启用WebSocket server,$swoole是`Swoole\WebSocket\Server`的实例,否则是是`Swoole\Http\Server`的实例
* @var \Swoole\WebSocket\Server|\Swoole\Http\Server $swoole
*/
$swoole = app('swoole');
var_dump($swoole->stats());// 单例

使用SwooleTable

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

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

 // 在"config/laravels.php"配置
[
// ...
'swoole_tables' => [
// 场景:WebSocket中UserId与FD绑定
'ws' => [// Key为Table名称,使用时会自动添加Table后缀,避免重名。这里定义名为wsTable的Table
'size' => ,//Table的最大行数
'column' => [// Table的列定义
['name' => 'value', 'type' => \Swoole\Table::TYPE_INT, 'size' => ],
],
],
//...继续定义其他Table
],
// ...
];

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

 namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebsocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
class WebSocketService implements WebSocketHandlerInterface
{
/**@var \Swoole\Table $wsTable */
private $wsTable;
public function __construct()
{
$this->wsTable = app('swoole')->wsTable;
}
// 场景:WebSocket中UserId与FD绑定
public function onOpen(Server $server, Request $request)
{
// var_dump(app('swoole') === $server);// 同一实例
/**
* 获取当前登录的用户
* 此特性要求建立WebSocket连接的路径要经过Authenticate之类的中间件。
* 例如:
* 浏览器端:var ws = new WebSocket("ws://127.0.0.1:5200/ws");
* 那么Laravel中/ws路由就需要加上类似Authenticate的中间件。
*/
// $user = Auth::user();
// $userId = $user ? $user->id : 0; // 0 表示未登录的访客用户
$userId = mt_rand(, );
$this->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// 绑定uid到fd的映射
$this->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// 绑定fd到uid的映射
$server->push($request->fd, "Welcome to LaravelS #{$request->fd}");
}
public function onMessage(Server $server, Frame $frame)
{
// 广播
foreach ($this->wsTable as $key => $row) {
if (strpos($key, 'uid:') === && $server->isEstablished($row['value'])) {
$content = sprintf('Broadcast: new message "%s" from #%d', $frame->data, $frame->fd);
$server->push($row['value'], $content);
}
}
}
public function onClose(Server $server, $fd, $reactorId)
{
$uid = $this->wsTable->get('fd:' . $fd);
if ($uid !== false) {
$this->wsTable->del('uid:' . $uid['value']); // 解绑uid映射
}
$this->wsTable->del('fd:' . $fd);// 解绑fd映射
$server->push($fd, "Goodbye #{$fd}");
}
}

多端口混合协议

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

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

  1. 创建Socket处理类,继承Hhxsv5\LaravelS\Swoole\Socket\{TcpSocket|UdpSocket|Http|WebSocket}

     namespace App\Sockets;
    use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket;
    use Swoole\Server;
    class TestTcpSocket extends TcpSocket
    {
    public function onConnect(Server $server, $fd, $reactorId)
    {
    \Log::info('New TCP connection', [$fd]);
    $server->send($fd, 'Welcome to LaravelS.');
    }
    public function onReceive(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(Server $server, $fd, $reactorId)
    {
    \Log::info('Close TCP connection', [$fd]);
    $server->send($fd, 'Goodbye');
    }
    }

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

    public function onReceive(Server $server, $fd, $reactorId, $data)
    {
    $port = $this->swoolePort; //获得`Swoole\Server\Port`对象
    }
  2. 注册套接字。

     // 修改文件 config/laravels.php
    // ...
    'sockets' => [
    [
    'host' => '127.0.0.1',
    'port' => ,
    '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参考

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

    • UDP
    'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => ,
    'type' => SWOOLE_SOCK_UDP,
    'settings' => [
    'open_eof_check' => true,
    'package_eof' => "\r\n",
    ],
    'handler' => \App\Sockets\TestUdpSocket::class,
    ],
    ],
    • Http
     'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => ,
    'type' => SWOOLE_SOCK_TCP,
    'settings' => [
    'open_http_protocol' => true,
    ],
    'handler' => \App\Sockets\TestHttp::class,
    ],
    ],
    • WebSocket:主服务器必须开启WebSocket,即需要将websocket.enable置为true
     'sockets' => [
    [
    'host' => '0.0.0.0',
    'port' => ,
    'type' => SWOOLE_SOCK_TCP,
    'settings' => [
    'open_http_protocol' => true,
    'open_websocket_protocol' => true,
    ],
    'handler' => \App\Sockets\TestWebSocket::class,
    ],
    ],
    协程

Swoole原始文档

  • 警告:协程下代码执行顺序是乱序的,请求级的数据应该以协程ID隔离,但Laravel/Lumen中存在很多单例、静态属性,不同请求间的数据会相互影响,这是不安全的。比如数据库连接就是单例,同一个数据库连接共享同一个PDO资源,这在同步阻塞模式下是没问题的,但在异步协程下是不行的,每次查询需要创建不同的连接,维护不同的IO状态,这就需要用到连接池。所以不要打开协程,仅自定义进程中可使用协程。
  • 启用协程,默认是关闭的。

     // 修改文件 `config/laravels.php`
    [
    //...
    'swoole' => [
    //...
    'enable_coroutine' => true
    ],
    ]
  • 协程客户端:需Swoole>=2.0
  • 运行时协程:需Swoole>=4.1.0,同时启用下面的配置。

    // 修改文件 `config/laravels.php`
    [
    //...
    'enable_coroutine_runtime' => true
    ]

自定义进程

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

  1. 创建Proccess类,实现CustomProcessInterface接口。

     namespace App\Processes;
    use App\Tasks\TestTask;
    use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface;
    use Hhxsv5\LaravelS\Swoole\Task\Task;
    use Swoole\Coroutine;
    use Swoole\Http\Server;
    use Swoole\Process;
    class TestProcess implements CustomProcessInterface
    {
    public static function getName()
    {
    // 进程名称
    return 'test';
    }
    public static function callback(Server $swoole, Process $process)
    {
    // 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
    \Log::info(__METHOD__, [posix_getpid(), $swoole->stats()]);
    while (true) {
    \Log::info('Do something');
    // sleep(1); // Swoole < 2.1
    Coroutine::sleep(); // Swoole>=2.1 callback()方法已自动创建了协程。
    // 自定义进程中也可以投递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');
    }
    }
    // 要求:LaravelS >= v3.4.0 并且 callback() 必须是异步非阻塞程序。
    public static function onReload(Server $swoole, Process $process)
    {
    // Stop the process...
    // Then end process
    $process->exit();
    }
    }
  2. 注册TestProcess。

     // 修改文件 config/laravels.php
    // ...
    'processes' => [
    [
    'class' => \App\Processes\TestProcess::class,
    'redirect' => false, // 是否重定向输入输出
    'pipe' => // 管道类型:0不创建管道,1创建SOCK_STREAM类型管道,2创建SOCK_DGRAM类型管道
    'enable' => true // 是否启用,默认true
    ],
    ],
  3. 注意:TestProcess::callback()方法不能退出,如果退出次数达到10次,Manager进程将会重新创建进程。

其他特性

配置Swoole的事件回调函数

支持的事件列表:

事件 需实现的接口 发生时机
ServerStart Hhxsv5LaravelSSwooleEventsServerStartInterface 发生在Master进程启动时,此事件中不应处理复杂的业务逻辑,只能做一些初始化的简单工作
ServerStop Hhxsv5LaravelSSwooleEventsServerStopInterface 发生在Server正常退出时,此事件中不能使用异步或协程相关的API
WorkerStart Hhxsv5LaravelSSwooleEventsWorkerStartInterface 发生在Worker/Task进程启动完成后
WorkerStop Hhxsv5LaravelSSwooleEventsWorkerStopInterface 发生在Worker/Task进程正常退出后
WorkerError Hhxsv5LaravelSSwooleEventsWorkerErrorInterface 发生在Worker/Task进程发生异常或致命错误时

1.创建事件处理类,实现相应的接口。

 namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
use Swoole\Atomic;
use Swoole\Http\Server;
class ServerStartEvent implements ServerStartInterface
{
public function __construct()
{
}
public function handle(Server $server)
{
// 初始化一个全局计数器(跨进程的可用)
$server->atomicCount = new Atomic(); // 控制器中调用:app('swoole')->atomicCount->get();
}
}
namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
class WorkerStartEvent implements WorkerStartInterface
{
public function __construct()
{
}
public function handle(Server $server, $workerId)
{
// 初始化一个数据库连接池对象
// DatabaseConnectionPool::init();
}
}

2.配置。

 // 修改文件 config/laravels.php
'event_handlers' => [
'ServerStart' => \App\Events\ServerStartEvent::class,
'WorkerStart' => \App\Events\WorkerStartEvent::class,
],

注意事项

  • 单例问题

    • 传统FPM下,单例模式的对象的生命周期仅在每次请求中,请求开始=>实例化单例=>请求结束后=>单例对象资源回收。
    • Swoole Server下,所有单例对象会常驻于内存,这个时候单例对象的生命周期与FPM不同,请求开始=>实例化单例=>请求结束=>单例对象依旧保留,需要开发者自己维护单例的状态。
    • 常见的解决方案:

      1. 写一个XxxCleaner类来清理单例对象状态,此类需实现接口Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然后注册到laravels.phpcleaners中。
      2. 用一个中间件重置单例对象的状态。
      3. 如果是以ServiceProvider注册的单例对象,可添加该ServiceProviderlaravels.phpregister_providers中,这样每次请求会重新注册该ServiceProvider,重新实例化单例对象,参考
    • LaravelS 已经内置了一些Cleaner
  • 常见问题:一揽子的已知问题和解决方案。
  • 调试方式:记录日志、Laravel Dump Server(Laravel 5.7已默认集成)
  • 应通过Illuminate\Http\Request对象来获取请求信息,是可读取的,_SERVER是部分可读的,不能使用、_POST、、_COOKIE、、_SESSION、$GLOBALS。

     public function form(\Illuminate\Http\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对象来响应请求,兼容echo、vardump()、print_r(),不能使用函数 dd()、exit()、die()、header()、setcookie()、http_response_code()。

    public function json()
    {
    return response()->json(['time' => time()])->header('header1', 'value1')->withCookie('c1', 'v1');
    }
  • 各种单例的连接将被常驻内存,建议开启持久连接
  1. 数据库连接,连接断开后会自动重连

     // config/database.php
    'connections' => [
    'my_conn' => [
    'driver' => 'mysql',
    'host' => env('DB_MY_CONN_HOST', 'localhost'),
    'port' => env('DB_MY_CONN_PORT', ),
    '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

     // config/database.php
    'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'), // 推荐使用phpredis,以获得更好的性能
    'default' => [
    'host' => env('REDIS_HOST', 'localhost'),
    'password' => env('REDIS_PASSWORD', null),
    'port' => env('REDIS_PORT', ),
    'database' => ,
    'persistent' => true, // 开启持久连接
    ],
    ],
    //...
  • 你声明的全局、静态变量必须手动清理或重置。
  • 无限追加元素到静态或全局变量中,将导致内存爆满。

     // 某类
    class Test
    {
    public static $array = [];
    public static $string = '';
    } // 某控制器
    public function test(Request $req)
    {
    // 内存爆满
    Test::$array[] = $req->input('param1');
    Test::$string .= $req->input('param2');
    }
  • Linux内核参数调整
  • 压力测试

用户与案例

  • KuCoin
  • 医联:WEB站、M站、APP、小程序的账户体系服务。
  • ITOK在线客服平台:用户IT工单的处理跟踪及在线实时沟通。
  • 盟呱呱
  • 微信公众号-广州塔:活动、商城
  • 企鹅游戏盒子、明星新势力、以及小程序广告服务
  • 小程序-修机匠手机上门维修服务:手机维修服务,提供上门服务,支持在线维修。
  • 亿健APP

推荐阅读:

实现websocket 主动消息推送,用laravel+Swoole

PHP laravel+thrift+swoole打造微服务框架

用Swoole+React 实现的聊天室

Swoole和Redis实现的并发队列处理系统

 

LaravelS - 基于Swoole加速Laravel/Lumen的更多相关文章

  1. 如何通过swoole加速laravel的问题?

    这篇文章主要介绍了关于如何使用swoole加速laravel,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下 再来复习一下吧,导致 php 慢的各种因素中解析性语言的特性可以说是罪魁祸首 ...

  2. 使用Swoole加速Laravel(正式环境中)

    1 Laravel的速度瓶颈在哪? 1.1 已有的一些优化方法 1.1.1 laravel官方提供了一些优化laravel的优化方法 php artisan optimize php artisan ...

  3. 在Swoole上加速Laravel应用

    Swoole是用于PHP的生产级异步编程框架.它是用纯C语言编写的PHP扩展,它使PHP开发人员可以在PHP中编写高性能,可伸缩的并发TCP,UDP,Unix套接字,HTTP,WebSocket服务, ...

  4. 使用 Swoole 加速你的 CMS 系统,并实现热更新 (基于 Laravel 框架)

    主题:使用 Swoole 加速你的 CMS 系统,并实现热更新 关于 Swoole 的简介不再在此赘述,各位可以自行查看官网的文档进行详细的了解. 本文以 MyCms 为例,简要说明 Swoole 结 ...

  5. laravels -- Swoole加速php

    LaravelS是一个胶水项目,用于快速集成Swoole到Laravel,然后赋予它们更好的性能.更多可能性. 环境 : ubuntu16 + nginx + php7.1 + LaravelS搭建高 ...

  6. swoole运行模式加速laravel应用的详细介绍

    本篇文章给大家带来的内容是关于swoole运行模式加速laravel应用的详细介绍,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 一.Swoole Swoole号称重新定义了PHP, ...

  7. 使用 Swoole 加速你的 CMS 系统

    项目介绍 MyCms是一款基于Laravel开发的开源免费的自媒体博客CMS系统,适用于个人网站及企业网站开发使用,助力个人开发者知识技术变现 Swoole介绍 Swoole: PHP的异步.并行.高 ...

  8. 手撸基于swoole 的分布式框架 实现分布式调用(20)讲

    最近看的一个swoole的课程,前段时间被邀请的参与的这个课程 比较有特点跟一定的深度,swoole的实战教程一直也不多,结合swoole构建一个新型框架,最后讲解如何实现分布式RPC的调用. 内容听 ...

  9. tcpproxy:基于 Swoole 实现的 TCP 数据包转发工具的方法

    假设我们希望有一台机器A(ip 192.168.1.101)要开放端口6379给用户访问,但可能实际情况是用户无法直接访问到A(ip 192.168.1.101), 但却有一台机器B(ip 192.1 ...

随机推荐

  1. Java基础(十一)回调(callback)与对象克隆(Cloneable)

    一.回调 1.回调是一种常见的程序设计模式,可以指出某个特定时间发生时应该采取的动作. 在java.swing包中有一个类Timer类,可以使用它在到达指定的时间间隔作出什么动作.那么就有两个问题,即 ...

  2. git push 报src refspec xxx does not match any的错误

    今天在向一个新的远程分支上推送项目的时候报错: 远程分支branch_new是其他人建的,我在自己本地修改后把自己分支的修改推送到这个远程分支上. 把修改提到本地仓库: git add ./ git ...

  3. Java连接MySQL简单步骤及常见问题解析

    这篇文章跟读者们介绍Java语言如何连接到MySQL数据库相关操作,及FAQ(Frequently Asked Questions)介绍. 选择工具和准备数据集 IDE:Eclipse 数据库:使用的 ...

  4. 激活Sublime Text 3,亲测有效!

    一.修改sunlime_text.exe 打开Sublime Text的安装目录,在修改之前要先备份一下sunlime_text.exe. 欢迎关注微信公众号:万猫学社,每周一分享Java技术干货. ...

  5. 康托展开&逆康托展开学习笔记

    啊...好久没写了...可能是最后一篇学习笔记了吧 题目大意:给定序列求其在全排列中的排名&&给定排名求排列. 这就是康托展开&&逆康托展开要干的事了.下面依次介绍 一 ...

  6. 考试T3麻将

    这题就是一个简单的暴力,但考试的时候不知道脑子在想什么,什么都没打出来,也许是我想的太多了... 这道题对于不会打麻将的人来说还是有点难理解规则的,我没说过我会打麻将,这里是题目链接. 20分思路,利 ...

  7. Java描述设计模式(18):享元模式

    本文源码:GitHub·点这里 || GitEE·点这里 一.使用场景 应用代码 public class C01_InScene { public static void main(String[] ...

  8. Pycharm创建项目时 自动添加头部信息

    1.打开PyCharm,选择File--Settings 2.依次选择Editor---Code Style-- File and Code Templates---Python Script 3.. ...

  9. printf的实现原理

    printf的声明    int _cdecl printf(const char* format, …);    _cdecl是C和C++程序的缺省调用方式 _CDEDL调用约定:    1.参数从 ...

  10. beacon帧字段结构最全总结(一)——beacon基本结构

    一.beacon帧主要结构 二.MAC  header 1.Version:版本号,目前为止802.11只有一个版本,所以协议编号为0 2.Type:定义802.11帧类型,802.11帧分为管理帧( ...