您好,登錄后才能下訂單哦!
php-msf的源碼介紹,相信很多沒有經(jīng)驗(yàn)的人對(duì)此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個(gè)問題。
從工程化的角度去看這個(gè)項(xiàng)目, 主要和上面的 架構(gòu) 區(qū)分, 在處理核心業(yè)務(wù), 也就是上面的 功能/特性 外, 工程化還涉及到 安全/測(cè)試/編碼規(guī)范/語言特性 等方面, 這些也是平時(shí)在寫業(yè)務(wù)代碼時(shí)思考較少并且實(shí)踐較少的部分
工具的使用, 推薦我現(xiàn)在使用的組合: phpstorm + 百度腦圖 + Markdown筆記 + blog和 php-msf 的淵源等寫技術(shù)生活相關(guān)的 blog 再來和大家八, 直接上菜.
生命周期 & 架構(gòu)
官方文檔制作了一張非常好的圖: 處理請(qǐng)求流程圖. 推薦各位同仁, 有閑暇時(shí)制作類似的圖, 對(duì)思維很有的幫助.
根據(jù)這張圖來思考 生命周期 & 架構(gòu), 這里就不贅述了, 這里分析一下 msf 中一些技術(shù)點(diǎn):
協(xié)程相關(guān)知識(shí)
msf 中技術(shù)點(diǎn)摘錄
協(xié)程
我會(huì)用我的方式來講解, 如果需要深入了解的, 可以看我后面推薦的資源.
類 vs 對(duì)象 是一組很重要的概念. 類代表我們對(duì)事物的抽象, 這個(gè)抽象的能力在我們以后會(huì)一直用到, 希望大家有意識(shí)的培養(yǎng)這方面的意識(shí), 至少可以起到觸類旁通的作用. 對(duì)象是 實(shí)例化 的類, 是 真正干活的, 我們要討論的 協(xié)程, 就是這樣一個(gè) 真正干活的 角色.
協(xié)程從哪里來, 到哪里去, 它是干什么的?
想一想這幾個(gè)簡單的問題, 也許你對(duì)協(xié)程的理解就更深刻了, 記住這幾個(gè)關(guān)鍵詞:
產(chǎn)生. 需要有地方來產(chǎn)生協(xié)程, 你可能不需要知道細(xì)節(jié), 但是需要知道什么時(shí)候發(fā)生了
調(diào)度. 肯定是有很多協(xié)程一起工作的, 所以需要調(diào)度, 怎么調(diào)度的呢?
銷毀. 是否會(huì)銷毀? 什么時(shí)候銷毀?
現(xiàn)在, 我們?cè)賮砜纯磪f(xié)程的使用方式對(duì)比, 這里注意一下, 我沒有用 協(xié)程的實(shí)現(xiàn)方式對(duì)比, 因?yàn)楹芏鄷r(shí)候, 需求實(shí)際是這樣的:
怎么實(shí)現(xiàn)我不管, 我選最好用的.
// msf - 單次協(xié)程調(diào)度 $response = yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine'); // msf - 并發(fā)協(xié)程調(diào)用 $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('/');
大致 是這樣的一個(gè)等式: 使用協(xié)程 = 加上 yield, 所以搞清楚哪些地方需要加上 yield 就好了 -- 有阻塞IO的地方, 比如 文件IO, 網(wǎng)絡(luò)IO(redis/mysql/http) 等.
當(dāng)然, 大致 就是還有需要注意的地方
協(xié)程調(diào)度順序, 如果不注意, 就可能會(huì)退化成同步調(diào)用.
調(diào)用鏈: 使用 yield 的調(diào)用鏈上, 都需要加上 yield. 比如下面這樣:
function a_test() { return yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine'); } $res = yield a_test(); // 如果不加 yield, 就變成了同步執(zhí)行
對(duì)比一下 swoole2.0 的協(xié)程方案:
$server = new Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([ 'worker_num' => 1, ]); // 需要在協(xié)程 server 的異步回調(diào)函數(shù)中 $server->on('Request', function ($request, $response) { $tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); // 需要配合使用協(xié)程客戶端 $tcpclient->connect('127.0.0.1', 9501,0.5) $tcpclient->send("hello world\n"); $redis = new Swoole\Coroutine\Redis(); $redis->connect('127.0.0.1', 6379); $redis->setDefer(); // 標(biāo)注延遲收包, 實(shí)現(xiàn)并發(fā)調(diào)用 $redis->get('key'); $mysql = new Swoole\Coroutine\MySQL(); $mysql->connect([ 'host' => '127.0.0.1', 'user' => 'user', 'password' => 'pass', 'database' => 'test', ]); $mysql->setDefer(); $mysql->query('select sleep(1)'); $httpclient = new Swoole\Coroutine\Http\Client('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 的協(xié)程方案, 好處很明顯:
不用加 yield 了
并發(fā)調(diào)用不用刻意注意 yield 的順序了, 使用 defer() 延遲收包即可
但是, 沒辦法直接用 使用協(xié)程 = 加上 yield 這樣一個(gè)簡單的等式了, 上面的例子需要配合使用 swoole 協(xié)程 server + swoole 協(xié)程 client:
server 在異步回調(diào)觸發(fā)時(shí) 生成協(xié)程
client 觸發(fā) 協(xié)程調(diào)度
異步回調(diào)執(zhí)行結(jié)束時(shí) 銷毀協(xié)程
這就導(dǎo)致了 2 個(gè)問題:
不在 swoole 協(xié)程 server 的異步回調(diào)中怎么辦: 使用 Swoole\Coroutine::create() 顯式生成協(xié)程
需要使用其他的協(xié)程 Client 怎么辦: 這是 Swoole3 的目標(biāo), Swoole2.0 可以考慮用協(xié)程 task 來偽裝
這樣看起來, 好像 使用協(xié)程 = 加上 yield 這樣要簡單一些? 我不這樣認(rèn)為, 補(bǔ)充一些觀點(diǎn), 大家自己斟酌:
使用 yield 的方式, 基于 php 生成器 + 自己實(shí)現(xiàn) PHP 協(xié)程調(diào)度器, 想要用起來不出錯(cuò), 比如上面 協(xié)程調(diào)度順序, 你還是需要去弄清楚這塊的實(shí)現(xiàn)
Swoole2.0 的原生方式, 理解起來其實(shí)更容易, 只需要知道協(xié)程 生成/調(diào)度/銷毀 的時(shí)機(jī)就可以用好
Swoole2.0 這樣異步回調(diào)中頻繁創(chuàng)建和銷毀協(xié)程, 是否十分損耗性能? -- 不會(huì)的, 實(shí)際是一些內(nèi)存操作, 比進(jìn)程/對(duì)象小很多
msf 中技術(shù)點(diǎn)摘錄
msf 在設(shè)計(jì)上有很多出彩的地方, 很多代碼都值得借鑒.
請(qǐng)求上下文 Context
這是從 fpm 到 swoole http server 非常重要的概念. fpm 是多進(jìn)程模式, 雖然 $_POST 等變量, 被稱之為超全局變量, 但是, 這些變量在不同 fpm 進(jìn)程間是隔離的. 但是到了 swoole http server 中, 一個(gè) worker 進(jìn)程, 會(huì)異步處理多個(gè)請(qǐng)求, 簡單理解就是下面的等式:
fpm worker : http request = 1 : 1 swoole worker : http request = 1 : n
所以, 我們就需要一種新的方式, 來進(jìn)行 request 間的隔離.
在編程語言里, 有一個(gè)專業(yè)詞匯 scope(作用域). 通常會(huì)使用 scope/生命周期, 所以我一直強(qiáng)調(diào)的生命周期的概念, 真的很重要.
swoole 本身是實(shí)現(xiàn)了隔離的:
$http = new swoole_http_server("127.0.0.1", 9501); $http->on('request', function ($request, $response) { $response->end("<h2>Hello Swoole. #".rand(1000, 9999)."</h2>"); }); $http->start();
msf 在 Context 上還做了一層封裝, 讓 Context 看起來 為所欲為:
// 你幾乎可以用這種方式, 完成任何需要的邏輯 $this->getContext()->xxxModule->xxxModuleFunction();
細(xì)節(jié)可以查看 src/Helpers/Context.php 文件
對(duì)象池
對(duì)象池這個(gè)概念, 大家可能比較陌生, 目的是減少對(duì)象的頻繁創(chuàng)建與銷毀, 以此來提升性能, msf 做了很好的封裝, 使用很簡單:
// getObject() 就可以了 /** @var DemoModel $demoModel */ $demoModel = $this->getObject(DemoModel::class, [1, 2]);
對(duì)象池的具體代碼在 src/Base/Pool.php 下:
底層使用反射來實(shí)現(xiàn)對(duì)象的動(dòng)態(tài)創(chuàng)建
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 來管理對(duì)象
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]; } // 管理對(duì)象 $pool->push($classInstance); $obj = $pool->shift();
連接池 & 代理
連接池 Pools
連接池的概念就不贅述了, 我們來直接看 msf 中的實(shí)現(xiàn), 代碼在 src/Pools/AsynPool.php 下:
public function __construct($config) { $this->callBacks = []; $this->commands = new \SplQueue(); $this->pool = new \SplQueue(); $this->config = $config; }
這里使用的 SplQueue 來管理連接和需要執(zhí)行的命令. 可以和上面對(duì)比一下, 想一想為什么一個(gè)使用 SplStack, 一個(gè)使用 SplQueue.
代理 Proxy
代理是在連接池的基礎(chǔ)上進(jìn)一步的封裝, msf 提供了 2 種封裝方式:
主從 master slave
集群 cluster
查看示例 App\Controllers\Redis 中的代碼:
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); } }
代理就是在連接池的基礎(chǔ)上進(jìn)一步 搞事情. 以 主從 模式為例:
主從策略: 讀主庫, 寫從庫
代理做的事情:
判斷是讀操作還是寫操作, 選擇相應(yīng)的庫去執(zhí)行
公共庫
msf 推行 公共庫 的做法, 希望不同功能組件可以做到 可插拔, 這一點(diǎn)可以看 laravel 框架和 symfony 框架, 都由框架核心加一個(gè)個(gè)的 package 組成. 這種思想我是非常推薦的, 但是仔細(xì)看 百度腦圖 - php-msf 源碼解讀 這張圖的話, 就會(huì)發(fā)現(xiàn)類與類之間的依賴關(guān)系, 分層/邊界 做得并不好. 如果看過我之前的 blog - laravel源碼解讀 / blog - yii源碼解讀, 進(jìn)行對(duì)比就會(huì)感受很明顯.
但是, 這并不意味著 代碼不好, 至少功能正常的代碼, 幾乎都能算是好代碼. 從功能之外建立的 優(yōu)越感, 更多的是對(duì) 美好生活的向往 -- 還可以更好一點(diǎn).
AOP
php AOP 擴(kuò)展: http://pecl.php.net/package/aop
PHP-AOP擴(kuò)展介紹 | rango: http://rango.swoole.com/archives/83
AOP, 面向切面編程, 韓老大 的 blog - PHP-AOP擴(kuò)展介紹 | rango 可以看看.
需不需要了解一個(gè)新事物, 先看看這個(gè)事物有什么作用:
AOP, 將業(yè)務(wù)代碼和業(yè)務(wù)無關(guān)的代碼進(jìn)行分離, 場景有 日志記錄 / 性能統(tǒng)計(jì) / 安全控制 / 事務(wù)處理 / 異常處理 / 緩存 等等.
這里引用一段 程序員DD - 翟永超的公眾號(hào) 文章里的代碼, 讓大家感受下:
同樣是 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) { // 參數(shù)等校驗(yàn)出錯(cuò),這類異常屬于已知異常,不需要打印堆棧,返回碼為-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)); }
代碼只用一行, 需要的特性一個(gè)沒少, 你是不是也想寫這樣的 CRUD 代碼?
配置文件管理
先明確一下配置管理的痛點(diǎn):
是否支撐熱更新, 常駐內(nèi)存需要考慮
考慮不同環(huán)境: dev test production
方便使用
熱更其實(shí)可以算是常駐內(nèi)存服務(wù)器的整體需求, 目前 php 常用的解決方案是 inotify, 可以參考我之前的 blog - swoft 源碼解讀 .
msf 使用第三方庫來解析處理配置文件, 這里著重提一個(gè) array_merge() 的細(xì)節(jié):
$a = ['a' => [ 'a1' => 'a1', ]]; $b = ['a' => [ 'b1' => 'b1', ]]; $arr = array_merge($a, $b); // 注意, array_merge() 并不會(huì)循環(huán)合并 var_dump($arr); // 結(jié)果 array(1) { ["a"]=> array(1) { ["b1"]=> string(2) "b1" } }
msf 中使用配置:
$ids = $this->getConfig()->get('params.mock_ids', []); // 對(duì)比一下 laravel $ids = cofnig('params.mock_ids', []);
看起來 laravel 中要簡單一些, 其實(shí)是通過 composer autoload 來加載函數(shù), 這個(gè)函數(shù)對(duì)實(shí)際的操作包裝了一層. 至于要不要這樣做, 就看自己需求了.
寫在最后
msf 最復(fù)雜的部分在 服務(wù)啟動(dòng)階段, 繼承也很長:
Child -> Server -> HttpServer -> MSFServer -> AppServer, 有興趣可以挑戰(zhàn)一下.
另外一個(gè)比較難的點(diǎn), 是 MongoDbTask 實(shí)現(xiàn)原理.
msf 還封裝了很多有用的功能, RPC / 消息隊(duì)列 / restful, 大家根據(jù)文檔自己探索即可.
看完上述內(nèi)容,你們掌握php-msf的源碼介紹的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。