diff --git a/Command/ServerProfileCommand.php b/Command/ServerProfileCommand.php index 4114a100..11796dbe 100644 --- a/Command/ServerProfileCommand.php +++ b/Command/ServerProfileCommand.php @@ -49,10 +49,10 @@ public function __construct(KernelInterface $kernel, HttpServer $server, HttpSer protected function configure(): void { $this->setName('swoole:server:profile') - ->setDescription('Handles specified amount of requests to a local swoole server. Useful for debug or benchmarking.') + ->setDescription('Handles specified amount of requests to a local swoole server. Useful for profiling.') ->addArgument('requests', InputArgument::REQUIRED, 'Number of requests to handle by the server') - ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host of the server') - ->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port of the server') + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host name to listen to.') + ->addOption('port', null, InputOption::VALUE_REQUIRED, 'Range 0-65535. When 0 random available port is chosen.') ->addOption('enable-static', null, InputOption::VALUE_NONE, 'Enables static files serving'); } @@ -73,13 +73,15 @@ protected function execute(InputInterface $input, OutputInterface $output): void $io = new SymfonyStyle($input, $output); - $host = (string) ($input->getOption('host') ?? $this->configuration->getHost()); - $port = (int) ($input->getOption('port') ?? $this->configuration->getPort()); + $this->configuration->changeSocket( + (string) ($input->getOption('host') ?? $this->configuration->getHost()), + (int) ($input->getOption('port') ?? $this->configuration->getPort()) + ); - $this->configuration->changeSocket($host, $port); - - if ((bool) $input->getOption('enable-static')) { - $this->configuration->enableServingStaticFiles(\dirname($this->kernel->getRootDir()).'/public'); + if (\filter_var($input->getOption('enable-static'), FILTER_VALIDATE_BOOLEAN)) { + $this->configuration->enableServingStaticFiles( + $this->configuration->hasPublicDir() ? $this->configuration->getPublicDir() : \dirname($this->kernel->getRootDir()).'/public' + ); } $requestLimit = (int) $input->getArgument('requests'); @@ -89,6 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): void $trustedHosts = ServerUtils::decodeStringAsSet($_SERVER['APP_TRUSTED_HOSTS']); $trustedProxies = ServerUtils::decodeStringAsSet($_SERVER['APP_TRUSTED_PROXIES']); + $this->driver->boot([ 'symfonyStyle' => $io, 'requestLimit' => $requestLimit, @@ -96,6 +99,8 @@ protected function execute(InputInterface $input, OutputInterface $output): void 'trustedProxies' => $trustedProxies, ]); + $this->server->setup($this->configuration); + $rows = [ ['env', $this->kernel->getEnvironment()], ['debug', \var_export($this->kernel->isDebug(), true)], @@ -110,7 +115,7 @@ protected function execute(InputInterface $input, OutputInterface $output): void $rows[] = ['public_dir', $this->configuration->getPublicDir()]; } - $io->success(\sprintf('Swoole HTTP Server started on http://%s:%d', $host, $port)); + $io->success(\sprintf('Swoole HTTP Server started on http://%s:%d', $this->configuration->getHost(), $this->configuration->getPort())); $io->table(['Configuration', 'Values'], $rows); if ($this->kernel->isDebug()) { @@ -118,7 +123,10 @@ protected function execute(InputInterface $input, OutputInterface $output): void dump($this->configuration->getSwooleSettings()); } - $this->server->setSymfonyStyle($io); - $this->server->start($this->driver, $this->configuration); + if ($this->server->start($this->driver)) { + $io->success('Swoole HTTP Server has been successfully shutdown.'); + } else { + $io->error('Failure during starting Swoole HTTP Server.'); + } } } diff --git a/Command/ServerRunCommand.php b/Command/ServerRunCommand.php index 6623ce50..66089c26 100644 --- a/Command/ServerRunCommand.php +++ b/Command/ServerRunCommand.php @@ -47,10 +47,10 @@ public function __construct(KernelInterface $kernel, HttpServer $server, HttpSer protected function configure(): void { $this->setName('swoole:server:run') - ->setDescription('Runs a local swoole server') - ->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host of the server') - ->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port of the server') - ->addOption('enable-static', null, InputOption::VALUE_NONE, 'Enables static files serving'); + ->setDescription('Runs a local swoole http server') + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host name to listen to.') + ->addOption('port', null, InputOption::VALUE_REQUIRED, 'Range 0-65535. When 0 random available port is chosen.') + ->addOption('enable-static', null, InputOption::VALUE_NONE, 'Enables static files serving. Uses configured public directory or symfony default one.'); } /** @@ -69,13 +69,15 @@ protected function execute(InputInterface $input, OutputInterface $output): void $io = new SymfonyStyle($input, $output); - $host = (string) ($input->getOption('host') ?? $this->configuration->getHost()); - $port = (int) ($input->getOption('port') ?? $this->configuration->getPort()); + $this->configuration->changeSocket( + (string) ($input->getOption('host') ?? $this->configuration->getHost()), + (int) ($input->getOption('port') ?? $this->configuration->getPort()) + ); - $this->configuration->changeSocket($host, $port); - - if ((bool) $input->getOption('enable-static')) { - $this->configuration->enableServingStaticFiles(\dirname($this->kernel->getRootDir()).'/public'); + if (\filter_var($input->getOption('enable-static'), FILTER_VALIDATE_BOOLEAN)) { + $this->configuration->enableServingStaticFiles( + $this->configuration->hasPublicDir() ? $this->configuration->getPublicDir() : \dirname($this->kernel->getRootDir()).'/public' + ); } $this->driver->boot([ @@ -83,6 +85,8 @@ protected function execute(InputInterface $input, OutputInterface $output): void 'trustedProxies' => ServerUtils::decodeStringAsSet($_SERVER['APP_TRUSTED_PROXIES']), ]); + $this->server->setup($this->configuration); + $rows = [ ['env', $this->kernel->getEnvironment()], ['debug', \var_export($this->kernel->isDebug(), true)], @@ -94,10 +98,13 @@ protected function execute(InputInterface $input, OutputInterface $output): void $rows[] = ['public_dir', $this->configuration->getPublicDir()]; } - $io->success(\sprintf('Swoole HTTP Server started on http://%s:%d', $host, $port)); + $io->success(\sprintf('Swoole HTTP Server started on http://%s:%d', $this->configuration->getHost(), $this->configuration->getPort())); $io->table(['Configuration', 'Values'], $rows); - $this->server->setSymfonyStyle($io); - $this->server->start($this->driver, $this->configuration); + if ($this->server->start($this->driver)) { + $io->success('Swoole HTTP Server has been successfully shutdown.'); + } else { + $io->error('Failure during starting Swoole HTTP Server.'); + } } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 8cdf4b9a..88e6d2ab 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -31,6 +31,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('http_server') ->children() ->scalarNode('host') + ->cannotBeEmpty() ->defaultValue('127.0.0.1') ->end() ->integerNode('port') @@ -38,6 +39,20 @@ public function getConfigTreeBuilder(): TreeBuilder ->max(65535) ->defaultValue(9501) ->end() + ->enumNode('runningMode') + ->defaultValue('process') + ->values(['process', 'reactor']) + ->cannotBeEmpty() + ->end() + ->enumNode('socketType') + ->cannotBeEmpty() + ->defaultValue('sock_tcp_ipv4') + ->values(['sock_tcp_ipv4', 'sock_tcp_ipv6', 'sock_udp_ipv4', 'sock_udp_ipv6', 'unix_dgram', 'unix_stream']) + ->end() + ->booleanNode('sslEnabled') + ->defaultFalse() + ->treatNullLike(false) + ->end() ->arrayNode('static') ->addDefaultsIfNotSet() ->children() diff --git a/DependencyInjection/SwooleExtension.php b/DependencyInjection/SwooleExtension.php index 6c86c803..7feadfd7 100644 --- a/DependencyInjection/SwooleExtension.php +++ b/DependencyInjection/SwooleExtension.php @@ -54,12 +54,6 @@ public function load(array $configs, ContainerBuilder $container): void */ private function registerHttpServer(array $config, ContainerBuilder $container): void { - $container->getDefinition('app.swoole.server.http_server.server_instance') - ->addArgument($config['host']) - ->addArgument($config['port']) - ->addArgument(SWOOLE_BASE) - ->addArgument(SWOOLE_TCP); - if (!empty($config['services'])) { $this->registerHttpServerServices($config['services'], $container); } @@ -73,6 +67,9 @@ private function registerHttpServerConfiguration(array $config, ContainerBuilder 'static' => $static, 'host' => $host, 'port' => $port, + 'runningMode' => $runningMode, + 'socketType' => $socketType, + 'sslEnabled' => $sslEnabled, 'settings' => $settings, ] = $config; @@ -101,6 +98,9 @@ private function registerHttpServerConfiguration(array $config, ContainerBuilder $container->getDefinition(HttpServerConfiguration::class) ->addArgument($host) ->addArgument($port) + ->addArgument($runningMode) + ->addArgument($socketType) + ->addArgument($sslEnabled) ->addArgument($settings); } diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 0b534445..655639c1 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -20,12 +20,11 @@ services: 'App\Bundle\SwooleBundle\Server\HttpServerConfiguration': - 'app.swoole.server.http_server.server_instance': - class: Swoole\Http\Server - lazy: true + 'app.swoole.server.http_server.factory': + class: App\Bundle\SwooleBundle\Server\HttpServerFactory 'App\Bundle\SwooleBundle\Server\HttpServer': - arguments: ['@app.swoole.server.http_server.server_instance'] + arguments: ['@app.swoole.server.http_server.factory'] 'App\Bundle\SwooleBundle\Command\ServerRunCommand': tags: ['console.command'] diff --git a/Server/HttpServer.php b/Server/HttpServer.php index 5121fdb9..e4eacd39 100644 --- a/Server/HttpServer.php +++ b/Server/HttpServer.php @@ -4,66 +4,68 @@ namespace App\Bundle\SwooleBundle\Server; -use RuntimeException; +use Assert\Assertion; use Swoole\Http\Server; -use Symfony\Component\Console\Style\SymfonyStyle; final class HttpServer { + /** + * @var Server|null + */ private $server; /** - * @var SymfonyStyle|null + * @var HttpServerFactory */ - private $symfonyStyle; + private $serverFactory; - public function __construct(Server $server) + public function __construct(HttpServerFactory $serverFactory) { - $this->server = $server; + $this->serverFactory = $serverFactory; } - public function setSymfonyStyle(SymfonyStyle $symfonyStyle): void + /** + * @param HttpServerConfiguration $configuration + * + * @throws \Assert\AssertionFailedException + */ + public function setup(HttpServerConfiguration $configuration): void { - $this->symfonyStyle = $symfonyStyle; - } + Assertion::null($this->server, 'Cannot setup swoole http server multiple times.'); + $server = $this->serverFactory->make($configuration); - public function start(RequestHandlerInterface $driver, HttpServerConfiguration $configuration): void - { - $this->server->port = $configuration->getPort(); - $this->server->host = $configuration->getHost(); - $this->server->on('request', [$driver, 'handle']); - $this->server->set($configuration->getSwooleSettings()); + $server->set($configuration->getSwooleSettings()); + + if (0 === $configuration->getPort()) { + $configuration->changePort($server->port); + } - $this->havingSymfonyStyle(function (SymfonyStyle $io): void { - if (!$this->server->start()) { - $io->error('Failure during starting Swoole HTTP Server.'); - } else { - $io->success('Swoole HTTP Server has been successfully shutdown.'); - } - }, function (): void { - if (!$this->server->start()) { - throw new RuntimeException('Failure during starting Swoole HTTP Server.'); - } - }); + $this->server = $server; } - public function shutdown(): void + /** + * @param RequestHandlerInterface $driver + * + * @throws \Assert\AssertionFailedException + * + * @return bool + */ + public function start(RequestHandlerInterface $driver): bool { - $this->server->shutdown(); + Assertion::isInstanceOf($this->server, Server::class, 'Swoole HTTP Server has not been setup yet. Please use setup() method.'); + + $this->server->on('request', [$driver, 'handle']); + + return $this->server->start(); } /** - * @param callable $having executes function if symfony style is available - * @param callable $notHaving executes function if symfony style is unavailable - * - * @return mixed + * @throws \Assert\AssertionFailedException */ - private function havingSymfonyStyle(callable $having, callable $notHaving) + public function shutdown(): void { - if ($this->symfonyStyle instanceof SymfonyStyle) { - return $having($this->symfonyStyle); - } + Assertion::isInstanceOf($this->server, Server::class, 'Swoole HTTP Server has not been setup yet. Please use setup() method.'); - return $notHaving(); + $this->server->shutdown(); } } diff --git a/Server/HttpServerConfiguration.php b/Server/HttpServerConfiguration.php index 91f618c7..dec69431 100644 --- a/Server/HttpServerConfiguration.php +++ b/Server/HttpServerConfiguration.php @@ -5,6 +5,8 @@ namespace App\Bundle\SwooleBundle\Server; use Assert\Assertion; +use DomainException; +use InvalidArgumentException; final class HttpServerConfiguration { @@ -14,7 +16,7 @@ final class HttpServerConfiguration * @see https://github.com/swoole/swoole-docs/blob/master/modules/swoole-server/configuration.md * @see https://github.com/swoole/swoole-docs/blob/master/modules/swoole-http-server/configuration.md */ - private const SWOOLE_HTTP_SERVER_SETTINGS_MAPPING = [ + private const SWOOLE_HTTP_SERVER_CONFIGURATION = [ 'reactor_count' => 'reactor_num', 'worker_count' => 'worker_num', 'serve_static_files' => 'enable_static_handler', @@ -25,46 +27,109 @@ final class HttpServerConfiguration ]; private const SWOOLE_LOG_LEVELS = [ - 'debug' => 0, - 'trace' => 1, - 'info' => 2, - 'notice' => 3, - 'warning' => 4, - 'error' => 5, + 'debug' => SWOOLE_LOG_DEBUG, + 'trace' => SWOOLE_LOG_TRACE, + 'info' => SWOOLE_LOG_INFO, + 'notice' => SWOOLE_LOG_NOTICE, + 'warning' => SWOOLE_LOG_WARNING, + 'error' => SWOOLE_LOG_ERROR, ]; + private const SWOOLE_RUNNING_MODE = [ + 'reactor' => SWOOLE_BASE, + 'thread' => SWOOLE_THREAD, + 'process' => SWOOLE_PROCESS, + ]; + + private const SWOOLE_SOCKET_TYPE = [ + 'sock_tcp_ipv4' => SWOOLE_SOCK_TCP, + 'sock_tcp_ipv6' => SWOOLE_SOCK_TCP6, + 'sock_udp_ipv4' => SWOOLE_SOCK_UDP, + 'sock_udp_ipv6' => SWOOLE_SOCK_UDP6, + 'sock_unix_dgram' => SWOOLE_SOCK_UNIX_DGRAM, + 'sock_unix_stream' => SWOOLE_SOCK_UNIX_STREAM, + ]; + + private const PORT_MAX_VALUE = 65535; + private const PORT_MIN_VALUE = 0; + + /** + * @see https://github.com/swoole/swoole-docs/blob/master/modules/swoole-server/methods/construct.md#parameter + * + * @var string + * @var int $port + * @var string $runningMode + * @var string $socketType + * @var bool $sslEnabled + */ private $host; private $port; + private $runningMode; + private $socketType; + private $sslEnabled; + + // Container for SWOOLE_HTTP_SERVER_CONFIGURATION values private $settings; /** * @param string $host * @param int $port - * @param array $settings settings available: - * - reactor_count (default: number of cpu cores) - * - worker_count (default: 2 * number of cpu cores) - * - serve_static_files (default: false) - * - public_dir (default: '%kernel.root_dir%/public') + * @param string $runningMode + * @param string $socketType + * @param bool $sslEnabled + * @param array $settings settings available: + * - reactor_count (default: number of cpu cores) + * - worker_count (default: 2 * number of cpu cores) + * - serve_static_files (default: false) + * - public_dir (default: '%kernel.root_dir%/public') * * @throws \Assert\AssertionFailedException */ - public function __construct(string $host = 'localhost', int $port = 9501, array $settings = []) - { - $this->changeSocket($host, $port); + public function __construct( + string $host = 'localhost', + int $port = 9501, + string $runningMode = 'process', + string $socketType = 'sock_tcp', + bool $sslEnabled = false, + array $settings = [] + ) { $this->initializeSettings($settings); + $this->changeSocket($host, $port, $runningMode, $socketType, $sslEnabled); } /** * @param string $host * @param int $port + * @param string $runningMode + * @param string $socketType + * @param bool $sslEnabled * * @throws \Assert\AssertionFailedException */ - public function changeSocket(string $host, int $port): void + public function changeSocket(string $host, int $port, string $runningMode = 'process', string $socketType = 'sock_tcp_ipv4', bool $sslEnabled = false): void { - Assertion::notBlank($host, 'Host cannot be blank'); - Assertion::greaterThan($port, 0, 'Port cannot be negative'); + Assertion::notBlank($host, 'Host cannot be blank.'); + Assertion::between($port, self::PORT_MIN_VALUE, self::PORT_MAX_VALUE, 'Provided port value "%s" is not between 0 and 65535.'); + Assertion::inArray($runningMode, \array_keys(self::SWOOLE_RUNNING_MODE)); + Assertion::inArray($socketType, \array_keys(self::SWOOLE_SOCKET_TYPE)); + + if ($sslEnabled) { + Assertion::defined('SWOOLE_SSL', 'Swoole SSL support is disabled. You must install php extension with SSL support enabled.'); + } + $this->host = $host; + $this->runningMode = $runningMode; + $this->port = $port; + $this->socketType = $socketType; + $this->sslEnabled = $sslEnabled; + } + + public function changePort(int $port): void + { + if (0 !== $this->port || $port <= self::PORT_MIN_VALUE || $port > self::PORT_MAX_VALUE) { + throw new DomainException('Method changePort() can be used directly, only if port originally was set to 0, which means random available port. Use changeSocket() instead.'); + } + $this->port = $port; } @@ -89,7 +154,7 @@ public function enableServingStaticFiles(string $publicDir): void */ private function validateSetting(string $key, $value): void { - Assertion::keyExists(self::SWOOLE_HTTP_SERVER_SETTINGS_MAPPING, $key, 'There is no configuration mapping for setting "%s".'); + Assertion::keyExists(self::SWOOLE_HTTP_SERVER_CONFIGURATION, $key, 'There is no configuration mapping for setting "%s".'); if ('serve_static_files' === $key) { Assertion::boolean($value, 'Serve static files setting must be a boolean'); @@ -169,6 +234,34 @@ public function getPort(): int return $this->port; } + public function getSwooleRunningMode(): int + { + return self::SWOOLE_RUNNING_MODE[$this->runningMode]; + } + + /** + * @return int + */ + public function getSwooleSocketType(): int + { + $type = self::SWOOLE_SOCKET_TYPE[$this->socketType]; + + if (!$this->isSslEnabled()) { + return $type; + } + + if (!\defined('SWOOLE_SSL')) { + throw new InvalidArgumentException('Swoole SSL support is disabled. You must install php extension with SSL support enabled.'); + } + + return $type | SWOOLE_SSL; + } + + public function isSslEnabled(): bool + { + return $this->sslEnabled; + } + public function hasPublicDir(): bool { return isset($this->settings['public_dir']); @@ -212,7 +305,7 @@ public function getSwooleSettings(): array { $swooleSettings = []; foreach ($this->settings as $key => $setting) { - $swooleSettingKey = self::SWOOLE_HTTP_SERVER_SETTINGS_MAPPING[$key]; + $swooleSettingKey = self::SWOOLE_HTTP_SERVER_CONFIGURATION[$key]; $swooleGetter = \sprintf('getSwoole%s', \str_replace('_', '', $swooleSettingKey)); $swooleSettings[$swooleSettingKey] = \method_exists($this, $swooleGetter) ? $this->{$swooleGetter}() : $setting; } @@ -225,7 +318,7 @@ public function getSwooleSettings(): array * * @return int */ - private function getSwooleLogLevel(): int + public function getSwooleLogLevel(): int { return self::SWOOLE_LOG_LEVELS[$this->settings['log_level']]; } diff --git a/Server/HttpServerFactory.php b/Server/HttpServerFactory.php new file mode 100644 index 00000000..d7641477 --- /dev/null +++ b/Server/HttpServerFactory.php @@ -0,0 +1,20 @@ +getHost(), + $configuration->getPort(), + $configuration->getSwooleRunningMode(), + $configuration->getSwooleSocketType() + ); + } +}