php-msf原始碼解讀【轉】
php-msf: https://github.com/pinguo/php-msf
百度腦圖 – php-msf 原始碼解讀: http://naotu.baidu.com/file/cc7b5a49dfed46001d22222b1afa99ba?token=c9628331e99143c2
原始碼解讀也做了一段時間了, 總結一下自己的心得:
- 抓住 生命週期, 讓程式碼在你腦海中 跑起來
- 分析架構, 關鍵字 分層 邊界 隔離
一個好的框架, 弄清楚 生命週期 和 架構, 基本就已經到了 熟悉 的狀態了, 之後是填充細節和編碼熟練了
這裡再介紹幾個次重要的心得:
- 弄明白這個工具擅長幹什麼, 適合幹什麼. 這個資訊也非常容易獲取到, 工具的文件通常都會顯眼標註出來, 可以通過這些 功能/特性, 嘗試以點見面
- 從工程化的角度去看這個專案, 主要和上面的 架構 區分, 在處理核心業務, 也就是上面的 功能/特性 外, 工程化還涉及到 安全/測試/編碼規範/語言特性 等方面, 這些也是平時在寫業務程式碼時思考較少並且實踐較少的部分
- 工具的使用, 推薦我現在使用的組合: phpstorm + 百度腦圖 + Markdown筆記 + blog
和 php-msf 的淵源等寫技術生活相關的 blog 再來和大家八, 直接上菜.
生命週期 & 架構
官方文件製作了一張非常好的圖: 處理請求流程圖. 推薦各位同仁, 有閒暇時製作類似的圖, 對思維很有的幫助.
根據這張圖來思考 生命週期 & 架構, 這裡就不贅述了, 這裡分析一下 msf 中一些技術點:
- 協程相關知識
- msf 中技術點摘錄
協程
我會用我的方式來講解, 如果需要深入瞭解的, 可以看我後面推薦的資源.
類 vs 物件 是一組很重要的概念. 類代表我們對事物的抽象, 這個抽象的能力在我們以後會一直用到, 希望大家有意識的培養這方面的意識, 至少可以起到觸類旁通的作用. 物件是 例項化 的類, 是 真正幹活的, 我們要討論的 協程, 就是這樣一個 真正幹活的 角色.
協程從哪裡來, 到哪裡去, 它是幹什麼的?
想一想這幾個簡單的問題, 也許你對協程的理解就更深刻了, 記住這幾個關鍵詞:
- 產生. 需要有地方來產生協程, 你可能不需要知道細節, 但是需要知道什麼時候發生了
- 排程. 肯定是有很多協程一起工作的, 所以需要排程, 怎麼排程的呢?
- 銷燬. 是否會銷燬? 什麼時候銷燬?
現在, 我們再來看看協程的使用方式對比, 這裡注意一下, 我沒有用 協程的實現方式對比, 因為很多時候, 需求實際是這樣的:
怎麼實現我不管, 我選最好用的.
// msf - 單次協程排程
$response = yield $this->getRedisPool(`tw`)->get(`apiCacheForABCoroutine`);
// msf - 併發協程呼叫
$client1 = $this->getObject(Client::class, [`http://www.baidu.com/`]);
yield $client1->goDnsLookup();
$client2 = $this->getObject(Client::class, [`http://www.qq.com/`]);
yield $client2->goDnsLookup();
$result[] = yield $client1->goGet(`/`);
$result[] = yield $client2->goGet(`/`);
大致 是這樣的一個等式: 使用協程 = 加上 yield
, 所以搞清楚哪些地方需要加上 yield 就好了 — 有阻塞IO的地方, 比如 檔案IO, 網路IO(redis/mysql/http) 等.
當然, 大致 就是還有需要注意的地方
- 協程排程順序, 如果不注意, 就可能會退化成同步呼叫.
- 呼叫鏈: 使用 yield 的呼叫鏈上, 都需要加上 yield. 比如下面這樣:
function a_test() {
return yield $this->getRedisPool(`tw`)->get(`apiCacheForABCoroutine`);
}
$res = yield a_test(); // 如果不加 yield, 就變成了同步執行
對比一下 swoole2.0 的協程方案:
$server = new SwooleHttpServer("127.0.0.1", 9502, SWOOLE_BASE);
$server->set([
`worker_num` => 1,
]);
// 需要在協程 server 的非同步回撥函式中
$server->on(`Request`, function ($request, $response) {
$tcpclient = new SwooleCoroutineClient(SWOOLE_SOCK_TCP); // 需要配合使用協程客戶端
$tcpclient->connect(`127.0.0.1`, 9501,0.5)
$tcpclient->send("hello world
");
$redis = new SwooleCoroutineRedis();
$redis->connect(`127.0.0.1`, 6379);
$redis->setDefer(); // 標註延遲收包, 實現併發呼叫
$redis->get(`key`);
$mysql = new SwooleCoroutineMySQL();
$mysql->connect([
`host` => `127.0.0.1`,
`user` => `user`,
`password` => `pass`,
`database` => `test`,
]);
$mysql->setDefer();
$mysql->query(`select sleep(1)`);
$httpclient = new SwooleCoroutineHttpClient(`0.0.0.0`, 9599);
$httpclient->setHeaders([`Host` => "api.mp.qq.com"]);
$httpclient->set([ `timeout` => 1]);
$httpclient->setDefer();
$httpclient->get(`/`);
$tcp_res = $tcpclient->recv();
$redis_res = $redis->recv();
$mysql_res = $mysql->recv();
$http_res = $httpclient->recv();
$response->end(`Test End`);
});
$server->start();
使用 swoole2.0 的協程方案, 好處很明顯:
- 不用加
yield
了 - 併發呼叫不用刻意注意
yield
的順序了, 使用defer()
延遲收包即可
但是, 沒辦法直接用 使用協程 = 加上 yield
這樣一個簡單的等式了, 上面的例子需要配合使用 swoole 協程 server + swoole 協程 client:
- server 在非同步回撥觸發時 生成協程
- client 觸發 協程排程
- 非同步回撥執行結束時 銷燬協程
這就導致了 2 個問題:
- 不在 swoole 協程 server 的非同步回撥中怎麼辦: 使用
SwooleCoroutine::create()
顯式生成協程 - 需要使用其他的協程 Client 怎麼辦: 這是 Swoole3 的目標, Swoole2.0 可以考慮用協程 task 來偽裝
這樣看起來, 好像 使用協程 = 加上 yield
這樣要簡單一些? 我不這樣認為, 補充一些觀點, 大家自己斟酌:
- 使用 yield 的方式, 基於 php 生成器 + 自己實現 PHP 協程排程器, 想要用起來不出錯, 比如上面 協程排程順序, 你還是需要去弄清楚這塊的實現
- Swoole2.0 的原生方式, 理解起來其實更容易, 只需要知道協程 生成/排程/銷燬 的時機就可以用好
- Swoole2.0 這樣非同步回撥中頻繁建立和銷燬協程, 是否十分損耗效能? — 不會的, 實際是一些記憶體操作, 比程式/物件小很多
想要繼續深入瞭解的同學, 可以繼續閱讀下面的文章:
php7 下協程實現: https://segmentfault.com/a/1190000012457145
在PHP中使用協程實現多工排程 | 鳥哥: http://www.laruence.com/2015/05/28/3038.html
php-msf doc – 協程原理: https://pinguo.gitbooks.io/php-msf-docs/chapter-2/2.3-%E5%8D%8F%E7%A8%8B%E5%8E%9F%E7%90%86.html
swoole 底層和 php yield 實現協程, 本質是一樣的, 畢竟都是 c 語言實現: https://github.com/php/php-src/blob/master/Zend/zend_generators.c
有時候 書讀百遍其義自見 還是很有道理的. 我希望我使用的 協程生成/排程/銷燬 的角度, 能給大家帶來幫助.
感謝韓老大, msf 的開發者, swoft 的小夥伴, 在這個過程中, 對我給予的耐心幫助.
msf 中技術點摘錄
msf 在設計上有很多出彩的地方, 很多程式碼都值得借鑑.
請求上下文 Context
這是從 fpm 到 swoole http server 非常重要的概念. fpm 是多程式模式, 雖然 $_POST
等變數, 被稱之為超全域性變數, 但是, 這些變數在不同 fpm 程式間是隔離的. 但是到了 swoole http server 中, 一個 worker 程式, 會非同步處理多個請求, 簡單理解就是下面的等式:
fpm worker : http request = 1 : 1
swoole worker : http request = 1 : n
所以, 我們就需要一種新的方式, 來進行 request 間的隔離.
在程式語言裡, 有一個專業詞彙 scope(作用域). 通常會使用
scope/生命週期
, 所以我一直強調的生命週期的概念, 真的很重要.
swoole 本身是實現了隔離的:
$http = new swoole_http_server("127.0.0.1", 9501);
$http->on(`request`, function ($request, $response) {
$response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
$http->start();
msf 在 Context 上還做了一層封裝, 讓 Context 看起來 為所欲為:
// 你幾乎可以用這種方式, 完成任何需要的邏輯
$this->getContext()->xxxModule->xxxModuleFunction();
細節可以檢視 src/Helpers/Context.php
檔案
物件池
物件池這個概念, 大家可能比較陌生, 目的是減少物件的頻繁建立與銷燬, 以此來提升效能, msf 做了很好的封裝, 使用很簡單:
// getObject() 就可以了
/** @var DemoModel $demoModel */
$demoModel = $this->getObject(DemoModel::class, [1, 2]);
注意一下這行註釋, 加上這個才有程式碼提示的效果的, 原理可以看我之前的 blog – 聊一聊 php 程式碼提示
物件池的具體程式碼在 src/Base/Pool.php
下:
- 底層使用反射來實現物件的動態建立
public function get($class, ...$args)
{
$poolName = trim($class, `\`);
if (!$poolName) {
return null;
}
$pool = $this->map[$poolName] ?? null;
if ($pool == null) {
$pool = $this->applyNewPool($poolName);
}
if ($pool->count()) {
$obj = $pool->shift();
$obj->__isConstruct = false;
return $obj;
} else {
// 使用反射
$reflector = new ReflectionClass($poolName);
$obj = $reflector->newInstanceWithoutConstructor();
$obj->__useCount = 0;
$obj->__genTime = time();
$obj->__isConstruct = false;
$obj->__DSLevel = Macro::DS_PUBLIC;
unset($reflector);
return $obj;
}
}
感興趣的同學可以去了解一下 反射, 可以給語言增加很多靈活性
- 使用 SplStack 來管理物件
private function applyNewPool($poolName)
{
if (array_key_exists($poolName, $this->map)) {
throw new Exception(`the name is exists in pool map`);
}
$this->map[$poolName] = new SplStack();
return $this->map[$poolName];
}
// 管理物件
$pool->push($classInstance);
$obj = $pool->shift();
msf doc 這塊的文章非常值得一讀, 特別是 php程式記憶體優化, 對我觸動很大: https://pinguo.gitbooks.io/php-msf-docs/chapter-5/5.6-%E5%AF%B9%E8%B1%A1%E6%B1%A0.html
連線池 & 代理
- 連線池 Pools
連線池的概念就不贅述了, 我們來直接看 msf 中的實現, 程式碼在 src/Pools/AsynPool.php
下:
public function __construct($config)
{
$this->callBacks = [];
$this->commands = new SplQueue();
$this->pool = new SplQueue();
$this->config = $config;
}
這裡使用的 SplQueue
來管理連線和需要執行的命令. 可以和上面對比一下, 想一想為什麼一個使用 SplStack
, 一個使用 SplQueue
.
- 代理 Proxy
代理是在連線池的基礎上進一步的封裝, msf 提供了 2 種封裝方式:
- 主從 master slave
- 叢集 cluster
檢視示例 AppControllersRedis
中的程式碼:
class Redis extends Controller
{
// Redis連線池讀寫示例
public function actionPoolSetGet()
{
yield $this->getRedisPool(`p1`)->set(`key1`, `val1`);
$val = yield $this->getRedisPool(`p1`)->get(`key1`);
$this->outputJson($val);
}
// Redis代理使用示例(分散式)
public function actionProxySetGet()
{
for ($i = 0; $i <= 100; $i++) {
yield $this->getRedisProxy(`cluster`)->set(`proxy` . $i, $i);
}
$val = yield $this->getRedisProxy(`cluster`)->get(`proxy22`);
$this->outputJson($val);
}
// Redis代理使用示例(主從)
public function actionMaserSlaveSetGet()
{
for ($i = 0; $i <= 100; $i++) {
yield $this->getRedisProxy(`master_slave`)->set(`M` . $i, $i);
}
$val = yield $this->getRedisProxy(`master_slave`)->get(`M66`);
$this->outputJson($val);
}
}
代理就是在連線池的基礎上進一步 搞事情. 以 主從 模式為例:
- 主從策略: 讀主庫, 寫從庫
代理做的事情:
- 判斷是讀操作還是寫操作, 選擇相應的庫去執行
公共庫
msf 推行 公共庫 的做法, 希望不同功能元件可以做到 可插拔, 這一點可以看 laravel 框架和 symfony 框架, 都由框架核心加一個個的 package 組成. 這種思想我是非常推薦的, 但是仔細看 百度腦圖 – php-msf 原始碼解讀 這張圖的話, 就會發現類與類之間的依賴關係, 分層/邊界 做得並不好. 如果看過我之前的 blog – laravel原始碼解讀 / blog – yii原始碼解讀, 進行對比就會感受很明顯.
但是, 這並不意味著 程式碼不好, 至少功能正常的程式碼, 幾乎都能算是好程式碼. 從功能之外建立的 優越感, 更多的是對 美好生活的嚮往 — 還可以更好一點.
AOP
php AOP 擴充套件: http://pecl.php.net/package/aop
PHP-AOP擴充套件介紹 | rango: http://rango.swoole.com/archives/83
AOP, 面向切面程式設計, 韓老大 的 blog – PHP-AOP擴充套件介紹 | rango 可以看看.
需不需要了解一個新事物, 先看看這個事物有什麼作用:
AOP, 將業務程式碼和業務無關的程式碼進行分離, 場景有 日誌記錄 / 效能統計 / 安全控制 / 事務處理 / 異常處理 / 快取 等等.
這裡引用一段 程式設計師DD – 翟永超的公眾號 文章裡的程式碼, 讓大家感受下:
- 同樣是 CRUD, 不使用 AOP
@PostMapping("/delete")
public Map<String, Object> delete(long id, String lang) {
Map<String, Object> data = new HashMap<String, Object>();
boolean result = false;
try {
// 語言(中英文提示不同)
Locale local = "zh".equalsIgnoreCase(lang) ? Locale.CHINESE : Locale.ENGLISH;
result = configService.delete(id, local);
data.put("code", 0);
} catch (CheckException e) {
// 引數等校驗出錯,這類異常屬於已知異常,不需要列印堆疊,返回碼為-1
data.put("code", -1);
data.put("msg", e.getMessage());
} catch (Exception e) {
// 其他未知異常,需要列印堆疊分析用,返回碼為99
log.error(e);
data.put("code", 99);
data.put("msg", e.toString());
}
data.put("result", result);
return data;
}
- 使用 AOP
@PostMapping("/delete")
public ResultBean<Boolean> delete(long id) {
return new ResultBean<Boolean>(configService.delete(id));
}
程式碼只用一行, 需要的特性一個沒少, 你是不是也想寫這樣的 CRUD 程式碼?
配置檔案管理
先明確一下配置管理的痛點:
- 是否支撐熱更新, 常駐記憶體需要考慮
- 考慮不同環境: dev test production
- 方便使用
熱更其實可以算是常駐記憶體伺服器的整體需求, 目前 php 常用的解決方案是 inotify, 可以參考我之前的 blog – swoft 原始碼解讀 .
msf 使用第三方庫來解析處理配置檔案, 這裡著重提一個 array_merge()
的細節:
$a = [`a` => [
`a1` => `a1`,
]];
$b = [`a` => [
`b1` => `b1`,
]];
$arr = array_merge($a, $b); // 注意, array_merge() 並不會迴圈合併
var_dump($arr);
// 結果
array(1) {
["a"]=>
array(1) {
["b1"]=>
string(2) "b1"
}
}
msf 中使用配置:
$ids = $this->getConfig()->get(`params.mock_ids`, []);
// 對比一下 laravel
$ids = cofnig(`params.mock_ids`, []);
看起來 laravel 中要簡單一些, 其實是通過 composer autoload 來載入函式, 這個函式對實際的操作包裝了一層. 至於要不要這樣做, 就看自己需求了.
寫在最後
msf 最複雜的部分在 服務啟動階段, 繼承也很長:
Child -> Server -> HttpServer -> MSFServer -> AppServer
, 有興趣可以挑戰一下.
另外一個比較難的點, 是 MongoDbTask 實現原理
.
msf 還封裝了很多有用的功能, RPC / 訊息佇列 / restful, 大家根據文件自己探索即可.
相關文章
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- WeakHashMap,原始碼解讀HashMap原始碼
- Handler原始碼解讀原始碼
- Laravel 原始碼解讀Laravel原始碼
- Swoft 原始碼解讀原始碼
- SDWebImage原始碼解讀Web原始碼
- MJExtension原始碼解讀原始碼
- Masonry原始碼解讀原始碼
- HashMap原始碼解讀HashMap原始碼
- Redux原始碼解讀Redux原始碼
- require() 原始碼解讀UI原始碼
- ZooKeeper原始碼解讀原始碼
- FairyGUI原始碼解讀AIGUI原始碼
- 【C++】【原始碼解讀】std::is_same函式原始碼解讀C++原始碼函式
- vuex 原始碼:原始碼系列解讀總結Vue原始碼
- Laravel 原始碼的解讀Laravel原始碼
- reselect原始碼解讀原始碼
- ThreadLocal 原始碼解讀thread原始碼
- Redux原始碼完全解讀Redux原始碼
- Seajs原始碼解讀JS原始碼
- Axios 原始碼解讀iOS原始碼
- HashMap原始碼個人解讀HashMap原始碼
- Vue原始碼解讀一Vue原始碼
- Slim 框架原始碼解讀框架原始碼
- ReentrantLock原始碼解讀ReentrantLock原始碼
- MJRefresh原始碼解讀原始碼
- GetBean原始碼全面解讀Bean原始碼
- LifeCycle原始碼解讀原始碼
- LinkedHashMap原始碼解讀HashMap原始碼
- ConcurrentHashMap原始碼解讀HashMap原始碼
- Disruptor-原始碼解讀原始碼
- webpack bootstrap原始碼解讀Webboot原始碼
- Kafka Eagle 原始碼解讀Kafka原始碼
- ThreadLocal原始碼解讀thread原始碼
- Masonry 原始碼解讀(上)原始碼
- Masonry 原始碼解讀(下)原始碼
- JSPatch原始碼解讀JS原始碼
- LinkedList原始碼解讀原始碼