__ __ _ _ ____ _
| \/ (_)_ __ (_) / ___|_ _____ ___ | | ___
| |\/| | | '_ \| | \___ \ \ /\ / / _ \ / _ \| |/ _ \
| | | | | | | | | ___) \ V V / (_) | (_) | | __/
|_| |_|_|_| |_|_| |____/ \_/\_/ \___/ \___/|_|\___|
- 仅支持HTTP
- Master-Worker 模式
- Module-Controller-Model 分层
- MySQL 断线自动重连
- Timer, Task 简易封装
- MySQL, Redis 连接池
- MySQL 分表分库
- JSON 作数据通信格式
- Shell 脚本控制服务
- Autoload
- 安全过滤
- 日志收集
- 心跳检测
- 自动路由
- 多模块划分
- 中间件与插件
- Process 管理
- PHP >= 7.0
- swoole, 建议 4+
- pdo
- redis
- pdo_mysql
- Git clone 至任一目录
- 创建数据库并导入SQL 文件
- EVN 的定义在 Boostrap.php 的第一句, 请升级脚本(deploy.py)自行根据环境修改
- 配置文件是 conf/ENV.php。 ENV 区分为 DEV, UAT, PRODUCTION, 请自行根据运行环境调整
- common 为公共配置部分, 影响整体
- $section 分为: common, http, mysql, redis 配置
- 配置文件的 key 务必使用小写字母
- 任意地方均可使用 Config::get($section) 来获取配置文件中 $section 的参数
- 任意地方均可使用 Config::get($section, $key) 来获取配置文件中 $section 中的 $key 对应的参数
- 启动: sh shell/socket.sh start
- 状态: sh shell/socket.sh status
- 停止: sh shell/socket.sh stop
- 重启: sh shell/socket.sh restart
- Reload: sh shell/socket.sh reload, 重启所有Worker/Task进程
- Process 心跳: sh shell/process.sh, 对所有的 Process 进行心跳检测
- 利用Crond 定时运行 shell/socket.sh heartbeat 即可
- 采用 Module-Controll-Model 模式, 所有的请求均转至 Module-Controller下处理
- 默认 Module 为index, 无须声明, 对应的控制器文件位是根目录的 controller
- 在配置文件的 module 中声明新模块,以英文逗号分隔,如 'Api, Admin, Mall, Shop', 对应的控制器文件是 /module/$moduleName/controller
- 中间件:很多时候,我们需要在请求前与后做一些前置与后置的统一业务,比如授权认证,日志与流量收集,中件间就派上用场了。
- 系统的错误文件由 $config['common']['log_file'] 指定
- $config['common']['error_level'] 指定手动记录日志的级别, [1|2|3|4|5], 分别代表 DEBUG, INFO, WARN, ERROR, FATAL,低于规定的日志级别则不记录
- $config['common']['error_file'] 指定手动记录日志的文件
- 任意地方调用 Logger::debug($msg); Logger::info($msg); Logger::warn($msg); Logger::error($msg); Logger::fatal($msg); 则将 $msg 以指定的级别写入 $config['common']['error_file']
- SQL 的错误文件由 $config['common']['mysql_log_file'] 指定, 当执行SQL发生错误时,自动写入, 级别均为 ERROR
public function log(){
Logger::debug('This is a debug msg');
Logger::info('This is an info msg');
Logger::warn('This is a warn msg');
Logger::error('This is an error msg');
Logger::fatal('This is a fatal msg');
Logger::log('This is a log msg');
$level = Config::get('common', 'error_level');
return 'Current error_level => '.$level;
}
- log_method 指定保存日志的方式,默认是 file, 可选 redis. 若选择 redis, 则配置多一个 redis_log, 好处是啥?直接对接至 ELK !
'redis_log' => [
'db' => '0',
'host' => '192.168.1.50',
'port' => 6379,
'pwd' => '123456',
'queue' => 'Queue_log',
],
- 将 http 段的enable 设置为 true, 其他服务设置为 false
- sh shell/socket.sh restart 重启服务
- ps -ef | grep Mini 将看到
Mini_Swoole_http_master: 为 master 进程
Mini_Swoole_manager: 为 manager 进程
Mini_Swoole_task: N 个 task 进程
Mini_Swoole_worker: M 个 worker 进程
- 根目录的 controller 的 Index.php/index(), 负责处理 http 的 index 事件, 也就是首页
- 为了将控制权由 onRequest 路由至控制器, 客户端应该在URL中指定处理该请求的 module (默认是index, 可以忽略), controller 及 action (默认是index, 可以忽略), 示例如下:
// ==== GET 的示例 ==== //
// Index 控制器下的 index() 来处理, 也就是首页, 则URL
http://127.0.0.1:9100
// Http 控制器下的 index() 来处理, 并且带上GET参数, 则URL
http://127.0.0.1:9100/http?username=dym&password=123456
// Http 控制器下的 login() 来处理, 并且带上GET参数, 则URL
http://127.0.0.1:9100/http/login?username=dym&password=123456
// ==== POST 的示例 ==== //
$url = 'http://127.0.0.1:9100/http/login';
$postData = [];
$postData['key'] = 'FOO';
$retval = HttpClient::post($url, $postData);
print_r($retval);
// Api模块的Login控制器下的 logout() 来处理, 则URL
http://127.0.0.1:9100/api/login/logout
// Api模块的User控制器下的 index() 来处理, 则URL
http://127.0.0.1:9100/api/user
- 暂时只支持 GET / POST 方法
- 如果 Controller 不存在, 客户端收到: Controller $controller not found
- 如果 action 不存在, 客户端收到: Method $action not found
- 控制器的方法中调用 $this->response->write($rep) 将数据发送至客户端, 可以调用多次
- 控制器的示例为 controller下的 Index.php 与 Http.php 及 module/Api/controller 下的 Login.php 和 User.php
- 更多 http server 信息请参考 https://wiki.swoole.com/wiki/page/326.html
'mysql' => [
'db' => 'slave',
'host' => '192.168.1.34',
'port' => 3306,
'user' => 'root',
'pwd' => '123456',
'max' => 3,
'log_sql' => true,
],
- 断线自动重连3次
- 配置文件中的 max 是指每一个 worker 有多少个连接对象组成一个连接池
- 控制器中使用 $this->m_user = $this->load('User'); 加载模型
- 使用链式操作 Filed($field)->Where($where)->Order($order)->Limit() 构建 SQL
- Insert(), MultiInsert(), SelectOne(), Select(), UpdateOne(), Update(), UpdateByID(), DeleteOne(), Delete(), DeleteByID()
- 根据ID 查询: SelectByID(), SelectFieldByID()
- 执行复杂的 SQL: Query($sql), QueryOne($sql)
- BeginTransaction(), Commit(), Rollback() 操作事务
- 通用模型(Default)减少复用性方法很少的模型文件
- 将 log_sql 设置为 true 将在 $config['common']['mysql_log_file'] 中记录下每一条被执行的 SQL 语句, 级别为 INFO
- 分页? 在 HTTP 为 GET 的情况下,调用 ->Limit() 方法, 就自动分页了,默认是一页 10 条记录
查询10条: Filed($field)->Where($where)->Order($order)->Limit()->Select();
一页20条: Filed($field)->Where($where)->Order($order)->Limit(20)->Select();
- 示例为 model 下的 User.php 和 Default.php, 其中 Default 为默认通用模型文件
<?php
class M_User extends Model {
function __construct(){
$this->table = 'user';
parent::__construct();
}
public function SelectAll(){
$field = ['id', 'username', 'password'];
return $this->Field($field)->Select();
}
//复杂的SQL(join 等)使用原生的SQL来写,方便维护
public function getOnlineUsers($roomID){
$sql = 'SELECT u.id, u.username, c.roomName FROM '.$this->table.' AS u LEFT JOIN '.TB_PREFIX.'chatroom AS c ON u.roomID = c.id WHERE u.roomID = "'.$roomID.'" ORDER BY u.id DESC LIMIT 20';
return $this->Query($sql);
}
}
// 控制器中调用通用的方法
$this->m_user = $this->load('User');
$field = ['id', 'username'];
$where = ['status' => 1];
$order = ['id' => 'DESC'];
$users = $this->m_user->Field($field)->Where($where)->Order($order)->Limit()->Select();
return JSON($users);
// 调用可复用的 SelectAll();
$users = $this->m_user->SelectAll();
return JSON($users);
// 默认的通用模型使用, model 目录下并没有 News.php, 但一样可以这样使用
$this->m_news = $this->load('News');
$where = ['status' => 1];
$order = ['id' => 'DESC'];
$news = $this->m_news->Where($where)->Order($order)->Select();
return JSON($news);
- 调用 Suffix($tb_suffix) 即可, 如 customer 有 1 至 100 个表,分别是 customer_1, customer_2, .... customer_100, 模型有一个 M_Customer 即可, 访问分表 customer_38 像这样
1: 配置文件中设置 tb_suffix_sf 为 _
2: 代码中: $customer = $this->load('Customer')->Suffix(38)->SelectOne();
- 为了减轻MySQL 主库压力, 有些时候有必要做读写分离,如何支持和切换主从呢? (注: 仅支持 Select 语句读从库, 因此从库的连接只有一个,并不像主库那样有连接池。当然,如果要实现从库也是连接池,也不难,改改即可)
- 配置文件中像 mysql 节点一样设置一个 mysql_slave
'mysql_slave' => [
'db' => 'slave',
'host' => '192.168.1.34',
'port' => 3306,
'user' => 'root',
'pwd' => '123456',
],
- 代码中调用 SetDB('SLAVE') 后再 Select()
$user = $this->load('User')->SetDB('SLAVE')->SelectOne();
- 调皮的你又想切换为 MASTER 呢
$user = $this->load('User')->SetDB('MASTER')->SelectOne();
- 还可以结合分表一起使用
$customer = $this->load('Customer')->SetDB('SLAVE')->Suffix(38)->SelectOne();
- 来个长的链式操作
$field = ['id', 'mobile', 'summary', 'address'];
$where = ['companyID' => 38];
$order = ['id' => 'DESC'];
$customer = $this->load('Customer_ref')->SetDB('SLAVE')->Suffix(38)->Field($field)->Where($where)->Order($order)->Limit()->Select();
return 'Slave with suffix => '.JSON($customer);
- Cache::get($key)
- Cache::del($key)
- Cache::set($key, $val)
- 任意地方均可调用
- 框架设置了 autoload 的目录是 vendor 与 library, 因此只要将类位于此目录下, 就能实现自动加载
- vendor 下的则用于 composer 包, 第一优先
- library 下的则是自己写的 class, 第二优先
- 例如控制器中要实例化 RabbitMQ, 文件名是 /library/RabbitMQ.php
public function rabbit(){
$rabbit = new RabbitMQ();
return 'A Rabbit is running happily now';
}
- 控制器中使用 $this->getParam($key) 来获取请求的参数,比如 $username = $this->getParam('username'), 默认会对数据进行过滤,若不过滤,将第二个参数设置为 FALSE: $username = $this->getParam('username', FALSE)
- getParam() 默认会进行 XSS 过滤, addslashes(), trim()
- 文件是 library/core/Security.php
- 将要增加的方法写入 library/Function.php 即可随处调用
- 控制器中想每2秒执行当前类的 tick() 方法, 并且传递 xyx 作为参数, 则这样做
Timer::add(2000, [$this, 'tick'], 'xyz');
tick 方法则这样接收, 然后使用Timer::clear($timerID);来清除定时器
public function tick(int $timerID, $args){
$this->response('Time in tick '.date("Y-m-d H:i:s\n"));
$this->response('Args in tick '.JSON($args));
// Clear timer
Timer::clear($timerID);
return;
}
- 控制器中想5秒后执行当前类的 after() 方法, 则这样做。
Timer::after(5000, [$this, 'after']);
after 方法
// 注: after定时器不接收任何参数
public function after(){
return 'Execute '.__METHOD__.' in after timer';
}
- 以数组形式指定 callback 与 param, 调用 Task::add($args)
- 以下例子投递一个任务, 由 Importer 的 Run() 处理, 参数是 ['Lakers', 'Swoole', 'Westlife'];
public function task(){
$args = [];
$args['callback'] = ['Importer', 'Run'];
$args['param'] = ['Lakers', 'Swoole', 'Westlife'];
$taskID = Task::add($args);
return 'Task has been set, id is => '.$taskID;
}
class Importer {
public static function run(...$param){
Logger::log('Param in '.__METHOD__.' => '.JSON($param));
}
}
2:任务完成时,task进程会将结果发送给onFinish函数,再由onFinish函数返回给worker
public static function onFinish(swoole_server $server, int $taskID, string $data){
Logger::log('taskID => '.$taskID.' => finish');
}
- 基于 Pipeline 模式实现
- 实现该中间件的类必须实现 Middleware 中的 handle() 方法。
- 若要中止流程, throw 一个 Error 即可, 最后别忘了调用 $next();
- TCP, UDP, HTTP, WEBSOCKET 均可使用哦
- 请参考 Auth.php 与 Importer.php
<?php
interface Middleware {
public static function handle(Closure $next);
}
<?php
class Auth implements Middleware {
public static function handle(Closure $next){
if(!Request::has('token') || empty(Request::get('token'))){
throw new Error('Access denied !', 401);
return;
}else{
Request::set('token', 'ABCDEFG');
$next();
}
}
}
<?php
class Importer {
public static function handle(Closure $next){
if(!Request::has('file') || empty(Request::get('file'))){
throw new Error('Empty file !', 402);
return;
}else{
$next();
}
}
}
- 调用方法: 控制器的构造函数中调用 $this->middleware([$pipe1, $pipe2, $pipeN]);
// Auth中间件
function __construct(){
$this->middleware(['Auth']);
return $this->getParam('token');
}
// Importer中间件
function __construct(){
$this->middleware(['Importer']);
return $this->getParam('token');
}
// 多个中间件一起使用, 先执行 Auth::handle(), 再执行 Importer::handle();
function __construct(){
$this->middleware(['Auth', 'Importer']);
return $this->getParam('token');
}
- 木有问题, 只要安装的 Swoole 版本 4+ 以上,任意地方调用 go(function()) 均可
public static function Remove(){
Logger::log(__METHOD__);
while(TRUE){
$queue = Config::get('elasticsearch', 'queue_remove');
$data = Cache::rpop($queue);
if($data){
go([__CLASS__, 'doRemove'], $data);
}
sleep(1);
}
}
private static function doRemove($data){
Logger::log(__METHOD__);
Logger::log('Removing '.$data);
// Do business here
}
- 将插件放在 plugin 目录下, 文件名与类名保持一致。如类名为 Permission, 则文件名为 Permission.php
- 插件均需要实现 interface Plugin 定义的 routerStartup() 与 routerShutdown()
- 插件在系统启动时自动加载,并存在 Registry
- 按如下示例取出插件并使用
public function plugin(){
$token = $this->getParam('token', FALSE);
// Permission 插件
$retval = Registry::get('Permission')->checkPermission($token);
if(!$retval){
$this->response->end('Bad token !');
return;
}
// I18N 插件
$i18n = Registry::get('I18N');
$username_text = $i18n->translate('username');
$password_text = $i18n->translate('password');
$this->response->write('English username_text => '.$username_text.'<br />');
$this->response->write('English password_text => '.$password_text.'<br />');
$username_text = $i18n->translate('username', 2);
$password_text = $i18n->translate('password', 2);
$this->response->write('Chinese username_text => '.$username_text.'<br />');
$this->response->write('Chinese password_text => '.$password_text.'<br />');
}
- 很多业务需要长驻内存不断的跑[while(true)],worker 和 task 就不适合了,更好的方式是创建自己的进程来处理
- 配置文件的 process 中配置自己想要创建的进程, 格式为
'process' => [
$进程名 => [
'num' => 进程的数量, 数字
'mysql' => 是否连接 MySQL, 布尔,
'redis' => 是否连接 Redis, 布尔,
'param' => 要带入的参数, 索引数组,
'callback' => 回调函数, 也就是进程创建后要执行的方法
],
],
'process' => [
'Tiny_Swoole_importer'=> [
'num' => 1,
'mysql' => true,
'redis' => true,
'callback' => ['Importer', 'run'],
],
],
class Importer {
public static function run(...$param){
Logger::log('Importer process is ready !');
while (TRUE) {
$key = 'Key_current_time';
Cache::set($key, date('Y-m-d H:i:s'));
$val = Cache::get($key);
Logger::log('Time => '.$val);
sleep(3);
}
}
}
- 定时调用 shell/process.sh 对进程作心跳检测
- 要单独停止一个 process 则需要编写单独的 shell 脚本, 参考 importer.sh