Swoole 在 Swoft 中的應用

date: 2017-12-14 21:34:51
title: swoole 在 swoft 中的應用php

swoft 官網: https://www.swoft.org/

swoft 源碼解讀: http://naotu.baidu.com/file/8...html

號外號外, 歡迎你們 star, 咱們開發組定了一個 star 1000+ 就線下聚一次的小目標mysql

上一篇 blog - swoft 源碼解讀 反響還不錯, 很多同窗推薦再加一篇, 講解一下 swoft 中使用到的 swoole 功能, 幫助你們開啓 swoole 的 實戰之旅.nginx

服務器開發涉及到的相關技術領域的知識很是多, 不日積月累打好基礎, 是很難真正作好的. 因此我建議:laravel

swoole wiki 最好看 3 遍, 包括評論. 第一遍快速過一遍, 造成大體印象; 第二遍邊看邊敲代碼; 第三遍能夠選擇衍生的開源框架進行實戰. swoft 就是不錯的選擇.

swoole wiki 發展到如今已經 1400+ 頁, 確實會有點難啃, 勇敢的少年呀, 加油.git

swoole 在 swoft 中的應用:github

  • Swoole\Server: swoole2.0 協程 Server
  • Swoole\HttpServer: swoole2.0 協程 http Server, 繼承自 Swoole\Server
  • Swoole\Coroutine\Client: 協程客戶端, swoole 封裝了 tcp / http / redis / mysql
  • Swoole\Coroutine: 協程工具集, 獲取當前協程id,反射調用等能力
  • Swoole\Process: 進程管理模塊, 能夠在 Swoole\Server 以外擴展更多功能
  • Swoole\Async: 異步文件 IO
  • Swoole\Timer: 基於 timerfd + epoll 實現的異步毫秒定時器,可完美的運行在 EventLoop 中
  • Swoole\Event: 直接操做底層 epoll/kqueue 事件循環(EventLoop)的接口
  • Swoole\Lock: 在 PHP 代碼中能夠很方便地建立一個鎖, 用來實現數據同步
  • Swoole\Table: 基於共享內存實現的超高性能數據結構

SwooleHttpServer

使用 swoole 的 http server 相較 tcp server 仍是要簡單一些, 只須要關心:web

  • Swoole\Http\Server
  • Swoole\Http\Request
  • Swoole\Http\Response

先看 http server:redis

// \Swoft\Server\HttpServer
public function start()
{
    // http server
    $this->server = new \Swoole\Http\Server($this->httpSetting['host'], $this->httpSetting['port'], $this->httpSetting['model'], $this->httpSetting['type']);

    // 設置事件監聽
    $this->server->set($this->setting);
    $this->server->on('start', [$this, 'onStart']);
    $this->server->on('workerStart', [$this, 'onWorkerStart']);
    $this->server->on('managerStart', [$this, 'onManagerStart']);
    $this->server->on('request', [$this, 'onRequest']);
    $this->server->on('task', [$this, 'onTask']);
    $this->server->on('pipeMessage', [$this, 'onPipeMessage']);
    $this->server->on('finish', [$this, 'onFinish']);

    // 啓動RPC服務
    if ((int)$this->serverSetting['tcpable'] === 1) {
        $this->listen = $this->server->listen($this->tcpSetting['host'], $this->tcpSetting['port'], $this->tcpSetting['type']);
        $tcpSetting = $this->getListenTcpSetting();
        $this->listen->set($tcpSetting);
        $this->listen->on('connect', [$this, 'onConnect']);
        $this->listen->on('receive', [$this, 'onReceive']);
        $this->listen->on('close', [$this, 'onClose']);
    }

    $this->beforeStart();
    $this->server->start();
}

使用 swoole server 十分簡單:sql

  • 傳入配置 server 配置信息, new 一個 swoole server
  • 設置事件監聽, 這一步須要你們對 swoole 的進程模型很是熟悉, 必定要看懂下面 2 張圖
  • 啓動服務器

進程流程圖

進程/線程結構圖

swoft 在使用 http server 時, 還會根據配置信息, 來判斷是否同時新建一個 RPC server, 使用 swoole 的 多端口監聽 來實現.

再來看 Request 和 Response, 提醒一下, 框架設計的時候, 要記住 規範先行:

PSR-7: HTTP message interfaces

SwooleHttpRequest

phper 比較熟悉的應該是 $_GET $_POST $_COOKIE $_FILES $_SERVER 這些全局變量, 這些在 swoole 中都獲得了支持, 而且提供了更多方便的功能:

// \Swoole\Http\Request $request
$request->get(); // -> $_GET
$request->post(); // -> $_POST
$request->cookie(); // -> $_COOKIE
$request->files(); // -> $_FILES
$request->server(); // -> $_SERVER

// 更方便的方法
$request->header(); // 原生 php 須要從 $_SERVER 中取
$request->rawContent(); // 獲取原始的POST包體

這裏強調一下 $request->rawContent(), phper 可能用 $_POST 比較 6, 致使一些知識不知道: post 的數據的格式. 由於這個知識, 因此 $_POST 不是全部時候都能取到數據的, 你們能夠網上查找資料, 或者本身使用 postman 這樣的工具本身測試驗證一下. 在 $_POST 取不到數據的狀況下, 會這樣處理:

$post = file_get_content('php://input');

$request->rawContent() 和這個等價的.

swoft 封裝 Request 對象的方法, 和主流框架差很少, 以 laravel 爲例(實際使用 symfony 的方法):

// SymfonyRequest::createFromGlobals()
public static function createFromGlobals()
{
    // With the php's bug #66606, the php's built-in web server
    // stores the Content-Type and Content-Length header values in
    // HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH fields.
    $server = $_SERVER;
    if ('cli-server' === PHP_SAPI) {
        if (array_key_exists('HTTP_CONTENT_LENGTH', $_SERVER)) {
            $server['CONTENT_LENGTH'] = $_SERVER['HTTP_CONTENT_LENGTH'];
        }
        if (array_key_exists('HTTP_CONTENT_TYPE', $_SERVER)) {
            $server['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE'];
        }
    }

    $request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server); // xglobal,

    if (0 === strpos($request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded')
        && in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), array('PUT', 'DELETE', 'PATCH'))
    ) {
        parse_str($request->getContent(), $data);
        $request->request = new ParameterBag($data);
    }

    return $request;
}

SwooleHttpResponse

Swoole\Http\Response 也是支持常見功能:

// Swoole\Http\Response $response
$response->header($key, $value); // -> header("$key: $valu", $httpCode)
$response->cookie(); // -> setcookie()
$response->status(); // http 狀態碼

固然, swoole 還提供了經常使用的功能:

$response->sendfile(); // 給客戶端發送文件
$response->gzip(); // nginx + fpm 的場景, nginx 處理掉了這個
$response->end(); // 返回數據給客戶端
$response->write(); // 分段傳輸數據, 最後調用 end() 表示數據傳輸結束

phper 注意下這裏的 write()end(), 這裏有一個 http chunk 的知識點. 須要返回大量數據給客戶端(>=2M)時, 須要分段(chunk)進行發送. 因此先用 write() 發送數據, 最後用 end() 表示結束. 數據量不大時, 直接調用 end($html) 返回就能夠了.

在框架具體實現上, 和上面同樣, laravel 依舊用的 SymfonyResponse, swoft 也是實現 PSR-7 定義的接口, 對 Swoole\Http\Response 進行封裝.

SwooleServer

swoft 使用 Swoole\Server 來實現 RPC 服務, 其實在上面的多端口監聽, 也是爲了開啓 RPC 服務. 注意一下單獨啓用中回調函數的區別:

// \Swoft\Server\RpcServer
public function start()
{
    // rpc server
    $this->server = new Server($this->tcpSetting['host'], $this->tcpSetting['port'], $this->tcpSetting['model'], $this->tcpSetting['type']);

    // 設置回調函數
    $listenSetting = $this->getListenTcpSetting();
    $setting = array_merge($this->setting, $listenSetting);
    $this->server->set($setting);
    $this->server->on('start', [$this, 'onStart']);
    $this->server->on('workerStart', [$this, 'onWorkerStart']);
    $this->server->on('managerStart', [$this, 'onManagerStart']);
    $this->server->on('task', [$this, 'onTask']);
    $this->server->on('finish', [$this, 'onFinish']);
    $this->server->on('connect', [$this, 'onConnect']);
    $this->server->on('receive', [$this, 'onReceive']);
    $this->server->on('pipeMessage', [$this, 'onPipeMessage']); // 接收管道信息時觸發的回調函數
    $this->server->on('close', [$this, 'onClose']);

    // before start
    $this->beforeStart();
    $this->server->start();
}

SwooleCoroutineClient

swoole 自帶的協程的客戶端, swoft 都封裝進了鏈接池, 用來提升性能. 同時, 爲了業務使用方便, 既有協程鏈接, 也有同步鏈接, 方便業務使用時無縫切換.

同步/協程鏈接的實現代碼:

// RedisConnect -> 使用 swoole 協程客戶端
public function createConnect()
{
    // 鏈接信息
    $timeout = $this->connectPool->getTimeout();
    $address = $this->connectPool->getConnectAddress();
    list($host, $port) = explode(":", $address);

    // 建立鏈接
    $redis = new \Swoole\Coroutine\Redis();
    $result = $redis->connect($host, $port, $timeout);
    if ($result == false) {
        App::error("redis鏈接失敗,host=" . $host . " port=" . $port . " timeout=" . $timeout);
        return;
    }

    $this->connect = $redis;
}

// SyncRedisConnect -> 使用 \Redis 同步客戶端
public function createConnect()
{
    // 鏈接信息
    $timeout = $this->connectPool->getTimeout();
    $address = $this->connectPool->getConnectAddress();
    list($host, $port) = explode(":", $address);

    // 初始化鏈接
    $redis = new \Redis();
    $redis->connect($host, $port, $timeout);
    $this->connect = $redis;
}

swoft 中實現鏈接池的代碼在 src/Pool 下實現, 由三部分組成:

  • Connect: 即上面代碼中的鏈接
  • Balancer: 負載均衡器, 目前實現了 隨機/輪詢 2 種方式
  • Pool: 鏈接池, 調用 Balancer, 返回 Connect

詳細內容能夠參考以前的 blog - swoft 源碼解讀

SwooleCoroutine

做爲首個使用 Swoole2.0 原生協程的框架, swoft 但願將協程的能力擴展到框架的核心設計中. 使用 Swoft\Base\Coroutine 進行封裝, 方便整個應用中使用:

public static function id()
{
    $cid = SwCoroutine::getuid(); // swoole 協程
    $context = ApplicationContext::getContext();

    if ($context == ApplicationContext::WORKER || $cid !== -1) {
        return $cid;
    }
    if ($context == ApplicationContext::TASK) {
        return Task::getId();
    }
    if($context == ApplicationContext::CONSOLE){
        return Console::id();
    }

    return Process::getId();
}

如同這段代碼所示, Swoft 但願將方便易用的協程的能力, 擴展到 Console/Worker/Task/Process 等等不一樣的應用場景中

原生的 call_user_func() / call_user_func_array() 中沒法使用協程 client, 因此 swoole 在協程組件中也封裝的了相應的實現, swoft 中也有使用到, 請自行閱讀源碼.

SwooleProcess

進程管理模塊, 適合處理和 Server 比較獨立的常駐進程任務, 在 swoft 中, 在如下場景中使用到:

  • 協程定時器 CronTimerProcess
  • 協程執行命令 CronExecProcess
  • 熱更新進程 ReloadProcess

swoft 使用 \Swoft\ProcessSwoole\Process 進行了封裝:

// \Swoft\Process
public static function create(
    AbstractServer $server,
    string $processName,
    string $processClassName
) {
    ...

    // 建立進程
    $process = new SwooleProcess(function (SwooleProcess $process) use ($processClass, $processName) {
        // reload
        BeanFactory::reload();
        $initApplicationContext = new InitApplicationContext();
        $initApplicationContext->init();

        App::trigger(AppEvent::BEFORE_PROCESS, null, $processName, $process, null);
        PhpHelper::call([$processClass, 'run'], [$process]);
        App::trigger(AppEvent::AFTER_PROCESS);
    }, $iout, $pipe); // 啓動 \Swoole\Process 並綁定回調函數便可

    return $process;
}

SwooleAsync

swoft 在日誌場景下使用 Swoole\Async 來提升性能, 同時保留了原有的同步方式, 方便進行切換

// \Swoft\Log\FileHandler
private function aysncWrite(string $logFile, string $messageText)
{
    while (true) {
        $result = \Swoole\Async::writeFile($logFile, $messageText, null, FILE_APPEND); // 使用起來很簡單
        if ($result == true) {
            break;
        }
    }
}

SwooleEvent

服務器出於性能考慮, 一般都是 常駐內存 的, 傳統的 php-fpm 也是, 修改了配置須要 reload 服務器才能生效. 也由於此, 服務器領域出現了新的需求 -- 熱更新. swoole 在進程管理上已經作了不少優化, 這裏摘抄部分 wiki 內容:

Swoole提供了柔性終止/重啓的機制
SIGTERM: 向主進程/管理進程發送此信號服務器將安全終止
SIGUSR1: 向主進程/管理進程發送SIGUSR1信號,將平穩地restart全部worker進程

目前你們採用的, 比較常見的方案, 是基於 Linux Inotify 特性, 經過監測文件變動來觸發 swoole server reload. PHP 中有 Inotify 擴展, 方便使用, 具體實如今 Swoft\Base\Inotify 中:

public function run()
{
    $inotify = inotify_init();

    // 設置爲非阻塞
    stream_set_blocking($inotify, 0);

    $tempFiles = [];
    $iterator = new \RecursiveDirectoryIterator($this->watchDir);
    $files = new \RecursiveIteratorIterator($iterator);
    foreach ($files as $file) {
        $path = dirname($file);

        // 只監聽目錄
        if (!isset($tempFiles[$path])) {
            $wd = inotify_add_watch($inotify, $path, IN_MODIFY | IN_CREATE | IN_IGNORED | IN_DELETE);
            $tempFiles[$path] = $wd;
            $this->watchFiles[$wd] = $path;
        }
    }

    // swoole Event add
    $this->addSwooleEvent($inotify);
}

private function addSwooleEvent($inotify)
{
    // swoole Event add
    Event::add($inotify, function ($inotify) { // 使用 \Swoole\Event
        // 讀取有事件變化的文件
        $events = inotify_read($inotify);
        if ($events) {
            $this->reloadFiles($inotify, $events);
        }
    }, null, SWOOLE_EVENT_READ);
}

SwooleLock

swoft 在 CircuitBreaker(熔斷器) 中的 HalfOpenState(半開狀態) 使用到了, 而且這塊的實現比較複雜, 推薦閱讀源碼:

// CircuitBreaker
public function init()
{
    // 狀態初始化
    $this->circuitState = new CloseState($this);
    $this->halfOpenLock = new \Swoole\Lock(SWOOLE_MUTEX); // 初始化互斥鎖
}

// HalfOpenState
public function doCall($callback, $params = [], $fallback = null)
{
    // 加鎖
    $lock = $this->circuitBreaker->getHalfOpenLock();
    $lock->lock();
    list($class ,$method) = $callback;

    ....

    // 釋放鎖
    $lock->unlock();

    ...
}

鎖的使用, 難點主要在瞭解各類不一樣鎖使用的場景, 目前 swoole 支持:

  • 文件鎖 SWOOLE_FILELOCK
  • 讀寫鎖 SWOOLE_RWLOCK
  • 信號量 SWOOLE_SEM
  • 互斥鎖 SWOOLE_MUTEX
  • 自旋鎖 SWOOLE_SPINLOCK

SwooleTimer & SwooleTable

定時器基本都會使用到, phper 用的比較多的應該是 crontab 了. 基於這個考慮, swoft 對 Timer 進行了封裝, 方便 phper 用 熟悉的姿式 繼續使用.

swoft 對 Swoole\Timer 進行了簡單的封裝, 代碼在 \Base\Timer 中:

// 設置定時器
public function addTickTimer(string $name, int $time, $callback, $params = [])
{
    array_unshift($params, $name, $callback);

    $tid = \Swoole\Timer::tick($time, [$this, 'timerCallback'], $params);

    $this->timers[$name][$tid] = $tid;

    return $tid;
}

// 清除定時器
public function clearTimerByName(string $name)
{
    if (!isset($this->timers[$name])) {
        return true;
    }
    foreach ($this->timers[$name] as $tid => $tidVal) {
        \Swoole\Timer::clear($tid);
    }
    unset($this->timers[$name]);

    return true;
}

Swoole\Table 是在內存中開闢一塊區域, 實現相似關係型數據庫表(Table)這樣的數據結構, 關於 Swoole\Table 的實現原理, rango 寫過專門的文章 swoole_table 實現原理剖析, 推薦閱讀.

Swoole\Table 在使用上須要注意如下幾點:

  • 相似關係型數據庫, 須要提早定義好 表結構
  • 須要預先判斷數據的大小(行數)
  • 注意內存, swoole 會更根據上面 2 個定義, 在調用 \Swoole\Table->create() 時分配掉這些內存

swoft 中則是使用這一功能, 來實現 crontab 方式的任務調度:

private $originTable;
private $runTimeTable;

private $originStruct = [
    'rule'       => [\Swoole\Table::TYPE_STRING, 100],
    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],
    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],
    'add_time'   => [\Swoole\Table::TYPE_STRING, 11],
];

private $runTimeStruct = [
    'taskClass'  => [\Swoole\Table::TYPE_STRING, 255],
    'taskMethod' => [\Swoole\Table::TYPE_STRING, 255],
    'minte'      => [\Swoole\Table::TYPE_STRING, 20],
    'sec'        => [\Swoole\Table::TYPE_STRING, 20],
    'runStatus'  => [\Swoole\TABLE::TYPE_INT, 4],
];

// 使用 \Swoole\Table
private function createOriginTable(): bool
{
    $this->setOriginTable(new \Swoole\Table('origin', self::TABLE_SIZE, $this->originStruct));

    return $this->getOriginTable()->create();
}

寫在最後

老生常談了, 不少人吐槽 swoole 坑, 文檔很差. 說句實話, 要勇於直面本身服務器開發能力不足的現實. 我常常提的一句話:

要把 swoole 的 wiki 看 3 遍.

寫這篇 blog 的初衷是給你們介紹一下 swoole 在 swoft 中的應用場景, 幫助你們嘗試進行 swoole 落地. 但願這篇 blog 能對你有所幫助, 也但願你能多多關注 swoole 社區, 關注 swoft 框架, 能感覺到服務器開發帶來的樂趣.

相關文章
相關標籤/搜索