1 Star 0 Fork 48

hackcat / laravel-s

forked from Xie Biao / laravel-s 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MIT
 _                               _  _____ 
| |                             | |/ ____|
| |     __ _ _ __ __ ___   _____| | (___  
| |    / _` | '__/ _` \ \ / / _ \ |\___ \ 
| |___| (_| | | | (_| |\ V /  __/ |____) |
|______\__,_|_|  \__,_| \_/ \___|_|_____/ 
                                           

🚀 LaravelS 是 Swoole 和 Laravel/Lumen 之间开箱即用的适配器

Watch此仓库,以获得最新的更新。

Latest Stable Version Latest Unstable Version Total Downloads License Build Status Code Intelligence Status

English Documentation

QQ交流群

  • 群1:698480528(已满) 点击加群
  • 群2:62075835 点击加群

Table of Contents

特性

要求

依赖 说明
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.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"
stop 停止LaravelS,并触发自定义进程的onStop方法
restart 重启LaravelS:先平滑Stop,然后再Start;在Start完成之前,服务是不可用的
reload 平滑重启所有Task/Worker/Timer进程(这些进程内包含了你的业务代码),并触发自定义进程的onReload方法,不会重启Master/Manger进程;修改config/laravels.php后,你只有调用restart来完成重启
info 显示组件的版本信息
help 显示帮助信息
  • 启动选项,针对startrestart命令。
选项 说明
-d|--daemonize 以守护进程的方式运行,此选项将覆盖laravels.phpswoole.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.conf LaravelS的运行时配置文件
storage/laravels.pid Master进程的PID文件
storage/laravels-timer-process.pid 定时器Timer进程的PID文件
storage/laravels-custom-processes.pid 所有自定义进程的PID文件

部署

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

[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\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
 * @see https://wiki.swoole.com/#/start/start_ws_server
 */
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/#/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_requestSwoole\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
{
    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);
        // throw new \Exception('an exception');// handle时抛出的异常上层会忽略,并记录到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);// 判断是否触发成功

异步的任务队列

此特性依赖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(2);// 模拟一些慢速的事件处理
        // throw new \Exception('an exception');// handle时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
        $this->result = 'the result of ' . $this->data;
    }
    // 可选的,完成事件,任务处理完后的逻辑,运行在Worker进程中,可以投递任务
    public function finish()
    {
        \Log::info(__CLASS__ . ':finish start', [$this->result]);
        Task::deliver(new TestTask2('task2')); // 投递其他任务
    }
}

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的毫秒定时器,封装的定时任务,取代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 = 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);
        }
        // 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' => 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());// 单例

使用SwooleTable

1.定义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增加监听的端口多端口混合协议

为了使我们的主服务器能支持除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`对象
    }
    namespace App\Http\Controllers;
    class TestController extends Controller
    {
        public function test()
        {
            /**@var \Swoole\Http\Server $swoole */
            $swoole = app('swoole');
            // $swoole->ports:遍历所有Port对象,https://wiki.swoole.com/#/server/properties?id=ports
            $port = $swoole->ports[0]; // 获得`Swoole\Server\Port`对象
            // $fd = 1; // Port中onReceive/onMessage回调的FD
            // $swoole->send($fd, 'Send tcp message from controller to port client');
            // $swoole->push($fd, 'Send websocket message from controller to port client');
        }
    }
  2. 注册套接字。

    // 修改文件 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,
        //...
    ];
  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'     => 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

  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
    {
        /**
         * @var bool 退出标记,用于Reload更新
         */
        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); // 强制退出进程
        }
    }
  2. 注册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秒
        ],
    ],
  3. 注意:callback()方法不能退出,如果退出,Manager进程将会重新创建进程。

  4. 示例:向自定义进程中写数据。

    // 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

  1. 启用Apollo组件:启动参数加上--enable-apollo以及Apollo的配置参数。

    php bin/laravels start --enable-apollo --apollo-server=http://127.0.0.1:8080 --apollo-app-id=LARAVEL-S-TEST
  2. 配置热更新(可选的)。

    // 修改文件 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(),
  3. 可用的参数列表。

参数名 描述 默认值 示例
apollo-server Apollo服务器URL - --apollo-server=http://127.0.0.1:8080
apollo-app-id Apollo应用ID - --apollo-app-id=LARAVEL-S-TEST
apollo-namespaces APP所属的命名空间,可指定多个 application --apollo-namespaces=application --apollo-namespaces=env
apollo-cluster APP所属的集群 default --apollo-cluster=default
apollo-client-ip 当前实例的IP,还可用于灰度发布 本机内网IP --apollo-client-ip=10.2.1.83
apollo-pull-timeout 拉取配置时的超时时间(秒) 5 --apollo-pull-timeout=5
apollo-backup-old-env 更新配置文件.env时是否备份老的配置文件 false --apollo-backup-old-env

其他特性

配置Swoole的事件回调函数

支持的事件列表:

事件 需实现的接口 发生时机
ServerStart Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface 发生在Master进程启动时,此事件中不应处理复杂的业务逻辑,只能做一些初始化的简单工作
ServerStop Hhxsv5\LaravelS\Swoole\Events\ServerStopInterface 发生在Server正常退出时,此事件中不能使用异步或协程相关的API
WorkerStart Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface 发生在Worker/Task进程启动完成后
WorkerStop Hhxsv5\LaravelS\Swoole\Events\WorkerStopInterface 发生在Worker/Task进程正常退出后
WorkerError Hhxsv5\LaravelS\Swoole\Events\WorkerErrorInterface 发生在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],
],

注意事项

  • 单例问题

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

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

    • 常见的解决方案:

      1. 写一个XxxCleaner清理器类来清理单例对象状态,此类需实现接口Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然后注册到laravels.phpcleaners中。

      2. 用一个中间件重置单例对象的状态。

      3. 如果是以ServiceProvider注册的单例对象,可添加该ServiceProviderlaravels.phpregister_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');
    }
  • 各种单例的连接将被常驻内存,建议开启持久连接

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

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

    // 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工单的处理跟踪及在线实时沟通。

    ITOK在线客服平台
  • 盟呱呱

    盟呱呱
  • WookTeam:WookTeam是一款轻量级的在线团队协作工具,提供各类文档工具、在线思维导图、在线流程图、项目管理、任务分发,知识库管理等工具。

  • 微信公众号-广州塔:活动、商城

    广州塔
  • 企鹅游戏盒子、明星新势力、以及小程序广告服务

    企鹅游戏盒子
  • 小程序-修机匠手机上门维修服务:手机维修服务,提供上门服务,支持在线维修。

    修机匠手机上门维修服务
  • 亿健APP

其他选择

赞助

您的支持是我们坚持的最大动力。

赞助

感谢

支持者 金额(元)
*思勇 18.88
*德国 18.88
魂之挽歌 100
小南瓜 10.01
*丁智 16.66
匿名 20
匿名 20
*洋 Blues 18.88
*钧泽 Panda 10.24
*翔 海贼王路飞 12
*跃 Axiong 10
落伽 10
很胖的胖子 15
霹格软件 18.88
Bygones 18.88
*春 Flymoo 100
异乡人 20
only丶妳 100
月殇 18.88
Shmily 20
*俊 20
*哲 20
Alex 20
X 20
*洋 20
*洋 20
*强 50
Anthony 18.88
*官龙 100
0o飞舞o0木木 *科 288
*勇 66.66
果酱社区 1076

License

MIT

MIT License Copyright (c) 2018 XieBiao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

简介

LaravelS 是 Swoole 和 Laravel/Lumen 之间开箱即用的适配器。 展开 收起
PHP
MIT
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
PHP
1
https://gitee.com/hackcat_admin/laravel-s.git
git@gitee.com:hackcat_admin/laravel-s.git
hackcat_admin
laravel-s
laravel-s
master

搜索帮助

14c37bed 8189591 565d56ea 8189591