LaravelS
LaravelS是一個膠水專案,用于快速集成Swoole到Laravel或Lumen,然后賦予它們更好的性能、更多可能性,Github
特性
- 內置Http/WebSocket服務器
- 多埠混合協議
- 協程
- 自定義行程
- 常駐記憶體
- 異步的事件監聽
- 異步的任務佇列
- 毫秒級定時任務
- 平滑Reload
- 修改代碼后自動Reload
- 同時支持Laravel與Lumen,兼容主流版本
- 簡單,開箱即用
要求
| 依賴 | 說明 |
|---|---|
| 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.php,Laravel 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.php中swoole.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.daemonize為false,
[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 [email protected] 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 App\Services; 2 use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface; 3 use Swoole\Http\Request; 4 use Swoole\WebSocket\Frame; 5 use Swoole\WebSocket\Server; 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' => \App\Services\WebSocketService::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將Swoole\Http\Request轉成Illuminate\Http\Request后,在Laravel內核處理請求前,1 // 修改`app/Providers/EventServiceProvider.php`, 添加下面監聽代碼到boot方法中 2 // 如果變數$events不存在,你也可以通過Facade呼叫\Event::listen(), 3 $events->listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) { 4 $req->query->set('get_key', 'hhxsv5');// 修改querystring 5 $req->request->set('post_key', 'hhxsv5'); // 修改post body 6 });
-
laravels.generated_response在Laravel內核處理完請求后,將Illuminate\Http\Response轉成Swoole\Http\Response之前(下一步將回應給客戶端),1 // 修改`app/Providers/EventServiceProvider.php`, 添加下面監聽代碼到boot方法中 2 // 如果變數$events不存在,你也可以通過Facade呼叫\Event::listen(), 3 $events->listen('laravels.generated_response', function (\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app) { 4 $rsp->headers->set('header-key', 'hhxsv5');// 修改header 5 });
自定義的異步事件
此特性依賴Swoole的AsyncTask,必須先設定config/laravels.php的swoole.task_worker_num,異步事件的處理能力受Task行程數影響,需合理設定task_worker_num,
1.創建事件類,
1 use Hhxsv5\LaravelS\Swoole\Task\Event; 2 class TestEvent extends Event 3 { 4 private $data; 5 public function __construct($data) 6 { 7 $this->data =https://www.cnblogs.com/a609251438/p/ $data; 8 } 9 public function getData() 10 { 11 return $this->data; 12 } 13 }
2.創建監聽器類,
1 use Hhxsv5\LaravelS\Swoole\Task\Task; 2 use Hhxsv5\LaravelS\Swoole\Task\Event; 3 use Hhxsv5\LaravelS\Swoole\Task\Listener; 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 \App\Tasks\TestEvent::class => [ 6 \App\Tasks\TestListener1::class, 7 //\App\Tasks\TestListener2::class, 8 ], 9 ], 10 // ... 11 ];
4.觸發事件,
1 // 實體化TestEvent并通過fire觸發,此操作是異步的,觸發后立即回傳,由Task行程繼續處理監聽器中的handle邏輯 2 use Hhxsv5\LaravelS\Swoole\Task\Event; 3 $success = Event::fire(new TestEvent('event data')); 4 var_dump($success);//判斷是否觸發成功
異步的任務佇列
此特性依賴Swoole的AsyncTask,必須先設定config/laravels.php的swoole.task_worker_num,異步任務的處理能力受Task行程數影響,需合理設定task_worker_num,
1.創建任務類,
1 use Hhxsv5\LaravelS\Swoole\Task\Task; 2 class TestTask extends Task 3 { 4 private $data; 5 private $result; 6 public function __construct($data) 7 { 8 $this->data =https://www.cnblogs.com/a609251438/p/ $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 Hhxsv5\LaravelS\Swoole\Task\Task; 3 $task = new TestTask('task data'); 4 // $task->delay(3);// 延遲3秒投放任務 5 $ret = Task::deliver($task); 6 var_dump($ret);//判斷是否投遞成功
毫秒級定時任務
基于Swoole的毫秒定時器,封裝的定時任務,取代Linux的Crontab,
1.創建定時任務類,
1 namespace App\Jobs\Timer; 2 use App\Tasks\TestTask; 3 use Swoole\Coroutine; 4 use Hhxsv5\LaravelS\Swoole\Task\Task; 5 use Hhxsv5\LaravelS\Swoole\Timer\CronJob; 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 // \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class, 9 // 兩種配置引數的方式: 10 // [\App\Jobs\Timer\TestCronJob::class, [1000, true]], // 注冊時傳入引數 11 \App\Jobs\Timer\TestCronJob::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是`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,
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' => \Swoole\Table::TYPE_INT, 'size' => 8], 10 ], 11 ], 12 //...繼續定義其他Table 13 ], 14 // ... 15 ];
2.訪問Table:所有的Table實體均系結在SwooleServer上,通過app('swoole')->xxxTable訪問,
1 namespace App\Services; 2 use Hhxsv5\LaravelS\Swoole\WebsocketHandlerInterface; 3 use Swoole\Http\Request; 4 use Swoole\WebSocket\Frame; 5 use Swoole\WebSocket\Server; 6 class WebSocketService implements WebSocketHandlerInterface 7 { 8 /**@var \Swoole\Table $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增加監聽的埠與多埠混合協議
為了使我們的主服務器能支持除HTTP和WebSocket外的更多協議,我們引入了Swoole的多埠混合協議特性,在LaravelS中稱為Socket,現在,可以很方便地在Laravel上構建TCP/UDP應用,
-
創建Socket處理類,繼承
Hhxsv5\LaravelS\Swoole\Socket\{TcpSocket|UdpSocket|Http|WebSocket}1 namespace App\Sockets; 2 use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket; 3 use Swoole\Server; 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 =https://www.cnblogs.com/a609251438/p/== "quit\r\n") { 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提供的組件如DB、Eloquent等,同時,如果需要使用該協議埠的Swoole\Server\Port物件,只需要像如下代碼一樣訪問Socket類的成員swoolePort即可,public function onReceive(Server $server, $fd, $reactorId, $data) { $port = $this->swoolePort; //獲得`Swoole\Server\Port`物件 }
-
注冊套接字,
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' => "\r\n", 11 ], 12 'handler' => \App\Sockets\TestTcpSocket::class, 13 ], 14 ],
關于心跳配置,只能設定在
主服務器上,不能配置在套接字上,但套接字會繼承主服務器的心跳配置,對于TCP協議,
dispatch_mode選項設為1/3時,底層會屏蔽onConnect/onClose事件,原因是這兩種模式下無法保證onConnect/onClose/onReceive的順序,如果需要用到這兩個事件,請將dispatch_mode改為2/4/5,參考,'swoole' => [ //... 'dispatch_mode' => 2, //... ];
- 測驗,
- TCP:
telnet 127.0.0.1 5291 - UDP:Linux下
echo "Hello LaravelS" > /dev/udp/127.0.0.1/5292
-
其他協議的注冊示例,
- UDP
'sockets' => [ [ 'host' => '0.0.0.0', 'port' => 5292, 'type' => SWOOLE_SOCK_UDP, 'settings' => [ 'open_eof_check' => true, 'package_eof' => "\r\n", ], 'handler' => \App\Sockets\TestUdpSocket::class, ], ],
- Http
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' => \App\Sockets\TestHttp::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' => \App\Sockets\TestWebSocket::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,
-
創建Proccess類,實作CustomProcessInterface介面,
1 namespace App\Processes; 2 use App\Tasks\TestTask; 3 use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface; 4 use Hhxsv5\LaravelS\Swoole\Task\Task; 5 use Swoole\Coroutine; 6 use Swoole\Http\Server; 7 use Swoole\Process; 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 }
-
注冊TestProcess,
1 // 修改檔案 config/laravels.php 2 // ... 3 'processes' => [ 4 [ 5 'class' => \App\Processes\TestProcess::class, 6 'redirect' => false, // 是否重定向輸入輸出 7 'pipe' => 0 // 管道型別:0不創建管道,1創建SOCK_STREAM型別管道,2創建SOCK_DGRAM型別管道 8 'enable' => true // 是否啟用,默認true 9 ], 10 ],
- 注意: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 App\Events; 2 use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface; 3 use Swoole\Atomic; 4 use Swoole\Http\Server; 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 App\Events; 19 use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface; 20 use Swoole\Http\Server; 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' => \App\Events\ServerStartEvent::class, 4 'WorkerStart' => \App\Events\WorkerStartEvent::class, 5 ],
注意事項
-
單例問題- 傳統FPM下,單例模式的物件的生命周期僅在每次請求中,請求開始=>實體化單例=>請求結束后=>單例物件資源回收,
- Swoole Server下,所有單例物件會常駐于記憶體,這個時候單例物件的生命周期與FPM不同,請求開始=>實體化單例=>請求結束=>單例物件依舊保留,需要開發者自己維護單例的狀態,
-
常見的解決方案:
- 寫一個
XxxCleaner類來清理單例物件狀態,此類需實作介面Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然后注冊到laravels.php的cleaners中, - 用一個
中間件來重置單例物件的狀態, - 如果是以
ServiceProvider注冊的單例物件,可添加該ServiceProvider到laravels.php的register_providers中,這樣每次請求會重新注冊該ServiceProvider,重新實體化單例物件,參考,
- 寫一個
- LaravelS 已經內置了一些Cleaner,
- 常見問題:一攬子的已知問題和解決方案,
- 除錯方式:記錄日志、Laravel Dump Server(Laravel 5.7已默認集成)
-
應通過
Illuminate\Http\Request物件來獲取請求資訊,是可讀取的,_SERVER是部分可讀的,不能使用、_POST、、_COOKIE、、_SESSION、$GLOBALS,1 public function form(\Illuminate\Http\Request $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 }
-
推薦通過回傳
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 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 //...
-
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實作的并發佇列處理系統
php Swoole實作毫秒級定時任務轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/105963.html
標籤:PHP
