├── .gitignore ├── bin ├── install-swow ├── benchmarker-future └── install-swow.sh ├── src ├── Exception │ ├── DriverNotFoundException.php │ ├── DriverExtNotFoundException.php │ ├── LoopException.php │ └── InvalidArgumentException.php ├── Timer.php ├── Storage.php ├── Loop.php └── Drivers │ ├── AbstractLoop.php │ ├── LoopInterface.php │ ├── EventLoop.php │ ├── OpenSwooleLoop.php │ ├── EvLoop.php │ ├── NativeLoop.php │ └── SwowLoop.php ├── env ├── docker-compose.yaml └── Dockerfile ├── tests ├── UnitTests │ ├── NativeLoopTest.php │ ├── SwowLoopTest.php │ ├── EventLoopTest.php │ ├── Units │ │ ├── SignalsUnit.php │ │ ├── TimerUnit.php │ │ └── StreamsUnit.php │ ├── AbstractTestCase.php │ └── EvLoopTest.php └── Benchmarks │ ├── Future │ ├── WhileFuture.php │ ├── SleepWhileFuture.php │ ├── EvLoopFuture.php │ ├── EventLoopFuture.php │ ├── SwowLoopFuture.php │ ├── NativeLoopFuture.php │ └── FutureTestsFactory.php │ └── AbstractBenchmark.php ├── .github └── workflows │ └── CI.yml ├── phpunit.xml ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /vendor 4 | .phpunit.cache 5 | composer.lock 6 | .phpunit.result.cache -------------------------------------------------------------------------------- /bin/install-swow: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | startTests(); -------------------------------------------------------------------------------- /env/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | workbunny-php: 4 | restart: always 5 | container_name: workbunny-php 6 | build: 7 | context: ./ 8 | image: workbunny-php 9 | volumes: 10 | - ./../:/var/www 11 | 12 | logging: 13 | driver: json-file 14 | options: 15 | max-size: "20m" 16 | max-file: "10" 17 | tty: true 18 | -------------------------------------------------------------------------------- /bin/install-swow.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | success(){ echo "✅ $1"; exit 0; } 4 | info(){ echo "ℹ️ $1";} 5 | 6 | info "Installing library. " 7 | 8 | apk add --no-cache \ 9 | bash \ 10 | autoconf \ 11 | build-base \ 12 | openssl-dev 13 | 14 | info "Building php-ext swow. " 15 | 16 | php ./../vendor/bin/swow-builder --quiet 17 | 18 | info "Clearing cache. " 19 | 20 | apk del \ 21 | bash \ 22 | autoconf \ 23 | build-base \ 24 | openssl-dev 25 | 26 | success "Done. " -------------------------------------------------------------------------------- /tests/UnitTests/NativeLoopTest.php: -------------------------------------------------------------------------------- 1 | setCount($this->getCount() + 1); 13 | if($this->getInitialTime() + 1 <= microtime(true)) { 14 | break; 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/Benchmarks/Future/SleepWhileFuture.php: -------------------------------------------------------------------------------- 1 | setCount($this->getCount() + 1); 13 | if($this->getInitialTime() + 1 <= microtime(true)) { 14 | break; 15 | } 16 | \sleep(0); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /env/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm-alpine 2 | 3 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ 4 | apk add --no-cache \ 5 | php-dom \ 6 | php-xml \ 7 | php-xmlwriter \ 8 | php-xmlreader \ 9 | php-tokenizer \ 10 | composer && \ 11 | composer self-update && \ 12 | curl -sSL https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions -o - | sh -s \ 13 | sockets pcntl zip event ev ffi xdebug opcache ds 14 | 15 | VOLUME /var/www 16 | WORKDIR /var/www -------------------------------------------------------------------------------- /tests/UnitTests/SwowLoopTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('SwowLoop tests skipped because ext-swow extension is not installed.'); 15 | } 16 | return new SwowLoop(); 17 | } 18 | 19 | /** @inheritDoc */ 20 | public function setTickTimeout(): float 21 | { 22 | return 0.001; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/UnitTests/EventLoopTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('ext-event tests skipped because ext-event is not installed.'); 16 | } 17 | return new EventLoop(); 18 | } 19 | 20 | /** @inheritDoc */ 21 | public function setTickTimeout(): float 22 | { 23 | return 0.02; //20ms 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception/LoopException.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop\Exception; 15 | 16 | /** 17 | * Class LoopException 18 | * @package WorkBunny\WorkBunny\EventLoop\Exception 19 | * @author chaz6chez 20 | */ 21 | class LoopException extends \RuntimeException 22 | {} -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | pull_request: 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | PHPUnit: 10 | name: PHPUnit (PHP ${{ matrix.php }}) 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: 15 | - 8.3 16 | - 8.2 17 | - 8.1 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: event, ev 24 | tools: phpunit:10, composer:v2 25 | coverage: none 26 | - run: composer install 27 | - run: vendor/bin/phpunit 28 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop\Exception; 15 | 16 | /** 17 | * Class LoopException 18 | * @package WorkBunny\WorkBunny\EventLoop\Exception 19 | * @author chaz6chez 20 | */ 21 | class InvalidArgumentException extends \RuntimeException 22 | {} -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/UnitTests 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/Benchmarks/Future/EvLoopFuture.php: -------------------------------------------------------------------------------- 1 | addTimer(0.0, 0.0, function () use ($loop){ 15 | $this->setCount($this->getCount() + 1); 16 | if($this->getInitialTime() + 1 >= microtime(true)){ 17 | return; 18 | } 19 | $loop->stop(); 20 | }); 21 | $loop->run(); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Benchmarks/Future/EventLoopFuture.php: -------------------------------------------------------------------------------- 1 | addTimer(0.0, 0.0, function () use ($loop){ 15 | $this->setCount($this->getCount() + 1); 16 | if($this->getInitialTime() + 1 >= microtime(true)){ 17 | return; 18 | } 19 | $loop->stop(); 20 | }); 21 | $loop->run(); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Benchmarks/Future/SwowLoopFuture.php: -------------------------------------------------------------------------------- 1 | addTimer(0.0, 0.0, function () use ($loop){ 16 | $this->setCount($this->getCount() + 1); 17 | if($this->getInitialTime() + 1 >= microtime(true)){ 18 | return; 19 | } 20 | $loop->stop(); 21 | }); 22 | $loop->run(); 23 | } 24 | } -------------------------------------------------------------------------------- /tests/Benchmarks/Future/NativeLoopFuture.php: -------------------------------------------------------------------------------- 1 | addTimer(0.0, 0.0, function () use ($loop){ 16 | $this->setCount($this->getCount() + 1); 17 | if($this->getInitialTime() + 1 >= microtime(true)){ 18 | return; 19 | } 20 | $loop->stop(); 21 | }); 22 | $loop->run(); 23 | } 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 WorkBunny 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workbunny/event-loop", 3 | "type": "library", 4 | "keywords": [ 5 | "event-loop", 6 | "ext-ev", 7 | "ext-event", 8 | "asynchronous", 9 | "reactor" 10 | ], 11 | "homepage": "https://github.com/workbunny", 12 | "license": "MIT", 13 | "description": "A high-performance event loop library for PHP", 14 | "authors": [ 15 | { 16 | "name": "chaz6chez", 17 | "email": "chaz6chez1993@outlook.com", 18 | "homepage": "https://chaz6chez.cn", 19 | "role": "Developer" 20 | } 21 | ], 22 | "support": { 23 | "email": "chaz6chez1993@outlook.com", 24 | "issues": "https://github.com/workbunny/event-loop/issues", 25 | "source": "https://github.com/workbunny/event-loop" 26 | }, 27 | "require": { 28 | "php": ">=8.1" 29 | }, 30 | "require-dev": { 31 | "symfony/var-dumper": "^6.0", 32 | "phpunit/phpunit": "^10.0", 33 | "swow/swow": "^1.0" 34 | }, 35 | "suggest": { 36 | "ext-event": "For EventLoop. ", 37 | "ext-ev": "For EvLoop. ", 38 | "swow/swow": "For SwowLoop. ", 39 | "ext-pcntl" : "Let NativeLoop support signal handling. " 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "WorkBunny\\EventLoop\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "WorkBunny\\Tests\\": "tests" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Timer.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop; 15 | 16 | use Closure; 17 | 18 | final class Timer 19 | { 20 | /** @var float 延迟 */ 21 | private float $delay; 22 | 23 | /** @var float|false 重复 */ 24 | private float|false $repeat; 25 | 26 | /** @var Closure 处理函数 */ 27 | private Closure $handler; 28 | 29 | /** 30 | * @param float $delay 31 | * @param float|false $repeat 32 | * @param Closure $handler 33 | */ 34 | public function __construct(float $delay, float|false $repeat, Closure $handler) 35 | { 36 | $this->delay = $delay; 37 | $this->repeat = $repeat; 38 | $this->handler = $handler; 39 | } 40 | 41 | /** 42 | * @return float 43 | */ 44 | public function getDelay(): float 45 | { 46 | return $this->delay; 47 | } 48 | 49 | /** 50 | * @return float|false 51 | */ 52 | public function getRepeat(): float|false 53 | { 54 | return $this->repeat; 55 | } 56 | 57 | /** 58 | * @return Closure 59 | */ 60 | public function getHandler(): Closure 61 | { 62 | return $this->handler; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Storage.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop; 15 | 16 | final class Storage 17 | { 18 | /** @var int */ 19 | private int $_count = 0; 20 | 21 | /** @var array storage */ 22 | private array $_storage = []; 23 | 24 | /** 25 | * @param string $key 26 | * @param mixed|null $value 27 | * @return string 28 | */ 29 | public function add(string $key, mixed $value): string 30 | { 31 | $this->_storage[$key] = $value; 32 | $this->_count ++; 33 | return $key; 34 | } 35 | 36 | /** 37 | * @param string $key 38 | * @param mixed|null $value 39 | * @return string 40 | */ 41 | public function set(string $key, mixed $value): string 42 | { 43 | if($this->exist($key)){ 44 | $this->_storage[$key] = $value; 45 | } 46 | return $key; 47 | } 48 | 49 | /** 50 | * @param string $key 51 | */ 52 | public function del(string $key): void 53 | { 54 | unset($this->_storage[$key]); 55 | if ($this->_count > 0){ 56 | $this->_count --; 57 | } 58 | } 59 | 60 | /** 61 | * @param string $key 62 | * @return mixed|null 63 | */ 64 | public function get(string $key): mixed 65 | { 66 | return $this->exist($key) ? $this->_storage[$key] : null; 67 | } 68 | 69 | /** 70 | * @return int 71 | */ 72 | public function count(): int 73 | { 74 | return $this->_count; 75 | } 76 | 77 | /** 78 | * @param string $key 79 | * @return bool 80 | */ 81 | public function exist(string $key): bool 82 | { 83 | return isset($this->_storage[$key]); 84 | } 85 | 86 | /** 87 | * @return bool 88 | */ 89 | public function isEmpty(): bool 90 | { 91 | return empty($this->_storage); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/UnitTests/Units/SignalsUnit.php: -------------------------------------------------------------------------------- 1 | getLoop()->delSignal(2); 17 | $this->assertTrue(true); 18 | } 19 | 20 | /** 21 | * 添加相同的信号 22 | * 23 | * @runInSeparateProcess 24 | * @return void 25 | */ 26 | public function testAddSameSignal(): void 27 | { 28 | if (!extension_loaded('posix')) { 29 | $this->markTestSkipped('Signal test skipped because ext-posix are missing.'); 30 | } 31 | $count1 = $count2 = 0; 32 | 33 | $this->getLoop()->addSignal(10, function ($signal) use (&$count1) { 34 | $count1 ++; 35 | $this->getLoop()->delSignal($signal); 36 | }); 37 | $this->getLoop()->addSignal(10, function ($signal) use (&$count2) { 38 | $count2 ++; 39 | $this->getLoop()->delSignal($signal); 40 | }); 41 | 42 | $this->getLoop()->addTimer(0.1,false, function () { 43 | \posix_kill(\getmypid(), 10); 44 | }); 45 | $this->getLoop()->addTimer(0.5,false, function () { 46 | $this->getLoop()->stop(); 47 | }); 48 | 49 | $this->getLoop()->run(); 50 | 51 | $this->assertEquals(1, $count1); 52 | $this->assertEquals(0, $count2); 53 | } 54 | 55 | /** 56 | * 信号响应 57 | * 58 | * @runInSeparateProcess 59 | * @return void 60 | */ 61 | public function testSignalResponse(): void 62 | { 63 | if (!extension_loaded('posix')) { 64 | $this->markTestSkipped('Signal test skipped because ext-posix are missing.'); 65 | } 66 | $count1 = $count2 = 0; 67 | 68 | $this->getLoop()->addSignal(10, function ($signal) use (&$count1) { 69 | $count1 ++; 70 | $this->getLoop()->delSignal($signal); 71 | }); 72 | $this->getLoop()->addSignal(12, function ($signal) use (&$count2) { 73 | $count2 ++; 74 | $this->getLoop()->delSignal($signal); 75 | }); 76 | 77 | $this->getLoop()->addTimer(0.1,false, function () { 78 | \posix_kill(\getmypid(), 12); 79 | }); 80 | 81 | $this->getLoop()->addTimer(0.5,false, function () { 82 | $this->getLoop()->stop(); 83 | }); 84 | 85 | $this->getLoop()->run(); 86 | 87 | $this->assertEquals(0, $count1); 88 | $this->assertEquals(1, $count2); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/Loop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop; 15 | 16 | use WorkBunny\EventLoop\Drivers\EvLoop; 17 | use WorkBunny\EventLoop\Drivers\LoopInterface; 18 | use WorkBunny\EventLoop\Drivers\NativeLoop; 19 | use WorkBunny\EventLoop\Drivers\EventLoop; 20 | use WorkBunny\EventLoop\Drivers\OpenSwooleLoop; 21 | use WorkBunny\EventLoop\Drivers\SwowLoop; 22 | use WorkBunny\EventLoop\Exception\DriverExtNotFoundException; 23 | use WorkBunny\EventLoop\Exception\DriverNotFoundException; 24 | 25 | final class Loop 26 | { 27 | /** @var LoopInterface[] */ 28 | protected static array $_loops = []; 29 | 30 | /** @var array|string[] */ 31 | protected static array $_drivers = [ 32 | EventLoop::class, 33 | EvLoop::class, 34 | NativeLoop::class, 35 | SwowLoop::class, 36 | OpenSwooleLoop::class 37 | ]; 38 | 39 | /** 40 | * 创建事件循环 41 | * 42 | * @param string $loop 43 | * @return LoopInterface 44 | * @throws DriverNotFoundException 驱动未找到 45 | * @throws DriverExtNotFoundException 驱动未安装php-ext 46 | */ 47 | public static function create(string $loop = NativeLoop::class): LoopInterface 48 | { 49 | if(!isset(self::$_loops[$loop])){ 50 | if(!in_array($loop, self::$_drivers)){ 51 | throw new DriverNotFoundException('not found :' . $loop); 52 | } 53 | /** 54 | * @throws DriverExtNotFoundException 55 | */ 56 | self::$_loops[$loop] = new $loop(); 57 | } 58 | return self::$_loops[$loop]; 59 | } 60 | 61 | /** 62 | * 移除事件循环 63 | * 64 | * @param string|null $loop 65 | * @return void 66 | */ 67 | public static function remove(?string $loop = null): void 68 | { 69 | if($loop === null){ 70 | self::$_loops = []; 71 | return; 72 | } 73 | if(isset(self::$_loops[$loop])){ 74 | unset(self::$_loops[$loop]); 75 | } 76 | } 77 | 78 | /** 79 | * 注册驱动 80 | * 81 | * @param string $loop 82 | * @return void 83 | */ 84 | public static function register(string $loop): void 85 | { 86 | if((new $loop()) instanceof LoopInterface){ 87 | self::$_drivers[] = $loop; 88 | } 89 | } 90 | 91 | 92 | } -------------------------------------------------------------------------------- /src/Drivers/AbstractLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | namespace WorkBunny\EventLoop\Drivers; 15 | 16 | use WorkBunny\EventLoop\Exception\DriverExtNotFoundException; 17 | use WorkBunny\EventLoop\Storage; 18 | 19 | abstract class AbstractLoop implements LoopInterface 20 | { 21 | /** @var resource[] */ 22 | protected array $_readFds = []; 23 | 24 | /** @var resource[] */ 25 | protected array $_writeFds = []; 26 | 27 | /** @var array All listeners for read event. */ 28 | protected array $_reads = []; 29 | 30 | /** @var array All listeners for write event. */ 31 | protected array $_writes = []; 32 | 33 | /** @var array Event listeners of signal. */ 34 | protected array $_signals = []; 35 | 36 | /** @var Storage 定时器容器 */ 37 | protected Storage $_storage; 38 | 39 | /** 40 | * @throws DriverExtNotFoundException 41 | */ 42 | public function __construct() 43 | { 44 | if(!$this->hasExt()) { 45 | $extName = $this->getExtName(); 46 | throw new DriverExtNotFoundException("php-ext: $extName not found. "); 47 | } 48 | $this->_storage = new Storage(); 49 | } 50 | 51 | /** 52 | * @return resource[] 53 | */ 54 | public function getReadFds(): array 55 | { 56 | return $this->_readFds; 57 | } 58 | 59 | /** 60 | * @return resource[] 61 | */ 62 | public function getWriteFds(): array 63 | { 64 | return $this->_writeFds; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getReads(): array 71 | { 72 | return $this->_reads; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getWrites(): array 79 | { 80 | return $this->_writes; 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function getSignals(): array 87 | { 88 | return $this->_signals; 89 | } 90 | 91 | /** 92 | * @return Storage 93 | */ 94 | public function getStorage(): Storage 95 | { 96 | return $this->_storage; 97 | } 98 | 99 | /** 100 | * @return void 101 | */ 102 | public function clear(): void 103 | { 104 | $this->_storage = new Storage(); 105 | $this->_writeFds = []; 106 | $this->_readFds = []; 107 | $this->_writes = []; 108 | $this->_reads = []; 109 | } 110 | } -------------------------------------------------------------------------------- /src/Drivers/LoopInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use Closure; 15 | use WorkBunny\EventLoop\Exception\InvalidArgumentException; 16 | 17 | interface LoopInterface 18 | { 19 | /** 20 | * 如无需拓展则返回null 21 | * 22 | * @return string|null 23 | */ 24 | public function getExtName(): null|string; 25 | 26 | /** 27 | * 是否存在拓展 28 | * 29 | * @return bool 30 | */ 31 | public function hasExt(): bool; 32 | 33 | /** 34 | * 添加信号处理器 35 | * 36 | * @param int $signal 37 | * @param Closure $handler 38 | * @throws InvalidArgumentException 当signal参数为非正整数时将抛出该异常 39 | */ 40 | public function addSignal(int $signal, Closure $handler): void; 41 | 42 | /** 43 | * 移除信号处理器 44 | * 45 | * @param int $signal 46 | * @throws InvalidArgumentException 当signal参数为非正整数时将抛出该异常 47 | */ 48 | public function delSignal(int $signal): void; 49 | 50 | /** 51 | * 添加读流处理器 52 | * 53 | * @param resource $stream 54 | * @param Closure $handler 55 | */ 56 | public function addReadStream($stream, Closure $handler): void; 57 | 58 | /** 59 | * 移除读流处理器 60 | * 61 | * @param resource $stream 62 | */ 63 | public function delReadStream($stream): void; 64 | 65 | /** 66 | * 创建写流处理器 67 | * 68 | * @param resource $stream 69 | * @param Closure $handler 70 | */ 71 | public function addWriteStream($stream, Closure $handler): void; 72 | 73 | /** 74 | * 移除写流处理器 75 | * 76 | * @param resource $stream 77 | */ 78 | public function delWriteStream($stream): void; 79 | 80 | /** 81 | * @Future [delay=0.0, repeat=false] 82 | * 在下一个周期执行,执行一次即销毁 83 | * @ReFuture [delay=0.0, repeat=0.0] 84 | * 在每一个周期执行,不会自动销毁 85 | * @DelayReFuture [delay>0.0, repeat=0.0] 86 | * 延迟delay秒后每一个周期执行,不会自动销毁 87 | * @Delayer [delay>0.0, repeat=false] 88 | * 延迟delay秒后执行,执行一次即销毁 89 | * @Timer [delay=0.0, repeat>0.0] 90 | * 在下一个周期开始每间隔repeat秒执行,不会自动销毁 91 | * @DelayTimer [delay>0.0, repeat>0.0] 92 | * 延迟delay秒后每间隔repeat秒执行,不会自动销毁 93 | * 94 | * @param float $delay 95 | * @param float|false $repeat 96 | * @param Closure $handler 97 | * @return string 98 | * @throws InvalidArgumentException delay或repeat为负数时抛出该异常 99 | */ 100 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string; 101 | 102 | /** 103 | * 移除定时处理器 104 | * 105 | * @param string $timerId 106 | */ 107 | public function delTimer(string $timerId): void; 108 | 109 | /** 110 | * 运行loop 111 | * 112 | * @return void 113 | */ 114 | public function run(): void; 115 | 116 | /** 117 | * 暂停loop 118 | * 119 | * @return void 120 | */ 121 | public function stop(): void; 122 | 123 | /** 124 | * 与stop()不同的是,该方法会暂停loop并清除所有处理器 125 | * 126 | * @return void 127 | */ 128 | public function destroy(): void; 129 | } 130 | -------------------------------------------------------------------------------- /tests/Benchmarks/AbstractBenchmark.php: -------------------------------------------------------------------------------- 1 | setInitialTime(microtime(true)); 25 | $this->setInitialMemoryUsage(memory_get_usage()); 26 | $this->handler(); 27 | $this->getDuration(true); 28 | $this->getUsedMemory(true); 29 | } 30 | 31 | /** 32 | * @return void 33 | */ 34 | public function clear(): void 35 | { 36 | $this->_initialTime = 0.0; 37 | $this->_initialMemoryUsage = 0; 38 | $this->_duration = 0.0; 39 | $this->_usedMemory = 0; 40 | $this->_count = 0; 41 | } 42 | 43 | /** 44 | * @return int 45 | */ 46 | public function getInitialMemoryUsage(): int 47 | { 48 | return $this->_initialMemoryUsage; 49 | } 50 | 51 | /** 52 | * @param int $initialMemoryUsage 53 | */ 54 | public function setInitialMemoryUsage(int $initialMemoryUsage): void 55 | { 56 | $this->_initialMemoryUsage = $initialMemoryUsage; 57 | } 58 | 59 | /** 60 | * @return float 61 | */ 62 | public function getInitialTime(): float 63 | { 64 | return $this->_initialTime; 65 | } 66 | 67 | /** 68 | * @param float $initialTime 69 | */ 70 | public function setInitialTime(float $initialTime): void 71 | { 72 | $this->_initialTime = $initialTime; 73 | } 74 | 75 | /** 76 | * @param bool $set 77 | * @return int 78 | */ 79 | public function getUsedMemory(bool $set = true): int 80 | { 81 | if($set){ 82 | $this->setUsedMemory(memory_get_usage() - $this->getInitialMemoryUsage()); 83 | } 84 | return $this->_usedMemory; 85 | } 86 | 87 | /** 88 | * @param int $usedMemory 89 | */ 90 | public function setUsedMemory(int $usedMemory): void 91 | { 92 | $this->_usedMemory = $usedMemory; 93 | } 94 | 95 | /** 96 | * @param bool $set 97 | * @return float 98 | */ 99 | public function getDuration(bool $set = true): float 100 | { 101 | if($set){ 102 | $this->setDuration(microtime(true) - $this->getInitialTime()); 103 | } 104 | return $this->_duration; 105 | } 106 | 107 | /** 108 | * @param float $duration 109 | */ 110 | public function setDuration(float $duration): void 111 | { 112 | $this->_duration = $duration; 113 | } 114 | 115 | /** 116 | * @return int 117 | */ 118 | public function getCount(): int 119 | { 120 | return $this->_count; 121 | } 122 | 123 | /** 124 | * @param int $count 125 | */ 126 | public function setCount(int $count): void 127 | { 128 | $this->_count = $count; 129 | } 130 | 131 | /** 132 | * 运行逻辑 133 | * 134 | * @return void 135 | */ 136 | abstract public function handler(): void; 137 | } -------------------------------------------------------------------------------- /tests/Benchmarks/Future/FutureTestsFactory.php: -------------------------------------------------------------------------------- 1 | WhileFuture::class, 16 | 'while-has-sleep' => SleepWhileFuture::class, 17 | 'native' => NativeLoopFuture::class, 18 | 'event' => EventLoopFuture::class, 19 | 'ev' => EvLoopFuture::class, 20 | 'swow' => SwowLoopFuture::class 21 | ]; 22 | 23 | /** 24 | * 结果数据 25 | * 26 | * @var array[][] 27 | */ 28 | private array $_result = [ 29 | 'schema' => [ 30 | 'name_strlen' => 4, 31 | 'count_strlen' => 5, 32 | 'memory_strlen' => 6 33 | ] 34 | ]; 35 | 36 | /** 37 | * 输出结果 38 | * 39 | * @return void 40 | */ 41 | protected function _resultTable(): void 42 | { 43 | $nameStrlenArr = array_column($this->_result, 'name_strlen'); 44 | $countStrlenArr = array_column($this->_result, 'count_strlen'); 45 | $memoryStrlenArr = array_column($this->_result, 'memory_strlen'); 46 | rsort($nameStrlenArr); 47 | rsort($countStrlenArr); 48 | rsort($memoryStrlenArr); 49 | $nameStrlen = 2 + $nameStrlenArr[0]; 50 | $countStrlen = 2 + $countStrlenArr[0]; 51 | $memoryStrlen = 2 + $memoryStrlenArr[0]; 52 | echo ' | Test Result: ' . str_repeat('-', 2 + $nameStrlen + $countStrlen + $memoryStrlen - 14) . '+' . PHP_EOL; 53 | echo ' |' . str_pad('Name', $nameStrlen, ' ', \STR_PAD_BOTH) . 54 | '|' . str_pad('Count', $countStrlen, ' ', \STR_PAD_BOTH) . 55 | '|' . str_pad('Memory', $memoryStrlen, ' ', \STR_PAD_BOTH) . 56 | '|' . PHP_EOL; 57 | echo ' |' . str_pad('-', $nameStrlen, '-', \STR_PAD_BOTH) . 58 | '|' . str_pad('-', $countStrlen, '-', \STR_PAD_BOTH) . 59 | '|' . str_pad('-', $memoryStrlen, '-', \STR_PAD_BOTH) . 60 | '|' . PHP_EOL; 61 | foreach ($this->_result as $key => $item) { 62 | if($key !== 'schema'){ 63 | echo ' |' . str_pad((string)$key, $nameStrlen, ' ', \STR_PAD_BOTH) . 64 | '|' . str_pad((string)$item['count'], $countStrlen, ' ', \STR_PAD_BOTH) . 65 | '|' . str_pad("{$item['memory']} B", $memoryStrlen, ' ', \STR_PAD_BOTH) . 66 | '|' . PHP_EOL; 67 | } 68 | } 69 | echo ' |' . str_repeat('-', 2 + $nameStrlen + $countStrlen + $memoryStrlen) . '+' . PHP_EOL; 70 | } 71 | 72 | /** 73 | * 运行测试 74 | * 75 | * @return void 76 | */ 77 | final public function startTests(): void 78 | { 79 | echo 'ℹ️ Wait. ' . PHP_EOL; 80 | foreach ($this->_futureTests as $name => $futureTest) { 81 | /** @var AbstractBenchmark $obj */ 82 | $obj = new $futureTest; 83 | $this->_result[$name]['count'] = $obj->getCount(); 84 | $this->_result[$name]['count_strlen'] = strlen((string)$obj->getCount()); 85 | $this->_result[$name]['memory'] = $obj->getUsedMemory(false); 86 | $this->_result[$name]['memory_strlen'] = strlen((string)$obj->getUsedMemory(false)); 87 | $this->_result[$name]['name_strlen'] = strlen($name); 88 | } 89 | echo chr(27) . "[1A"; 90 | echo '✅ Done! ' . PHP_EOL; 91 | $this->_resultTable(); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/UnitTests/AbstractTestCase.php: -------------------------------------------------------------------------------- 1 | [$expected, $actual], 27 | * 2 => [$expected, $actual] 28 | * ] 29 | */ 30 | protected array $info = []; 31 | protected int $count = 0; 32 | protected string $string = ''; 33 | protected float $startTime = 0.0; 34 | protected float $endTime = 0.0; 35 | 36 | /** 37 | * 初始化 38 | * @return void 39 | */ 40 | public function setUp(): void 41 | { 42 | $this->loop = $this->setLoop(); 43 | $this->tickTimeout = $this->setTickTimeout(); 44 | 45 | $this->startTime = $this->endTime = 0.0; 46 | $this->string = ''; 47 | $this->count = 0; 48 | $this->info = []; 49 | } 50 | 51 | /** 创建循环 */ 52 | abstract public function setLoop() : AbstractLoop; 53 | 54 | /** 设置tickTimeout */ 55 | abstract public function setTickTimeout(): float; 56 | 57 | /** 获取循环 */ 58 | public function getLoop(): ? AbstractLoop 59 | { 60 | return $this->loop; 61 | } 62 | 63 | /** 64 | * 设置开始时间 65 | * @param float $startTime 66 | * @return void 67 | */ 68 | public function setStartTime(float $startTime): void 69 | { 70 | $this->startTime = $startTime; 71 | } 72 | 73 | /** 74 | * 设置结束时间 75 | * @param float $endTime 76 | * @return void 77 | */ 78 | public function setEndTime(float $endTime): void 79 | { 80 | $this->endTime = $endTime; 81 | } 82 | 83 | /** 84 | * @return float 85 | */ 86 | public function getStartTime(): float 87 | { 88 | return $this->startTime; 89 | } 90 | 91 | /** 92 | * @return float 93 | */ 94 | public function getEndTime(): float 95 | { 96 | return $this->startTime; 97 | } 98 | 99 | /** 100 | * 设置计数 101 | * @param int $count 102 | * @return void 103 | */ 104 | public function setCountNum(int $count): void 105 | { 106 | $this->count = $count; 107 | } 108 | 109 | /** 110 | * 获取计数 111 | * @return int 112 | */ 113 | public function getCountNum(): int 114 | { 115 | return $this->count; 116 | } 117 | 118 | /** 119 | * @param int $count 120 | * @param mixed $expected 121 | * @param mixed $actual 122 | * @return void 123 | */ 124 | public function addInfo(int $count, mixed $expected, mixed $actual): void 125 | { 126 | $this->info[$count] = [$expected, $actual]; 127 | } 128 | 129 | /** 130 | * @return array 131 | */ 132 | public function getInfo(): array 133 | { 134 | return $this->info; 135 | } 136 | 137 | /** 138 | * 运行loop 139 | * @return void 140 | */ 141 | public function runLoop(): void 142 | { 143 | $this->setStartTime(microtime(true)); 144 | $this->getLoop()->run(); 145 | $this->setEndTime(microtime(true)); 146 | } 147 | 148 | /** 149 | * 利用@Future模拟单次loop 150 | * @param float $delay 151 | * @return void 152 | */ 153 | public function tickLoop(float $delay = 0.0): void 154 | { 155 | $this->getLoop()->addTimer($delay, false, function () { 156 | $this->getLoop()->stop(); 157 | }); 158 | $this->runLoop(); 159 | } 160 | 161 | /** 162 | * 断言检查info 163 | * @return void 164 | */ 165 | public function assertInfo(): void 166 | { 167 | foreach ($this->getInfo() as $count => list($expected, $actual)) { 168 | $this->assertLessThan($expected, $actual, "Error Count: $count"); 169 | } 170 | } 171 | 172 | /** 173 | * 比较时间 174 | * @param float $maxInterval 175 | * @return void 176 | */ 177 | public function assertRunFasterThan(float $maxInterval): void 178 | { 179 | $this->runLoop(); 180 | $this->assertGreaterThan($this->getEndTime() - $this->getStartTime(), $maxInterval); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

workbunny

3 | 4 | **

workbunny/event-loop

** 5 | 6 | **

🐇 A high-performance event loop library for PHP 🐇

** 7 | 8 |
9 | 10 | Build Status 11 | 12 | 13 | PHP Version Require 14 | 15 | 16 | GitHub license 17 | 18 | 19 |
20 | 21 | 22 | ## 简介 23 | 24 | 一个事件循环库,目的是为了构建高性能网络应用。 25 | 26 | ## 使用 27 | 28 | 注:本文档为 1.2.x 版本,旧版请点击 **[1.1.x 版本](https://github.com/workbunny/event-loop/tree/1.1.x)** 跳转 29 | 注:swowloop还未完成单元测试,敬请等待 30 | 31 | ### 安装 32 | ``` 33 | composer require workbunny/event-loop 34 | ``` 35 | 36 | ### 创建loop 37 | 38 | ```php 39 | use WorkBunny\EventLoop\Loop; 40 | use WorkBunny\EventLoop\Drivers\NativeLoop; 41 | use WorkBunny\EventLoop\Drivers\EventLoop; 42 | use WorkBunny\EventLoop\Drivers\EvLoop; 43 | use WorkBunny\EventLoop\Drivers\SwowLoop; 44 | 45 | // 创建PHP原生loop 46 | $loop = Loop::create(NativeLoop::class); 47 | // 创建ext-event loop 48 | $loop = Loop::create(EventLoop::class); 49 | // 创建ext-ev loop 50 | $loop = Loop::create(EvLoop::class); 51 | // 创建swow loop 52 | $loop = Loop::create(SwowLoop::class); 53 | ``` 54 | 55 | ### 注册loop 56 | 57 | - 创建 YourLoopClass 实现 LoopInterface 58 | - 调用 Loop::register() 注册 YourLoopClass 59 | 60 | ```php 61 | use WorkBunny\EventLoop\Loop; 62 | // 注册 63 | loop::register(YourLoopClass::class); 64 | // 创建 65 | $yourLoop = Loop::create(YourLoopClass::class); 66 | ``` 67 | 68 | ### 创建定时器 69 | 70 | - Future 触发器 71 | ```php 72 | /** 73 | * @Future [delay=0.0, repeat=false] 74 | * 在下一个周期执行,执行一次即自动销毁 75 | */ 76 | $loop->addTimer(0.0, false, function (){ echo 'timer'; }); // loop->run()后立即输出字符串 77 | ``` 78 | 79 | - ReFuture 重复触发器 80 | ```php 81 | /** 82 | * @ReFuture [delay=0.0, repeat=0.0] 83 | * 在每一个周期执行,不会自动销毁 84 | */ 85 | $id = $loop->addTimer(0.0, 0.0, function () use(&$loop, &$id) { 86 | // 此方法可以实现自我销毁 87 | $loop->delTimer($id); 88 | }); 89 | ``` 90 | 91 | - DelayReFuture 延迟的重复触发器 92 | ```php 93 | /** 94 | * @DelayReFuture [delay>0.0, repeat=0.0] 95 | * 延迟delay秒后每一个周期执行,不会自动销毁 96 | */ 97 | $id = $loop->addTimer(1.0, 0.0, function () use(&$loop, &$id) { 98 | // 此方法可以实现自我销毁 99 | $loop->delTimer($id); 100 | }); 101 | ``` 102 | 103 | - Delayer 延迟器 104 | ```php 105 | /** 106 | * @Delayer [delay>0.0, repeat=false] 107 | * 延迟delay秒后执行,执行一次即自动销毁 108 | */ 109 | $loop->addTimer(2.0, false, function (){ echo 'timer'; }); // loop->run() 2秒后输出字符串 110 | ``` 111 | 112 | - Timer 定时器 113 | ```php 114 | /** 115 | * @Timer [delay=0.0, repeat>0.0] 116 | * 在下一个周期开始每间隔repeat秒执行,不会自动销毁 117 | */ 118 | $id = $loop->addTimer(0.1, 0.1, function () use(&$loop, &$id) { 119 | // 此方法可以实现自我销毁 120 | $loop->delTimer($id); 121 | }); 122 | ``` 123 | 124 | - DelayTimer 延迟的定时器 125 | ```php 126 | /** 127 | * @DelayTimer [delay>0.0, repeat>0.0] 128 | * 延迟delay秒后每间隔repeat秒执行,不会自动销毁 129 | */ 130 | $id = $loop->addTimer(0.2, 0.1, function () use(&$loop, &$id) { 131 | // 此方法可以实现自我销毁 132 | $loop->delTimer($id); 133 | }); 134 | ``` 135 | 136 | ### 流事件 137 | 138 | 这里的流是指 **[PHP Streams](https://www.php.net/manual/zh/book.stream.php)** 139 | 140 | - 读取流 141 | ```php 142 | // 创建 143 | $loop->addReadStream(resource, function (resource $stream) { }); 144 | // 注意:EvLoop在这里较为特殊,回调函数的入参为EvIo对象 145 | $loop->addReadStream(resource, function (\EvIo $evio) { 146 | $evio->stream // resource 资源类型 147 | }); 148 | // 移除 149 | $loop->delReadStream(resource); 150 | ``` 151 | 152 | - 写入流 153 | ```php 154 | // 创建 155 | $loop->addWriteStream(resource, function (resource $stream) { }); 156 | // 注意:EvLoop在这里较为特殊,回调函数的入参为EvIo对象 157 | $loop->addWriteStream(resource, function (\EvIo $evio) { 158 | $evio->stream // resource 资源类型 159 | }); 160 | // 移除 161 | $loop->delWriteStream(resource); 162 | ``` 163 | 164 | ### 信号事件 165 | 166 | 用于接收系统的信号,比如kill等 167 | ```php 168 | // 注册 169 | $loop->addSignal(\SIGUSR1, function (){}); 170 | // 移除 171 | $loop->delSignal(\SIGUSR1, function (){}); 172 | ``` 173 | 174 | ### 启动/停止 175 | 176 | - 启动 177 | 178 | 以下代码会持续阻塞,请放在程序最后一行 179 | ```php 180 | # 该函数后会阻塞 181 | $loop->loop(); 182 | 183 | # 该行代码不会执行 184 | var_dump('123'); 185 | ``` 186 | 187 | - 停止 188 | 189 | 以下代码不会阻塞等待 190 | ```php 191 | $loop->destroy(); 192 | 193 | # 该行代码会执行 194 | var_dump('123'); 195 | ``` 196 | 197 | --- 198 | -------------------------------------------------------------------------------- /src/Drivers/EventLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use EventConfig; 15 | use EventBase; 16 | use Event; 17 | use Closure; 18 | 19 | class EventLoop extends AbstractLoop 20 | { 21 | /** @var EventBase */ 22 | protected EventBase $_eventBase; 23 | 24 | /** @inheritDoc */ 25 | public function __construct() 26 | { 27 | parent::__construct(); 28 | $config = new EventConfig(); 29 | if (\DIRECTORY_SEPARATOR !== '\\') { 30 | $config->requireFeatures(EventConfig::FEATURE_FDS); 31 | } 32 | $this->_eventBase = new EventBase($config); 33 | } 34 | 35 | /** @inheritDoc */ 36 | public function getExtName(): string 37 | { 38 | return 'event'; 39 | } 40 | 41 | /** @inheritDoc */ 42 | public function hasExt(): bool 43 | { 44 | return extension_loaded($this->getExtName()); 45 | } 46 | 47 | /** @inheritDoc */ 48 | public function addReadStream($stream, Closure $handler): void 49 | { 50 | if(is_resource($stream) and !isset($this->_readFds[$key = (int)$stream])){ 51 | $event = new Event($this->_eventBase, $stream, Event::READ | Event::PERSIST, $handler); 52 | if ($event->add()) { 53 | $this->_reads[$key] = $event; 54 | $this->_readFds[$key] = $stream; 55 | } 56 | } 57 | } 58 | 59 | /** @inheritDoc */ 60 | public function delReadStream($stream): void 61 | { 62 | if(is_resource($stream) and !empty($this->_reads[$key = (int)$stream])){ 63 | /** @var Event $event */ 64 | $event = $this->_reads[$key]; 65 | $event->del(); 66 | unset( 67 | $this->_reads[$key], 68 | $this->_readFds[$key] 69 | ); 70 | } 71 | } 72 | 73 | /** @inheritDoc */ 74 | public function addWriteStream($stream, Closure $handler): void 75 | { 76 | if(is_resource($stream) and !isset($this->_writeFds[$key = (int)$stream])){ 77 | $event = new Event($this->_eventBase, $stream, Event::WRITE | Event::PERSIST, $handler); 78 | if ($event->add()) { 79 | $this->_writes[$key] = $event; 80 | $this->_writeFds[$key] = $stream; 81 | } 82 | } 83 | } 84 | 85 | /** @inheritDoc */ 86 | public function delWriteStream($stream): void 87 | { 88 | if(is_resource($stream) and isset($this->_writes[$key = (int)$stream])){ 89 | /** @var Event $event */ 90 | $event = $this->_writes[(int)$stream]; 91 | $event->del(); 92 | unset( 93 | $this->_writes[$key], 94 | $this->_writeFds[$key], 95 | $event 96 | ); 97 | } 98 | } 99 | 100 | /** @inheritDoc */ 101 | public function addSignal(int $signal, Closure $handler): void 102 | { 103 | if(!isset($this->_signals[$signal])){ 104 | $event = new Event($this->_eventBase, $signal, Event::SIGNAL | Event::PERSIST, $handler); 105 | if ($event->add()) { 106 | $this->_signals[$signal] = $event; 107 | } 108 | } 109 | } 110 | 111 | /** @inheritDoc */ 112 | public function delSignal(int $signal): void 113 | { 114 | if(isset($this->_signals[$signal])){ 115 | /** @var Event $event */ 116 | $event = $this->_signals[$signal]; 117 | $event->del(); 118 | unset($this->_signals[$signal], $event); 119 | } 120 | } 121 | 122 | /** @inheritDoc */ 123 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string 124 | { 125 | $event = new Event($this->_eventBase, -1, Event::TIMEOUT, function () use(&$event, $repeat, $handler){ 126 | if($this->getStorage()->exist($id = spl_object_hash($event))){ 127 | $repeat === false ? $this->delTimer($id) : $event->add($repeat); 128 | \call_user_func($handler); 129 | }else{ 130 | $event->free(); 131 | unset($event); 132 | } 133 | }); 134 | $event->add($delay); 135 | return $this->_storage->add(spl_object_hash($event), $event); 136 | } 137 | 138 | /** @inheritDoc */ 139 | public function delTimer(string $timerId): void 140 | { 141 | /** @var Event $event */ 142 | if($event = $this->_storage->get($timerId)){ 143 | $event->del(); 144 | $this->_storage->del($timerId); 145 | } 146 | } 147 | 148 | /** @inheritDoc */ 149 | public function run(): void 150 | { 151 | if($this->_storage->isEmpty() and !$this->_reads and !$this->_writes and !$this->_signals){ 152 | return; 153 | } 154 | if (\DIRECTORY_SEPARATOR !== '\\') { 155 | $this->_eventBase->loop(EventBase::STARTUP_IOCP); 156 | }else{ 157 | $this->_eventBase->loop(); 158 | } 159 | } 160 | 161 | /** @inheritDoc */ 162 | public function stop(): void 163 | { 164 | $this->_eventBase->stop(); 165 | } 166 | 167 | /** @inheritDoc */ 168 | public function destroy(): void 169 | { 170 | $this->stop(); 171 | $this->clear(); 172 | } 173 | } 174 | 175 | -------------------------------------------------------------------------------- /src/Drivers/OpenSwooleLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use Closure; 15 | use Swoole\Event; 16 | use Swoole\Process; 17 | use Swoole\Timer; 18 | use WorkBunny\EventLoop\Exception\InvalidArgumentException; 19 | use WorkBunny\EventLoop\Exception\LoopException; 20 | use WorkBunny\EventLoop\Timer as TimerObj; 21 | 22 | /** 23 | * @deprecated 24 | */ 25 | class OpenSwooleLoop extends AbstractLoop 26 | { 27 | 28 | /** @inheritDoc */ 29 | public function getExtName(): string 30 | { 31 | return 'openswoole'; 32 | } 33 | 34 | /** @inheritDoc */ 35 | public function hasExt(): bool 36 | { 37 | return extension_loaded($this->getExtName()); 38 | } 39 | 40 | /** @inheritDoc */ 41 | public function addReadStream($stream, Closure $handler): void 42 | { 43 | if(is_resource($stream) and !isset($this->_readFds[$key = (int)$stream])){ 44 | Event::add($stream,$handler,null,\SWOOLE_EVENT_READ); 45 | $this->_readFds[$key] = $stream; 46 | } 47 | } 48 | 49 | /** @inheritDoc */ 50 | public function delReadStream($stream): void 51 | { 52 | if( 53 | is_resource($stream) and 54 | isset($this->_readFds[$key = (int)$stream]) and 55 | Event::isset($stream,\SWOOLE_EVENT_READ) 56 | ){ 57 | Event::del($stream); 58 | unset($this->_readFds[$key]); 59 | } 60 | } 61 | 62 | /** @inheritDoc */ 63 | public function addWriteStream($stream, Closure $handler): void 64 | { 65 | if(is_resource($stream) and !isset($this->_writeFds[$key = (int)$stream])){ 66 | Event::add($stream,null,$handler,\SWOOLE_EVENT_WRITE); 67 | $this->_writeFds[$key] = $stream; 68 | } 69 | } 70 | 71 | /** @inheritDoc */ 72 | public function delWriteStream($stream): void 73 | { 74 | if( 75 | is_resource($stream) and 76 | isset($this->_writeFds[$key = (int)$stream]) and 77 | Event::isset($stream,\SWOOLE_EVENT_WRITE) 78 | ){ 79 | Event::del($stream); 80 | unset($this->_writeFds[$key]); 81 | } 82 | } 83 | 84 | /** @inheritDoc */ 85 | public function addSignal(int $signal, Closure $handler): void 86 | { 87 | if(!isset($this->_signals[$signal])){ 88 | $this->_signals[$signal] = $handler; 89 | Process::signal($signal, $handler); 90 | } 91 | } 92 | 93 | /** @inheritDoc */ 94 | public function delSignal(int $signal): void 95 | { 96 | if(isset($this->_signals[$signal])){ 97 | unset($this->_signals[$signal]); 98 | Process::signal($signal, function (){});# 模拟 SIG_IGN 99 | } 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | * @param float $delay 105 | * @param float|false $repeat 106 | * @param Closure $handler 107 | * @return string 108 | * @throws LoopException 109 | */ 110 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string 111 | { 112 | $timer = new TimerObj($delay, $repeat, $handler); 113 | $timerId = spl_object_hash($timer); 114 | $delay = $this->_floatToInt($delay); 115 | $repeat = $this->_floatToInt($repeat); 116 | $equals = ($delay === $repeat); 117 | $id = 0; 118 | 119 | if($repeat === 0){ 120 | if($equals){ 121 | Event::defer(function () use($timerId, $handler){ 122 | $handler(); 123 | $this->_storage->del($timerId); 124 | }); 125 | }else{ 126 | $id = Timer::after($delay, function () use($timerId, $handler){ 127 | $handler(); 128 | $this->_storage->del($timerId); 129 | }); 130 | } 131 | }else{ 132 | if($equals){ 133 | $id = Timer::tick($repeat, $handler); 134 | }else{ 135 | Event::defer(function() use($timerId, &$id, $repeat, $handler){ 136 | if($id = Timer::tick($repeat, $handler)){ 137 | $this->_storage->set($timerId, $id); 138 | } 139 | $handler(); 140 | }); 141 | } 142 | } 143 | return $this->_storage->add($timerId, (int)$id); 144 | } 145 | 146 | /** @inheritDoc */ 147 | public function delTimer(string $timerId): void 148 | { 149 | $id = $this->_storage->get($timerId); 150 | if($id !== 0){ 151 | Timer::clear($id); 152 | } 153 | $this->_storage->del($timerId); 154 | } 155 | 156 | /** @inheritDoc */ 157 | public function run(): void 158 | { 159 | Event::wait(); 160 | } 161 | 162 | /** @inheritDoc */ 163 | public function stop(): void 164 | { 165 | Event::exit(); 166 | } 167 | 168 | /** @inheritDoc */ 169 | public function destroy(): void 170 | { 171 | $this->stop(); 172 | $this->clear(); 173 | } 174 | 175 | /** 获取小数点位数 */ 176 | protected function _floatToInt(float|false $float): int|false 177 | { 178 | if($float === false){ 179 | return false; 180 | } 181 | $float = $float * 1000; 182 | if($float < 0.0){ 183 | throw new InvalidArgumentException('Minimum support 0.001'); 184 | } 185 | return (int)($float); 186 | } 187 | } 188 | 189 | -------------------------------------------------------------------------------- /src/Drivers/EvLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use Ev; 15 | use EvIo; 16 | use EvSignal; 17 | use EvTimer; 18 | use EvLoop as BaseEvLoop; 19 | use Closure; 20 | 21 | class EvLoop extends AbstractLoop 22 | { 23 | /** @var BaseEvLoop loop */ 24 | protected BaseEvLoop $_loop; 25 | 26 | /** @inheritDoc */ 27 | public function __construct() 28 | { 29 | parent::__construct(); 30 | 31 | $this->_loop = new BaseEvLoop(); 32 | } 33 | 34 | /** @inheritDoc */ 35 | public function getExtName(): string 36 | { 37 | return 'ev'; 38 | } 39 | 40 | /** @inheritDoc */ 41 | public function hasExt(): bool 42 | { 43 | return extension_loaded($this->getExtName()); 44 | } 45 | 46 | /** @inheritDoc */ 47 | public function addReadStream($stream, Closure $handler): void 48 | { 49 | if(is_resource($stream) and !isset($this->_reads[$key = (int)$stream])){ 50 | $event = $this->_loop->io($stream, Ev::READ, $handler); 51 | $this->_reads[$key] = $event; 52 | $this->_readFds[spl_object_hash($event)] = $stream; 53 | } 54 | } 55 | 56 | /** 57 | * @param resource|EvIo $stream 58 | * @return void 59 | */ 60 | public function delReadStream($stream): void 61 | { 62 | if(is_resource($stream) and isset($this->_reads[$key = (int)$stream])){ 63 | /** @var EvIo $event */ 64 | $event = $this->_reads[$key]; 65 | $event->stop(); 66 | unset( 67 | $this->_reads[$key], 68 | $this->_readFds[spl_object_hash($event)] 69 | ); 70 | } 71 | 72 | if($stream instanceof EvIo and isset($this->_readFds[spl_object_hash($stream)])){ 73 | $stream->stop(); 74 | $key = (int)($this->_readFds[spl_object_hash($stream)]); 75 | unset( 76 | $this->_reads[$key], 77 | $this->_readFds[spl_object_hash($stream)] 78 | ); 79 | } 80 | } 81 | 82 | /** @inheritDoc */ 83 | public function addWriteStream($stream, Closure $handler): void 84 | { 85 | if(is_resource($stream) and !isset($this->_writes[$key = (int)$stream])){ 86 | $event = $this->_loop->io($stream, Ev::WRITE, $handler); 87 | $this->_writes[$key] = $event; 88 | $this->_writeFds[spl_object_hash($event)] = $stream; 89 | } 90 | } 91 | 92 | /** 93 | * @param EvIo|resource $stream 94 | * @return void 95 | */ 96 | public function delWriteStream($stream): void 97 | { 98 | if(is_resource($stream) and isset($this->_writes[$key = (int)$stream])){ 99 | /** @var EvIo $event */ 100 | $event = $this->_writes[$key]; 101 | $event->stop(); 102 | unset( 103 | $this->_writes[$key], 104 | $this->_writeFds[spl_object_hash($event)] 105 | ); 106 | } 107 | 108 | if($stream instanceof EvIo and isset($this->_writeFds[spl_object_hash($stream)])){ 109 | $stream->stop(); 110 | $key = (int)($this->_writeFds[spl_object_hash($stream)]); 111 | unset( 112 | $this->_writes[$key], 113 | $this->_writeFds[spl_object_hash($stream)] 114 | ); 115 | } 116 | } 117 | 118 | /** @inheritDoc */ 119 | public function addSignal(int $signal, Closure $handler): void 120 | { 121 | if(!isset($this->_signals[$signal])){ 122 | $event = $this->_loop->signal($signal, function (\EvSignal $evSignal, int $revents) use (&$handler){ 123 | \call_user_func($handler, $evSignal->signum); 124 | }); 125 | $this->_signals[$signal] = $event; 126 | } 127 | } 128 | 129 | /** @inheritDoc */ 130 | public function delSignal(int $signal): void 131 | { 132 | if(isset($this->_signals[$signal])){ 133 | /** @var EvSignal $event */ 134 | $event = $this->_signals[$signal]; 135 | $event->stop(); 136 | unset($this->_signals[$signal]); 137 | } 138 | } 139 | 140 | /** @inheritDoc */ 141 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string 142 | { 143 | /** @var EvTimer $event */ 144 | $event = $this->_loop->timer($delay, $repeat !== false ? $repeat : 0.0, $func = function () use (&$event, &$func, $repeat, $handler) { 145 | if($this->getStorage()->exist($timerId = spl_object_hash($event))){ 146 | \call_user_func($handler); 147 | if($repeat === 0.0){ 148 | $event->start(); 149 | } 150 | if($repeat === false){ 151 | $this->delTimer($timerId); 152 | } 153 | }else{ 154 | $event->clear(); 155 | unset($event); 156 | } 157 | 158 | }); 159 | return $this->_storage->add(spl_object_hash($event), $event); 160 | } 161 | 162 | /** @inheritDoc */ 163 | public function delTimer(string $timerId): void 164 | { 165 | /** @var EvTimer $event */ 166 | if($event = $this->_storage->get($timerId)){ 167 | $event->stop(); 168 | $this->_storage->del($timerId); 169 | } 170 | } 171 | 172 | /** @inheritDoc */ 173 | public function run(): void 174 | { 175 | if($this->_storage->isEmpty() and !$this->_reads and !$this->_writes and !$this->_signals){ 176 | return; 177 | } 178 | $this->_loop->run(); 179 | } 180 | 181 | /** @inheritDoc */ 182 | public function stop(): void 183 | { 184 | $this->_loop->stop(); 185 | } 186 | 187 | /** @inheritDoc */ 188 | public function destroy(): void 189 | { 190 | $this->stop(); 191 | $this->clear(); 192 | } 193 | } -------------------------------------------------------------------------------- /src/Drivers/NativeLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use WorkBunny\EventLoop\Timer; 15 | use SplPriorityQueue; 16 | use Closure; 17 | 18 | class NativeLoop extends AbstractLoop 19 | { 20 | /** @var SplPriorityQueue 优先队列 */ 21 | protected SplPriorityQueue $_queue; 22 | 23 | /** @var SplPriorityQueue */ 24 | protected SplPriorityQueue $_cacheQueue; 25 | 26 | /** @var bool */ 27 | protected bool $_stopped = false; 28 | 29 | /** @inheritDoc */ 30 | public function __construct() 31 | { 32 | parent::__construct(); 33 | 34 | $this->_queue = new SplPriorityQueue(); 35 | $this->_cacheQueue = new SplPriorityQueue(); 36 | $this->_queue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 37 | $this->_cacheQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 38 | $this->_readFds = []; 39 | $this->_writeFds = []; 40 | } 41 | 42 | /** @inheritDoc */ 43 | public function getExtName(): string 44 | { 45 | return 'pcntl'; 46 | } 47 | 48 | /** @inheritDoc */ 49 | public function hasExt(): bool 50 | { 51 | return extension_loaded($this->getExtName()); 52 | } 53 | 54 | /** @inheritDoc */ 55 | public function addReadStream($stream, Closure $handler): void 56 | { 57 | if(is_resource($stream) and !isset($this->_readFds[$key = (int)$stream])){ 58 | $this->_readFds[$key] = $stream; 59 | $this->_reads[$key] = $handler; 60 | } 61 | } 62 | 63 | /** @inheritDoc */ 64 | public function delReadStream($stream): void 65 | { 66 | if(is_resource($stream) and isset($this->_readFds[$key = (int)$stream])){ 67 | unset( 68 | $this->_reads[$key], 69 | $this->_readFds[$key] 70 | ); 71 | } 72 | } 73 | 74 | /** @inheritDoc */ 75 | public function addWriteStream($stream, Closure $handler): void 76 | { 77 | if(is_resource($stream) and !isset($this->_writeFds[$key = (int) $stream])){ 78 | $this->_writeFds[$key] = $stream; 79 | $this->_writes[$key] = $handler; 80 | } 81 | } 82 | 83 | /** @inheritDoc */ 84 | public function delWriteStream($stream): void 85 | { 86 | if(is_resource($stream) and isset($this->_writeFds[$key = (int)$stream])){ 87 | unset( 88 | $this->_writes[$key], 89 | $this->_writeFds[$key] 90 | ); 91 | } 92 | } 93 | 94 | /** @inheritDoc */ 95 | public function addSignal(int $signal, Closure $handler): void 96 | { 97 | if(!isset($this->_signals[$signal])){ 98 | $this->_signals[$signal] = $handler; 99 | \pcntl_signal($signal, function($signal){ 100 | $this->_signals[$signal]($signal); 101 | }); 102 | } 103 | } 104 | 105 | /** @inheritDoc */ 106 | public function delSignal(int $signal): void 107 | { 108 | if(isset($this->_signals[$signal])){ 109 | unset($this->_signals[$signal]); 110 | \pcntl_signal($signal, \SIG_IGN); 111 | } 112 | } 113 | 114 | /** @inheritDoc */ 115 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string 116 | { 117 | $timer = new Timer($delay, $repeat, $handler); 118 | $runTime = \hrtime(true) * 1e-9 + $delay; 119 | $this->_queue->insert($id = spl_object_hash($timer), -$runTime); 120 | return $this->_storage->add($id, $timer); 121 | } 122 | 123 | /** @inheritDoc */ 124 | public function delTimer(string $timerId): void 125 | { 126 | $this->_storage->del($timerId); 127 | } 128 | 129 | /** @inheritDoc */ 130 | public function run(): void 131 | { 132 | $this->_stopped = false; 133 | while (!$this->_stopped) { 134 | if(!$this->_readFds and !$this->_writeFds and !$this->_signals and $this->_storage->isEmpty()){ 135 | break; 136 | } 137 | \pcntl_signal_dispatch(); 138 | $writes = $this->_writeFds; 139 | $reads = $this->_readFds; 140 | $excepts = []; 141 | foreach ($writes as $key => $socket) { 142 | if (!isset($reads[$key]) && @\ftell($socket) === 0) { 143 | $excepts[$key] = $socket; 144 | } 145 | } 146 | if($writes or $reads or $excepts){ 147 | try { 148 | @\stream_select($reads, $writes, $excepts, 0,0); 149 | } catch (\Throwable $e) {} 150 | 151 | foreach ($reads as $stream) { 152 | $key = (int)$stream; 153 | if (isset($this->_reads[$key])) { 154 | ($this->_reads[$key])($stream); 155 | } 156 | } 157 | foreach ($writes as $stream) { 158 | $key = (int)$stream; 159 | if (isset($this->_writes[$key])) { 160 | ($this->_writes[$key])($stream); 161 | } 162 | } 163 | } 164 | $this->_tick(); 165 | } 166 | } 167 | 168 | /** @inheritDoc */ 169 | public function stop(): void 170 | { 171 | $this->_stopped = true; 172 | } 173 | 174 | /** @inheritDoc */ 175 | public function destroy(): void 176 | { 177 | $this->stop(); 178 | $this->clear(); 179 | } 180 | 181 | /** @inheritDoc */ 182 | public function clear(): void 183 | { 184 | parent::clear(); 185 | $this->_queue = new SplPriorityQueue(); 186 | $this->_cacheQueue = new SplPriorityQueue(); 187 | $this->_queue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 188 | $this->_cacheQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 189 | } 190 | 191 | /** 执行 */ 192 | protected function _tick(): void 193 | { 194 | foreach ($this->_queue as $item) { 195 | $runTime = -$item['priority']; 196 | $timerId = $item['data']; 197 | /** @var Timer $data */ 198 | if($data = $this->_storage->get($timerId)){ 199 | $repeat = $data->getRepeat(); 200 | $callback = $data->getHandler(); 201 | $timeNow = \hrtime(true) * 1e-9; 202 | if (($runTime - $timeNow) <= 0) { 203 | \call_user_func($callback); 204 | if($repeat !== false){ 205 | $nextTime = \hrtime(true) * 1e-9 + $repeat; 206 | $this->_cacheQueue->insert($timerId, -$nextTime); 207 | }else{ 208 | $this->delTimer($timerId); 209 | } 210 | }else{ 211 | $this->_cacheQueue->insert($timerId, -$runTime); 212 | } 213 | } 214 | } 215 | foreach ($this->_cacheQueue as $item){ 216 | $priority = $item['priority']; 217 | $timerId = $item['data']; 218 | $this->_queue->insert($timerId, $priority); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Drivers/SwowLoop.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/event-loop 10 | * @license https://github.com/workbunny/event-loop/blob/main/LICENSE 11 | */ 12 | namespace WorkBunny\EventLoop\Drivers; 13 | 14 | use Closure; 15 | use Swow\Coroutine; 16 | use Swow\Signal; 17 | use Swow\SignalException; 18 | use Swow\Sync\WaitGroup; 19 | use WorkBunny\EventLoop\Exception\InvalidArgumentException; 20 | use function Swow\Sync\waitAll; 21 | use function msleep; 22 | 23 | class SwowLoop extends AbstractLoop 24 | { 25 | 26 | /** @var bool */ 27 | protected bool $_stopped = false; 28 | 29 | /** @inheritDoc */ 30 | public function getExtName(): string 31 | { 32 | return 'swow'; 33 | } 34 | 35 | /** @inheritDoc */ 36 | public function hasExt(): bool 37 | { 38 | return extension_loaded($this->getExtName()); 39 | } 40 | 41 | /** @inheritDoc */ 42 | public function addReadStream($stream, Closure $handler): void 43 | { 44 | if(\is_resource($stream) and !isset($this->_readFds[$key = (int)$stream])){ 45 | $this->_reads[$key] = null; 46 | $this->_readFds[$key] = $stream; 47 | Coroutine::run(function () use ($handler, $key): void { 48 | try { 49 | $this->_reads[$key] = Coroutine::getCurrent(); 50 | while (!$this->_stopped) { 51 | if (!isset($this->_readFds[$key])) { 52 | break; 53 | } 54 | if ($this->_reads[$key] === null) { 55 | continue; 56 | } 57 | $event = stream_poll_one($stream = $this->_readFds[$key], STREAM_POLLIN | STREAM_POLLHUP); 58 | 59 | if ($event !== STREAM_POLLNONE) { 60 | \call_user_func($handler, $stream); 61 | } 62 | if ($event !== STREAM_POLLIN) { 63 | $this->delReadStream($stream); 64 | break; 65 | } 66 | } 67 | } catch (\RuntimeException) { 68 | $this->delReadStream($stream); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | /** @inheritDoc */ 75 | public function delReadStream($stream): void 76 | { 77 | if( 78 | \is_resource($stream) and 79 | isset($this->_readFds[$key = (int)$stream]) and 80 | isset($this->_reads[$key]) 81 | ){ 82 | unset($this->_readFds[$key], $this->_reads[$key]); 83 | } 84 | } 85 | 86 | /** @inheritDoc */ 87 | public function addWriteStream($stream, Closure $handler): void 88 | { 89 | if(\is_resource($stream) and !isset($this->_writeFds[$key = (int)$stream])) { 90 | $this->_writes[$key] = null; 91 | $this->_writeFds[$key] = $stream; 92 | Coroutine::run(function () use ($handler, $key): void { 93 | try { 94 | $this->_writes[$key] = Coroutine::getCurrent(); 95 | while (!$this->_stopped) { 96 | if (!isset($this->_writeFds[$key])) { 97 | break; 98 | } 99 | if ($this->_writes[$key] === null) { 100 | continue; 101 | } 102 | $event = stream_poll_one($stream = $this->_writeFds[$key], STREAM_POLLOUT | STREAM_POLLHUP); 103 | 104 | if ($event !== STREAM_POLLNONE) { 105 | \call_user_func($handler, $stream); 106 | } 107 | if ($event !== STREAM_POLLOUT) { 108 | $this->delWriteStream($stream); 109 | break; 110 | } 111 | } 112 | } catch (\RuntimeException) { 113 | $this->delWriteStream($stream); 114 | } 115 | }); 116 | } 117 | } 118 | 119 | /** @inheritDoc */ 120 | public function delWriteStream($stream): void 121 | { 122 | if( 123 | \is_resource($stream) and 124 | isset($this->_writeFds[$key = (int)$stream]) and 125 | isset($this->_writes[$key]) 126 | ){ 127 | unset($this->_writeFds[$key], $this->_writes[$key]); 128 | } 129 | } 130 | 131 | /** @inheritDoc */ 132 | public function addSignal(int $signal, Closure $handler): void 133 | { 134 | if(!isset($this->_signals[$signal])){ 135 | // 占位 136 | $this->_signals[$signal] = null; 137 | Coroutine::run(function () use ($signal, $handler): void { 138 | $this->_signals[$signal] = Coroutine::getCurrent(); 139 | while (!$this->_stopped) { 140 | try { 141 | Signal::wait($signal); 142 | if (!isset($this->_signals[$signal])) { 143 | break; 144 | } 145 | if ($this->_signals[$signal] === null) { 146 | continue; 147 | } 148 | \call_user_func($handler, $signal); 149 | } catch (SignalException) {} 150 | } 151 | }); 152 | } 153 | } 154 | 155 | /** @inheritDoc */ 156 | public function delSignal(int $signal): void 157 | { 158 | if(isset($this->_signals[$signal])){ 159 | unset($this->_signals[$signal]); 160 | } 161 | } 162 | 163 | /** @inheritDoc */ 164 | public function addTimer(float $delay, float|false $repeat, Closure $handler): string 165 | { 166 | $delay = $this->_floatToInt($delay); 167 | $repeat = $this->_floatToInt($repeat); 168 | $coroutine = Coroutine::run(function () use ($delay, $repeat, $handler): void { 169 | $first = true; 170 | while (!$this->_stopped) { 171 | if($repeat === false){ 172 | $this->_storage->del(spl_object_hash(Coroutine::getCurrent())); 173 | break; 174 | } 175 | if($first){ 176 | msleep($delay); 177 | }else{ 178 | msleep($repeat); 179 | } 180 | \call_user_func($handler); 181 | $first = false; 182 | } 183 | }); 184 | return $this->_storage->add(spl_object_hash($coroutine), $coroutine->getId()); 185 | } 186 | 187 | /** @inheritDoc */ 188 | public function delTimer(string $timerId): void 189 | { 190 | $id = $this->_storage->get($timerId); 191 | if($id !== null){ 192 | Coroutine::get($id)->kill(); 193 | } 194 | $this->_storage->del($timerId); 195 | } 196 | 197 | /** @inheritDoc */ 198 | public function run(): void 199 | { 200 | $this->_stopped = false; 201 | waitAll(); 202 | } 203 | 204 | /** @inheritDoc */ 205 | public function stop(): void 206 | { 207 | $this->_stopped = true; 208 | Coroutine::killAll(); 209 | } 210 | 211 | /** @inheritDoc */ 212 | public function destroy(): void 213 | { 214 | $this->stop(); 215 | $this->clear(); 216 | } 217 | 218 | /** 获取小数点位数 */ 219 | protected function _floatToInt(float|false $float): int|false 220 | { 221 | if($float === false){ 222 | return false; 223 | } 224 | $float = $float * 1000; 225 | if($float < 0.0){ 226 | throw new InvalidArgumentException('Minimum support 0.001'); 227 | } 228 | return (int)($float); 229 | } 230 | } 231 | 232 | -------------------------------------------------------------------------------- /tests/UnitTests/EvLoopTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('ExtEvLoop tests skipped because ext-ev extension is not installed.'); 17 | } 18 | 19 | return new EvLoop(); 20 | } 21 | 22 | /** @inheritDoc */ 23 | public function setTickTimeout(): float 24 | { 25 | return 0.02; 26 | } 27 | 28 | 29 | public function testTimerPriority(): void 30 | { 31 | $this->markTestSkipped('TimerPriority of the EvLoop does not apply to this test.'); 32 | } 33 | 34 | public function testDelayTimerPriority(): void 35 | { 36 | $this->markTestSkipped('DelayTimerPriority of the EvLoop does not apply to this test.'); 37 | } 38 | 39 | /** 40 | * @see AbstractTestCase::testReadStreamBeforeTimer() 41 | * @dataProvider provider 42 | * @param bool $bio 43 | * @return void 44 | */ 45 | public function testReadStreamBeforeTimer(bool $bio): void 46 | { 47 | $this->markTestSkipped('The priority of the EvLoop does not apply to this test.'); 48 | } 49 | 50 | /** 51 | * @see AbstractTestCase::testWriteStreamBeforeTimer() 52 | * @dataProvider provider 53 | * @param bool $bio 54 | * @return void 55 | */ 56 | public function testWriteStreamBeforeTimer(bool $bio): void 57 | { 58 | $this->markTestSkipped('The priority of the EvLoop does not apply to this test.'); 59 | } 60 | 61 | /** 62 | * @see AbstractTestCase::testReadStreamBeforeTimer() 与之相反 63 | * @dataProvider provider 64 | * @param bool $bio 65 | * @return void 66 | */ 67 | public function testReadStreamAfterTimer(bool $bio): void 68 | { 69 | list ($input, $output) = \stream_socket_pair( 70 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 71 | STREAM_SOCK_STREAM, 72 | STREAM_IPPROTO_IP 73 | ); 74 | stream_set_blocking($input, $bio); 75 | stream_set_blocking($output, $bio); 76 | 77 | fwrite($output, 'foo' . PHP_EOL); 78 | 79 | $string = ''; 80 | 81 | $this->getLoop()->addTimer(0.0,false, function () use (&$string){ 82 | $string .= 'timer' . PHP_EOL; 83 | }); 84 | 85 | $this->getLoop()->addReadStream($input, function() use(&$string){ 86 | $string .= 'read' . PHP_EOL; 87 | }); 88 | 89 | $this->tickLoop(); 90 | 91 | $this->assertEquals('timer' . PHP_EOL . 'read' . PHP_EOL, $string); 92 | } 93 | 94 | /** 95 | * @see AbstractTestCase::testWriteStreamBeforeTimer() 与之相反 96 | * @dataProvider provider 97 | * @param bool $bio 98 | * @return void 99 | */ 100 | public function testWriteStreamAfterTimer(bool $bio): void 101 | { 102 | 103 | list ($input, $output) = \stream_socket_pair( 104 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 105 | STREAM_SOCK_STREAM, 106 | STREAM_IPPROTO_IP 107 | ); 108 | stream_set_blocking($output, $bio); 109 | fwrite($output, 'foo' . PHP_EOL); 110 | 111 | $string = ''; 112 | 113 | $this->getLoop()->addTimer(0.0,false, function () use (&$string){ 114 | $string .= 'timer' . PHP_EOL; 115 | }); 116 | 117 | $this->getLoop()->addWriteStream($output, function() use(&$string){ 118 | $string .= 'write' . PHP_EOL; 119 | }); 120 | 121 | $this->tickLoop(); 122 | 123 | $this->assertEquals('timer' . PHP_EOL . 'write' . PHP_EOL, $string); 124 | } 125 | 126 | /** 127 | * @param bool $bio 128 | * @return void 129 | *@see StreamsUnit::testReadStreamHandlerReceivesDataFromStreamReference() 130 | * @dataProvider provider 131 | */ 132 | public function testReadStreamHandlerReceivesDataFromStreamReference(bool $bio): void 133 | { 134 | list ($input, $output) = \stream_socket_pair( 135 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 136 | STREAM_SOCK_STREAM, 137 | STREAM_IPPROTO_IP 138 | ); 139 | stream_set_blocking($input, $bio); 140 | stream_set_blocking($output, $bio); 141 | 142 | $this->received = ''; 143 | fwrite($input, 'hello'); 144 | fclose($input); 145 | 146 | $this->getLoop()->addReadStream($output, function (EvIo $io) { 147 | $output = $io->fd; 148 | $chunk = fread($output, 1024); 149 | if ($chunk === '') { 150 | $this->received .= 'X'; 151 | $this->getLoop()->delReadStream($io); 152 | fclose($output); 153 | $this->getLoop()->destroy(); 154 | } else { 155 | $this->received .= '[' . $chunk . ']'; 156 | } 157 | 158 | }); 159 | $this->assertEquals('', $this->received); 160 | 161 | $this->assertRunFasterThan($this->tickTimeout * 2); 162 | 163 | $this->assertEquals('[hello]X', $this->received); 164 | } 165 | 166 | /** 167 | * @param bool $bio 168 | * @return void 169 | *@see StreamsUnit::testRemoveReadStreams() 170 | * @dataProvider provider 171 | */ 172 | public function testRemoveReadStreams(bool $bio): void 173 | { 174 | list ($input1, $output1) = \stream_socket_pair( 175 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 176 | STREAM_SOCK_STREAM, 177 | STREAM_IPPROTO_IP 178 | ); 179 | list ($input2, $output2) = \stream_socket_pair( 180 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 181 | STREAM_SOCK_STREAM, 182 | STREAM_IPPROTO_IP 183 | ); 184 | 185 | stream_set_blocking($input1, $bio); 186 | stream_set_blocking($input2, $bio); 187 | stream_set_blocking($output1, $bio); 188 | stream_set_blocking($output2, $bio); 189 | 190 | 191 | $this->getLoop()->addReadStream($input1, function (EvIo $io) { 192 | $this->getLoop()->delReadStream($io); 193 | }); 194 | 195 | $this->getLoop()->addReadStream($input2, function (EvIo $io) { 196 | $this->getLoop()->delReadStream($io); 197 | }); 198 | 199 | fwrite($output1, "foo1\n"); 200 | fwrite($output2, "foo2\n"); 201 | 202 | $this->tickLoop($this->tickTimeout); 203 | 204 | $this->assertCount(0, $this->getLoop()->getReadFds()); 205 | $this->assertCount(0, $this->getLoop()->getReads()); 206 | } 207 | 208 | /** 209 | * @param bool $bio 210 | * @return void 211 | *@see StreamsUnit::testRemoveWriteStreams() 212 | * @dataProvider provider 213 | */ 214 | public function testRemoveWriteStreams(bool $bio): void 215 | { 216 | list ($input1) = \stream_socket_pair( 217 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 218 | STREAM_SOCK_STREAM, 219 | STREAM_IPPROTO_IP 220 | ); 221 | list ($input2) = \stream_socket_pair( 222 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 223 | STREAM_SOCK_STREAM, 224 | STREAM_IPPROTO_IP 225 | ); 226 | 227 | stream_set_blocking($input1, $bio); 228 | stream_set_blocking($input2, $bio); 229 | 230 | $this->getLoop()->addWriteStream($input1, function (EvIo $io) { 231 | $this->getLoop()->delWriteStream($io); 232 | }); 233 | 234 | $this->getLoop()->addWriteStream($input2, function (EvIo $io) { 235 | $this->getLoop()->delWriteStream($io); 236 | }); 237 | 238 | $this->tickLoop($this->tickTimeout); 239 | 240 | $this->assertCount(0, $this->getLoop()->getWriteFds()); 241 | $this->assertCount(0, $this->getLoop()->getWrites()); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tests/UnitTests/Units/TimerUnit.php: -------------------------------------------------------------------------------- 1 | assertEquals(0, $this->getCountNum()); 21 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 22 | // create @Future 23 | $this->getLoop()->addTimer(0.0, false, function () { 24 | $this->setCountNum($this->getCountNum() + 1); 25 | }); 26 | // before loop start 27 | $this->assertEquals(0, $this->getCountNum()); 28 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 29 | // loop start 30 | $this->tickLoop($this->tickTimeout); 31 | 32 | $this->assertNotEquals(0.0, $startTime = $this->getStartTime()); 33 | $this->assertNotEquals(0.0, $endTime = $this->getEndTime()); 34 | // less or equal 1ms 35 | $this->assertLessThan($this->tickTimeout, $endTime - $startTime); 36 | $this->assertEquals(1, $this->getCountNum()); 37 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 38 | } 39 | 40 | /** 41 | * 测试@ReFuture 的创建及销毁 42 | * More Tag @see LoopInterface::addTimer() 43 | * 44 | * @return void 45 | */ 46 | public function testReFuture(): void 47 | { 48 | // before create @ReFuture 49 | $this->assertEquals(0, $this->getCountNum()); 50 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 51 | // create @ReFuture 52 | $timerId = $this->getLoop()->addTimer(0.0, 0.0, function () use (&$timerId) { 53 | $this->setCountNum($count = ($this->getCountNum() + 1)); 54 | 55 | if($count === 1){ 56 | $this->addInfo($count, $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 57 | } 58 | if($count === 2){ 59 | $this->addInfo($count, $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 60 | // del timer 61 | $this->getLoop()->delTimer($timerId); 62 | } 63 | }); 64 | // before loop run 65 | $this->assertEquals(0, $this->getCountNum()); 66 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 67 | // loop tick run 68 | $this->tickLoop($this->tickTimeout); 69 | // after loop run 70 | $this->assertNotEquals(0.0, $startTime = $this->getStartTime()); 71 | $this->assertNotEquals(0.0, $endTime = $this->getEndTime()); 72 | // assert info 73 | $this->assertInfo(); 74 | // less or equal 1000ms 75 | $this->assertLessThan($this->tickTimeout, $endTime - $startTime); 76 | $this->assertEquals(2, $this->getCountNum()); 77 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 78 | } 79 | 80 | /** 81 | * 测试@DelayReFuture 的创建及销毁 82 | * More Tag @see LoopInterface::addTimer() 83 | * 84 | * @return void 85 | */ 86 | public function testDelayReFuture(): void 87 | { 88 | $this->assertEquals(0, $this->getCountNum()); 89 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 90 | // create @DelayReFuture 91 | $timerId = $this->getLoop()->addTimer(1.0, 0.0, function () use (&$timerId){ 92 | $this->setCountNum($count = ($this->getCountNum() + 1)); 93 | if($count === 1 or $count === 2 or $count === 3){ 94 | $this->addInfo($count, 1.0 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 95 | } 96 | // del timer 97 | if($count >= 3){ 98 | $this->getLoop()->delTimer($timerId); 99 | } 100 | }); 101 | // before loop run 102 | $this->assertEquals(0, $this->getCountNum()); 103 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 104 | // loop tick run 105 | $this->tickLoop(1.0 + $this->tickTimeout); 106 | // after loop run 107 | $this->assertNotNull($startTime = $this->getStartTime()); 108 | $this->assertNotNull($endTime = $this->getEndTime()); 109 | // assert info 110 | $this->assertInfo(); 111 | // less or equal 2000ms 112 | $this->assertLessThan(1.0 + $this->tickTimeout, $endTime - $startTime); 113 | $this->assertEquals(3, $this->getCountNum()); 114 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 115 | } 116 | 117 | /** 118 | * 测试@Delayer 的创建及自动销毁 119 | * More Tage @see LoopInterface::addTimer() 120 | * 121 | * @return void 122 | */ 123 | public function testDelayer(): void 124 | { 125 | $this->assertEquals(0, $this->getCountNum()); 126 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 127 | // create @Delayer 128 | $this->getLoop()->addTimer(1.0, false, function () { 129 | $this->setCountNum($this->getCountNum() + 1); 130 | }); 131 | // before loop run 132 | $this->assertEquals(0, $this->getCountNum(), 'Before Loop. '); 133 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 134 | // tick loop 135 | $this->tickLoop(1.0 + $this->tickTimeout); 136 | // after loop run 137 | $this->assertNotNull($startTime = $this->getStartTime()); 138 | $this->assertNotNull($endTime = $this->getEndTime()); 139 | // less or equal 2000ms 140 | $this->assertLessThan(1.0 + $this->tickTimeout, $endTime - $startTime); 141 | $this->assertEquals(1, $this->getCountNum()); 142 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 143 | } 144 | 145 | /** 146 | * 测试@Timer 的创建及销毁 147 | * More Tag @see LoopInterface::addTimer() 148 | * 149 | * @return void 150 | */ 151 | public function testTimer(): void 152 | { 153 | $this->assertEquals(0, $this->getCountNum()); 154 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 155 | // create @Timer 156 | $timerId = $this->getLoop()->addTimer(0.0, 0.2, function () use (&$timerId){ 157 | $this->setCountNum($count = ($this->getCountNum() + 1)); 158 | 159 | if($count === 1){ 160 | $this->addInfo($count, $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 161 | } 162 | if($count === 2){ 163 | $this->addInfo($count, 0.2 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 164 | } 165 | if($count === 3){ 166 | $this->addInfo($count, 0.2 * 2 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 167 | } 168 | if($count >= 3){ 169 | $this->getLoop()->delTimer($timerId); 170 | } 171 | }); 172 | // before loop run 173 | $this->assertEquals(0, $this->getCountNum()); 174 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 175 | // loop run 176 | $this->tickLoop(0.2 * 2 + $this->tickTimeout); 177 | // after loop run 178 | $this->assertNotNull($startTime = $this->getStartTime()); 179 | $this->assertNotNull($endTime = $this->getEndTime()); 180 | // assert info 181 | $this->assertInfo(); 182 | $this->assertLessThan(0.2 * 2 + $this->tickTimeout, $endTime - $startTime); 183 | $this->assertEquals(3, $this->getCountNum()); 184 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 185 | } 186 | 187 | /** 188 | * 测试@DelayTimer 的创建及销毁 189 | * More Tag @see LoopInterface::addTimer() 190 | * 191 | * @return void 192 | */ 193 | public function testDelayTimer(): void 194 | { 195 | $this->assertEquals(0, $this->getCountNum()); 196 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 197 | // create @DelayTimer 198 | $timerId = $this->getLoop()->addTimer(0.2, 0.1, function () use (&$timerId){ 199 | $this->setCountNum($count = ($this->getCountNum() + 1)); 200 | if($count === 1){ 201 | $this->addInfo($count, 0.2 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 202 | } 203 | if($count === 2){ 204 | $this->addInfo($count, 0.2 + 0.1 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 205 | } 206 | if($count === 3){ 207 | $this->addInfo($count, 0.2 + 0.1 + 0.1 + $this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 208 | $this->getLoop()->delTimer($timerId); 209 | } 210 | }); 211 | // before loop run 212 | $this->assertEquals(0, $this->getCountNum()); 213 | $this->assertEquals(1, $this->getLoop()->getStorage()->count()); 214 | // tick run 215 | $this->tickLoop( 0.2 + 0.1 + 0.1 + $this->tickTimeout); 216 | // after loop run 217 | $this->assertNotNull($startTime = $this->getStartTime()); 218 | $this->assertNotNull($endTime = $this->getEndTime()); 219 | // assert info 220 | $this->assertInfo(); 221 | $this->assertLessThan(0.2 + 0.1 + 0.1 + $this->tickTimeout, $endTime - $startTime); 222 | $this->assertEquals(3, $this->getCountNum()); 223 | $this->assertEquals(0, $this->getLoop()->getStorage()->count()); 224 | } 225 | 226 | /** 227 | * 移除一个不存在的定时器 228 | * 229 | * @return void 230 | */ 231 | public function testRemoveNonExistingTimer(): void 232 | { 233 | $this->getLoop()->delTimer('test'); 234 | $this->tickLoop(); 235 | $this->assertLessThan($this->tickTimeout, $this->getEndTime() - $this->getStartTime()); 236 | } 237 | 238 | /** 239 | * 测试@Future的优先级 240 | * More Tag @see LoopInterface::addTimer() 241 | * 242 | * @return void 243 | */ 244 | public function testFuturePriority(): void 245 | { 246 | $this->expectOutputString('@Future-1' . PHP_EOL . '@Future-2' . PHP_EOL); 247 | 248 | $this->getLoop()->addTimer(0.0, false, function () { 249 | echo '@Future-1' . PHP_EOL; 250 | }); 251 | $this->getLoop()->addTimer(0.0, false, function () { 252 | echo '@Future-2' . PHP_EOL; 253 | }); 254 | 255 | $this->tickLoop($this->tickTimeout); 256 | } 257 | 258 | /** 259 | * 测试@ReFuture的优先级 260 | * More Tag @see LoopInterface::addTimer() 261 | * 262 | * @return void 263 | */ 264 | public function testReFuturePriority(): void 265 | { 266 | $this->expectOutputString( 267 | '@ReFuture-1' . PHP_EOL . 268 | '@ReFuture-2' . PHP_EOL . 269 | '@ReFuture-1' . PHP_EOL . 270 | '@ReFuture-2' . PHP_EOL 271 | ); 272 | $reFuture1Count = 0; 273 | $reFuture2Count = 0; 274 | $reFuture1 = $this->getLoop()->addTimer(0.0, 0.0, function () use(&$reFuture1, &$reFuture1Count) { 275 | $reFuture1Count ++; 276 | echo '@ReFuture-1' . PHP_EOL; 277 | if($reFuture1Count === 2) { 278 | $this->getLoop()->delTimer($reFuture1); 279 | } 280 | }); 281 | $reFuture2 = $this->getLoop()->addTimer(0.0, 0.0, function () use(&$reFuture2, &$reFuture2Count) { 282 | $reFuture2Count ++; 283 | echo '@ReFuture-2' . PHP_EOL; 284 | if($reFuture2Count === 2) { 285 | $this->getLoop()->delTimer($reFuture2); 286 | } 287 | }); 288 | $this->tickLoop($this->tickTimeout); 289 | } 290 | 291 | /** 292 | * 测试@DelayReFuture的优先级 293 | * More Tag @see LoopInterface::addTimer() 294 | * 295 | * @return void 296 | */ 297 | public function testDelayReFuturePriority(): void 298 | { 299 | $this->expectOutputString( 300 | '@DelayReFuture-1' . PHP_EOL . 301 | '@DelayReFuture-2' . PHP_EOL . 302 | '@DelayReFuture-1' . PHP_EOL . 303 | '@DelayReFuture-2' . PHP_EOL 304 | ); 305 | $DelayReFuture1Count = 0; 306 | $DelayReFuture2Count = 0; 307 | $DelayReFuture1 = $this->getLoop()->addTimer(1.0, 0.0, function () use(&$DelayReFuture1, &$DelayReFuture1Count) { 308 | $DelayReFuture1Count ++; 309 | echo '@DelayReFuture-1' . PHP_EOL; 310 | if($DelayReFuture1Count === 2) { 311 | $this->getLoop()->delTimer($DelayReFuture1); 312 | } 313 | }); 314 | $DelayReFuture2 = $this->getLoop()->addTimer(1.0, 0.0, function () use(&$DelayReFuture2, &$DelayReFuture2Count) { 315 | $DelayReFuture2Count ++; 316 | echo '@DelayReFuture-2' . PHP_EOL; 317 | if($DelayReFuture2Count === 2) { 318 | $this->getLoop()->delTimer($DelayReFuture2); 319 | } 320 | }); 321 | $this->tickLoop(1.0 + $this->tickTimeout); 322 | } 323 | 324 | /** 325 | * 测试@Delayer的优先级 326 | * More Tag @see LoopInterface::addTimer() 327 | * 328 | * @return void 329 | */ 330 | public function testDelayerPriority(): void 331 | { 332 | $this->expectOutputString( 333 | '@Delayer-1' . PHP_EOL . '@Delayer-2' . PHP_EOL 334 | ); 335 | $this->getLoop()->addTimer(1.0, false, function () { 336 | echo '@Delayer-1' . PHP_EOL; 337 | }); 338 | $this->getLoop()->addTimer(1.0, false, function () { 339 | echo '@Delayer-2' . PHP_EOL; 340 | }); 341 | $this->tickLoop(1.0 + $this->tickTimeout); 342 | } 343 | 344 | /** 345 | * 测试@Timer的优先级 346 | * More Tag @see LoopInterface::addTimer() 347 | * 348 | * @return void 349 | */ 350 | public function testTimerPriority(): void 351 | { 352 | $this->expectOutputString( 353 | '@Timer-1' . PHP_EOL . 354 | '@Timer-2' . PHP_EOL . 355 | '@Timer-1' . PHP_EOL . 356 | '@Timer-2' . PHP_EOL 357 | ); 358 | $timer1Count = 0; 359 | $timer2Count = 0; 360 | $timer1 = $this->getLoop()->addTimer(0.0, 1.0, function () use (&$timer1, &$timer1Count){ 361 | $timer1Count ++; 362 | echo '@Timer-1' . PHP_EOL; 363 | if($timer1Count === 2){ 364 | $this->getLoop()->delTimer($timer1); 365 | } 366 | }); 367 | $timer2 = $this->getLoop()->addTimer(0.0, 1.0, function () use (&$timer2, &$timer2Count){ 368 | $timer2Count ++; 369 | echo '@Timer-2' . PHP_EOL; 370 | if($timer2Count === 2){ 371 | $this->getLoop()->delTimer($timer2); 372 | } 373 | }); 374 | $this->tickLoop(1.0 + $this->tickTimeout); 375 | } 376 | 377 | /** 378 | * 测试@DelayTimer的优先级 379 | * More Tag @see LoopInterface::addTimer() 380 | * 381 | * @return void 382 | */ 383 | public function testDelayTimerPriority(): void 384 | { 385 | $this->expectOutputString( 386 | '@DelayTimer-1' . PHP_EOL . 387 | '@DelayTimer-2' . PHP_EOL . 388 | '@DelayTimer-1' . PHP_EOL . 389 | '@DelayTimer-2' . PHP_EOL 390 | ); 391 | $delayTimer1Count = 0; 392 | $delayTimer2Count = 0; 393 | $delayTimer1 = $this->getLoop()->addTimer(0.5, 1.0, function () use(&$delayTimer1, &$delayTimer1Count){ 394 | $delayTimer1Count ++; 395 | echo '@DelayTimer-1' . PHP_EOL; 396 | if($delayTimer1Count === 2){ 397 | $this->getLoop()->delTimer($delayTimer1); 398 | } 399 | }); 400 | $delayTimer2 = $this->getLoop()->addTimer(0.5, 1.0, function () use(&$delayTimer2, &$delayTimer2Count){ 401 | $delayTimer2Count ++; 402 | echo '@DelayTimer-2' . PHP_EOL; 403 | if($delayTimer2Count === 2){ 404 | $this->getLoop()->delTimer($delayTimer2); 405 | } 406 | }); 407 | $this->tickLoop(0.5 + 1.0 + $this->tickTimeout); 408 | } 409 | 410 | /** 411 | * 测试定时器最大间隔的设置是否正常 412 | * 413 | * @return void 414 | */ 415 | public function testTimerIntervalCanBeFarInFuture(): void 416 | { 417 | $interval = PHP_INT_MAX / 100000; 418 | // set delay 419 | $timer = $this->getLoop()->addTimer((float)$interval,0.0, function () {}); 420 | // assert timer isset 421 | $this->assertEquals(true, $this->getLoop()->getStorage()->exist($timer)); 422 | // tick run 423 | $this->tickLoop($this->tickTimeout); 424 | // assert timer deleted 425 | $this->assertTrue($this->getLoop()->getStorage()->exist($timer)); 426 | // clear 427 | $this->getLoop()->clear(); 428 | $this->assertFalse($this->getLoop()->getStorage()->exist($timer)); 429 | 430 | // set repeat 431 | $timer = $this->getLoop()->addTimer(0.0, (float)$interval, function () {}); 432 | // assert timer isset 433 | $this->assertEquals(true, $this->getLoop()->getStorage()->exist($timer)); 434 | // tick run 435 | $this->tickLoop($this->tickTimeout); 436 | // assert timer deleted 437 | $this->assertTrue($this->getLoop()->getStorage()->exist($timer)); 438 | // clear 439 | $this->getLoop()->clear(); 440 | $this->assertFalse($this->getLoop()->getStorage()->exist($timer)); 441 | } 442 | 443 | /** 444 | * 定时器嵌套 445 | * 446 | * @return void 447 | */ 448 | public function testTimerNesting(): void 449 | { 450 | $this->expectOutputString('timer nesting' . PHP_EOL); 451 | $this->getLoop()->addTimer(0.0,false, 452 | function () use(&$string){ 453 | $this->getLoop()->addTimer(0.1,false, 454 | function () use(&$string){ 455 | $this->getLoop()->addTimer(0.2,0.1, 456 | function () use(&$string){ 457 | echo 'timer nesting' . PHP_EOL; 458 | $this->getLoop()->destroy(); 459 | } 460 | ); 461 | } 462 | ); 463 | } 464 | ); 465 | $this->getLoop()->run(); 466 | } 467 | } -------------------------------------------------------------------------------- /tests/UnitTests/Units/StreamsUnit.php: -------------------------------------------------------------------------------- 1 | getLoop(); 33 | $timeout = $loop->addTimer(0.1,0.0, function () use ($input, $loop) { 34 | $loop->delReadStream($input); 35 | $loop->stop(); 36 | }); 37 | 38 | $called = 0; 39 | $this->getLoop()->addReadStream($input, function () use (&$called, $loop, $input, $timeout) { 40 | ++$called; 41 | $loop->delReadStream($input); 42 | $loop->delTimer($timeout); 43 | $loop->stop(); 44 | }); 45 | 46 | fwrite($output, 'foo' . PHP_EOL); 47 | 48 | $this->getLoop()->run(); 49 | 50 | $this->assertEquals(1, $called); 51 | } 52 | 53 | /** 54 | * 测试socket关闭时创建read stream handler 55 | * 56 | * @dataProvider provider 57 | * @param bool $bio 58 | * @return void 59 | */ 60 | public function testAddReadStreamHandlerWhenSocketCloses(bool $bio): void 61 | { 62 | list ($input, $output) = \stream_socket_pair( 63 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 64 | STREAM_SOCK_STREAM, 65 | STREAM_IPPROTO_IP 66 | ); 67 | 68 | stream_set_blocking($input, $bio); 69 | stream_set_blocking($output, $bio); 70 | 71 | $loop = $this->getLoop(); 72 | $timeout = $loop->addTimer(0.1,0.0, function () use ($input, $loop) { 73 | $loop->delReadStream($input); 74 | $loop->stop(); 75 | }); 76 | 77 | $called = 0; 78 | $this->getLoop()->addReadStream($input, function () use (&$called, $loop, $input, $timeout) { 79 | ++$called; 80 | $loop->delReadStream($input); 81 | $loop->delTimer($timeout); 82 | $loop->stop(); 83 | }); 84 | 85 | fclose($output); 86 | 87 | $this->getLoop()->run(); 88 | 89 | $this->assertEquals(1, $called); 90 | } 91 | 92 | /** 93 | * read stream重复创建后者无效 94 | * 95 | * @dataProvider provider 96 | * @param bool $bio 97 | * @return void 98 | */ 99 | public function testAddReadStreamIgnoresSecondAddReadStream(bool $bio): void 100 | { 101 | list ($input, $output) = \stream_socket_pair( 102 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 103 | STREAM_SOCK_STREAM, 104 | STREAM_IPPROTO_IP 105 | ); 106 | 107 | stream_set_blocking($input, $bio); 108 | stream_set_blocking($output, $bio); 109 | 110 | $count1 = 0; 111 | $count2 = 0; 112 | $this->getLoop()->addReadStream($input, function () use(&$count1){ 113 | $count1 ++; 114 | }); 115 | $this->getLoop()->addReadStream($input, function () use(&$count2){ 116 | $count2 ++; 117 | }); 118 | 119 | $this->assertCount(1, $this->getLoop()->getReadFds()); 120 | 121 | fwrite($output, 'foo' . PHP_EOL); 122 | $this->tickLoop(); 123 | 124 | $this->assertEquals(1, $count1); 125 | $this->assertEquals(0, $count2); 126 | } 127 | 128 | /** 129 | * 读流处理器多次触发 130 | * 131 | * @dataProvider provider 132 | * @param bool $bio 133 | * @return void 134 | */ 135 | public function testReadStreamHandlerTriggeredMultiTimes(bool $bio): void 136 | { 137 | list ($input, $output) = \stream_socket_pair( 138 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 139 | STREAM_SOCK_STREAM, 140 | STREAM_IPPROTO_IP 141 | ); 142 | stream_set_blocking($input, $bio); 143 | stream_set_blocking($output, $bio); 144 | 145 | $count = 0; 146 | $this->getLoop()->addReadStream($input, function() use(&$count){ 147 | $count ++; 148 | }); 149 | 150 | fwrite($output, 'foo' . PHP_EOL); 151 | $this->tickLoop(); 152 | 153 | fwrite($output, 'bar' . PHP_EOL); 154 | $this->tickLoop(); 155 | 156 | $this->assertEquals(2, $count); 157 | } 158 | 159 | /** 160 | * 读流处理器可以引用接受数据 161 | * 162 | * @dataProvider provider 163 | * @param bool $bio 164 | * @return void 165 | */ 166 | public function testReadStreamHandlerReceivesDataFromStreamReference(bool $bio): void 167 | { 168 | list ($input, $output) = \stream_socket_pair( 169 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 170 | STREAM_SOCK_STREAM, 171 | STREAM_IPPROTO_IP 172 | ); 173 | stream_set_blocking($input, $bio); 174 | stream_set_blocking($output, $bio); 175 | 176 | $this->received = ''; 177 | fwrite($input, 'hello'); 178 | fclose($input); 179 | 180 | $this->getLoop()->addReadStream($output, function ($output) { 181 | $chunk = fread($output, 1024); 182 | if ($chunk === '') { 183 | $this->received .= 'X'; 184 | $this->getLoop()->delReadStream($output); 185 | fclose($output); 186 | $this->getLoop()->destroy(); 187 | } else { 188 | $this->received .= '[' . $chunk . ']'; 189 | } 190 | 191 | }); 192 | $this->assertEquals('', $this->received); 193 | $this->assertRunFasterThan($this->tickTimeout * 2); 194 | $this->assertEquals('[hello]X', $this->received); 195 | } 196 | 197 | /** 198 | * 在添加读流之后立即移除 199 | * 200 | * @dataProvider provider 201 | * @param bool $bio 202 | * @return void 203 | */ 204 | public function testRemoveReadStreamInstantly(bool $bio): void 205 | { 206 | list ($input, $output) = \stream_socket_pair( 207 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 208 | STREAM_SOCK_STREAM, 209 | STREAM_IPPROTO_IP 210 | ); 211 | stream_set_blocking($input, $bio); 212 | stream_set_blocking($output, $bio); 213 | $count = 0; 214 | $this->getLoop()->addReadStream($input, function() use(&$count){ 215 | $count ++; 216 | }); 217 | $this->getLoop()->delReadStream($input); 218 | 219 | fwrite($output, 'bar' . PHP_EOL); 220 | $this->tickLoop(); 221 | 222 | $this->assertEquals(0, $count); 223 | } 224 | 225 | /** 226 | * 读流读取后移除 227 | * 228 | * @dataProvider provider 229 | * @param bool $bio 230 | * @return void 231 | */ 232 | public function testRemoveReadStreamAfterReading(bool $bio): void 233 | { 234 | list ($input, $output) = \stream_socket_pair( 235 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 236 | STREAM_SOCK_STREAM, 237 | STREAM_IPPROTO_IP 238 | ); 239 | stream_set_blocking($input, $bio); 240 | stream_set_blocking($output, $bio); 241 | $count = 0; 242 | $this->getLoop()->addReadStream($input, function() use (&$count){ 243 | $count ++; 244 | }); 245 | 246 | fwrite($output, 'foo' . PHP_EOL); 247 | $this->tickLoop(); 248 | 249 | $this->getLoop()->delReadStream($input); 250 | 251 | fwrite($output, 'bar' . PHP_EOL); 252 | $this->tickLoop(); 253 | 254 | $this->assertEquals(1, $count); 255 | } 256 | 257 | /** 258 | * 测试socket连接成功时创建write stream handler 259 | * 260 | * @dataProvider provider 261 | * @param bool $bio 262 | * @return void 263 | */ 264 | public function testAddWriteStreamHandlerWhenSocketConnectionSucceeds(bool $bio): void 265 | { 266 | $server = stream_socket_server('127.0.0.1:0'); 267 | 268 | $errcode = $errmsg = null; 269 | $connecting = stream_socket_client( 270 | stream_socket_get_name($server, false), 271 | $errcode, 272 | $errmsg, 273 | 274 | 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT 275 | ); 276 | stream_set_blocking($connecting, $bio); 277 | 278 | $timeout = $this->getLoop()->addTimer(0.1,0.0, function () use ($connecting) { 279 | $this->getLoop()->delWriteStream($connecting); 280 | $this->getLoop()->destroy(); 281 | }); 282 | 283 | $called = 0; 284 | $this->getLoop()->addWriteStream($connecting, function () use (&$called, $connecting, $timeout) { 285 | $called ++; 286 | $this->getLoop()->delWriteStream($connecting); 287 | $this->getLoop()->delTimer($timeout); 288 | $this->getLoop()->destroy(); 289 | }); 290 | 291 | $this->getLoop()->run(); 292 | 293 | $this->assertEquals(1, $called); 294 | } 295 | 296 | /** 297 | * socket连接被拒绝时添加write stream handler 298 | * 299 | * @dataProvider provider 300 | * @param bool $bio 301 | * @return void 302 | */ 303 | public function testAddWriteStreamHandlerWhenSocketConnectionRefused(bool $bio): void 304 | { 305 | 306 | // first verify the operating system actually refuses the connection and no firewall is in place 307 | // use higher timeout because Windows retires multiple times and has a noticeable delay 308 | // @link https://stackoverflow.com/questions/19440364/why-do-failed-attempts-of-socket-connect-take-1-sec-on-windows 309 | $errcode = $errmsg = null; 310 | if ( 311 | @stream_socket_client('127.0.0.1:1', $errcode, $errmsg, 10.0) !== false or 312 | (defined('SOCKET_ECONNREFUSED') and $errcode !== SOCKET_ECONNREFUSED) 313 | ) { 314 | $this->markTestSkipped('Expected host to refuse connection, but got error ' . $errcode . ': ' . $errmsg); 315 | } 316 | 317 | $connecting = stream_socket_client( 318 | '127.0.0.1:1', 319 | $errcode, 320 | $errmsg, 321 | 0, 322 | STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT 323 | ); 324 | stream_set_blocking($connecting, $bio); 325 | 326 | $timeout = $this->getLoop()->addTimer(10.0,0.0, function () use ($connecting) { 327 | $this->getLoop()->delWriteStream($connecting); 328 | $this->getLoop()->destroy(); 329 | }); 330 | 331 | $called = 0; 332 | $this->getLoop()->addWriteStream($connecting, function () use (&$called, $connecting, $timeout) { 333 | $called ++; 334 | $this->getLoop()->delWriteStream($connecting); 335 | $this->getLoop()->delTimer($timeout); 336 | $this->getLoop()->destroy(); 337 | }); 338 | 339 | $this->getLoop()->run(); 340 | 341 | $this->assertEquals(1, $called); 342 | } 343 | 344 | /** 345 | * write stream 重复创建后者忽略 346 | * 347 | * @dataProvider provider 348 | * @param bool $bio 349 | * @return void 350 | */ 351 | public function testAddWriteStreamIgnoresSecondAddWriteStream(bool $bio): void 352 | { 353 | list ($input) = \stream_socket_pair( 354 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 355 | STREAM_SOCK_STREAM, 356 | STREAM_IPPROTO_IP 357 | ); 358 | stream_set_blocking($input, $bio); 359 | $count1 = $count2 = 0; 360 | $this->getLoop()->addWriteStream($input, function() use(&$count1){ 361 | $count1 ++; 362 | }); 363 | $this->getLoop()->addWriteStream($input, function() use(&$count2){ 364 | $count2 ++; 365 | }); 366 | $this->assertCount(1, $this->getLoop()->getWriteFds()); 367 | $this->tickLoop(); 368 | $this->assertEquals(1, $count1); 369 | $this->assertEquals(0, $count2); 370 | } 371 | 372 | /** 373 | * 写流处理器的多次触发 374 | * 375 | * @dataProvider provider 376 | * @param bool $bio 377 | * @return void 378 | */ 379 | public function testWriteStreamHandlerTriggeredMultiTimes(bool $bio): void 380 | { 381 | list ($input) = \stream_socket_pair( 382 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 383 | STREAM_SOCK_STREAM, 384 | STREAM_IPPROTO_IP 385 | ); 386 | stream_set_blocking($input, $bio); 387 | $count = 0; 388 | $this->getLoop()->addWriteStream($input, function() use(&$count){ 389 | $count ++; 390 | }); 391 | $this->tickLoop(); 392 | 393 | $this->tickLoop(); 394 | $this->assertEquals(2, $count); 395 | } 396 | 397 | /** 398 | * 添加写流后立即移除 399 | * 400 | * @dataProvider provider 401 | * @param bool $bio 402 | * @return void 403 | */ 404 | public function testRemoveWriteStreamInstantly(bool $bio): void 405 | { 406 | list ($input) = \stream_socket_pair( 407 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 408 | STREAM_SOCK_STREAM, 409 | STREAM_IPPROTO_IP 410 | ); 411 | stream_set_blocking($input, $bio); 412 | $count = 0; 413 | $this->getLoop()->addWriteStream($input, function() use(&$count){ 414 | $count ++; 415 | }); 416 | $this->getLoop()->delWriteStream($input); 417 | $this->tickLoop(); 418 | $this->assertEquals(0, $count); 419 | } 420 | 421 | /** 422 | * 写流写入后移除 423 | * 424 | * @dataProvider provider 425 | * @param bool $bio 426 | * @return void 427 | */ 428 | public function testRemoveWriteStreamAfterWriting(bool $bio): void 429 | { 430 | list ($input) = \stream_socket_pair( 431 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 432 | STREAM_SOCK_STREAM, 433 | STREAM_IPPROTO_IP 434 | ); 435 | stream_set_blocking($input, $bio); 436 | $count = 0; 437 | $this->getLoop()->addWriteStream($input, function() use (&$count){ 438 | $count ++; 439 | }); 440 | $this->tickLoop(); 441 | 442 | $this->getLoop()->delWriteStream($input); 443 | $this->tickLoop(); 444 | $this->assertEquals(1, $count); 445 | } 446 | 447 | /** 448 | * 移除读流 449 | * 450 | * @dataProvider provider 451 | * @param bool $bio 452 | * @return void 453 | */ 454 | public function testRemoveReadStreams(bool $bio): void 455 | { 456 | list ($input1, $output1) = \stream_socket_pair( 457 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 458 | STREAM_SOCK_STREAM, 459 | STREAM_IPPROTO_IP 460 | ); 461 | list ($input2, $output2) = \stream_socket_pair( 462 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 463 | STREAM_SOCK_STREAM, 464 | STREAM_IPPROTO_IP 465 | ); 466 | 467 | stream_set_blocking($input1, $bio); 468 | stream_set_blocking($input2, $bio); 469 | stream_set_blocking($output1, $bio); 470 | stream_set_blocking($output2, $bio); 471 | 472 | $this->getLoop()->addReadStream($input1, function ($stream) { 473 | $this->getLoop()->delReadStream($stream); 474 | }); 475 | 476 | $this->getLoop()->addReadStream($input2, function ($stream) { 477 | $this->getLoop()->delReadStream($stream); 478 | }); 479 | 480 | fwrite($output1, "foo1\n"); 481 | fwrite($output2, "foo2\n"); 482 | 483 | $this->tickLoop(); 484 | 485 | $this->assertCount(0, $this->getLoop()->getReadFds()); 486 | $this->assertCount(0, $this->getLoop()->getReads()); 487 | } 488 | 489 | /** 490 | * 移除写流 491 | * 492 | * @dataProvider provider 493 | * @param bool $bio 494 | * @return void 495 | */ 496 | public function testRemoveWriteStreams(bool $bio): void 497 | { 498 | list ($input1) = \stream_socket_pair( 499 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 500 | STREAM_SOCK_STREAM, 501 | STREAM_IPPROTO_IP 502 | ); 503 | list ($input2) = \stream_socket_pair( 504 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 505 | STREAM_SOCK_STREAM, 506 | STREAM_IPPROTO_IP 507 | ); 508 | 509 | stream_set_blocking($input1, $bio); 510 | stream_set_blocking($input2, $bio); 511 | 512 | $this->getLoop()->addWriteStream($input1, function ($stream) { 513 | $this->getLoop()->delWriteStream($stream); 514 | }); 515 | 516 | $this->getLoop()->addWriteStream($input2, function ($stream) { 517 | $this->getLoop()->delWriteStream($stream); 518 | }); 519 | 520 | $this->tickLoop(); 521 | 522 | $this->assertCount(0, $this->getLoop()->getWriteFds()); 523 | $this->assertCount(0, $this->getLoop()->getWrites()); 524 | } 525 | 526 | /** 527 | * 仅移除读流 528 | * 529 | * @dataProvider provider 530 | * @param bool $bio 531 | * @return void 532 | */ 533 | public function testRemoveStreamForReadOnly(bool $bio): void 534 | { 535 | list ($input, $output) = \stream_socket_pair( 536 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 537 | STREAM_SOCK_STREAM, 538 | STREAM_IPPROTO_IP 539 | ); 540 | stream_set_blocking($input, $bio); 541 | stream_set_blocking($output, $bio); 542 | $count1 = $count2 = 0; 543 | $this->getLoop()->addReadStream($input, function() use(&$count1){ 544 | $count1 ++; 545 | }); 546 | $this->getLoop()->addWriteStream($output, function() use(&$count2){ 547 | $count2 ++; 548 | }); 549 | $this->getLoop()->delReadStream($input); 550 | 551 | fwrite($output, 'foo' . PHP_EOL); 552 | $this->tickLoop(); 553 | $this->assertEquals(0, $count1); 554 | $this->assertEquals(1, $count2); 555 | } 556 | 557 | /** 558 | * 仅移除写流 559 | * 560 | * @dataProvider provider 561 | * @param bool $bio 562 | * @return void 563 | */ 564 | public function testRemoveStreamForWriteOnly(bool $bio): void 565 | { 566 | list ($input, $output) = \stream_socket_pair( 567 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 568 | STREAM_SOCK_STREAM, 569 | STREAM_IPPROTO_IP 570 | ); 571 | stream_set_blocking($input, $bio); 572 | stream_set_blocking($output, $bio); 573 | fwrite($output, 'foo' . PHP_EOL); 574 | 575 | $count1 = $count2 = 0; 576 | $this->getLoop()->addReadStream($input, function() use(&$count1){ 577 | $count1 ++; 578 | }); 579 | $this->getLoop()->addWriteStream($output, function() use(&$count2){ 580 | $count2 ++; 581 | }); 582 | $this->getLoop()->delWriteStream($output); 583 | 584 | $this->tickLoop(); 585 | $this->assertEquals(1, $count1); 586 | $this->assertEquals(0, $count2); 587 | } 588 | 589 | /** 590 | * 移除未注册的流事件 591 | * 592 | * @dataProvider provider 593 | * @param bool $bio 594 | * @return void 595 | */ 596 | public function testRemoveUnregisteredStream(bool $bio): void 597 | { 598 | list ($stream) = \stream_socket_pair( 599 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 600 | STREAM_SOCK_STREAM, 601 | STREAM_IPPROTO_IP 602 | ); 603 | stream_set_blocking($stream, $bio); 604 | $this->getLoop()->delReadStream($stream); 605 | $this->getLoop()->delWriteStream($stream); 606 | 607 | $this->assertTrue(true); 608 | } 609 | 610 | /** 611 | * 读流先于timer触发 612 | * 613 | * @dataProvider provider 614 | * @param bool $bio 615 | * @return void 616 | */ 617 | public function testReadStreamBeforeTimer(bool $bio): void 618 | { 619 | list ($input, $output) = \stream_socket_pair( 620 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 621 | STREAM_SOCK_STREAM, 622 | STREAM_IPPROTO_IP 623 | ); 624 | stream_set_blocking($input, $bio); 625 | stream_set_blocking($output, $bio); 626 | fwrite($input, 'read' . PHP_EOL); 627 | 628 | $this->expectOutputString('read' . PHP_EOL . 'timer' . PHP_EOL); 629 | $this->getLoop()->addTimer(0.0,false, function () { 630 | echo 'timer' . PHP_EOL; 631 | }); 632 | $this->getLoop()->addReadStream($output, function($stream) { 633 | $this->getLoop()->delReadStream($stream); 634 | echo 'read' . PHP_EOL; 635 | }); 636 | $this->tickLoop($this->tickTimeout); 637 | } 638 | 639 | /** 640 | * 写流先于timer触发 641 | * 642 | * @dataProvider provider 643 | * @param bool $bio 644 | * @return void 645 | */ 646 | public function testWriteStreamBeforeTimer(bool $bio): void 647 | { 648 | list ($input, $output) = \stream_socket_pair( 649 | (DIRECTORY_SEPARATOR === '\\') ? STREAM_PF_INET : STREAM_PF_UNIX, 650 | STREAM_SOCK_STREAM, 651 | STREAM_IPPROTO_IP 652 | ); 653 | stream_set_blocking($input, $bio); 654 | stream_set_blocking($output, $bio); 655 | fwrite($input, 'write' . PHP_EOL); 656 | 657 | $this->expectOutputString('write' . PHP_EOL . 'timer' . PHP_EOL); 658 | $this->getLoop()->addTimer(0.0,false, function () { 659 | echo 'timer' . PHP_EOL; 660 | }); 661 | $this->getLoop()->addWriteStream(\STDOUT, function($stream) { 662 | $this->getLoop()->delWriteStream($stream); 663 | echo 'write' . PHP_EOL; 664 | }); 665 | $this->tickLoop($this->tickTimeout); 666 | } 667 | } --------------------------------------------------------------------------------