• LaravelS


    LaravelS

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

    特性

    要求

    依赖说明
    PHP >= 5.5.9 推荐PHP7+
    Swoole >= 1.7.19 从2.0.12开始不再支持PHP5 推荐4.2.3+
    Laravel/Lumen >= 5.1 推荐5.6+

    安装

    1.通过Composer安装(packagist)。有可能找不到3.0版本,解决方案移步#81

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

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

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

      'providers' => [
          //...
          Hhxsv5LaravelSIlluminateLaravelSServiceProvider::class,
      ],
    • Lumen: 修改文件bootstrap/app.php

      $app->register(Hhxsv5LaravelSIlluminateLaravelSServiceProvider::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配合使用

     1 LoadModule proxy_module /yyypath/modules/mod_deflate.so
     2 <IfModule deflate_module>
     3     SetOutputFilter DEFLATE
     4     DeflateCompressionLevel 2
     5     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
     6 </IfModule>
     7 
     8 <VirtualHost *:80>
     9     # 别忘了绑Host哟
    10     ServerName www.laravels.com
    11     ServerAdmin hhxsv5@sina.com
    12 
    13     DocumentRoot /xxxpath/laravel-s-test/public;
    14     DirectoryIndex index.html index.htm
    15     <Directory "/">
    16         AllowOverride None
    17         Require all granted
    18     </Directory>
    19 
    20     LoadModule proxy_module /yyypath/modules/mod_proxy.so
    21     LoadModule proxy_module /yyypath/modules/mod_proxy_balancer.so
    22     LoadModule proxy_module /yyypath/modules/mod_lbmethod_byrequests.so.so
    23     LoadModule proxy_module /yyypath/modules/mod_proxy_http.so.so
    24     LoadModule proxy_module /yyypath/modules/mod_slotmem_shm.so
    25     LoadModule proxy_module /yyypath/modules/mod_rewrite.so
    26 
    27     ProxyRequests Off
    28     ProxyPreserveHost On
    29     <Proxy balancer://laravels>  
    30         BalancerMember http://192.168.1.1:5200 loadfactor=7
    31         #BalancerMember http://192.168.1.2:5200 loadfactor=3
    32         #BalancerMember http://192.168.1.3:5200 loadfactor=1 status=+H
    33         ProxySet lbmethod=byrequests
    34     </Proxy>
    35     #ProxyPass / balancer://laravels/
    36     #ProxyPassReverse / balancer://laravels/
    37 
    38     # Apache处理静态资源,LaravelS处理动态资源。
    39     RewriteEngine On
    40     RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
    41     RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
    42     RewriteRule ^/(.*)$ balancer://laravels/%{REQUEST_URI} [P,L]
    43 
    44     ErrorLog ${APACHE_LOG_DIR}/www.laravels.com.error.log
    45     CustomLog ${APACHE_LOG_DIR}/www.laravels.com.access.log combined
    46 </VirtualHost>

    启用WebSocket服务器

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

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

     1 namespace AppServices;
     2 use Hhxsv5LaravelSSwooleWebSocketHandlerInterface;
     3 use SwooleHttpRequest;
     4 use SwooleWebSocketFrame;
     5 use SwooleWebSocketServer;
     6 /**
     7  * @see https://wiki.swoole.com/wiki/page/400.html
     8  */
     9 class WebSocketService implements WebSocketHandlerInterface
    10 {
    11     // 声明没有参数的构造函数
    12     public function __construct()
    13     {
    14     }
    15     public function onOpen(Server $server, Request $request)
    16     {
    17         // 在触发onOpen事件之前,建立WebSocket的HTTP请求已经经过了Laravel的路由,
    18         // 所以Laravel的Request、Auth等信息是可读的,Session是可读写的,但仅限在onOpen事件中。
    19         // Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]);
    20         $server->push($request->fd, 'Welcome to LaravelS');
    21         // throw new Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    22     }
    23     public function onMessage(Server $server, Frame $frame)
    24     {
    25         // Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
    26         $server->push($frame->fd, date('Y-m-d H:i:s'));
    27         // throw new Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    28     }
    29     public function onClose(Server $server, $fd, $reactorId)
    30     {
    31         // throw new Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    32     }
    33 }

    2.更改配置config/laravels.php

     1 // ...
     2 'websocket'      => [
     3     'enable'  => true, // 看清楚,这里是true
     4     'handler' => AppServicesWebSocketService::class,
     5 ],
     6 'swoole'         => [
     7     //...
     8     // dispatch_mode只能设置为2、4、5,https://wiki.swoole.com/wiki/page/277.html
     9     'dispatch_mode' => 2,
    10     //...
    11 ],
    12 // ...

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

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

    参考 WebSocket代理
     1 map $http_upgrade $connection_upgrade {
     2     default upgrade;
     3     ''      close;
     4 }
     5 upstream swoole {
     6     # 通过 IP:Port 连接
     7     server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
     8     # 通过 UnixSocket Stream 连接,小诀窍:将socket文件放在/dev/shm目录下,可获得更好的性能
     9     #server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
    10     #server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
    11     #server 192.168.1.2:5200 backup;
    12     keepalive 16;
    13 }
    14 server {
    15     listen 80;
    16     # 别忘了绑Host哟
    17     server_name laravels.com;
    18     root /xxxpath/laravel-s-test/public;
    19     access_log /yyypath/log/nginx/$server_name.access.log  main;
    20     autoindex off;
    21     index index.html index.htm;
    22     # Nginx处理静态资源(建议开启gzip),LaravelS处理动态资源。
    23     location / {
    24         try_files $uri @laravels;
    25     }
    26     # 当请求PHP文件时直接响应404,防止暴露public/*.php
    27     #location ~* .php$ {
    28     #    return 404;
    29     #}
    30     # Http和WebSocket共存,Nginx通过location区分
    31     # !!! WebSocket连接时路径为/ws
    32     # Javascript: var ws = new WebSocket("ws://laravels.com/ws");
    33     location =/ws {
    34         # proxy_connect_timeout 60s;
    35         # proxy_send_timeout 60s;
    36         # proxy_read_timeout:如果60秒内被代理的服务器没有响应数据给Nginx,那么Nginx会关闭当前连接;同时,Swoole的心跳设置也会影响连接的关闭
    37         # proxy_read_timeout 60s;
    38         proxy_http_version 1.1;
    39         proxy_set_header X-Real-IP $remote_addr;
    40         proxy_set_header X-Real-PORT $remote_port;
    41         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    42         proxy_set_header Host $http_host;
    43         proxy_set_header Scheme $scheme;
    44         proxy_set_header Server-Protocol $server_protocol;
    45         proxy_set_header Server-Name $server_name;
    46         proxy_set_header Server-Addr $server_addr;
    47         proxy_set_header Server-Port $server_port;
    48         proxy_set_header Upgrade $http_upgrade;
    49         proxy_set_header Connection $connection_upgrade;
    50         proxy_pass http://swoole;
    51     }
    52     location @laravels {
    53         # proxy_connect_timeout 60s;
    54         # proxy_send_timeout 60s;
    55         # proxy_read_timeout 60s;
    56         proxy_http_version 1.1;
    57         proxy_set_header Connection "";
    58         proxy_set_header X-Real-IP $remote_addr;
    59         proxy_set_header X-Real-PORT $remote_port;
    60         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    61         proxy_set_header Host $http_host;
    62         proxy_set_header Scheme $scheme;
    63         proxy_set_header Server-Protocol $server_protocol;
    64         proxy_set_header Server-Name $server_name;
    65         proxy_set_header Server-Addr $server_addr;
    66         proxy_set_header Server-Port $server_port;
    67         proxy_pass http://swoole;
    68     }
    69 }

    5.心跳配置

    • Swoole的心跳配置

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

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

    监听事件

    系统事件

    通常,你可以在这些事件中重置或销毁一些全局或静态的变量,也可以修改当前的请求和响应。
    • laravels.received_request 将SwooleHttpRequest转成IlluminateHttpRequest后,在Laravel内核处理请求前。

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

      1 // 修改`app/Providers/EventServiceProvider.php`, 添加下面监听代码到boot方法中
      2 // 如果变量$events不存在,你也可以通过Facade调用Event::listen()。
      3 $events->listen('laravels.generated_response', function (IlluminateHttpRequest $req, SymfonyComponentHttpFoundationResponse $rsp, $app) {
      4     $rsp->headers->set('header-key', 'hhxsv5');// 修改header
      5 });

    自定义的异步事件

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

    1.创建事件类。

     1 use Hhxsv5LaravelSSwooleTaskEvent;
     2 class TestEvent extends Event
     3 {
     4     private $data;
     5     public function __construct($data)
     6     {
     7         $this->data = $data;
     8     }
     9     public function getData()
    10     {
    11         return $this->data;
    12     }
    13 }

    2.创建监听器类。

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

    3.绑定事件与监听器。

     1 // 在"config/laravels.php"中绑定事件与监听器,一个事件可以有多个监听器,多个监听器按顺序执行
     2 [
     3     // ...
     4     'events' => [
     5         AppTasksTestEvent::class => [
     6             AppTasksTestListener1::class,
     7             //AppTasksTestListener2::class,
     8         ],
     9     ],
    10     // ...
    11 ];

    4.触发事件。

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

    异步的任务队列

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

    1.创建任务类。

     1 use Hhxsv5LaravelSSwooleTaskTask;
     2 class TestTask extends Task
     3 {
     4     private $data;
     5     private $result;
     6     public function __construct($data)
     7     {
     8         $this->data = $data;
     9     }
    10     // 处理任务的逻辑,运行在Task进程中,不能投递任务
    11     public function handle()
    12     {
    13         Log::info(__CLASS__ . ':handle start', [$this->data]);
    14         sleep(2);// 模拟一些慢速的事件处理
    15         // throw new Exception('an exception');// handle时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    16         $this->result = 'the result of ' . $this->data;
    17     }
    18     // 可选的,完成事件,任务处理完后的逻辑,运行在Worker进程中,可以投递任务
    19     public function finish()
    20     {
    21         Log::info(__CLASS__ . ':finish start', [$this->result]);
    22         Task::deliver(new TestTask2('task2')); // 投递其他任务
    23     }
    24 }

    2.投递任务。

    1 // 实例化TestTask并通过deliver投递,此操作是异步的,投递后立即返回,由Task进程继续处理TestTask中的handle逻辑
    2 use Hhxsv5LaravelSSwooleTaskTask;
    3 $task = new TestTask('task data');
    4 // $task->delay(3);// 延迟3秒投放任务
    5 $ret = Task::deliver($task);
    6 var_dump($ret);//判断是否投递成功

    毫秒级定时任务

    基于Swoole的毫秒定时器,封装的定时任务,取代LinuxCrontab

    1.创建定时任务类。

     1 namespace AppJobsTimer;
     2 use AppTasksTestTask;
     3 use SwooleCoroutine;
     4 use Hhxsv5LaravelSSwooleTaskTask;
     5 use Hhxsv5LaravelSSwooleTimerCronJob;
     6 class TestCronJob extends CronJob
     7 {
     8     protected $i = 0;
     9     // !!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):一是重载对应的方法,二是注册定时任务时传入参数。
    10     // --- 重载对应的方法来返回配置:开始
    11     public function interval()
    12     {
    13         return 1000;// 每1秒运行一次
    14     }
    15     public function isImmediate()
    16     {
    17         return false;// 是否立即执行第一次,false则等待间隔时间后执行第一次
    18     }
    19     // --- 重载对应的方法来返回配置:结束
    20     public function run()
    21     {
    22         Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
    23         // do something
    24         // sleep(1); // Swoole < 2.1
    25         Coroutine::sleep(1); // Swoole>=2.1 run()方法已自动创建了协程。
    26         $this->i++;
    27         Log::info(__METHOD__, ['end', $this->i, microtime(true)]);
    28 
    29         if ($this->i >= 10) { // 运行10次后不再执行
    30             Log::info(__METHOD__, ['stop', $this->i, microtime(true)]);
    31             $this->stop(); // 终止此任务
    32             // CronJob中也可以投递Task,但不支持Task的finish()回调。
    33             // 注意:
    34             // 1.参数2需传true
    35             // 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
    36             $ret = Task::deliver(new TestTask('task data'), true);
    37             var_dump($ret);
    38         }
    39         // throw new Exception('an exception');// 此时抛出的异常上层会忽略,并记录到Swoole日志,需要开发者try/catch捕获处理
    40     }
    41 }

    2.注册定时任务类。

     1 // 在"config/laravels.php"注册定时任务类
     2 [
     3     // ...
     4     'timer'          => [
     5         'enable' => true, // 启用Timer
     6         'jobs'   => [ // 注册的定时任务类列表
     7             // 启用LaravelScheduleJob来执行`php artisan schedule:run`,每分钟一次,替代Linux Crontab
     8             // Hhxsv5LaravelSIlluminateLaravelScheduleJob::class,
     9             // 两种配置参数的方式:
    10             // [AppJobsTimerTestCronJob::class, [1000, true]], // 注册时传入参数
    11             AppJobsTimerTestCronJob::class, // 重载对应的方法来返回参数
    12         ],
    13         'max_wait_time' => 5, // Reload时最大等待时间
    14     ],
    15     // ...
    16 ];

    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是`SwooleWebSocketServer`的实例,否则是是`SwooleHttpServer`的实例
     * @var SwooleWebSocketServer|SwooleHttpServer $swoole
     */
    $swoole = app('swoole');
    var_dump($swoole->stats());// 单例

    使用SwooleTable

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

    Swoole启动之前会创建定义的所有Table。
     1 // 在"config/laravels.php"配置
     2 [
     3     // ...
     4     'swoole_tables'  => [
     5         // 场景:WebSocket中UserId与FD绑定
     6         'ws' => [// Key为Table名称,使用时会自动添加Table后缀,避免重名。这里定义名为wsTable的Table
     7             'size'   => 102400,//Table的最大行数
     8             'column' => [// Table的列定义
     9                 ['name' => 'value', 'type' => SwooleTable::TYPE_INT, 'size' => 8],
    10             ],
    11         ],
    12         //...继续定义其他Table
    13     ],
    14     // ...
    15 ];

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

     1 namespace AppServices;
     2 use Hhxsv5LaravelSSwooleWebsocketHandlerInterface;
     3 use SwooleHttpRequest;
     4 use SwooleWebSocketFrame;
     5 use SwooleWebSocketServer;
     6 class WebSocketService implements WebSocketHandlerInterface
     7 {
     8     /**@var SwooleTable $wsTable */
     9     private $wsTable;
    10     public function __construct()
    11     {
    12         $this->wsTable = app('swoole')->wsTable;
    13     }
    14     // 场景:WebSocket中UserId与FD绑定
    15     public function onOpen(Server $server, Request $request)
    16     {
    17         // var_dump(app('swoole') === $server);// 同一实例
    18         /**
    19          * 获取当前登录的用户
    20          * 此特性要求建立WebSocket连接的路径要经过Authenticate之类的中间件。
    21          * 例如:
    22          * 浏览器端:var ws = new WebSocket("ws://127.0.0.1:5200/ws");
    23          * 那么Laravel中/ws路由就需要加上类似Authenticate的中间件。
    24          */
    25         // $user = Auth::user();
    26         // $userId = $user ? $user->id : 0; // 0 表示未登录的访客用户
    27         $userId = mt_rand(1000, 10000);
    28         $this->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// 绑定uid到fd的映射
    29         $this->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// 绑定fd到uid的映射
    30         $server->push($request->fd, "Welcome to LaravelS #{$request->fd}");
    31     }
    32     public function onMessage(Server $server, Frame $frame)
    33     {
    34         // 广播
    35         foreach ($this->wsTable as $key => $row) {
    36             if (strpos($key, 'uid:') === 0 && $server->isEstablished($row['value'])) {
    37                 $content = sprintf('Broadcast: new message "%s" from #%d', $frame->data, $frame->fd);
    38                 $server->push($row['value'], $content);
    39             }
    40         }
    41     }
    42     public function onClose(Server $server, $fd, $reactorId)
    43     {
    44         $uid = $this->wsTable->get('fd:' . $fd);
    45         if ($uid !== false) {
    46             $this->wsTable->del('uid:' . $uid['value']); // 解绑uid映射
    47         }
    48         $this->wsTable->del('fd:' . $fd);// 解绑fd映射
    49         $server->push($fd, "Goodbye #{$fd}");
    50     }
    51 }

    多端口混合协议

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

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

    1. 创建Socket处理类,继承Hhxsv5LaravelSSwooleSocket{TcpSocket|UdpSocket|Http|WebSocket}

       1 namespace AppSockets;
       2 use Hhxsv5LaravelSSwooleSocketTcpSocket;
       3 use SwooleServer;
       4 class TestTcpSocket extends TcpSocket
       5 {
       6     public function onConnect(Server $server, $fd, $reactorId)
       7     {
       8         Log::info('New TCP connection', [$fd]);
       9         $server->send($fd, 'Welcome to LaravelS.');
      10     }
      11     public function onReceive(Server $server, $fd, $reactorId, $data)
      12     {
      13         Log::info('Received data', [$fd, $data]);
      14         $server->send($fd, 'LaravelS: ' . $data);
      15         if ($data === "quit
      ") {
      16             $server->send($fd, 'LaravelS: bye' . PHP_EOL);
      17             $server->close($fd);
      18         }
      19     }
      20     public function onClose(Server $server, $fd, $reactorId)
      21     {
      22         Log::info('Close TCP connection', [$fd]);
      23         $server->send($fd, 'Goodbye');
      24     }
      25 }

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

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

       1 // 修改文件 config/laravels.php
       2 // ...
       3 'sockets' => [
       4     [
       5         'host'     => '127.0.0.1',
       6         'port'     => 5291,
       7         'type'     => SWOOLE_SOCK_TCP,// 支持的嵌套字类型:https://wiki.swoole.com/wiki/page/16.html#entry_h2_0
       8         'settings' => [// Swoole可用的配置项:https://wiki.swoole.com/wiki/page/526.html
       9             'open_eof_check' => true,
      10             'package_eof'    => "
      ",
      11         ],
      12         'handler'  => AppSocketsTestTcpSocket::class,
      13     ],
      14 ],

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

      对于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'    => "
      ",
              ],
              'handler'  => AppSocketsTestUdpSocket::class,
          ],
      ],
      • Http
       1 'sockets' => [
       2     [
       3         'host'     => '0.0.0.0',
       4         'port'     => 5293,
       5         'type'     => SWOOLE_SOCK_TCP,
       6         'settings' => [
       7             'open_http_protocol' => true,
       8         ],
       9         'handler'  => AppSocketsTestHttp::class,
      10     ],
      11 ],
      • WebSocket:主服务器必须开启WebSocket,即需要将websocket.enable置为true
       1 'sockets' => [
       2     [
       3         'host'     => '0.0.0.0',
       4         'port'     => 5294,
       5         'type'     => SWOOLE_SOCK_TCP,
       6         'settings' => [
       7             'open_http_protocol'      => true,
       8             'open_websocket_protocol' => true,
       9         ],
      10         'handler'  => AppSocketsTestWebSocket::class,
      11     ],
      12 ],
      13 协程
    Swoole原始文档
    • 警告:协程下代码执行顺序是乱序的,请求级的数据应该以协程ID隔离,但Laravel/Lumen中存在很多单例、静态属性,不同请求间的数据会相互影响,这是不安全的。比如数据库连接就是单例,同一个数据库连接共享同一个PDO资源,这在同步阻塞模式下是没问题的,但在异步协程下是不行的,每次查询需要创建不同的连接,维护不同的IO状态,这就需要用到连接池。所以不要打开协程,仅自定义进程中可使用协程。
    • 启用协程,默认是关闭的。

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

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

    自定义进程

    支持开发者创建一些特殊的工作进程,用于监控、上报或者其他特殊的任务,参考addProcess
    1. 创建Proccess类,实现CustomProcessInterface接口。

       1 namespace AppProcesses;
       2 use AppTasksTestTask;
       3 use Hhxsv5LaravelSSwooleProcessCustomProcessInterface;
       4 use Hhxsv5LaravelSSwooleTaskTask;
       5 use SwooleCoroutine;
       6 use SwooleHttpServer;
       7 use SwooleProcess;
       8 class TestProcess implements CustomProcessInterface
       9 {
      10     public static function getName()
      11     {
      12         // 进程名称
      13         return 'test';
      14     }
      15     public static function callback(Server $swoole, Process $process)
      16     {
      17         // 进程运行的代码,不能退出,一旦退出Manager进程会自动再次创建该进程。
      18         Log::info(__METHOD__, [posix_getpid(), $swoole->stats()]);
      19         while (true) {
      20             Log::info('Do something');
      21             // sleep(1); // Swoole < 2.1
      22             Coroutine::sleep(1); // Swoole>=2.1 callback()方法已自动创建了协程。
      23             // 自定义进程中也可以投递Task,但不支持Task的finish()回调。
      24             // 注意:
      25             // 1.参数2需传true
      26             // 2.config/laravels.php中修改配置task_ipc_mode为1或2,参考 https://wiki.swoole.com/wiki/page/296.html
      27             $ret = Task::deliver(new TestTask('task data'), true);
      28             var_dump($ret);
      29             // 上层会捕获callback中抛出的异常,并记录到Swoole日志,如果异常数达到10次,此进程会退出,Manager进程会重新创建进程,所以建议开发者自行try/catch捕获,避免创建进程过于频繁。
      30             // throw new Exception('an exception');
      31         }
      32     }
      33     // 要求:LaravelS >= v3.4.0 并且 callback() 必须是异步非阻塞程序。
      34     public static function onReload(Server $swoole, Process $process)
      35     {
      36         // Stop the process...
      37         // Then end process
      38         $process->exit(0);
      39     }
      40 }
    2. 注册TestProcess。

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

    其他特性

    配置Swoole的事件回调函数

    支持的事件列表:

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

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

     1 namespace AppEvents;
     2 use Hhxsv5LaravelSSwooleEventsServerStartInterface;
     3 use SwooleAtomic;
     4 use SwooleHttpServer;
     5 class ServerStartEvent implements ServerStartInterface
     6 {
     7     public function __construct()
     8     {
     9     }
    10     public function handle(Server $server)
    11     {
    12         // 初始化一个全局计数器(跨进程的可用)
    13         $server->atomicCount = new Atomic(2233);
    14 
    15         // 控制器中调用:app('swoole')->atomicCount->get();
    16     }
    17 }
    18 namespace AppEvents;
    19 use Hhxsv5LaravelSSwooleEventsWorkerStartInterface;
    20 use SwooleHttpServer;
    21 class WorkerStartEvent implements WorkerStartInterface
    22 {
    23     public function __construct()
    24     {
    25     }
    26     public function handle(Server $server, $workerId)
    27     {
    28         // 初始化一个数据库连接池对象
    29         // DatabaseConnectionPool::init();
    30     }
    31 }

    2.配置。

    1 // 修改文件 config/laravels.php
    2 'event_handlers' => [
    3     'ServerStart' => AppEventsServerStartEvent::class,
    4     'WorkerStart' => AppEventsWorkerStartEvent::class,
    5 ],

    注意事项

    • 单例问题

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

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

       1 public function form(IlluminateHttpRequest $request)
       2 {
       3     $name = $request->input('name');
       4     $all = $request->all();
       5     $sessionId = $request->cookie('sessionId');
       6     $photo = $request->file('photo');
       7     // 调用getContent()来获取原始的POST body,而不能用file_get_contents('php://input')
       8     $rawContent = $request->getContent();
       9     //...
      10 }
    • 推荐通过返回IlluminateHttpResponse对象来响应请求,兼容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. 数据库连接,连接断开后会自动重连

       1 // config/database.php
       2 'connections' => [
       3     'my_conn' => [
       4         'driver'    => 'mysql',
       5         'host'      => env('DB_MY_CONN_HOST', 'localhost'),
       6         'port'      => env('DB_MY_CONN_PORT', 3306),
       7         'database'  => env('DB_MY_CONN_DATABASE', 'forge'),
       8         'username'  => env('DB_MY_CONN_USERNAME', 'forge'),
       9         'password'  => env('DB_MY_CONN_PASSWORD', ''),
      10         'charset'   => 'utf8mb4',
      11         'collation' => 'utf8mb4_unicode_ci',
      12         'prefix'    => '',
      13         'strict'    => false,
      14         'options'   => [
      15             // 开启持久连接
      16             PDO::ATTR_PERSISTENT => true,
      17         ],
      18     ],
      19     //...
      20 ],
      21 //...
    2. Redis连接,连接断开后不会立即自动重连,会抛出一个关于连接断开的异常,下次会自动重连。需确保每次操作Redis前正确的SELECT DB

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

       1 // 某类
       2 class Test
       3 {
       4     public static $array = [];
       5     public static $string = '';
       6 }
       7 
       8 // 某控制器
       9 public function test(Request $req)
      10 {
      11     // 内存爆满
      12     Test::$array[] = $req->input('param1');
      13     Test::$string .= $req->input('param2');
      14 }
    • Linux内核参数调整
    • 压力测试

    用户与案例

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

    推荐阅读:

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

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

    用Swoole+React 实现的聊天室

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

     
  • 相关阅读:
    【中位数 均分纸牌】 糖果传递
    【模板】 均分纸牌
    【离散化】 电影
    【离散化】 区间和
    【最大子矩阵】 城市游戏
    vue中如何引入Element-ui
    详细教你:如何搭建vue环境和创建一个新的vue项目
    vue中如何引入bootstrap
    十天冲刺(4)
    十天冲刺(3)
  • 原文地址:https://www.cnblogs.com/a609251438/p/11936675.html
Copyright © 2020-2023  润新知