panda843 / laravel-s
🚀 LaravelS是Swoole与Laravel/Lumen之间的现成适配器。
Requires
- php: >=5.5.9
- ext-curl: *
- ext-json: *
- ext-pcntl: *
- swoole/ide-helper: @dev
- symfony/console: >=2.7.0
Requires (Dev)
- phpunit/phpunit: >=4.8.36
Suggests
- ext-inotify: Inotify, used to real-time reload.
- ext-swoole: Coroutine-based concurrency library for PHP, require >= 1.7.19.
- dev-master
- v3.7.31
- v3.7.30
- v3.7.29
- v3.7.28
- v3.7.27
- v3.7.26
- v3.7.25
- v3.7.24
- v3.7.23
- v3.7.22
- v3.7.21
- v3.7.20
- v3.7.19
- v3.7.18
- v3.7.17
- v3.7.16
- v3.7.15
- v3.7.14
- v3.7.13
- v3.7.12
- v3.7.11
- v3.7.10
- v3.7.9
- v3.7.8
- v3.7.7
- v3.7.6
- v3.7.5
- v3.7.4
- v3.7.3
- v3.7.2
- v3.7.1
- v3.7.0
- v3.6.4
- v3.6.3
- v3.6.2
- v3.6.1
- v3.6.0
- v3.5.17
- v3.5.16
- v3.5.15
- v3.5.14
- v3.5.13
- v3.5.12
- v3.5.11
- v3.5.10
- v3.5.9
- v3.5.8
- v3.5.7
- v3.5.6
- v3.5.5
- v3.5.4
- v3.5.3
- v3.5.2
- v3.5.1
- v3.5.0
- v3.4.4
- v3.4.3
- v3.4.2
- v3.4.1
- v3.4.0
- v3.3.10
- v3.3.9
- v3.3.8
- v3.3.7
- v3.3.6
- v3.3.5
- v3.3.4
- v3.3.3
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.1
- v3.2.0
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v2.0.0-Alpha
- v1.11.3
- v1.11.2
- v1.11.1
- v1.11.0
- v1.10.19
- v1.10.18
- v1.10.17
- v1.10.16
- v1.10.15
- v1.10.14
- v1.10.13
- v1.10.12
- v1.10.11
- v1.10.10
- v1.10.9
- v1.10.8
- v1.10.7
- v1.10.6
- v1.10.5
- v1.10.3
- v1.10.2
- v1.10.1
- v1.10.0
- v1.9.17
- v1.9.16
- v1.9.15
- v1.9.14
- v1.9.13
- v1.9.12
- v1.9.11
- v1.9.10
- v1.9.9
- v1.9.8
- v1.9.7
- v1.9.6
- v1.9.5
- v1.9.4
- v1.9.2
- v1.9.1
- v1.9.0
- v1.8.14
- v1.8.13
- v1.8.12
- v1.8.11
- v1.8.10
- v1.8.9
- v1.8.8
- v1.8.7
- v1.8.6
- v1.8.5
- v1.8.4
- v1.8.3
- v1.8.2
- v1.8.1
- v1.8.0
- v1.7.14
- v1.7.13
- v1.7.12
- v1.7.11
- v1.7.10
- v1.7.9
- v1.7.8
- v1.7.7
- v1.7.6
- v1.7.5
- v1.7.4
- v1.7.3
- v1.7.1
- v1.7.0
- v1.6.13
- v1.6.12
- v1.6.11
- v1.6.10
- v1.6.9
- v1.6.8
- v1.6.7
- v1.6.6
- v1.6.5
- v1.6.4
- v1.6.3
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.2
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.1
- v1.0.0
- dev-feature/prometheus
- dev-revert-375-master
- dev-feature/dd
- dev-feature/connection_pool
- dev-feature/full_support_coroutine
- dev-feature/coroutine_clients
This package is auto-updated.
Last update: 2024-09-24 08:07:06 UTC
README
🚀 LaravelS是Laravel/Lumen与Swoole之间的现成适配器
持续更新
- 请
关注
此仓库以获取最新更新。
目录
- 功能
- 基准测试
- 要求
- 安装
- 运行
- 部署
- 与Nginx协同(推荐)
- 与Apache协同
- 启用WebSocket服务器
- 监听事件
- 异步任务队列
- 毫秒级定时任务
- 修改代码后自动重新加载
- 在您的项目中获取SwooleServer实例
- 使用SwooleTable
- 多端口混合协议
- 协程
- 自定义进程
- 通用组件
- 其他功能
- 重要通知
- 用户和案例
- 替代方案
- 赞助
- 许可
功能
-
内置Http/WebSocket服务器
-
内存驻留
-
优雅地重新加载
-
支持Laravel/Lumen,具有良好的兼容性
-
简单 & 现成
基准测试
要求
安装
composer require "hhxsv5/laravel-s:~3.7.0" -vvv # Make sure that your composer.lock file is under the VCS
2.注册服务提供者(选择以下两个之一)。
-
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后,您需要重新发布;点击此处查看每个版本的变更说明。
php artisan laravels publish # Configuration: config/laravels.php # Binary: bin/laravels bin/fswatch bin/inotify
修改config/laravels.php
:监听ip,监听端口,参考设置。
性能调整
-
工作进程数量:LaravelS使用Swoole的
同步IO
模式,工作进程数量设置越大,并发性能越好,但会导致更大的内存使用和进程切换开销。如果每个请求需要100ms,为了提供1000QPS的并发,至少需要配置100个工作进程。计算方法是:工作进程数 = 1000QPS/(1s/1ms) = 100,因此需要进行增量压力测试以计算最佳工作进程数
。
运行
运行前请仔细阅读通知
,重要通知(IMPORTANT)。
- 命令:
php bin/laravels {start|stop|restart|reload|info|help}
。
- 命令
start
和restart
的启动选项。
- 运行时文件:
start
将自动执行php artisan laravels config
并生成这些文件,开发者通常不需要关注它们,建议将它们添加到.gitignore
文件中。
部署
建议通过Supervisord监督主进程,前提是不带
-d
选项,并将swoole.daemonize
设置为false
。
[program:laravel-s-test]
directory=/var/www/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 { # Connect IP:Port server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s; # Connect UnixSocket Stream file, tips: put the socket file in the /dev/shm directory to get better performance #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; # Don't forget to bind the 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 handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource. location / { try_files $uri @laravels; } # Response 404 directly when request the PHP file, to avoid exposing 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" is the 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> # Don't forget to bind the 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 handles the static resources, LaravelS handles the dynamic resource. 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服务器的监听地址与Http服务器相同。
1. 创建WebSocket处理器类,并实现WebSocketHandlerInterface
接口。实例在启动时会自动创建,您不需要手动创建它。
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://www.swoole.co.uk/docs/modules/swoole-websocket-server */ class WebSocketService implements WebSocketHandlerInterface { // Declare constructor without parameters public function __construct() { } // public function onHandShake(Request $request, Response $response) // { // Custom handshake: https://www.swoole.co.uk/docs/modules/swoole-websocket-server-on-handshake // The onOpen event will be triggered automatically after a successful handshake // } public function onOpen(Server $server, Request $request) { // Before the onOpen event is triggered, the HTTP request to establish the WebSocket has passed the Laravel route, // so Laravel's Request, Auth information are readable, Session is readable and writable, but only in the onOpen event. // \Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]); // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. $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]); // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. $server->push($frame->fd, date('Y-m-d H:i:s')); } public function onClose(Server $server, $fd, $reactorId) { // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. } }
2. 修改config/laravels.php
文件。
// ... 'websocket' => [ 'enable' => true, // Note: set enable to true 'handler' => \App\Services\WebSocketService::class, ], 'swoole' => [ //... // Must set dispatch_mode in (2, 4, 5), see https://www.swoole.co.uk/docs/modules/swoole-server/configuration 'dispatch_mode' => 2, //... ], // ...
3. 使用SwooleTable
绑定FD与UserId,可选的,参考Swoole Table 示例。您也可以使用其他全局存储服务,如Redis/Memcached/MySQL,但请注意,多个Swoole服务器
之间的FD可能会发生冲突。
4. 与Nginx协同(推荐)
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream swoole { # Connect IP:Port server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s; # Connect UnixSocket Stream file, tips: put the socket file in the /dev/shm directory to get better performance #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; # Don't forget to bind the 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 handles the static resources(recommend enabling gzip), LaravelS handles the dynamic resource. location / { try_files $uri @laravels; } # Response 404 directly when request the PHP file, to avoid exposing public/*.php #location ~* \.php$ { # return 404; #} # Http and WebSocket are concomitant, Nginx identifies them by "location" # !!! The location of WebSocket is "/ws" # Javascript: var ws = new WebSocket("ws://laravels.com/ws"); location =/ws { # proxy_connect_timeout 60s; # proxy_send_timeout 60s; # proxy_read_timeout: Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds; At the same time, this close behavior is also affected by heartbeat setting of 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' => [ //... // All connections are traversed every 60 seconds. If a connection does not send any data to the server within 600 seconds, the connection will be forced to close. 'heartbeat_idle_time' => 600, 'heartbeat_check_interval' => 60, //... ],
-
Nginx的代理读取超时
# Nginx will close the connection if the proxied server does not send data to Nginx in 60 seconds 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); } }
监听事件
系统事件
通常,您可以重置/销毁一些
global/static
变量,或更改当前的Request/Response
对象。
-
laravels.received_request
在LaravelS将Swoole\Http\Request
解析为Illuminate\Http\Request
之后,在Laravel的Kernel处理此请求之前。// Edit file `app/Providers/EventServiceProvider.php`, add the following code into method `boot` // If no variable $events, you can also call Facade \Event::listen(). $events->listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) { $req->query->set('get_key', 'hhxsv5');// Change query of request $req->request->set('post_key', 'hhxsv5'); // Change post of request });
-
laravels.generated_response
在Laravel的Kernel处理请求之后,在LaravelS将Illuminate\Http\Response
解析为Swoole\Http\Response
之前。// Edit file `app/Providers/EventServiceProvider.php`, add the following code into method `boot` // If no variable $events, you can also call 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');// Change header of response });
自定义异步事件
此功能依赖于
Swoole
的AsyncTask
,您需要首先在config/laravels.php
中设置swoole.task_worker_num
。异步事件处理的性能受Swoole任务进程数的影响,您需要适当设置task_worker_num。
1. 创建事件类。
use Hhxsv5\LaravelS\Swoole\Task\Event; class TestEvent extends Event { protected $listeners = [ // Listener list 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);// Simulate the slow codes // Deliver task in CronJob, but NOT support callback finish() of task. // Note: Modify task_ipc_mode to 1 or 2 in config/laravels.php, see https://www.swoole.co.uk/docs/modules/swoole-server/configuration $ret = Task::deliver(new TestTask('task data')); var_dump($ret); // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. } }
3. 触发事件。
// Create instance of event and fire it, "fire" is asynchronous. use Hhxsv5\LaravelS\Swoole\Task\Event; $event = new TestEvent('event data'); // $event->delay(10); // Delay 10 seconds to fire event // $event->setTries(3); // When an error occurs, try 3 times in total $success = Event::fire($event); var_dump($success);// Return true if sucess, otherwise false
异步任务队列
此功能依赖于
Swoole
的AsyncTask
,您需要首先在config/laravels.php
中设置swoole.task_worker_num
。任务处理的性能受Swoole任务进程数的影响,您需要适当设置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; } // The logic of task handling, run in task process, CAN NOT deliver task public function handle() { \Log::info(__CLASS__ . ':handle start', [$this->data]); sleep(2);// Simulate the slow codes // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. $this->result = 'the result of ' . $this->data; } // Optional, finish event, the logic of after task handling, run in worker process, CAN deliver task public function finish() { \Log::info(__CLASS__ . ':finish start', [$this->result]); Task::deliver(new TestTask2('task2 data')); // Deliver the other task } }
2. 发送任务。
// Create instance of TestTask and deliver it, "deliver" is asynchronous. use Hhxsv5\LaravelS\Swoole\Task\Task; $task = new TestTask('task data'); // $task->delay(3);// delay 3 seconds to deliver task // $task->setTries(3); // When an error occurs, try 3 times in total $ret = Task::deliver($task); var_dump($ret);// Return true if sucess, otherwise false
毫秒级定时任务
基于Swoole的毫秒级定时器的cron作业包装器,替换
Linux
的Crontab
。
1. 创建cron作业类。
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; // !!! The `interval` and `isImmediate` of cron job can be configured in two ways(pick one of two): one is to overload the corresponding method, and the other is to pass parameters when registering cron job. // --- Override the corresponding method to return the configuration: begin public function interval() { return 1000;// Run every 1000ms } public function isImmediate() { return false;// Whether to trigger `run` immediately after setting up } // --- Override the corresponding method to return the configuration: end public function run() { \Log::info(__METHOD__, ['start', $this->i, microtime(true)]); // do something // sleep(1); // Swoole < 2.1 Coroutine::sleep(1); // Swoole>=2.1 Coroutine will be automatically created for run(). $this->i++; \Log::info(__METHOD__, ['end', $this->i, microtime(true)]); if ($this->i >= 10) { // Run 10 times only \Log::info(__METHOD__, ['stop', $this->i, microtime(true)]); $this->stop(); // Stop this cron job, but it will run again after restart/reload. // Deliver task in CronJob, but NOT support callback finish() of task. // Note: Modify task_ipc_mode to 1 or 2 in config/laravels.php, see https://www.swoole.co.uk/docs/modules/swoole-server/configuration $ret = Task::deliver(new TestTask('task data')); var_dump($ret); } // The exceptions thrown here will be caught by the upper layer and recorded in the Swoole log. Developers need to try/catch manually. } }
2. 注册cron作业。
// Register cron jobs in file "config/laravels.php" [ // ... 'timer' => [ 'enable' => true, // Enable Timer 'jobs' => [ // The list of cron job // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab // \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class, // Two ways to configure parameters: // [\App\Jobs\Timer\TestCronJob::class, [1000, true]], // Pass in parameters when registering \App\Jobs\Timer\TestCronJob::class, // Override the corresponding method to return the configuration ], 'max_wait_time' => 5, // Max waiting time of reloading // Enable the global lock to ensure that only one instance starts the timer when deploying multiple instances. This feature depends on Redis, please see https://laravel.net.cn/docs/7.x/redis 'global_lock' => false, 'global_lock_key' => config('app.name', 'Laravel'), ], // ... ];
3. 注意:构建服务器集群时,将启动多个定时器,因此您需要确保只启动一个定时器以避免重复执行任务。
4. LaravelS v3.4.0
开始支持[重载]热重启Timer
进程。LaravelS收到SIGUSR1
信号后,将等待max_wait_time
(默认5秒)结束进程,然后Manager进程将重新拉起Timer
进程。
5. 如果您只需要使用分钟级
计划任务,建议启用Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob
而不是Linux Crontab,这样您就可以遵循Laravel任务调度的编码习惯并配置Kernel
。
// app/Console/Kernel.php protected function schedule(Schedule $schedule) { // runInBackground() will start a new child process to execute the task. This is asynchronous and will not affect the execution timing of other tasks. $schedule->command(TestCommand::class)->runInBackground()->everyMinute(); }
修改代码后自动重新加载
-
通过
inotify
支持Linux。1. 安装inotify扩展。
2. 在设置中打开开关。
3.注意:仅在
Linux
系统中修改文件以接收文件更改事件。建议使用最新的Docker。Vagrant解决方案。 -
通过
fswatch
,支持OS X/Linux/Windows。1.安装fswatch。
2.在项目根目录下运行命令。
# Watch current directory ./bin/fswatch # Watch app directory ./bin/fswatch ./app
-
通过
inotifywait
,支持Linux。1.安装inotify-tools。
2.在项目根目录下运行命令。
# Watch current directory ./bin/inotify # Watch app directory ./bin/inotify ./app
-
当上述方法不起作用时,最终解决方案:设置
max_request=1,worker_num=1
,使Worker
进程在处理请求后重启。此方法性能非常差,因此仅适用于开发环境
。
获取项目中SwooleServer
的实例。
/** * $swoole is the instance of `Swoole\WebSocket\Server` if enable WebSocket server, otherwise `Swoole\Http\Server` * @var \Swoole\WebSocket\Server|\Swoole\Http\Server $swoole */ $swoole = app('swoole'); var_dump($swoole->stats()); $swoole->push($fd, 'Push WebSocket message');
使用SwooleTable
。
1.定义表,支持多个。
所有定义的表将在Swoole启动前创建。
// in file "config/laravels.php" [ // ... 'swoole_tables' => [ // Scene:bind UserId & FD in WebSocket 'ws' => [// The Key is table name, will add suffix "Table" to avoid naming conflicts. Here defined a table named "wsTable" 'size' => 102400,// The max size 'column' => [// Define the columns ['name' => 'value', 'type' => \Swoole\Table::TYPE_INT, 'size' => 8], ], ], //...Define the other tables ], // ... ];
2.访问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; } // Scene:bind UserId & FD in WebSocket public function onOpen(Server $server, Request $request) { // var_dump(app('swoole') === $server);// The same instance /** * Get the currently logged in user * This feature requires that the path to establish a WebSocket connection go through middleware such as Authenticate. * E.g: * Browser side: var ws = new WebSocket("ws://127.0.0.1:5200/ws"); * Then the /ws route in Laravel needs to add the middleware like Authenticate. * Route::get('/ws', function () { * // Respond any content with status code 200 * return 'websocket'; * })->middleware(['auth']); */ // $user = Auth::user(); // $userId = $user ? $user->id : 0; // 0 means a guest user who is not logged in $userId = mt_rand(1000, 10000); // if (!$userId) { // // Disconnect the connections of unlogged users // $server->disconnect($request->fd); // return; // } $this->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// Bind map uid to fd $this->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// Bind map fd to uid $server->push($request->fd, "Welcome to LaravelS #{$request->fd}"); } public function onMessage(Server $server, Frame $frame) { // Broadcast 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']); // Unbind uid map } $this->wsTable->del('fd:' . $fd);// Unbind fd map $server->push($fd, "Goodbye #{$fd}"); } }
多端口混合协议
有关更多信息,请参阅Swoole Server AddListener。
为了使我们的主服务器支持更多协议,而不仅仅是Http和WebSocket,我们在LaravelS中引入了Swoole的multi-port mixed protocol
特性,并将其命名为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'); } }
这些
Socket
连接与您的HTTP
/WebSocket
连接共享相同的Worker进程。因此,如果您想要发送任务,使用SwooleTable
,甚至是Laravel组件(如DB、Eloquent等),这完全没问题。同时,您可以通过成员属性swoolePort
直接访问Swoole\Server\Port
对象。public function onReceive(Server $server, $fd, $reactorId, $data) { $port = $this->swoolePort; // Get the `Swoole\Server\Port` object }
namespace App\Http\Controllers; class TestController extends Controller { public function test() { /**@var \Swoole\Http\Server|\Swoole\WebSocket\Server $swoole */ $swoole = app('swoole'); // $swoole->ports: Traverse all Port objects, https://www.swoole.co.uk/docs/modules/swoole-server/multiple-ports $port = $swoole->ports[0]; // Get the `Swoole\Server\Port` object, $port[0] is the port of the main server foreach ($port->connections as $fd) { // Traverse all connections // $swoole->send($fd, 'Send tcp message'); // if($swoole->isEstablished($fd)) { // $swoole->push($fd, 'Send websocket message'); // } } } }
-
注册Sockets。
// Edit `config/laravels.php` //... 'sockets' => [ [ 'host' => '127.0.0.1', 'port' => 5291, 'type' => SWOOLE_SOCK_TCP,// Socket type: SWOOLE_SOCK_TCP/SWOOLE_SOCK_TCP6/SWOOLE_SOCK_UDP/SWOOLE_SOCK_UDP6/SWOOLE_UNIX_DGRAM/SWOOLE_UNIX_STREAM 'settings' => [// Swoole settings:https://www.swoole.co.uk/docs/modules/swoole-server-methods#swoole_server-addlistener 'open_eof_check' => true, 'package_eof' => "\r\n", ], 'handler' => \App\Sockets\TestTcpSocket::class, 'enable' => true, // whether to enable, default true ], ],
关于心跳配置,它只能设置在
main server
上,不能在Socket
上配置,但Socket
会继承main server
的心跳配置。对于TCP套接字,当Swoole的
dispatch_mode
为1/3
时,onConnect
和onClose
事件将被阻塞,因此如果您想解除这两个事件的阻塞,请将dispatch_mode
设置为2/4/5
。'swoole' => [ //... 'dispatch_mode' => 2, //... ];
-
测试。
-
TCP:
telnet 127.0.0.1 5291
-
UDP: [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, ], ],
协程
-
警告:协程中的代码执行顺序是乱序的。请求级别的数据应该通过协程ID进行隔离。然而,Laravel/Lumen中有许多单例和静态属性,不同请求之间的数据会相互影响,这是
不安全的
。例如,数据库连接是一个单例,相同的数据库连接共享相同的PDO资源。这在同步阻塞模式下是可以的,但在异步协程模式下不行。每个查询都需要创建不同的连接并维护不同连接的IO状态,这需要一个连接池。 -
不要
启用协程,只有自定义进程可以使用协程。
自定义进程
支持开发者为监控、报告或其他特殊任务创建特殊的工作进程。参阅addProcess。
-
创建实现CustomProcessInterface的Proccess类。
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 { /** * @var bool Quit tag for Reload updates */ private static $quit = false; public static function callback(Server $swoole, Process $process) { // The callback method cannot exit. Once exited, Manager process will automatically create the process while (!self::$quit) { \Log::info('Test process: running'); // sleep(1); // Swoole < 2.1 Coroutine::sleep(1); // Swoole>=2.1: Coroutine & Runtime will be automatically enabled for callback(). // Deliver task in custom process, but NOT support callback finish() of task. // Note: Modify task_ipc_mode to 1 or 2 in config/laravels.php, see https://www.swoole.co.uk/docs/modules/swoole-server/configuration $ret = Task::deliver(new TestTask('task data')); var_dump($ret); // The upper layer will catch the exception thrown in the callback and record it in the Swoole log, and then this process will exit. The Manager process will re-create the process after 3 seconds, so developers need to try/catch to catch the exception by themselves to avoid frequent process creation. // throw new \Exception('an exception'); } } // Requirements: LaravelS >= v3.4.0 & callback() must be async non-blocking program. 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); // Force exit process } // Requirements: LaravelS >= v3.7.4 & callback() must be async non-blocking program. 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); // Force exit process } }
-
注册TestProcess。
// Edit `config/laravels.php` // ... 'processes' => [ 'test' => [ // Key name is process name 'class' => \App\Processes\TestProcess::class, 'redirect' => false, // Whether redirect stdin/stdout, true or false 'pipe' => 0, // The type of pipeline, 0: no pipeline 1: SOCK_STREAM 2: SOCK_DGRAM 'enable' => true, // Whether to enable, default true //'num' => 3 // To create multiple processes of this class, default is 1 //'queue' => [ // Enable message queue as inter-process communication, configure empty array means use default parameters // 'msg_key' => 0, // The key of the message queue. Default: ftok(__FILE__, 1). // 'mode' => 2, // Communication mode, default is 2, which means contention mode // 'capacity' => 8192, // The length of a single message, is limited by the operating system kernel parameters. The default is 8192, and the maximum is 65536 //], //'restart_interval' => 5, // After the process exits abnormally, how many seconds to wait before restarting the process, default 5 seconds ], ],
-
注意:callback() 不能退出。如果退出,管理进程将重新创建进程。
-
示例:向自定义进程写入数据。
// 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
文件。同时,LaravelS
将启动自定义进程apollo
来监控配置,并在配置更改时自动重新加载
。
-
启用 Apollo:将
--enable-apollo
和 Apollo 参数添加到启动参数中。php bin/laravels start --enable-apollo --apollo-server=http://127.0.0.1:8080 --apollo-app-id=LARAVEL-S-TEST
-
支持热更新(可选)。
// Edit `config/laravels.php` 'processes' => Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),
// When there are other custom process configurations 'processes' => [ 'test' => [ 'class' => \App\Processes\TestProcess::class, 'redirect' => false, 'pipe' => 1, ], // ... ] + Hhxsv5\LaravelS\Components\Apollo\Process::getDefinition(),
-
可用参数列表。
Prometheus
支持 Prometheus 监控和警报,使用 Grafana 可视化查看监控指标。请参考 Docker Compose 了解 Prometheus 和 Grafana 的环境构建。
-
需要扩展 APCu >= 5.0.0,请使用
pecl install apcu
安装它。 -
将配置文件
prometheus.php
复制到您的项目config
目录。根据需要修改配置。# Execute commands in the project root directory cp vendor/hhxsv5/laravel-s/config/prometheus.php config/
如果您的项目是
Lumen
,您还需要在bootstrap/app.php
中手动加载配置$app->configure('prometheus');
。 -
配置全局中间件:
Hhxsv5\LaravelS\Components\Prometheus\RequestMiddleware::class
。为了尽可能精确地统计请求时间消耗,RequestMiddleware
必须是第一个全局中间件,需要将其放在其他中间件之前。 -
注册 ServiceProvider:
Hhxsv5\LaravelS\Components\Prometheus\ServiceProvider::class
。 -
在
config/laravels.php
中配置 CollectorProcess 以定期收集 Swoole Worker/Task/Timer 进程的指标。'processes' => Hhxsv5\LaravelS\Components\Prometheus\CollectorProcess::getDefinition(),
-
创建输出指标的路由。
use Hhxsv5\LaravelS\Components\Prometheus\Exporter; Route::get('/actuator/prometheus', function () { $result = app(Exporter::class)->render(); return response($result, 200, ['Content-Type' => Exporter::REDNER_MIME_TYPE]); });
-
完成 Prometheus 的配置并启动它。
global: scrape_interval: 5s scrape_timeout: 5s evaluation_interval: 30s scrape_configs: - job_name: laravel-s-test honor_timestamps: true metrics_path: /actuator/prometheus scheme: http follow_redirects: true static_configs: - targets: - 127.0.0.1:5200 # The ip and port of the monitored service # Dynamically discovered using one of the supported service-discovery mechanisms # https://prometheus.ac.cn/docs/prometheus/latest/configuration/configuration/#scrape_config # - job_name: laravels-eureka # honor_timestamps: true # scrape_interval: 5s # metrics_path: /actuator/prometheus # scheme: http # follow_redirects: true # eureka_sd_configs: # - server: http://127.0.0.1:8080/eureka # follow_redirects: true # refresh_interval: 5s
-
启动 Grafana,然后导入 panel json。
其他功能
配置Swoole事件
支持的事件
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) { // Initialize a global counter (available across processes) $server->atomicCount = new Atomic(2233); // Invoked in controller: 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) { // Initialize a database connection pool // DatabaseConnectionPool::init(); } }
2. 配置。
// Edit `config/laravels.php` 'event_handlers' => [ 'ServerStart' => [\App\Events\ServerStartEvent::class], // Trigger events in array order 'WorkerStart' => [\App\Events\WorkerStartEvent::class], ],
无服务器
阿里云函数计算
函数计算.
1. 修改 bootstrap/app.php
并设置存储目录。因为项目目录是只读的,所以 /tmp
目录只能读写。
$app->useStoragePath(env('APP_STORAGE_PATH', '/tmp/storage'));
2. 创建一个 shell 脚本 laravels_bootstrap
并授予权限。
#!/usr/bin/env bash set +e # Create storage-related directories 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 # Set the environment variable APP_STORAGE_PATH, please make sure it's the same as APP_STORAGE_PATH in .env export APP_STORAGE_PATH=/tmp/storage # Start LaravelS php bin/laravels start
3. 配置 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
中注册它。 -
通过
Middleware
重置单例实例的状态。 -
重新注册
ServiceProvider
,将XxxServiceProvider
添加到laravels.php
文件的register_providers
中。这样可以在每个请求中重新初始化单例实例。参考。
-
清理器
已知问题
已知问题:包含已知问题和解决方案的包。
调试方法
-
日志记录;如果您想输出到控制台,可以使用
stderr
,Log::channel('stderr')->debug('debug message')。 -
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'); // Call getContent() to get the raw POST body, instead of 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' => [ // Enable persistent connection \PDO::ATTR_PERSISTENT => true, ], ], ],
- Redis连接,它
不会
在断开连接后立即自动重连,而是会抛出关于丢失连接的异常,下次重连。您需要确保每次操作Redis之前SELECT DB
正确。
// config/database.php 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), // It is recommended to use phpredis for better performance. 'default' => [ 'host' => env('REDIS_HOST', 'localhost'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => 0, 'persistent' => true, // Enable persistent connection ], ],
关于内存泄露
-
避免使用全局变量。如有必要,请手动清理或重置它们。
-
无限地向
static
/global
变量中添加元素会导致 OOM(内存不足)。class Test { public static $array = []; public static $string = ''; } // Controller public function test(Request $req) { // Out of Memory Test::$array[] = $req->input('param1'); Test::$string .= $req->input('param2'); }
-
内存泄露检测方法
-
修改
config/laravels.php
:worker_num=1, max_request=1000000
,记得测试后恢复设置; -
添加无
route middleware
的路由/debug-memory-leak
以观察Worker
进程的内存变化;
Route::get('/debug-memory-leak', function () { global $previous; $current = memory_get_usage(); $stats = [ 'prev_mem' => $previous, 'curr_mem' => $current, 'diff_mem' => $current - $previous, ]; $previous = $current; return $stats; });
-
启动
LaravelS
并请求/debug-memory-leak
,直到diff_mem
小于或等于零;如果diff_mem
总是大于零,这意味着可能存在Global Middleware
或Laravel Framework
中的内存泄露; -
完成
步骤3
后,交替
请求业务路由和/debug-memory-leak
(建议使用ab
/wrk
对业务路由进行大量请求),内存的初始增加是正常的。在对业务路由进行大量请求后,如果diff_mem
总是大于零且curr_mem
继续增加,则很可能存在内存泄露;如果curr_mem
总是在一定范围内变化而不继续增加,则内存泄露的可能性较低。 -
如果您仍然无法解决问题,max_request 是最后的保障。
-