7 附录
最后更新于:2022-03-31 23:48:07
# 7 附录
## 监控
服务器运行状态信息,框架内置Monitor控制器,如下方式访问:
```
[worker@newdev ~]$ curl http://127.0.0.1:8000/Monitor | jq .
{
"worker": { // worker进程信息
"worker0": { // 编号为0worker的进程
"pid": 1092, // 进程 ID
"coroutine": { // 当前正在执行的协程信息
"total": 0 // 正在执行的协程总数
},
"memory": { // 当进程的内存使用信息
"peak": "0.999M", // 内存峰值M单位
"usage": "0.951M", // 当前使用内存M单位
"peak_byte": 1047504, // 内存峰值B单位
"usage_byte": 996920 // 当前使用内存B单位
},
"request": { // 当前进程处理的请求信息
"worker_request_count": 3 // 当前进程处理的请求总数
},
"object_pool": { // 对象池
"PG\\MSF\\Controllers\\Monitor": 0, // 当前内存\PG\MSF\Controllers\Monitor对象总数
"PG\\MSF\\Helpers\\Context": 0,
"PG\\MSF\\Base\\Input": 0,
"PG\\MSF\\Base\\Output": 0
},
"dns_cache_http": { // DNS缓存
"127.0.0.1": [
"127.0.0.1",
1504774150,
1
],
"www.baidu.com": [
"14.215.177.38",
1504774202,
1
]
},
"exit": 0 // 进程异常退出次数
}
},
"tcp": {
"start_time": 1504753470, // 服务启动时间
"connection_num": 0, // 当前连接总数
"accept_count": 3, // 已接受连接总数
"close_count": 3, // 已关闭连接总数
"tasking_num": 0, // 当前正在排队的task数量
"request_count": 3 // 已处理请求总数
},
"running": {
"qps": 0, // 当前QPS
"last_qpm": 1, // 上一分钟处理请求数
"qpm": 0, // 当前分钟处理请求数
"concurrency": 0 // 当前并发数
},
"sys_cache": { // 共享内存信息
"memory_size": 1241513984,
"slots_memory_size": 167772160,
"values_memory_size": 1073741824,
"segment_size": 4194304,
"segment_num": 256,
"miss": 6607,
"hits": 9108,
"fails": 0,
"kicks": 0,
"recycles": 0,
"slots_size": 1048576,
"slots_used": 4
}
}
```
其中端口8000,根据实际情况调整为你的服务端口。
6 常见问题
最后更新于:2022-03-31 23:48:05
# 6 常见问题
## 镜像问题
### Q:无法拉取(pull)镜象怎么办?
A: 我们提供[多种环境](https://pinguo.gitbooks.io/php-msf-docs/chapter-3/3.3-docker.html)的镜象.如果仍然无法拉取,可以尝试使用代理方式:在Docker 配置文件中添加 `export http_proxy="http://<PROXY_HOST>:<PROXY_PORT>"`.
### Q:挂载的共享目录,在容器内进行操作没有权限(Permission Denied)?
A: 这种情况很大原因可能是[启用了SElinux导致](https://stackoverflow.com/questions/24288616/permission-denied-on-accessing-host-directory-in-docker)的(如果`ls -l` 可以看到`-rwxr-xr-x.`最后的点,那么就是启用了SELinux),解决方案至少有两种方式:
+ Docker启用过程使用`--privileged`指定更高权限运行容器。
+ 关闭SElinux,你可以使用:
- set "setenforce 0" 临时关闭
- 修改/etc/sysconfig/selinux文件,将SELINUX的值设置为disabled
## 代码问题
### Q:Windows下server无法启动
A: 可以参见[这里](https://github.com/pinguo/php-msf-demo/issues/10)
### Q: 代码变更之后worker没有自动reload
A: 请确保配置文件已经开启自动重启: `$config['auto_reload_enable'] = true;` 文件监听使用的是Inotify,但目前[Docker-for-windows无法正常工作](https://docs.docker.com/docker-for-windows/troubleshoot/#docker-knowledge-hub).这种情况下,可以使用docker镜像内配置的[nodemon](https://github.com/remy/nodemon)来达到同样的效果.
5.12 小结
最后更新于:2022-03-31 23:48:02
# 5.12 小结
本章节介绍了大部分php-msf的高性能支撑组件,是高性能的基石,并且已经打磨足够稳定可用,通过本章节的学习,我们已经可以构建一个高性能复杂业务的应用系统。
5.11 杂项
最后更新于:2022-03-31 23:48:00
# 5.11 杂项
## 消息队列
当前消息队列支持Redis、RabbitMQ、Kafka,使用方法也很简单,一般只需要关心set和get方法即可。
### Redis作为消息队列
```php
//准备一个队列
$redisQueue = $this->getObject(\PG\MSF\Queue\Redis::class, ['p1']);
// Enqueue入队,默认队列为default
$res = yield $redisQueue->set(string $data, string $queue = 'default');
// Dequeue出队
$res = yield $redisQueue->get(string $queue = 'default');
```
### RabbitMQ消息队列
```php
//首先需要配置自己的队列
$config['amqp'] = [
'rabbit' => [
'host' => '127.0.0.1',
'port' => '5672'
]
];
//准备一个队列,并配置路由key,默认为default
$rabbit = $this->getObject(PG\MSF\Queue\RabbitMQ::class,
['rabbit', $routing_key = 'default']);
// Enqueue入队,默认队列为default
$res = yield $rabbit->set(string $data, string $queue = 'default');
// Dequeue出队,默认直接Ack,也可以手工Ack
$res = yield $rabbit->get(string $queue = 'default', $isAck = true);
```
### Kafka作为消息队列
```php
//首先需要配置自己的队列
$config['kafka'] = [
'local' => [
'socket.keepalive.enable' => true,
'bootstrap.servers' => '127.0.0.1:9092',
'group.id' => 'default'
]
];
//准备一个队列
$kafka = $this->getObject(PG\MSF\Queue\Kafka::class, ['local']);
// Enqueue入队,默认队列为default
$res = yield $kafka->set(string $data, string $queue = 'default');
// Dequeue
$res = yield $kafka->get(string $queue = 'default');
```
## Shell Exec
在写定时任务的时候,难免会使用到`shell_exec`这个php函数,但是这个函数是阻塞的,
所以我们提供了异步协程执行shell脚本的特性,使用方式也很简单。
```php
//result为shell执行后屏幕的输出,如果执行失败,会返回false
$result = yield $this->getObject(\PG\MSF\Coroutine\Shell::class)->goExec('ps aux | grep msf');
```
5.10 多语言
最后更新于:2022-03-31 23:47:58
# 5.10 多语言
MSF通过插件支持多语言(i18n),国际化(I18N)是指在设计软件时,
使它可以无需做大的改变就能够适应不同的语言和地区的需要。对于 Web 应用程序,
这有着特别重要的意义,因为潜在的用户可能会在全球范围内。
MSF 提供的国际化功能支持全方位信息翻译, 视图翻译,日期和数字格式化。
## 区域和语言
区域设置是一组参数以定义用户希望能在他们的用户界面所看到用户的语言,国家和任何特殊的偏好。
它通常是由语言 ID 和区域 ID 组成。 例如,ID “en-US” 代表英语和美国的语言环境。
为了保持一致性, 在 MSF 应用程序中使用的所有区域 ID 应该规范化为 `ll-CC`。
例如 zh_CN(简体中文)、en_US(美国英语)、zh_TW(繁体中文) ...
## 项目支持配置
```php
$config['params']['i18n'] = [
'demo' => [ // 消息类别名称
'sourceLanguage' => 'en_us', //源语言
'basePath' => ROOT_PATH . '/languages', //翻译语言配置文件路径
'fileMap' => [
'common' => 'common.php', //翻译配置文件
'error' => 'error.php', //随意自定义即可
...
]
],
...
];
```
## 翻译语言文件示例
```php
// languages/zh_CN/common.php
return [
'sayHi' => '你好,{name}' // {name}为变量占位符
];
// languages/zh_CN/error.php
return [
'200' => '正常'
];
// languages/en_US/common.php
return [
'sayHi' => 'Hello,{name}'
];
// languages/en_US/error.php
return [
'200' => 'Ok'
];
```
## 在业务中使用
首先需要在业务中初始化I18N:
```php
I18N::getInstance(getInstance()->config->get('params.i18n', []));
// params.i18n 就是上面的配置段,可以自定义,但是要和上面的配置名对应好。
// 这行最好在自己的AppServer里初始化或者在业务控制器基类里的构造方法初始化。
```
```php
public function actionI18n()
{
// 这行最好在自己的AppServer里初始化或者在业务控制器基类里的构造方法初始化,这里只作为演示方便
I18N::getInstance(getInstance()->config->get('params.i18n', []));
$sayHi = [
'zh_cn' => I18N::t('demo.common', 'sayHi', ['name' => '品果微服务框架'], 'zh_CN'),
'en_us' => I18N::t('demo.common', 'sayHi', ['name' => 'msf'], 'en_US'),
];
$this->outputJson(['data' => $sayHi, 'status' => 200, 'msg' => I18N::t('demo.error', 200, [], 'zh_CN')]);
}
```
5.9 RESTful
最后更新于:2022-03-31 23:47:55
# 5.9 RESTful
MSF 原生支持RESTful风格api,提供GET/POST/PUT/PATCH/HEAD/OPTIONS/DELETE动作的支持。
### RESTful参考
* [理解 RESTful 架构](http://www.ruanyifeng.com/blog/2011/09/restful.html)
* [RESTful API 设计指南](http://www.ruanyifeng.com/blog/2014/05/restful_api.html?from=timeline&isappinstalled=0)
* [从消费者的角度评估 REST 的价值](http://hippoom.github.io/blogs/value-of-hypermedia-from-client-perspective.html)
### verb 介绍
```
'GET', // 从服务器取出资源(一项或多项)
'POST', // 在服务器新建一个资源
'PUT', // 在服务器更新资源(客户端提供改变后的完整资源)
'PATCH', // 在服务器更新资源(客户端提供改变的属性)
'DELETE', // 从服务器删除资源
'HEAD', // 获取 head 元数据
'OPTIONS', // 获取信息,关于资源的哪些属性是客户端可以改变的
```
### MSF实现RESTful程序
```
Rest |- Controller.php 控制器
|- Route.php 路由器
```
### 使用方式
1. 在配置文件中配置路由器为: `$config['server']['route_tool'] = '\\PG\\MSF\\Route\\RestRoute'`
1. 配置URL路由规则
1. 控制器继承 `PG\MSF\Rest\Controller`
### 推荐控制器接收不同动作映射
```
'PUT,PATCH {id}' => 'update', // 更新资源,如:/users/<id>
'DELETE {id}' => 'delete', // 删除资源,如:/users/<id>
'GET,HEAD {id}' => 'view', // 查看资源单条数据,如:/users/<id>
'POST' => 'create', // 新建资源,如:/users
'GET,HEAD' => 'index', // 查看资源列表数据(可分页),如:/users
'{id}' => 'options', // 查看资源所支持的HTTP动词,如:/users/<id> | /users
'' => 'options',
```
### URL路由配置
通过请求url中的path和动作类型即可路由到对应控制器下的某个方法(method)。URL路由配置支持正则方式,在url的path中可携带参数。例如:
```
$config['rest']['route']['rules'] = [
'GET,POST /groups' => '/account/profile',
'GET /users/ask' => 'user/apply',
'GET /users' => 'user/index',
'GET /users/<uid:\d+>' => 'user/view',
'PUT /users/<method:\w+>' => 'user/<method>',
'DELETE /users/<uid:\d+>' => 'user/delete',
]
```
### 响应
在控制器中使用 `$this->output(<params>,); `即可;
### 状态码
状态码只能是标准的http状态码,所有状态码见 `\PG\MSF\Base\Output::$codes`
5.10 多语言
5.8 公共库
最后更新于:2022-03-31 23:47:53
# 5.8 公共库
## 原则
* 与框架“无关”,具有独立性
* 使用 composer 管理
* 具有版本特性
* 升级便捷,耦合度低
* 应用服务各取所需,单个库功能单一
* 协程的适配
## 已开源
* php-log 日志库
* php-exception 异常相关库
* php-aop 切面编程支持
* php-context 请求上下文
* php-i18n 多语言
5.7 RPC
最后更新于:2022-03-31 23:47:51
# 5.7 RPC
RPC(Remote Procedure Call)即远程过程调用,是指网络计算机之间相互调用过程函数,而不需要关心底层的网络通信。MSF的RPC 目前是基于Http协议,当然使用者不需要关心,因为后续有可能会有调整。
MSF实现的RPC具体的基本原则是:
1. 任何类可导出并提供服务
2. RPC Client与RPC Server的类名与方法名一致
3. 任何服务可以很方便的导出自己的类与类方法
## 服务配置
```php
<?php
/**
* RPC服务配置
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
$config['service']['demo']['host'] = 'http://127.0.0.1:8000';
$config['service']['demo']['timeout'] = 2000;
return $config;
```
示例配置代码:
[https://github.com/pinguo/php-msf-demo/config/docker/rpc.php](https://github.com/pinguo/php-msf-demo/blob/master/config/docker/rpc.php)
## 导出服务
```php
<?php
/**
* RPC示例Handler
*/
namespace App\Models\Handlers;
use PG\MSF\Models\Model;
class Sum extends Model
{
public $a;
public function __construct($a = 0)
{
$this->a = $a;
}
/**
* 求和
*
* @param array ...$args
* @return float|int
*/
public function multi(...$args)
{
return array_sum($args);
}
}
```
示例配置代码:
[https://github.com/pinguo/php-msf-demo/app/Models/Handlers/Sum.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Models/Handlers/Sum.php)
## 调用服务
```php
<?php
/**
* RPC示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\RpcClient;
class RpcTest extends Controller
{
public function actionGetSum()
{
/**
* @var RpcClient $client
*/
$client = $this->getObject(RpcClient::class, ['demo']);
$sum = yield $client->handler('sum')->multi(1, 2, 3, 4, 5);
$this->outputJson($sum);
}
}
```
`$this->getObject(RpcClient::class, ['demo']);`注意第二个参数表示调用哪一个服务,和配置相对应。
## 并行调用服务
```php
<?php
/**
* RPC示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\RpcClient;
class RpcTest extends Controller
{
public function actionConcurrentSum()
{
$rpc[] = $this->getObject(RpcClient::class, ['demo'])->handler('sum')->func('multi', 1, 2, 3, 4, 5);
$rpc[] = $this->getObject(RpcClient::class, ['demo'])->handler('sum')->func('multi', 1, 2, 3, 4);
$rpc[] = $this->getObject(RpcClient::class, ['demo'])->handler('sum')->func('multi', 1, 2, 3);
$rpc[] = $this->getObject(RpcClient::class, ['demo'])->handler('sum')->func('multi', 1, 2);
$sum = yield RpcClient::goConcurrent($rpc);
$this->outputJson($sum);
}
}
```
`RpcClient::goConcurrent($rpc);`可以并行发送RPC请求,然后依次获取结果。
5.6 对象池
最后更新于:2022-03-31 23:47:49
# 5.6 对象池
对象池服务可以减少从头创建每个对象的系统开销。在需要对象时从池中提取,在使用完对象时,把它放回池中,等待下一个请求。对象池使你能够控制所使用的对象数量。在PHP的长驻进程模式下,对象池尤其重要,由于PHP的GC缺陷,在高并发下,PHP常驻进程内直接通过new创建对象,会导致PHP进程占用大量的内存,而且很容易出现OOM(out of memory)。
## 主要特性
- 创建固定数量的对象;
- 需要时从池中提取,不需要时归还池中;
- 自动归还对象;
- 根据有效期和使用次数淘汰对象
## 获取对象池
```
$this->getContext()->getObjectPool()
```
理论上基于MSF框架的任何代码都可以通过请求上下文对象来获取对象池对象
## 获取对象
如获取Http Client对象:
```
$client = $this->getContext()->getObjectPool()->get(\PG\MSF\Client\Http\Client::class)
```
### 初始化对象
虽然我们使用了对象池模式,但我们也可以使用__construct()方法,无须额外调用初始化方法。
```
$this->getContext()->getObjectPool()->get(FeedComment::class, ['构造参数1', '构造参数2', ...]);
```
通常情况下,框架内置的类均`use MI;`,则可以直接使用`$this->getObject($className, ['构造参数列表'])`来创建对象,第三方lib也可使用trait来扩展类的功能。
### 资源释放
className::destroy()
每一个类,理论上都需要定义destroy()方法,用于资源的手工释放,但是不需要显示调用,在请求结束时由框架自动调用。
对于和请求相关的数据,理论上我们是需要在请求结束后释放相关资源,框架对于资源释放的策略及优先级如下:
1. 响应请求后调用Controller::destroy()
2. 依次调用当前请求所使用的对象的destroy()方法
3. 将所有public的类属性的值设置为初始值
通常情况下,destroy方法用于处理private,protected的类属性,public由框架自动清理。
如下示例:
```php
<?php
/**
* Demo模型
*/
namespace App\Models;
use PG\MSF\Models\Model;
class Demo extends Model
{
public $pro1;
public $pro2;
protected $pro3;
private $pro4;
public function __construct($pro1, $pro2)
{
parent::__construct();
$this->pro1 = $pro1;
$this->pro2 = $pro2;
$this->pro3 = "protected";
$this->pro2 = "private";
}
public function getMockIds()
{
// 读取配置后返回
return $this->getConfig()->get('params.mock_ids', []);
}
public function destroy()
{
parent::destroy();
$this->pro3 = null;
}
}
```
1. pro1,pro2属性由框架自动在请求结束后赋值为初始值
2. pro3由业务手工在destroy()内清理
3. pro4长驻进程,永久使用
### 资源释放级别
除了上述资源释放策略,php-msf还提供自定义的资源释放策略,只需要在获取对象时提供第三个参数,如:
```php
function DS()
{
$this->getObject(Demo::class, [1, 2], \Marco::DS_PUBLIC | \Marco::DS_PROTECTED);
}
```
1. Marco::DS_PUBLIC 为默认策略
2. Marco::DS_PROTECTED 为释放protected属性
3. Marco::DS_PRIVATE 为释放private属性
4. Marco::DS_NONE 不释放任何属性
## 对象池实现原理
![对象池实现原理](../images/对象池实现原理.png "对象池实现原理")
## PHP进程内存优化
对象池模式对PHP进程占用的内存优化可以说“完全超出预期”,如下图所示:
![内存优化-30d.png](../images/内存优化-30d.png "内存优化-30d.png")
上图为近30天基于MSF重构的某业务其中一台机器的一个worker进程内存占用的监控数据,从图中有几点:
1. 5-10~5-27,近15天内存占用波峰达1.25G,波谷的值在持续的攀升;
2. 5-27~5-31,近5天内存占用从一个较低的点持续攀升;
3. 6-01之后,内存占用持续稳定在25M上下小辐波动;
在1阶段,框架大量类的对象是直接使用new关键字创建,完全依赖的PHP GC进行内存资源的回收;
在2阶段,框架采用对象池的方案完全重构大量的逻辑,效果很明显,但仍然有部分内存泄露;
在3阶段,优化了业务逻辑,完全按照“资源释放”的策略调整业务代码,内存占用已稳定。
5.5 连接池
最后更新于:2022-03-31 23:47:46
# 5.5 连接池
连接池的重要性,这时就不赘述了,下面具体介绍框架中实现的哪些连接池。
## Redis连接池
### 主要特性
- 支持异步+协程
- 支持断线重连
- 支持自动提取和归还连接
- 统一同步和异步调用方式
### 配置
```php
<?php
/**
* 本地环境
*/
$config['redis']['p1']['ip'] = '127.0.0.1';
$config['redis']['p1']['port'] = 6379;
//$config['redis']['p1']['password'] = 'xxxx';
//$config['redis']['p1']['select'] = 1;
// Redis序列化选项等同于phpredis序列化的各个选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
//$config['redis']['p1']['redisSerialize'] = \Redis::SERIALIZER_PHP;
// PHP序列化选项,为了兼容yii迁移项目的set,get,mset,mget,选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
//$config['redis']['p1']['phpSerialize'] = \Redis::SERIALIZER_NONE;
// 是否将key md5后储存,默认为0,开启为1
//$config['redis']['p1']['hashKey'] = 1;
// 设置key的前缀
//$config['redis']['p1']['keyPrefix'] = 'demo_';
return $config;
```
示例配置代码:
[./php-msf-demo/app/config/docker/redis.php](https://github.com/pinguo/php-msf/pinguo/config/docker/redis.php)
- $config['redis']
代表Redis连接池相关配置
- p1,p2,p3,p4,p5,p6
这里的p仅代表一台或者一组Redis服务器,在使用连接池时会用到,如果Redis服务器端分片(比如twemproxy)就填写为集群导出的地址与端口等信息。
- ip
Redis服务器地址
- port
Redis服务器端口
- password
Redis认证密钥
- select
Redis DB
- redisSerialize
Redis序列化选项等同于phpredis序列化的各个选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
- phpSerialize
PHP序列化选项,为了兼容yii迁移项目的set,get,mset,mget,选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
- hashKey
是否将key md5后储存,默认为0,开启为1
- keyPrefix
设置key的前缀
### Redis连接池的使用
```php
/**
* Redis示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use App\Models\Demo as DemoModel;
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);
}
}
```
1. $this->getRedisPool($name)
获取连接池对象,并选择名为$name的连接池,$name由配置文件中声明,比如上述配置中的tw
2. 连接池对象的所有方法映射为标准的Redis操作指令
如:`SETEX key seconds value`映射为`$this->getRedisPool($name)->setex($key, $seconds, $value)`
3. string类型的简化操作
`$this->getRedisPool($name)->cache($key, $value = '', $expire = 0)`,`$key`为redis key,`$value`为缓存的值,`$expire`为过期时间,默认不会过期。
4. 执行lua脚本
`$this->getRedisPool($name)->evalMock($script, $args = array(), $numKeys = 0)`
如:
```php
<?php
function luaExample()
{
$num = 100;
$lua = "
local allWorks = {}
local recWorks = {}
local random = nil
for k, v in pairs(KEYS) do
local works = redis.call('sRandMember', v, '" . $num . "')
if works ~= nil then
for key, val in pairs(works) do
table.insert(allWorks, val)
end
end
end
while #recWorks < " . $num . " and #allWorks > 0 do
random = math.random(#allWorks)
table.insert(recWorks, allWorks[random])
table.remove(allWorks, random)
end
return cjson.encode(recWorks)
";
$keys = ['feedId1', 'feedId2', 'feedId3'];
$this->getRedisPool('tw')->evalMock($lua, $keys, count($keys));
}
```
## Redis代理
在Redis连接池的基本上,MSF框架还实现了Redis代理的基本功能,主要特性有:
- 支持分布式自动分片
- 支持master-slave读写分离
- 支持故障自动failover
### 配置
```php
<?php
/**
* 本地环境
*/
$config['redis']['p1']['ip'] = '127.0.0.1';
$config['redis']['p1']['port'] = 6379;
//$config['redis']['p1']['password'] = 'xxxx';
//$config['redis']['p1']['select'] = 1;
// Redis序列化选项等同于phpredis序列化的各个选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
//$config['redis']['p1']['redisSerialize'] = \Redis::SERIALIZER_PHP;
// PHP序列化选项,为了兼容yii迁移项目的set,get,mset,mget,选项如:\Redis::SERIALIZER_PHP,\Redis::SERIALIZER_IGBINARY
//$config['redis']['p1']['phpSerialize'] = \Redis::SERIALIZER_NONE;
// 是否将key md5后储存,默认为0,开启为1
//$config['redis']['p1']['hashKey'] = 1;
// 设置key的前缀
//$config['redis']['p1']['keyPrefix'] = 'demo_';
$config['redis']['p2']['ip'] = '127.0.0.1';
$config['redis']['p2']['port'] = 6380;
$config['redis']['p3']['ip'] = '127.0.0.1';
$config['redis']['p3']['port'] = 6381;
$config['redis']['p4']['ip'] = '127.0.0.1';
$config['redis']['p4']['port'] = 7379;
$config['redis']['p5']['ip'] = '127.0.0.1';
$config['redis']['p5']['port'] = 7380;
$config['redis']['p6']['ip'] = '127.0.0.1';
$config['redis']['p6']['port'] = 7381;
$config['redis_proxy']['master_slave'] = [
'pools' => ['p1', 'p2', 'p3'],
'mode' => \PG\MSF\Marco::MASTER_SLAVE,
];
$config['redis_proxy']['cluster'] = [
'pools' => [
'p4' => 1,
'p5' => 1,
'p6' => 1
],
'mode' => \PG\MSF\Marco::CLUSTER,
];
return $config;
```
示例配置代码:
[https://github.com/pinguo/php-msf-demo/app/config/docker/redis.php](https://github.com/pinguo/php-msf-demo/blob/master/config/docker/redis.php)
- $config['redis_proxy']
代表Redis代理相关配置
- cluster
这里的cluster仅代表一组Redis服务器集群,是一个标识
- mode
Redis集群类型,\PG\MSF\Marco::CLUSTER代表分布式的Redis集群;\PG\MSF\Marco::MASTER_SLAVE代表主从结构的Redis集群
- pools
当mode设置为\PG\MSF\Marco::CLUSTER时,pools为array,他的key表示Redis连接池名称,value表示Redis连接池权重;当mode设置为\PG\MSF\Marco::MASTER_SLAVE,pools为英文逗号分隔的Redis连接池名称列表。
### Redis代理的使用
```php
<?php
/**
* Redis示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use App\Models\Demo as DemoModel;
class Redis extends Controller
{
// 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);
}
}
```
## Redis连接池与代理的关系
![Redis连接池与代表的关系](../images/redis连接池和代理.png "Redis连接池与代表的关系")
## MySQL连接池
### 配置
```php
<?php
/**
* Docker环境
*/
$config['mysql']['master']['host'] = '127.0.0.1';
$config['mysql']['master']['port'] = 3306;
$config['mysql']['master']['user'] = 'root';
$config['mysql']['master']['password'] = '123456';
$config['mysql']['master']['charset'] = 'utf8';
$config['mysql']['master']['database'] = 'demo';
$config['mysql']['slave1']['host'] = '127.0.0.1';
$config['mysql']['slave1']['port'] = 3306;
$config['mysql']['slave1']['user'] = 'root';
$config['mysql']['slave1']['password'] = '123456';
$config['mysql']['slave1']['charset'] = 'utf8';
$config['mysql']['slave1']['database'] = 'demo';
$config['mysql']['slave2']['host'] = '127.0.0.1';
$config['mysql']['slave2']['port'] = 3306;
$config['mysql']['slave2']['user'] = 'root';
$config['mysql']['slave2']['password'] = '123456';
$config['mysql']['slave2']['charset'] = 'utf8';
$config['mysql']['slave2']['database'] = 'demo';
$config['mysql_proxy']['master_slave'] = [
'pools' => [
'master' => 'master',
'slaves' => ['slave1', 'slave2'],
],
'mode' => \PG\MSF\Marco::MASTER_SLAVE,
];
return $config;
```
示例配置代码:
[https://github.com/pinguo/php-msf-demo/app/config/docker/mysql.php](https://github.com/pinguo/php-msf-demo/blob/master/config/docker/mysql.php)
### 执行SQL
```php
<?php
/**
* MySQL示例控制器
*
* app/data/demo.sql可以导入到mysql再运行示例方法
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
class MySQL extends Controller
{
// MySQL连接池示例
public function actionBizLists()
{
// SQL DBBuilder更多参考 https://github.com/jstayton/Miner
$bizLists = yield $this->getMysqlPool('master')->select("*")->from('biz')->go();
$this->outputJson($bizLists);
}
// 直接执行sql
public function actionShowDB()
{
/**
* @var \PG\MSF\Pools\Miner $DBBuilder
*/
$dbs = yield $this->getMysqlPool('master')->go(null, 'show databases');
$this->outputJson($dbs);
}
// 事务示例
public function actionTransaction()
{
/**
* @var \PG\MSF\Pools\Miner|\PG\MSF\Pools\MysqlAsynPool $mysqlPool
*/
$mysqlPool = $this->getMysqlPool('master');
// 开启一个事务,并返回事务ID
$id = yield $mysqlPool->goBegin();
$up = yield $mysqlPool->update('user')->set('name', '徐典阳-1')->where('id', 3)->go($id);
$ex = yield $mysqlPool->select('*')->from('user')->where('id', 3)->go($id);
if ($ex['result']) {
yield $mysqlPool->goCommit($id);
$this->outputJson('commit');
} else {
yield $mysqlPool->goRollback($id);
$this->outputJson('rollback');
}
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Controllers/MySQL.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/MySQL.php)
### DBQueryBuilder
目前php-msf整合的是DB Query Builder是[Miner](https://github.com/jstayton/Miner),更多SQL的拼装请参考它。
另外,$this->getMysqlPool('连接池配置名'),获取的连接池对象,可以在上面直接调用Miner的相关方法,进行sql拼装。
### 关于 go($id = null, $sql = null)
`go($id = null, $sql = null)`是以协程方法执行SQL,它会创建一个MySQL协程,其中`$id`为事务ID,如果未启用事务,默认为null。`$sql`为手工书写待执行的SQL。
### 事务
事务操作的一般流程为:
1. 开启一个事务,并返回事务ID
2. 执行一个SQL,设置事务ID,执行一个SQL,设置事务ID,...
3. 提交(回滚)事务
用代码实现即:
```
try {
$id = yield $mysqlPool->goBegin();
$res1 = yield $mysqlPool->update($table)->set($filed, $value)->go($id)
$res1 = yield $mysqlPool->update($table)->set($filed, $value)->go($id)
} catch (\Exception $e) {
yield $mysqlPool->goRollback($id);
throw $e;
}
yield $mysqlPool->goCommit($id);
```
## MySQL代理
在MySQL连接池的基本上,MSF框架还实现了MySQL代理的基本功能,主要特性有:
* 支持master-slave读写分离
* 支持事务
### 配置代理
如上述配置代码
```php
$config['mysql_proxy']['master_slave'] = [
'pools' => [
'master' => 'master',
'slaves' => ['slave1', 'slave2'],
],
'mode' => \PG\MSF\Marco::MASTER_SLAVE,
];
```
- $config['mysql_proxy']
代表MySQL代理相关配置
- master_slave
这里的master_slave仅代表一组MySQL服务器集群,是一个标识
- mode
MySQL集群类型\PG\MSF\Marco::MASTER_SLAVE代表主从结构的MySQL集群
- pools
当mode设置为\PG\MSF\Marco::MASTER_SLAVE, `pools.master`表示MySQL主节点对应的连接池标识;`pools.slaves`为数字索引MySQL从节点对应的连接池标识列表
### MySQL代理的使用
```php
<?php
/**
* MySQL示例控制器
*
* app/data/demo.sql可以导入到mysql再运行示例方法
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
class MySQL extends Controller
{
// MySQL代理使用示例
public function actionProxy()
{
/**
* @var \PG\MSF\Pools\Miner|\PG\MSF\Pools\MysqlAsynPool $mysqlProxy
*/
$mysqlProxy = $this->getMysqlProxy('master_slave');
$bizLists = yield $mysqlProxy->select("*")->from('user')->go();
$up = yield $mysqlProxy->update('user')->set('name', '徐典阳-6')->where('id', 3)->go();
$this->outputJson($bizLists);
}
// MySQL代理事务,事务只会在主节点上执行
public function actionProxyTransaction()
{
/**
* @var \PG\MSF\Pools\Miner|\PG\MSF\Pools\MysqlAsynPool $mysqlProxy
*/
$mysqlProxy = $this->getMysqlProxy('master_slave');
// 开启一个事务,并返回事务ID
$id = yield $mysqlProxy->goBegin();
$up = yield $mysqlProxy->update('user')->set('name', '徐典阳-2')->where('id', 3)->go($id);
$ex = yield $mysqlProxy->select('*')->from('user')->where('id', 3)->go($id);
if ($ex['result']) {
yield $mysqlProxy->goCommit($id);
$this->outputJson('commit');
} else {
yield $mysqlProxy->goRollback($id);
$this->outputJson('rollback');
}
}
}
```
MySQL代理基于连接池,它和连接池的使用唯一区别在于从`$this->getMysqlPool`切换为`$this->getMysqlProxy`,所有的调用方式和连接池保持一致,就是这么简单。
## MySQL同步模式
有一些场景,需要用到MySQL同步查询数据,比如Task在Tasker进程中执行,由于Tasker是同步阻塞的进程模型,在处理数据过程中又需要查询数据库中的数据,然后再计算相关数据,这个时候就需要使用MySQL同步模式。
php-msf框架内部已经将异步和同步查询MySQL数据的差异屏蔽,同步模式下采用长连接,如果连接断开,驱动会自动重连,唯一的区别在于同步模式不需要添加yield关键字,如:
### MySQL同步Task
```php
<?php
/**
* Demo Task
*
* 注意理论上本文件代码应该在Tasker进程中执行
*/
namespace App\Tasks;
use \PG\MSF\Tasks\Task;
/**
* Class Demo
* @package App\Tasks
*/
class Demo extends Task
{
/**
* 连接池执行同步查询
*
* @return array
*/
public function syncMySQLPool()
{
$user = $this->getMysqlPool('master')->select("*")->from("user")->go();
return $user;
}
/**
* 代理执行同步查询
*
* @return array
*/
public function syncMySQLProxy()
{
$user = $this->getMysqlProxy('master_slave')->select("*")->from("user")->go();
return $user;
}
/**
* 连接池执行同步事务
*
* @return boolean
*/
public function syncMySQLPoolTransaction()
{
$mysqlPool = $this->getMysqlPool('master');
$id = $mysqlPool->begin();
// 开启一个事务,并返回事务ID
$up = $mysqlPool->update('user')->set('name', '徐典阳-1')->where('id', 3)->go($id);
$ex = $mysqlPool->select('*')->from('user')->where('id', 3)->go($id);
if ($ex['result']) {
$mysqlPool->commit();
return true;
} else {
$mysqlPool->rollback();
return false;
}
}
/**
* 代理执行同步事务
*
* @return boolean
*/
public function syncMySQLProxyTransaction()
{
$mysqlPool = $this->getMysqlProxy('master_slave');
$id = $mysqlPool->begin();
// 开启一个事务,并返回事务ID
$up = $mysqlPool->update('user')->set('name', '徐典阳-1')->where('id', 3)->go($id);
$ex = $mysqlPool->select('*')->from('user')->where('id', 3)->go($id);
if ($ex['result']) {
$mysqlPool->commit();
return true;
} else {
$mysqlPool->rollback();
return false;
}
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Tasks/Demo.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Tasks/Demo.php)
### 调用MySQL同步查询数据
```php
<?php
/**
* MySQL示例控制器
*
* app/data/demo.sql可以导入到mysql再运行示例方法
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use App\Tasks\Demo as DemoTask;
class MySQL extends Controller
{
// 通过Task,同步执行MySQL查询(连接池)
public function actionSyncMySQLPoolTask()
{
/**
* @var DemoTask $demoTask
*/
$demoTask = $this->getObject(DemoTask::class);
$user = yield $demoTask->syncMySQLPool();
$this->outputJson($user);
}
// 通过Task,同步执行MySQL查询(代理)
public function actionSyncMySQLProxyTask()
{
/**
* @var DemoTask $demoTask
*/
$demoTask = $this->getObject(DemoTask::class);
$user = yield $demoTask->syncMySQLProxy();
$this->outputJson($user);
}
// 通过Task,同步执行MySQL事务查询(连接池)
public function actionSyncMySQLPoolTaskTransaction()
{
/**
* @var DemoTask $demoTask
*/
$demoTask = $this->getObject(DemoTask::class);
$user = yield $demoTask->syncMySQLPoolTransaction();
$this->outputJson($user);
}
// 通过Task,同步执行MySQL事务查询(代理)
public function actionSyncMySQLProxyTaskTransaction()
{
/**
* @var DemoTask $demoTask
*/
$demoTask = $this->getObject(DemoTask::class);
$user = yield $demoTask->syncMySQLProxyTransaction();
$this->outputJson($user);
}
}
```
5.4 请求上下文
最后更新于:2022-03-31 23:47:44
# 5.4 请求上下文
我们实现了纯异步长驻的PHP Http Server,内存中的对象和数据均会复用,所以PHP的全局变量$_GET/$_POST/$_GLOBAL/$_REQUEST/类的静态属性要慎用,最好是不用。因为同一个Worker进程在同一时间有可能正在处理多个请求,比如A请求修改了全局的数据很有可能会影响B请求的正确处理。
那在处理请求过程中,如何能够让程序控制流向下传递数据,如何在同一个请求中共享和当前请求相关的数据呢,如何在任何地方方便的获取当前请求相关的数据?这就是请求上下文解决的问题。
## 请求上下文对象
在框架中封装了请求的上下文,在任何时候,有访问请求对象、响应输出对象、日志、日志ID、对象池操作、自定义请求数据的需要,都必须通过请求上下文对象来完成。
### 获取请求上下文对象
通常情况下,在MSF框架内直接使用$this->getContext()即可,包括在Controller,Model,Task。
需要特别注意的是,第三方或者自定义lib,通过对象池创建的对象是直接注入context属性,也是通过$this->getContext()的方法获取请求上下文对象。
### 请求体
提供了获取get/post/header/cookie/server等数据,通过请求上下文来获取,即$this->getContext()->getInput(),如下示例:
在控制器中使用示例:
```
// 指定获取 get 参数
$this->getContext()->getInput()->get('uid');
// 指定获取 post 参数
$this->getContext()->getInput()->post('uid');
// 指定获取 get & post 参数
$this->getContext()->getInput()->getPost('uid'); // 优先从 get 中取
$this->getContext()->getInput()->postGet('uid'); // 优先从 post 中去
// 获取所有 get & post 参数
$this->getContext()->getInput()->getAllPostGet();
// 获取 header 头信息
$this->getContext()->getInput()->getHeader('x-ngx-id');
// 获取所有 header 头信息
$this->getContext()->getInput()->getAllHeader();
// 获取所有 server 信息
$this->getContext()->getInput()->getAllServer();
// 获取原始的 post 包体
$this->getContext()->getInput()->getRawContent();
// 获取 cookie 参数
$this->getContext()->getInput()->getCookie('uid');
// 获取用户 ip
$this->getContext()->getInput()->getRemoteAddr();
// 获取 pathinfo 信息
$this->getContext()->getInput()->getPathInfo();
// 获取请求 uri
$this->getContext()->getInput()->getRequestUri();
```
### 响应体
提供了设置常用的header(如:content-type/cookie)等方法,响应json、html等数据格式
在控制器中调用:
```
// 响应数据
$this->getContext()->getOutput()->output($data, $status = 200);
```
响应结果如:
```
581af00d4b58d59d22e8d7a6
```
```
// 输出json数据格式
$this->getContext()->getOutput()->outputJson($data, $status = 200);
```
响应结果如:
```json
[
'581af00d4b58d59d22e8d7a6',
'581af00d4b58d59d22e8d7a7',
'581af00d4b58d59d22e8d7a8',
]
```
`$status`为HTTP状态码,默认为200。
```
// 输出html数据(自动加载并渲染模板)
$this->getContext()->getOutput()->outputView($data);
```
但是在控制器基类中已经直接封装了三个同名的方法`Controller::output($data, $status = 200)`, `Controller::outputJson($data, $status = 200)`,`Controller::outputView($data, $view = null)`,可以直接使用。
```
// 设置Content-Type报头
$this->getContext()->getOutput()->setContentType('text/html; charset=UTF-8');
```
```
// 直接设置任何其他报头
$this->getContext()->getOutput()->setHeader('ContentType: text/html; charset=UTF-8');
```
另外,需要特别注意的是Http Server响应请求,实际上是调用了`$this->getContext()->getOutput()->end()`方法,所以如果仅仅是输出响应也可以直接调用:
```
// 直接响应http请求,不经过任何的加工
$this->getContext()->getOutput()->end('ok');
```
### 日志对象
日志对象提供框架的日志功能,是满足pr-4的标准日志对象,如下示例:
```
$this->getContext()->getLog()->error('致命错误')
$this->getContext()->getLog()->warning('警告错误')
$this->getContext()->getLog()->info('info日志')
$this->getContext()->getLog()->pushLog('key', $value)
```
### 对象池
对象池,任何有创建类对象的需求,均要使用对象池的方式,更多的介绍会在`对象池`,如下示例:
```
$this->getContext()->getObjectPool()->get(ClassName)
```
通常情况下,框架内置的类均`use MI;`,则可以直接使用`$this->getObject($className, ['构造参数列表'])`来创建对象,第三方lib也可使用trait来扩展类的功能。
### 自定义数据
用户自定义的请求上下文数据应用场景比如,多个地方均需要验证用户的登录信息,验证成功就可以将用户了uid/name/email等信息设置到请求的上下文,然后任何其他代码需要访问用户信息就可以直接使用。另外需要注意的是尽可能设置为标题和数组的自定义数据,减少内存泄露的风险。
```
// 设置自定义key对应的value
$this->getContext()->setUserDefined('key', 'value')
// 获取自定义的key对应的value
$this->getContext()->getUserDefined('key')
```
### 其他
```
// 获取当前请求的控制器名
$this->getContext()->getControllerName()
// 获取当前请求的控制器方法名
$this->getContext()->getActionName()
```
5.3 异步Http Client
最后更新于:2022-03-31 23:47:42
# 5.3 异步Http Client
支持协程的异步Http Client很关键,在微服务系统框架中,服务与服务的交互大多是通过Http接口来实现,即使有封装RPC,也大多在Http Client的基础上,当然我们也可以选择自定义的Tcp文本或者二进制协议,这里我们主要介绍MSF框架中的Http Client的实现与使用。本节中的示例代码:[https://github.com/pinguo/php-msf-demo/app/Controllers/Http.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/Http.php)
## 实现
框架对http client的支持是基于swoole_http_client,同时在此基础上封装了dns查询、dns缓存、keep-alive、简单快捷操作、多个请求并行的各种方法。
## 基本用法
```php
<?php
/**
* 异步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
/**
* 获取百度首页,手工进行DNS解析和数据拉取
*/
public function actionBaiduIndexWithOutDNS()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
yield $client->goDnsLookup('http://www.baidu.com');
$sendGet = $client->goGet('/');
$result = yield $sendGet;
$this->outputView(['html' => $result['body']]);
}
}
```
这种用法将一个Http请求方法分成为两步: 第一步,DNS查询;第二步,Get请求。可能大家会很奇怪,不就一个Http请求嘛,还分两步?其实我们原来使用CURL扩展的时候,也是这两个步骤,只是CURL内部把我们完成了DNS查询。
另外,由于DNS查询是一次UDP的请求,PHP内置函数`string gethostbyname ( string $hostname )`是同步阻塞模式,如果使用这个函数,将使我们的Sever退化为同步Server,MSF框架进行DNS查询使用了`swoole_async_dns_lookup()`进行异步DNS解析。
一个http请求,开发代码进行两次yield,开发效率不高,但是性能是最好的;同时我们也提供一些快捷的方法,在只有一次接口请求的开发中性能和效率均可得到提升。
## 快捷POST/GET
```php
<?php
/**
* 异步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
/**
* 获取百度首页,自动进行DNS,自动通过Get拉取数据
*/
public function actionBaiduIndexGet()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$result = yield $client->goSingleGet('http://www.baidu.com/');
$this->outputView(['html' => $result['body']]);
}
/**
* 获取百度首页,自动进行DNS,自动通过Post拉取数据
*/
public function actionBaiduIndexPost()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$result = yield $client->goSinglePost('http://www.baidu.com/');
$this->outputView(['html' => $result['body']]);
}
}
```
`\PG\MSF\Client\Http\Client::goSingleGet()`,`\PG\MSF\Client\Http\Client::goSinglePost()`两个方法可以快捷的自动完成DNS和请求的发送,直接返回响应内容。
## 响应数据
```
[
'errCode' => 0
'sock' => 17
'host' => '180.97.33.107'
'port' => 80
'headers' => [
'content-type' => 'text/html'
'content-encoding' => 'gzip'
'cache-control' => 'no-cache'
'pragma' => 'no-cache'
'content-length' => '363'
'set-cookie' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
]
'type' => 1025
'requestHeaders' => [
'Host' => 'www.baidu.com'
'X-Ngx-LogId' => '59496e12c3474d040f41fda2'
]
'requestBody' => null
'cookies' => [
'bai' => '16.'
]
'set_cookie_headers' => [
'bai' => 'bai=16.;Domain=.baidu.com;Path=/;Max-Age=10'
]
'body' => '<body></body><script type=\"text/javascript\">u=\"https://www.baidu.com/?tn=93817326_hao_pg\";d=document;/webkit/i.test(navigator.userAgent)?(f=d.createElement(\'iframe\'),f.style.width=1,f.style.height=1,f.frameBorder=0,d.body.appendChild(f).src=\'javascript:\"<script>top.location.replace(\\\'\'+u+\'\\\')<\\/script>\"\'):(d.open(),d.write([\'<meta http-equiv=\"refresh\"content=\"0;url=\',\'\"/>\'].join(u)),d.close());function g(k){return v=eval(\"/\"+k+\"=(.*?)(&|$)/i.exec(location.href)\"),v?v[1]:\"\"}</script>'
'statusCode' => 200
]
```
其中:
errCode的具体含义:[附录:Linux错误信息(errno)列表](https://wiki.swoole.com/wiki/page/172.html)
statusCode为Http响应的状态码:
[维基百科HTTP状态码](https://zh.wikipedia.org/wiki/HTTP%E7%8A%B6%E6%80%81%E7%A0%81)
[OSCHINA HTTP状态码](http://tool.oschina.net/commons?type=5)
body为响应正文
## 并行请求
Http请求分成了DNS查询和发送数据两个异步部分,从而在多个内部接口请求中会写大量的冗余代码的,故框架封装了简单实用的并行的Http Client,大大的简化了发送并行请求。
```php
<?php
/**
* 异步HTTP CLIENT示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use \PG\MSF\Client\Http\Client;
class Http extends Controller
{
// 略
/**
* 并行多次获取百度首页,自动进行DNS,自动通过Get或者Post拉取数据
*/
public function actionConcurrentBaiduIndex()
{
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$requests = [
'http://www.baidu.com/',
[
'url' => 'http://www.baidu.com/',
'method' => 'POST'
],
];
$results = yield $client->goConcurrent($requests);
$this->outputView(['html' => $results[0]['body'] . $results[0]['body']]);
}
}
```
`\PG\MSF\Client\Http\Client::goConcurrent($requests)`是我们封装的快捷并行请求方式,`$requests`的数据结构如:
```php
[
'http://www.baidu.com/xxx',
[
// 必须为全路径URL
'url' => 'http://www.baidu.com/xxx',
'method' => 'GET',
'dns_timeout' => 1000, // 默认为30s
'timeout' => 3000, // 默认不超时
'headers' => [], // 默认为空
'data' => ['a' => 'b'] // 发送数据
],
[
'url' => 'http://www.baidu.com/xxx',
'method' => 'POST',
'timeout' => 3000,
'headers' => [],
'data' => ['a' => 'b'] // 发送数据
],
[
'url' => 'http://www.baidu.com/xxx',
'method' => 'POST',
'timeout' => 3000,
'headers' => [],
'data' => ['a' => 'b'] // 发送数据
],
]
```
## DNS缓存
HTTP Client的DNS查询为提升性能,默认情况下,已经开启缓存,缓存策略为:
1. DNS缓存有效时间默认为60s
2. 已解析DNS使用次数上限为10000次
只要判断有效时间,如果已过有效期即缓存失效;如果在有效期内,使用次数超过10000次,则重新进行DNS解析
## DNS配置
默认开启了DNS缓存,并有相应的策略,我们也提供了配置项来修改缓存策略,
```php
$config['http']['dns'] = [
// 有效时间,单位秒
'expire' => 30,
// 使用次数上限
'times' => 1000,
];
```
## Keep-Alive
HTTP持久连接(HTTP persistent connection,也称作HTTP keep-alive或HTTP connection reuse)
是使用同一个TCP连接来发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。
如果客户端支持 keep-alive,它会在请求的包头中添加:
```
Connection: Keep-Alive
```
然后当服务器收到请求,作出回应的时候,它也添加一个头在响应中:
```
Connection: Keep-Alive
```
这样做,连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接。
这一直继续到客户端或服务器端认为会话已经结束,其中一方中断连接。
#### 优势
- 较少的CPU和内存的使用(由于同时打开的连接的减少了)
- 允许请求和应答的HTTP管线化
- 降低拥塞控制 (TCP连接减少了)
- 减少了后续请求的延迟(无需再进行握手)
- 报告错误无需关闭TCP连接
## Keep-Alive 配置
由上面的描述可知,Keep-Alive需要客户端和服务端都支持才可以。
假如我们的请求链路是:浏览器->Nginx->php-msf->后端API服务
那么:浏览器是纯客户端,后端API服务是纯服务端,Nginx和php-msf既是服务端又是客户端。
#### 保持和client的长连接
Nginx作为http服务端,默认情况下,已经自动开启了对client连接的keep-alive支持。
一般场景可以直接使用,但是对于一些比较特殊的场景,还是有必要调整个别参数。
需要修改Nginx的配置文件(在nginx安装目录下的conf/nginx.conf):
```
http {
keepalive_timeout 120s 120s;
keepalive_requests 10000;
}
```
keepalive_timeout指令的语法:
```
Syntax: keepalive_timeout timeout [header_timeout];
Default: keepalive_timeout 75s;
Context: http, server, location
```
第一个参数设置keep-alive客户端连接在服务器端保持开启的超时值。值为0会禁用keep-alive客户端连接。
可选的第二个参数在响应的header域中设置一个值“Keep-Alive: timeout=time”。这两个参数可以不一样。
注:默认75s一般情况下也够用,对于一些请求比较大的内部服务器通讯的场景,适当加大为120s或者300s。第二个参数通常可以不用设置。
keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量。当最大请求数量达到时,连接被关闭。默认是100。
这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。
这个参数往往被大多数人忽略,因为大多数情况下当QPS(每秒请求数)不是很高时,默认值100凑合够用。但是,对于一些QPS比较高(比如超过10000QPS,甚至达到30000,50000甚至更高) 的场景,默认的100就显得太低。
简单计算一下,QPS=10000时,客户端每秒发送10000个请求(通常建立有多个长连接),每个连接只能最多跑100次请求,意味着平均每秒钟就会有100个长连接因此被nginx关闭。同样意味着为了保持QPS,客户端不得不每秒中重新新建100个连接。因此,如果用netstat命令看客户端机器,就会发现有大量的TIME_WAIT的socket连接(即使此时keep alive已经在client和nginx之间生效)。
因此对于QPS较高的场景,非常有必要加大这个参数,以避免出现大量连接被生成再抛弃的情况,减少TIME_WAIT。
#### 保持和php-msf server的长连接
为了让nginx和php-msf server(nginx称为upstream)之间保持长连接,典型设置如下:
```
http {
upstream MSF_BACKEND {
server 127.0.0.1:8000;
keepalive 300; // 这个很重要!设置每个worker进程在缓冲中保持的到upstream服务器的空闲keepalive连接的最大数量.当这个数量被突破时,最近使用最少的连接将被关闭。
}
server {
listen 80 default_server;
server_name "";
location / {
proxy_pass http://MSF_BACKEND;
proxy_set_header Host $Host;
proxy_set_header x-forwarded-for $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
add_header Cache-Control no-store;
add_header Pragma no-cache;
proxy_http_version 1.1; // 这两个最好也设置
proxy_set_header Connection "";
client_max_body_size 3072k;
client_body_buffer_size 128k;
}
}
}
```
## MSF长连接
#### 保持和客户端的长连接
和Nginx一样,只要客户端支持长连接,msf就会默认支持长连接,不需要任何配置。
#### 保持和后端接口服务的长连接
当我们的服务需要请求其他的服务的时候,那么这个场景长连接就是需要的了,php-msf默认开启的长连接,默认的配置如下:
```
http.keepAlive.expire = 120 //每个长连接有效期为20s
http.keepAlive.times = 10000 //每个长连接在有效期内最多处理10000个请求
```
也可用通过
```
$config['http']['keepAlive'] = [
'expire' => 60, // 为0时表示关闭 keep-alive
'times' => 1000
]
```
来控制 http-client 的keep-alive行为。
5.2 类的加载
最后更新于:2022-03-31 23:47:39
# 5.2 类的加载
## 类的加载
php-msf类的加载采用自动加载机制,它是基于命名空间的全自动加载。
## 获取对象
php-msf要求所有的对象要求通过对象池的方式来获取,如下示例:
```php
<?php
function httpLoadObject()
{
// 获取Http Client
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 获取Task
$client = $this->getObject(\App\Tasks\Idallloc::class);
// 获取Model
$client = $this->getObject(\App\Models\Demo::class, [1, 2]);
// 获取内建对象
$client = $this->getObject(\swoole_http_client::class, [$ip, $port]);
}
```
5.1 协程
最后更新于:2022-03-31 23:47:37
# 5.1 协程
在前面的章节[2.3](../chapter-2/2.3-协程原理.md)介绍了协程的原理及PHP用户空间如何实现协程,本节将重点介绍MSF框架的协程如何使用,同时会剖析PHP工程级协程调度器的实现及调度算法。
## 为什么要使用协程?
至于为什么使用协程,可能大家看法还不统一,这里举例来说明,我们实现一个接口,此接口内包含:
2次http请求其他接口A、B,
1次Redis请求C
他们之间的依赖关系为:`((A && B) || C)`
```php
<?php
/**
* 协程示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\Http\Client;
class CoroutineTest extends Controller
{
/**
* 异步回调的方式实现(A && B) || C
*/
public function actionCallBackMode()
{
$client = new \swoole_redis;
$client->connect('127.0.0.1', 6379, function (\swoole_redis $client, $result) {
$client->get('apiCacheForABCallBack', function (\swoole_redis $client, $result) {
if (!$result) {
swoole_async_dns_lookup("www.baidu.com", function($host, $ip) use ($client) {
$cli = new \swoole_http_client($ip, 443, true);
$cli->setHeaders([
'Host' => $host,
]);
$apiA = "";
$cli->get('/', function ($cli) use ($client, $apiA) {
$apiA = $cli->body;
swoole_async_dns_lookup("www.qiniu.com", function($host, $ip) use ($client, $apiA) {
$cli = new \swoole_http_client($ip, 443, true);
$cli->setHeaders([
'Host' => $host,
]);
$apiB = "";
$cli->get('/', function ($cli) use ($client, $apiA, $apiB) {
$apiB = $cli->body;
if ($apiA && $apiB) {
$client->set('apiCacheForABCallBack', $apiA . $apiB, function (\swoole_redis $client, $result) {});
$this->outputJson($apiA . $apiB);
} else {
$this->outputJson('', 'error');
}
});
});
});
});
} else {
$this->outputJson($result);
}
});
});
}
/**
* 协程的方式实现(A && B) || C
*/
public function actionCoroutineMode()
{
// 从Redis获取get apiCacheForABCoroutine
$response = yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine');
if (!$response) {
// 从远程拉取数据
$request = [
'https://www.baidu.com/',
'https://www.qiniu.com/',
];
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$results = yield $client->goConcurrent($request);
// 写入redis
$this->getRedisPool('tw')->set('apiCacheForABCoroutine', $results[0]['body'] . $results[1]['body'])->break();
$response = $results[0]['body'] . $results[1]['body'];
}
// 响应结果
$this->outputJson($response);
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Controllers/CoroutineTest.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/CoroutineTest.php)
http://127.0.0.1:8000/CoroutineTest/CoroutineMode
http://127.0.0.1:8000/CoroutineTest/CallBackMode
1. Swoole实现了异步非阻塞的IO模型它是高性能的基础,但是书写逻辑代码非常复杂,需要多层嵌套回调,阅读和维护困难
2. 基于Yield的协程可以用同步的代码编写方式,达到异步IO的效果和性能,避免了传统异步回调所带来多层回调而导致代码无法维护
## 协程的调度顺序
```php
<?php
/**
* 协程示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\Http\Client;
class CoroutineTest extends Controller
{
// 略...
// 姿势一
public function actionNested()
{
$result = [];
/**
* @var Client $client1
*/
$client1 = $this->getObject(Client::class, ['http://www.baidu.com/']);
yield $client1->goDnsLookup();
/**
* @var Client $client2
*/
$client2 = $this->getObject(Client::class, ['http://www.qq.com/']);
yield $client2->goDnsLookup();
$result[] = yield $client1->goGet('/');
$result[] = yield $client2->goGet('/');
$this->outputJson([strlen($result[0]['body']), strlen($result[1]['body'])]);
}
// 姿势二
public function actionUnNested()
{
$result = [];
/**
* @var Client $client1
*/
$client1 = $this->getObject(Client::class, ['http://www.baidu.com/']);
$dns[] = $client1->goDnsLookup();
/**
* @var Client $client2
*/
$client2 = $this->getObject(Client::class, ['http://www.qq.com/']);
$dns[] = $client2->goDnsLookup();
yield $dns[0];
yield $dns[1];
$req[] = $client1->goGet('/');
$req[] = $client2->goGet('/');
$result[] = yield $req[0];
$result[] = yield $req[1];
$this->outputJson([strlen($result[0]['body']), strlen($result[1]['body'])]);
}
}
```
### 解析
两种姿势看上去都使用了协程的yield关键字;姿势一由于协程在调度时,第一个yield没有接收数据时,程序控制流就不会往下继续执行,从而退化为串行请求第三方接口;姿势二由于DNS查询是异步的,就同时进行多个DNS查询,通过yield关键获取协程执行的结果,再同时异步请求多个接口,最后通过yield关键字获取接口响应结果。
通过两种姿势的对比,使用php-msf协程yield关键字,很好的解决了异步IO回调的写法,让程序看上去是同步执行的,yield起来了接收数据的作用,这也是前面所说的yield具有双向通信的最要特性。
姿势一协程调度过程
send http dns1 lookup->rev http dns1 lookup->send http dns2 lookup->rev http dns2 lookup->send get1->rev get1->send get2-> rev get2
姿势二协程调度过程
send http dns1 lookup->send http dns2 lookup->rev http dns1 lookup->rev http dns2 lookup->send get1->send get2->rev get1->rev get2
**需要特别注意的是: 如果某个异步IO操作不需要获取返回值,如设置缓存set key val,框架允许不加yield关键字,需要调用break()方法,这可以大大的提升接口的并发能力**
## 使用MSF协程
通过上述的示例代码,我们不难得出MSF协程的使用方式,通常情况下,我们使用异步客户端发送请求,使用yield关键字获取协程任务的运行结果。
### Http
```php
<?php
function Http()
{
// 获取Http客户端
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 单个接口GET请求(自动完成DNS查询->Http请求发送->获取结果)
$res = yield $client->goSingleGet('http://www.baidu.com/');
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 单个接口POST请求(自动完成DNS查询->Http请求发送->获取结果)
$res = yield $client->goSinglePost('http://www.baidu.com/');
// 多个接口同时请求(自动完成DNS查询->Http请求发送->获取结果)
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
$res = yield $client->goConcurrent(['http://www.baidu.com/', 'http://www.qq.com']);
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 手工DNS查询
yield $client->goDnsLookup('http://www.baidu.com/');
// 手工发送HTTP请求
$res = yield $client->goGet('/');
}
```
### Task
```php
<?php
function Task() {
$idAllloc = $this->getObject(Idallloc::class);
$newId = yield $idAllloc->getNextId();
}
```
### Redis
```php
<?php
function RedisCoroutine() {
$sendRedisGet = $this->getRedisPool('tw')->get('apiCacheForABCoroutine');
$cache = yield $sendRedisGet;
}
```
## MSF协程调度器
MSF协程基于Generator/Yield,IO事件触发,是自主研发的调度器,它的核心思想是发生任何异步IO操作之后,程序的控制流就切换到其他请求,待异步IO可读之后,由程序自行调用调度器接口,进行一次调度,并响应请求。MSF协程完全抛弃了定时器每隔一定时间轮询任务的方案,使得调度器的性能更加接近原生的异步回调方式。
### 关键技术点
* Generator/Yield
* SplStack
* taskMap
### 主要特性
* 协程独立堆栈
* 支持嵌套
* 全调用链路异常捕获
* 调度统计
* 多入口调度
### 协程调度流程
![协程执行流程图](../images/协程执行流程图V2.png "协程执行流程图")
5 框架组件
最后更新于:2022-03-31 23:47:35
# 5.0 框架组件
MSF框架除了实现了简单的高性能MVC,还实现了很多高级的组件,而这些高级组件的应用,则是提升性能的关键。本章节将着重介绍各个组件的实现原理和使用方法,目前实现的组件有:
- [5.1 协程](5.1协程.md)
- [5.2 类的加载](5.2类的加载.md)
- [5.3 异步Http Client](5.3异步HttpClient.md)
- [5.4 请求上下文](5.4请求上下文.md)
- [5.5 连接池](5.5连接池.md)
- [5.6 对象池](5.6对象池.md)
- [5.7 RPC](5.7RPC.md)
- [5.8 公共库](5.8公共库.md)
- [5.9 RESTful](5.9RESTful.md)
- [5.10 多语言](5.10多语言.md)
- [5.11 杂项](5.11杂项.md)
- [5.12 小结](5.12小结.md)
4.8 小结
最后更新于:2022-03-31 23:47:33
# 4.8 小结
本章是介绍框架实现与使用的核心,从宏观上剖析了框架处理请求的流程,并在框架结构上进行了详细说明,使得大家能够在宏观上掌握MSF框架的精髓。然后,我们用了大量的篇幅章节来介绍框架中控制器、模型、同步任务、视图、配置、路由等核心模块的使用,相信大家读完本章之后,完成一些简单的业务开发已经不是问题,后面我们将继续介绍框架的一些高级组件的实现原理和使用。
4.7 路由
最后更新于:2022-03-31 23:47:30
# 4.7 路由
任何一个MVC框架都有路由的功能,是解析请求的URI,将请求分发给相应的控制器和方法,然后进行逻辑处理。
## 内置路由
MSF框架默认路由器为`\PG\MSF\Route\NormalRoute`,也可以实现`\PG\MSF\Route\IRoute`接口类,自定义路由。
## 路由规则
内置路由支持无限层级的路由,即Controller可以无限嵌套目录,如:
http://127.0.0.1:8000/Backend/Config/Page
执行的方法为:\App\Controllers\Backend\Config::actionPage()
http://127.0.0.1:8000/a/b/c/d/f
如F为控制器名,执行的方法为:\App\Controllers\A\B\C\D\F::actionIndex()
如F为方法名,执行的方法为:\App\Controllers\A\B\C\D::actionF()
## 路由缓存
默认开启了路由缓存,也即解析一次请求之后,后续请求会使用路由缓存来将请求分发到相应的控制器动作。如果由于某些需求不能缓存路由,需要用户自行继承`PG\MSF\Route\NormalRoute`将属性`enableCache`设置为false。4.8 小结
4.6 配置
最后更新于:2022-03-31 23:47:28
# 4.6 配置
配置是框架的重要组成部分,MSF框架的配置组件采用了第三方的[hassankhan/config](https://github.com/hassankhan/config),它支持多种配置书写的文件格式如:PHP,INI,XML,JSON,YAML,可以满足大部分的配置需求。但是建议采用PHP数组文件来配置我们的服务。
## PHP配置数组文件
配置采用PHP数组的形式进行书写,每个配置文件的最后一行都需要返回,即`return $config`,如:
```php
<?php
/**
* MongoDb配置文件
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
$config['mongodb']['test']['server'] = 'mongodb://192.168.1.106:27017';
$config['mongodb']['test']['options'] = ['connect' => true];
$config['mongodb']['test']['driverOptions'] = [];
return $config;
````
## 配置目录
在应用程序的主目录中有`config`目录,是用来存放所有配置文件,配置目录的参考结构:
```
├── config // 配置目录
│ ├── check.php // 代码检查配置
│ ├── server.php // 主配置文件(server服务相关)
│ ├── constant.php // 业务常量定义文件
│ ├── log.php // 全局日志配置
│ ├── http.php // HTTP服务配置
│ ├── params.php // 全局业务配置(和运行环境无关)
│ ├── dev // 研发联调环境特殊配置目录
│ ├── docker // docker环境特殊配置目录
│ ├── product // 生产环境特殊配置目录
│ ├── qa // QA环境特殊配置目录
```
对于各个环境的差异配置,单独存在在不同的目录中,如:
`config/docker` 存放Docker环境配置
`config/dev` 存放研发联调环境配置
`config/qa` 存放QA环境配置
`product` 存放生产环境配置
## 主配置文件
是配置服务实例的大部分配置,包括Http Server的参数配置,服务器进程pid文件目录,服务进程名称,服务器worker、Task、reactor数量等等。
### 主配置重要选项说明
#### $config['server']
- $config['server']['process_title']
server服务进程标题前缀
- $config['server']['runtime_path']
server服务运行时目录
- $config['server']['pid_path']
server服务运行时pid目录
- $config['server']['route_tool']
server服务路由分发请求的类名,首先查找app\Route目录,再查到php-msf\src\Route目录,默认为`NormalRoute`
#### $config['server']['set']
- $config['server']['set']['reactor_num']
swoole server的reactor数量,更多请参考[reactor_num](https://wiki.swoole.com/wiki/page/281.html)
- $config['server']['set']['worker_num']
swoole server的worker数量,更多请参考[worker_num](https://wiki.swoole.com/wiki/page/275.html)
- $config['server']['set']['backlog']
swoole server的backlog队列长度,更多请参考[backlog](https://wiki.swoole.com/wiki/page/279.html)
- $config['server']['set']['dispatch_mode']
swoole server的数据包分发策略,对于http服务器推荐设置为1,更多请参考[dispatch_mode](https://wiki.swoole.com/wiki/page/277.html)
- $config['server']['set']['task_worker_num']
swoole server的task worker数量,更多请参考[task_worker_num](https://wiki.swoole.com/wiki/page/276.html)
swoole server的其他配置项,具体参考:[配置选项](https://wiki.swoole.com/wiki/page/274.html)
#### $config['config_manage_enable']
是否启动配置进程,设置为true或者false
#### $config['auto_reload_enable']
是否开启自动检测文件更新,并重启服务,在开发环境建议设置为true,生产环境设置为false
#### $config['user_timer_enable']
是否开启自定义业务Timer进程,为业务需求而定,主要用于一些定时业务,设置为true或者false
#### $config['http_server']
Http服务器相关配置,主要有以下几个重要子配置项
- $config['http_server']['port']
Http服务器监听的端口
- $config['http_server']['socket']
Http服务器监听的地址
#### $config['http']
- $config['http']['domain']
使用Http服务器时,绑定的域名列表
- $config['http']['domain']['localhost']
使用Http服务器时,绑定一个localhost的域名
- $config['http']['domain']['localhost']['root']
使用Http服务器时,绑定了一个localhost的域名,并且域名对应的根目录为此配置项的值,需要特别注意的是,一旦配置了root,那该目录下所有文件就可以被用户访问到
- $config['http']['domain']['localhost']['index']
使用Http服务器时,绑定了一个localhost的域名,并且域名对应的索引文件此配置项的值
- $config['http']['default_method']
使用Http服务器时,Controller的默认方法名,在路由请求时使用
## 日志配置
```php
<?php
/**
* 日志配置文件
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
$config['server']['log'] = [
'handlers' => [
'application' => [
'levelList' => [
\PG\Log\PGLog::EMERGENCY,
\PG\Log\PGLog::ALERT,
\PG\Log\PGLog::CRITICAL,
\PG\Log\PGLog::ERROR,
\PG\Log\PGLog::WARNING
],
'dateFormat' => "Y/m/d H:i:s",
'format' => "%datetime% [%level_name%] [%channel%] [logid:%logId%] %message%\n",
'stream' => RUNTIME_DIR . '/application.log',
'buffer' => 0,
],
'notice' => [
'levelList' => [
\PG\Log\PGLog::NOTICE,
\PG\Log\PGLog::INFO,
\PG\Log\PGLog::DEBUG
],
'dateFormat' => "Y/m/d H:i:s",
'format' => "%datetime% [%level_name%] [%channel%] [logid:%logId%] %message%\n",
'stream' => RUNTIME_DIR . '/notice.log',
'buffer' => 0,
]
]
];
return $config;
```
更新配置示例请参考示例项目[php-msf-demo/config](https://github.com/pinguo/php-msf-demo/blob/master/config/log.php)
## 使用配置
在任何地方都可以通过`getInstance()->config`来访问读取配置,另外在继承`\PG\MSF\Base\Core`类的子类(比如Controller/Model等等)都可以通过`$this->getConfig()`来获取配置对象,访问配置项的代码如:
[php-msf/config/params.php](https://github.com/pinguo/php-msf/pinguo/config/params.php)
```php
<?php
/**
* 业务参数
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
$config['params']['mock_ids'] = [
'581af00d4b58d59d22e8d7a6',
'581198754b58d54465ef4cad',
'580c7fa44b58d53f43e21c43',
'57ef6aec4b58d50d24e21a2a',
'57ee28ed4b58d52f24e21969',
'57eb6a484b58d50e3d078076',
'57e23b444b58d52f1e6689fc',
'57dfc9fc4b58d5581e668918',
'57de06b74b58d5471e66882f',
'57d8b78f4b58d5311e6686d5',
];
return $config;
```
```php
getInstance()->config->get('params.mock_ids', [])
```
get()方法的第一个参数为key名称,对于多维数组的配置,采用点分隔;第二个参数为配置选项默认值,即没有读取到配置时的默认配置。
4.5 同步任务
最后更新于:2022-03-31 23:47:26
# 4.5 同步任务
同步任务即Task,用来做一些异步的慢速任务,比如发邮件、批量任何、任何不支持异步Client的服务调用等等。MSF框架是纯异步Server架构,也就是说任何耗时的任务的代码不能放到Worker进程中执行,所以我们利用Swoole的Task模块来完成耗时任务。
## Task进程
同步任务代码是在Task进程中执行的,Task进程具有以下的特性:
- 同步阻塞
- 支持定时器
目前MSF框架封装了异步的Http Client、Redis Client、MySQL Client,除了这几种原生支持异步外,任何其他的非异步Client的网络IO均要封装成Task,比如MongoDB Task。
## Task示例
框架内封装了Task基类`\PG\MSF\Tasks\Task`,自定义的Task都应该继承此类。另外,框架内置一个MongoDbTask类,是操作MongoDB的任务类,封装了查询和修改MongoDB数据库的一些基本方法,如:
### composer安装依赖
编辑项目的composer.json,加入依赖`alcaeus/mongo-php-adapter`
```json
{
"require": {
"alcaeus/mongo-php-adapter": "^1.0"
}
}
```
或者
```bash
$composer require alcaeus/mongo-php-adapter
```
### MongoDB配置
```php
<?php
/**
* MongoDb配置文件
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
$config['mongodb']['test']['server'] = 'mongodb://192.168.1.106:27017';
$config['mongodb']['test']['options'] = ['connect' => true];
$config['mongodb']['test']['driverOptions'] = [];
return $config;
```
示例代码:
[./php-msf-demo/config/docker/mongodb.php](https://github.com/pinguo/php-msf/pinguo/config/docker/mongodb.php)
### Task业务逻辑类
```php
<?php
namespace App\Tasks;
use \PG\MSF\Tasks\MongoDbTask;
class Idallloc extends MongoDbTask
{
/**
* 当前要用的配置 配置名,db名,collection名
* @var array
*/
protected $mongoConf = ['test', 'demo', 'idalloc'];
public function getNextId($key)
{
$condition = [
'_id' => $key,
];
$update = [
'$inc' => [
'last' => 1,
],
];
$options = [
'new' => true,
'upsert' => true,
];
$doc = $this->mongoCollection->findAndModify($condition, $update, [], $options);
return isset($doc['last']) ? $doc['last'] : false;
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Tasks/Idallloc.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Tasks/Idallloc.php)
### 调用Task
```php
<?php
/**
* MongoDB操作示例
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use App\Tasks\Idallloc;
class MongoDBTest extends Controller
{
public function actionGetNewId()
{
/**
* @var Idallloc $idAlloc
*/
$idAlloc = $this->getObject(Idallloc::class);
$newId = yield $idAlloc->getNextId('test');
$this->output($newId);
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Controllers/MongoDBTest.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/MongoDBTest.php)
我们需要认识到:
1. Worker进程只是将任务投递给Tasker进程后立即返回,即是非阻塞的投递;
2. Tasker进程执行相应的业务逻辑,在这里就是从MongoDB获取新的一个ID;
3. Worker进程是通过协程获取到Tasker执行结果,即调用需要加yield关键字;
### 访问接口
```bash
[worker@newdev ~]$ curl http://127.0.0.1:8000/MongoDBTest/GetNewId
1
```
4.4 视图
最后更新于:2022-03-31 23:47:24
# 4.4 视图
视图是MVC模式中的一部分,它代表数据到终端用户展示逻辑,视图文件为PHP脚本,主要包含HTML代码和展示类PHP代码。MSF框架整合了第三方的模板引擎[plates](https://github.com/pinguo/plates),并修改了部分源码。
## 使用视图
```php
<?php
/**
* 示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use App\Models\Demo as DemoModel;
class Demo extends Controller
{
// 略...
public function actionTplView()
{
$data = [
'title' => 'MSF Demo View',
'friends' => [
[
'name' => 'Rango',
],
[
'name' => '鸟哥',
],
[
'name' => '小马哥',
],
]
];
$this->outputView($data);
}
}
```
示例代码:
[./php-msf-demo/app/Controllers/Demo.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/Demo.php)
- Controller::outputView($data, $view = null)
加载视图文件,并渲染$data数据
`$data`传递到视图文件的数据,为一个关联数组
`$view`视图文件名称,是相对`app/Views`或者`php-msf/src/Views`的文件名,注意不需要写`.php`的后缀
加载的视图文件为[./app/Views/Demo/TplView.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Views/Demo/TplView.php),即:
```php
<h1><?=$this->e($title)?></h1>
<h2>Friends</h2>
<ul>
<?php foreach($friends as $friend): ?>
<li>
<?=$this->e($friend['name'])?>
</li>
<?php endforeach ?>
</ul>
```
## 视图加载策略
### 策略1
默认情况下框架会根据请求的控制器名和方法名自动加载视图文件,比如:
[http://127.0.0.1:8000/Demo/TplView](http://127.0.0.1:8000/Demo/TplView)
这样的URL会自动首先加载的视图文件为`app/Views/Demo/TplView.php`,如果失败,会继续加载`php-msf/src/Views/Demo/TplView.php`,如果还是失败,则会抛出异常。
### 策略2
如果传递了outputView()方法的第二个参数,则会根据$view来加载视图文件,策略同1,先加载app下面的视图,如果未找到则加载框架内置的视图文件
## 模板语法
更多的模板方法,请参考[plates docs](https://github.com/pinguo/plates/tree/master/docs)