本文主要介紹了php-msf原始碼的意思以及用法相關問題,有需要的朋友跟著參考學習下吧。希望能幫助大家。
原始碼解讀也做了一段時間了, 總結一下自己的心得:
抓住生命週期, 讓程式碼在你腦海中跑起來
分析架構, 關鍵字分層邊界隔離
一個好的框架, 弄清楚生命週期和架構, 基本上就已經到了熟悉的狀態了, 之後是填充細節和編碼熟練了
這裡再介紹幾個次重要的心得:
弄清楚這個工具擅長幹什麼, 適合幹什麼. 這個信息也非常容易獲取到, 工具的文檔通常都會顯眼標註出來, 可以通過這些功能/特性, 試著以點見面
從工程化的角度去看這個專案, 主要和上面的架構區分, 在處理核心業務, 也就是上面的功能/特性外, 工程化還涉及到安全性/測試/編碼規格/語言特性等方面, 這些也是平時在寫業務代碼時思考較少並且實踐較少的部分
工具的使用, 推薦我現在使用的組合: 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 Swoole\Http\Server("127.0.0.1", 9502, SWOOLE_BASE); $server->set([ 'worker_num' => 1, ]); // 需要在协程 server 的异步回调函数中 $server->on('Request', function ($request, $response) { $tcpclient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); // 需要配合使用协程客户端 $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(); // 标注延迟收包, 实现并发调用 $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 的協程方案, 好處很明顯:
#不用加yield 了
並發呼叫不用刻意注意yield 的順序了,使用defer() 延遲收包即可
但是, 沒辦法直接用使用協程= 加上yield 這樣一個簡單的等式了, 上面的例子需要配合使用swoole 協程server + swoole 協程client:
server 在非同步回呼觸發時產生協程
client 觸發協程調度
異步回呼執行結束時銷毀協程
這就導致了2 個問題:
不在swoole 協程server 的非同步回呼中怎麼辦: 使用Swoole\Coroutine::create() 明確產生協程
要用其他的協程Client 怎麼辦: 這是Swoole3 的目標, Swoole2.0 可以考慮用協程task 來偽裝
這樣看起來, 好像使用協程= 加上yield 這樣要簡單一些? 我不這樣認為, 補充一些觀點, 大家自己斟酌:
使用yield 的方式, 基於php 生成器+ 自己實現PHP 協程調度器, 想要用起來不出錯, 例如上面協程調度順序, 你還是需要去弄清楚這塊的實現
Swoole2.0 的原生方式, 理解起來其實更容易, 只需要知道協程生成/調度/銷毀的時機可以用好
Swoole2.0 這樣異步回調中頻繁創建和銷毀協程, 是否十分損耗性能? -- 不會的, 實際是一些內存操作, 比進程/物件小很多
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]);
对象池的具体代码在 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();
连接池 & 代理
连接池 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
查看示例 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); } }
代理就是在连接池的基础上进一步 搞事情. 以 主从 模式为例:
主从策略: 读主库, 写从库
代理做的事情:
判断是读操作还是写操作, 选择相应的库去执行
公共库
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, 大家根据文档自己探索即可。
以上是實例詳解php-msf源碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!