이 글은 주로 php-msf 소스 코드의 의미와 사용법 관련 문제를 소개합니다. 도움이 필요한 친구들이 따라가며 참고하고 공부할 수 있습니다. 그것이 모두에게 도움이 되기를 바랍니다.
저는 한동안 소스 코드 해석을 해왔고 제 경험을 요약하고 싶습니다.
라이프 사이클을 파악하고 코드가 마음 속에서 실행되도록 하세요
아키텍처 분석, 키워드 계층적 경계 격리
A 좋음 수명 주기와 아키텍처를 명확히 하는 프레임워크는 기본적으로 친숙한 상태에 도달했으며 세부 사항을 채우고 코딩하는 데 능숙합니다.
다음은 몇 가지 중요한 통찰력입니다.
이 도구의 장점을 알아보세요. 이 정보는 일반적으로 눈에 띄게 표시되어 있습니다. 이 기능을 사용하여 프로젝트를 하나씩 살펴보세요. 엔지니어링 관점은 위의 아키텍처와 주로 구별됩니다. 핵심 비즈니스, 즉 위의 기능/특성 외에도 엔지니어링에는 보안/테스트/코딩 표준/언어 기능 등과 같은 측면도 포함됩니다. 비즈니스 코드 작성시 생각도 덜하고 연습도 덜 필요한 부분
Tools 용도로는 지금 제가 사용하는 조합인 phpstorm + 바이두 마인드맵 + 마크다운 노트 + 블로그의 출처와 php-msf 등을 추천드립니다. 기술적인 삶에 관련된 블로그를 쓰세요. 모두와 이야기하고 직접 봉사하세요.
Life Cycle & Architecture
공식 문서는 매우 좋은 그림을 만들어냈습니다. 요청 처리 흐름도는 동료들에게 비슷한 그림을 만들도록 권장합니다.
이 그림을 기반으로 라이프 사이클과 아키텍처에 대해 생각해 보세요. 여기서는 msf의 몇 가지 기술적 사항을 자세히 설명하지 않겠습니다. 코루틴 관련 지식
msf의 기술 포인트 발췌
코루틴
다음과 같은 간단한 질문에 대해 생각해 보세요. 아마도 코루틴에 대해 더 깊이 이해하게 될 것입니다. 다음 키워드를 기억하세요:
스케줄링이 발생하는 시기는 알아야 합니다. 함께 작동하는 코루틴이 많을 텐데 어떻게 예약해야 할까요?
파괴되면 파멸되나요?
이제 코루틴이 어떻게 사용되는지 비교해 보겠습니다. 여기서는 코루틴의 구현 방법을 비교하지 않았습니다. 왜냐하면 실제 수요는 다음과 같기 때문입니다.
// 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('/');대략 다음과 같은 등식입니다. 코루틴 사용 = 수익률 더하기, 어디에서 수익률을 추가해야 하는지 알아보세요. 파일 IO, 네트워크 IO(redis/mysql/http) 등과 같이 IO가 차단되는 곳이 있습니다. 물론코루틴 스케줄링 순서에 주의해야 할 사항이 있습니다. 주의하지 않으면 동기 호출로 변질될 수 있습니다.
콜 체인: 콜 체인에 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 코루틴 서버 + swoole 코루틴 클라이언트를 사용해야 합니다.
클라이언트가 코루틴 스케줄링을 트리거합니다.
비동기 콜백 실행이 종료됩니다. 코루틴을 삭제하면
두 가지 문제가 발생합니다.
다른 것을 사용해야 합니다 코루틴 클라이언트 수행할 작업: 이것이 Swoole3의 목표입니다. Swoole2.0은 코루틴 작업을 사용하여 위장하는 것을 고려할 수 있습니다.
코루틴을 사용하는 것이 더 간단할 것 같습니다. 여러분 스스로 결정하세요.
Swoole2.0의 기본 방법은 실제로 이해하기 더 쉽습니다. 코루틴 생성/스케줄링/파괴 시점만 알면 효과적으로 활용할 수 있습니다
Swoole2.0 비동기 콜백에서 코루틴을 자주 생성하고 삭제하면 성능이 크게 저하되나요? -- 아니요, 실제로는 프로세스/객체보다 훨씬 작은 일부 메모리 작업입니다.
msf는 디자인적으로 뛰어난 점이 많고, 배울 만한 코드도 많습니다.Request ContextContext
这是从 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!