1 Star 0 Fork 48

levent / laravel-s

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

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

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

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

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.5.0" -vvv
# 确保你的composer.lock文件是在版本控制中

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

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

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

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

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

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

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

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

运行

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

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

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

部署

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

[program:laravel-s-test]
command=/user/local/bin/php /opt/www/laravel-s-test/bin/laravels start -i
numprocs=1
autostart=true
autorestart=true
startretries=3
user=www-data
redirect_stderr=true
stdout_logfile=/opt/www/laravel-s-test/storage/logs/supervisord-stdout.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:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.1.2:5200 backup;
    keepalive 16;
}
server {
    listen 80;
    # 别忘了绑Host哟
    server_name laravels.com;
    root /xxxpath/laravel-s-test/public;
    access_log /yyypath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    # Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }
    # 当请求PHP文件时直接响应404,防止暴露public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}
    location @laravels {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout 120s;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_pass http://swoole;
    }
}

与Apache配合使用

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

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

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

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

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

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

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

启用WebSocket服务器

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

1.创建WebSocket Handler类,并实现接口WebSocketHandlerInterface。start时会自动实例化,不需要手动创建实例。

namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
 * @see https://wiki.swoole.com/wiki/page/400.html
 */
class WebSocketService implements WebSocketHandlerInterface
{
    // 声明没有参数的构造函数
    public function __construct()
    {
    }
    public function onOpen(Server $server, Request $request)
    {
        // 在触发onOpen事件之前Laravel的生命周期已经完结,所以Laravel的Request是可读的,Session是可读写的
        // \Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]);
        $server->push($request->fd, 'Welcome to LaravelS');
        // throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    }
    public function onMessage(Server $server, Frame $frame)
    {
        // \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
        $server->push($frame->fd, date('Y-m-d H:i:s'));
        // throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    }
    public function onClose(Server $server, $fd, $reactorId)
    {
        // throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    }
}

2.更改配置config/laravels.php

// ...
'websocket'      => [
    'enable'  => true, // 看清楚,这里是true
    'handler' => \App\Services\WebSocketService::class,
],
'swoole'         => [
    //...
    // dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/wiki/page/277.html
    'dispatch_mode' => 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:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    #server 192.168.1.2:5200 backup;
    keepalive 16;
}
server {
    listen 80;
    # 别忘了绑Host哟
    server_name laravels.com;
    root /xxxpath/laravel-s-test/public;
    access_log /yyypath/log/nginx/$server_name.access.log  main;
    autoindex off;
    index index.html index.htm;
    # Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    location / {
        try_files $uri @laravels;
    }
    # 当请求PHP文件时直接响应404,防止暴露public/*.php
    #location ~* \.php$ {
    #    return 404;
    #}
    # Http和WebSocket共存,Nginx通过location区分
    # !!! WebSocket连接时路径为/ws
    # Javascript: var ws = new WebSocket("ws://laravels.com/ws");
    location =/ws {
        # proxy_connect_timeout 60s;
        # proxy_send_timeout 60s;
        # proxy_read_timeout:如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接;同时,Swoole的心跳设置也会影响连接的关闭
        # proxy_read_timeout 60s;
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Real-PORT $remote_port;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header Server-Protocol $server_protocol;
        proxy_set_header Server-Name $server_name;
        proxy_set_header Server-Addr $server_addr;
        proxy_set_header Server-Port $server_port;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://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;

监听事件

系统事件

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

  • 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
{
    private $data;
    public function __construct($data)
    {
        $this->data = $data;
    }
    public function getData()
    {
        return $this->data;
    }
}

2.创建监听器类。

use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Task\Event;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
class TestListener1 extends Listener
{
    // 声明没有参数的构造函数
    public function __construct()
    {
    }
    public function handle(Event $event)
    {
        \Log::info(__CLASS__ . ':handle start', [$event->getData()]);
        sleep(2);// 模拟一些慢速的事件处理
        // 监听器中也可以投递Task,但不支持Task的finish()回调。
        // 注意:
        // 1.参数2需传true
        // 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
        $ret = Task::deliver(new TestTask('task data'), true);
        var_dump($ret);
        // throw new \Exception('an exception');// handle时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    }
}

3.绑定事件与监听器。

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

4.触发事件。

// 实例化TestEvent并通过fire触发,此操作是异步的,触发后立即返回,由Task进程继续处理监听器中的handle逻辑
use Hhxsv5\LaravelS\Swoole\Task\Event;
$success = Event::fire(new TestEvent('event data'));
var_dump($success);//判断是否触发成功

异步的任务队列

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

1.创建任务类。

use Hhxsv5\LaravelS\Swoole\Task\Task;
class TestTask extends Task
{
    private $data;
    private $result;
    public function __construct($data)
    {
        $this->data = $data;
    }
    // 处理任务的逻辑,运行在Task进程中,不能投递任务
    public function handle()
    {
        \Log::info(__CLASS__ . ':handle start', [$this->data]);
        sleep(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秒投放任务
$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(); // 终止此任务
            // CronJob中也可以投递Task,但不支持Task的finish()回调。
            // 注意:
            // 1.参数2需传true
            // 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
            $ret = Task::deliver(new TestTask('task data'), true);
            var_dump($ret);
        }
        // throw new \Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    }
}

2.注册定时任务类。

// 在"config/laravels.php"注册定时任务类
[
    // ...
    'timer'          => [
        'enable' => true, // 启用Timer
        'jobs'   => [ // 注册的定时任务类列表
            // 启用LaravelScheduleJob来执行`php artisan schedule:run`,每分钟一次,替代Linux Crontab
            // \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
            // 两种配置参数的方式:
            // [\App\Jobs\Timer\TestCronJob::class, [1000, true]], // 注册时传入参数
            \App\Jobs\Timer\TestCronJob::class, // 重载对应的方法来返回参数
        ],
        'max_wait_time' => 5, // Reload时最大等待时间
    ],
    // ...
];

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

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

修改代码后自动Reload

  • 基于inotify,仅支持Linux。

    1.安装inotify扩展。

    2.开启配置项

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

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

    1.安装fswatch

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

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

    1.安装inotify-tools

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

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

在你的项目中使用SwooleServer实例

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

使用SwooleTable

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

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

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

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

use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;

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

多端口混合协议

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

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

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

    namespace App\Sockets;
    use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket;
    use Swoole\Server;
    class TestTcpSocket extends TcpSocket
    {
        public function onConnect(Server $server, $fd, $reactorId)
        {
            \Log::info('New TCP connection', [$fd]);
            $server->send($fd, 'Welcome to LaravelS.');
        }
        public function onReceive(Server $server, $fd, $reactorId, $data)
        {
            \Log::info('Received data', [$fd, $data]);
            $server->send($fd, 'LaravelS: ' . $data);
            if ($data === "quit\r\n") {
                $server->send($fd, 'LaravelS: bye' . PHP_EOL);
                $server->close($fd);
            }
        }
        public function onClose(Server $server, $fd, $reactorId)
        {
            \Log::info('Close TCP connection', [$fd]);
            $server->send($fd, 'Goodbye');
        }
    }

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

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

    // 修改文件 config/laravels.php
    // ...
    'sockets' => [
        [
            'host'     => '127.0.0.1',
            'port'     => 5291,
            'type'     => SWOOLE_SOCK_TCP,// 支持的嵌套字类型:https://wiki.swoole.com/wiki/page/16.html#entry_h2_0
            'settings' => [// Swoole可用的配置项:https://wiki.swoole.com/wiki/page/526.html
                'open_eof_check' => true,
                'package_eof'    => "\r\n",
            ],
            'handler'  => \App\Sockets\TestTcpSocket::class,
        ],
    ],

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

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

    '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
    {
        public static function getName()
        {
            // 进程名称
            return 'test';
        }
        public static function callback(Server $swoole, Process $process)
        {
            // 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
            \Log::info(__METHOD__, [posix_getpid(), $swoole->stats()]);
            while (true) {
                \Log::info('Do something');
                // sleep(1); // Swoole < 2.1
                Coroutine::sleep(1); // Swoole>=2.1 callback()方法已自动创建了协程。
                // 自定义进程中也可以投递Task,但不支持Task的finish()回调。
                // 注意:
                // 1.参数2需传true
                // 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
                $ret = Task::deliver(new TestTask('task data'), true);
                var_dump($ret);
                // 上层会捕获callback中抛出的异常,并记录到Swoole日志,如果异常数达到10次,此进程会退出,Manager进程会重新创建进程,所以建议开发者自行try/catch捕获,避免创建进程过于频繁。
                // throw new \Exception('an exception');
            }
        }
        // 要求:LaravelS >= v3.4.0 并且 callback() 必须是异步非阻塞程序。
        public static function onReload(Server $swoole, Process $process)
        {
            // Stop the process...
            // Then end process
            $process->exit(0);
        }
    }
  2. 注册TestProcess。

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

其他特性

配置Swoole的事件回调函数

支持的事件列表:

事件 需实现的接口 发生时机
BeforeStart Hhxsv5\LaravelS\Swoole\Events\BeforeStartInterface 发生在Master进程启动之前,此事件中不应处理复杂的业务逻辑,只能做一些初始化的简单工作
WorkerStart Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface 发生在Worker/Task进程启动时,并且已经完成Laravel初始化
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\BeforeStartInterface;
use Swoole\Atomic;
use Swoole\Http\Server;
class BeforeStartEvent implements BeforeStartInterface
{
    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' => [
    'BeforeStart' => \App\Events\BeforeStartEvent::class,
    'WorkerStart' => \App\Events\WorkerStartEvent::class,
],

注意事项

  • 单例问题

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

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

    • 常见的解决方案:

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

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

      3. 如果是以ServiceProvider注册的单例对象,可添加该ServiceProviderlaravels.phpregister_providers中,这样每次请求会重新注册该ServiceProvider,重新实例化单例对象,参考

    • LaravelS 已经内置了一些Cleaner

  • 常见问题:一揽子的已知问题和解决方案。

  • 调试方式:记录日志、Laravel Dump Server(Laravel 5.7已默认集成)

  • 应通过Illuminate\Http\Request对象来获取请求信息,$_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' => [
            '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内核参数调整

  • 压力测试

用户与案例

  • KuCoin

  • 医联:WEB站、M站、APP、小程序的账户体系服务。

    医联
  • ITOK在线客服平台:用户IT工单的处理跟踪及在线实时沟通。

    ITOK在线客服平台
  • 盟呱呱

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

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

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

    修机匠手机上门维修服务
  • 亿健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

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/levent123_admin/laravel-s.git
git@gitee.com:levent123_admin/laravel-s.git
levent123_admin
laravel-s
laravel-s
master

搜索帮助

14c37bed 8189591 565d56ea 8189591