LaravelS

摘要:
LaravelSLaravelS是一个胶水项目,用于快速集成Swoole到Laravel或Lumen,然后赋予它们更好的性能、更多可能性。Laravel:修改文件config/app.php,Laravel5.5+支持包自动发现,你应该跳过这步'providers'=˃[//...Hhxsv5LaravelSIlluminateLaravelSServiceProvider::class,],Lumen:修改文件bootstrap/app.php$app-˃register;3.发布配置和二进制文件。每次升级LaravelS后,需重新publish;点击Release去了解各个版本的变更记录。运行phpbin/laravels{start|stop|restart|reload|info|help}在运行之前,请先仔细阅读:注意事项。命令说明start启动LaravelS,展示已启动的进程列表"ps-ef|greplaravels"。
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=1autostart=trueautorestart=truestartretries=3user=www-data
redirect_stderr=truestdout_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:5200backup;
    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 namespaceAppServices;
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 classWebSocketService implements WebSocketHandlerInterface
10 {
11     //声明没有参数的构造函数
12     publicfunction __construct()
13 {
14 }
15     publicfunction 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     publicfunction 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     publicfunction 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     defaultupgrade;
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:5200backup;
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_requestSwooleHttpRequest转成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 classTestEvent extends Event
3 {
4     private$data;
5     publicfunction __construct($data)
6 {
7         $this->data =$data;
8 }
9     publicfunction getData()
10 {
11         return $this->data;
12 }
13 }

2.创建监听器类。

1 use Hhxsv5LaravelSSwooleTaskTask;
2 use Hhxsv5LaravelSSwooleTaskEvent;
3 use Hhxsv5LaravelSSwooleTaskListener;
4 classTestListener1 extends Listener
5 {
6     //声明没有参数的构造函数
7     publicfunction __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 classTestTask extends Task
3 {
4     private$data;
5     private$result;
6     publicfunction __construct($data)
7 {
8         $this->data =$data;
9 }
10     //处理任务的逻辑,运行在Task进程中,不能投递任务
11     publicfunction 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     publicfunction 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 namespaceAppJobsTimer;
2 use AppTasksTestTask;
3 use SwooleCoroutine;
4 use Hhxsv5LaravelSSwooleTaskTask;
5 use Hhxsv5LaravelSSwooleTimerCronJob;
6 classTestCronJob extends CronJob
7 {
8     protected $i = 0;
9     //!!! 定时任务的`interval`和`isImmediate`有两种配置方式(二选一):一是重载对应的方法,二是注册定时任务时传入参数。
10     //--- 重载对应的方法来返回配置:开始
11     publicfunction interval()
12 {
13         return 1000;//每1秒运行一次
14 }
15     publicfunction isImmediate()
16 {
17         return false;//是否立即执行第一次,false则等待间隔时间后执行第一次
18 }
19     //--- 重载对应的方法来返回配置:结束
20     publicfunction 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.LaravelSv3.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 namespaceAppServices;
2 use Hhxsv5LaravelSSwooleWebsocketHandlerInterface;
3 use SwooleHttpRequest;
4 use SwooleWebSocketFrame;
5 use SwooleWebSocketServer;
6 classWebSocketService implements WebSocketHandlerInterface
7 {
8     /**@var SwooleTable $wsTable */
9     private$wsTable;
10     publicfunction __construct()
11 {
12         $this->wsTable = app('swoole')->wsTable;
13 }
14     //场景:WebSocket中UserId与FD绑定
15     publicfunction 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     publicfunction 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     publicfunction 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 namespaceAppSockets;
    2 use Hhxsv5LaravelSSwooleSocketTcpSocket;
    3 use SwooleServer;
    4 classTestTcpSocket extends TcpSocket
    5 {
    6     publicfunction onConnect(Server $server, $fd, $reactorId)
    7 {
    8         Log::info('New TCP connection', [$fd]);
    9         $server->send($fd, 'Welcome to LaravelS.');
    10 }
    11     publicfunction 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     publicfunction 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即可。

    publicfunction 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 namespaceAppProcesses;
    2 use AppTasksTestTask;
    3 use Hhxsv5LaravelSSwooleProcessCustomProcessInterface;
    4 use Hhxsv5LaravelSSwooleTaskTask;
    5 use SwooleCoroutine;
    6 use SwooleHttpServer;
    7 use SwooleProcess;
    8 classTestProcess implements CustomProcessInterface
    9 {
    10     public staticfunction getName()
    11 {
    12         //进程名称
    13         return 'test';
    14 }
    15     public staticfunction 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 staticfunction 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的事件回调函数

支持的事件列表:

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

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

1 namespaceAppEvents;
2 use Hhxsv5LaravelSSwooleEventsServerStartInterface;
3 use SwooleAtomic;
4 use SwooleHttpServer;
5 classServerStartEvent implements ServerStartInterface
6 {
7     publicfunction __construct()
8 {
9 }
10     publicfunction handle(Server $server)
11 {
12         //初始化一个全局计数器(跨进程的可用)
13         $server->atomicCount = new Atomic(2233);
14 
15         //控制器中调用:app('swoole')->atomicCount->get();
16 }
17 }
18 namespaceAppEvents;
19 use Hhxsv5LaravelSSwooleEventsWorkerStartInterface;
20 use SwooleHttpServer;
21 classWorkerStartEvent implements WorkerStartInterface
22 {
23     publicfunction __construct()
24 {
25 }
26     publicfunction 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 publicfunction 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()。

    publicfunction 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 classTest
    3 {
    4     public static $array =[];
    5     public static $string = '';
    6 }
    7 
    8 //某控制器
    9 publicfunction 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实现的并发队列处理系统

免责声明:文章转载自《LaravelS》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇IISManager 的启动在AutoCAD中生成贝塞尔曲线下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Java-API:javax.servlet.http.HttpServletResponse

ylbtech-Java-API:javax.servlet.http.HttpServletResponse 1.返回顶部 1、 javax.servlet.httpInterface HttpServletResponse All Superinterfaces: ServletResponse All Known Implementing...

php批量上传图片并把图片名放入数据库

第一步肯定是首先把图片上传到对应的图片目录下,直接用框架中已经有的上传类: try{ $upload=newUpload(); $upload->set_ext(array('zip')); $path='目录名'; if( ! Io::mkdir($path)) // 创建目录 { thrownewException("无...

加载配置文件

前言:如果我们需要调用某个方法,其中的参数为可更改,我们最好是采用配置文件的方式来写,这样便于管理 比如我在写一个通过SFTP连接服务器的环节 public class SFTPUtil { private static Logger log=Logger.getLogger(SFTPUtil.class.getName());...

php-fpm和cgi,并发响应的理解以及高并发和多线程的关系

首先搞清楚php-fpm与cgi的关系 cgi cgi是一个web server与cgi程序(这里可以理解为是php解释器)之间进行数据传输的协议,保证了传递的是标准数据。 php-cgi php-cgi是php解释器,就是上文提到的cgi程序。 Fastcgi Fastcgi是用来提高cgi程序(php-cgi)性能的方案/协议。 cgi程序的性能问题在...

1、网络基本配置

网络命令 1.traceroute:追踪路由   选项1.1 -g <网关>:设置网关-n:不将IP解析为域名-p <端口>:设置目的端口-s <来源地址>:设置本地送出数据包的IP地址-N <次数>:尝试探测的次数,默认16-i <接口>:指定网络接口   例1.2:从本地追踪到www.16...

Zabbix监控Windows事件日志

1.zabbix_agentd.win文件修改: LogFile=c:zabbixzabbix_agentd.log Server=1.16.2.4 ServerActive=1.16.2.4 Hostname=1.16.3.6 #该值必须和Host里面创建的该host的Host Name一致,不加该句默认为该主机的计算机名称,不加这个会出现“acc...