├── .gitignore ├── LICENSE ├── README.md ├── bin ├── function.php └── swoole-task.php ├── composer.json └── src ├── Base ├── App.php ├── Ctrl.php ├── Dao.php └── Helper.php └── HttpServer.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | swoole-task 2 | ----------- 3 | swoole-task是基于PHP swoole扩展开发的一个异步多进程任务处理框架,服务端和客户端通过http协议进行交互。 4 | 5 | 它适用于任务需要花费较长时间处理,而客户端不必关注任务执行结果的场景.比如数据清洗统计类的工作,报表生成类任务。 6 | 7 | ### 环境要求 8 | 9 | - PHP 5.4 以上版本,强烈推荐适用5.6,性能更好 10 | - Swoole 1.8.7 以上版本 11 | - PDO PDO_Mysql 12 | 13 | ### 安装方法 14 | 15 | 鉴于目前composer已经成为PHP包管理事实的标准,swoole-task直接支持通过composer 命令安装使用。 16 | 17 | 关于composer的使用请参考 composer 官网学习。 18 | 19 | 假设当前我们项目的名称为play,需要在项目中集成swoole-task,服务,执行如下命令。 20 | 21 | 22 | ```sh 23 | #由于某种不知名原因,访问 packagist太慢,可使用国内镜像 24 | #composer config repo.packagist composer https://packagist.phpcomposer.com 25 | composer require "ping/swoole-task:dev-master" 26 | 27 | ``` 28 | 29 | > 如果项目根目录中不存在composer.json文件,需要执行composer init 命令。 30 | 31 | ### 使用方法 32 | 33 | 安装完成之后,在项目的vendor/bin 目录下会有swoole-tssk.php这个脚本,此脚本就是swoole-task服务的管理脚本。 34 | 35 | 以默认配置启动swoole-task服务 36 | 37 | ```sh 38 | php swoole-task.php start 39 | ``` 40 | > 第一次启动会执行初始化动作。 41 | 42 | 关于swoole-task.php脚本的详细说明 43 | 44 | - 参数 45 | 46 | ``` 47 | --app 应用目录名称,默认 sw-app,可根据自己需求设置 48 | --nodaemon 以非守护进程模式启动,默认读取配置文件http_server.php 中的 daemonize的值 49 | --help 显示帮忙 50 | --host 指定绑定的ip 默认读取配置文件 http_server.php中的host取值 51 | --port 指定绑定的端口,默认读取配置文件中的 http_server.php中的port的取值 52 | ``` 53 | 54 | - 命令 55 | 56 | ``` 57 | start //启动服务 58 | stop //停止服务 59 | restart //重启,配置文件常驻内存,修改后需要重启 60 | status //查看状态 61 | list //swoole-task服务列表 62 | ``` 63 | 64 | 用例说明: 65 | 66 | ``` 67 | //启动swoole-task服务,以项目根目录下的sw目录为业务目录 68 | php swoole-task.php --app sw start 69 | //停止swoole-task 服务 70 | php swoole-task.php -app sw stop 71 | //启动swoole-task 服务,使用默认app目录(sw-app),使用host和port 覆盖默认配置 72 | php swoole-task.php --host 127.0.0.1 --port 9520 start 73 | //显示服务状态 74 | php swoole-task.php --app sw status 75 | ``` 76 | 77 | ### 配置说明 78 | 第一次使用 php swoole-task.php start 启动服务的时候,默认的业务逻辑编写目录是sw-app,和vendor在同级目录。 79 | 80 | 自动生成如下目录结构 81 | 82 | ``` 83 | - sw-app 和vendor是同级目录 84 | - Conf 配置文件目录,不可更改 85 | - http_server.php 主要是针对HttpServer的配置 86 | - app.php 主要是对实际业务的配置 87 | - dev 开发环境配置文件目录(列如 db.php redis.php等等,文件名为key值) 88 | - test 测试环境配置文件目录 89 | - prod 生产环境配置文件目录 90 | - Runtime 运行时输出目录 91 | - Ctrl controller目录,可修改app.php配置文件,使用别的名称来替代Ctrl 92 | - Dao 数据访问业务Dao,可修改app.php配置文件,使用别的名称替代Dao 93 | - Helper 帮助类,修改配置文件可以使用别的名称 94 | ``` 95 | 96 | 默认配置文件说明 97 | 98 | app.php(没强迫症建议直接使用默认配置不必修改) 99 | 100 | ``` 101 | ns_pre 命名空间前缀,默认值 SwTask 102 | ns_ctrl ctrl类的命名空间,注意命名空间和ctrl类是一致的,遵循psr4规则, 103 | ns_dao dao类的命名空间 104 | ns_helper heper类的命名空间 105 | ``` 106 | 107 | http_server.php(修改配置好之后需要重启加载配置文件) 108 | 109 | ``` 110 | tz 时区设置,默认是上海时区,数据处理的时候非常重要 111 | host 监听的主机ip地址,默认配置0.0.0.0 112 | port 监听的主机端口,默认值 9523 113 | app_env swoole-task 业务的环境,支持dev,test,prod三个值 114 | ps_name 进程名称前缀 默认 swTask 115 | daemonize 是否守护进程模式 0 非守护进程 1 守护进程,默认0 116 | worker_num worker 进程数量,推荐数量和cpu核数保持一致 117 | task_woker_num 任务进程数量,根据机器配合和实际需求设置 118 | task_max_request 每个任务进程最多可处理请求数,超过重启,保证内存不泄露的机制 119 | ``` 120 | 121 | ### swoole-task 服务启动流程说明 122 | 123 | 初次运行swoole-task,执行php vendor/bin/swoole-task.php 命令,不添加任何参数,会执行如下初始化工作 124 | 125 | ``` 126 | 1 根据默认配置,创建Ctrl, Dao,Helper, 127 | Conf, Conf/test, Conf/dev, Conf/prod, Runtime, Runtime/log 目录, 128 | 2 创建TplCtrl,TplDao 文件,写入到Ctrl/Dao 目录下,作为模板文件 129 | 3 初始化配置文件app.php , http_server.php,写入到Conf目录下 130 | ``` 131 | 运行 php swoole-task.php 命令,不添加任何参数,会在项目根目录下检查是否存在sw-app 目录。 132 | 133 | 如果不存在,执行初始化工作; 134 | 135 | 如果存在,加载sw-app/Conf目录下的相关配置,启动服务。 136 | 137 | ### 路由说明 138 | 139 | 客户端和服务端http协议交互,形式如下 140 | 141 | curl "127.0.0.1:9523/ctrlName/actionName" 142 | 143 | curl "127.0.0.1:9523?op=ctrlName.actionName" 144 | 145 | 初始化后,默认生成的TplCtrl.php 文件,其中包含了一个 helloAction的方法 146 | 147 | 访问这个action的命令为 148 | 149 | curl "127.0.0.1:9523/tpl/hello" 150 | 151 | 或者 152 | 153 | curl "127.0.0.1:9523?op=tpl.hello" 154 | 155 | 156 | -------------------------------------------------------------------------------- /bin/function.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'ns_pre' => 'SwTask',//swoole-task 应用默认 namespace 前缀 18 | 'ns_ctrl' => 'Ctrl',//Ctrl 代码目录,命名空间 SwTask\Ctrl\FooCtrl 19 | 'ns_dao' => 'Dao',//Dao 代码目录,命名空间 SwTask\Dao\FooDao 20 | 'ns_helper' => 'Helper',//Helper 代码目录,命名空间 SwTask\Helper\FooHelper 21 | ], 22 | 'http_server' => [ 23 | 'tz' => 'Asia/Shanghai',//时区设定,数据统计时区非常重要 24 | 'host' => '127.0.0.1', //默认监听ip 25 | 'port' => '9523', //默认监听端口 26 | 'app_env' => 'dev', //运行环境 dev|test|prod, 基于运行环境加载不同配置 27 | 'ps_name' => 'swTask', //默认swoole 进程名称 28 | 'daemonize' => 0, //是否守护进程 1=>守护进程| 0 => 非守护进程 29 | 'worker_num' => 2, //worker进程 cpu核数 1-4倍,一般选择和cpu核数一致 30 | 'task_worker_num' => 2, //task进程,根据实际情况配置 31 | 'task_max_request' => 10000, //当task进程处理请求超过此值则关闭task进程,保障进程无内存泄露 32 | 'open_tcp_nodelay' => 1, //关闭Nagle算法,提高HTTP服务器响应速度 33 | ], 34 | ]; 35 | 36 | /** 37 | * @var string ctrl 默认模板 38 | */ 39 | $ctrlTpl = <<testDao = \$this->getDao('TestDao'); 57 | } 58 | public function helloAction() 59 | { 60 | echo 'hello world' . PHP_EOL; 61 | var_dump(\$this->params); 62 | } 63 | } 64 | PHP; 65 | /** 66 | * @var string dao 默认模板 67 | */ 68 | $daoTpl = << $a[0], 145 | 'ip' => $ip, 146 | 'port' => $p, 147 | ]; 148 | } 149 | } 150 | 151 | return $ret; 152 | } 153 | 154 | function swTaskStart($conf) 155 | { 156 | echo "正在启动 swoole-task 服务" . PHP_EOL; 157 | if (!is_writable(dirname($conf['pid_file']))) { 158 | exit("swoole-task-pid文件需要目录的写入权限:" . dirname($conf['pid_file']) . PHP_EOL); 159 | } 160 | 161 | if (file_exists($conf['pid_file'])) { 162 | $pid = explode("\n", file_get_contents($conf['pid_file'])); 163 | $cmd = "ps ax | awk '{ print $1 }' | grep -e \"^{$pid[0]}$\""; 164 | exec($cmd, $out); 165 | if (!empty($out)) { 166 | exit("swoole-task pid文件 " . $conf['pid_file'] . " 存在,swoole-task 服务器已经启动,进程pid为:{$pid[0]}" . PHP_EOL); 167 | } else { 168 | echo "警告:swoole-task pid文件 " . $conf['pid_file'] . " 存在,可能swoole-task服务上次异常退出(非守护模式ctrl+c终止造成是最大可能)" . PHP_EOL; 169 | unlink($conf['pid_file']); 170 | } 171 | } 172 | $bind = swTaskPort($conf['port']); 173 | if ($bind) { 174 | foreach ($bind as $k => $v) { 175 | if ($v['ip'] == '*' || $v['ip'] == $conf['host']) { 176 | exit("端口已经被占用 {$conf['host']}:{$conf['port']}, 占用端口进程ID {$k}" . PHP_EOL); 177 | } 178 | } 179 | } 180 | date_default_timezone_set($conf['tz']); 181 | $server = new Ping\SwooleTask\HttpServer($conf); 182 | $server->run(); 183 | //确保服务器启动后swoole-task-pid文件必须生成 184 | /*if (!empty(portBind($port)) && !file_exists(SWOOLE_TASK_PID_PATH)) { 185 | exit("swoole-task pid文件生成失败( " . SWOOLE_TASK_PID_PATH . ") ,请手动关闭当前启动的swoole-task服务检查原因" . PHP_EOL); 186 | }*/ 187 | exit("启动 swoole-task 服务成功" . PHP_EOL); 188 | } 189 | 190 | function swTaskStop($conf, $isRestart = false) 191 | { 192 | echo "正在停止 swoole-task 服务" . PHP_EOL; 193 | if (!file_exists($conf['pid_file'])) { 194 | exit('swoole-task-pid文件:' . $conf['pid_file'] . '不存在' . PHP_EOL); 195 | } 196 | $pid = explode("\n", file_get_contents($conf['pid_file'])); 197 | $bind = swTaskPort($conf['port']); 198 | if (empty($bind) || !isset($bind[$pid[0]])) { 199 | exit("指定端口占用进程不存在 port:{$conf['port']}, pid:{$pid[0]}" . PHP_EOL); 200 | } 201 | $cmd = "kill {$pid[0]}"; 202 | exec($cmd); 203 | do { 204 | $out = []; 205 | $c = "ps ax | awk '{ print $1 }' | grep -e \"^{$pid[0]}$\""; 206 | exec($c, $out); 207 | if (empty($out)) { 208 | break; 209 | } 210 | } while (true); 211 | //确保停止服务后swoole-task-pid文件被删除 212 | if (file_exists($conf['pid_file'])) { 213 | unlink($conf['pid_file']); 214 | } 215 | $msg = "执行命令 {$cmd} 成功,端口 {$conf['host']}:{$conf['port']} 进程结束" . PHP_EOL; 216 | if ($isRestart) { 217 | echo $msg; 218 | } else { 219 | exit($msg); 220 | } 221 | } 222 | 223 | function swTaskStatus($conf) 224 | { 225 | echo "swoole-task {$conf['host']}:{$conf['port']} 运行状态" . PHP_EOL; 226 | $cmd = "curl -s '{$conf['host']}:{$conf['port']}?cmd=status'"; 227 | exec($cmd, $out); 228 | if (empty($out)) { 229 | exit("{$conf['host']}:{$conf['port']} swoole-task服务不存在或者已经停止" . PHP_EOL); 230 | } 231 | foreach ($out as $v) { 232 | $a = json_decode($v); 233 | foreach ($a as $k1 => $v1) { 234 | echo "$k1:\t$v1" . PHP_EOL; 235 | } 236 | } 237 | exit(); 238 | } 239 | 240 | //WARN macOS 下因为不支持进程修改名称,此方法使用有问题 241 | function swTaskList($conf) 242 | { 243 | echo "本机运行的swoole-task服务进程" . PHP_EOL; 244 | $cmd = "ps aux|grep " . $conf['ps_name'] . "|grep -v grep|awk '{print $1, $2, $6, $8, $9, $11}'"; 245 | exec($cmd, $out); 246 | if (empty($out)) { 247 | exit("没有发现正在运行的swoole-task服务" . PHP_EOL); 248 | } 249 | echo "USER PID RSS(kb) STAT START COMMAND" . PHP_EOL; 250 | foreach ($out as $v) { 251 | echo $v . PHP_EOL; 252 | } 253 | exit(); 254 | } 255 | -------------------------------------------------------------------------------- /bin/swoole-task.php: -------------------------------------------------------------------------------- 1 | #!/bin/env php 2 | $v) { 92 | if ($k == "app") { 93 | if (empty($v)) { 94 | exit("参数 --app 必须指定值,例如swoole-task,此参数用于指定初始化时swoole-task处理业务的根目录\n"); 95 | } 96 | } 97 | if ($k == 'host') { 98 | if (empty($v)) { 99 | exit("参数 --host 必须指定值\n"); 100 | } 101 | } 102 | if ($k == 'port') { 103 | if (empty($v)) { 104 | exit("参数--port 必须指定值\n"); 105 | } 106 | } 107 | } 108 | 109 | //命令检查 110 | $cmd = $argv[$argc - 1]; 111 | if (!in_array($cmd, $cmds)) { 112 | exit("输入命令有误 : {$cmd}, 请查看帮助文档\n"); 113 | } 114 | 115 | $app = 'sw-app'; 116 | if (!empty($opts['app'])) { 117 | $app = $opts['app']; 118 | } 119 | 120 | $appDir = SW_APP_ROOT . DS . $app; 121 | if (!file_exists($appDir)) { 122 | swTaskInit($appDir); 123 | } 124 | //读取swoole-httpSerer配置 125 | $httpConf = include $appDir . DS . SW_APP_CONF . DS . 'http_server.php'; 126 | 127 | /** 128 | * swoole_task业务实际目录 129 | */ 130 | $httpConf['app_dir'] = $appDir; 131 | /** 132 | * @var string 监听主机ip, 0.0.0.0 表示监听所有本机ip, 如果命令行提供 ip 则覆盖配置项 133 | */ 134 | if (!empty($opts['host'])) { 135 | if (!filter_var($host, FILTER_VALIDATE_IP)) { 136 | exit("输入host有误:{$host}"); 137 | } 138 | $httpConf['host'] = $opts['host']; 139 | } 140 | /** 141 | * @var int 监听端口 142 | */ 143 | if (!empty($opts['port'])) { 144 | $port = (int)$opts['port']; 145 | if ($port <= 0) { 146 | exit("输入port有误:{$port}"); 147 | } 148 | $httpConf['port'] = $port; 149 | } 150 | //确定port之后则进程文件确定,可在conf中加入 151 | $httpConf['pid_file'] = $httpConf['app_dir'] . DS . SW_APP_RUNTIME . DS . 'sw-' . $httpConf['port'] . '.pid'; 152 | /** 153 | * @var bool swoole-httpServer运行模式,参数nodaemon 以非守护进程模式运行,否则以配置文件设置值为默认值 154 | */ 155 | if (isset($opts['nodaemon'])) { 156 | $httpConf['daemonize'] = 0; 157 | } 158 | 159 | //启动 160 | if ($cmd == 'start') { 161 | swTaskStart($httpConf); 162 | } 163 | //停止 164 | if ($cmd == 'stop') { 165 | swTaskStop($httpConf); 166 | } 167 | //重启 168 | if ($cmd == 'restart') { 169 | echo "重启swoole-task服务" . PHP_EOL; 170 | swTaskStop($httpConf, true); 171 | swTaskStart($httpConf); 172 | } 173 | //状态 174 | if ($cmd == 'status') { 175 | swTaskStatus($httpConf); 176 | } 177 | //列表 WARN macOS 下因为进程名称修改问题,此方法使用有问题 178 | if ($cmd == 'list') { 179 | swTaskList($httpConf); 180 | } 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ping/swoole-task", 3 | "description": "基于swoole扩展的异步并发任务处理服务框架", 4 | "type": "library", 5 | "version": "master", 6 | "require": { 7 | "php": ">=5.4", 8 | "ext-swoole": ">=1.8.7", 9 | "ext-PDO": "*", 10 | "ext-pdo_mysql": "*" 11 | }, 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "luxixing", 16 | "email": "xixing.lu@outlook.com" 17 | } 18 | ], 19 | "minimum-stability": "stable", 20 | "bin": [ 21 | "bin/swoole-task.php" 22 | ], 23 | "autoload": { 24 | "psr-4": { 25 | "Ping/SwooleTask\\": "src" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Base/App.php: -------------------------------------------------------------------------------- 1 | [], 79 | //公共默认配置 Conf/app.php 80 | 'app' => [], 81 | ]; 82 | 83 | //单例需要 84 | private function __construct() 85 | { 86 | } 87 | 88 | //单例需要 89 | private function __clone() 90 | { 91 | } 92 | 93 | /** 94 | * TODO 继续完善优化规则 95 | * 简单路由规则实现 96 | * 97 | * @param $request 98 | * @param $id 99 | * 100 | * @return mixed 错误返回代码 成功返回op 101 | */ 102 | private function route($request, $id) 103 | { 104 | $this->_request[$id] = []; 105 | if (!empty($request->get)) { 106 | $this->_request[$id] = array_merge($this->_request[$id], $request->get); 107 | } 108 | if (!empty($request->post)) { 109 | $this->_request[$id] = array_merge($this->_request[$id], $request->post); 110 | } 111 | $route = ['index', 'index']; 112 | if (!empty($request->server['path_info'])) { 113 | $route = explode('/', trim($request->server['path_info'], '/')); 114 | } 115 | if (!empty($this->_request[$id]['op'])) { 116 | //请求显式的指定路由:op=ctrl.action 117 | $route = explode('.', $this->_request[$id]['op']); 118 | } 119 | if (count($route) < 2) { 120 | return 1; 121 | } 122 | $this->_op[$id] = implode('.', $route); 123 | $ctrl = '\\' . self::$conf['app']['ns_pre'] . '\\' . self::$conf['app']['ns_ctrl'] . '\\' . ucfirst($route[0]) . 'Ctrl'; 124 | $action = lcfirst($route[1]) . 'Action'; 125 | if (!class_exists($ctrl) || !method_exists($ctrl, $action)) { 126 | return 2; 127 | } 128 | $this->_ctrl[$id] = $ctrl; 129 | $this->_action[$id] = $action; 130 | 131 | //设置请求调试模式 132 | $debug = false; 133 | if (!empty($this->_request[$id]['debug'])) { 134 | //请求中携带 debug 标识,优先级最高 135 | $debug = $this->_request[$id]['debug']; 136 | } 137 | $debug = ($debug === true || $debug === 'yes' || $debug === 'true' || $debug === 1 || $debug === '1') ? true : false; 138 | $this->_debug[$id] = $debug; 139 | 140 | return $this->_op[$id]; 141 | } 142 | 143 | /** 144 | * app conf 获取 首先加载内置配置 145 | * 146 | * @param string $key 147 | * $param mixed $default 148 | * 149 | * @return array 150 | */ 151 | public static function getConfig($key = '', $default = '') 152 | { 153 | if ($key === '') { 154 | return self::$conf; 155 | } 156 | $value = []; 157 | $keyList = explode('.', $key); 158 | $firstKey = array_shift($keyList); 159 | if (isset(self::$conf[$firstKey])) { 160 | $value = self::$conf[$firstKey]; 161 | } else { 162 | if (!isset(self::$conf['conf'][$firstKey])) { 163 | return $value; 164 | } 165 | $value = self::$conf['conf'][$firstKey]; 166 | } 167 | //递归深度最大5层 168 | $i = 0; 169 | do { 170 | if ($i > 5) { 171 | break; 172 | } 173 | $k = array_shift($keyList); 174 | if (!isset($value[$k])) { 175 | $value = empty($default) ? [] : $default; 176 | 177 | return $value; 178 | } 179 | $value = $value[$k]; 180 | $i++; 181 | } while ($keyList); 182 | 183 | return $value; 184 | } 185 | 186 | /** 187 | * 获取一个app的实例 188 | * 189 | * @param $server 190 | * 191 | * @return App|null 192 | */ 193 | public static function getApp($server = null) 194 | { 195 | if (self::$_app) { 196 | return self::$_app; 197 | } 198 | //swoole-httpServer 199 | self::$server = $server; 200 | //swoole-app运行环境 dev|test|prod 201 | self::$env = self::$server->setting['app_env']; 202 | 203 | /** 204 | * 配置加载 205 | */ 206 | //默认app配置加载 207 | $configDir = self::$server->setting['app_dir'] . DS . SW_APP_CONF; 208 | self::$conf['app'] = include $configDir . DS . 'app.php'; 209 | self::$conf['app']['conf'] = SW_APP_CONF; 210 | self::$conf['app']['runtime'] = SW_APP_RUNTIME; 211 | //根据环境加载不同的配置 212 | $confFiles = []; 213 | if (file_exists($configDir . DS . self::$env)) { 214 | $confFiles = Helper::getFiles($configDir); 215 | } 216 | foreach ($confFiles as $inc) { 217 | $file = pathinfo($inc); 218 | self::$conf['conf'][$file['filename']] = include $inc; 219 | } 220 | 221 | //vendor目录加载,支持composer 222 | $vendorFile = self::$server->setting['app_dir'] . DS . 'vendor' . DS . 'autoload.php'; 223 | if (file_exists($vendorFile)) { 224 | include $vendorFile; 225 | } 226 | 227 | //自动加载机制实现,遵循psr4 228 | spl_autoload_register(function ($className) { 229 | $path = array_filter(explode('\\', $className)); 230 | $className = array_pop($path); 231 | $realPath = str_replace(self::$conf['app']['ns_pre'], '', implode(DS, $path)); 232 | include self::$server->setting['app_dir'] . $realPath . DS . $className . '.php'; 233 | 234 | return true; 235 | }); 236 | 237 | self::$_app = new self(); 238 | 239 | return self::$_app; 240 | } 241 | 242 | /** 243 | * 获取当前请求调试模式的值 244 | * 245 | * @param $id 246 | * 247 | * @return bool 248 | */ 249 | public function getDebug($id) 250 | { 251 | return $this->_debug[$id]; 252 | } 253 | 254 | 255 | public function logger($msg, $type = null) 256 | { 257 | if (empty($msg)) { 258 | return false; 259 | } 260 | //参数处理 261 | $type = $type ? $type : 'debug'; 262 | if (!is_string($msg)) { 263 | $msg = var_export($msg, true); 264 | } 265 | $msg = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL; 266 | 267 | $maxSize = 2097152;//2M 268 | list($y, $m, $d) = explode('-', date('Y-m-d')); 269 | $dir = self::$server->setting['app_dir'] . DS . self::$conf['app']['runtime'] . DS . 'log' . DS . $y . $m; 270 | $file = "{$dir}/{$type}-{$d}.log"; 271 | if (!file_exists($dir)) { 272 | mkdir($dir, 0777); 273 | } 274 | if (file_exists($file) && filesize($file) >= $maxSize) { 275 | $a = pathinfo($file); 276 | $bak = $a['dirname'] . DIRECTORY_SEPARATOR . $a['filename'] . '-bak.' . $a['extension']; 277 | if (!rename($file, $bak)) { 278 | echo "rename file:{$file} to {$bak} failed"; 279 | } 280 | } 281 | error_log($msg, 3, $file); 282 | } 283 | 284 | public function debug($msg) 285 | { 286 | $this->logger($msg, 'debug'); 287 | } 288 | 289 | public function error($msg) 290 | { 291 | $this->logger($msg, 'error'); 292 | } 293 | 294 | public function warn($msg) 295 | { 296 | $this->logger($msg, 'warn'); 297 | } 298 | 299 | public function info($msg) 300 | { 301 | $this->logger($msg, 'info'); 302 | } 303 | 304 | public function sql($msg) 305 | { 306 | $this->logger($msg, 'sql'); 307 | } 308 | 309 | public function op($msg) 310 | { 311 | $this->logger($msg, 'op'); 312 | } 313 | 314 | /** 315 | * 执行一个请求 316 | * 317 | * @param $request 318 | * @param $taskId 319 | * @param $fromId 320 | * 321 | * @return mixed 322 | */ 323 | public function run($request, $taskId, $fromId) 324 | { 325 | //id由 workid#taskId组成,能唯一标识一个请求的来源 326 | $id = "{$fromId}#{$taskId}"; 327 | //请求运行开始时间 328 | $runStart = time(); 329 | //请求运行开始内存 330 | $mem = memory_get_usage(); 331 | 332 | //TODO before route 333 | $op = $this->route($request, $id); 334 | if (is_int($op)) { 335 | if ($op == 1) { 336 | $error = '缺少路由'; 337 | $this->error($error); 338 | 339 | return false; 340 | } 341 | if ($op == 2) { 342 | $error = "路由解析失败:{$this->_op[$id]}"; 343 | $this->error($error); 344 | 345 | return false; 346 | } 347 | } 348 | 349 | //TODO after route 350 | try { 351 | $ctrl = new $this->_ctrl[$id]($this->_request[$id], $this->_op[$id], $id); 352 | 353 | //before action:比如一些ctrl的默认初始化动作,加载dao等 354 | if (method_exists($ctrl, 'init')) { 355 | //执行action之前进行init 356 | $ctrl->init(); 357 | } 358 | $res = $ctrl->{$this->_action[$id]}(); 359 | //FIXME $res 返回如果不是数组会报错 360 | //after action 361 | if (method_exists($ctrl, 'done')) { 362 | //执行完action之后要做的事情 363 | $ctrl->done(); 364 | } 365 | 366 | //请求运行时间和内存记录 367 | $runSpend = time() - $runStart;//请求花费时间 368 | $info = "op:{$op}, spend: {$runSpend}s, memory:" . Helper::convertSize(memory_get_usage() - $mem) . ", peak memory:" . Helper::convertSize(memory_get_peak_usage()); 369 | $info .= ",date:{$ctrl->date}"; 370 | $this->op($info); 371 | 372 | 373 | return $res; 374 | } catch (\Exception $e) { 375 | $this->error($e->getMessage() . PHP_EOL . $e->getTraceAsString()); 376 | } finally { 377 | //WARN 请求结束后必须释放相关的数据,避免内存使用的无限增长 378 | unset($this->_op[$id], $this->_ctrl[$id], $this->_action[$id], $this->_request[$id], $this->_debug[$id]); 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/Base/Ctrl.php: -------------------------------------------------------------------------------- 1 | '', 33 | //错误代码 34 | 'errno' => self::ERRNO_SUCCESS, 35 | //错误信息 36 | 'error' => self::ERROR_SUCCESS, 37 | //任务结束设置的回调函数 38 | 'finish' => '', 39 | //请求参数 40 | 'params' => [], 41 | //返回结果 可提供给回调函数使用 42 | 'data' => [], 43 | ]; 44 | /** 45 | * 请求唯一标识符 fromId#taskId 46 | * 47 | * @var string 48 | */ 49 | public $id = ''; 50 | /** 51 | * 当前请求 52 | * 53 | * @var string 54 | */ 55 | public $op = ''; 56 | /** 57 | * 数据抓取日期 58 | * 59 | * @var date 60 | */ 61 | public $date = ''; 62 | 63 | /** 64 | * 请求参数 65 | * 66 | * @var array 67 | */ 68 | public $params = []; 69 | 70 | public function __construct($params, $op, $id) 71 | { 72 | $this->id = $id; 73 | $this->op = $this->ret['op'] = $op; 74 | $this->params = $params; 75 | $this->date = isset($params['dt']) ? date('Y-m-d', strtotime("-1 day {$params['dt']}")) : date('Y-m-d', 76 | strtotime('-1 day')); 77 | $this->ret['params'] = $this->params; 78 | } 79 | 80 | public function __destruct() 81 | { 82 | //TODO 资源释放 83 | } 84 | 85 | /** 86 | * 在ctrl 里面快速访问config 87 | * 88 | * @param $key 89 | * 90 | * @return mixed 91 | */ 92 | public function getConfig($key) 93 | { 94 | return App::getConfig($key); 95 | } 96 | 97 | /** 98 | * 获取dao 99 | * 100 | * @param $dao 101 | * 102 | * @return bool 103 | */ 104 | public function getDao($dao) 105 | { 106 | if (isset($this->_dao[$dao])) { 107 | return $this->_dao[$dao]; 108 | } 109 | $class = App::$conf['app']['ns_pre'] . '\\' . App::$conf['app']['ns_dao'] . '\\' . $dao; 110 | if (!class_exists($class)) { 111 | return false; 112 | } 113 | $obj = new $class($this->isDebug()); 114 | $this->_dao[$class] = $obj; 115 | 116 | return $obj; 117 | } 118 | 119 | public function getApp() 120 | { 121 | return App::getApp(); 122 | } 123 | 124 | /** 125 | * @return bool true|false true 表示当前请求为debug模式 126 | */ 127 | public function isDebug() 128 | { 129 | return $this->getApp()->getDebug($this->id); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Base/Dao.php: -------------------------------------------------------------------------------- 1 | debug = $debug; 47 | } 48 | 49 | /** 50 | * 非dao类调用进行数据库操作,比如helper类 51 | * 52 | * @param $name 53 | * 54 | * @return null|\PDO 55 | * @throws \Exception 56 | */ 57 | public static function getPdoX($name) 58 | { 59 | $pdo = null; 60 | if (!empty(self::$_pdo[$name])) { 61 | self::ping(self::$_pdo[$name], $name); 62 | $pdo = self::$_pdo[$name]; 63 | } else { 64 | $pdo = self::$_pdo[$name] = self::getPdoInstance($name); 65 | } 66 | 67 | return $pdo; 68 | } 69 | 70 | /** 71 | * 如果mysql go away,连接重启 72 | * 73 | * @param \PDO $pdo 74 | * @param $name 75 | * 76 | * @return bool 77 | * @throws \Exception 78 | */ 79 | private static function ping($pdo, $name) 80 | { 81 | //是否重新连接 0 => ping连接正常, 没有重连 1=>ping 连接超时, 重新连接 82 | $isReconnect = 0; 83 | if (!is_object($pdo)) { 84 | $isReconnect = 1; 85 | self::$_pdo[$name] = self::getPdoInstance($name); 86 | App::getApp()->sql("mysql ping:pdo instance [{$name}] is null, reconnect"); 87 | } else { 88 | try { 89 | //warn 此处如果mysql gone away会有一个警告,此处屏蔽继续重连 90 | @$pdo->query('SELECT 1'); 91 | } catch (\PDOException $e) { 92 | //WARN 非超时连接错误 93 | if ($e->getCode() != 'HY000' || !stristr($e->getMessage(), 'server has gone away')) { 94 | throw $e; 95 | } 96 | //手动重连 97 | self::$_pdo[$name] = self::getPdoInstance($name); 98 | $isReconnect = 1; 99 | } finally { 100 | if ($isReconnect) { 101 | APP::getApp()->sql("mysql ping: reconnect {$name}"); 102 | } 103 | } 104 | } 105 | 106 | return $isReconnect; 107 | } 108 | 109 | /** 110 | * 创建数据库连接实例 111 | * 112 | * @param $name 113 | * 114 | * @return \PDO 115 | * @throws \Exception 116 | */ 117 | private static function getPdoInstance($name) 118 | { 119 | $default = [ 120 | //连接成功默认执行的命令 121 | 'commands' => [ 122 | 'set time_zone="+8:00"', 123 | ], 124 | 'host' => '127.0.0.1', 125 | //unix_socket 126 | 'socket' => '', 127 | 'username' => 'root', 128 | 'password' => '', 129 | //默认字符编码 130 | 'charset' => 'utf8', 131 | 'port' => '3306', 132 | 'dbname' => 'mysql', 133 | 'options' => [ 134 | \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'", 135 | \PDO::ATTR_CASE => \PDO::CASE_LOWER, 136 | \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, // 默认以数组方式提取数据 137 | \PDO::ATTR_ORACLE_NULLS => \PDO::NULL_TO_STRING, // 所有 null 转换为 string 138 | ], 139 | ]; 140 | try { 141 | $conf = App::getApp()->getConfig("conf.db.{$name}"); 142 | if (empty($conf)) { 143 | throw new \Exception("pdo config: conf.db.{$name} not found"); 144 | } 145 | foreach ($default as $k => $v) { 146 | $default[$k] = isset($conf[$k]) ? $conf[$k] : $v; 147 | } 148 | //PDO错误处理模式强制设定为exception, 连接强制设定为持久连接 149 | $default['options'][\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION; 150 | $default['options'][\PDO::ATTR_PERSISTENT] = false; 151 | $default['options'][\PDO::ATTR_TIMEOUT] = 200; 152 | 153 | $dsn = "mysql:dbname={$default['dbname']}"; 154 | if ($default['host']) { 155 | $dsn .= ";host={$default['host']};port={$default['port']}"; 156 | } else { 157 | $dsn .= ";unix_socket={$default['socket']}"; 158 | } 159 | //create pdo connection 160 | $pdo = new \PDO( 161 | $dsn, 162 | $default['username'], 163 | $default['password'], 164 | $default['options'] 165 | ); 166 | if (!empty($default['commands'])) { 167 | $commands = is_array($default['commands']) ? $default['commands'] : [$default['commands']]; 168 | foreach ($commands as $v) { 169 | $pdo->exec($v); 170 | } 171 | } 172 | 173 | return $pdo; 174 | } catch (\Exception $e) { 175 | App::getApp()->error($e->getMessage()); 176 | throw $e; 177 | } 178 | } 179 | 180 | /** 181 | * 获取当前请求执行的所有sql 182 | * 183 | * @return array 184 | */ 185 | public static function logs() 186 | { 187 | return self::$_log; 188 | } 189 | 190 | /** 191 | * 设定数据库连接,可执行链式操作 192 | * 193 | * @param $name 194 | * 195 | * @return $this 196 | * @throws \Exception 197 | */ 198 | public function getPdo($name) 199 | { 200 | $this->pdoName = $name; 201 | if (!empty(self::$_pdo[$name])) { 202 | $this->ping(self::$_pdo[$name], $name); 203 | $this->pdo = self::$_pdo[$name]; 204 | } else { 205 | $this->pdo = self::$_pdo[$name] = $this->getPdoInstance($name); 206 | } 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * 执行一条sql 213 | * 214 | * @param $sql 215 | * 216 | * @return int|string 217 | */ 218 | final public function query($sql) 219 | { 220 | $lastId = 0; 221 | try { 222 | $this->debug($sql); 223 | $sth = $this->pdo->prepare($sql); 224 | if ($sth) { 225 | $sth->execute(); 226 | $lastId = $this->pdo->lastInsertId(); 227 | } 228 | 229 | } catch (\PDOException $e) { 230 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 231 | } 232 | 233 | return $lastId; 234 | } 235 | 236 | private function debug($sql, $data = []) 237 | { 238 | //WARN sql记录最多保持100条,防止内存占用过多 239 | $max = 100; 240 | $query = $this->buildQuery($sql, $data); 241 | if (count(self::$_log) > $max) { 242 | array_shift(self::$_log); 243 | } 244 | self::$_log[] = $query; 245 | if ($this->debug) { 246 | $this->getApp()->sql($query); 247 | } 248 | } 249 | 250 | /** 251 | * prepare语句真实执行的sql 252 | * 253 | * @param $sql 254 | * @param array $data 255 | * 256 | * @return mixed 257 | */ 258 | private function buildQuery($sql, $data = []) 259 | { 260 | $placeholder = []; 261 | $params = []; 262 | if (empty($data) || !is_array($data)) { 263 | return $sql; 264 | } 265 | foreach ($data as $k => $v) { 266 | $placeholder[] = is_numeric($k) ? '/[?]/' : ($k[0] === ':' ? "/{$k}/" : "/:{$k}/"); 267 | $params[] = is_numeric($v) ? (int)$v : "'{$v}'"; 268 | } 269 | 270 | return preg_replace($placeholder, $params, $sql, 1, $count); 271 | } 272 | 273 | /** 274 | * @return App|null 275 | */ 276 | public function getApp() 277 | { 278 | return App::getApp(); 279 | } 280 | 281 | /** 282 | * 最近一次sql查询 283 | * 284 | * @return mixed 285 | */ 286 | public function lastQuery() 287 | { 288 | return end(self::$_log); 289 | } 290 | 291 | /** 292 | * 执行一个sql,获取一条结果 293 | * 294 | * @param string $sql 295 | * @param array $data 296 | * 297 | * @return array 298 | */ 299 | final public function getOne($sql, $data = []) 300 | { 301 | return $this->baseGet($sql, $data, 'fetch'); 302 | } 303 | 304 | /** 305 | * @param string $sql 要执行的sql 306 | * @param array $data 绑定参数 307 | * @param string $method [fetch|fetchAll|fetchColumn|fetchObject] 308 | * @param mixed $pdo \PDO|null 309 | * 310 | * @return array 311 | * @throws \Exception 312 | */ 313 | private function baseGet($sql, $data, $method, $pdo = null) 314 | { 315 | $ret = []; 316 | try { 317 | $this->debug($sql, $data); 318 | $pdo = $pdo instanceof \PDO ? $pdo : $this->pdo; 319 | $sth = $pdo->prepare($sql); 320 | if ($sth) { 321 | $sth->execute($data); 322 | if ($rows = $sth->$method(\PDO::FETCH_ASSOC)) { 323 | $ret = $rows; 324 | } 325 | } 326 | 327 | } catch (\PDOException $e) { 328 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 329 | } 330 | 331 | return $ret; 332 | } 333 | 334 | /** 335 | * 执行一个sql,获取所有结果 336 | * 337 | * @param string $sql 338 | * @param array $data 339 | * 340 | * @return array 341 | */ 342 | final public function getAll($sql, $data = []) 343 | { 344 | return $this->baseGet($sql, $data, 'fetchAll'); 345 | } 346 | 347 | /** 348 | * 使用生成器进行数据的批量查询,低内存占用 349 | * 350 | * @param string $pdoName 数据库连接配置名称 351 | * @param string $sql 欲执行sql 352 | * @param array $data 绑定参数 353 | * @param int $limit 限制查询数量 354 | * 355 | * @return \Generator 356 | * @throws \Exception 357 | */ 358 | final public function getIterator($pdoName, $sql, $data = [], $limit = 500) 359 | { 360 | //pdo设置 361 | $pdo = null; 362 | if (isset(self::$_pdo[$pdoName])) { 363 | $this->ping(self::$_pdo[$pdoName], $pdoName); 364 | $pdo = self::$_pdo[$pdoName]; 365 | } else { 366 | $pdo = $this->getPdoInstance($pdoName); 367 | } 368 | //迭代查询 369 | $offset = 0; 370 | do { 371 | //ping 防止超时 372 | if ($this->ping($pdo, $pdoName)) { 373 | $pdo = self::$_pdo[$pdoName]; 374 | } 375 | $s = "{$sql} limit {$offset}, {$limit}"; 376 | $ret = $this->baseGet($s, $data, 'fetchAll', $pdo); 377 | if (empty($ret)) { 378 | break; 379 | } 380 | foreach ($ret as $v) { 381 | yield $v; 382 | } 383 | $offset += $limit; 384 | } while (true); 385 | } 386 | 387 | /** 388 | * 预初始化一个pdo的prepare语句,返回准备好的PDOStatement环境,用于批量执行相同查询 389 | * 390 | * @param string $pdoName 数据库连接名称(db.php db数组的key名称) 391 | * @param string $sql 欲查询的sql 392 | * @param string $fetchMode 数据获取模式 [fetch|fetchAll|fetchColumn|fetchObject] 对应statement的方法 393 | * 394 | * @return \Closure 395 | * @throws \Exception 396 | */ 397 | final public function getBatchSth($pdoName, $sql, $fetchMode = 'fetch') 398 | { 399 | $pdo = null; 400 | $sth = null; 401 | try { 402 | if (isset(self::$_pdo[$pdoName])) { 403 | //WARN 注意,顺序非常重要 404 | $this->ping(self::$_pdo[$pdoName], $pdoName); 405 | $pdo = self::$_pdo[$pdoName]; 406 | } else { 407 | $pdo = $this->getPdoInstance($pdoName); 408 | } 409 | $sth = $pdo->prepare($sql); 410 | } catch (\PDOException $e) { 411 | $this->getApp()->error($e->getMessage()); 412 | } 413 | 414 | $batch = function ($d) use ($sth, $fetchMode, $sql, $pdo, $pdoName) { 415 | // WARN 重连之后因为pdo对象变化,需要重新预处理sql 416 | if ($this->ping($pdo, $pdoName)) { 417 | $pdo = self::$_pdo[$pdoName]; 418 | $sth = $pdo->prepare($sql); 419 | } 420 | try { 421 | $this->debug($sql, $d); 422 | $sth->execute($d); 423 | $ret = $sth->$fetchMode(); 424 | 425 | return $ret; 426 | } catch (\PDOException $e) { 427 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 428 | } 429 | return null; 430 | }; 431 | 432 | return $batch; 433 | } 434 | 435 | /** 436 | * 向表里插入一条数据 437 | * 438 | * @param string $table 439 | * @param array $data 440 | * 441 | * @return int|string 442 | */ 443 | final public function add($table, $data, $ignore = false) 444 | { 445 | $lastId = 0; 446 | if (empty($data)) { 447 | return $lastId; 448 | } 449 | $keys = array_keys($data); 450 | $cols = implode(',', $keys); 451 | $params = ':' . implode(',:', $keys); 452 | $sql = ''; 453 | if ($ignore) { 454 | $sql = <<debug($sql, $data); 465 | 466 | $sth = $this->pdo->prepare($sql); 467 | $sth->execute($data); 468 | //WARN 当引擎为innodb 则不会自动提交,需要手动提交 469 | //$this->pdo->query('commit'); 470 | $lastId = $this->pdo->lastInsertId(); 471 | } catch (\PDOException $e) { 472 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 473 | } 474 | 475 | return $lastId; 476 | } 477 | 478 | /** 479 | * 生成一个prepare的Statement语句,用于批量插入 480 | * 481 | * @param string $pdoName pdo连接名称 482 | * @param string $table 表名称 483 | * @param array $bindCols 插入数据字段数组 484 | * 485 | * @return \Closure 486 | * @throws \Exception 487 | */ 488 | final public function addBatchSth($pdoName, $table, $bindCols) 489 | { 490 | $cols = implode(',', $bindCols); 491 | $params = ':' . implode(',:', $bindCols); 492 | $sql = <<ping(self::$_pdo[$pdoName], $pdoName); 500 | $pdo = self::$_pdo[$pdoName]; 501 | } else { 502 | $pdo = $this->getPdoInstance($pdoName); 503 | } 504 | $sth = $pdo->prepare($sql); 505 | } catch (\PDOException $e) { 506 | $this->getApp()->error($e->getMessage()); 507 | } 508 | //匿名函数用于sql prepare之后的批量操作 509 | $batch = function ($d) use ($sth, $bindCols, $pdo, $sql, $pdoName) { 510 | if ($this->ping($pdo, $pdoName)) { 511 | $pdo = self::$_pdo[$pdoName]; 512 | $sth = $pdo->prepare($sql); 513 | } 514 | try { 515 | $bindParam = []; 516 | foreach ($bindCols as $v) { 517 | $bindParam[":{$v}"] = $d[$v]; 518 | } 519 | $this->debug($sql, $bindParam); 520 | $sth->execute($bindParam); 521 | 522 | return $pdo->lastInsertId(); 523 | } catch (\PDOException $e) { 524 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 525 | } 526 | }; 527 | 528 | return $batch; 529 | } 530 | 531 | final public function addUpdate($table, $data) 532 | { 533 | //TODO 534 | } 535 | 536 | /** 537 | * 更新指定表数据 538 | * 539 | * @param string $table 540 | * @param array $data 绑定参数 541 | * @param string $where 542 | * 543 | * @return int 544 | */ 545 | final public function update($table, $data, $where) 546 | { 547 | $rowsCount = 0; 548 | $keys = array_keys($data); 549 | $cols = []; 550 | foreach ($keys as $v) { 551 | $cols[] = "{$v}=:$v"; 552 | } 553 | $cols = implode(', ', $cols); 554 | $sql = <<debug($sql, $data); 559 | $sth = $this->pdo->prepare($sql); 560 | $sth->execute($data); 561 | $rowsCount = $sth->rowCount(); 562 | } catch (\PDOException $e) { 563 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 564 | } 565 | 566 | return $rowsCount; 567 | } 568 | 569 | /** 570 | * 删除指定表数据,返回影响行数 571 | * 572 | * @param string $table 表名称 573 | * @param string $where where 条件 574 | * 575 | * @return int 576 | */ 577 | final public function del($table, $where) 578 | { 579 | $rowsCount = 0; 580 | $sql = <<debug($sql); 586 | //sth 返回值取决于pdo连接设置的errorMode,如果设置为exception,则抛出异常 587 | $sth = $this->pdo->prepare($sql); 588 | $sth->execute(); 589 | $rowsCount = $sth->rowCount(); 590 | } catch (\PDOException $e) { 591 | $this->getApp()->error($e->getMessage() . PHP_EOL . $this->lastQuery()); 592 | } 593 | 594 | return $rowsCount; 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/Base/Helper.php: -------------------------------------------------------------------------------- 1 | getFilename(); 36 | if ($file == '.' || $file == '..') { 37 | continue; 38 | } 39 | $files[] = $info->getPathname(); 40 | } 41 | 42 | return $files; 43 | } 44 | 45 | public static function curl($url, $data, $method = 'post', $port = 9510) 46 | { 47 | $url = $method == 'post' ? $url : $url . '?' . http_build_query($data); 48 | $curl = curl_init(); 49 | curl_setopt($curl, CURLOPT_URL, $url); 50 | curl_setopt($curl, CURLOPT_PORT, $port); 51 | curl_setopt($curl, CURLOPT_USERAGENT, LOG_AGENT); 52 | if ($method == 'post') { 53 | curl_setopt($curl, CURLOPT_POST, 1); 54 | curl_setopt($curl, CURLOPT_POSTFIELDS, $data); 55 | } 56 | 57 | $response = curl_exec($curl); 58 | if (curl_errno($curl)) { 59 | echo 'Curl error: ' . curl_error($curl); 60 | 61 | } 62 | curl_close($curl); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/HttpServer.php: -------------------------------------------------------------------------------- 1 | setting = $conf; 76 | } 77 | 78 | public function getSetting() 79 | { 80 | return $this->setting; 81 | } 82 | 83 | public function run() 84 | { 85 | $this->server = new \swoole_http_server($this->setting['host'], $this->setting['port']); 86 | 87 | $this->loadFramework(); 88 | $this->server->set($this->setting); 89 | 90 | //回调函数 91 | $call = [ 92 | 'start', 93 | 'workerStart', 94 | 'managerStart', 95 | 'request', 96 | 'task', 97 | 'finish', 98 | 'workerStop', 99 | 'shutdown', 100 | ]; 101 | //事件回调函数绑定 102 | foreach ($call as $v) { 103 | $m = 'on' . ucfirst($v); 104 | if (method_exists($this, $m)) { 105 | $this->server->on($v, [$this, $m]); 106 | } 107 | } 108 | //注入框架 常驻内存 109 | $this->app = BaseApp::getApp($this->server); 110 | $this->server->start(); 111 | } 112 | 113 | 114 | /** 115 | * swoole-server master start 116 | * 117 | * @param $server 118 | */ 119 | public function onStart($server) 120 | { 121 | echo 'Date:' . date('Y-m-d H:i:s') . "\t swoole_http_server master worker start\n"; 122 | $this->setProcessName($server->setting['ps_name'] . '-master'); 123 | //记录进程id,脚本实现自动重启 124 | $pid = "{$this->server->master_pid}\n{$this->server->manager_pid}"; 125 | file_put_contents($this->setting['pid_file'], $pid); 126 | } 127 | 128 | /** 129 | * manager worker start 130 | * 131 | * @param $server 132 | */ 133 | public function onManagerStart($server) 134 | { 135 | echo 'Date:' . date('Y-m-d H:i:s') . "\t swoole_http_server manager worker start\n"; 136 | $this->setProcessName($server->setting['ps_name'] . '-manager'); 137 | } 138 | 139 | /** 140 | * swoole-server master shutdown 141 | */ 142 | public function onShutdown() 143 | { 144 | unlink($this->setting['pid_file']); 145 | echo 'Date:' . date('Y-m-d H:i:s') . "\t swoole_http_server shutdown\n"; 146 | } 147 | 148 | /** 149 | * worker start 加载业务脚本常驻内存 150 | * 151 | * @param $server 152 | * @param $workerId 153 | */ 154 | public function onWorkerStart($server, $workerId) 155 | { 156 | if ($workerId >= $this->setting['worker_num']) { 157 | $this->setProcessName($server->setting['ps_name'] . '-task'); 158 | } else { 159 | $this->setProcessName($server->setting['ps_name'] . '-work'); 160 | } 161 | } 162 | 163 | /** 164 | * worker 进程停止 165 | * 166 | * @param $server 167 | * @param $workerId 168 | */ 169 | public function onWorkerStop($server, $workerId) 170 | { 171 | echo 'Date:' . date('Y-m-d H:i:s') . "\t swoole_http_server[{$server->setting['ps_name']}] worker:{$workerId} shutdown\n"; 172 | } 173 | 174 | 175 | /** 176 | * http请求处理 177 | * 178 | * @param $request 179 | * @param $response 180 | * 181 | * @return mixed 182 | */ 183 | public function onRequest($request, $response) 184 | { 185 | //获取swoole服务的当前状态 186 | if (isset($request->get['cmd']) && $request->get['cmd'] == 'status') { 187 | $res = $this->server->stats(); 188 | $res['start_time'] = date('Y-m-d H:i:s', $res['start_time']); 189 | $response->end(json_encode($res)); 190 | 191 | return true; 192 | } 193 | //TODO 非task请求处理 194 | $this->server->task($request); 195 | $out = '[' . date('Y-m-d H:i:s') . '] ' . json_encode($request) . PHP_EOL; 196 | //INFO 立即返回 非阻塞 197 | $response->end($out); 198 | 199 | return true; 200 | } 201 | 202 | /** 203 | * 任务处理 204 | * 205 | * @param $server 206 | * @param $taskId 207 | * @param $fromId 208 | * @param $request 209 | * 210 | * @return mixed 211 | */ 212 | public function onTask($server, $taskId, $fromId, $request) 213 | { 214 | //任务执行 worker_pid实际上是就是处理任务进程的task进程id 215 | $ret = $this->app->run($request, $taskId, $fromId); 216 | if (!isset($ret['workerPid'])) { 217 | //处理此任务的task-worker-id 218 | $ret['workerPid'] = $server->worker_pid; 219 | } 220 | 221 | //INFO swoole-1.7.18之后return 就会自动调用finish 222 | return $ret; 223 | } 224 | 225 | /** 226 | * 任务结束回调函数 227 | * 228 | * @param $server 229 | * @param $taskId 230 | * @param $ret 231 | */ 232 | public function onFinish($server, $taskId, $ret) 233 | { 234 | $fromId = $server->worker_id; 235 | if (!empty($ret['errno'])) { 236 | //任务成功运行不再提示 237 | //echo "\tTask[taskId:{$taskId}] success" . PHP_EOL; 238 | $error = PHP_EOL . var_export($ret, true); 239 | echo "\tTask[taskId:$fromId#{$taskId}] failed, Error[$error]" . PHP_EOL; 240 | } 241 | } 242 | } 243 | --------------------------------------------------------------------------------