├── .gitignore ├── Daemon.php ├── LICENSE ├── Lib ├── Event.php ├── LibInterface.php └── Select.php ├── README.md ├── Timer.php ├── composer.json ├── composer.lock ├── init.php └── test └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | 4 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 5 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 6 | # composer.lock 7 | -------------------------------------------------------------------------------- /Daemon.php: -------------------------------------------------------------------------------- 1 | 0) { 55 | exit(0); 56 | } 57 | if (-1 === posix_setsid()) { 58 | throw new Exception("setsid fail"); 59 | } 60 | // Fork again avoid SVR4 system regain the control of terminal. 61 | $pid = pcntl_fork(); 62 | if (-1 === $pid) { 63 | throw new Exception("fork fail"); 64 | } elseif (0 !== $pid) { 65 | exit(0); 66 | } 67 | } 68 | 69 | /** 70 | * 改变工作目录 71 | * @return [type] [description] 72 | */ 73 | protected static function chdir() 74 | { 75 | if (!chdir('/')) { 76 | throw new Exception("change dir fail", 1); 77 | } 78 | } 79 | 80 | /** 81 | * 关闭标准输出、标准错误 82 | * @return [type] [description] 83 | */ 84 | protected static function closeSTD() 85 | { 86 | //定义两个全局变量 87 | global $STDOUT, $STDERR; 88 | $handle = fopen(static::$stdoutFile, "a"); 89 | if ($handle) { 90 | unset($handle); 91 | set_error_handler(function () {}); 92 | fclose($STDOUT); 93 | fclose($STDERR); 94 | fclose(STDOUT); 95 | fclose(STDERR); 96 | $STDOUT = fopen(static::$stdoutFile, "a"); 97 | $STDERR = fopen(static::$stdoutFile, "a"); 98 | 99 | restore_error_handler(); 100 | } else { 101 | throw new Exception('can not open stdoutFile ' . static::$stdoutFile); 102 | } 103 | } 104 | 105 | /** 106 | * 设置定时器名字 107 | * 108 | * @param string $title 109 | * @return void 110 | */ 111 | protected static function setProcessTitle($title) 112 | { 113 | set_error_handler(function () {}); 114 | // >=php 5.5 115 | if (function_exists('cli_set_process_title')) { 116 | cli_set_process_title($title); 117 | } // Need proctitle when php<=5.5 . 118 | elseif (extension_loaded('proctitle') && function_exists('setproctitle')) { 119 | setproctitle($title); 120 | } 121 | restore_error_handler(); 122 | } 123 | 124 | /** 125 | * 返回当前执行环境 126 | * @return [type] [description] 127 | */ 128 | public static function getOS() 129 | { 130 | return self::$OS; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mrtwenty 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 | -------------------------------------------------------------------------------- /Lib/Event.php: -------------------------------------------------------------------------------- 1 | eventBase = new $class_name(); 39 | } 40 | 41 | /** 42 | * @see EventInterface::add() 43 | */ 44 | public function add($fd, $func, $flag = true, $args = array()) 45 | { 46 | $flag = $flag === true ? self::EV_TIMER : self::EV_TIMER_ONCE; 47 | 48 | if (class_exists('\\\\Event', false)) { 49 | $class_name = '\\\\Event'; 50 | } else { 51 | $class_name = '\Event'; 52 | } 53 | 54 | $param = array($func, (array) $args, $flag, $fd, self::$timerId); 55 | $event = new $class_name($this->eventBase, -1, $class_name::TIMEOUT | $class_name::PERSIST, array($this, "timerCallback"), $param); 56 | if (!$event || !$event->addTimer($fd)) { 57 | return false; 58 | } 59 | $this->eventTimer[self::$timerId] = $event; 60 | return self::$timerId++; 61 | } 62 | 63 | public function del($fd) 64 | { 65 | if (isset($this->eventTimer[$fd])) { 66 | $this->eventTimer[$fd]->del(); 67 | unset($this->eventTimer[$fd]); 68 | } 69 | return true; 70 | } 71 | 72 | /** 73 | * Timer callback. 74 | * @param null $fd 75 | * @param int $what 76 | * @param int $timer_id 77 | */ 78 | public function timerCallback($fd, $what, $param) 79 | { 80 | $timer_id = $param[4]; 81 | 82 | if ($param[2] === self::EV_TIMER_ONCE) { 83 | $this->eventTimer[$timer_id]->del(); 84 | unset($this->eventTimer[$timer_id]); 85 | } 86 | 87 | try { 88 | call_user_func_array($param[0], $param[1]); 89 | } catch (\Exception $e) { 90 | exit(250); 91 | } catch (\Error $e) { 92 | exit(250); 93 | } 94 | } 95 | 96 | /** 97 | * @see Events\EventInterface::clearAllTimer() 98 | * @return void 99 | */ 100 | public function clearAllTimer() 101 | { 102 | foreach ($this->eventTimer as $event) { 103 | $event->del(); 104 | } 105 | $this->eventTimer = array(); 106 | } 107 | 108 | /** 109 | * @see EventInterface::loop() 110 | */ 111 | public function loop() 112 | { 113 | $this->eventBase->loop(); 114 | } 115 | 116 | /** 117 | * Get timer count. 118 | * 119 | * @return integer 120 | */ 121 | public function getTimerCount() 122 | { 123 | return count($this->eventTimer); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Lib/LibInterface.php: -------------------------------------------------------------------------------- 1 | socket = stream_socket_pair( 20 | DIRECTORY_SEPARATOR === '/' ? 21 | STREAM_PF_UNIX : STREAM_PF_INET, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); 22 | $this->scheduler = new \SplPriorityQueue(); 23 | $this->scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 24 | } 25 | 26 | public function add($fd, $func, $flag = true, $args = array()) 27 | { 28 | $flag = $flag === true ? self::EV_TIMER : self::EV_TIMER_ONCE; 29 | 30 | $timer_id = $this->timerId++; 31 | $run_time = microtime(true) + $fd; 32 | 33 | $this->scheduler->insert($timer_id, -$run_time); 34 | $this->eventTimer[$timer_id] = array($func, (array) $args, $flag, $fd); 35 | $select_timeout = ($run_time - microtime(true)) * 1000000; 36 | if ($this->selectTimeout > $select_timeout) { 37 | $this->selectTimeout = $select_timeout; 38 | } 39 | return $timer_id; 40 | } 41 | 42 | public function loop() 43 | { 44 | while (1) { 45 | $read = $this->socket; 46 | set_error_handler(function () {}); 47 | $ret = stream_select($read, $write = [], $except = [], 0, $this->selectTimeout); 48 | restore_error_handler(); 49 | 50 | if (!$this->scheduler->isEmpty()) { 51 | $this->tick(); 52 | } 53 | } 54 | } 55 | 56 | public function getTimerCount() 57 | { 58 | return count($this->eventTimer); 59 | } 60 | 61 | /** 62 | * Tick for timer. 63 | * 64 | * @return void 65 | */ 66 | protected function tick() 67 | { 68 | while (!$this->scheduler->isEmpty()) { 69 | $scheduler_data = $this->scheduler->top(); 70 | $timer_id = $scheduler_data['data']; 71 | $next_run_time = -$scheduler_data['priority']; 72 | $time_now = microtime(true); 73 | $this->selectTimeout = ($next_run_time - $time_now) * 1000000; 74 | if ($this->selectTimeout <= 0) { 75 | $this->scheduler->extract(); 76 | 77 | if (!isset($this->eventTimer[$timer_id])) { 78 | continue; 79 | } 80 | // [func, args, flag, timer_interval] 81 | $task_data = $this->eventTimer[$timer_id]; 82 | if ($task_data[2] === self::EV_TIMER) { 83 | $next_run_time = $time_now + $task_data[3]; 84 | $this->scheduler->insert($timer_id, -$next_run_time); 85 | } 86 | call_user_func_array($task_data[0], $task_data[1]); 87 | if (isset($this->eventTimer[$timer_id]) && $task_data[2] === self::EV_TIMER_ONCE) { 88 | $this->del($timer_id, self::EV_TIMER_ONCE); 89 | } 90 | continue; 91 | } 92 | return; 93 | } 94 | $this->selectTimeout = 100000000; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function del($fd) 101 | { 102 | $fd_key = (int) $fd; 103 | unset($this->eventTimer[$fd_key]); 104 | return true; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function clearAllTimer() 111 | { 112 | $this->scheduler = new \SplPriorityQueue(); 113 | $this->scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 114 | $this->eventTimer = array(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timer 2 | php定时器,参考了workerman源码,由于workerman源码太过复杂, 故而抽取了一些出来,重新整理出来,实现一个单进程(守护进程)的定时器。 3 | 4 | ## 原理 5 | 1. 利用pcntl,守护进程化 6 | 2. 利用stream_select的超时机制,来实现sleep,如果有event扩展的话,优先使用event扩展 7 | 3. 定时器是时间堆的方式实现,利用php的spl的优先队列 8 | 9 | ## 使用方式 10 | 1.安装 11 | ``` 12 | composer require mrtwenty/timer 13 | ``` 14 | 2.编写index.php 15 | ```php 16 | add(0.5, function () { 30 | 31 | if (Daemon::getOS() === OS_TYPE_WIN) { 32 | echo microtime_float() . "\n"; 33 | } else { 34 | file_put_contents("/tmp/test.txt", microtime_float() . "\n", FILE_APPEND); 35 | } 36 | }); 37 | 38 | $timer->add(1, function () { 39 | 40 | if (Daemon::getOS() === OS_TYPE_WIN) { 41 | echo microtime_float() . "once \n"; 42 | } else { 43 | file_put_contents("/tmp/test.txt", microtime_float() . "once \n", FILE_APPEND); 44 | } 45 | }, false); 46 | 47 | $timer->loop(); 48 | ``` 49 | 3. 在cli环境上执行: 50 | ``` 51 | php index.php 52 | ``` 53 | 54 | 55 | -------------------------------------------------------------------------------- /Timer.php: -------------------------------------------------------------------------------- 1 | =5.3" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "timer\\": "./" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "85d8536c8e35d1119d34a194ae853866", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "dev", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.3" 17 | }, 18 | "platform-dev": [] 19 | } 20 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | add(0.5, function () { 16 | 17 | if (Daemon::getOS() === OS_TYPE_WIN) { 18 | echo microtime_float() . "\n"; 19 | } else { 20 | file_put_contents("/tmp/test.txt", microtime_float() . "\n", FILE_APPEND); 21 | } 22 | }); 23 | 24 | $timer->add(1, function () { 25 | 26 | if (Daemon::getOS() === OS_TYPE_WIN) { 27 | echo microtime_float() . "once \n"; 28 | } else { 29 | file_put_contents("/tmp/test.txt", microtime_float() . "once \n", FILE_APPEND); 30 | } 31 | }, false); 32 | 33 | $timer->loop(); 34 | --------------------------------------------------------------------------------