LaravelS
是一個膠水專案,用於快速整合Swoole
到Laravel
或Lumen
,然後賦予它們更好的效能、更多可能性。Github
QQ交流群
- 群1:
698480528
(已滿) - 群2:
62075835
特性
-
內建Http/WebSocket伺服器
-
常駐記憶體
-
平滑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.4.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後,需重新發布
php artisan laravels publish # 配置檔案:config/laravels.php # 二進位制檔案:bin/laravels bin/fswatch
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程式 |
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 laravels {
# 通過 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://laravels;
}
}
與Apache配合使用
LoadModule proxy_module /yyypath/modules/mod_deflate.so
<IfModule deflate_module>
SetOutputFilter DEFLATE
DeflateCompressionLevel 2
AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml
</IfModule>
<VirtualHost *:80>
# 別忘了綁Host喲
ServerName www.laravels.com
ServerAdmin hhxsv5@sina.com
DocumentRoot /xxxpath/laravel-s-test/public;
DirectoryIndex index.html index.htm
<Directory "/">
AllowOverride None
Require all granted
</Directory>
LoadModule proxy_module /yyypath/modules/mod_proxy.so
LoadModule proxy_module /yyypath/modules/mod_proxy_balancer.so
LoadModule proxy_module /yyypath/modules/mod_lbmethod_byrequests.so.so
LoadModule proxy_module /yyypath/modules/mod_proxy_http.so.so
LoadModule proxy_module /yyypath/modules/mod_slotmem_shm.so
LoadModule proxy_module /yyypath/modules/mod_rewrite.so
ProxyRequests Off
ProxyPreserveHost On
<Proxy balancer://laravels>
BalancerMember http://192.168.1.1:5200 loadfactor=7
#BalancerMember http://192.168.1.2:5200 loadfactor=3
#BalancerMember http://192.168.1.3:5200 loadfactor=1 status=+H
ProxySet lbmethod=byrequests
</Proxy>
#ProxyPass / balancer://laravels/
#ProxyPassReverse / balancer://laravels/
# Apache處理靜態資源,LaravelS處理動態資源。
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ balancer://laravels/%{REQUEST_URI} [P,L]
ErrorLog ${APACHE_LOG_DIR}/www.laravels.com.error.log
CustomLog ${APACHE_LOG_DIR}/www.laravels.com.access.log combined
</VirtualHost>
啟用WebSocket伺服器
WebSocket伺服器監聽的IP和埠與Http伺服器相同。
1.建立WebSocket Handler類,並實現介面WebSocketHandlerInterface
。start時會自動例項化,不需要手動建立示例。
namespace App\Services;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
/**
* @see https://wiki.swoole.com/wiki/page/400.html
*/
class WebSocketService implements WebSocketHandlerInterface
{
// 宣告沒有引數的建構函式
public function __construct()
{
}
public function onOpen(Server $server, Request $request)
{
// 在觸發onOpen事件之前Laravel的生命週期已經完結,所以Laravel的Request是可讀的,Session是可讀寫的
// \Log::info('New WebSocket connection', [$request->fd, request()->all(), session()->getId(), session('xxx'), session(['yyy' => time()])]);
$server->push($request->fd, 'Welcome to LaravelS');
// throw new \Exception('an exception');// 此時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
}
public function onMessage(Server $server, Frame $frame)
{
// \Log::info('Received message', [$frame->fd, $frame->data, $frame->opcode, $frame->finish]);
$server->push($frame->fd, date('Y-m-d H:i:s'));
// throw new \Exception('an exception');// 此時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
}
public function onClose(Server $server, $fd, $reactorId)
{
// throw new \Exception('an exception');// 此時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
}
}
2.更改配置config/laravels.php
。
// ...
'websocket' => [
'enable' => true, // 看清楚,這裡是true
'handler' => \App\Services\WebSocketService::class,
],
'swoole' => [
//...
// dispatch_mode只能設定為2、4、5,https://wiki.swoole.com/wiki/page/277.html
'dispatch_mode' => 2,
//...
],
// ...
3.使用SwooleTable
繫結FD與UserId,可選的,Swoole Table示例。也可以用其他全域性儲存服務,例如Redis/Memcached/MySQL,但需要注意多個Swoole Server
例項時FD可能衝突。
4.與Nginx配合使用(推薦)
參考 WebSocket代理
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream laravels {
# 通過 IP:Port 連線
server 127.0.0.1:5200 weight=5 max_fails=3 fail_timeout=30s;
# 通過 UnixSocket Stream 連線,小訣竅:將socket檔案放在/dev/shm目錄下,可獲得更好的效能
#server unix:/xxxpath/laravel-s-test/storage/laravels.sock weight=5 max_fails=3 fail_timeout=30s;
#server 192.168.1.1:5200 weight=3 max_fails=3 fail_timeout=30s;
#server 192.168.1.2:5200 backup;
keepalive 16;
}
server {
listen 80;
# 別忘了綁Host喲
server_name laravels.com;
root /xxxpath/laravel-s-test/public;
access_log /yyypath/log/nginx/$server_name.access.log main;
autoindex off;
index index.html index.htm;
# Nginx處理靜態資源(建議開啟gzip),LaravelS處理動態資源。
location / {
try_files $uri @laravels;
}
# 當請求PHP檔案時直接響應404,防止暴露public/*.php
#location ~* \.php$ {
# return 404;
#}
# Http和WebSocket共存,Nginx通過location區分
# !!! WebSocket連線時路徑為/ws
# Javascript: var ws = new WebSocket("ws://laravels.com/ws");
location =/ws {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout:如果60秒內被代理的伺服器沒有響應資料給Nginx,那麼Nginx會關閉當前連線;同時,Swoole的心跳設定也會影響連線的關閉
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://laravels;
}
location @laravels {
# proxy_connect_timeout 60s;
# proxy_send_timeout 60s;
# proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://laravels;
}
}
5.心跳配置
-
Swoole的心跳配置
// config/laravels.php 'swoole' => [ //... // 表示每60秒遍歷一次,一個連線如果600秒內未向伺服器傳送任何資料,此連線將被強制關閉 'heartbeat_idle_time' => 600, 'heartbeat_check_interval' => 60, //... ],
-
Nginx讀取代理伺服器超時的配置
# 如果60秒內被代理的伺服器沒有響應資料給Nginx,那麼Nginx會關閉當前連線 proxy_read_timeout 60s;
監聽事件
系統事件
通常,你可以在這些事件中重置或銷燬一些全域性或靜態的變數,也可以修改當前的請求和響應。
-
laravels.received_request
將Swoole\Http\Request
轉成Illuminate\Http\Request
後,在Laravel核心處理請求前。// 修改`app/Providers/EventServiceProvider.php`, 新增下面監聽程式碼到boot方法中 // 如果變數$events不存在,你也可以通過Facade呼叫\Event::listen()。 $events->listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) { $req->query->set('get_key', 'hhxsv5');// 修改querystring $req->request->set('post_key', 'hhxsv5'); // 修改post body });
-
laravels.generated_response
在Laravel核心處理完請求後,將Illuminate\Http\Response
轉成Swoole\Http\Response
之前(下一步將響應給客戶端)。// 修改`app/Providers/EventServiceProvider.php`, 新增下面監聽程式碼到boot方法中 // 如果變數$events不存在,你也可以通過Facade呼叫\Event::listen()。 $events->listen('laravels.generated_response', function (\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app) { $rsp->headers->set('header-key', 'hhxsv5');// 修改header });
自定義的非同步事件
此特性依賴
Swoole
的AsyncTask
,必須先設定config/laravels.php
的swoole.task_worker_num
。非同步事件的處理能力受Task程式數影響,需合理設定task_worker_num。
1.建立事件類。
use Hhxsv5\LaravelS\Swoole\Task\Event;
class TestEvent extends Event
{
private $data;
public function __construct($data)
{
$this->data = $data;
}
public function getData()
{
return $this->data;
}
}
2.建立監聽器類。
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Task\Event;
use Hhxsv5\LaravelS\Swoole\Task\Listener;
class TestListener1 extends Listener
{
// 宣告沒有引數的建構函式
public function __construct()
{
}
public function handle(Event $event)
{
\Log::info(__CLASS__ . ':handle start', [$event->getData()]);
sleep(2);// 模擬一些慢速的事件處理
// 監聽器中也可以投遞Task,但不支援Task的finish()回撥。
// 注意:
// 1.引數2需傳true
// 2.config/laravels.php中修改配置task_ipc_mode為1或2,參考 https://wiki.swoole.com/wiki/page/296.html
$ret = Task::deliver(new TestTask('task data'), true);
var_dump($ret);
// throw new \Exception('an exception');// handle時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
}
}
3.繫結事件與監聽器。
// 在"config/laravels.php"中繫結事件與監聽器,一個事件可以有多個監聽器,多個監聽器按順序執行
[
// ...
'events' => [
\App\Tasks\TestEvent::class => [
\App\Tasks\TestListener1::class,
//\App\Tasks\TestListener2::class,
],
],
// ...
];
4.觸發事件。
// 例項化TestEvent並通過fire觸發,此操作是非同步的,觸發後立即返回,由Task程式繼續處理監聽器中的handle邏輯
use Hhxsv5\LaravelS\Swoole\Task\Event;
$success = Event::fire(new TestEvent('event data'));
var_dump($success);//判斷是否觸發成功
非同步的任務佇列
此特性依賴
Swoole
的AsyncTask
,必須先設定config/laravels.php
的swoole.task_worker_num
。非同步任務的處理能力受Task程式數影響,需合理設定task_worker_num。
1.建立任務類。
use Hhxsv5\LaravelS\Swoole\Task\Task;
class TestTask extends Task
{
private $data;
private $result;
public function __construct($data)
{
$this->data = $data;
}
// 處理任務的邏輯,執行在Task程式中,不能投遞任務
public function handle()
{
\Log::info(__CLASS__ . ':handle start', [$this->data]);
sleep(2);// 模擬一些慢速的事件處理
// throw new \Exception('an exception');// handle時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
$this->result = 'the result of ' . $this->data;
}
// 可選的,完成事件,任務處理完後的邏輯,執行在Worker程式中,可以投遞任務
public function finish()
{
\Log::info(__CLASS__ . ':finish start', [$this->result]);
Task::deliver(new TestTask2('task2')); // 投遞其他任務
}
}
2.投遞任務。
// 例項化TestTask並通過deliver投遞,此操作是非同步的,投遞後立即返回,由Task程式繼續處理TestTask中的handle邏輯
use Hhxsv5\LaravelS\Swoole\Task\Task;
$task = new TestTask('task data');
// $task->delay(3);// 延遲3秒投放任務
$ret = Task::deliver($task);
var_dump($ret);//判斷是否投遞成功
毫秒級定時任務
基於Swoole的毫秒定時器,封裝的定時任務,取代
Linux
的Crontab
。
1.建立定時任務類。
namespace App\Jobs\Timer;
use App\Tasks\TestTask;
use Swoole\Coroutine;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
class TestCronJob extends CronJob
{
protected $i = 0;
// !!! 定時任務的`interval`和`isImmediate`有兩種配置方式(二選一):一是過載對應的方法,二是註冊定時任務時傳入引數。
// --- 過載對應的方法來返回配置:開始
public function interval()
{
return 1000;// 每1秒執行一次
}
public function isImmediate()
{
return false;// 是否立即執行第一次,false則等待間隔時間後執行第一次
}
// --- 過載對應的方法來返回配置:結束
public function run()
{
\Log::info(__METHOD__, ['start', $this->i, microtime(true)]);
// do something
// sleep(1); // Swoole < 2.1
Coroutine::sleep(1); // Swoole>=2.1 run()方法已自動建立了協程。
$this->i++;
\Log::info(__METHOD__, ['end', $this->i, microtime(true)]);
if ($this->i >= 10) { // 執行10次後不再執行
\Log::info(__METHOD__, ['stop', $this->i, microtime(true)]);
$this->stop(); // 終止此任務
// CronJob中也可以投遞Task,但不支援Task的finish()回撥。
// 注意:
// 1.引數2需傳true
// 2.config/laravels.php中修改配置task_ipc_mode為1或2,參考 https://wiki.swoole.com/wiki/page/296.html
$ret = Task::deliver(new TestTask('task data'), true);
var_dump($ret);
}
// throw new \Exception('an exception');// 此時丟擲的異常上層會忽略,並記錄到Swoole日誌,需要開發者try/catch捕獲處理
}
}
2.註冊定時任務類。
// 在"config/laravels.php"註冊定時任務類
[
// ...
'timer' => [
'enable' => true, // 啟用Timer
'jobs' => [ // 註冊的定時任務類列表
// 啟用LaravelScheduleJob來執行`php artisan schedule:run`,每分鐘一次,替代Linux Crontab
// \Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
// 兩種配置引數的方式:
// [\App\Jobs\Timer\TestCronJob::class, [1000, true]], // 註冊時傳入引數
\App\Jobs\Timer\TestCronJob::class, // 過載對應的方法來返回引數
],
'max_wait_time' => 5, // Reload時最大等待時間
],
// ...
];
3.注意在構建伺服器叢集時,會啟動多個定時器
,要確保只啟動一個定期器,避免重複執行定時任務。
4.LaravelS v3.4.0
開始支援熱重啟[Reload]定時器
程式,LaravelS 在收到SIGUSR1
訊號後會等待max_wait_time
(預設5)秒再結束程式,然後Manager
程式會重新拉起定時器
程式。
修改程式碼後自動Reload
-
基於
inotify
,僅支援Linux。1.安裝inotify擴充套件。
2.開啟配置項。
3.注意:
inotify
只有在Linux
內修改檔案才能收到檔案變更事件,建議使用最新版Docker,Vagrant解決方案。 -
基於
fswatch
,支援OS X、Linux、Windows。1.安裝fswatch。
2.在專案根目錄下執行命令。
# 監聽當前目錄 ./bin/fswatch # 監聽app目錄 ./bin/fswatch ./app
在你的專案中使用SwooleServer
例項
/**
* 如果啟用WebSocket server,$swoole是`Swoole\WebSocket\Server`的例項,否則是是`Swoole\Http\Server`的例項
* @var \Swoole\WebSocket\Server|\Swoole\Http\Server $swoole
*/
$swoole = app('swoole');
var_dump($swoole->stats());// 單例
使用SwooleTable
1.定義Table,支援定義多個Table。
Swoole啟動之前會建立定義的所有Table。
// 在"config/laravels.php"配置
[
// ...
'swoole_tables' => [
// 場景:WebSocket中UserId與FD繫結
'ws' => [// Key為Table名稱,使用時會自動新增Table字尾,避免重名。這裡定義名為wsTable的Table
'size' => 102400,//Table的最大行數
'column' => [// Table的列定義
['name' => 'value', 'type' => \Swoole\Table::TYPE_INT, 'size' => 8],
],
],
//...繼續定義其他Table
],
// ...
];
2.訪問Table:所有的Table例項均繫結在SwooleServer
上,通過app('swoole')->xxxTable
訪問。
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
// 場景:WebSocket中UserId與FD繫結
public function onOpen(Server $server, Request $request)
{
// var_dump(app('swoole') === $server);// 同一例項
$userId = mt_rand(1000, 10000);
app('swoole')->wsTable->set('uid:' . $userId, ['value' => $request->fd]);// 繫結uid到fd的對映
app('swoole')->wsTable->set('fd:' . $request->fd, ['value' => $userId]);// 繫結fd到uid的對映
$server->push($request->fd, 'Welcome to LaravelS');
}
public function onMessage(Server $server, Frame $frame)
{
foreach (app('swoole')->wsTable as $key => $row) {
if (strpos($key, 'uid:') === 0 && $server->exist($row['value'])) {
$server->push($row['value'], 'Broadcast: ' . date('Y-m-d H:i:s'));// 廣播
}
}
}
public function onClose(Server $server, $fd, $reactorId)
{
$uid = app('swoole')->wsTable->get('fd:' . $fd);
if ($uid !== false) {
app('swoole')->wsTable->del('uid:' . $uid['value']);// 解綁uid對映
}
app('swoole')->wsTable->del('fd:' . $fd);// 解綁fd對映
$server->push($fd, 'Goodbye');
}
多埠混合協議
更多的資訊,請參考Swoole增加監聽的埠與多埠混合協議
為了使我們的主伺服器能支援除HTTP
和WebSocket
外的更多協議,我們引入了Swoole
的多埠混合協議
特性,在LaravelS中稱為Socket
。現在,可以很方便地在Laravel
上被構建TCP/UDP
應用。
-
建立Socket處理類,繼承
Hhxsv5\LaravelS\Swoole\Socket\{TcpSocket|UdpSocket|Http|WebSocket}
namespace App\Sockets; use Hhxsv5\LaravelS\Swoole\Socket\TcpSocket; use Swoole\Server; class TestTcpSocket extends TcpSocket { public function onConnect(Server $server, $fd, $reactorId) { \Log::info('New TCP connection', [$fd]); $server->send($fd, 'Welcome to LaravelS.'); } public function onReceive(Server $server, $fd, $reactorId, $data) { \Log::info('Received data', [$fd, $data]); $server->send($fd, 'LaravelS: ' . $data); if ($data === "quit\r\n") { $server->send($fd, 'LaravelS: bye' . PHP_EOL); $server->close($fd); } } public function onClose(Server $server, $fd, $reactorId) { \Log::info('Close TCP connection', [$fd]); $server->send($fd, 'Goodbye'); } }
這些連線和主伺服器上的HTTP/WebSocket連線共享Worker程式,因此可以在這些事件操作中使用LaravelS提供的
非同步任務投遞
、SwooleTable
、Laravel提供的元件如DB
、Eloquent
等。同時,如果需要使用該協議埠的Swoole\Server\Port
物件,只需要像如下程式碼一樣訪問Socket
類的成員swoolePort
即可。public function onReceive(Server $server, $fd, $reactorId, $data) { $port = $this->swoolePort; //獲得`Swoole\Server\Port`物件 }
-
註冊套接字。
// 修改檔案 config/laravels.php // ... 'sockets' => [ [ 'host' => '127.0.0.1', 'port' => 5291, 'type' => SWOOLE_SOCK_TCP,// 支援的巢狀字型別:https://wiki.swoole.com/wiki/page/16.html#entry_h2_0 'settings' => [// Swoole可用的配置項:https://wiki.swoole.com/wiki/page/526.html 'open_eof_check' => true, 'package_eof' => "\r\n", ], 'handler' => \App\Sockets\TestTcpSocket::class, ], ],
關於心跳配置,只能設定在
主伺服器
上,不能配置在套接字
上,但套接字
會繼承主伺服器
的心跳配置。對於TCP協議,
dispatch_mode
選項設為1/3
時,底層會遮蔽onConnect
/onClose
事件,原因是這兩種模式下無法保證onConnect
/onClose
/onReceive
的順序。如果需要用到這兩個事件,請將dispatch_mode
改為2/4/5
,參考。'swoole' => [ //... 'dispatch_mode' => 2, //... ];
- 測試。
-
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
'sockets' => [ [ 'host' => '0.0.0.0', 'port' => 5293, 'type' => SWOOLE_SOCK_TCP, 'settings' => [ 'open_http_protocol' => true, ], 'handler' => \App\Sockets\TestHttp::class, ], ],
- WebSocket
'sockets' => [ [ 'host' => '0.0.0.0', 'port' => 5294, 'type' => SWOOLE_SOCK_TCP, 'settings' => [ 'open_http_protocol' => true, 'open_websocket_protocol' => true, ], 'handler' => \App\Sockets\TestWebSocket::class, ], ],
-
協程
-
警告:Laravel/Lumen中存在大量單例和靜態屬性,在協程下是
不安全
的,不建議
開啟協程,但自定義程式、定時器
中可使用協程。 -
啟用協程,預設是關閉的。
// 修改檔案 `config/laravels.php` [ //... 'swoole' => [ //... 'enable_coroutine' => true ], ]
-
協程客戶端:需
Swoole>=2.0
。 -
執行時協程:需
Swoole>=4.1.0
,同時啟用下面的配置。// 修改檔案 `config/laravels.php` [ //... 'enable_coroutine_runtime' => true ]
自定義程式
支援開發者建立一些特殊的工作程式,用於監控、上報或者其他特殊的任務,參考addProcess。
-
建立Proccess類,實現CustomProcessInterface介面。
namespace App\Processes; use App\Tasks\TestTask; use Hhxsv5\LaravelS\Swoole\Process\CustomProcessInterface; use Hhxsv5\LaravelS\Swoole\Task\Task; use Swoole\Coroutine; use Swoole\Http\Server; use Swoole\Process; class TestProcess implements CustomProcessInterface { public static function getName() { // 程式名稱 return 'test'; } public static function isRedirectStdinStdout() { // 是否重定向輸入輸出 return false; } public static function getPipeType() { // 管道型別:0不建立管道,1建立SOCK_STREAM型別管道,2建立SOCK_DGRAM型別管道 return 0; } public static function callback(Server $swoole, Process $process) { // 程式執行的程式碼,不能退出,一旦退出Manager程式會自動再次建立該程式。 \Log::info(__METHOD__, [posix_getpid(), $swoole->stats()]); while (true) { \Log::info('Do something'); // sleep(1); // Swoole < 2.1 Coroutine::sleep(1); // Swoole>=2.1 callback()方法已自動建立了協程。 // 自定義程式中也可以投遞Task,但不支援Task的finish()回撥。 // 注意: // 1.引數2需傳true // 2.config/laravels.php中修改配置task_ipc_mode為1或2,參考 https://wiki.swoole.com/wiki/page/296.html $ret = Task::deliver(new TestTask('task data'), true); var_dump($ret); // 上層會捕獲callback中丟擲的異常,並記錄到Swoole日誌,如果異常數達到10次,此程式會退出,Manager程式會重新建立程式,所以建議開發者自行try/catch捕獲,避免建立程式過於頻繁。 // throw new \Exception('an exception'); } } // 要求:LaravelS >= v3.4.0 並且 callback() 必須是非同步非阻塞程式。 public static function onReload(Server $swoole, Process $process) { // Stop the process... // Then end process $process->exit(0); } }
-
註冊TestProcess。
// 修改檔案 config/laravels.php // ... 'processes' => [ \App\Processes\TestProcess::class, ],
- 注意:TestProcess::callback()方法不能退出,如果退出次數達到10次,Manager程式將會重新建立程式。
其他特性
配置Swoole
的事件回撥函式
支援的事件列表:
事件 | 需實現的介面 | 發生時機 |
---|---|---|
WorkerStart | Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface | 發生在Worker/Task程式啟動時,並且已經完成Laravel初始化 |
WorkerStop | Hhxsv5\LaravelS\Swoole\Events\WorkerStopInterface | 發生在Worker/Task程式正常退出時。 |
WorkerError | Hhxsv5\LaravelS\Swoole\Events\WorkerErrorInterface | 發生在Worker/Task程式發生異常或致命錯誤時。 |
1.建立事件處理類,實現相應的介面。
namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
class WorkerStartEvent implements WorkerStartInterface
{
public function __construct()
{
}
public function handle(Server $server, $workerId)
{
// 初始化一個資料庫連線池物件
// DatabaseConnectionPool::init();
}
}
2.配置。
// 修改檔案 config/laravels.php
'event_handlers' => [
'WorkerStart' => \App\Events\WorkerStartEvent::class,
],
注意事項
-
單例問題
-
傳統FPM下,單例模式的物件的生命週期僅在每次請求中,請求開始=>例項化單例=>請求結束後=>單例物件資源回收。
-
Swoole Server下,所有單例物件會常駐於記憶體,這個時候單例物件的生命週期與FPM不同,請求開始=>例項化單例=>請求結束=>單例物件依舊保留,需要開發者自己維護單例的狀態。
-
如果你的專案中使用到了Session、Authentication、JWT,請根據情況解除
laravels.php
中cleaners
的註釋。 -
常見的解決方案:
-
寫一個
XxxCleaner
類來清理單例物件狀態,此類需實現介面Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface
,然後註冊到laravels.php
的cleaners
中。 -
用一個
中介軟體
來重置
單例物件的狀態。 - 如果是以
ServiceProvider
註冊的單例物件,可新增該ServiceProvider
到laravels.php
的register_providers
中,這樣每次請求會重新註冊該ServiceProvider
,重新例項化單例物件,參考。
-
-
-
應通過
Illuminate\Http\Request
物件來獲取請求資訊,$_ENV是可讀取的,$_SERVER是部分可讀的,不能使用
$_GET、$_POST、$_FILES、$_COOKIE、$_REQUEST、$_SESSION、$GLOBALS。public function form(\Illuminate\Http\Request $request) { $name = $request->input('name'); $all = $request->all(); $sessionId = $request->cookie('sessionId'); $photo = $request->file('photo'); // 呼叫getContent()來獲取原始的POST body,而不能用file_get_contents('php://input') $rawContent = $request->getContent(); //... }
-
推薦通過返回
Illuminate\Http\Response
物件來響應請求,相容echo、vardump()、print_r(),不能使用
函式像 dd()、exit()、die()、header()、setcookie()、http_response_code()。public function json() { return response()->json(['time' => time()])->header('header1', 'value1')->withCookie('c1', 'v1'); }
- 各種
單例的連線
將被常駐記憶體,建議開啟持久連線
。- 資料庫連線,連線斷開後會自動重連
// config/database.php 'connections' => [ 'my_conn' => [ 'driver' => 'mysql', 'host' => env('DB_MY_CONN_HOST', 'localhost'), 'port' => env('DB_MY_CONN_PORT', 3306), 'database' => env('DB_MY_CONN_DATABASE', 'forge'), 'username' => env('DB_MY_CONN_USERNAME', 'forge'), 'password' => env('DB_MY_CONN_PASSWORD', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'strict' => false, 'options' => [ // 開啟持久連線 \PDO::ATTR_PERSISTENT => true, ], ], //... ], //...
- 資料庫連線,連線斷開後會自動重連
- Redis連線,連線斷開後
不會立即
自動重連,會丟擲一個關於連線斷開的異常,下次會自動重連。需確保每次操作Redis前正確的SELECT DB
。// config/database.php 'redis' => [ 'default' => [ 'host' => env('REDIS_HOST', 'localhost'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => 0, 'persistent' => true, // 開啟持久連線 ], ], //...
-
你宣告的全域性、靜態變數必須手動清理或重置。
-
無限追加元素到靜態或全域性變數中,將導致記憶體爆滿。
// 某類 class Test { public static $array = []; public static $string = ''; } // 某控制器 public function test(Request $req) { // 記憶體爆滿 Test::$array[] = $req->input('param1'); Test::$string .= $req->input('param2'); }
- 壓力測試
使用者與案例
-
醫聯:WEB站、M站、APP、小程式的賬戶體系服務。
-
ITOK線上客服平臺:使用者IT工單的處理跟蹤及線上實時溝通。
-
-
微信公眾號-廣州塔:活動、商城
-
企鵝遊戲盒子、明星新勢力、以及小程式廣告服務
-
小程式-修機匠手機上門維修服務:手機維修服務,提供上門服務,支援線上維修。
- 億健APP
打賞
您的支援是我們堅持的最大動力。
Github LaravelS
QQ交流群
- 群1:
698480528
(已滿) - 群2:
62075835