LaravelS
是一個膠水項目,用於快速集成Swoole
到Laravel
或Lumen
,而後賦予它們更好的性能、更多可能性。 Github
特性javascript
依賴 | 說明 |
---|---|
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。php
composer require "hhxsv5/laravel-s:~3.5.0" -vvv # 確保你的composer.lock文件是在版本控制中
2.註冊Service Provider(如下兩步二選一)。css
Laravel
: 修改文件config/app.php
,Laravel 5.5+支持包自動發現,你應該跳過這步
html
'providers' => [ //... Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class, ],
Lumen
: 修改文件bootstrap/app.php
java
$app->register(Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class);
3.發佈配置和二進制文件。mysql
每次升級LaravelS後,需從新publish;點擊Release去了解各個版本的變動記錄。
php artisan laravels publish
# 配置文件:config/laravels.php # 二進制文件:bin/laravels bin/fswatch bin/inotify
4.修改配置config/laravels.php
:監聽的IP、端口等,請參考配置項。react
php bin/laravels {start|stop|restart|reload|info|help}
在運行以前,請先仔細閱讀:
注意事項(很是重要)。nginx
命令 | 說明 |
---|---|
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
示例。
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; } }
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服務器監聽的IP和端口與Http服務器相同。
1.建立WebSocket Handler類,並實現接口WebSocketHandlerInterface
。start時會自動實例化,不須要手動建立實例。laravel
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
。git
1 // ... 2 'websocket' => [ 3 'enable' => true, // 看清楚,這裏是true 4 'handler' => \App\Services\WebSocketService::class, 5 ], 6 'swoole' => [ 7 //... 8 // dispatch_mode只能設置爲二、四、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 = $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 = $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
進程會從新拉起定時器
進程。
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 === "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, //... ];
telnet 127.0.0.1 5291
echo "Hello LaravelS" > /dev/udp/127.0.0.1/5292
其餘協議的註冊示例。
'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, ], ],
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.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原始文檔
不安全
的。好比數據庫鏈接就是單例,同一個數據庫鏈接共享同一個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 ],
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 ],
單例問題
常見的解決方案:
XxxCleaner
類來清理單例對象狀態,此類需實現接口Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface
,而後註冊到laravels.php
的cleaners
中。中間件
來重置
單例對象的狀態。ServiceProvider
註冊的單例對象,可添加該ServiceProvider
到laravels.php
的register_providers
中,這樣每次請求會從新註冊該ServiceProvider
,從新實例化單例對象,參考。應經過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 }
推薦閱讀:
實現websocket 主動消息推送,用laravel+Swoole