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ù)就在其中.
- 全局容器注入
在進(jìn)入這個(gè)概念之前, 先要認(rèn)清另一個(gè)概念: 面向?qū)ο缶幊?/strong>. 更常用的可能是 面向過(guò)程編程 vs 面向?qū)ο缶幊?/strong>. 這里不會(huì)長(zhǎng)篇大論, 只就思維方式來(lái)進(jìn)行比較:
- 面向過(guò)程編程: 一條接一條指令的執(zhí)行, 這是計(jì)算機(jī)喜歡的方式
- 面向?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 種形式,
item
和item/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ō)明一下
- 限制條件一: 需要 swoole2.0 的協(xié)程 server + 協(xié)程 client 配合
- 限制條件二: 在協(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)源可以算是一條捷徑.