swoft 源碼解讀

date: 2017-12-12 17:21:58
title: swoft 源碼解讀

官網(wǎng): https://www.swoft.org/

源碼解讀: http://naotu.baidu.com/file/814e81c9781b733e04218ac7a0494e2a?token=f009094c71a791c5

號(hào)外號(hào)外, 歡迎大家 star, 我們開(kāi)發(fā)組定了一個(gè) star 1000+ 就線下聚一次的小目標(biāo)

繼續(xù)源碼解讀系列. php 里面的 yii/laravel 框架算是非常「重」的了. 這里的 先不具體到 性能 層面, 主要是框架的設(shè)計(jì)思想和框架集成的服務(wù), 讓框架可以既可以快速解決很多問(wèn)題, 又可以輕松擴(kuò)展.

PHP 中的框架, 有 yii/laravel 在, 在復(fù)雜度上, 應(yīng)該無(wú)出其右了.

這次解讀 swoft 的源碼 -- 基于 swoole2.0 原生協(xié)程的框架. 同時(shí), swoft 使用了大量 swoole 提供的功能, 也非常適合閱讀它的代碼, 來(lái)學(xué)習(xí)如何造輪子. 其實(shí)解讀過(guò) yii/laravel 這樣的框架后, 一些 通用 的框架設(shè)計(jì)思想就不贅述了, 主要講解和 服務(wù)器開(kāi)發(fā) 相關(guān)的部分, 思路也會(huì)按照官網(wǎng)的 feature list 展開(kāi).

前半部分聚焦框架常用的功能:

  • 全局容器注入 & MVC 分層設(shè)計(jì)
  • 注解機(jī)制(亮點(diǎn), 強(qiáng)烈推薦了解一下)
  • 高性能路由
  • 別名機(jī)制 $aliases
  • RestFul風(fēng)格
  • 事件機(jī)制
  • 強(qiáng)大的日志系統(tǒng)
  • 國(guó)際化(i18n)
  • 數(shù)據(jù)庫(kù) ORM

后半部分聚焦服務(wù)器相關(guān)的功能:

  • 基礎(chǔ)概念(亮點(diǎn), 第一個(gè)基于 swoole2.0 原生協(xié)程的框架)
  • 連接池
  • 服務(wù)治理: 熔斷岂却、降級(jí)忿薇、負(fù)載、注冊(cè)與發(fā)現(xiàn)
  • 任務(wù)投遞 & Crontab 定時(shí)任務(wù)
  • 用戶自定義進(jìn)程
  • Inotify 自動(dòng) Reload

PHP 框架的設(shè)計(jì)過(guò)程中, 可以參考 PSR(PHP Standards Recommendations).

全局容器注入 & MVC 分層設(shè)計(jì)

之所以把這 2 個(gè)放一起講, 是因?yàn)橐粋€(gè)是 , 一個(gè)是 . 只是新人聽(tīng)得比較多的是 MVC 的分層設(shè)計(jì)思想, 全局容器注入了解相對(duì)較少.

  • MVC 分層設(shè)計(jì): 更偏向于業(yè)務(wù)

MVC 是一種簡(jiǎn)單通用并且實(shí)用的 對(duì)業(yè)務(wù)進(jìn)行拆分然后加以實(shí)現(xiàn) 的設(shè)計(jì), 本質(zhì)還是 分層設(shè)計(jì). 更重要的, 還是掌握 分層設(shè)計(jì) 的思想, 這個(gè)在工程實(shí)踐中大量的使用到, 著名的 OSI 7 層網(wǎng)絡(luò)模型 和 TCP/IP 4 層網(wǎng)絡(luò)模型. 分層設(shè)計(jì)可以有效的確定 系統(tǒng)邊界和職責(zé)劃分.

想要培養(yǎng)分層設(shè)計(jì)的思想, 其實(shí)可以從 入手, 在拆輪子然后拼輪子的過(guò)程中, 你會(huì)驚奇的發(fā)現(xiàn), 藝術(shù)就在其中.

榫卯: https://www.douban.com/note/373132669/

  • 全局容器注入

在進(jìn)入這個(gè)概念之前, 先要認(rèn)清另一個(gè)概念: 面向?qū)ο缶幊?/strong>. 更常用的可能是 面向過(guò)程編程 vs 面向?qū)ο缶幊?/strong>. 這里不會(huì)長(zhǎng)篇大論, 只就思維方式來(lái)進(jìn)行比較:

  1. 面向過(guò)程編程: 一條接一條指令的執(zhí)行, 這是計(jì)算機(jī)喜歡的方式
  2. 面向?qū)ο缶幊? 將不同的事物 抽象 為不同的對(duì)象, 通過(guò)事物(對(duì)象)之間的聯(lián)系, 來(lái)解決與之相關(guān)的業(yè)務(wù).

從這個(gè)角度來(lái)看, 面向?qū)ο?/strong> 可能是更符合人類的思維方式, 或者說(shuō)更智能的思維方式:

上者勞人. 控制(抽象)好對(duì)象, 從而更好的完成任務(wù).

但是使用面向?qū)ο缶幊痰倪^(guò)程中, 會(huì)出現(xiàn)一個(gè)問(wèn)題: new, 需要管理好對(duì)象之間依賴關(guān)系, 全局容器注入就是做這樣一件事. 使用 new, 表明一個(gè)對(duì)象需要依賴另一個(gè)對(duì)象, 但是使用容器, 則是一個(gè)對(duì)象告訴容器它需要什么對(duì)象.

怎么實(shí)現(xiàn)我不管 -- 這就是使用 new 和容器注入的區(qū)別, 學(xué)名叫 控制反轉(zhuǎn).

所以, 容器是 , 在處理具體業(yè)務(wù)時(shí), 由容器按需提供相應(yīng)的 MVC 對(duì)象來(lái)處理.

注解進(jìn)制

在容器的實(shí)現(xiàn)上, 或者說(shuō)框架的底層上, 其實(shí)各個(gè)框架都 大同小異. 這里說(shuō)一下 swoft 非常亮眼的地方 -- 引入注解機(jī)制.

簡(jiǎn)單解釋一下注解機(jī)制: 通過(guò)添加注釋 & 解析注釋, 將注釋轉(zhuǎn)化為一些特定的有意義的代碼.

更簡(jiǎn)單一點(diǎn): 注釋 == 代碼

實(shí)現(xiàn)起來(lái)其實(shí)也很簡(jiǎn)單, 只是可能接觸的比較少手段 -- 反射:

// Bean\Parser\InjectParser
class InjectParser extends AbstractParser
{

    /**
     * Inject注解解析
     *
     * @param string $className
     * @param object $objectAnnotation
     * @param string $propertyName
     * @param string $methodName
     *
     * @return array
     */
    public function parser(string $className, $objectAnnotation = null, string $propertyName = "", string $methodName = "", $propertyValue = null)
    {
        $injectValue = $objectAnnotation->getName();
        if (!empty($injectValue)) {
            return [$injectValue, true];
        }

        // phpdoc解析
        $phpReader = new PhpDocReader(); // 將注釋轉(zhuǎn)化為類
        $property = new \ReflectionProperty($className, $propertyName); // 使用反射, 獲取類的信息
        $propertyClass = $phpReader->getPropertyClass($property);

        $isRef = true;
        $injectProperty = $propertyClass;
        return [$injectProperty, $isRef];
    }
}

如果熟悉 java, 會(huì)發(fā)現(xiàn)里面有很多地方在方法前用到了 @override, 在 symfony 中也使用到了這樣的方式. 好處是一定程度的內(nèi)聚, 使用起來(lái)更加簡(jiǎn)潔, 而且可以減少配置.

高性能路由

首先回答一個(gè)問(wèn)題, 路由是什么? 從對(duì)象的角度出發(fā), 其實(shí)路由就對(duì)應(yīng) URL. 那 URL 是什么呢?

URL, Uniform Resource Locator, 統(tǒng)一資源定位符.

所以, 路由這一層抽象, 就是為了解決 -- 找到 URL 對(duì)應(yīng)需要執(zhí)行的邏輯.

現(xiàn)在再來(lái)解釋一下 swoft 提到的高性能:

// app/routes.php: 路由配置文件
$router = \Swoft\App::getBean('httpRouter'); // 通過(guò)容器拿 httpRouter

// config/beans/base.php: beans 配置文件
'httpRouter'      => [
    'class'          => \Swoft\Router\Http\HandlerMapping::class, // httpRouter 其實(shí)對(duì)應(yīng)這個(gè)
    'ignoreLastSep'  => false,
    'tmpCacheNumber' => 1000,
    'matchAll'       => '',
],

// \Swoft\Router\Http\HandlerMapping
private $cacheCounter = 0;
private $staticRoutes = []; // 靜態(tài)路由
private $regularRoutes = []; // 動(dòng)態(tài)路由
protected function cacheMatchedParamRoute($path, array $conf){} // 會(huì)緩存匹配到的路由
// 路由匹配的方法也很簡(jiǎn)單: 校驗(yàn) -> 處理靜態(tài)路由 -> 處理動(dòng)態(tài)路由
public function map($methods, $route, $handler, array $opts = [])
{
    ...
    $methods = static::validateArguments($methods, $handler);
    ...
    if (self::isNoDynamicParam($route)) {
        ...
    }
    ...
    list($first, $conf) = static::parseParamRoute($route, $params, $conf);
}

高性能 = 簡(jiǎn)單的匹配計(jì)算 + 路由緩存

別名機(jī)制 $aliases

用過(guò) yii 的對(duì)這個(gè)就比較熟悉了, 其實(shí)是這樣一個(gè) 進(jìn)化過(guò)程:

  • 使用 __DIR__ / DIRECTORY_SEPARATOR 等拼接出絕對(duì)路徑
  • 使用 define() / defined() 定義全局變量來(lái)使用絕對(duì)路徑
  • 使用 $aliases 變量替代全局變量

這里只展示一下配置的地方, 實(shí)現(xiàn)很簡(jiǎn)單, 在 App 類中使用 $aliases 屬性維護(hù):

// config/define.php
// 基礎(chǔ)根目錄
!defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
// 注冊(cè)別名
$aliases = [
    '@root'       => BASE_PATH,
    '@app'        => '@root/app',
    '@res'        => '@root/resources',
    '@runtime'    => '@root/runtime',
    '@configs'    => '@root/config',
    '@resources'  => '@root/resources',
    '@beans'      => '@configs/beans',
    '@properties' => '@configs/properties',
    '@commands'   => '@app/Commands'
];
App::setAliases($aliases);

RestFul風(fēng)格

restful 的思想可簡(jiǎn)單概括: 以資源為核心, 業(yè)務(wù)其實(shí)是圍繞資源的增刪改查.

具體到 http 中:

  • url 只作為資源標(biāo)識(shí), 有 2 種形式, itemitem/id, 后者表示操作某個(gè)具體資源
  • http method(get/post/put等)用來(lái)對(duì)應(yīng)資源的 CRUD
  • 使用 json 格式進(jìn)行數(shù)據(jù)的 輸入輸出

swoft 中實(shí)現(xiàn)起來(lái)只需要修改 2 出地方: 修改路由匹配 + 修改請(qǐng)求數(shù)據(jù)的解析

事件機(jī)制

先用 3W1H(who what why how) 分析法的思路來(lái)解釋一下 事件機(jī)制, 更重要的是, 這個(gè)有什么用.

正常的程序執(zhí)行, 或者說(shuō)人的思維趨勢(shì), 都是按照 時(shí)間線性串行 的, 保持 連續(xù)性. 不過(guò)現(xiàn)實(shí)中會(huì)存在各種 打斷, 程序也不是永遠(yuǎn)都是 就緒狀態(tài), 那么, 就需要有一種機(jī)制, 來(lái)處理可能出現(xiàn)的各種打斷, 或者在程序不同狀態(tài)之間切換.

事件機(jī)制發(fā)展到現(xiàn)在, 有時(shí)候也被用作一種預(yù)留手段, 根據(jù)你的經(jīng)驗(yàn)在需要的地方 埋點(diǎn), 方便之后 打補(bǔ)丁.

swoft 的事件機(jī)制基于 PSR-14 實(shí)現(xiàn), 高度內(nèi)聚簡(jiǎn)潔.

由三部分組成:

  • EventManager: 事件管理器
  • Event: 事件
  • EventHandler / Listener: 事件處理器/監(jiān)聽(tīng)器

執(zhí)行流程:

  • 先生成 EventManager
  • 將 Event 和 EventHandler 注冊(cè)到 EventManager
  • 觸發(fā) Event, EventManager 就會(huì)調(diào)用相應(yīng)的 EventHandler

使用起來(lái)就更加簡(jiǎn)單了:

use Swoft\Event\EventManager;

$em = new EventManager;

// 注冊(cè)事件監(jiān)聽(tīng)
$em->attach('someEvent', 'callback_handler'); // 這里也可以使用注解機(jī)制, 實(shí)現(xiàn)事件監(jiān)聽(tīng)注冊(cè)

// 觸發(fā)事件
$em->trigger('someEvent', 'target', ['more params']);

// 也可以
$event = new Event('someEvent', ['more params']);
$em->trigger($event);

來(lái)看一下 swoft 在事件機(jī)制中用來(lái)提升性能的地方:

namespace Swoft\Event;

class ListenerQueue implements \IteratorAggregate, \Countable
{
    protected $store;

    /**
     * 優(yōu)先級(jí)隊(duì)列
     * @var \SplPriorityQueue
     */
    protected $queue;

    /**
     * 計(jì)數(shù)器
     * 設(shè)定最大值為 PHP_INT_MAX == 300
     * @var int
     */
    private $counter = PHP_INT_MAX;

    public function __construct()
    {
        $this->store = new \SplObjectStorage(); // Event 對(duì)象先添加都這里
        $this->queue = new \SplPriorityQueue(); // 然后加入優(yōu)先級(jí)隊(duì)列, 之后進(jìn)行調(diào)度
    }
    ...
}

稍微玩過(guò) ACM 的人對(duì) 優(yōu)先級(jí)隊(duì)列 就不會(huì)陌生了, 基本所有 OJ 都有相關(guān)的題庫(kù). 不過(guò) PHPer 不用太操心底層實(shí)現(xiàn), 直接借助 SPL 庫(kù)即可.

SPL, Standard PHP Library, 類似 C++ 的 STL, PHPer 一定要了解一下.

強(qiáng)大的日志系統(tǒng)

使用 monolog/monolog 來(lái)實(shí)現(xiàn)日志系統(tǒng)基本已成為標(biāo)配了, 當(dāng)然底層還是實(shí)現(xiàn) PSR-3 標(biāo)準(zhǔn). 不過(guò)這個(gè)標(biāo)準(zhǔn)出現(xiàn)比較早, 發(fā)展到現(xiàn)在, 隱藏得比較深了.

這也是建立技術(shù)標(biāo)準(zhǔn)/協(xié)議的理由, 劃定好 最佳實(shí)踐, 之后的努力都是朝著越來(lái)越易用發(fā)展.

swoft 的日志系統(tǒng), 由 2 部分組成:

  • Swoft\Log\Logger: 日志主體功能
  • Swoft\Log\FileHandler: 輸出日志

至于另一個(gè)文件, Swoft\Log\Log, 只是對(duì) Logger 的一層封裝, 調(diào)用起來(lái)更方便而已.

swoft 的日志系統(tǒng)和 yii2 框架有明顯相似的地方:

// 都在 App 中暴露日志功能
public static function info($message, array $context = array())
{
    self::getLogger()->info($message, $context); // 其實(shí)還是使用 Logger 來(lái)處理
}

// 都添加了 profile 功能
public static function profileStart(string $name)
{
    self::getLogger()->profileStart($name);
}
public static function profileEnd($name)
{
    self::getLogger()->profileEnd($name);
}

值得一提的是, yii2 框架的日志系統(tǒng)由三部分組成:

  • Logger: 日志主體功能
  • Dispatch: 日志分發(fā), 可以將同一個(gè)日志分發(fā)給不同的 Target 處理
  • Target: 日志消費(fèi)者

這樣的設(shè)計(jì), 其實(shí)是將 FileHandler 的功能進(jìn)行拆解, 更靈活, 更方便擴(kuò)展.

來(lái)看看 swoft 日志系統(tǒng)強(qiáng)大的一面:

private function aysncWrite(string $logFile, string $messageText)
{
    while (true) {
        // 使用 swoole 異步文件 IO
        $result = \Swoole\Async::writeFile($logFile, $messageText, null, FILE_APPEND);
        if ($result == true) {
            break;
        }
    }
}

也可以選擇同步的方式:

private function syncWrite(string $logFile, string $messageText)
{
    $fp = fopen($logFile, 'a');
    if ($fp === false) {
        throw new \InvalidArgumentException("Unable to append to log file: {$this->logFile}");
    }
    flock($fp, LOCK_EX); // 注意要加鎖
    fwrite($fp, $messageText);
    flock($fp, LOCK_UN);
    fclose($fp);
}

PS: 日志統(tǒng)計(jì)分析功能開(kāi)發(fā)團(tuán)隊(duì)正在開(kāi)發(fā)中, 歡迎大家推薦方案

國(guó)際化(i18n)

這個(gè)功能的實(shí)現(xiàn)比較簡(jiǎn)單, 不過(guò) i18n 這個(gè)詞倒是可以多講一句, 原詞是 internationalization, 不過(guò)實(shí)在太長(zhǎng)了, 所以簡(jiǎn)寫為 i18n, 類似的還有 kubernetes -> k8s.

數(shù)據(jù)庫(kù) ORM

ORM 這個(gè)發(fā)展很也成熟了, 看清楚下面的進(jìn)化史就好了:

  • Statement: 直接執(zhí)行 sql 語(yǔ)句
  • QueryBuild: 使用鏈?zhǔn)秸{(diào)用, 來(lái)實(shí)現(xiàn)拼接 sql 語(yǔ)句, 最后還是 Statement 這樣的執(zhí)行, 可能對(duì)返回值再封裝一下
  • ActiveRecord: Model, 用來(lái)映射數(shù)據(jù)庫(kù)中的表, 實(shí)際還是封裝的 QueryBuild, 會(huì)封裝一層來(lái)處理 sql 返回的數(shù)據(jù)和 Model 的屬性

這一層層的封裝好處也很明顯, 減少 sql 的存在感(隱藏復(fù)雜度).

// insert
$post = new Post();
$post->title = 'daydaygo';
$post->save();

// query
$post = Post::find(1);

// update
$post->content = 'coder at work';
$post->save();

// delete
$post->del();

要實(shí)現(xiàn)這樣的效果, 還是有一定的代碼量的, 也會(huì)遇到一些問(wèn)題, 比如 代碼提示, 還有一些更高級(jí)的功能, 比如 關(guān)聯(lián)查詢

基本概念

  • 并發(fā) vs 并行

抓住 并行 這個(gè)范圍更小的概念就容易理解了, 并行是要 同時(shí)執(zhí)行, 那么只能多 cpu 核心同時(shí)運(yùn)算才行; 并發(fā)則是因?yàn)?cpu運(yùn)行和切換速度快, 時(shí)間段內(nèi)執(zhí)行多個(gè)程序, 宏觀上 看起來(lái) 像在同時(shí)執(zhí)行

  • 協(xié)程 vs 進(jìn)程

一種簡(jiǎn)單的說(shuō)法 協(xié)程是用戶態(tài)的線程. 線程由操作系統(tǒng)進(jìn)行調(diào)度, 可以自動(dòng)調(diào)度到多 cpu 上執(zhí)行; 同一個(gè)時(shí)刻同一個(gè) cpu 核心上只有一個(gè)協(xié)程運(yùn)行, 當(dāng)遇到用戶代碼中的阻塞 IO 時(shí), 底層調(diào)度器會(huì)進(jìn)入事件循環(huán), 達(dá)到 協(xié)程由用戶調(diào)度 的效果

  • swoole2.0 原生協(xié)程

具體的實(shí)現(xiàn)原理大家到官網(wǎng)查看, 會(huì)有更詳細(xì)的 wiki 說(shuō)明, 我這里從 工具 使用的角度來(lái)說(shuō)明一下

  1. 限制條件一: 需要 swoole2.0 的協(xié)程 server + 協(xié)程 client 配合
  2. 限制條件二: 在協(xié)程 server 的 onRequet, onReceive, onConnect 事件回調(diào)中才能使用
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

// 1: 創(chuàng)建一個(gè)協(xié)程
$server->on('Request', function($request, $response) {
    $mysql = new Swoole\Coroutine\MySQL();
    // 協(xié)程 client 有阻塞 IO 操作, 觸發(fā)協(xié)程調(diào)度
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    ]);
    // 阻塞 IO 事件就緒, 協(xié)程恢復(fù)執(zhí)行
    if ($res == false) {
        $response->end("MySQL connect fail!");
        return;
    }
    // 出現(xiàn)阻塞 IO, 繼續(xù)協(xié)程調(diào)度
    $ret = $mysql->query('show tables', 2);
    $response->end("swoole response is ok, result=".var_export($ret, true));
});

$server->start();

注意: 觸發(fā)一次回調(diào)函數(shù), 就會(huì)在開(kāi)始的時(shí)候生成一個(gè)協(xié)程, 結(jié)束的時(shí)候銷毀這個(gè)協(xié)程, 協(xié)程的生命周期, 伴隨此處回調(diào)函數(shù)執(zhí)行的生命周期

連接池

swoft 的連接池功能實(shí)現(xiàn), 主要在 src/Pool 下, 主要由三部分組成:

  • Connect: 連接, 值得一提的是, 為了后續(xù)使用方便, 這里同時(shí)配置了 同步連接 + 異步連接
  • Balancer: 負(fù)載均衡器, 目前提供 2 種策略, 隨機(jī)數(shù) + 輪詢
  • Pool: 連接池, 核心部分, 負(fù)責(zé)連接的管理和調(diào)度

PS: 自由切換同步/異步客戶端只需要切換一下連接就好

直接上代碼:

// 使用 SqlQueue 來(lái)管理連接
public function getConnect()
{
    if ($this->queue == null) {
        $this->queue = new \SplQueue(); // 又見(jiàn) Spl
    }

    $connect = null;
    if ($this->currentCounter > $this->maxActive) {
        return null;
    }
    if (!$this->queue->isEmpty()) {
        $connect = $this->queue->shift(); // 有可用連接, 直接取
        return $connect;
    }

    $connect = $this->createConnect();
    if ($connect !== null) {
        $this->currentCounter++;
    }
    return $connect;
}

// 如果接入了服務(wù)治理, 將使用調(diào)度器
public function getConnectAddress()
{
    $serviceList = $this->getServiceList(); // 從 serviceProvider 那里獲取到服務(wù)列表
    return $this->balancer->select($serviceList); // 使用 balancer 調(diào)度
}

服務(wù)治理熔斷躏哩、降級(jí)署浩、負(fù)載、注冊(cè)與發(fā)現(xiàn)

swoft 的服務(wù)治理相關(guān)的功能, 主要在 src/Service 下:

  • Packer: 封包器, 和協(xié)議進(jìn)行對(duì)應(yīng), 看過(guò) swoole 文檔的同學(xué), 就能知道協(xié)議的作用了
  • ServiceProvider: 服務(wù)提供者, 用來(lái)對(duì)接第三方服務(wù)管理方案, 目前已實(shí)現(xiàn) Consul
  • Service: RPC服務(wù)調(diào)用, 包含同步調(diào)用和協(xié)程調(diào)用(deferCall()), 目前采用 callback 實(shí)現(xiàn)簡(jiǎn)單的 降級(jí)
  • ServiceConnect: 連接池中 Connect 的 RPC Service 實(shí)現(xiàn), 不過(guò)個(gè)人認(rèn)為放到連接池中實(shí)現(xiàn)更好
  • Circuit: 熔斷, 在 src/Circuit 中實(shí)現(xiàn), 有三種狀態(tài), 關(guān)閉/開(kāi)啟/半開(kāi)
  • DispatcherService: 服務(wù)調(diào)度器, 在 Service 之前封裝一層, 添加 Middleware/Event 等功能

這里看看熔斷這部分的代碼, 半開(kāi)狀態(tài)的邏輯復(fù)雜一些, 值得參考:

// Swoft\Circuit\CircuitBreaker
public function init()
{
    // 狀態(tài)初始化
    $this->circuitState = new CloseState($this);
    $this->halfOpenLock = new \swoole_lock(SWOOLE_MUTEX); // 使用 swoole lock
}

// Swoft\Circuit\HalfOpenState
public function doCall($callback, $params = [], $fallback = null)
{
    // 加鎖
    $lock = $this->circuitBreaker->getHalfOpenLock();
    $lock->lock();
    ...
    // 釋放鎖
    $lock->unlock();
}

任務(wù)投遞 & Crontab 定時(shí)任務(wù)

swoft 任務(wù)投遞的實(shí)現(xiàn)機(jī)制當(dāng)然離不開(kāi) Swoole\Timer::tick()(\Swoole\Server->task() 底層執(zhí)行機(jī)制是一樣的) , swoft 在實(shí)現(xiàn)的時(shí)候, 添加了 喜聞樂(lè)見(jiàn) 的 crontab 方式, 實(shí)現(xiàn)在 src/Crontab 下:

  • ParseCrontab: 解析 crontab
  • TableCrontab: 使用 Swoole\Table 實(shí)現(xiàn), 用來(lái)存儲(chǔ) crontab 任務(wù)
  • Crontab: 連接 Task 和 TableCrontab

這里主要看一下 TableCrontab:

// 存儲(chǔ)原始的任務(wù)
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]
];
// 存儲(chǔ)解析后的任務(wù)
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]
];

用戶自定義進(jìn)程

自定義進(jìn)程對(duì) \Swoole\Process 的封裝, swoft 封裝之后, 想要使用用戶自定義進(jìn)程更簡(jiǎn)單了:

繼承 AbstractProcess 類, 并實(shí)現(xiàn) run() 來(lái)執(zhí)行業(yè)務(wù)邏輯.

swoft 中功能實(shí)現(xiàn)在 src/Process 下, 框架自帶三個(gè)自定義進(jìn)程:

  • Reload: 配合 ext-inotify 擴(kuò)展實(shí)現(xiàn)自動(dòng) reload, 下面會(huì)具體講解
  • CronTimer: crontab 里的 task 在這里觸發(fā) \Swoole\Server->tick()
  • CronExec: 實(shí)現(xiàn)協(xié)程 task, 實(shí)現(xiàn)中.

代碼就不貼了, 這里再擴(kuò)展一個(gè)比較適合使用自定義進(jìn)程的場(chǎng)景: 訂閱服務(wù)

Inotify 自動(dòng) Reload

服務(wù)器程序大都是常駐進(jìn)程, 有效減少對(duì)象的生成和銷毀, 提高性能, 但是這樣也給服務(wù)器程序的開(kāi)發(fā)帶來(lái)了問(wèn)題, 需要 reload 來(lái)查看生效后的程序. 使用 ext-inotify 擴(kuò)展可以解決這個(gè)問(wèn)題.

直接上代碼, 看看 swoft 中的實(shí)現(xiàn):

// Swoft\Process\ReloadProcess
public function run(Process $process)
{
    $pname = $this->server->getPname();
    $processName = "$pname reload process";
    $process->name($processName);

    /* @var Inotify $inotify */
    $inotify = App::getBean('inotify'); // 使用自定義進(jìn)程來(lái)啟動(dòng) inotify
    $inotify->setServer($this->server);
    $inotify->run();
}

// Swoft\Base\Inotify
public function run()
{

    $inotify = inotify_init(); // 使用 inotify 擴(kuò)展

    // 設(shè)置為非阻塞
    stream_set_blocking($inotify, 0);

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

        // 只監(jiān)聽(tīng)目錄
        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
    swoole_event_add($inotify, function ($inotify) { // 使用 \Swoole\Event
        // 讀取有事件變化的文件
        $events = inotify_read($inotify);
        if ($events) {
            $this->reloadFiles($inotify, $events); // 監(jiān)聽(tīng)到文件變動(dòng)進(jìn)行更新
        }
    }, null, SWOOLE_EVENT_READ);
}

寫在最后

再補(bǔ)充一點(diǎn), 在實(shí)現(xiàn)服務(wù)管理(reload stop)時(shí), 使用的 posix_kill(pid, sig);, 并不是用 \Swoole\Server 中自帶的 reload() 方法, 因?yàn)槲覀儺?dāng)前環(huán)境的上下文并不一定在\Swoole\Server 中.

想要做好一個(gè)框架, 尤其是一個(gè)開(kāi)源框架, 實(shí)際上要比我們平時(shí)寫 業(yè)務(wù)代碼 要難很多, 一方面是業(yè)務(wù)初期的 多快好省, 往往要上一些 能跑 的代碼. 這里引入一些關(guān)于代碼的觀點(diǎn):

  • 代碼質(zhì)量: bug 率 + 性能
  • 代碼規(guī)范: 形成規(guī)范可以提高代碼開(kāi)發(fā)/使用的體驗(yàn)
  • 代碼復(fù)用: 這是軟件工程的難題, 需要慢慢積累, 有些地方可以通過(guò)遵循規(guī)范走走捷徑

總結(jié)起來(lái)就一句話:

想要顯著提高編碼水平或者快速積累相關(guān)技術(shù)知識(shí), 參與開(kāi)源可以算是一條捷徑.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扫尺,一起剝皮案震驚了整個(gè)濱河市筋栋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌正驻,老刑警劉巖弊攘,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抢腐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡襟交,警方通過(guò)查閱死者的電腦和手機(jī)迈倍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)捣域,“玉大人啼染,你說(shuō)我怎么就攤上這事【顾危” “怎么了提完?”我有些...
    開(kāi)封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)丘侠。 經(jīng)常有香客問(wèn)我徒欣,道長(zhǎng),這世上最難降的妖魔是什么蜗字? 我笑而不...
    開(kāi)封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任打肝,我火速辦了婚禮,結(jié)果婚禮上挪捕,老公的妹妹穿的比我還像新娘粗梭。我一直安慰自己,他們只是感情好级零,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布断医。 她就那樣靜靜地躺著,像睡著了一般奏纪。 火紅的嫁衣襯著肌膚如雪鉴嗤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天序调,我揣著相機(jī)與錄音醉锅,去河邊找鬼。 笑死发绢,一個(gè)胖子當(dāng)著我的面吹牛硬耍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播边酒,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼经柴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了墩朦?” 一聲冷哼從身側(cè)響起口锭,我...
    開(kāi)封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后鹃操,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體韭寸,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年荆隘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恩伺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椰拒,死狀恐怖晶渠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情燃观,我是刑警寧澤褒脯,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站缆毁,受9級(jí)特大地震影響番川,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜脊框,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一颁督、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浇雹,春花似錦沉御、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至烂完,卻和暖如春试疙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窜护。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留非春,地道東北人柱徙。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像奇昙,于是被迫代替她去往敵國(guó)和親护侮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容