php - LaravelS - 基于Swoole加速Laravel/Lumen - 带你飞🚀 - 个人文章 - SegmentFault 思否


本站和网页 https://segmentfault.com/a/1190000013358289 的作者无关,不对其内容负责。快照谨为网络故障时之索引,不代表被搜索网站的即时页面。

php - LaravelS - 基于Swoole加速Laravel/Lumen - 带你飞🚀 - 个人文章 - SegmentFault 思否注册登录问答专栏标签招聘活动发现✓使用“Bing”搜本站使用“Google”搜本站使用“百度”搜本站站内搜索注册登录LaravelS - 基于Swoole加速Laravel/Lumen - 带你飞🚀hhxsv57091411关注作者首页专栏php文章详情56LaravelS - 基于Swoole加速Laravel/Lumen - 带你飞🚀hhxsv57091411发布于2018-02-24  LaravelS🚀 LaravelS 是 Swoole 和 Laravel/Lumen 之间开箱即用的适配器。QQ交流群群1:698480528(已满)群2:62075835特性内置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)。有可能找不到3.0版本,解决方案移步#81。composer require "hhxsv5/laravel-s:~3.7.0" -vvv
# 确保你的composer.lock文件是在版本控制中2.注册Service Provider(以下两步二选一)。Laravel: 修改文件config/app.php,Laravel 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/inotify4.修改配置config/laravels.php:监听的IP、端口等,请参考配置项。运行在运行之前,请先仔细阅读:注意事项(这非常重要)。操作命令:php bin/laravels {start|stop|restart|reload|info|help}。命令说明start启动LaravelS,展示已启动的进程列表 "ps -ef|grep laravels"stop停止LaravelS,并触发自定义进程的onStop方法restart重启LaravelS:先平滑Stop,然后再Start;在Start完成之前,服务是不可用的reload平滑重启所有Task/Worker/Timer进程(这些进程内包含了你的业务代码),并触发自定义进程的onReload方法,不会重启Master/Manger进程;修改config/laravels.php后,你只有调用restart来完成重启info显示组件的版本信息help显示帮助信息启动选项,针对start和restart命令。选项说明-d|--daemonize以守护进程的方式运行,此选项将覆盖laravels.php中swoole.daemonize设置-e|--env指定运行的环境,如--env=testing将会优先使用配置文件.env.testing,这个特性要求Laravel 5.2+-i|--ignore忽略检查Master进程的PID文件-x|--x-version记录当前工程的版本号(分支),保存在$_ENV/$_SERVER中,访问方式:$_ENV['X_VERSION'] $_SERVER['X_VERSION'] $request->server->get('X_VERSION')运行时文件:start时会自动执行php artisan laravels config并生成这些文件,开发者一般不需要关注它们,建议将它们加到.gitignore中。文件说明storage/laravels.confLaravelS的运行时配置文件storage/laravels.pidMaster进程的PID文件storage/laravels-timer-process.pid定时器Timer进程的PID文件storage/laravels-custom-processes.pid所有自定义进程的PID文件部署建议通过Supervisord监管主进程,前提是不能加-d选项并且设置swoole.daemonize为false。[program:laravel-s-test]
directory=/var/wwww/laravel-s-test
command=/usr/local/bin/php bin/laravels start -i
numprocs=1
autostart=true
autorestart=true
startretries=3
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log与Nginx配合使用(推荐)示例。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 swoole {
# 通过 IP:Port 连接
server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/yourpath/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 /yourpath/laravel-s-test/public;
access_log /yourpath/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;
# “swoole”是指upstream
proxy_pass http://swoole;
}与Apache配合使用LoadModule proxy_module /yourpath/modules/mod_proxy.so
LoadModule proxy_balancer_module /yourpath/modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module /yourpath/modules/mod_lbmethod_byrequests.so
LoadModule proxy_http_module /yourpath/modules/mod_proxy_http.so
LoadModule slotmem_shm_module /yourpath/modules/mod_slotmem_shm.so
LoadModule rewrite_module /yourpath/modules/mod_rewrite.so
LoadModule remoteip_module /yourpath/modules/mod_remoteip.so
LoadModule deflate_module /yourpath/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 /yourpath/laravel-s-test/public;
DirectoryIndex index.html index.htm
<Directory "/">
AllowOverride None
Require all granted
</Directory>
RemoteIPHeader X-Forwarded-For
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\Http\Response;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
* @see https://wiki.swoole.com/#/start/start_ws_server
*/
class WebSocketService implements WebSocketHandlerInterface
// 声明没有参数的构造函数
public function __construct()
// public function onHandShake(Request $request, Response $response)
// {
// 自定义握手:https://wiki.swoole.com/#/websocket_server?id=onhandshake
// 成功握手之后会自动触发onOpen事件
// }
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()])]);
// 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
$server->push($request->fd, 'Welcome to LaravelS');
public function onMessage(Server $server, Frame $frame)
// \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
// 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
$server->push($frame->fd, date('Y-m-d H:i:s'));
public function onClose(Server $server, $fd, $reactorId)
// 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
}2.更改配置config/laravels.php。// ...
'websocket' => [
'enable' => true, // 注意:设置enable为true
'handler' => \App\Services\WebSocketService::class,
],
'swoole' => [
//...
// dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'dispatch_mode' => 2,
//...
],
// ...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:5200 weight=5 max_fails=3 fail_timeout=30s;
# 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
#server unix:/yourpath/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 /yourpath/laravel-s-test/public;
access_log /yourpath/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' => 600,
'heartbeat_check_interval' => 60,
//...
],Nginx读取代理服务器超时的配置# 如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接
proxy_read_timeout 60s;6.在控制器中推送数据namespace App\Http\Controllers;
class TestController extends Controller
public function push()
$fd = 1; // Find fd by userId from a map [userId=>fd].
/**@var \Swoole\WebSocket\Server $swoole */
$swoole = app('swoole');
$success = $swoole->push($fd, 'Push data to fd#1 in Controller');
var_dump($success);
}监听事件系统事件通常,你可以在这些事件中重置或销毁一些全局或静态的变量,也可以修改当前的请求和响应。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
});自定义的异步事件此特性依赖Swoole的AsyncTask,必须先设置config/laravels.php的swoole.task_worker_num。异步事件的处理能力受Task进程数影响,需合理设置task_worker_num。1.创建事件类。use Hhxsv5\LaravelS\Swoole\Task\Event;
class TestEvent extends Event
protected $listeners = [
// 监听器列表
TestListener1::class,
// TestListener2::class,
];
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\Listener;
class TestListener1 extends Listener
/**
* @var TestEvent
*/
protected $event;
public function handle()
\Log::info(__CLASS__ . ':handle start', [$this->event->getData()]);
sleep(2);// 模拟一些慢速的事件处理
// 监听器中也可以投递Task,但不支持Task的finish()回调。
// 注意:config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
$ret = Task::deliver(new TestTask('task data'));
var_dump($ret);
// 此处抛出的异常会被上层捕获并记录到Swoole日志,开发者需要手动try/catch
}3.触发事件。// 实例化TestEvent并通过fire触发,此操作是异步的,触发后立即返回,由Task进程继续处理监听器中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Event;
$event = new TestEvent('event data');
// $event->delay(10); // 延迟10秒触发
// $event->setTries(3); // 出现异常时,累计尝试3次
$success = Event::fire($event);
var_dump($success);// 判断是否触发成功异步的任务队列此特性依赖Swoole的AsyncTask,必须先设置config/laravels.php的swoole.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(2);// 模拟一些慢速的事件处理
// 此处抛出的异常会被上层捕获并记录到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秒投递任务
// $task->setTries(3); // 出现异常时,累计尝试3次
$ret = Task::deliver($task);
var_dump($ret);// 判断是否投递成功毫秒级定时任务基于Swoole的毫秒定时器,封装的定时任务,取代Linux的Crontab。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 = 0;
// !!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):一是重载对应的方法,二是注册定时任务时传入参数。
// --- 重载对应的方法来返回配置:开始
public function interval()
return 1000;// 每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(1); // Swoole>=2.1 run()方法已自动创建了协程。
$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(); // 终止此定时任务,但restart/reload后会再次运行
// CronJob中也可以投递Task,但不支持Task的finish()回调。
// 注意:修改config/laravels.php,配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
$ret = Task::deliver(new TestTask('task data'));
var_dump($ret);
// 此处抛出的异常会被上层捕获并记录到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' => 5, // Reload时最大等待时间
// 打开全局定时器开关:当多实例部署时,确保只有一个实例运行定时任务,此功能依赖 Redis,具体请看 https://laravel.com/docs/7.x/redis
'global_lock' => false,
'global_lock_key' => config('app.name', 'Laravel'),
],
// ...
];3.注意在构建服务器集群时,会启动多个定时器,要确保只启动一个定期器,避免重复执行定时任务。4.LaravelS v3.4.0开始支持热重启[Reload]定时器进程,LaravelS 在收到SIGUSR1信号后会等待max_wait_time(默认5)秒再结束进程,然后Manager进程会重新拉起定时器进程。5.如果你仅需要用到分钟级的定时任务,建议启用Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob来替代Linux Crontab,这样就可以沿用Laravel任务调度的编码习惯,配置Kernel即可。// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
// runInBackground()方法会新启子进程执行任务,这是异步的,不会影响其他任务的执行时机
$schedule->command(TestCommand::class)->runInBackground()->everyMinute();
}修改代码后自动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当以上方法都不行时,终极解决方案:配置max_request=1,worker_num=1,这样Worker进程处理完一个请求就会重启,这种方法的性能非常差,故仅限在开发环境使用。在你的项目中使用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());// 单例使用SwooleTable1.定义Table,支持定义多个Table。Swoole启动之前会创建定义的所有Table。// 在"config/laravels.php"配置
// ...
'swoole_tables' => [
// 场景:WebSocket中UserId与FD绑定
'ws' => [// Key为Table名称,使用时会自动添加Table后缀,避免重名。这里定义名为wsTable的Table
'size' => 102400,//Table的最大行数
'column' => [// Table的列定义
['name' => 'value', 'type' => \Swoole\Table::TYPE_INT, 'size' => 8],
],
],
//...继续定义其他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的中间件。
* Route::get('/ws', function () {
* // 响应状态码200的任意内容
* return 'websocket';
* })->middleware(['auth']);
*/
// $user = Auth::user();
// $userId = $user ? $user->id : 0; // 0 表示未登录的访客用户
$userId = mt_rand(1000, 10000);
// if (!$userId) {
// // 未登录用户直接断开连接
// $server->disconnect($request->fd);
// return;
// }
$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:') === 0 && $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增加监听的端口与多端口混合协议为了使我们的主服务器能支持除HTTP和WebSocket外的更多协议,我们引入了Swoole的多端口混合协议特性,在LaravelS中称为Socket。现在,可以很方便地在Laravel上构建TCP/UDP应用。创建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提供的组件如DB、Eloquent等。同时,如果需要使用该协议端口的Swoole\Server\Port对象,只需要像如下代码一样访问Socket类的成员swoolePort即可。public function onReceive(Server $server, $fd, $reactorId, $data)
$port = $this->swoolePort; // 获得`Swoole\Server\Port`对象
}namespace App\Http\Controllers;
class TestController extends Controller
public function test()
/**@var \Swoole\Http\Server|\Swoole\WebSocket\Server $swoole */
$swoole = app('swoole');
// $swoole->ports:遍历所有Port对象,https://wiki.swoole.com/#/server/properties?id=ports
$port = $swoole->ports[1]; // 获得`Swoole\Server\Port`对象,$port[0]是主服务器的端口
foreach ($port->connections as $fd) { // 遍历所有连接
// $swoole->send($fd, 'Send tcp message');
// if($swoole->isEstablished($fd)) {
// $swoole->push($fd, 'Send websocket message');
// }
}注册套接字。// 修改文件 config/laravels.php
// ...
'sockets' => [
'host' => '127.0.0.1',
'port' => 5291,
'type' => SWOOLE_SOCK_TCP,// 支持的嵌套字类型:https://wiki.swoole.com/#/consts?id=socket-%e7%b1%bb%e5%9e%8b
'settings' => [// Swoole可用的配置项:https://wiki.swoole.com/#/server/port?id=%e5%8f%af%e9%80%89%e5%8f%82%e6%95%b0
'open_eof_check' => true,
'package_eof' => "\r\n",
],
'handler' => \App\Sockets\TestTcpSocket::class,
'enable' => true, // 是否启用,默认为true
],
],关于心跳配置,只能设置在主服务器上,不能配置在套接字上,但套接字会继承主服务器的心跳配置。对于TCP协议,dispatch_mode选项设为1/3时,底层会屏蔽onConnect/onClose事件,原因是这两种模式下无法保证onConnect/onClose/onReceive的顺序。如果需要用到这两个事件,请将dispatch_mode改为2/4/5,参考。'swoole' => [
//...
'dispatch_mode' => 2,
//...
];测试。TCP:telnet 127.0.0.1 5291UDP:Linux下 echo "Hello LaravelS" > /dev/udp/127.0.0.1/5292其他协议的注册示例。UDP'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'sockets' => [
'host' => '0.0.0.0',
'port' => 5293,
'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' => 5294,
'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。创建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
/** */
private static $quit = false;
public static function callback(Server $swoole, Process $process)
// 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
while (!self::$quit) {
\Log::info('Test process: running');
// sleep(1); // Swoole < 2.1
Coroutine::sleep(1); // Swoole>=2.1 已自动为callback()方法创建了协程并启用了协程Runtime。
// 自定义进程中也可以投递Task,但不支持Task的finish()回调。
// 注意:修改config/laravels.php,配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/#/server/setting?id=task_ipc_mode
$ret = Task::deliver(new TestTask('task data'));
var_dump($ret);
// 上层会捕获callback中抛出的异常,并记录到Swoole日志,然后此进程会退出,3秒后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
\Log::info('Test process: reloading');
self::$quit = true;
// $process->exit(0); // 强制退出进程
// 要求:LaravelS >= v3.7.4 并且 callback() 必须是异步非阻塞程序。
public static function onStop(Server $swoole, Process $process)
// Stop the process...
// Then end process
\Log::info('Test process: stopping');
self::$quit = true;
// $process->exit(0); // 强制退出进程
```
注册TestProcess。// 修改文件 config/laravels.php
// ...
'processes' => [
'test' => [ // Key为进程名
'class' => \App\Processes\TestProcess::class,
'redirect' => false, // 是否重定向输入输出
'pipe' => 0, // 管道类型:0不创建管道,1创建SOCK_STREAM类型管道,2创建SOCK_DGRAM类型管道
'enable' => true, // 是否启用,默认true
//'queue' => [ // 启用消息队列作为进程间通信,配置空数组表示使用默认参数
// 'msg_key' => 0, // 消息队列的KEY,默认会使用ftok(__FILE__, 1)
// 'mode' => 2, // 通信模式,默认为2,表示争抢模式
// 'capacity' => 8192, // 单个消息长度,长度受限于操作系统内核参数的限制,默认为8192,最大不超过65536
//],
//'restart_interval' => 5, // 进程异常退出后需等待多少秒再重启,默认5秒
],
],注意:callback()方法不能退出,如果退出,Manager进程将会重新创建进程。示例:向自定义进程中写数据。// config/laravels.php
'processes' => [
'test' => [
'class' => \App\Processes\TestProcess::class,
'redirect' => false,
'pipe' => 1,
],
],// app/Processes/TestProcess.php
public static function callback(Server $swoole, Process $process)
while ($data = $process->read()) {
\Log::info('TestProcess: read data', [$data]);
$process->write('TestProcess: ' . $data);
}// app/Http/Controllers/TestController.php
public function testProcessWrite()
/**@var \Swoole\Process $process */
$process = app('swoole')->customProcesses['test'];
$process->write('TestController: write data' . time());
var_dump($process->read());
}常用组件Apollo启动LaravelS时会获取Apollo配置并写入到.env文件,同时会启动自定义进程apollo用于监听配置变更,当配置发生变更时自动reload。启用Apollo组件:启动参数加上--enable-apollo以及Apollo的配置参数。php bin/laravels start --enable-apollo --apollo-server=http://127.0.0.1:8080 --apollo-app-id=LARAVEL-S-TEST配置热更新(可选的)。// 修改文件 config/laravels.php
'processes' => Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),// 当存在其他自定义进程配置时
'processes' => [
'test' => [
'class' => \App\Processes\TestProcess::class,
'redirect' => false,
'pipe' => 1,
],
// ...
] + Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),可用的参数列表。参数名描述默认值示例apollo-serverApollo服务器URL---apollo-server=http://127.0.0.1:8080apollo-app-idApollo应用ID---apollo-app-id=LARAVEL-S-TESTapollo-namespacesAPP所属的命名空间,可指定多个application--apollo-namespaces=application --apollo-namespaces=envapollo-clusterAPP所属的集群default--apollo-cluster=defaultapollo-client-ip当前实例的IP,还可用于灰度发布本机内网IP--apollo-client-ip=10.2.1.83apollo-pull-timeout拉取配置时的超时时间(秒)5--apollo-pull-timeout=5apollo-backup-old-env更新配置文件.env时是否备份老的配置文件false--apollo-backup-old-env其他特性配置Swoole事件支持的事件列表:事件需实现的接口发生时机ServerStartHhxsv5LaravelSSwooleEventsServerStartInterface发生在Master进程启动时,此事件中不应处理复杂的业务逻辑,只能做一些初始化的简单工作ServerStopHhxsv5LaravelSSwooleEventsServerStopInterface发生在Server正常退出时,此事件中不能使用异步或协程相关的APIWorkerStartHhxsv5LaravelSSwooleEventsWorkerStartInterface发生在Worker/Task进程启动完成后WorkerStopHhxsv5LaravelSSwooleEventsWorkerStopInterface发生在Worker/Task进程正常退出后WorkerErrorHhxsv5LaravelSSwooleEventsWorkerErrorInterface发生在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(2233);
// 控制器中调用: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],
],Serverless阿里云函数计算函数计算官方文档。1.修改bootstrap/app.php,设置storage目录。因为项目目录只读,/tmp目录才可读写。$app->useStoragePath(env('APP_STORAGE_PATH', '/tmp/storage'));2.创建Shell脚本laravels_bootstrap,并赋予可执行权限。#!/usr/bin/env bash
set +e
# 创建storage相关目录
mkdir -p /tmp/storage/app/public
mkdir -p /tmp/storage/framework/cache
mkdir -p /tmp/storage/framework/sessions
mkdir -p /tmp/storage/framework/testing
mkdir -p /tmp/storage/framework/views
mkdir -p /tmp/storage/logs
# 设置环境变量APP_STORAGE_PATH,请确保与.env的APP_STORAGE_PATH一样
export APP_STORAGE_PATH=/tmp/storage
# Start LaravelS
php bin/laravels start3.配置template.xml。ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
laravel-s-demo:
Type: 'Aliyun::Serverless::Service'
Properties:
Description: 'LaravelS Demo for Serverless'
fc-laravel-s:
Type: 'Aliyun::Serverless::Function'
Properties:
Handler: laravels.handler
Runtime: custom
MemorySize: 512
Timeout: 30
CodeUri: ./
InstanceConcurrency: 10
EnvironmentVariables:
BOOTSTRAP_FILE: laravels_bootstrap
注意事项单例问题传统FPM下,单例模式的对象的生命周期仅在每次请求中,请求开始=>实例化单例=>请求结束后=>单例对象资源回收。Swoole Server下,所有单例对象会常驻于内存,这个时候单例对象的生命周期与FPM不同,请求开始=>实例化单例=>请求结束=>单例对象依旧保留,需要开发者自己维护单例的状态。常见的解决方案:写一个XxxCleaner清理器类来清理单例对象状态,此类需实现接口Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然后注册到laravels.php的cleaners中。用一个中间件来重置单例对象的状态。如果是以ServiceProvider注册的单例对象,可添加该ServiceProvider到laravels.php的register_providers中,这样每次请求会重新注册该ServiceProvider,重新实例化单例对象,参考。LaravelS 已经内置了一些清理器。常见问题:一揽子的已知问题和解决方案。调试方式:记录日志、Laravel Dump Server(Laravel 5.7已默认集成)应通过Illuminate\Http\Request对象来获取请求信息,$_ENV是可读取的,$_SERVER是部分可读的,不能使用$_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST、$_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');
}各种单例的连接将被常驻内存,建议开启持久连接。数据库连接,连接断开后会自动重连// 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,
],
],
//...
],
//...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', 6379),
'database' => 0,
'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内核参数调整压力测试用户与案例Pool-X:可以交易锁定资产的新一代POS矿池。KuCoin:全球化第一的交易所。医联:WEB站、M站、APP、小程序的账户体系服务。ITOK在线客服平台:用户IT工单的处理跟踪及在线实时沟通。盟呱呱微信公众号-广州塔:活动、商城企鹅游戏盒子、明星新势力、以及小程序广告服务小程序-修机匠手机上门维修服务:手机维修服务,提供上门服务,支持在线维修。亿健APP赞助您的支持是我们坚持的最大动力。如果对你有帮助,Star Me LaravelSQQ交流群群1:698480528(已满)群2:62075835phplaravelswoolelumen阅读 25.2k更新于 2021-01-25 赞56收藏73分享本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议hhxsv5Think Much, Code Less.709 声望36 粉丝关注作者0 条评论得票最新提交评论评论支持部分 Markdown 语法:**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用 @ 来通知其他用户。推荐阅读招聘Java/前端/测试[20~60K][18~24薪][成都][可远程]库珀科技是一家提供高并发低延迟技术解决方案的科技公司,主要给海外类似于美国上市公司Coinbase这样的平台提供全量的技术解决方案,我们服务的企业业务在高速增长,已经进入全球Top5。hhxsv5阅读 2One 一个简洁的博客、微博客系统代码:[链接]文档:[链接]系统预览首页:微博列表:微博详细:文章列表:文章详细:归档:搜索,目前只能依据分类、标签搜索😀:管理后台:Eyeswap赞 45阅读 2.2k评论 1怎样用 PHP 来实现枚举?在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...唯一丶赞 25阅读 6.1k评论 4PHP 性能终极 Debug - 生成火焰图2012 年刚开始学习 PHP,那个时候的 PHP 应用很简单,没有太多复杂的设计模式,像依赖注入,工厂模式这些还几乎没有,Reflection API 那时也才刚出来,一个 PHP 应用就是一些包了前端代码的脚本文件,正是因为 PH...路易港赞 5阅读 3.4kLaravel : Syntax error or access violation: 1055 Error?上面这样一段代码, 测试服务器很好, 上线后报错了.Syntax error or access violation: 1055 Error : MySQL : isn't in GROUP BY云云小金子赞 7阅读 12k评论 4golang实现php里的serialize()和unserialize()序列和反序列方法Golang 实现 PHP里的 serialize() 、 unserialize()安装 {代码...} 用法 {代码...} github地址:[链接]JonLee赞 2阅读 6.6k郑方方打怪升级日记 — 初识HTML5与CSS3任务名称:响应式砸蛋页面任务背景前辈:方方啊,最近项目也没什么事情,你看这个砸蛋页面不是很好看,要不你做一个响应式砸蛋页面吧?系统:郑方方接下前辈的任务 - 郑方方自动解析任务步骤任务:响应式砸蛋页面HTML5与C...郑方方赞 1阅读 3.1k评论 3hhxsv5Think Much, Code Less.709 声望36 粉丝关注作者宣传栏文章目录跟随▲5673产品热门问答热门专栏热门课程最新活动翻译酷工作课程Java 开发课程PHP 开发课程Python 开发课程前端开发课程移动开发课程资源每周精选用户排行榜帮助中心建议反馈合作关于我们广告投放职位发布讲师招募联系我们合作伙伴关注产品技术日志社区运营日志市场运营日志团队日志社区访谈条款服务协议隐私政策下载 AppCopyright © 2011-2022 SegmentFault. 当前呈现版本 22.12.19浙ICP备15005796号-2浙公网安备33010602002000号ICP 经营许可 浙B2-20201554杭州堆栈科技有限公司版权所有