官網: https://www.swoft.org/源碼解讀: http://naotu.baidu.com/file/8...php
號外號外, 歡迎你們 star, 咱們開發組定了一個 star 1000+ 就線下聚一次的小目標java
PHP 裏面的 yii/laravel 框架算是很是「重」的了. 這裏的 重 先不具體到 性能 層面, 主要是框架的設計思想和框架集成的服務, 讓框架能夠既能夠快速解決不少問題, 又能夠輕鬆擴展.mysql
PHP 中的框架, 有 yii/laravel 在, 應該無出其右了.
此次解讀 swoft 的源碼 -- 基於 swoole2.0 原生協程的框架. 同時, swoft 使用了大量 swoole 提供的功能, 也很是適合閱讀它的代碼, 來學習如何造輪子. 其實解讀過 yii/laravel 這樣的框架後, 一些 通用 的框架設計思想就不贅述了, 主要講解和 服務器開發 相關的部分, 思路也會按照官網的 feature list 展開.laravel
前半部分聚焦框架經常使用的功能:git
$aliases
後半部分聚焦服務器相關的功能:github
PHP 框架的設計, 能夠參考 [PSR(PHP Standards Recommendations
)]( http://www.php-fig.org/psr/).
之因此把這 2 個放一塊兒講, 是由於一個是 裏, 一個是 表. 只是新人聽得比較多的是 MVC 的分層設計思想, 全局容器注入瞭解相對較少.sql
MVC 是一種簡單通用而且實用的 對業務進行拆分而後加以實現 的設計, 本質仍是 分層設計. 更重要的, 仍是掌握 分層設計 的思想, 這個在工程實踐中大量的使用到, 好比 OSI 7 層網絡模型 和 TCP/IP 4 層網絡模型. 我分層設計能夠有效的肯定 系統邊界和職責劃分.數據庫
想要培營養層設計的思想, 其實能夠從 拆 入手, 在拆輪子而後拼輪子的過程當中, 你會驚奇的發現, 藝術就在其中.編程
榫卯 app: https://www.douban.com/note/3...
在進入這個概念以前, 先要認清另外一個概念: 面向對象編程. 更經常使用的多是 面向過程編程 vs 面向對象編程. 這裏不會長篇大論, 只就思惟方式來進行比較:json
從這個角度來看, 面向對象 多是更符合人類的思惟方式, 或者說更智能的思惟方式:
上者勞人. 抽象好管理對象, 從而更好的完成任務.
可是使用面向對象編程的過程當中, 就會出現一個問題: new
, 須要管理好對象之間依賴關係, 全局容器注入就是作這樣一件事. 使用 new
, 代表一個對象須要依賴另外一個對象, 可是使用容器, 則是一個對象告訴容器它須要什麼對象.
怎麼實現我無論 -- 這就是使用
new
和容器注入的區別, 學名叫
控制反轉.
因此, 容器是 裏, 在處理具體業務時, 由容器按需提供相應的 MVC 對象來處理.
在容器的實現上, 或者說框架的底層上, 其實各個框架都 大同小異. 這裏說一下 swoft 不一樣的地方 -- 引入註解進制.
簡單解釋一下註解進制: 經過添加註釋 & 解析註釋, 將註釋轉化爲一些特定的有意義的代碼.
更簡單一點: 註釋 == 代碼
實現起來其實也很簡單, 只是可能接觸的比較少 -- 反射:
// 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(); // 將註釋轉化爲類 $property = new \ReflectionProperty($className, $propertyName); // 使用反射 $propertyClass = $phpReader->getPropertyClass($property); $isRef = true; $injectProperty = $propertyClass; return [$injectProperty, $isRef]; } }
若是熟悉 java, 會發現裏面有不少地方在方法前用到了 @override
, 在 symfony 中也使用到了這樣的方式. 好處是必定程度的內聚, 使用起來更加簡潔, 並且能夠減小配置.
首先回答一個問題, 路由是什麼? 從對象的角度出發, 其實路由就對應 URL. 那 URL 是什麼呢?
URL, Uniform Resource Locator, 統一資源定位符.
因此, 路由這一層抽象, 就是爲了解決 -- 找到 URL 對應須要執行的邏輯.
如今再來解釋一下 swoft 提到的高性能:
// app/routes.php: 路由配置文件 $router = \Swoft\App::getBean('httpRouter'); // 經過容器拿 httpRouter // config/beans/base.php: beans 配置文件 'httpRouter' => [ 'class' => \Swoft\Router\Http\HandlerMapping::class, // httpRouter 其實對應這個 'ignoreLastSep' => false, 'tmpCacheNumber' => 1000, 'matchAll' => '', ], // \Swoft\Router\Http\HandlerMapping private $cacheCounter = 0; private $staticRoutes = []; // 靜態路由 private $regularRoutes = []; // 動態路由 protected function cacheMatchedParamRoute($path, array $conf){} // 會緩存匹配到的路由 // 路由匹配的方法也很簡單: 校驗 -> 處理靜態路由 -> 處理動態路由 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); }
高性能 = 路由匹配邏輯簡單 + 路由緩存
$aliases
用過 yii 的對這個就比較熟悉了, 實際上是這樣一個 進化過程:
__DIR__
/ DIRECTORY_SEPARATOR
等拼接出絕對路徑define() / defined()
定義全局變量來使用路徑$aliases
變量替代全局變量這裏只展現一下配置的地方, 實現只是在類中開一個變 $aliases
屬性存儲一下就好了:
// config/define.php // 基礎根目錄 !defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1)); // 註冊別名 $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 的思想其實很簡單: 以資源爲核心, 業務實際上是圍繞資源的增刪改查. 具體到 http 中:
item
和 item/id
, 後者表示操做具體某個資源實現起來也很簡單: 路由 + 返回
先用 3W1H(who what why how) 分析法的思路來解釋一下 事件機制, 更重要的是, 這個有什麼用.
正常的程序執行, 或者說人的思惟趨勢, 都是按照 時間線性串行 的, 保持 連續性. 不過現實中會存在各類 打斷, 程序也不是永遠都是 就緒狀態, 那麼, 就須要有一種機制, 來處理可能出現的各類打斷, 或者在程序不一樣狀態之間切換.
事件機制發展到如今, 有時候也算是一種預留手段, 根據你的經驗在須要的地方 埋點, 方便以後 打補丁.
swoft 的事件機制基於 PSR-14 實現, 高度內聚簡潔.
由三部分組成:
執行流程:
使用起來就更加簡單了:
use Swoft\Event\EventManager; $em = new EventManager; // 註冊事件監聽 $em->attach('someEvent', 'callback_handler'); // 這裏也可使用註解機制, 實現事件監聽註冊 // 觸發事件 $em->trigger('someEvent', 'target', ['more params']); // 也能夠 $event = new Event('someEvent', ['more params']); $em->trigger($event);
來看一下 swoft 在事件機制這裏用來提高性能的亮點:
namespace Swoft\Event; class ListenerQueue implements \IteratorAggregate, \Countable { protected $store; /** * 優先級隊列 * @var \SplPriorityQueue */ protected $queue; /** * 計數器 * 設定最大值爲 PHP_INT_MAX == 300 * @var int */ private $counter = PHP_INT_MAX; public function __construct() { $this->store = new \SplObjectStorage(); // Event 對象先添加都這裏 $this->queue = new \SplPriorityQueue(); // 而後加入優先級隊列, 以後進行調度 } ... }
稍微玩過 ACM 的人對 優先級隊列 就不會陌生了, 基本全部 OJ 都有相關的題庫. 不過 PHPer 不用太操心底層實現, 直接藉助 SPL 庫便可.
SPL, Standard PHP Library, 相似 C++ 的 STL, PHPer 必定要了解一下.
使用 monolog/monolog 來實現日誌系統基本已成爲標配了, 固然底層仍是實現 PSR-3 標準. 不過這個標準出現比較早, 發展到如今, 隱藏得比較深了.
這也是創建技術標準/協議的理由, 劃定好 最佳實踐, 以後的努力都是朝着愈來愈易用發展.
swoft 的日誌系統, 由 2 部分組成:
Swoft\Log\Logger
: 日誌主體功能Swoft\Log\FileHandler
: 輸出日誌至於另外一個文件, Swoft\Log\Log
, 只是對 Logger 的一層封裝, 調用起來更方便而已.
固然, swoft 的日誌系統和 yii2 框架有明顯類似的地方:
// 都在 App 中快讀暴露日誌功能 public static function info($message, array $context = array()) { self::getLogger()->info($message, $context); // 其實仍是使用 Logger 來處理 } // 都添加了 profile 功能 public static function profileStart(string $name) { self::getLogger()->profileStart($name); } public static function profileEnd($name) { self::getLogger()->profileEnd($name); }
值得一提的是, yii2 框架的日誌系統由三部分組成:
Logger
: 日誌主體功能Dispatch
: 日誌分發, 能夠將同一個日誌分發給不一樣的 Target 處理Target
: 日誌消費者這樣的設計, 實際上是將 FileHandler
的功能進行拆解, 更靈活, 更方便擴展.
來看看 swoft 日誌系統強大的一面:
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: 日誌統計分析功能開發團隊正在開發中, 歡迎你們推薦方案~
這個功能的實現比較簡單, 不過 i18n 這個詞卻是能夠多講一句, 原詞是 internationalization
, 不過實在太長了, 因此簡寫爲 i18n
, 相似的還有 kubernetes -> k8s
.
ORM 這個發展很也成熟了, 看清楚下面的進化史就行了:
固然這一層層的封裝好處也很明顯, 減小 sql 的存在感.
// 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();
要實現這樣的效果, 仍是有必定的代碼量的, 也會遇到一些問題, 好比 代碼提示, 還有一些更高級的功能, 好比 關聯查詢
抓住 並行 這個範圍更小的概念就容易理解了, 並行是要 同時執行, 那麼只能多 cpu 核心同時運算才行; 併發則是由於 cpu運行和切換速度快, 時間段內執行多個程序, 宏觀上 看起來 像在同時執行
一種簡單的說法 協程是用戶態的線程. 線程由操做系統進行調度, 能夠自動調度到多 cpu 上執行; 同一個時刻同一個 cpu 核心上只有一個協程運行, 當遇到用戶代碼中的阻塞 IO 時, 底層調度器會進入事件循環, 達到 協程由用戶調度 的效果
具體的實現原理你們到官網查看, 會有更詳細的 wiki 說明, 我這裏從 工具 使用的角度來講明一下
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE); // 1: 建立一個協程 $server->on('Request', function($request, $response) { $mysql = new Swoole\Coroutine\MySQL(); // 協程 client 有阻塞 IO 操做, 觸發協程調度 $res = $mysql->connect([ 'host' => '127.0.0.1', 'user' => 'root', 'password' => 'root', 'database' => 'test', ]); // 阻塞 IO 事件就緒, 協程恢復執行 if ($res == false) { $response->end("MySQL connect fail!"); return; } // 出現阻塞 IO, 繼續協程調度 $ret = $mysql->query('show tables', 2); $response->end("swoole response is ok, result=".var_export($ret, true)); }); $server->start();
注意: 觸發一次回調函數, 就會在開始的時候生成一個協程, 結束的時候銷燬這個協程, 協程的生命週期, 伴隨此處回調函數執行的生命週期
swoft 的鏈接池功能實現, 主要在 src/Pool
下, 主要由三部分組成:
PS: 自由切換同步/異步客戶端很是簡單, 切換一下鏈接就好
直接上代碼:
// 使用 SqlQueue 來管理鏈接 public function getConnect() { if ($this->queue == null) { $this->queue = new \SplQueue(); // 又見 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; } // 若是接入了服務治理, 將使用調度器 public function getConnectAddress() { $serviceList = $this->getServiceList(); // 從 serviceProvider 那裏獲取到服務列表 return $this->balancer->select($serviceList); }
swoft 的服務治理相關的功能, 主要在 src/Service
下:
deferCall()
), 目前添加 callback 實現簡單的 降級 src/Circuit
中實現, 有三種狀態, 關閉/開啓/半開這裏看看熔斷這部分的代碼, 半開狀態的邏輯複雜一些, 值得參考:
// Swoft\Circuit\CircuitBreaker public function init() { // 狀態初始化 $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(); }
swoft 任務投遞的實現機制固然離不開 Swoole\Timer::tick()
(\Swoole\Server->task()
底層執行機制是同樣的) , swoft 在實現的時候, 添加了 喜聞樂見 的 crontab 方式, 實如今 src/Crontab
下:
Swoole\Table
實現, 用來存儲 crontab 任務這裏主要看一下 TableCrontab:
// 存儲原始的任務 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\Process
的封裝, swoft 封裝以後, 想要使用用戶自定義進程更簡單了:
繼承 AbstractProcess
類, 並實現 run()
來執行業務邏輯.
swoft 中功能實如今 src/Process
下, 框架自帶三個自定義進程:
ext-inotify
擴展實現自動 reload, 下面會具體講解\Swoole\Server->tick()
代碼就不貼了, 這裏再擴展一個比較適合使用自定義進程的場景: 訂閱服務
服務器程序大都是常駐進程, 有效減小對象的生成和銷燬, 提供性能, 可是這樣也給服務器程序的開發帶來了問題, 須要 reload 來查看生效後的程序. 使用 ext-inotify
擴展能夠解決這個問題.
直接上代碼, 看看 swoft 中的實現:
// 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'); // 自定義進程來啓動 inotify $inotify->setServer($this->server); $inotify->run(); } // Swoft\Base\Inotify public function run() { $inotify = inotify_init(); // 使用 inotify 擴展 // 設置爲非阻塞 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 swoole_event_add($inotify, function ($inotify) { // 使用 \Swoole\Event // 讀取有事件變化的文件 $events = inotify_read($inotify); if ($events) { $this->reloadFiles($inotify, $events); // 監聽到文件變更進行更新 } }, null, SWOOLE_EVENT_READ); }
再補充一點, 在實現服務管理(reload stop)時, 使用的 posix_kill(pid, sig);
, 並非用 \Swoole\Server
中自帶的 reload()
方法, 由於咱們當前環境的上下文並不必定在\Swoole\Server
中.
想要作好一個框架, 尤爲是一個開源框架, 實際上要比咱們平時寫 業務代碼 要難不少, 一方面是業務初期的 多快好省, 每每要上一些 能跑 的代碼. 這裏引入一些關於代碼的觀點:
總結起來就一句話:
想要顯著提升編碼水平或者快速積累相關技術知識, 參與開源能夠算是一條捷徑.