├── .gitattributes ├── .gitignore ├── .noninteractive ├── .scrutinizer.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── example ├── bootstrap │ └── autoload.php ├── socket_conn_tcp.php ├── socket_conn_tcp_ipv6.php ├── socket_conn_unix.php ├── socket_info.php ├── socket_only_client.php ├── socket_only_server.php ├── socket_quickstart.php ├── socket_ssl.php ├── socket_ssl_only_client.php ├── socket_ssl_only_server.php └── ssl │ ├── _ssl_cert.pem │ └── _ssl_key.pem ├── phpunit.xml ├── src └── Socket │ ├── Socket.php │ ├── SocketInterface.php │ ├── SocketListener.php │ └── SocketListenerInterface.php └── test ├── Callback.php ├── TModule.php ├── TModule ├── SocketTest.php └── _Data │ ├── _ssl_cert.pem │ └── _ssl_key.pem ├── TUnit.php ├── TUnit ├── SocketListenerTest.php ├── SocketTest.php └── _Data │ ├── _ssl_cert.pem │ └── _ssl_key.pem ├── _Simulation ├── Event.php ├── EventCollection.php ├── Simulation.php └── SimulationInterface.php └── bootstrap.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | composer.phar -------------------------------------------------------------------------------- /.noninteractive: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dazzle-php/socket/7f896d8ebdabbd9cc158fca44a46a6a0a6f0b6e5/.noninteractive -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - 'build/*' 4 | - 'build-ci/*' 5 | - 'example/*' 6 | - 'example-bench/*' 7 | - 'test/*' 8 | - 'vendor/*' 9 | 10 | checks: 11 | php: 12 | return_doc_comments: true 13 | remove_extra_empty_lines: true 14 | 15 | coding_style: 16 | php: 17 | indentation: 18 | general: 19 | use_tabs: false 20 | size: 4 21 | switch: 22 | indent_case: true 23 | spaces: 24 | general: 25 | linefeed_character: newline 26 | before_parentheses: 27 | function_declaration: false 28 | closure_definition: false 29 | function_call: false 30 | if: true 31 | for: true 32 | while: true 33 | switch: true 34 | catch: true 35 | array_initializer: false 36 | around_operators: 37 | assignment: true 38 | logical: true 39 | equality: true 40 | relational: true 41 | bitwise: true 42 | additive: true 43 | multiplicative: true 44 | shift: true 45 | unary_additive: false 46 | concatenation: true 47 | negation: false 48 | before_left_brace: 49 | class: true 50 | function: true 51 | if: true 52 | else: true 53 | for: true 54 | while: true 55 | do: true 56 | switch: true 57 | try: true 58 | catch: true 59 | finally: true 60 | before_keywords: 61 | else: true 62 | while: true 63 | catch: true 64 | finally: true 65 | within: 66 | brackets: false 67 | array_initializer: false 68 | grouping: false 69 | function_call: false 70 | function_declaration: false 71 | if: false 72 | for: false 73 | while: false 74 | switch: false 75 | catch: false 76 | type_cast: false 77 | ternary_operator: 78 | before_condition: true 79 | after_condition: true 80 | before_alternative: true 81 | after_alternative: true 82 | in_short_version: false 83 | other: 84 | before_comma: false 85 | after_comma: true 86 | before_semicolon: false 87 | after_semicolon: true 88 | after_type_cast: true 89 | braces: 90 | classes_functions: 91 | class: new-line 92 | function: new-line 93 | closure: end-of-line 94 | if: 95 | opening: new-line 96 | always: false 97 | else_on_new_line: true 98 | for: 99 | opening: new-line 100 | always: true 101 | while: 102 | opening: new-line 103 | always: true 104 | do_while: 105 | opening: new-line 106 | always: true 107 | while_on_new_line: true 108 | switch: 109 | opening: new-line 110 | try: 111 | opening: new-line 112 | catch_on_new_line: true 113 | finally_on_new_line: true 114 | upper_lower_casing: 115 | keywords: 116 | general: lower 117 | constants: 118 | true_false_null: lower 119 | 120 | tools: 121 | external_code_coverage: 122 | timeout: 1800 123 | runs: 1 124 | php_code_coverage: false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | sudo: required 5 | 6 | php: 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | 11 | before_install: 12 | - export PHP_MAJOR="$(echo $TRAVIS_PHP_VERSION | cut -d '.' -f 1,2)" 13 | 14 | install: 15 | - travis_retry composer self-update 16 | - travis_retry composer install --prefer-source --no-interaction 17 | - php -m 18 | 19 | script: 20 | - vendor/bin/phpunit -d memory_limit=1024M --coverage-text --coverage-clover=coverage.clover 21 | 22 | after_script: 23 | - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi 24 | - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | This changelog references the relevant changes, bug and security fixes done. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and accepted via **Pull Requests** on [GitHub](https://github.com/dazzle-php/throwable). 4 | 5 | ## Pull Requests 6 | 7 | - **Naming convention** - all pull requests fixing a problem should match "Fix #issue Message" pattern, the new features and non-fix changes should match "Resolve #issue Message", the rest should contain only "Message". 8 | - **Follow our template of code** - all contributions have to follow [PSR-2 coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) with an exception of control structures, which have to have opening parenthesis always placed in the next line instead of the same. 9 | - **Add tests** - the contribution won't be accepted if it doesn't have tests. 10 | - **Document any change in behaviour** - make sure the `README.md` is kept up to date. 11 | - **Create feature branches** - don't create pull requests from your master branch. 12 | - **One pull request per feature** - for multiple things that you want to do, send also multiple pull requests. 13 | - **Keep coherent history** - make sure each individual commit in your pull request is meaningful. If you had to make multiple commits during development cycle, please squash them before submitting. 14 | 15 | ## Running Tests 16 | 17 | ``` 18 | $> vendor/bin/phpunit 19 | ``` 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Kamil Jamróz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dazzle Async Socket 2 | 3 | [![Build Status](https://travis-ci.org/dazzle-php/socket.svg)](https://travis-ci.org/dazzle-php/socket) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/dazzle-php/socket/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/dazzle-php/socket/?branch=master) 5 | [![Code Quality](https://scrutinizer-ci.com/g/dazzle-php/socket/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/dazzle-php/socket/?branch=master) 6 | [![Latest Stable Version](https://poser.pugx.org/dazzle-php/socket/v/stable)](https://packagist.org/packages/dazzle-php/socket) 7 | [![Latest Unstable Version](https://poser.pugx.org/dazzle-php/socket/v/unstable)](https://packagist.org/packages/dazzle-php/socket) 8 | [![License](https://poser.pugx.org/dazzle-php/socket/license)](https://packagist.org/packages/dazzle-php/socket/license) 9 | 10 | > **Note:** This repository is part of [Dazzle Project](https://github.com/dazzle-php/dazzle) - the next-gen library for PHP. The project's purpose is to provide PHP developers with a set of complete tools to build functional async applications. Please, make sure you read the attached README carefully and it is guaranteed you will be surprised how easy to use and powerful it is. In the meantime, you might want to check out the rest of our async libraries in [Dazzle repository](https://github.com/dazzle-php) for the full extent of Dazzle experience. 11 | 12 |
13 |

14 | 15 |

16 | 17 | ## Description 18 | 19 | Dazzle Socket is a component that implements asynchronous tcp, udp and unix socket handling for PHP. The library also provides interface for implementing self inter-process communication via external services. 20 | 21 | ## Feature Highlights 22 | 23 | Dazzle Socket features: 24 | 25 | * Asynchronous handling of incoming and outcoming messages, 26 | * Support for TCP, UDP and Unix sockets, 27 | * ...and more. 28 | 29 | ## Provided Example(s) 30 | 31 | ### Quickstart 32 | 33 | Server file which accepts the range in format of $min-$max and returns randomized number. 34 | 35 | ```php 36 | $loop = new Loop(new SelectLoop); 37 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop); 38 | 39 | $server->on('connect', function($server, SocketInterface $client) { 40 | $client->write("Hello!\n"); 41 | $client->write("Welcome to Dazzle server!\n"); 42 | $client->write("Tell me a range and I will randomize a number for you!\n\n"); 43 | 44 | $client->on('data', function(SocketInterface $client, $data) use(&$buffer) { 45 | $client->write("Your number is: " . rand(...explode('-', $data))); 46 | }); 47 | }); 48 | 49 | $loop->onStart(function() use($server) { 50 | $server->start(); 51 | }); 52 | $loop->start(); 53 | ``` 54 | 55 | Client file which sends the $min-$max format to the above server and gets the response. 56 | 57 | ```php 58 | $loop = new Loop(new SelectLoop); 59 | $socket = new Socket('tcp://127.0.0.1:2080', $loop); 60 | 61 | $socket->on('data', function($socket, $data) { 62 | printf("%s", $data); 63 | }); 64 | $socket->write('1-100'); 65 | 66 | $loop->start(); 67 | ``` 68 | 69 | ### Additional 70 | 71 | Additional examples can be found in [example](https://github.com/dazzle-php/socket/tree/master/example) directory. Below is the list of provided examples as a reference and preferred consumption order: 72 | 73 | - [Quickstart](https://github.com/dazzle-php/socket/blob/master/example/events_quickstart.php) 74 | - [Using socket client](https://github.com/dazzle-php/socket/blob/master/example/socket_only_client.php) 75 | - [Using socket server](https://github.com/dazzle-php/socket/blob/master/example/socket_only_server.php) 76 | - [Creating TCP IPv4 client-server connection](https://github.com/dazzle-php/socket/blob/master/example/socket_conn_tcp.php) 77 | - [Creating TCP IPv6 client-server connection](https://github.com/dazzle-php/socket/blob/master/example/socket_conn_tcp_ipv6.php) 78 | - [Creating UNIX socket client-server connection](https://github.com/dazzle-php/socket/blob/master/example/socket_conn_unix.php) 79 | - [Getting connection info](https://github.com/dazzle-php/socket/blob/master/example/socket_info.php) 80 | - [Using secure SSL socket client](https://github.com/dazzle-php/socket/blob/master/example/socket_ssl_only_client.php) 81 | - [Using secure SSL socket server](https://github.com/dazzle-php/socket/blob/master/example/socket_ssl_only_server.php) 82 | - [Creating secure SSL client-server connection](https://github.com/dazzle-php/socket/blob/master/example/socket_ssl.php) 83 | 84 | If any of the above examples has left you confused, please take a look in the [tests](https://github.com/dazzle-php/socket/tree/master/test) directory as well. 85 | 86 | ## Requirements 87 | 88 | Dazzle Socket requires: 89 | 90 | * PHP-5.6 or PHP-7.0+, 91 | * UNIX or Windows OS. 92 | 93 | ## Installation 94 | 95 | To install this library make sure you have [composer](https://getcomposer.org/) installed, then run following command: 96 | 97 | ``` 98 | $> composer require dazzle-php/socket 99 | ``` 100 | 101 | ## Tests 102 | 103 | Tests can be run via: 104 | 105 | ``` 106 | $> vendor/bin/phpunit -d memory_limit=1024M 107 | ``` 108 | 109 | ## Versioning 110 | 111 | Versioning of Dazzle libraries is being shared between all packages included in [Dazzle Project](https://github.com/dazzle-php/dazzle). That means the releases are being made concurrently for all of them. On one hand this might lead to "empty" releases for some packages at times, but don't worry. In the end it is far much easier for contributors to maintain and -- what's the most important -- much more straight-forward for users to understand the compatibility and inter-operability of the packages. 112 | 113 | ## Contributing 114 | 115 | Thank you for considering contributing to this repository! 116 | 117 | - The contribution guide can be found in the [contribution tips](https://github.com/dazzle-php/socket/blob/master/CONTRIBUTING.md). 118 | - Open tickets can be found in [issues section](https://github.com/dazzle-php/socket/issues). 119 | - Current contributors are listed in [graphs section](https://github.com/dazzle-php/socket/graphs/contributors) 120 | - To contact the author(s) see the information attached in [composer.json](https://github.com/dazzle-php/socket/blob/master/composer.json) file. 121 | 122 | ## License 123 | 124 | Dazzle Socket is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 125 | 126 |
127 |

128 | "Everything is possible. The impossible just takes longer." ― Dan Brown 129 |

130 | 131 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dazzle-php/socket", 3 | "description": "Dazzle Asynchronous Socket.", 4 | "keywords": [ 5 | "dazzle", "dazzle-php", "ipc", "inter", "inter-process", "message-driven", "sock", "socket", "tcp", "udp", "unix" 6 | ], 7 | "license": "MIT", 8 | "support": { 9 | "issues": "https://github.com/dazzle-php/socket/issues", 10 | "source": "https://github.com/dazzle-php/socket" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Kamil Jamroz", 15 | "homepage": "https://github.com/khelle" 16 | }, 17 | { 18 | "name": "The contributors", 19 | "homepage": "http://github.com/dazzle-php/socket/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.6.7", 24 | "dazzle-php/event": "0.5.*", 25 | "dazzle-php/loop": "0.5.*", 26 | "dazzle-php/stream": "0.5.*", 27 | "dazzle-php/throwable": "0.5.*" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": ">=4.8.0 <5.4.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Dazzle\\Socket\\": "src/Socket", 35 | "Dazzle\\Socket\\Test\\": "test" 36 | } 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "0.5-dev" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /example/bootstrap/autoload.php: -------------------------------------------------------------------------------- 1 | 'client', 7 | ]; 8 | 9 | foreach ($argv as $argIndex=>$argVal) 10 | { 11 | if ($argIndex && preg_match('#^--([^=]+)=(.+)$#si', $argVal, $matches) && $matches) 12 | { 13 | $options[$matches[1]] = $matches[2]; 14 | } 15 | } 16 | 17 | foreach ($options as $optionKey=>$optionVal) 18 | { 19 | $$optionKey = strtolower($optionVal); 20 | } 21 | -------------------------------------------------------------------------------- /example/socket_conn_tcp.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_conn_tcp.php 15 | * 16 | * Following flags are supported to test example: 17 | * 18 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 19 | * 20 | * Remember that to see working communication, you need to create server and at least one client! 21 | * 22 | * $> php ./example/socket_conn_tcp.php --mode=server 23 | * $> php ./example/socket_conn_tcp.php --mode=client 24 | * 25 | * --------------------------------------------------------------------------------------------------------------------- 26 | */ 27 | 28 | $mode = 'client'; 29 | 30 | require_once __DIR__ . '/bootstrap/autoload.php'; 31 | 32 | use Dazzle\Loop\Model\SelectLoop; 33 | use Dazzle\Loop\Loop; 34 | use Dazzle\Socket\Socket; 35 | use Dazzle\Socket\SocketInterface; 36 | use Dazzle\Socket\SocketListener; 37 | 38 | $loop = new Loop(new SelectLoop); 39 | 40 | if ($mode === 'server') 41 | { 42 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop); 43 | 44 | $server->on('connect', function($server, SocketInterface $client) { 45 | printf("New connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 46 | 47 | $client->on('data', function($client, $data) use(&$buffer) { 48 | printf("Received message=\"%s\"\n", $data); 49 | }); 50 | $client->on('close', function() use($res) { 51 | printf("Closed connection #$res\n"); 52 | }); 53 | }); 54 | $server->start(); 55 | } 56 | 57 | if ($mode === 'client') 58 | { 59 | $socket = new Socket('tcp://127.0.0.1:2080', $loop); 60 | $socket->on('close', function() use($loop) { 61 | printf("Server has closed the connection!\n"); 62 | $loop->stop(); 63 | }); 64 | 65 | $loop->addPeriodicTimer(1, function() use($socket) { 66 | $socket->write('Hello World!'); 67 | }); 68 | } 69 | 70 | $loop->start(); 71 | -------------------------------------------------------------------------------- /example/socket_conn_tcp_ipv6.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_conn_tcp_ipv6.php 15 | * 16 | * Following flags are supported to test example: 17 | * 18 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 19 | * 20 | * Remember that to see working communication, you need to create server and at least one client! 21 | * 22 | * $> php ./example/socket_conn_tcp_ipv6.php --mode=server 23 | * $> php ./example/socket_conn_tcp_ipv6.php --mode=client 24 | * 25 | * --------------------------------------------------------------------------------------------------------------------- 26 | */ 27 | 28 | $mode = 'client'; 29 | 30 | require_once __DIR__ . '/bootstrap/autoload.php'; 31 | 32 | use Dazzle\Loop\Model\SelectLoop; 33 | use Dazzle\Loop\Loop; 34 | use Dazzle\Socket\Socket; 35 | use Dazzle\Socket\SocketInterface; 36 | use Dazzle\Socket\SocketListener; 37 | 38 | $loop = new Loop(new SelectLoop); 39 | 40 | if ($mode === 'server') 41 | { 42 | $server = new SocketListener('tcp://[::1]:2080', $loop); 43 | 44 | $server->on('connect', function($server, SocketInterface $client) { 45 | printf("New connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 46 | 47 | $client->on('data', function($client, $data) use(&$buffer) { 48 | printf("Received message=\"%s\"\n", $data); 49 | }); 50 | $client->on('close', function() use($res) { 51 | printf("Closed connection #$res\n"); 52 | }); 53 | }); 54 | $server->start(); 55 | } 56 | 57 | if ($mode === 'client') 58 | { 59 | $socket = new Socket('tcp://[::1]:2080', $loop); 60 | $socket->on('close', function() use($loop) { 61 | printf("Server has closed the connection!\n"); 62 | $loop->stop(); 63 | }); 64 | 65 | $loop->addPeriodicTimer(1, function() use($socket) { 66 | $socket->write('Hello World!'); 67 | }); 68 | } 69 | 70 | $loop->start(); 71 | -------------------------------------------------------------------------------- /example/socket_conn_unix.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_conn_unix.php 16 | * 17 | * Following flags are supported to test example: 18 | * 19 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 20 | * 21 | * Remember that to see working communication, you need to create server and at least one client! 22 | * 23 | * $> php ./example/socket_conn_unix.php --mode=server 24 | * $> php ./example/socket_conn_unix.php --mode=client 25 | * 26 | * --------------------------------------------------------------------------------------------------------------------- 27 | */ 28 | 29 | $mode = 'client'; 30 | 31 | require_once __DIR__ . '/bootstrap/autoload.php'; 32 | 33 | use Dazzle\Loop\Model\SelectLoop; 34 | use Dazzle\Loop\Loop; 35 | use Dazzle\Socket\Socket; 36 | use Dazzle\Socket\SocketInterface; 37 | use Dazzle\Socket\SocketListener; 38 | 39 | $loop = new Loop(new SelectLoop); 40 | 41 | if ($mode === 'server') 42 | { 43 | $server = new SocketListener('unix://' . __DIR__ . '/mysock.sock', $loop); 44 | 45 | $server->on('connect', function($server, SocketInterface $client) { 46 | printf("New connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 47 | 48 | $client->on('data', function($client, $data) use(&$buffer) { 49 | printf("Received message=\"%s\"\n", $data); 50 | }); 51 | $client->on('close', function() use($res) { 52 | printf("Closed connection #$res\n"); 53 | }); 54 | }); 55 | $server->start(); 56 | } 57 | 58 | if ($mode === 'client') 59 | { 60 | $socket = new Socket('unix://' . __DIR__ . '/mysock.sock', $loop); 61 | $socket->on('close', function() use($loop) { 62 | printf("Server has closed the connection!\n"); 63 | $loop->stop(); 64 | }); 65 | 66 | $loop->addPeriodicTimer(1, function() use($socket) { 67 | $socket->write('Hello World!'); 68 | }); 69 | } 70 | 71 | $loop->start(); 72 | -------------------------------------------------------------------------------- /example/socket_info.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_info.php 16 | * 17 | * Following flags are supported to test example: 18 | * 19 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 20 | * 21 | * Remember that to see working communication, you need to create server and at least one client! 22 | * 23 | * $> php ./example/socket_info.php --mode=server 24 | * $> php ./example/socket_info.php --mode=client 25 | * 26 | * --------------------------------------------------------------------------------------------------------------------- 27 | */ 28 | 29 | $mode = 'client'; 30 | 31 | require_once __DIR__ . '/bootstrap/autoload.php'; 32 | 33 | use Dazzle\Loop\Model\SelectLoop; 34 | use Dazzle\Loop\Loop; 35 | use Dazzle\Socket\Socket; 36 | use Dazzle\Socket\SocketInterface; 37 | use Dazzle\Socket\SocketListener; 38 | 39 | $loop = new Loop(new SelectLoop); 40 | 41 | if ($mode === 'server') 42 | { 43 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop); 44 | 45 | $server->on('connect', function($server, SocketInterface $client) { 46 | 47 | printf("%s\n", str_repeat('-', 42)); 48 | printf("%s\n", 'Client info:'); 49 | printf("%s\n", str_repeat('-', 42)); 50 | printf("%-20s%s\n", 'Resource ID:', '#' . $client->getResourceId()); 51 | printf("%-20s%s\n", 'Local endpoint:', $client->getLocalEndpoint()); 52 | printf("%-20s%s\n", 'Local protocol:', $client->getLocalProtocol()); 53 | printf("%-20s%s\n", 'Local address:', $client->getLocalAddress()); 54 | printf("%-20s%s\n", 'Local host:', $client->getLocalHost()); 55 | printf("%-20s%s\n", 'Local port:', $client->getLocalPort()); 56 | printf("%-20s%s\n", 'Remote endpoint:', $client->getRemoteEndpoint()); 57 | printf("%-20s%s\n", 'Remote protocol:', $client->getRemoteProtocol()); 58 | printf("%-20s%s\n", 'Remote address:', $client->getRemoteAddress()); 59 | printf("%-20s%s\n", 'Remote host:', $client->getRemoteHost()); 60 | printf("%-20s%s\n", 'Remote port:', $client->getRemotePort()); 61 | printf("%s\n", str_repeat('-', 42)); 62 | 63 | $client->close(); 64 | }); 65 | $server->start(); 66 | } 67 | 68 | if ($mode === 'client') 69 | { 70 | $socket = new Socket('tcp://127.0.0.1:2080', $loop); 71 | $socket->on('close', function() use($loop) { 72 | $loop->stop(); 73 | }); 74 | } 75 | 76 | $loop->start(); 77 | -------------------------------------------------------------------------------- /example/socket_only_client.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_only_client.php 15 | * 16 | * --------------------------------------------------------------------------------------------------------------------- 17 | */ 18 | 19 | require_once __DIR__ . '/bootstrap/autoload.php'; 20 | 21 | use Dazzle\Loop\Model\SelectLoop; 22 | use Dazzle\Loop\Loop; 23 | use Dazzle\Socket\Socket; 24 | use Dazzle\Socket\SocketInterface; 25 | 26 | $loop = new Loop(new SelectLoop); 27 | 28 | $socket = new Socket('tcp://127.0.0.1:2080', $loop); 29 | 30 | $socket->on('data', function(SocketInterface $client, $data) use(&$buffer) { 31 | printf("%s\n", $data); 32 | $client->close(); 33 | }); 34 | $socket->on('close', function() use($loop) { 35 | $loop->stop(); 36 | }); 37 | 38 | $loop->onStart(function() use($socket) { 39 | $socket->write('Hello!'); 40 | }); 41 | 42 | $loop->start(); 43 | -------------------------------------------------------------------------------- /example/socket_only_server.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_only_server.php 15 | * 16 | * --------------------------------------------------------------------------------------------------------------------- 17 | */ 18 | 19 | require_once __DIR__ . '/bootstrap/autoload.php'; 20 | 21 | use Dazzle\Loop\Model\SelectLoop; 22 | use Dazzle\Loop\Loop; 23 | use Dazzle\Socket\SocketInterface; 24 | use Dazzle\Socket\SocketListener; 25 | 26 | $loop = new Loop(new SelectLoop); 27 | 28 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop); 29 | 30 | $server->on('connect', function($server, SocketInterface $client) { 31 | printf("New secure connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 32 | 33 | $client->on('data', function(SocketInterface $client, $data) use(&$buffer) { 34 | printf("%s\n", $data); 35 | $client->write('secret answer!'); 36 | }); 37 | $client->on('close', function() use($res) { 38 | printf("Closed connection #$res\n"); 39 | }); 40 | }); 41 | $server->start(); 42 | 43 | $loop->start(); 44 | -------------------------------------------------------------------------------- /example/socket_quickstart.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_quickstart.php 15 | * 16 | * Following flags are supported to test example: 17 | * 18 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 19 | * 20 | * Remember that to see working communication, you need to create server and at least one client! 21 | * 22 | * $> php ./example/socket_quickstart.php --mode=server 23 | * $> php ./example/socket_quickstart.php --mode=client 24 | * 25 | * --------------------------------------------------------------------------------------------------------------------- 26 | */ 27 | 28 | $mode = 'client'; 29 | 30 | require_once __DIR__ . '/bootstrap/autoload.php'; 31 | 32 | use Dazzle\Loop\Model\SelectLoop; 33 | use Dazzle\Loop\Loop; 34 | use Dazzle\Socket\Socket; 35 | use Dazzle\Socket\SocketInterface; 36 | use Dazzle\Socket\SocketListener; 37 | 38 | $loop = new Loop(new SelectLoop); 39 | 40 | if ($mode === 'server') 41 | { 42 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop); 43 | 44 | $server->on('connect', function($server, SocketInterface $client) { 45 | $client->write("Hello!\n"); 46 | $client->write("Welcome to Dazzle server!\n"); 47 | $client->write("Tell me a range and I will randomize a number for you!\n\n"); 48 | 49 | $client->on('data', function(SocketInterface $client, $data) use(&$buffer) { 50 | $client->write("Your number is: " . rand(...explode('-', $data))); 51 | }); 52 | }); 53 | $server->start(); 54 | } 55 | 56 | if ($mode === 'client') 57 | { 58 | $socket = new Socket('tcp://127.0.0.1:2080', $loop); 59 | $socket->on('data', function($socket, $data) { 60 | printf("%s", $data); 61 | }); 62 | $socket->write('1-100'); 63 | } 64 | 65 | $loop->start(); 66 | -------------------------------------------------------------------------------- /example/socket_ssl.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_ssl.php 16 | * 17 | * Following flags are supported to test example: 18 | * 19 | * --mode : define whether socket or client should be created, default: standard, supported: [ client, server ] 20 | * 21 | * Remember that to see working communication, you need to create server and at least one client! 22 | * 23 | * $> php ./example/socket_ssl.php --mode=server 24 | * $> php ./example/socket_ssl.php --mode=client 25 | * 26 | * --------------------------------------------------------------------------------------------------------------------- 27 | */ 28 | 29 | $mode = 'client'; 30 | 31 | require_once __DIR__ . '/bootstrap/autoload.php'; 32 | 33 | use Dazzle\Loop\Model\SelectLoop; 34 | use Dazzle\Loop\Loop; 35 | use Dazzle\Socket\Socket; 36 | use Dazzle\Socket\SocketInterface; 37 | use Dazzle\Socket\SocketListener; 38 | 39 | $loop = new Loop(new SelectLoop); 40 | 41 | if ($mode === 'server') 42 | { 43 | // additional supported configuration options: 44 | // ssl_method 45 | // ssl_name 46 | // ssl_verify_sign 47 | // ssl_verify_peer 48 | // ssl_verify_depth 49 | $config = [ 50 | 'ssl' => true, 51 | 'ssl_cert' => __DIR__ . '/ssl/_ssl_cert.pem', 52 | 'ssl_key' => __DIR__ . '/ssl/_ssl_key.pem', 53 | 'ssl_secret' => 'secret', 54 | ]; 55 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop, $config); 56 | 57 | $server->on('connect', function($server, SocketInterface $client) { 58 | printf("New secure connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 59 | 60 | $client->on('data', function(SocketInterface $client, $data) use(&$buffer) { 61 | printf("Received question=\"%s\"\n", $data); 62 | $client->write('secret answer!'); 63 | }); 64 | $client->on('close', function() use($res) { 65 | printf("Closed connection #$res\n"); 66 | }); 67 | }); 68 | $server->start(); 69 | } 70 | 71 | if ($mode === 'client') 72 | { 73 | // additional supported configuration options: 74 | // ssl_method 75 | // ssl_name 76 | // ssl_verify_sign 77 | // ssl_verify_peer 78 | // ssl_verify_depth 79 | $config = [ 80 | 'ssl' => true 81 | ]; 82 | $socket = new Socket('tcp://127.0.0.1:2080', $loop, $config); 83 | 84 | $socket->on('data', function(SocketInterface $client, $data) use(&$buffer) { 85 | printf("Received answer=\"%s\"\n", $data); 86 | }); 87 | $socket->on('close', function() use($loop) { 88 | printf("Server has closed the connection!\n"); 89 | $loop->stop(); 90 | }); 91 | 92 | $loop->addPeriodicTimer(1, function() use($socket) { 93 | $socket->write('secret question!'); 94 | }); 95 | } 96 | 97 | $loop->start(); 98 | -------------------------------------------------------------------------------- /example/socket_ssl_only_client.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_ssl_only_client.php 15 | * 16 | * --------------------------------------------------------------------------------------------------------------------- 17 | */ 18 | 19 | require_once __DIR__ . '/bootstrap/autoload.php'; 20 | 21 | use Dazzle\Loop\Model\SelectLoop; 22 | use Dazzle\Loop\Loop; 23 | use Dazzle\Socket\Socket; 24 | use Dazzle\Socket\SocketInterface; 25 | 26 | $loop = new Loop(new SelectLoop); 27 | 28 | $config = [ 29 | 'ssl' => true 30 | ]; 31 | $socket = new Socket('tcp://127.0.0.1:2080', $loop, $config); 32 | 33 | $socket->on('data', function(SocketInterface $client, $data) use(&$buffer) { 34 | printf("%s\n", $data); 35 | $client->close(); 36 | }); 37 | $socket->on('close', function() use($loop) { 38 | $loop->stop(); 39 | }); 40 | 41 | $loop->onStart(function() use($socket) { 42 | $socket->write('Hello!'); 43 | }); 44 | 45 | $loop->start(); 46 | -------------------------------------------------------------------------------- /example/socket_ssl_only_server.php: -------------------------------------------------------------------------------- 1 | php ./example/socket_ssl_only_server.php 15 | * 16 | * --------------------------------------------------------------------------------------------------------------------- 17 | */ 18 | 19 | require_once __DIR__ . '/bootstrap/autoload.php'; 20 | 21 | use Dazzle\Loop\Model\SelectLoop; 22 | use Dazzle\Loop\Loop; 23 | use Dazzle\Socket\SocketInterface; 24 | use Dazzle\Socket\SocketListener; 25 | 26 | $loop = new Loop(new SelectLoop); 27 | 28 | // additional supported configuration options: 29 | // ssl_method 30 | // ssl_name 31 | // ssl_verify_sign 32 | // ssl_verify_peer 33 | // ssl_verify_depth 34 | $config = [ 35 | 'ssl' => true, 36 | 'ssl_cert' => __DIR__ . '/ssl/_ssl_cert.pem', 37 | 'ssl_key' => __DIR__ . '/ssl/_ssl_key.pem', 38 | 'ssl_secret' => 'secret', 39 | ]; 40 | $server = new SocketListener('tcp://127.0.0.1:2080', $loop, $config); 41 | 42 | $server->on('connect', function($server, SocketInterface $client) { 43 | printf("New secure connection #%s from %s!\n", $res = $client->getResourceId(), $client->getLocalAddress()); 44 | 45 | $client->on('data', function(SocketInterface $client, $data) use(&$buffer) { 46 | printf("%s\n", $data); 47 | $client->write('secret answer!'); 48 | }); 49 | $client->on('close', function() use($res) { 50 | printf("Closed connection #$res\n"); 51 | }); 52 | }); 53 | $server->start(); 54 | 55 | $loop->start(); 56 | -------------------------------------------------------------------------------- /example/ssl/_ssl_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9jCCAd4CCQC1n9FarkDiuTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCU0gxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 4 | ZDAeFw0xNzAxMjcwMjI0MjNaFw0xODAxMjcwMjI0MjNaMD0xCzAJBgNVBAYTAkNO 5 | MQswCQYDVQQIDAJTSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 6 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6P2tbxw2JBEr/jI6dyU/ 7 | ppopZ3XbUybS4A3V2G/k5wXocjPOx2ttb4J3cClKYzyyG96AFRftmkjps1ixxknO 8 | ZlXWVJ+Cdgw3RXn0kt9s8zjYtGKFnDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUf 9 | poAVkeohGwVQQKjiBANZo4Civwae71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EY 10 | OQQTb18UVEdKyVM8RQbpnE3DZ0tdB7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66CO 11 | q2rI3CN4mNYBybNrfjby5FK8Qgnu8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD 12 | +wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBWSUEWA161OYo9XM6PVqP++RQyIq7d 13 | YFgdwgxjZTUINuOlBQnQEPbCskeCEaxEK/APkyGfV8M+b8NzLgXSJJAXjI6tfBmR 14 | CMLXk/cTGImAd1c6KQ3wCPQqbn8bxCaC1ca9MrJPqB4EOjIiVw+zBNXS6n3kDqkD 15 | a6N3DfBXZycY6tfOSnbej6q2/hPzgqP2VquNvsiloRpay5i+yfcx3rbh/bJVHsb7 16 | 1YoqYZU/Kn88Lt2U8Qhwkag2qAvSd7uGAldxNkzgbAXZ3QneoOKFcWCvGLbS3XjN 17 | mMWkYg8EHsks6PzLRtQesHKiq8t92JdZsxvasMXMNAs9GHDQ88En8iUL 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /example/ssl/_ssl_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA6P2tbxw2JBEr/jI6dyU/ppopZ3XbUybS4A3V2G/k5wXocjPO 3 | x2ttb4J3cClKYzyyG96AFRftmkjps1ixxknOZlXWVJ+Cdgw3RXn0kt9s8zjYtGKF 4 | nDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUfpoAVkeohGwVQQKjiBANZo4Civwae 5 | 71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EYOQQTb18UVEdKyVM8RQbpnE3DZ0td 6 | B7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66COq2rI3CN4mNYBybNrfjby5FK8Qgnu 7 | 8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD+wIDAQABAoIBAGr6BwbsMiOvSr7F 8 | Ozj3flk1kWsynwh32/0J/TUKc1RrzwWYqgnAVpgLlGGIY+JavQC00aRdvzfG4J77 9 | S2za+IzePrHOJ22iQEKDM3u4YGLwp/dzARvsyJwdEUEwGIfLUCxzHTGnOHrrC3hL 10 | tda2AvlE+CJuGNM6RlXZ9SSYkTDTgBORkuAJf4pTZrG5wVedTGR0us1lj4BKzuDF 11 | MsApnBum8Dpj73nZCcfT9MMgbuXGp1jsDuE7eZsiDzklarroZxOcYX9YMTpsG2iY 12 | f2drGWSVssZKwmxaTiAHi4iMIhawxQwE13PxK5FqP1DQjQYmPjoDY5CQ1/OD6oGJ 13 | klRpEykCgYEA+XDlDHRP3UK1UpZJY6heudRgdbuEdd4vFbEVG6Kk1vh3ub6Y33UJ 14 | 1nvHIvjzCOEnjO5wZbBpoJy1O2fCJt/rdP1nWI2SLXtBIv4floia1SK5S1fmrr4C 15 | 1uDAB3+/PAJe5gSBX6l9dt1JesB6ERrrVYU05GCnW64T+kDg19IZaUUCgYEA7x4M 16 | sOGXjdunmk5DmgaAZcKKUXm6G5+DGHCiHzylUr3LsJ4dqBMFYDfA0s2YNlT+pKpK 17 | BEDBxfzlx1ewaorr2Hge3QAHeGWa4oQok42u7rv1ernl/RYOfI3JcZaX89cu482b 18 | /tFUjeMDfxqiD74hl2JtK3vkBTfldApPS10ZbD8CgYEA4S1S+C0/s4P23/kfg0GJ 19 | UcQw19dRun26j6BuZ6YpnI5stlci9gZXvTG8EQwfiZKH+6d7+7CHiiAtUtVV5XYa 20 | vl/LnZsfrMMpiAdUSLZE38ca8rMFYhXRxlzZvWtLHUcemBLVH6CHzHfzT0bWav6c 21 | F3XKy6edLRw2mKFc2DeoDOkCgYEAndDhY2Sg2Bme9rKs311nJMJO9BM5B74xkdHU 22 | znUCUCe/5eOVgzZ6l9R0SS00Rre2EQvrKf9rZLbTGMwBPXBO1GIDTK0WQRoeLV44 23 | QZqDWEFpdQR4jJ8gFIin6XYQ9/iPk/5B9N5HxhWMWzlCuM0t8nIdx1NPXTpVEE81 24 | onHhs5UCgYA6y5LpvJ5vOpZyHdTISgQK+4SmhUSu3e29qE8AjixaTBFwGa/PKuVO 25 | ggpceNZjl+pg1F74IMEpg1c6N1MKEIhK+NEYhxS9QkShPNYwasfkhg87QfkOxUi+ 26 | jed5G+yO01Re0bkyE6YiS+JrJQZJEdna/x9eb5IxF/LiTWkZhkdywg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | test/TModule 21 | 22 | 23 | 24 | test/TUnit 25 | 26 | 27 | 28 | 29 | 30 | src 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Socket/Socket.php: -------------------------------------------------------------------------------- 1 | configure($config); 111 | 112 | if (!is_resource($endpointOrResource)) 113 | { 114 | $endpointOrResource = $this->createClient($endpointOrResource, $config); 115 | } 116 | 117 | parent::__construct($endpointOrResource, $loop); 118 | 119 | $this->read(); 120 | } 121 | catch (Error $ex) 122 | { 123 | throw new InstantiationException('SocketClient could not be created.', 0, $ex); 124 | } 125 | catch (Exception $ex) 126 | { 127 | throw new InstantiationException('SocketClient could not be created.', 0, $ex); 128 | } 129 | } 130 | 131 | /** 132 | * @override 133 | * @inheritDoc 134 | */ 135 | public function stop() 136 | { 137 | $this->close(); 138 | } 139 | 140 | /** 141 | * @override 142 | * @inheritDoc 143 | */ 144 | public function getLocalEndpoint() 145 | { 146 | return $this->parseEndpoint(false); 147 | } 148 | 149 | /** 150 | * @override 151 | * @inheritDoc 152 | */ 153 | public function getRemoteEndpoint() 154 | { 155 | return $this->parseEndpoint(true); 156 | } 157 | 158 | /** 159 | * @override 160 | * @inheritDoc 161 | */ 162 | public function getLocalAddress() 163 | { 164 | $endpoint = explode('://', $this->getLocalEndpoint(), 2); 165 | 166 | return isset($endpoint[1]) ? $endpoint[1] : $endpoint[0]; 167 | } 168 | 169 | /** 170 | * @override 171 | * @inheritDoc 172 | */ 173 | public function getLocalHost() 174 | { 175 | $address = explode(':', $this->getLocalAddress(), 2); 176 | 177 | return $address[0]; 178 | } 179 | 180 | /** 181 | * @override 182 | * @inheritDoc 183 | */ 184 | public function getLocalPort() 185 | { 186 | $address = explode(':', $this->getLocalAddress(), 2); 187 | 188 | return isset($address[1]) ? $address[1] : ''; 189 | } 190 | 191 | /** 192 | * @override 193 | * @inheritDoc 194 | */ 195 | public function getLocalProtocol() 196 | { 197 | $endpoint = explode('://', $this->getLocalEndpoint(), 2); 198 | 199 | return isset($endpoint[0]) ? $endpoint[0] : ''; 200 | } 201 | 202 | /** 203 | * @override 204 | * @inheritDoc 205 | */ 206 | public function getRemoteAddress() 207 | { 208 | $endpoint = explode('://', $this->getRemoteEndpoint(), 2); 209 | 210 | return isset($endpoint[1]) ? $endpoint[1] : $endpoint[0]; 211 | } 212 | 213 | /** 214 | * @override 215 | * @inheritDoc 216 | */ 217 | public function getRemoteHost() 218 | { 219 | $address = explode(':', $this->getRemoteAddress(), 2); 220 | 221 | return $address[0]; 222 | } 223 | 224 | /** 225 | * @override 226 | * @inheritDoc 227 | */ 228 | public function getRemotePort() 229 | { 230 | $address = explode(':', $this->getRemoteAddress(), 2); 231 | 232 | return isset($address[1]) ? $address[1] : ''; 233 | } 234 | 235 | /** 236 | * @override 237 | * @inheritDoc 238 | */ 239 | public function getRemoteProtocol() 240 | { 241 | $endpoint = explode('://', $this->getRemoteEndpoint(), 2); 242 | 243 | return isset($endpoint[0]) ? $endpoint[0] : ''; 244 | } 245 | 246 | /** 247 | * @override 248 | * @inheritDoc 249 | */ 250 | public function isEncrypted() 251 | { 252 | return $this->crypto !== 0; 253 | } 254 | 255 | /** 256 | * Create the client resource. 257 | * 258 | * This method creates client resource for socket connections. 259 | * 260 | * @param string $endpoint 261 | * @param mixed[] $config 262 | * @return resource 263 | * @throws LogicException 264 | */ 265 | protected function createClient($endpoint, $config = []) 266 | { 267 | $ssl = $this->config['ssl']; 268 | $name = $this->config['ssl_name']; 269 | $verifySign = $this->config['ssl_verify_sign']; 270 | $verifyPeer = $this->config['ssl_verify_peer']; 271 | $verifyDepth = $this->config['ssl_verify_depth']; 272 | 273 | $context = []; 274 | $context['socket'] = [ 275 | 'connect' => $endpoint 276 | ]; 277 | $context['ssl'] = [ 278 | 'capture_peer_cert' => true, 279 | 'capture_peer_chain' => true, 280 | 'capture_peer_cert_chain' => true, 281 | 'allow_self_signed' => !$verifySign, 282 | 'verify_peer' => $verifyPeer, 283 | 'verify_peer_name' => $verifyPeer, 284 | 'verify_depth' => $verifyDepth, 285 | 'SNI_enabled' => $name !== '', 286 | 'SNI_server_name' => $name, 287 | 'peer_name' => $name, 288 | 'disable_compression' => true, 289 | 'honor_cipher_order' => true, 290 | ]; 291 | 292 | if ($ssl && isset($config['ssl_cafile'])) 293 | { 294 | $context['ssl']['cafile'] = $config['ssl_cafile']; 295 | } 296 | 297 | if ($ssl && isset($config['ssl_capath'])) 298 | { 299 | $context['ssl']['capath'] = $config['ssl_capath']; 300 | } 301 | 302 | $context = stream_context_create($context); 303 | // Error reporting suppressed since stream_socket_client() emits an E_WARNING on failure. 304 | $socket = @stream_socket_client( 305 | $endpoint, 306 | $errno, 307 | $errstr, 308 | 0, // Timeout does not apply for async connect. 309 | STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT, 310 | $context 311 | ); 312 | 313 | if (!$socket || $errno > 0) 314 | { 315 | throw new LogicException( 316 | sprintf('Could not connect socket [%s] because of error [%d; %s]', $endpoint, $errno, $errstr) 317 | ); 318 | } 319 | 320 | stream_set_blocking($socket, false); 321 | 322 | return $socket; 323 | } 324 | 325 | /** 326 | * Handle socket encryption. 327 | * 328 | * @internal 329 | */ 330 | public function handleEncrypt() 331 | { 332 | $ex = null; 333 | 334 | try 335 | { 336 | if ($this->isEncrypted()) 337 | { 338 | return; 339 | } 340 | 341 | $this->encrypt($this->config['ssl_method']); 342 | 343 | if ($this->isEncrypted()) 344 | { 345 | $this->pause(); 346 | $this->resume(); 347 | } 348 | } 349 | catch (Error $ex) 350 | {} 351 | catch (Exception $ex) 352 | {} 353 | 354 | if ($ex !== null) 355 | { 356 | $this->close(); 357 | $this->emit('error', [ $this, $ex ]); 358 | } 359 | } 360 | 361 | /** 362 | * @internal 363 | * @override 364 | * @inheritDoc 365 | */ 366 | public function handleRead() 367 | { 368 | $ex = null; 369 | 370 | try 371 | { 372 | $data = fread($this->resource, $this->bufferSize); 373 | 374 | if ($data !== '' && $data !== false) 375 | { 376 | $this->emit('data', [ $this, $data ]); 377 | $this->emit('end', [ $this ]); 378 | } 379 | 380 | if ($data === '' || $data === false || !is_resource($this->resource) || feof($this->resource)) 381 | { 382 | $this->close(); 383 | } 384 | } 385 | catch (Error $ex) 386 | {} 387 | catch (Exception $ex) 388 | {} 389 | 390 | if ($ex !== null) 391 | { 392 | $this->emit('error', [ $this, $ex ]); 393 | } 394 | } 395 | 396 | /** 397 | * @internal 398 | * @override 399 | * @inheritDoc 400 | */ 401 | public function handleWrite() 402 | { 403 | $ex = null; 404 | 405 | try 406 | { 407 | parent::handleWrite(); 408 | } 409 | catch (Error $ex) 410 | {} 411 | catch (Exception $ex) 412 | {} 413 | 414 | if ($ex !== null) 415 | { 416 | $this->emit('error', [ $this, $ex ]); 417 | } 418 | } 419 | 420 | /** 421 | * @internal 422 | * @override 423 | * @inheritDoc 424 | */ 425 | public function handleClose() 426 | { 427 | $this->pause(); 428 | 429 | if (is_resource($this->resource)) 430 | { 431 | // http://chat.stackoverflow.com/transcript/message/7727858#7727858 432 | stream_socket_shutdown($this->resource, STREAM_SHUT_RDWR); 433 | stream_set_blocking($this->resource, 0); 434 | fclose($this->resource); 435 | } 436 | } 437 | 438 | /** 439 | * Get function that should be invoked on read event. 440 | * 441 | * @return callable 442 | */ 443 | protected function getHandleReadFunction() 444 | { 445 | return $this->config['ssl'] === true && !$this->isEncrypted() 446 | ? [ $this, 'handleEncrypt' ] 447 | : [ $this, 'handleRead' ]; 448 | } 449 | 450 | /** 451 | * Get function that should be invoked on write event. 452 | * 453 | * @return callable 454 | */ 455 | protected function getHandleWriteFunction() 456 | { 457 | return $this->config['ssl'] === true && !$this->isEncrypted() 458 | ? [ $this, 'handleEncrypt' ] 459 | : [ $this, 'handleWrite' ]; 460 | } 461 | 462 | /** 463 | * Configure socket. 464 | * 465 | * @param string[] $config 466 | */ 467 | private function configure($config = []) 468 | { 469 | $this->config = $config; 470 | 471 | $this->configureVariable('ssl'); 472 | $this->configureVariable('ssl_method'); 473 | $this->configureVariable('ssl_name'); 474 | $this->configureVariable('ssl_verify_sign'); 475 | $this->configureVariable('ssl_verify_peer'); 476 | $this->configureVariable('ssl_verify_depth'); 477 | } 478 | 479 | /** 480 | * Configure config variable. 481 | * 482 | * @param $configKey 483 | */ 484 | private function configureVariable($configKey) 485 | { 486 | $configStaticKey = 'CONFIG_DEFAULT_' . strtoupper($configKey); 487 | $this->config[$configKey] = isset($this->config[$configKey]) ? $this->config[$configKey] : constant("static::$configStaticKey"); 488 | } 489 | 490 | /** 491 | * @param bool $wantPeer 492 | * @return string 493 | */ 494 | private function parseEndpoint($wantPeer = false) 495 | { 496 | $wantIndex = (int)$wantPeer; 497 | 498 | if (isset($this->cachedEndpoint[$wantIndex])) 499 | { 500 | return $this->cachedEndpoint[$wantIndex]; 501 | } 502 | 503 | if (get_resource_type($this->resource) === Socket::TYPE_UNKNOWN) 504 | { 505 | return ''; 506 | } 507 | 508 | $name = stream_socket_get_name($this->resource, $wantPeer); 509 | $type = $this->getStreamType(); 510 | 511 | switch ($type) 512 | { 513 | case Socket::TYPE_UNIX: 514 | $transport = 'unix://'; 515 | $endpoint = $transport . $name; 516 | break; 517 | 518 | case Socket::TYPE_TCP: 519 | $transport = 'tcp://'; 520 | if (substr_count($name, ':') > 1) 521 | { 522 | $parts = explode(':', $name); 523 | $count = count($parts); 524 | $port = $parts[$count - 1]; 525 | unset($parts[$count - 1]); 526 | $endpoint = $transport.'[' . implode(':', $parts) . ']:' . $port; 527 | } 528 | else 529 | { 530 | $endpoint = $transport . $name; 531 | } 532 | break; 533 | 534 | case Socket::TYPE_UDP: 535 | $transport = 'udp://'; 536 | $endpoint = $transport . $name; 537 | break; 538 | 539 | default: 540 | $endpoint = ''; 541 | } 542 | 543 | $this->cachedEndpoint[$wantIndex] = $endpoint; 544 | 545 | return $endpoint; 546 | } 547 | 548 | /** 549 | * @override 550 | * @inheritDoc 551 | */ 552 | private function encrypt($method) 553 | { 554 | $type = $this->selectCryptoType($method); 555 | 556 | if ($type === self::CRYPTO_TYPE_UNKNOWN) 557 | { 558 | throw new ExecutionException('Socket encryption method is invalid!'); 559 | } 560 | 561 | $resource = $this->getResource(); 562 | 563 | if ($type === self::CRYPTO_TYPE_SERVER && $this->cryptoMethod === 0) 564 | { 565 | $raw = @stream_socket_recvfrom($resource, 11, STREAM_PEEK); 566 | 567 | if ($raw === '') 568 | { 569 | return; 570 | } 571 | 572 | if (11 > strlen($raw)) 573 | { 574 | throw new ExecutionException('Failed to read crypto handshake.'); 575 | } 576 | 577 | $data = unpack('ctype/nversion/nlength/Nembed/nmax-version', $raw); 578 | if (0x16 !== $data['type']) 579 | { 580 | throw new ExecutionException('Invalid crypto handshake.'); 581 | } 582 | 583 | // Check if version was available in $method. 584 | $version = $this->selectCryptoVersion($data['max-version']); 585 | if ($method & $version) 586 | { 587 | $method = $version; 588 | } 589 | } 590 | 591 | $this->cryptoMethod = $method; 592 | $result = @stream_socket_enable_crypto($resource, true, $this->cryptoMethod); 593 | 594 | if ($result === 0) 595 | { 596 | return; 597 | } 598 | else if (!$result) 599 | { 600 | $message = 'Socket encryption failed.'; 601 | if ($error = error_get_last()) 602 | { 603 | $message .= sprintf(' Errno: %d; %s', $error['type'], $error['message']); 604 | } 605 | throw new ExecutionException($message); 606 | } 607 | else 608 | { 609 | $this->crypto = $this->cryptoMethod; 610 | } 611 | } 612 | 613 | /** 614 | * Checks type of crypto. 615 | * 616 | * @param int $version 617 | * @return int 618 | */ 619 | private function selectCryptoType($version) 620 | { 621 | switch ($version) 622 | { 623 | case STREAM_CRYPTO_METHOD_SSLv3_SERVER: 624 | case STREAM_CRYPTO_METHOD_TLSv1_0_SERVER: 625 | case STREAM_CRYPTO_METHOD_TLSv1_1_SERVER: 626 | case STREAM_CRYPTO_METHOD_TLSv1_2_SERVER: 627 | return self::CRYPTO_TYPE_SERVER; 628 | 629 | case STREAM_CRYPTO_METHOD_SSLv3_CLIENT: 630 | case STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT: 631 | case STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT: 632 | case STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT: 633 | return self::CRYPTO_TYPE_CLIENT; 634 | 635 | default: 636 | return self::CRYPTO_TYPE_UNKNOWN; 637 | } 638 | } 639 | 640 | /** 641 | * Returns highest supported crypto method constant based on protocol version identifier. 642 | * 643 | * @param int $version 644 | * @return int 645 | */ 646 | private function selectCryptoVersion($version) 647 | { 648 | switch ($version) 649 | { 650 | case 0x300: return STREAM_CRYPTO_METHOD_SSLv3_SERVER; 651 | case 0x301: return STREAM_CRYPTO_METHOD_TLSv1_0_SERVER; 652 | case 0x302: return STREAM_CRYPTO_METHOD_TLSv1_1_SERVER; 653 | default: return STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; 654 | } 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /src/Socket/SocketInterface.php: -------------------------------------------------------------------------------- 1 | configure($config); 87 | $this->endpoint = $endpointOrResource; 88 | $this->socket = null; 89 | $this->loop = $loop; 90 | $this->started = false; 91 | $this->paused = true; 92 | } 93 | 94 | /** 95 | * 96 | */ 97 | public function __destruct() 98 | { 99 | $this->handleClose(); 100 | 101 | parent::__destruct(); 102 | 103 | unset($this->socket); 104 | unset($this->loop); 105 | unset($this->started); 106 | unset($this->paused); 107 | unset($this->config); 108 | unset($this->endpoint); 109 | } 110 | 111 | /** 112 | * @override 113 | * @inheritDoc 114 | */ 115 | public function start() 116 | { 117 | if ($this->isOpen()) 118 | { 119 | return; 120 | } 121 | 122 | try 123 | { 124 | if (!is_resource($this->endpoint)) 125 | { 126 | $this->socket = $this->createServer($this->endpoint, $this->config); 127 | } 128 | else 129 | { 130 | $this->socket = &$this->endpoint; 131 | } 132 | 133 | $this->resume(); 134 | $this->started = true; 135 | } 136 | catch (Error $ex) 137 | { 138 | throw new InstantiationException('SocketListener could not be created.', 0, $ex); 139 | } 140 | catch (Exception $ex) 141 | { 142 | throw new InstantiationException('SocketListener could not be created.', 0, $ex); 143 | } 144 | } 145 | 146 | /** 147 | * @override 148 | * @inheritDoc 149 | */ 150 | public function stop() 151 | { 152 | $this->close(); 153 | } 154 | 155 | /** 156 | * @override 157 | * @inheritDoc 158 | */ 159 | public function getLocalEndpoint() 160 | { 161 | return $this->parseEndpoint(); 162 | } 163 | 164 | /** 165 | * @override 166 | * @inheritDoc 167 | */ 168 | public function getLocalAddress() 169 | { 170 | $endpoint = explode('://', $this->getLocalEndpoint(), 2); 171 | 172 | return isset($endpoint[1]) ? $endpoint[1] : $endpoint[0]; 173 | } 174 | 175 | /** 176 | * @override 177 | * @inheritDoc 178 | */ 179 | public function getLocalHost() 180 | { 181 | $address = explode(':', $this->getLocalAddress(), 2); 182 | 183 | return $address[0]; 184 | } 185 | 186 | /** 187 | * @override 188 | * @inheritDoc 189 | */ 190 | public function getLocalPort() 191 | { 192 | $address = explode(':', $this->getLocalAddress(), 2); 193 | 194 | return isset($address[1]) ? $address[1] : ''; 195 | } 196 | 197 | /** 198 | * @override 199 | * @inheritDoc 200 | */ 201 | public function getLocalProtocol() 202 | { 203 | $endpoint = explode('://', $this->getLocalEndpoint(), 2); 204 | 205 | return isset($endpoint[0]) ? $endpoint[0]:''; 206 | } 207 | 208 | /** 209 | * @override 210 | * @inheritDoc 211 | */ 212 | public function getResource() 213 | { 214 | return $this->socket; 215 | } 216 | 217 | /** 218 | * @override 219 | * @inheritDoc 220 | */ 221 | public function getResourceId() 222 | { 223 | return (int) $this->socket; 224 | } 225 | 226 | /** 227 | * @override 228 | * @inheritDoc 229 | */ 230 | public function getMetadata() 231 | { 232 | if ($this->isOpen()) 233 | { 234 | return stream_get_meta_data($this->socket); 235 | } 236 | return []; 237 | } 238 | 239 | /** 240 | * @override 241 | * @inheritDoc 242 | */ 243 | public function getStreamType() 244 | { 245 | $data = $this->getMetadata(); 246 | 247 | return isset($data['stream_type']) ? $data['stream_type'] : 'undefined'; 248 | } 249 | 250 | /** 251 | * @override 252 | * @inheritDoc 253 | */ 254 | public function getWrapperType() 255 | { 256 | $data = $this->getMetadata(); 257 | 258 | return isset($data['wrapper_type']) ? $data['wrapper_type'] : 'undefined'; 259 | } 260 | 261 | /** 262 | * @override 263 | * @inheritDoc 264 | */ 265 | public function isOpen() 266 | { 267 | return $this->started; 268 | } 269 | 270 | /** 271 | * @override 272 | * @inheritDoc 273 | */ 274 | public function isPaused() 275 | { 276 | return $this->paused; 277 | } 278 | 279 | /** 280 | * @override 281 | * @inheritDoc 282 | */ 283 | public function isEncrypted() 284 | { 285 | return isset($this->config['ssl']) && $this->config['ssl'] === true; 286 | } 287 | 288 | /** 289 | * @override 290 | * @inheritDoc 291 | */ 292 | public function close() 293 | { 294 | if (!$this->isOpen()) 295 | { 296 | return; 297 | } 298 | 299 | $this->started = false; 300 | 301 | $this->emit('close', [ $this ]); 302 | $this->handleClose(); 303 | $this->emit('done', [ $this ]); 304 | } 305 | 306 | /** 307 | * @override 308 | * @inheritDoc 309 | */ 310 | public function pause() 311 | { 312 | if (!$this->paused) 313 | { 314 | $this->paused = true; 315 | 316 | if (isset($this->loop)) 317 | { 318 | $this->loop->removeReadStream($this->socket); 319 | } 320 | } 321 | } 322 | 323 | /** 324 | * @override 325 | * @inheritDoc 326 | */ 327 | public function resume() 328 | { 329 | if ($this->paused) 330 | { 331 | $this->paused = false; 332 | 333 | if (isset($this->loop)) 334 | { 335 | $this->loop->addReadStream($this->socket, [ $this, 'handleConnect' ]); 336 | } 337 | } 338 | } 339 | 340 | /** 341 | * Create the server resource. 342 | * 343 | * This method creates server resource for socket connections. 344 | * 345 | * @param string $endpoint 346 | * @param mixed[] $config 347 | * @return resource 348 | * @throws LogicException 349 | */ 350 | protected function createServer($endpoint, $config = []) 351 | { 352 | if (stripos($endpoint, 'unix://') !== false) 353 | { 354 | if ($endpoint[7] === DIRECTORY_SEPARATOR) 355 | { 356 | $path = substr($endpoint, 7); 357 | } 358 | else 359 | { 360 | $path = getcwd() . DIRECTORY_SEPARATOR . substr($endpoint, 7); 361 | $endpoint = 'unix://' . $path; 362 | } 363 | 364 | if (file_exists($path)) 365 | { 366 | unlink($path); 367 | } 368 | } 369 | 370 | $ssl = $this->config['ssl']; 371 | $name = $this->config['ssl_name']; 372 | $verifySign = $this->config['ssl_verify_sign']; 373 | $verifyPeer = $this->config['ssl_verify_peer']; 374 | $verifyDepth = $this->config['ssl_verify_depth']; 375 | 376 | $backlog = (int) (isset($config['backlog']) ? $config['backlog'] : self::DEFAULT_BACKLOG); 377 | $reuseaddr = (bool) (isset($config['reuseaddr']) ? $config['reuseaddr'] : false); 378 | $reuseport = (bool) (isset($config['reuseport']) ? $config['reuseport'] : false); 379 | 380 | $context = []; 381 | $context['socket'] = [ 382 | 'bindto' => $endpoint, 383 | 'backlog' => $backlog, 384 | 'ipv6_v6only' => true, 385 | 'so_reuseaddr' => $reuseaddr, 386 | 'so_reuseport' => $reuseport, 387 | ]; 388 | $context['ssl'] = [ 389 | 'allow_self_signed' => !$verifySign, 390 | 'verify_peer' => $verifyPeer, 391 | 'verify_peer_name' => $verifyPeer, 392 | 'verify_depth' => $verifyDepth, 393 | 'disable_compression' => true, 394 | 'SNI_enabled' => $name !== '', 395 | 'SNI_server_name' => $name, 396 | 'peer_name' => $name, 397 | ]; 398 | 399 | if ($ssl && isset($config['ssl_cert'])) 400 | { 401 | $context['ssl']['local_cert'] = $config['ssl_cert']; 402 | } 403 | 404 | if ($ssl && isset($config['ssl_key'])) 405 | { 406 | $context['ssl']['local_pk'] = $config['ssl_key']; 407 | } 408 | 409 | if ($ssl && isset($config['ssl_secret'])) 410 | { 411 | $context['ssl']['passphrase'] = $config['ssl_secret']; 412 | } 413 | 414 | $bitmask = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; 415 | 416 | $context = stream_context_create($context); 417 | 418 | // Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure. 419 | $socket = @stream_socket_server( 420 | $endpoint, 421 | $errno, 422 | $errstr, 423 | $bitmask, 424 | $context 425 | ); 426 | 427 | if (!$socket || $errno > 0) 428 | { 429 | throw new LogicException( 430 | sprintf('Could not bind socket [%s] because of error [%d; %s]', $endpoint, $errno, $errstr) 431 | ); 432 | } 433 | 434 | return $socket; 435 | } 436 | 437 | /** 438 | * Create the client resource. 439 | * 440 | * This method creates client resource for socket connections. 441 | * 442 | * @param resource $resource 443 | * @param string[] $config 444 | * @return SocketInterface 445 | */ 446 | protected function createClient($resource, $config = []) 447 | { 448 | return new Socket($resource, $this->loop, $config); 449 | } 450 | 451 | /** 452 | * Handle the new connection. 453 | * 454 | * @internal 455 | */ 456 | public function handleConnect() 457 | { 458 | $socket = @stream_socket_accept($this->socket); 459 | 460 | if ($socket === false) 461 | { 462 | $this->emit('error', [ $this, new ReadException('Socket could not accept new connection.') ]); 463 | return; 464 | } 465 | 466 | $client = null; 467 | $ex = null; 468 | 469 | try 470 | { 471 | $client = $this->createClient($socket, [ 472 | 'ssl' => $this->config['ssl'], 473 | 'ssl_method' => $this->config['ssl_method'], 474 | ]); 475 | 476 | $this->emit('connect', [ $this, $client ]); 477 | } 478 | catch (Error $ex) 479 | {} 480 | catch (Exception $ex) 481 | {} 482 | 483 | if ($ex !== null) 484 | { 485 | $this->handleDisconnect($socket); 486 | $this->emit('error', [ $this, new ReadException('Socket could not wrap new connection!') ]); 487 | } 488 | } 489 | 490 | private function handleDisconnect($resource) 491 | { 492 | if (is_resource($resource)) 493 | { 494 | // http://chat.stackoverflow.com/transcript/message/7727858#7727858 495 | stream_socket_shutdown($resource, STREAM_SHUT_RDWR); 496 | stream_set_blocking($resource, 0); 497 | fclose($resource); 498 | } 499 | } 500 | 501 | /** 502 | * Handle closing event. 503 | * 504 | * @internal 505 | */ 506 | public function handleClose() 507 | { 508 | $this->pause(); 509 | 510 | if (is_resource($this->socket)) 511 | { 512 | if ($this->getStreamType() === Socket::TYPE_UNIX) 513 | { 514 | $path = substr($this->parseEndpoint(), 7); 515 | unlink($path); 516 | } 517 | fclose($this->socket); 518 | } 519 | } 520 | 521 | /** 522 | * Configure socket. 523 | * 524 | * @param string[] $config 525 | */ 526 | private function configure($config = []) 527 | { 528 | $this->config = $config; 529 | 530 | $this->configureVariable('ssl'); 531 | $this->configureVariable('ssl_method'); 532 | $this->configureVariable('ssl_name'); 533 | $this->configureVariable('ssl_verify_sign'); 534 | $this->configureVariable('ssl_verify_peer'); 535 | $this->configureVariable('ssl_verify_depth'); 536 | } 537 | 538 | /** 539 | * Configure static key 540 | * 541 | * @param $configKey 542 | */ 543 | private function configureVariable($configKey) 544 | { 545 | $configStaticKey = 'CONFIG_DEFAULT_' . strtoupper($configKey); 546 | $this->config[$configKey] = isset($this->config[$configKey]) ? $this->config[$configKey] : constant("static::$configStaticKey"); 547 | } 548 | 549 | /** 550 | * @return string 551 | */ 552 | private function parseEndpoint() 553 | { 554 | if ($this->isOpen()) 555 | { 556 | $name = stream_socket_get_name($this->socket, false); 557 | $type = $this->getStreamType(); 558 | 559 | switch ($type) 560 | { 561 | case Socket::TYPE_UNIX: 562 | $transport = 'unix://'; 563 | $endpoint = $transport . $name; 564 | break; 565 | 566 | case Socket::TYPE_TCP: 567 | $transport = 'tcp://'; 568 | if (substr_count($name, ':') > 1) 569 | { 570 | $parts = explode(':', $name); 571 | $count = count($parts); 572 | $port = $parts[$count - 1]; 573 | unset($parts[$count - 1]); 574 | $endpoint = $transport.'[' . implode(':', $parts) . ']:' . $port; 575 | } 576 | else 577 | { 578 | $endpoint = $transport . $name; 579 | } 580 | break; 581 | 582 | case Socket::TYPE_UDP: 583 | $transport = 'udp://'; 584 | $endpoint = $transport . $name; 585 | break; 586 | 587 | default: 588 | $endpoint = ''; 589 | } 590 | 591 | return $endpoint; 592 | } 593 | else 594 | { 595 | return is_string($this->endpoint) ? $this->endpoint : ''; 596 | } 597 | } 598 | } 599 | -------------------------------------------------------------------------------- /src/Socket/SocketListenerInterface.php: -------------------------------------------------------------------------------- 1 | loop = null; 46 | $this->sim = null; 47 | } 48 | 49 | /** 50 | * 51 | */ 52 | public function tearDown() 53 | { 54 | unset($this->sim); 55 | unset($this->loop); 56 | } 57 | 58 | /** 59 | * @return LoopInterface|null 60 | */ 61 | public function getLoop() 62 | { 63 | return $this->loop; 64 | } 65 | 66 | /** 67 | * Run test scenario as simulation. 68 | * 69 | * @param callable(Simulation) $scenario 70 | * @return TModule 71 | */ 72 | public function simulate(callable $scenario) 73 | { 74 | try 75 | { 76 | $this->loop = new Loop(new SelectLoop); 77 | $this->loop->erase(true); 78 | 79 | $this->sim = new Simulation($this->loop); 80 | $this->sim->setScenario($scenario); 81 | $this->sim->begin(); 82 | } 83 | catch (Exception $ex) 84 | { 85 | $this->fail($ex->getMessage()); 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param $events 93 | * @param int $flags 94 | * @return TModule 95 | */ 96 | public function expect($events, $flags = Simulation::EVENTS_COMPARE_IN_ORDER) 97 | { 98 | $expectedEvents = []; 99 | 100 | foreach ($events as $event) 101 | { 102 | $data = isset($event[1]) ? $event[1] : []; 103 | $expectedEvents[] = new Event($event[0], $data); 104 | } 105 | 106 | $this->assertEvents( 107 | $this->sim->getExpectations(), 108 | $expectedEvents, 109 | $flags 110 | ); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param Event[] $actualEvents 117 | * @param Event[] $expectedEvents 118 | * @param int $flags 119 | */ 120 | public function assertEvents($actualEvents = [], $expectedEvents = [], $flags = Simulation::EVENTS_COMPARE_IN_ORDER) 121 | { 122 | $count = max(count($actualEvents), count($expectedEvents)); 123 | 124 | if ($flags === Simulation::EVENTS_COMPARE_RANDOMLY) 125 | { 126 | sort($actualEvents); 127 | sort($expectedEvents); 128 | } 129 | 130 | for ($i=0; $i<$count; $i++) 131 | { 132 | if (!isset($actualEvents[$i])) 133 | { 134 | $this->fail( 135 | sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, $expectedEvents[$i]->name(), 'null') 136 | ); 137 | } 138 | else if (!isset($expectedEvents[$i])) 139 | { 140 | $this->fail( 141 | sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, 'null', $actualEvents[$i]->name()) 142 | ); 143 | } 144 | 145 | $actualEvent = $actualEvents[$i]; 146 | $expectedEvent = $expectedEvents[$i]; 147 | 148 | $this->assertSame( 149 | $expectedEvent->name(), 150 | $actualEvent->name(), 151 | sprintf(self::MSG_EVENT_NAME_ASSERTION_FAILED, $i) 152 | ); 153 | $this->assertSame( 154 | $expectedEvent->data(), 155 | $actualEvent->data(), 156 | sprintf(self::MSG_EVENT_DATA_ASSERTION_FAILED, $i) 157 | ); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/TModule/SocketTest.php: -------------------------------------------------------------------------------- 1 | simulate(function(SimulationInterface $sim) use($endpoint) { 24 | $loop = $sim->getLoop(); 25 | 26 | $server = new SocketListener($endpoint, $loop); 27 | $server->start(); 28 | $server->on('connect', function(SocketListenerInterface $server, SocketInterface $conn) use($sim) { 29 | $conn->on('data', function(SocketInterface $conn, $data) use($server, $sim) { 30 | $sim->expect('data', $data); 31 | $conn->write('secret answer!'); 32 | $server->close(); 33 | }); 34 | }); 35 | $server->on('error', $this->expectCallableNever()); 36 | $server->on('close', function() use($sim) { 37 | $sim->expect('close'); 38 | }); 39 | $server->start(); 40 | 41 | $client = new Socket($endpoint, $loop); 42 | $client->on('data', function(SocketInterface $conn, $data) use($loop, $sim) { 43 | $sim->expect('data', $data); 44 | $conn->close(); 45 | $sim->done(); 46 | }); 47 | $client->on('error', $this->expectCallableNever()); 48 | $client->on('close', function() use($sim) { 49 | $sim->expect('close'); 50 | }); 51 | 52 | $client->write('secret question!'); 53 | }); 54 | $sim = $sim->expect([ 55 | [ 'data', 'secret question!' ], 56 | [ 'close' ], 57 | [ 'data', 'secret answer!' ], 58 | [ 'close' ] 59 | ]); 60 | } 61 | 62 | /** 63 | * @dataProvider endpointSSLProvider 64 | */ 65 | public function testSocketWritesAndReadsDataCorrectly_WithSSLConfiguration($endpoint) 66 | { 67 | $sim = $this->simulate(function(SimulationInterface $sim) use($endpoint) { 68 | $loop = $sim->getLoop(); 69 | 70 | $serverConfig = [ 71 | 'ssl' => true, 72 | 'ssl_cert' => __DIR__ . '/_Data/_ssl_cert.pem', 73 | 'ssl_key' => __DIR__ . '/_Data/_ssl_key.pem', 74 | 'ssl_secret' => 'secret', 75 | ]; 76 | 77 | $server = new SocketListener($endpoint, $loop, $serverConfig); 78 | $server->on('connect', function(SocketListenerInterface $server, SocketInterface $conn) use($sim) { 79 | $conn->on('data', function(SocketInterface $conn, $data) use($server, $sim) { 80 | $sim->expect('data', $data); 81 | $conn->write('secret answer!'); 82 | $server->close(); 83 | }); 84 | }); 85 | $server->on('error', $this->expectCallableNever()); 86 | $server->on('close', function() use($sim) { 87 | $sim->expect('close'); 88 | }); 89 | $server->start(); 90 | 91 | $clientConfig = [ 92 | 'ssl' => true, 93 | ]; 94 | $client = new Socket($endpoint, $loop, $clientConfig); 95 | $client->on('data', function(SocketInterface $conn, $data) use($loop, $sim) { 96 | $sim->expect('data', $data); 97 | $conn->close(); 98 | $sim->done(); 99 | }); 100 | $client->on('error', $this->expectCallableNever()); 101 | $client->on('close', function() use($sim) { 102 | $sim->expect('close'); 103 | }); 104 | 105 | $client->write('secret question!'); 106 | }); 107 | $sim = $sim->expect([ 108 | [ 'data', 'secret question!' ], 109 | [ 'close' ], 110 | [ 'data', 'secret answer!' ], 111 | [ 'close' ] 112 | ]); 113 | } 114 | 115 | /** 116 | * @return string[][] 117 | */ 118 | public function endpointProvider() 119 | { 120 | return [ 121 | [ 'tcp://127.0.0.1:2080' ], 122 | [ 'unix://mysocket.sock' ] 123 | ]; 124 | } 125 | 126 | /** 127 | * @return string[][] 128 | */ 129 | public function endpointSSLProvider() 130 | { 131 | return [ 132 | [ 'tcp://127.0.0.1:2080' ], 133 | ]; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/TModule/_Data/_ssl_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9jCCAd4CCQC1n9FarkDiuTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCU0gxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 4 | ZDAeFw0xNzAxMjcwMjI0MjNaFw0xODAxMjcwMjI0MjNaMD0xCzAJBgNVBAYTAkNO 5 | MQswCQYDVQQIDAJTSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 6 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6P2tbxw2JBEr/jI6dyU/ 7 | ppopZ3XbUybS4A3V2G/k5wXocjPOx2ttb4J3cClKYzyyG96AFRftmkjps1ixxknO 8 | ZlXWVJ+Cdgw3RXn0kt9s8zjYtGKFnDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUf 9 | poAVkeohGwVQQKjiBANZo4Civwae71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EY 10 | OQQTb18UVEdKyVM8RQbpnE3DZ0tdB7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66CO 11 | q2rI3CN4mNYBybNrfjby5FK8Qgnu8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD 12 | +wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBWSUEWA161OYo9XM6PVqP++RQyIq7d 13 | YFgdwgxjZTUINuOlBQnQEPbCskeCEaxEK/APkyGfV8M+b8NzLgXSJJAXjI6tfBmR 14 | CMLXk/cTGImAd1c6KQ3wCPQqbn8bxCaC1ca9MrJPqB4EOjIiVw+zBNXS6n3kDqkD 15 | a6N3DfBXZycY6tfOSnbej6q2/hPzgqP2VquNvsiloRpay5i+yfcx3rbh/bJVHsb7 16 | 1YoqYZU/Kn88Lt2U8Qhwkag2qAvSd7uGAldxNkzgbAXZ3QneoOKFcWCvGLbS3XjN 17 | mMWkYg8EHsks6PzLRtQesHKiq8t92JdZsxvasMXMNAs9GHDQ88En8iUL 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/TModule/_Data/_ssl_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA6P2tbxw2JBEr/jI6dyU/ppopZ3XbUybS4A3V2G/k5wXocjPO 3 | x2ttb4J3cClKYzyyG96AFRftmkjps1ixxknOZlXWVJ+Cdgw3RXn0kt9s8zjYtGKF 4 | nDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUfpoAVkeohGwVQQKjiBANZo4Civwae 5 | 71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EYOQQTb18UVEdKyVM8RQbpnE3DZ0td 6 | B7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66COq2rI3CN4mNYBybNrfjby5FK8Qgnu 7 | 8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD+wIDAQABAoIBAGr6BwbsMiOvSr7F 8 | Ozj3flk1kWsynwh32/0J/TUKc1RrzwWYqgnAVpgLlGGIY+JavQC00aRdvzfG4J77 9 | S2za+IzePrHOJ22iQEKDM3u4YGLwp/dzARvsyJwdEUEwGIfLUCxzHTGnOHrrC3hL 10 | tda2AvlE+CJuGNM6RlXZ9SSYkTDTgBORkuAJf4pTZrG5wVedTGR0us1lj4BKzuDF 11 | MsApnBum8Dpj73nZCcfT9MMgbuXGp1jsDuE7eZsiDzklarroZxOcYX9YMTpsG2iY 12 | f2drGWSVssZKwmxaTiAHi4iMIhawxQwE13PxK5FqP1DQjQYmPjoDY5CQ1/OD6oGJ 13 | klRpEykCgYEA+XDlDHRP3UK1UpZJY6heudRgdbuEdd4vFbEVG6Kk1vh3ub6Y33UJ 14 | 1nvHIvjzCOEnjO5wZbBpoJy1O2fCJt/rdP1nWI2SLXtBIv4floia1SK5S1fmrr4C 15 | 1uDAB3+/PAJe5gSBX6l9dt1JesB6ERrrVYU05GCnW64T+kDg19IZaUUCgYEA7x4M 16 | sOGXjdunmk5DmgaAZcKKUXm6G5+DGHCiHzylUr3LsJ4dqBMFYDfA0s2YNlT+pKpK 17 | BEDBxfzlx1ewaorr2Hge3QAHeGWa4oQok42u7rv1ernl/RYOfI3JcZaX89cu482b 18 | /tFUjeMDfxqiD74hl2JtK3vkBTfldApPS10ZbD8CgYEA4S1S+C0/s4P23/kfg0GJ 19 | UcQw19dRun26j6BuZ6YpnI5stlci9gZXvTG8EQwfiZKH+6d7+7CHiiAtUtVV5XYa 20 | vl/LnZsfrMMpiAdUSLZE38ca8rMFYhXRxlzZvWtLHUcemBLVH6CHzHfzT0bWav6c 21 | F3XKy6edLRw2mKFc2DeoDOkCgYEAndDhY2Sg2Bme9rKs311nJMJO9BM5B74xkdHU 22 | znUCUCe/5eOVgzZ6l9R0SS00Rre2EQvrKf9rZLbTGMwBPXBO1GIDTK0WQRoeLV44 23 | QZqDWEFpdQR4jJ8gFIin6XYQ9/iPk/5B9N5HxhWMWzlCuM0t8nIdx1NPXTpVEE81 24 | onHhs5UCgYA6y5LpvJ5vOpZyHdTISgQK+4SmhUSu3e29qE8AjixaTBFwGa/PKuVO 25 | ggpceNZjl+pg1F74IMEpg1c6N1MKEIhK+NEYhxS9QkShPNYwasfkhg87QfkOxUi+ 26 | jed5G+yO01Re0bkyE6YiS+JrJQZJEdna/x9eb5IxF/LiTWkZhkdywg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/TUnit.php: -------------------------------------------------------------------------------- 1 | exactly(2); 34 | } 35 | 36 | /** 37 | * Creates a callback that must be called $amount times or the test will fail. 38 | * 39 | * @param $amount 40 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 41 | */ 42 | public function expectCallableExactly($amount) 43 | { 44 | $mock = $this->createCallableMock(); 45 | $mock 46 | ->expects($this->exactly($amount)) 47 | ->method('__invoke'); 48 | 49 | return $mock; 50 | } 51 | 52 | /** 53 | * Creates a callback that must be called once. 54 | * 55 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 56 | */ 57 | public function expectCallableOnce() 58 | { 59 | $mock = $this->createCallableMock(); 60 | $mock 61 | ->expects($this->once()) 62 | ->method('__invoke'); 63 | 64 | return $mock; 65 | } 66 | 67 | /** 68 | * Creates a callback that must be called twice. 69 | * 70 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 71 | */ 72 | public function expectCallableTwice() 73 | { 74 | $mock = $this->createCallableMock(); 75 | $mock 76 | ->expects($this->exactly(2)) 77 | ->method('__invoke'); 78 | 79 | return $mock; 80 | } 81 | 82 | /** 83 | * Creates a callable that must not be called once. 84 | * 85 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 86 | */ 87 | public function expectCallableNever() 88 | { 89 | $mock = $this->createCallableMock(); 90 | $mock 91 | ->expects($this->never()) 92 | ->method('__invoke'); 93 | 94 | return $mock; 95 | } 96 | 97 | /** 98 | * Creates a callable mock. 99 | * 100 | * @return callable|\PHPUnit_Framework_MockObject_MockObject 101 | */ 102 | public function createCallableMock() 103 | { 104 | return $this->getMock(Callback::class); 105 | } 106 | 107 | /** 108 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 109 | */ 110 | public function createLoopMock() 111 | { 112 | return $this->getMock('Dazzle\Loop\LoopInterface'); 113 | } 114 | 115 | /** 116 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 117 | */ 118 | public function createWritableLoopMock() 119 | { 120 | $loop = $this->createLoopMock(); 121 | $loop 122 | ->expects($this->once()) 123 | ->method('addWriteStream') 124 | ->will($this->returnCallback(function($stream, $listener) { 125 | call_user_func($listener, $stream); 126 | })); 127 | 128 | return $loop; 129 | } 130 | 131 | /** 132 | * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject 133 | */ 134 | public function createReadableLoopMock() 135 | { 136 | $loop = $this->createLoopMock(); 137 | $loop 138 | ->expects($this->once()) 139 | ->method('addReadStream') 140 | ->will($this->returnCallback(function($stream, $listener) { 141 | call_user_func($listener, $stream); 142 | })); 143 | 144 | return $loop; 145 | } 146 | 147 | /** 148 | * Check if protected property exists. 149 | * 150 | * @param object $object 151 | * @param string $property 152 | * @return bool 153 | */ 154 | public function existsProtectedProperty($object, $property) 155 | { 156 | $reflection = new ReflectionClass($object); 157 | return $reflection->hasProperty($property); 158 | } 159 | 160 | /** 161 | * Get protected property from given object via reflection. 162 | * 163 | * @param object $object 164 | * @param string $property 165 | * @return mixed 166 | */ 167 | public function getProtectedProperty($object, $property) 168 | { 169 | $reflection = new ReflectionClass($object); 170 | $reflection_property = $reflection->getProperty($property); 171 | $reflection_property->setAccessible(true); 172 | 173 | return $reflection_property->getValue($object); 174 | } 175 | 176 | /** 177 | * Set protected property on a given object via reflection. 178 | * 179 | * @param object $object 180 | * @param string $property 181 | * @param mixed $value 182 | * @return object 183 | */ 184 | public function setProtectedProperty($object, $property, $value) 185 | { 186 | $reflection = new ReflectionClass($object); 187 | $reflection_property = $reflection->getProperty($property); 188 | $reflection_property->setAccessible(true); 189 | $reflection_property->setValue($object, $value); 190 | 191 | return $object; 192 | } 193 | 194 | /** 195 | * Call protected method on a given object via reflection. 196 | * 197 | * @param object|string $objectOrClass 198 | * @param string $method 199 | * @param mixed[] $args 200 | * @return mixed 201 | */ 202 | public function callProtectedMethod($objectOrClass, $method, $args = []) 203 | { 204 | $reflection = new ReflectionClass($objectOrClass); 205 | $reflectionMethod = $reflection->getMethod($method); 206 | $reflectionMethod->setAccessible(true); 207 | $reflectionTarget = is_object($objectOrClass) ? $objectOrClass : null; 208 | 209 | return $reflectionMethod->invokeArgs($reflectionTarget, $args); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /test/TUnit/SocketListenerTest.php: -------------------------------------------------------------------------------- 1 | createSocketListenerMock(); 19 | 20 | $this->assertInstanceOf(SocketListener::class, $socket); 21 | $this->assertInstanceOf(SocketListenerInterface::class, $socket); 22 | } 23 | 24 | /** 25 | * 26 | */ 27 | public function testApiDestructor_DoesNotThrowException() 28 | { 29 | $socket = $this->createSocketListenerMock(); 30 | unset($socket); 31 | } 32 | 33 | /** 34 | * 35 | */ 36 | public function testStart_ThrowsException_OnInvalidResource() 37 | { 38 | $this->setExpectedException(InstantiationException::class); 39 | $socket = $this->createSocketListenerMock('invalid'); 40 | $socket->start(); 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | public function testStart_ThrowsException_OnOccupiedEndpoint() 47 | { 48 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 49 | $this->setExpectedException(InstantiationException::class); 50 | $socket = $this->createSocketListenerMock(); 51 | $socket->start(); 52 | } 53 | 54 | /** 55 | * 56 | */ 57 | public function testApiGetLocalEndpoint_ReturnsEndpoint() 58 | { 59 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 60 | $this->assertRegExp('#^tcp://(([0-9]*?)\.){3}([0-9]*?):([0-9]*?)$#si', $socket->getLocalEndpoint()); 61 | } 62 | 63 | /** 64 | * 65 | */ 66 | public function testApiGetLocalAddress_ReturnsAddress() 67 | { 68 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 69 | 70 | $pattern = '#^(([0-9]*?)\.){3}([0-9]*?):([0-9]*?)$#si'; 71 | 72 | $this->assertRegExp($pattern, $socket->getLocalAddress()); 73 | } 74 | 75 | /** 76 | * 77 | */ 78 | public function testApiGetLocalHost_ReturnsHost() 79 | { 80 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 81 | 82 | $pattern = '#^(([0-9]*?)\.){3}([0-9]*?)$#si'; 83 | 84 | $this->assertRegExp($pattern, $socket->getLocalHost()); 85 | } 86 | 87 | /** 88 | * 89 | */ 90 | public function testApiGetLocalPort_ReturnsPort() 91 | { 92 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 93 | 94 | $pattern = '#^([0-9]*?)$#si'; 95 | 96 | $this->assertRegExp($pattern, $socket->getLocalPort()); 97 | } 98 | 99 | /** 100 | * 101 | */ 102 | public function testApiGetResource_ReturnsResource() 103 | { 104 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 105 | $socket->start(); 106 | $this->assertTrue(is_resource($socket->getResource())); 107 | } 108 | 109 | /** 110 | * 111 | */ 112 | public function testApiGetResourceId_ReturnsResourceId() 113 | { 114 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 115 | $socket->start(); 116 | $this->assertTrue(is_numeric($socket->getResourceId())); 117 | } 118 | 119 | /** 120 | * 121 | */ 122 | public function testApiGetMetadata_ReturnsMetadata() 123 | { 124 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 125 | $this->assertTrue(is_array($socket->getMetadata())); 126 | } 127 | 128 | /** 129 | * 130 | */ 131 | public function testApiGetStreamType_ReturnsStreamType() 132 | { 133 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 134 | $socket->start(); 135 | $this->assertSame('tcp_socket/ssl', $socket->getStreamType()); 136 | } 137 | 138 | /** 139 | * 140 | */ 141 | public function testApiGetWrapperType_ReturnsWrapperType() 142 | { 143 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 144 | $this->assertSame('undefined', $socket->getWrapperType()); 145 | } 146 | 147 | /** 148 | * 149 | */ 150 | public function testApiIsOpen_ReturnsIfSocketIsOpened() 151 | { 152 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 153 | $this->assertFalse($socket->isOpen()); 154 | $socket->start(); 155 | $this->assertTrue($socket->isOpen()); 156 | } 157 | 158 | /** 159 | * 160 | */ 161 | public function testApiIsPaused_ReturnsIfSocketIsPaused() 162 | { 163 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 164 | $this->assertTrue($socket->isPaused()); 165 | $socket->start(); 166 | $this->assertFalse($socket->isPaused()); 167 | } 168 | 169 | //todo 170 | public function testApiStart_CannotBeInvokedMoreThanOnce() 171 | { 172 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 173 | } 174 | 175 | //todo 176 | public function testApiStop_CannotBeInvokedMoreThanOnce() 177 | { 178 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 179 | } 180 | 181 | //todo 182 | public function testApiStart_CanBeInvokedAfterStop() 183 | { 184 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 185 | 186 | } 187 | 188 | //todo 189 | public function testApiStop_CanBeInvokedAfterStart() 190 | { 191 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 192 | } 193 | 194 | /** 195 | * 196 | */ 197 | public function testApiStart_StartSocket() 198 | { 199 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 200 | $this->assertFalse($socket->isOpen()); 201 | $socket->start(); 202 | $this->assertTrue($socket->isOpen()); 203 | } 204 | 205 | /** 206 | * 207 | */ 208 | public function testApiClose_ClosesSocket() 209 | { 210 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 211 | $socket->start(); 212 | $this->assertTrue($socket->isOpen()); 213 | $socket->close(); 214 | $this->assertFalse($socket->isOpen()); 215 | } 216 | 217 | /** 218 | * 219 | */ 220 | public function testApiPause_PausesSocket() 221 | { 222 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 223 | $socket->start(); 224 | $this->assertFalse($socket->isPaused()); 225 | $socket->pause(); 226 | $this->assertTrue($socket->isPaused()); 227 | } 228 | 229 | /** 230 | * 231 | */ 232 | public function testApiResume_ResumesSocket() 233 | { 234 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress()); 235 | 236 | $socket->pause(); 237 | $this->assertTrue($socket->isPaused()); 238 | 239 | $socket->resume(); 240 | $this->assertFalse($socket->isPaused()); 241 | } 242 | 243 | /** 244 | * @throws InstantiationException 245 | */ 246 | public function testApiStart_SslSocket() 247 | { 248 | $config = [ 249 | 'ssl' => true, 250 | 'ssl_cert' => __DIR__ . '/_Data/_ssl_cert.pem', 251 | 'ssl_secret' => __DIR__ . '/_Data/_ssl_key.pem', 252 | 'ssl_key'=> 'secret', 253 | ]; 254 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress(), null, $config); 255 | 256 | $this->assertFalse($socket->isOpen()); 257 | $socket->start(); 258 | 259 | $this->assertTrue($socket->isOpen()); 260 | } 261 | 262 | /** 263 | * @throws InstantiationException 264 | */ 265 | public function testApiClose_SslSocket() 266 | { 267 | $config = [ 268 | 'ssl' => true, 269 | 'ssl_cert' => __DIR__ . '/_Data/_ssl_cert.pem', 270 | 'ssl_secret' => __DIR__ . '/_Data/_ssl_key.pem', 271 | 'ssl_key'=> 'secret', 272 | ]; 273 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress(), null, $config); 274 | $socket->start(); 275 | 276 | $this->assertTrue($socket->isOpen()); 277 | $socket->close(); 278 | 279 | $this->assertFalse($socket->isOpen()); 280 | } 281 | 282 | /** 283 | * @throws InstantiationException 284 | */ 285 | public function testApiPause_SslSocket() 286 | { 287 | $config = [ 288 | 'ssl' => true, 289 | 'ssl_cert' => __DIR__ . '/_Data/_ssl_cert.pem', 290 | 'ssl_secret' => __DIR__ . '/_Data/_ssl_key.pem', 291 | 'ssl_key'=> 'secret', 292 | ]; 293 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress(), null, $config); 294 | $socket->start(); 295 | 296 | $this->assertFalse($socket->isPaused()); 297 | $socket->pause(); 298 | 299 | $this->assertTrue($socket->isPaused()); 300 | } 301 | 302 | /** 303 | * @throws InstantiationException 304 | */ 305 | public function testApiResume_SslSocket() 306 | { 307 | $config = [ 308 | 'ssl' => true, 309 | 'ssl_cert' => __DIR__ . '/_Data/_ssl_cert.pem', 310 | 'ssl_secret' => __DIR__ . '/_Data/_ssl_key.pem', 311 | 'ssl_key'=> 'secret', 312 | ]; 313 | $socket = $this->createSocketListenerMock($this->tempSocketRemoteAddress(), null, $config); 314 | $socket->pause(); 315 | $this->assertTrue($socket->isPaused()); 316 | 317 | $socket->resume(); 318 | $this->assertFalse($socket->isPaused()); 319 | } 320 | 321 | /** 322 | * @param resource|null $resource 323 | * @param LoopInterface $loop 324 | * @param array $config 325 | * @return SocketListener 326 | */ 327 | protected function createSocketListenerMock($resource = null, LoopInterface $loop = null , $config = []) 328 | { 329 | return $this->createSocketListenerInjection( 330 | is_null($resource) ? $this->tempSocketRemoteAddress() : $resource, 331 | is_null($loop) ? $this->createLoopMock() : $loop , $config 332 | ); 333 | } 334 | 335 | /** 336 | * @param string|resource $endpointOrResource 337 | * @param LoopInterface $loop 338 | * @param array $config 339 | * @return SocketListener 340 | */ 341 | protected function createSocketListenerInjection($endpointOrResource, LoopInterface $loop , $config = []) 342 | { 343 | return new SocketListener($endpointOrResource, $loop, $config); 344 | } 345 | 346 | /** 347 | * @return string 348 | */ 349 | private function tempSocketRemoteAddress() 350 | { 351 | return 'tcp://127.0.0.1:10080'; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /test/TUnit/SocketTest.php: -------------------------------------------------------------------------------- 1 | tempSocketAddress()); 20 | 21 | if (file_exists($path)) 22 | { 23 | unlink($path); 24 | } 25 | 26 | parent::tearDown(); 27 | } 28 | 29 | public function testApiConstructor_CreatesInstance() 30 | { 31 | $server = stream_socket_server($this->tempSocketAddress()); 32 | $socket = $this->createSocketMock(); 33 | 34 | $this->assertInstanceOf(Socket::class, $socket); 35 | $this->assertInstanceOf(SocketInterface::class, $socket); 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | public function testApiConstructor_ThrowsException_WhenNoServerExists() 42 | { 43 | $this->setExpectedException(InstantiationException::class); 44 | $this->createSocketMock(); 45 | } 46 | 47 | /** 48 | * 49 | */ 50 | public function testApiConstructor_ThrowsException_OnInvalidResource() 51 | { 52 | $this->setExpectedException(InstantiationException::class); 53 | $this->createSocketMock('invalid'); 54 | } 55 | 56 | /** 57 | * 58 | */ 59 | public function testDestructor_DoesNotThrowException() 60 | { 61 | $server = stream_socket_server($this->tempSocketAddress()); 62 | $socket = $this->createSocketMock(); 63 | 64 | unset($socket); 65 | } 66 | 67 | /** 68 | * 69 | */ 70 | public function testApiStop_StopsSocket() 71 | { 72 | $server = stream_socket_server($this->tempSocketAddress()); 73 | $socket = $this->createSocketMock(); 74 | 75 | $this->assertTrue($socket->isOpen()); 76 | $socket->stop(); 77 | $this->assertFalse($socket->isOpen()); 78 | } 79 | 80 | /** 81 | * 82 | */ 83 | public function testApiGetLocalEndpoint_ReturnsEndpoint() 84 | { 85 | $server = stream_socket_server($this->tempSocketRemoteAddress(),$errno,$errstr,STREAM_SERVER_BIND); 86 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 87 | 88 | $this->assertRegExp('#^tcp://(([0-9]*?)\.){3}([0-9]*?):([0-9]*?)$#si', $socket->getLocalEndpoint()); 89 | } 90 | 91 | /** 92 | * 93 | */ 94 | public function testApiGetRemoteEndpoint_ReturnsEndpoint() 95 | { 96 | $remote = $this->tempSocketRemoteAddress(); 97 | $server = stream_socket_server($remote,$errno,$errstr); 98 | $socket = $this->createSocketMock($remote); 99 | 100 | $this->assertEquals($remote, $socket->getRemoteEndpoint()); 101 | } 102 | 103 | /** 104 | * 105 | */ 106 | public function testApiGetLocalAddress_ReturnsAddress() 107 | { 108 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 109 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 110 | $pattern = '#^(([0-9]*?)\.){3}([0-9]*?):([0-9]*?)$#si'; 111 | 112 | $this->assertRegExp($pattern, $socket->getLocalAddress()); 113 | } 114 | 115 | /** 116 | * 117 | */ 118 | public function testApiGetRemoteAddress_ReturnsAddress() 119 | { 120 | $remote = $this->tempSocketRemoteAddress(); 121 | $server = stream_socket_server($remote); 122 | $socket = $this->createSocketMock($remote); 123 | 124 | $address = str_replace('tcp://', '', $remote); 125 | 126 | $this->assertEquals($address, $socket->getRemoteAddress()); 127 | } 128 | 129 | /** 130 | * 131 | */ 132 | public function testApiGetLocalHost_ReturnsHost() 133 | { 134 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 135 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 136 | 137 | $pattern = '#^(([0-9]*?)\.){3}([0-9]*?)$#si'; 138 | 139 | $this->assertRegExp($pattern, $socket->getLocalHost()); 140 | } 141 | 142 | /** 143 | * 144 | */ 145 | public function testApiGetRemoteHost_ReturnsHost() 146 | { 147 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 148 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 149 | 150 | $pattern = '#^(([0-9]*?)\.){3}([0-9]*?)$#si'; 151 | 152 | $this->assertRegExp($pattern, $socket->getRemoteHost()); 153 | } 154 | 155 | /** 156 | * 157 | */ 158 | public function testApiGetLocalPort_ReturnsPort() 159 | { 160 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 161 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 162 | 163 | $pattern = '#^([0-9]*?)$#si'; 164 | 165 | $this->assertRegExp($pattern, $socket->getLocalPort()); 166 | } 167 | 168 | /** 169 | * 170 | */ 171 | public function testApiGetRemotePort_ReturnsPort() 172 | { 173 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 174 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 175 | 176 | $pattern = '#^([0-9]*?)$#si'; 177 | 178 | $this->assertRegExp($pattern, $socket->getRemotePort()); 179 | } 180 | 181 | public function testApiGetLocalProtocol_ReturnsProtocol() 182 | { 183 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 184 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 185 | 186 | $transports = stream_get_transports(); 187 | $this->assertTrue(in_array($socket->getLocalProtocol(), $transports)); 188 | } 189 | 190 | public function testApiGetRemoteProtocol_ReturnsProtocol() 191 | { 192 | $server = stream_socket_server($this->tempSocketRemoteAddress()); 193 | $socket = $this->createSocketMock($this->tempSocketRemoteAddress()); 194 | 195 | $transports = stream_get_transports(); 196 | 197 | $this->assertTrue(in_array($socket->getRemoteProtocol(), $transports)); 198 | } 199 | 200 | /** 201 | * @param resource|null $resource 202 | * @param LoopInterface $loop 203 | * @param array $config 204 | * @return Socket 205 | */ 206 | protected function createSocketMock($resource = null, LoopInterface $loop = null, $config = []) 207 | { 208 | return $this->createSocketInjection( 209 | is_null($resource) ? $this->tempSocketAddress() : $resource, 210 | is_null($loop) ? $this->createLoopMock() : $loop, $config 211 | ); 212 | } 213 | 214 | /** 215 | * @param string|resource $endpointOrResource 216 | * @param LoopInterface $loop 217 | * @param array $config 218 | * @return Socket 219 | */ 220 | protected function createSocketInjection($endpointOrResource, LoopInterface $loop, $config = []) 221 | { 222 | return new Socket($endpointOrResource, $loop ,$config); 223 | } 224 | 225 | /** 226 | * @return string 227 | */ 228 | private function tempSocketAddress() 229 | { 230 | return 'unix://' . $this->basePath() . '/temp.sock'; 231 | } 232 | 233 | /** 234 | * @return string 235 | */ 236 | private function tempSocketRemoteAddress() 237 | { 238 | return 'tcp://127.0.0.1:10080'; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /test/TUnit/_Data/_ssl_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC9jCCAd4CCQC1n9FarkDiuTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCU0gxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 4 | ZDAeFw0xNzAxMjcwMjI0MjNaFw0xODAxMjcwMjI0MjNaMD0xCzAJBgNVBAYTAkNO 5 | MQswCQYDVQQIDAJTSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 6 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6P2tbxw2JBEr/jI6dyU/ 7 | ppopZ3XbUybS4A3V2G/k5wXocjPOx2ttb4J3cClKYzyyG96AFRftmkjps1ixxknO 8 | ZlXWVJ+Cdgw3RXn0kt9s8zjYtGKFnDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUf 9 | poAVkeohGwVQQKjiBANZo4Civwae71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EY 10 | OQQTb18UVEdKyVM8RQbpnE3DZ0tdB7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66CO 11 | q2rI3CN4mNYBybNrfjby5FK8Qgnu8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD 12 | +wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBWSUEWA161OYo9XM6PVqP++RQyIq7d 13 | YFgdwgxjZTUINuOlBQnQEPbCskeCEaxEK/APkyGfV8M+b8NzLgXSJJAXjI6tfBmR 14 | CMLXk/cTGImAd1c6KQ3wCPQqbn8bxCaC1ca9MrJPqB4EOjIiVw+zBNXS6n3kDqkD 15 | a6N3DfBXZycY6tfOSnbej6q2/hPzgqP2VquNvsiloRpay5i+yfcx3rbh/bJVHsb7 16 | 1YoqYZU/Kn88Lt2U8Qhwkag2qAvSd7uGAldxNkzgbAXZ3QneoOKFcWCvGLbS3XjN 17 | mMWkYg8EHsks6PzLRtQesHKiq8t92JdZsxvasMXMNAs9GHDQ88En8iUL 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test/TUnit/_Data/_ssl_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA6P2tbxw2JBEr/jI6dyU/ppopZ3XbUybS4A3V2G/k5wXocjPO 3 | x2ttb4J3cClKYzyyG96AFRftmkjps1ixxknOZlXWVJ+Cdgw3RXn0kt9s8zjYtGKF 4 | nDBTyi27KMVjpdGiOl9iJA71FgWeRuUOzFUfpoAVkeohGwVQQKjiBANZo4Civwae 5 | 71LJ0HwN+JtX/l8Rcqrlt5DYAAix13ZzF9EYOQQTb18UVEdKyVM8RQbpnE3DZ0td 6 | B7WAiUBOd3H9DICPSCytSSVfOFYyWXzM66COq2rI3CN4mNYBybNrfjby5FK8Qgnu 7 | 8boVLPlpq70j8SnAik1drfGVdf0W2UOvxWcD+wIDAQABAoIBAGr6BwbsMiOvSr7F 8 | Ozj3flk1kWsynwh32/0J/TUKc1RrzwWYqgnAVpgLlGGIY+JavQC00aRdvzfG4J77 9 | S2za+IzePrHOJ22iQEKDM3u4YGLwp/dzARvsyJwdEUEwGIfLUCxzHTGnOHrrC3hL 10 | tda2AvlE+CJuGNM6RlXZ9SSYkTDTgBORkuAJf4pTZrG5wVedTGR0us1lj4BKzuDF 11 | MsApnBum8Dpj73nZCcfT9MMgbuXGp1jsDuE7eZsiDzklarroZxOcYX9YMTpsG2iY 12 | f2drGWSVssZKwmxaTiAHi4iMIhawxQwE13PxK5FqP1DQjQYmPjoDY5CQ1/OD6oGJ 13 | klRpEykCgYEA+XDlDHRP3UK1UpZJY6heudRgdbuEdd4vFbEVG6Kk1vh3ub6Y33UJ 14 | 1nvHIvjzCOEnjO5wZbBpoJy1O2fCJt/rdP1nWI2SLXtBIv4floia1SK5S1fmrr4C 15 | 1uDAB3+/PAJe5gSBX6l9dt1JesB6ERrrVYU05GCnW64T+kDg19IZaUUCgYEA7x4M 16 | sOGXjdunmk5DmgaAZcKKUXm6G5+DGHCiHzylUr3LsJ4dqBMFYDfA0s2YNlT+pKpK 17 | BEDBxfzlx1ewaorr2Hge3QAHeGWa4oQok42u7rv1ernl/RYOfI3JcZaX89cu482b 18 | /tFUjeMDfxqiD74hl2JtK3vkBTfldApPS10ZbD8CgYEA4S1S+C0/s4P23/kfg0GJ 19 | UcQw19dRun26j6BuZ6YpnI5stlci9gZXvTG8EQwfiZKH+6d7+7CHiiAtUtVV5XYa 20 | vl/LnZsfrMMpiAdUSLZE38ca8rMFYhXRxlzZvWtLHUcemBLVH6CHzHfzT0bWav6c 21 | F3XKy6edLRw2mKFc2DeoDOkCgYEAndDhY2Sg2Bme9rKs311nJMJO9BM5B74xkdHU 22 | znUCUCe/5eOVgzZ6l9R0SS00Rre2EQvrKf9rZLbTGMwBPXBO1GIDTK0WQRoeLV44 23 | QZqDWEFpdQR4jJ8gFIin6XYQ9/iPk/5B9N5HxhWMWzlCuM0t8nIdx1NPXTpVEE81 24 | onHhs5UCgYA6y5LpvJ5vOpZyHdTISgQK+4SmhUSu3e29qE8AjixaTBFwGa/PKuVO 25 | ggpceNZjl+pg1F74IMEpg1c6N1MKEIhK+NEYhxS9QkShPNYwasfkhg87QfkOxUi+ 26 | jed5G+yO01Re0bkyE6YiS+JrJQZJEdna/x9eb5IxF/LiTWkZhkdywg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/_Simulation/Event.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->data = $data; 25 | } 26 | 27 | /** 28 | * @return mixed 29 | */ 30 | public function name() 31 | { 32 | return $this->name; 33 | } 34 | 35 | /** 36 | * @return mixed[] 37 | */ 38 | public function data() 39 | { 40 | return $this->data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/_Simulation/EventCollection.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 65 | $this->scenario = function() {}; 66 | $this->events = []; 67 | $this->failureMessage = null; 68 | $this->startCallback = function() {}; 69 | $this->stopCallback = function() {}; 70 | $this->stopFlags = false; 71 | } 72 | 73 | /** 74 | * 75 | */ 76 | public function __destruct() 77 | { 78 | $this->stop(); 79 | 80 | unset($loop); 81 | unset($this->scenario); 82 | unset($this->events); 83 | unset($this->failureMessage); 84 | unset($this->startCallback); 85 | unset($this->stopCallback); 86 | unset($this->stopFlags); 87 | } 88 | 89 | /** 90 | * @param callable(SimulationInterface) $scenario 91 | */ 92 | public function setScenario(callable $scenario) 93 | { 94 | $this->scenario = $scenario; 95 | } 96 | 97 | /** 98 | * @return callable(SimulationInterface)|null $scenario 99 | */ 100 | public function getScenario() 101 | { 102 | return $this->scenario; 103 | } 104 | 105 | /** 106 | * @return LoopInterface 107 | */ 108 | public function getLoop() 109 | { 110 | return $this->loop; 111 | } 112 | 113 | /** 114 | * 115 | */ 116 | public function begin() 117 | { 118 | $this->start(); 119 | } 120 | 121 | /** 122 | * 123 | */ 124 | public function done() 125 | { 126 | $this->stop(); 127 | } 128 | 129 | /** 130 | * 131 | */ 132 | public function fail($message) 133 | { 134 | $this->failureMessage = $message; 135 | $this->stop(); 136 | } 137 | 138 | /** 139 | * @param mixed $expected 140 | * @param mixed $actual 141 | * @param string $message 142 | */ 143 | public function assertSame($expected, $actual, $message = "Assertion failed, expected \"%s\" got \"%s\"") 144 | { 145 | if ($expected !== $actual) 146 | { 147 | $stringExpected = (is_object($expected) || is_array($expected)) ? json_encode($expected) : (string) $expected; 148 | $stringActual = (is_object($actual) || is_array($actual)) ? json_encode($actual) : (string) $actual; 149 | 150 | $this->fail(sprintf($message, $stringExpected, $stringActual)); 151 | } 152 | } 153 | 154 | /** 155 | * @param string $name 156 | * @param mixed $data 157 | */ 158 | public function expect($name, $data = []) 159 | { 160 | $this->events[] = new Event($name, $data); 161 | } 162 | 163 | /** 164 | * @return Event[] 165 | */ 166 | public function getExpectations() 167 | { 168 | return $this->events; 169 | } 170 | 171 | /** 172 | * @param callable $callable 173 | */ 174 | public function onStart(callable $callable) 175 | { 176 | $this->startCallback = $callable; 177 | } 178 | 179 | /** 180 | * @param callable $callable 181 | */ 182 | public function onStop(callable $callable) 183 | { 184 | $this->stopCallback = $callable; 185 | } 186 | 187 | /** 188 | * @param string $model 189 | * @param mixed[] $config 190 | * @return object 191 | */ 192 | public function reflect($model, $config = []) 193 | { 194 | foreach ($config as $key=>$value) 195 | { 196 | if ($value === 'Dazzle\Loop\Loop' || $value === 'Dazzle\Loop\LoopInterface') 197 | { 198 | $config[$key] = $this->getLoop(); 199 | } 200 | } 201 | 202 | return (new ReflectionClass($model))->newInstanceArgs($config); 203 | } 204 | 205 | /** 206 | * @throws Exception 207 | */ 208 | private function start() 209 | { 210 | $sim = $this; 211 | 212 | $scenario = $this->scenario; 213 | $scenario($sim); 214 | 215 | if ($this->stopFlags === true) 216 | { 217 | return; 218 | } 219 | 220 | $onStart = $this->startCallback; 221 | $loop = $this->loop; 222 | 223 | $loop->onStart(function() use($sim, $onStart) { 224 | $onStart($sim); 225 | }); 226 | $loop->addTimer(5, function() use($sim) { 227 | $sim->fail('Timeout for test has been reached.'); 228 | }); 229 | 230 | $loop->start(); 231 | 232 | if ($sim->failureMessage !== null) 233 | { 234 | throw new Exception($sim->failureMessage); 235 | } 236 | } 237 | 238 | /** 239 | * 240 | */ 241 | private function stop() 242 | { 243 | if ($this->loop !== null && $this->loop->isRunning()) 244 | { 245 | $this->loop->stop(); 246 | } 247 | 248 | if ($this->stopFlags === false) 249 | { 250 | $callable = $this->stopCallback; 251 | $callable($this); 252 | } 253 | 254 | $this->stopFlags = true; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /test/_Simulation/SimulationInterface.php: -------------------------------------------------------------------------------- 1 |