LaravelS - 基於 Swoole 加速 Laravel/Lumen - 帶你飛 ?

好好先森V5發表於2018-02-09

LaravelS是一個膠水專案,用於快速整合SwooleLaravelLumen,然後賦予它們更好的效能、更多可能性。Github

QQ交流群

  • 群1:698480528(已滿)
  • 群2:62075835

特性

要求

依賴 說明
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.phpLaravel 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.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程式
info 顯示元件的版本資訊
help 顯示幫助資訊

部署

建議通過Supervisord監管主程式,前提是不能加-d選項並且設定swoole.daemonizefalse

[program:laravel-s-test]
command=/user/local/bin/php /opt/www/laravel-s-test/bin/laravels start -i
numprocs=1
autostart=true
autorestart=true
startretries=3
user=www-data
redirect_stderr=true
stdout_logfile=/opt/www/laravel-s-test/storage/logs/supervisord-stdout.log

與Nginx配合使用(推薦)

示例

gzip on;
gzip_min_length 1024;
gzip_comp_level 2;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml;
gzip_vary on;
gzip_disable "msie6";
upstream 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_requestSwoole\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
    });

自定義的非同步事件

此特性依賴SwooleAsyncTask,必須先設定config/laravels.phpswoole.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);//判斷是否觸發成功

非同步的任務佇列

此特性依賴SwooleAsyncTask,必須先設定config/laravels.phpswoole.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的毫秒定時器,封裝的定時任務,取代LinuxCrontab

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增加監聽的埠多埠混合協議

為了使我們的主伺服器能支援除HTTPWebSocket外的更多協議,我們引入了Swoole多埠混合協議特性,在LaravelS中稱為Socket。現在,可以很方便地在Laravel上被構建TCP/UDP應用。

  1. 建立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提供的元件如DBEloquent等。同時,如果需要使用該協議埠的Swoole\Server\Port物件,只需要像如下程式碼一樣訪問Socket類的成員swoolePort即可。

    public function onReceive(Server $server, $fd, $reactorId, $data)
    {
        $port = $this->swoolePort; //獲得`Swoole\Server\Port`物件
    }
  2. 註冊套接字。

    // 修改檔案 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,
        //...
    ];
  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'    => "\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,
      ],
      ],

協程

Swoole原始文件

  • 警告:Laravel/Lumen中存在大量單例和靜態屬性,在協程下是不安全的,不建議開啟協程,但自定義程式、定時器中可使用協程。

  • 啟用協程,預設是關閉的。

    // 修改檔案 `config/laravels.php`
    [
        //...
        'swoole' => [
            //...
            'enable_coroutine' => true
         ],
    ]
  • 協程客戶端:需Swoole>=2.0

  • 執行時協程:需Swoole>=4.1.0,同時啟用下面的配置。

    // 修改檔案 `config/laravels.php`
    [
        //...
        'enable_coroutine_runtime' => true
    ]

自定義程式

支援開發者建立一些特殊的工作程式,用於監控、上報或者其他特殊的任務,參考addProcess

  1. 建立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);
        }
    }
  2. 註冊TestProcess。

    // 修改檔案 config/laravels.php
    // ...
    'processes' => [
        \App\Processes\TestProcess::class,
    ],
  3. 注意: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.phpcleaners的註釋。

    • 常見的解決方案:

      1. 寫一個XxxCleaner類來清理單例物件狀態,此類需實現介面Hhxsv5\LaravelS\Illuminate\Cleaners\CleanerInterface,然後註冊到laravels.phpcleaners中。

      2. 用一個中介軟體重置單例物件的狀態。

      3. 如果是以ServiceProvider註冊的單例物件,可新增該ServiceProviderlaravels.phpregister_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');
    }
  • 各種單例的連線將被常駐記憶體,建議開啟持久連線
    1. 資料庫連線,連線斷開後會自動重連
      // 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,
          ],
      ],
      //...
      ],
      //...
  1. 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');
    }
  • Linux核心引數調整

  • 壓力測試

使用者與案例

  • KuCoin

  • 醫聯:WEB站、M站、APP、小程式的賬戶體系服務。

    醫聯
  • ITOK線上客服平臺:使用者IT工單的處理跟蹤及線上實時溝通。

    ITOK線上客服平臺
  • 盟呱呱

    盟呱呱
  • 微信公眾號-廣州塔:活動、商城

    廣州塔
  • 企鵝遊戲盒子、明星新勢力、以及小程式廣告服務

    企鵝遊戲盒子
  • 小程式-修機匠手機上門維修服務:手機維修服務,提供上門服務,支援線上維修。

    修機匠手機上門維修服務
  • 億健APP

打賞

您的支援是我們堅持的最大動力。

打賞

Github LaravelS
QQ交流群

  • 群1:698480528(已滿)
  • 群2:62075835

hhxsv5

相關文章