├── images ├── group.png └── laravel-octane-gatewayworker group qrcode.png ├── src ├── WebmanResponse.php ├── Commands │ ├── stubs │ │ ├── Sockets.stub │ │ ├── Events.stub │ │ └── CustomProcess.stub │ ├── WorkermanGatewayMakeEventsCommand.php │ ├── WorkermanGatewayMakeSocketsCommand.php │ ├── WorkermanGatewayMakeCustomProcessCommand.php │ ├── Concerns │ │ ├── InstallsWorkermanDependencies.php │ │ └── InstallsGatewayWorkerDependencies.php │ └── StartWorkermanGatewayCommand.php ├── Process │ ├── DdosProxyHttp.php │ ├── DdosProxyHttps.php │ ├── DatabaseHeartbeat.php │ ├── HttpWorkerProcess.php │ └── Monitor.php ├── Gateway.php ├── webman_helpers.php ├── WorkermanGatewayWorkerServiceProvider.php ├── LaravelOctaneWorkermanServiceProvider.php ├── webman_bootstrap.php ├── WebmanRequest.php ├── WorkerTrait.php ├── WebmanPlugin.php ├── Workerman │ ├── ServerStateFile.php │ ├── ServerProcessInspector.php │ ├── Actions │ │ └── ConvertWorkermanRequestToIlluminateRequest.php │ └── WorkermanClient.php ├── WebmanConfig.php └── helpers.php ├── webman_plugin_require_example ├── plugin │ └── webman │ │ └── push │ │ ├── app.php │ │ ├── process.php │ │ └── route.php └── README.md ├── LICENSE ├── .php_cs ├── composer.json ├── bin ├── gatewayworker-server └── createGatewayWorker.php ├── README.md └── config └── workerman.php /images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JieAnthony/laravel-octane-workerman/HEAD/images/group.png -------------------------------------------------------------------------------- /images/laravel-octane-gatewayworker group qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JieAnthony/laravel-octane-workerman/HEAD/images/laravel-octane-gatewayworker group qrcode.png -------------------------------------------------------------------------------- /src/WebmanResponse.php: -------------------------------------------------------------------------------- 1 | $method(...$args); 15 | } 16 | 17 | return static::worekrCall($method, $args); 18 | } 19 | } -------------------------------------------------------------------------------- /webman_plugin_require_example/plugin/webman/push/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | 'websocket' => 'websocket://0.0.0.0:3131', 5 | 'api' => 'http://0.0.0.0:3232', 6 | 'app_key' => '14489806b8d0a8a6c12e3a0ca58f6cfa', 7 | 'app_secret' => 'd645dab5f96f87ab7a764f6792f3f7c9', 8 | 'channel_hook' => 'http://127.0.0.1:8787/plugin/webman/push/hook', 9 | 'auth' => '/plugin/webman/push/auth' 10 | ]; -------------------------------------------------------------------------------- /src/Commands/stubs/Sockets.stub: -------------------------------------------------------------------------------- 1 | close(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Commands/stubs/Events.stub: -------------------------------------------------------------------------------- 1 | close(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Process/DdosProxyHttp.php: -------------------------------------------------------------------------------- 1 | pipe($con); 16 | $con->pipe($rcon); 17 | 18 | $rcon->connect(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Gateway.php: -------------------------------------------------------------------------------- 1 | onConnect = function ($con) { 10 | }; 11 | 12 | $worker = new Worker('tcp://0.0.0.0:443'); 13 | $worker->count = 8; 14 | $worker->onConnect = function ($con) { 15 | $rcon = new AsyncTcpConnection('tcp://内网ip:443'); 16 | $rcon->pipe($con); 17 | $con->pipe($rcon); 18 | $rcon->connect(); 19 | }; 20 | 21 | 22 | class DdosProxyHttps 23 | { 24 | public function onConnect($con) 25 | { 26 | $rcon = new AsyncTcpConnection('tcp://内网ip:80'); 27 | $rcon->pipe($con); 28 | $con->pipe($rcon); 29 | $rcon->connect(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webman_plugin_require_example/plugin/webman/push/process.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'enable' => true, 8 | 'handler' => Server::class, 9 | 'listen' => config('plugin.webman.push.app.websocket'), 10 | 'count' => 1, // 必须是1 11 | 'reloadable' => false, // 执行reload不重启 12 | 'constructor' => [ 13 | 'api_listen' => config('plugin.webman.push.app.api'), 14 | 'app_info' => [ 15 | config('plugin.webman.push.app.app_key') => [ 16 | 'channel_hook' => config('plugin.webman.push.app.channel_hook'), 17 | 'app_secret' => config('plugin.webman.push.app.app_secret'), 18 | ], 19 | ] 20 | ] 21 | ] 22 | ]; -------------------------------------------------------------------------------- /src/Process/DatabaseHeartbeat.php: -------------------------------------------------------------------------------- 1 | $item) { 26 | if ($item['driver'] == 'mysql') { 27 | DB::connection($key)->select('select 1'); 28 | } 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Anthony <407968526@qq.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Commands/stubs/CustomProcess.stub: -------------------------------------------------------------------------------- 1 | worker = $worker; 16 | 17 | var_dump("worker {$worker->id} start"); 18 | } 19 | 20 | public function onWorkerStop(Worker $worker) 21 | { 22 | var_dump("worker {$worker->id} stop"); 23 | } 24 | 25 | public function onConnect(TcpConnection $connection) 26 | { 27 | $this->connection = $connection; 28 | 29 | var_dump("client connect to worker_id {$this->worker->id} successful, current connection_id is {$connection->id}"); 30 | } 31 | 32 | public function onMessage(TcpConnection $connection, $data) 33 | { 34 | var_dump($message = "the worker_id {$this->worker->id} of connection_id {$connection->id} receive message from client: " . $data); 35 | 36 | $connection->send($message); 37 | } 38 | 39 | public function onClose(TcpConnection $connection) 40 | { 41 | $connection->close(); 42 | 43 | var_dump("the worker_id {$this->worker->id} of connection_id {$connection->id} closed"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/WorkermanGatewayWorkerServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 15 | $configPath = __DIR__.'/../config/workerman.php' => config_path('workerman.php'), 16 | ], 'laravel-octane-workerman'); 17 | 18 | $this->mergeConfigFrom( 19 | $configPath, 20 | 'workerman' 21 | ); 22 | 23 | if (!config('octane.workerman')) { 24 | config([ 25 | 'octane.workerman' => config('workerman'), 26 | ]); 27 | } 28 | 29 | Gateway::resolveRegisterAddress(); 30 | } 31 | 32 | public function boot() 33 | { 34 | if ($this->app->runningInConsole()) { 35 | $this->commands([ 36 | WorkermanGatewayMakeSocketsCommand::class, 37 | WorkermanGatewayMakeEventsCommand::class, 38 | WorkermanGatewayMakeCustomProcessCommand::class, 39 | ]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LaravelOctaneWorkermanServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(WorkermanServerProcessInspector::class, function ($app) { 16 | return new WorkermanServerProcessInspector( 17 | $app->make(WorkermanServerStateFile::class), 18 | new SymfonyProcessFactory(), 19 | ); 20 | }); 21 | 22 | $this->app->bind(WorkermanServerStateFile::class, function ($app) { 23 | return new WorkermanServerStateFile($app['config']->get( 24 | 'octane.state_file', 25 | storage_path('logs/octane-server-state.json') 26 | )); 27 | }); 28 | } 29 | 30 | public function boot() 31 | { 32 | if ($this->app->runningInConsole()) { 33 | $this->commands([ 34 | StartWorkermanGatewayCommand::class, 35 | ]); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/WorkermanGatewayMakeEventsCommand.php: -------------------------------------------------------------------------------- 1 | option('path')) { 36 | $name = \Str::replaceFirst($this->rootNamespace(), '', $name); 37 | $filename = str_replace('\\', '/', $name); 38 | 39 | $path = trim($path, '/'); 40 | 41 | return sprintf( 42 | '%s/%s/%s.php', 43 | $this->laravel['path.base'], 44 | $path, 45 | $filename 46 | ); 47 | } 48 | 49 | return parent::getPath($name); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/WorkermanGatewayMakeSocketsCommand.php: -------------------------------------------------------------------------------- 1 | option('path')) { 36 | $name = \Str::replaceFirst($this->rootNamespace(), '', $name); 37 | $filename = str_replace('\\', '/', $name); 38 | 39 | $path = trim($path, '/'); 40 | 41 | return sprintf( 42 | '%s/%s/%s.php', 43 | $this->laravel['path.base'], 44 | $path, 45 | $filename 46 | ); 47 | } 48 | 49 | return parent::getPath($name); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Commands/WorkermanGatewayMakeCustomProcessCommand.php: -------------------------------------------------------------------------------- 1 | option('path')) { 36 | $name = \Str::replaceFirst($this->rootNamespace(), '', $name); 37 | $filename = str_replace('\\', '/', $name); 38 | 39 | $path = trim($path, '/'); 40 | 41 | return sprintf( 42 | '%s/%s/%s.php', 43 | $this->laravel['path.base'], 44 | $path, 45 | $filename 46 | ); 47 | } 48 | 49 | return parent::getPath($name); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/webman_bootstrap.php: -------------------------------------------------------------------------------- 1 | $projects) { 9 | foreach ($projects as $name => $project) { 10 | foreach ($project['autoload']['files'] ?? [] as $file) { 11 | include_once $file; 12 | } 13 | } 14 | } 15 | 16 | $container = \Illuminate\Container\Container::getInstance(); 17 | 18 | # webman route 19 | if (class_exists(\Webman\Route::class)) { 20 | \Webman\Route::container($container); 21 | } 22 | 23 | # webman middleware 24 | if (class_exists(\Webman\Middleware::class)) { 25 | \Webman\Middleware::container($container); 26 | \Webman\Middleware::load(config('middleware', [])); 27 | foreach (webman_config('plugin', []) as $firm => $projects) { 28 | foreach ($projects as $name => $project) { 29 | \Webman\Middleware::load($project['middleware'] ?? []); 30 | } 31 | } 32 | \Webman\Middleware::load(['__static__' => config('static.middleware', [])]); 33 | } 34 | 35 | # bootstrap 36 | foreach (config('bootstrap', []) as $class_name) { 37 | /** @var \Webman\Bootstrap $class_name */ 38 | $class_name::start($worker); 39 | } 40 | 41 | foreach (webman_config('plugin', []) as $firm => $projects) { 42 | foreach ($projects as $name => $project) { 43 | foreach ($project['bootstrap'] ?? [] as $class_name) { 44 | /** @var \Webman\Bootstrap $class_name */ 45 | $class_name::start($worker); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRules([ 5 | '@PSR2' => true, 6 | 'binary_operator_spaces' => true, 7 | 'blank_line_after_opening_tag' => true, 8 | 'compact_nullable_typehint' => true, 9 | 'declare_equal_normalize' => true, 10 | 'lowercase_cast' => true, 11 | 'lowercase_static_reference' => true, 12 | 'new_with_braces' => true, 13 | 'no_unused_imports' => true, 14 | 'no_blank_lines_after_class_opening' => true, 15 | 'no_leading_import_slash' => true, 16 | 'no_whitespace_in_blank_line' => true, 17 | 'ordered_class_elements' => [ 18 | 'order' => [ 19 | 'use_trait', 20 | ], 21 | ], 22 | 'ordered_imports' => [ 23 | 'imports_order' => [ 24 | 'class', 25 | 'function', 26 | 'const', 27 | ], 28 | 'sort_algorithm' => 'none', 29 | ], 30 | 'return_type_declaration' => true, 31 | 'short_scalar_cast' => true, 32 | 'single_blank_line_before_namespace' => true, 33 | 'single_trait_insert_per_statement' => true, 34 | 'ternary_operator_spaces' => true, 35 | 'unary_operator_spaces' => true, 36 | 'visibility_required' => [ 37 | 'elements' => [ 38 | 'const', 39 | 'method', 40 | 'property', 41 | ], 42 | ], 43 | ]) 44 | ->setFinder( 45 | PhpCsFixer\Finder::create() 46 | ->exclude('vendor') 47 | ->in([ 48 | __DIR__.'/src/', 49 | __DIR__.'/bin/' 50 | ]) 51 | ) 52 | ; 53 | -------------------------------------------------------------------------------- /src/WebmanRequest.php: -------------------------------------------------------------------------------- 1 | getRemoteIp(); 17 | 18 | if ($safe_mode && !static::isIntranetIp($remote_ip)) { 19 | return $remote_ip; 20 | } 21 | 22 | return $this->header('client-ip', $this->header('x-forwarded-for', 23 | $this->header('x-real-ip', $this->header('x-client-ip', 24 | $this->header('via', $remote_ip))))); 25 | } 26 | 27 | public static function isIntranetIp(string $ip) 28 | { 29 | $reserved_ips = [ 30 | '167772160' => 184549375, /* 10.0.0.0 - 10.255.255.255 */ 31 | '3232235520' => 3232301055, /* 192.168.0.0 - 192.168.255.255 */ 32 | '2130706432' => 2147483647, /* 127.0.0.0 - 127.255.255.255 */ 33 | '2886729728' => 2887778303, /* 172.16.0.0 - 172.31.255.255 */ 34 | ]; 35 | 36 | $ip_long = ip2long($ip); 37 | 38 | foreach ($reserved_ips as $ip_start => $ip_end) { 39 | if (($ip_long >= $ip_start) && ($ip_long <= $ip_end)) { 40 | return true; 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | 47 | public function __call($method, $args) 48 | { 49 | if (method_exists(\request(), $method)) { 50 | return \request()->$method(...$args); 51 | } 52 | 53 | return static::workerCall($method, $args); 54 | } 55 | } -------------------------------------------------------------------------------- /src/WorkerTrait.php: -------------------------------------------------------------------------------- 1 | $instance) { 49 | if (method_exists($instance, $method)) { 50 | return $instance->$method(...$args); 51 | } 52 | } 53 | 54 | throw new \Exception("\\request() not found method {$method}, please contact my24251325@gmail.com"); 55 | } 56 | 57 | public static function __callStatic($method, $args) 58 | { 59 | if (!static::$_instance) { 60 | static::$_instance = new static(); 61 | } 62 | 63 | return static::$_instance->$method(...$args); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /webman_plugin_require_example/README.md: -------------------------------------------------------------------------------- 1 | # 论坛 2 | 3 | 访问地址 https://laravel-workerman.iwnweb.com/ 4 | 5 | # 引入 webman/push 适配说明文档 6 | 7 | https://github.com/mouyong/laravel-octane-workerman/tree/gatewayworker/webman_plugin_require_example 8 | 9 | ------- 10 | 11 | # 在 laravel 框架中引入 webman plugin 12 | 13 | ## 1. 添加插件安装脚本到项目中 14 | 15 | 在项目的 `composer.json` 添加如下内容。可参考 `laravel-octane-workerman` 的 `composer.json` 中,`scripts` 配置 16 | 17 | ```json 18 | { 19 | // ... 20 | "scripts": { 21 | "post-package-install": [ 22 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::install" 23 | ], 24 | "post-package-update": [ 25 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::install" 26 | ], 27 | "pre-package-uninstall": [ 28 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::uninstall" 29 | ], 30 | // ... 31 | } 32 | // ... 33 | } 34 | ``` 35 | 36 | ## 2. 引入 webman 插件 37 | 38 | ``` 39 | composer require webman/push -vvv 40 | ``` 41 | 42 | ## 3. 适配 laravel 框架 43 | 44 | ### 1. 路由适配 45 | 46 | - 修改插件的 `route.php` 文件 47 | - 将 `Webman\Request` 替换为 `Illuminate\Http\Request` 48 | - 将 `Webman\Route` 替换为 `Illuminate\Routing\Router` 49 | - 使用 laravel 的路由分组包含路由 50 | - 替换 `Route::` 调用为 `$route->` 51 | 52 | 示例: 53 | ```php 54 | // use support\Request; 55 | // use Webman\Route; 56 | use Illuminate\Http\Request; 57 | use Illuminate\Routing\Router; 58 | 59 | app('router')->middleware(['web'])->group(function (Router $route) { 60 | /** 61 | * 推送js客户端文件 62 | */ 63 | // here before 64 | // Route::any('plugin/webman/push/push.js', function (Request $request) { 65 | // return response()->file(base_path() . '/vendor/webman/push/src/push.js'); 66 | // }); 67 | 68 | // here after 69 | $route->any('plugin/webman/push/push.js', function (Request $request) { 70 | return response()->file(base_path() . '/vendor/webman/push/src/push.js'); 71 | }); 72 | }); 73 | ``` 74 | - 手动执行 `composer dump-autoload`,让项目自动自行 `@php artisan package:discover --ansi` 命令 75 | ```bash 76 | composer du # 简写 77 | ```` 78 | -------------------------------------------------------------------------------- /src/WebmanPlugin.php: -------------------------------------------------------------------------------- 1 | getOperation(); 12 | 13 | $autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload(); 14 | 15 | if (!isset($autoload['psr-4'])) { 16 | return; 17 | } 18 | 19 | $namespace = key($autoload['psr-4']); 20 | 21 | $install_function = "\\{$namespace}Install::install"; 22 | $plugin_const = "\\{$namespace}Install::WEBMAN_PLUGIN"; 23 | 24 | if (defined($plugin_const) && is_callable($install_function)) { 25 | try { 26 | $install_function(); 27 | } catch (\Throwable $e) { 28 | echo $e->getMessage()."\n"; 29 | } 30 | } 31 | } 32 | 33 | public static function update($event) 34 | { 35 | static::install($event); 36 | } 37 | 38 | public static function uninstall($event) 39 | { 40 | static::findHepler(); 41 | 42 | $autoload = $event->getOperation()->getPackage()->getAutoload(); 43 | 44 | if (!isset($autoload['psr-4'])) { 45 | return; 46 | } 47 | 48 | $namespace = key($autoload['psr-4']); 49 | 50 | $uninstall_function = "\\{$namespace}Install::uninstall"; 51 | $plugin_const = "\\{$namespace}Install::WEBMAN_PLUGIN"; 52 | 53 | if (defined($plugin_const) && is_callable($uninstall_function)) { 54 | try { 55 | $uninstall_function(); 56 | } catch (\Throwable $e) { 57 | echo $e->getMessage()."\n"; 58 | } 59 | } 60 | } 61 | 62 | protected static function findHepler() 63 | {// Plugin.php in webman 64 | $file = __DIR__ . '/helpers.php'; 65 | if (is_file($file)) { 66 | require_once $file; 67 | } 68 | 69 | // Plugin.php in webman 70 | $file = base_path() . '/vendor/autoload.php'; 71 | if (is_file($file)) { 72 | require_once $file; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Workerman/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 21 | ? json_decode(file_get_contents($this->path), true) 22 | : []; 23 | 24 | return [ 25 | 'masterProcessId' => $state['masterProcessId'] ?? null, 26 | 'state' => $state['state'] ?? [], 27 | ]; 28 | } 29 | 30 | /** 31 | * Write the given process ID to the server state file. 32 | * 33 | * @param int $masterProcessId 34 | * @return void 35 | */ 36 | public function writeProcessId(int $masterProcessId): void 37 | { 38 | if (!is_writable($this->path) && !is_writable(dirname($this->path))) { 39 | throw new RuntimeException('Unable to write to process ID file.'); 40 | } 41 | 42 | file_put_contents($this->path, json_encode( 43 | array_merge($this->read(), ['masterProcessId' => $masterProcessId]), 44 | JSON_PRETTY_PRINT 45 | )); 46 | } 47 | 48 | /** 49 | * Write the given state array to the server state file. 50 | * 51 | * @param array $newState 52 | * @return void 53 | */ 54 | public function writeState(array $newState): void 55 | { 56 | if (!is_writable($this->path) && !is_writable(dirname($this->path))) { 57 | throw new RuntimeException('Unable to write to process ID file.'); 58 | } 59 | 60 | file_put_contents($this->path, json_encode( 61 | array_merge($this->read(), ['state' => $newState]), 62 | JSON_PRETTY_PRINT 63 | )); 64 | } 65 | 66 | /** 67 | * Delete the process ID file. 68 | * 69 | * @return bool 70 | */ 71 | public function delete(): bool 72 | { 73 | if (is_writable($this->path)) { 74 | return @unlink($this->path); 75 | } 76 | 77 | return false; 78 | } 79 | 80 | /** 81 | * Get the path to the process ID file. 82 | * 83 | * @return string 84 | */ 85 | public function path(): string 86 | { 87 | return $this->path; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InstallsWorkermanDependencies.php: -------------------------------------------------------------------------------- 1 | confirm('Octane requires "workerman/workerman:^4.0" and "workerman/psr7:^1.4.4". Do you wish to install it as a dependency?')) { 28 | $this->error('Octane requires "workerman/workerman" and and "workerman/psr7".'); 29 | 30 | return false; 31 | } 32 | 33 | $command = $this->findComposer().' require workerman/workerman:^4.0 workerman/psr7:^1.4.4 --with-all-dependencies'; 34 | 35 | $process = Process::fromShellCommandline($command, null, null, null, null); 36 | 37 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 38 | try { 39 | $process->setTty(true); 40 | } catch (RuntimeException $e) { 41 | $this->output->writeln('Warning: '.$e->getMessage()); 42 | } 43 | } 44 | 45 | try { 46 | $process->run(function ($type, $line) { 47 | $this->output->write($line); 48 | }); 49 | } catch (ProcessSignaledException $e) { 50 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 51 | throw $e; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * Get the composer command for the environment. 60 | * 61 | * @return string 62 | */ 63 | protected function findComposer() 64 | { 65 | $composerPath = getcwd().'/composer.phar'; 66 | 67 | $phpPath = (new PhpExecutableFinder)->find(); 68 | 69 | if (! file_exists($composerPath)) { 70 | $composerPath = (new ExecutableFinder())->find('composer'); 71 | } 72 | 73 | return '"'.$phpPath.'" '.$composerPath; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Process/HttpWorkerProcess.php: -------------------------------------------------------------------------------- 1 | workerman = $worker; 25 | $this->workermanConfig = config('workerman'); 26 | $this->httpConfig = config('workerman.http'); 27 | $this->workermanClient = new WorkermanClient(); 28 | 29 | Http::requestClass(ServerRequest::class); 30 | /** @var OctaneWorker $worker */ 31 | $this->worker = tap((new OctaneWorker( 32 | new ApplicationFactory($_SERVER['APP_BASE_PATH']), $this->workermanClient 33 | )))->boot(); 34 | } 35 | 36 | public function onWorkerStop(Worker $worker) 37 | { 38 | // var_dump("worker {$worker->id} stop"); 39 | } 40 | 41 | public function onConnect(TcpConnection $connection) 42 | { 43 | $this->connection = $connection; 44 | 45 | // var_dump("client connect to worker_id {$this->workerman->id} successful, current connection_id is {$connection->id}"); 46 | } 47 | 48 | public function onMessage(ConnectionInterface $connection, $request) 49 | { 50 | $worker = $this->worker; 51 | $workerman = $this->workerman; 52 | $workermanClient = $this->workermanClient; 53 | 54 | try { 55 | // bind webman request and response 56 | request_bind_connection($workerman, $worker, $connection, $request); 57 | response_bind_connection($workerman, $worker, $connection); 58 | } catch (\Throwable $e) { 59 | $connection->send($e->getMessage()); 60 | 61 | exit(1); 62 | } 63 | 64 | [$request, $context] = $workermanClient->marshalRequest(new RequestContext([ 65 | 'request' => $request, 66 | 'connection' => $connection, 67 | 'publicPath' => $this->httpConfig['publicPath'], 68 | ])); 69 | 70 | $worker->handle($request, $context); 71 | } 72 | 73 | public function onClose(TcpConnection $connection) 74 | { 75 | $connection->close(); 76 | 77 | // var_dump("the worker_id {$this->workerman->id} of connection_id {$connection->id} closed"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InstallsGatewayWorkerDependencies.php: -------------------------------------------------------------------------------- 1 | error("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n"); 27 | 28 | return false; 29 | } 30 | 31 | if (!extension_loaded('posix')) { 32 | $this->error("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n"); 33 | 34 | return false; 35 | } 36 | 37 | if (!$this->confirm('laravel-octane-workerman requires "workerman/gateway-worker" and "workerman/gatewayclient". Do you wish to install it as a dependency?')) { 38 | $this->error('laravel-octane-workerman requires "workerman/gateway-worker" and "workerman/gatewayclient"'); 39 | 40 | return false; 41 | } 42 | 43 | $command = $this->findComposer() . ' require workerman/gateway-worker:^3.0 workerman/gatewayclient:^3.0 --with-all-dependencies'; 44 | 45 | $process = Process::fromShellCommandline($command, null, null, null, null); 46 | 47 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 48 | try { 49 | $process->setTty(true); 50 | } catch (RuntimeException $e) { 51 | $this->output->writeln('Warning: ' . $e->getMessage()); 52 | } 53 | } 54 | 55 | try { 56 | $process->run(function ($type, $line) { 57 | $this->output->write($line); 58 | }); 59 | } catch (ProcessSignaledException $e) { 60 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 61 | throw $e; 62 | } 63 | } 64 | 65 | return true; 66 | } 67 | 68 | /** 69 | * Get the composer command for the environment. 70 | * 71 | * @return string 72 | */ 73 | protected function findComposer() 74 | { 75 | $composerPath = getcwd() . '/composer.phar'; 76 | 77 | $phpPath = (new PhpExecutableFinder())->find(); 78 | 79 | if (!file_exists($composerPath)) { 80 | $composerPath = (new ExecutableFinder())->find('composer'); 81 | } 82 | 83 | return '"' . $phpPath . '" ' . $composerPath; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jie-anthony/laravel-octane-workerman", 3 | "description": "Start the laravel project through gatewayworker to make the development of laravel in the Internet of Things more convenient. Fetch and communicate with different gateways via API.", 4 | "keywords": ["laravel-octane-workerman", "octane", "laravel", "workerman", "webman"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Anthony", 10 | "email": "407968526@qq.com", 11 | "homepage": "https://github.com/JieAnthony", 12 | "role": "Creator & Developer" 13 | }, 14 | { 15 | "name": "mouyong", 16 | "email": "my24251325@gmail.com", 17 | "homepage": "https://github.com/mouyong", 18 | "role": "Creator & Developer" 19 | } 20 | ], 21 | "homepage": "https://laravel-workerman.iwnweb.com", 22 | "support": { 23 | "email": "my24251325@gmail.com", 24 | "source": "https://github.com/mouyong/laravel-octane-workerman", 25 | "issues": "https://laravel-workerman.iwnweb.com", 26 | "forum": "https://laravel-workerman.iwnweb.com", 27 | "wiki": "https://laravel-workerman.iwnweb.com" 28 | }, 29 | "require": { 30 | "php": "^8.0", 31 | "ext-json": "*", 32 | "ext-pcntl": "*", 33 | "ext-posix": "*", 34 | "laravel/octane": "^1.0", 35 | "workerman/gateway-worker": "^3.0", 36 | "workerman/gatewayclient": "^3.0", 37 | "workerman/workerman": "^4.0", 38 | "workerman/psr7": "^1.0" 39 | }, 40 | "require-dev": { 41 | "friendsofphp/php-cs-fixer": "^2.15" 42 | }, 43 | "bin": [ 44 | "bin/gatewayworker-server" 45 | ], 46 | "autoload": { 47 | "files": [ 48 | "src/helpers.php" 49 | ], 50 | "psr-4": { 51 | "JieAnthony\\LaravelOctaneWorkerman\\": "src/" 52 | } 53 | }, 54 | "extra": { 55 | "laravel": { 56 | "providers": [ 57 | "JieAnthony\\LaravelOctaneWorkerman\\LaravelOctaneWorkermanServiceProvider", 58 | "JieAnthony\\LaravelOctaneWorkerman\\WorkermanGatewayWorkerServiceProvider" 59 | ] 60 | } 61 | }, 62 | "config": { 63 | "sort-packages": true 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true, 67 | "scripts": { 68 | "post-package-install": [ 69 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::install" 70 | ], 71 | "post-package-update": [ 72 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::install" 73 | ], 74 | "pre-package-uninstall": [ 75 | "JieAnthony\\LaravelOctaneWorkerman\\WebmanPlugin::uninstall" 76 | ], 77 | "check-style": "vendor/bin/php-cs-fixer fix --using-cache=no --config=.php_cs --diff --dry-run --ansi", 78 | "fix-style": "vendor/bin/php-cs-fixer fix --using-cache=no --config=.php_cs --ansi" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /bin/gatewayworker-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $config) { 64 | if ($config['enable'] ?? true) { 65 | worker_start($process_name, $config); 66 | } 67 | } 68 | 69 | foreach (webman_config('plugin') ?? [] as $firm => $projects) { 70 | foreach ($projects as $name => $project) { 71 | foreach ($project['process'] ?? [] as $process_name => $config) { 72 | if ($config['enable'] ?? true) { 73 | worker_start("plugin.$firm.$name.$process_name", $config); 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | if (!empty($workermanConfig['eventLoopClass'])) { 81 | if(class_exists($workermanConfig['eventLoopClass'])) { 82 | WorkermanWorker::$eventLoopClass = $workermanConfig['eventLoopClass']; 83 | } 84 | } 85 | 86 | WorkermanWorker::runAll(); 87 | -------------------------------------------------------------------------------- /webman_plugin_require_example/plugin/webman/push/route.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright walkor 12 | * @link http://www.workerman.net/ 13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | 16 | // use support\Request; 17 | // use Webman\Route; 18 | use Webman\Push\Api; 19 | use Illuminate\Http\Request; 20 | use Illuminate\Routing\Router; 21 | 22 | app('router')->group([], function (Router $route) { 23 | 24 | /** 25 | * 推送js客户端文件 26 | */ 27 | $route->any('plugin/webman/push/push.js', function (Request $request) { 28 | return response()->file(base_path() . '/vendor/webman/push/src/push.js'); 29 | }); 30 | 31 | /** 32 | * 私有频道鉴权,这里应该使用session辨别当前用户身份,然后确定该用户是否有权限监听channel_name 33 | */ 34 | $route->any(config('plugin.webman.push.app.auth'), function (Request $request) { 35 | $pusher = new Api(str_replace('0.0.0.0', '127.0.0.1', config('plugin.webman.push.app.api')), config('plugin.webman.push.app.app_key'), config('plugin.webman.push.app.app_secret')); 36 | $channel_name = $request->post('channel_name'); 37 | $session = $request->session(); 38 | // 这里应该通过session和channel_name判断当前用户是否有权限监听channel_name 39 | $has_authority = true; 40 | if ($has_authority) { 41 | return response($pusher->socketAuth($channel_name, $request->post('socket_id'))); 42 | } else { 43 | return response('Forbidden', 403); 44 | } 45 | }); 46 | 47 | /** 48 | * 当频道上线以及下线时触发的回调 49 | * 频道上线:是指某个频道从没有连接在线到有连接在线的事件 50 | * 频道下线:是指某个频道的所有连接都断开触发的事件 51 | */ 52 | $route->any(parse_url(config('plugin.webman.push.app.channel_hook'), PHP_URL_PATH), function (Request $request) { 53 | 54 | // 没有x-pusher-signature头视为伪造请求 55 | if (!$webhook_signature = $request->header('x-pusher-signature')) { 56 | return response('401 Not authenticated', 401); 57 | } 58 | 59 | $body = $request->rawBody(); 60 | 61 | // 计算签名,$app_secret 是双方使用的密钥,是保密的,外部无从得知 62 | $expected_signature = hash_hmac('sha256', $body, config('plugin.webman.push.app.app_secret'), false); 63 | 64 | // 安全校验,如果签名不一致可能是伪造的请求,返回401状态码 65 | if ($webhook_signature !== $expected_signature) { 66 | return response('401 Not authenticated', 401); 67 | } 68 | 69 | // 这里存储这上线 下线的channel数据 70 | $payload = json_decode($body, true); 71 | 72 | $channels_online = $channels_offline = []; 73 | 74 | foreach ($payload['events'] as $event) { 75 | if ($event['name'] === 'channel_added') { 76 | $channels_online[] = $event['channel']; 77 | } else if ($event['name'] === 'channel_removed') { 78 | $channels_offline[] = $event['channel']; 79 | } 80 | } 81 | 82 | // 业务根据需要处理上下线的channel,例如将在线状态写入数据库,通知其它channel等 83 | // 上线的所有channel 84 | echo 'online channels: ' . implode(',', $channels_online) . "\n"; 85 | // 下线的所有channel 86 | echo 'offline channels: ' . implode(',', $channels_offline) . "\n"; 87 | 88 | return 'OK'; 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/Workerman/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | $masterProcessId] = $this->serverStateFile->read(); 25 | 26 | return (bool) $masterProcessId; 27 | } 28 | 29 | public function writeProcessId() 30 | { 31 | $pid = @file_get_contents(config('workerman.http.pidFile')); 32 | 33 | if ($pid) { 34 | $this->serverStateFile->writeProcessId($pid); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public function getServer($mode, array $args = []) 42 | { 43 | $command = [ 44 | (new PhpExecutableFinder())->find(), 45 | 'gatewayworker-server', 46 | $mode, 47 | $this->serverStateFile->path(), 48 | ...$args, 49 | ]; 50 | 51 | $cwd = realpath(__DIR__ . '/../../bin'); 52 | 53 | $env = [ 54 | 'APP_BASE_PATH' => base_path(), 55 | 'LARAVEL_OCTANE' => 1, 56 | ]; 57 | 58 | return $this->processFactory->createProcess( 59 | command: $command, 60 | cwd: $cwd, 61 | env: $env, 62 | input: null, 63 | timeout: null 64 | ); 65 | } 66 | 67 | /** 68 | * Start the Workerman workers. 69 | * 70 | * @return \Symfony\Component\Process\Process 71 | */ 72 | public function startServer() 73 | { 74 | if ($this->serverIsRunning()) { 75 | $this->stopServer(); 76 | 77 | return; 78 | } 79 | 80 | $server = $this->getServer('start'); 81 | 82 | $server->start(); 83 | 84 | $server->waitUntil([$this, 'writeProcessId']); 85 | 86 | $this->writeProcessId($server->getPid()); 87 | 88 | return $server; 89 | } 90 | 91 | /** 92 | * Start the Workerman workers as daemon mode. 93 | * 94 | * @return \Symfony\Component\Process\Process 95 | */ 96 | public function startDaemonServer() 97 | { 98 | if ($this->serverIsRunning()) { 99 | return; 100 | } 101 | 102 | $server = $this->getServer('start', ['-d']); 103 | 104 | $server->start(); 105 | 106 | $server->waitUntil([$this, 'writeProcessId']); 107 | 108 | return $server; 109 | } 110 | 111 | /** 112 | * Reload the Workerman workers. 113 | * 114 | * @return void 115 | */ 116 | public function reloadServer() 117 | { 118 | return $this->getServer('reload')->run(); 119 | } 120 | 121 | /** 122 | * Stop the Workerman server. 123 | * 124 | * @return void 125 | */ 126 | public function stopServer(): void 127 | { 128 | $this->getServer('stop')->run(); 129 | 130 | $this->serverStateFile->delete(); 131 | 132 | Worker::stopAll(); 133 | } 134 | 135 | /** 136 | * Get the Workerman server status. 137 | * 138 | * @return \Symfony\Component\Process\Process 139 | */ 140 | public function getServerStatus(callable $callable, bool $debug = false) 141 | { 142 | if (!$this->serverIsRunning()) { 143 | echo "Workerman not running\n"; 144 | return; 145 | } 146 | 147 | $debug = $debug ? '-d' : null; 148 | 149 | $server = $this->getServer('status', [$debug]); 150 | 151 | $server->start(); 152 | 153 | return $server->waitUntil($callable); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Workerman/Actions/ConvertWorkermanRequestToIlluminateRequest.php: -------------------------------------------------------------------------------- 1 | prepareServerVariables( 21 | $workermanRequest->server ?? [], 22 | $workermanRequest->header ?? [], 23 | $phpSapi 24 | ); 25 | 26 | $request = new SymfonyRequest( 27 | $workermanRequest->get() ?? [], 28 | $workermanRequest->post() ?? [], 29 | [], 30 | $workermanRequest->cookie() ?? [], 31 | $workermanRequest->file() ?? [], 32 | $serverVariables, 33 | $workermanRequest->rawBody(), 34 | ); 35 | 36 | if (str_starts_with((string) $request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') && 37 | in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'PATCH', 'DELETE'])) { 38 | parse_str($request->getContent(), $data); 39 | 40 | $request->request = new ParameterBag($data); 41 | } 42 | 43 | return Request::createFromBase($request); 44 | } 45 | 46 | /** 47 | * Parse the "server" variables and headers into a single array of $_SERVER variables. 48 | * 49 | * @param array $server 50 | * @param array $headers 51 | * @param string $phpSapi 52 | * @return array 53 | */ 54 | protected function prepareServerVariables(array $server, array $headers, string $phpSapi): array 55 | { 56 | $results = []; 57 | 58 | foreach ($server as $key => $value) { 59 | $results[strtoupper($key)] = $value; 60 | } 61 | 62 | $results = array_merge( 63 | $results, 64 | $this->formatHttpHeadersIntoServerVariables($headers) 65 | ); 66 | 67 | if (isset($results['REQUEST_URI'], $results['QUERY_STRING']) && 68 | strlen($results['QUERY_STRING']) > 0 && 69 | strpos($results['REQUEST_URI'], '?') === false) { 70 | $results['REQUEST_URI'] .= '?'.$results['QUERY_STRING']; 71 | } 72 | 73 | return $phpSapi === 'cli-server' 74 | ? $this->correctHeadersSetIncorrectlyByPhpDevServer($results) 75 | : $results; 76 | } 77 | 78 | /** 79 | * Format the given HTTP headers into properly formatted $_SERVER variables. 80 | * 81 | * @param array $headers 82 | * @return array 83 | */ 84 | protected function formatHttpHeadersIntoServerVariables(array $headers): array 85 | { 86 | $results = []; 87 | 88 | foreach ($headers as $key => $value) { 89 | $key = strtoupper(str_replace('-', '_', $key)); 90 | 91 | if (! in_array($key, ['HTTPS', 'REMOTE_ADDR', 'SERVER_PORT'])) { 92 | $key = 'HTTP_'.$key; 93 | } 94 | 95 | $results[$key] = $value; 96 | } 97 | 98 | return $results; 99 | } 100 | 101 | /** 102 | * Correct headers set incorrectly by built-in PHP development server. 103 | * 104 | * @param array $headers 105 | * @return array 106 | */ 107 | protected function correctHeadersSetIncorrectlyByPhpDevServer(array $headers): array 108 | { 109 | if (array_key_exists('HTTP_CONTENT_LENGTH', $headers)) { 110 | $headers['CONTENT_LENGTH'] = $headers['HTTP_CONTENT_LENGTH']; 111 | } 112 | 113 | if (array_key_exists('HTTP_CONTENT_TYPE', $headers)) { 114 | $headers['CONTENT_TYPE'] = $headers['HTTP_CONTENT_TYPE']; 115 | } 116 | 117 | return $headers; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /bin/createGatewayWorker.php: -------------------------------------------------------------------------------- 1 | name = $workermanConfig['register']['name']; 12 | $register->onWorkerStart = function ($worker) { 13 | }; 14 | } 15 | 16 | // gateway 进程, websocket 协议 17 | if ($workermanConfig['gateway-websocket']['enable']) { 18 | $gatewayWebsocket = new Gateway("{$workermanConfig['gateway-websocket']['protocol']}://{$workermanConfig['gateway-websocket']['host']}:{$workermanConfig['gateway-websocket']['port']}"); 19 | 20 | $gatewayWebsocket->name = $workermanConfig['gateway-websocket']['name']; 21 | $gatewayWebsocket->count = $workermanConfig['gateway-websocket']['count']; 22 | $gatewayWebsocket->lanIp = $workermanConfig['gateway-websocket']['lanIp']; 23 | $gatewayWebsocket->startPort = $workermanConfig['gateway-websocket']['startPort']; 24 | $gatewayWebsocket->pingInterval = $workermanConfig['gateway-websocket']['pingInterval']; 25 | $gatewayWebsocket->pingData = $workermanConfig['gateway-websocket']['pingData']; 26 | $gatewayWebsocket->registerAddress = $workermanConfig['gateway-websocket']['registerAddress']; 27 | $gatewayWebsocket->onWorkerStart = $workermanConfig['gateway-websocket']['onWorkerStart'] ?? function ($worker) { 28 | }; 29 | $gatewayWebsocket->onWorkerStop = $workermanConfig['gateway']['onWorkerStop'] ?? function () { 30 | }; 31 | $gatewayWebsocket->onConnect = $workermanConfig['gateway']['onConnect'] ?? function () { 32 | }; 33 | $gatewayWebsocket->onClose = $workermanConfig['gateway']['onClose'] ?? function () { 34 | }; 35 | } 36 | 37 | // gateway 进程, tcp 协议 38 | if ($workermanConfig['gateway-tcp']['enable']) { 39 | $gatewayTcp = new Gateway("{$workermanConfig['gateway-tcp']['protocol']}://{$workermanConfig['gateway-tcp']['host']}:{$workermanConfig['gateway-tcp']['port']}"); 40 | 41 | $gatewayTcp->name = $workermanConfig['gateway-tcp']['name']; 42 | $gatewayTcp->count = $workermanConfig['gateway-tcp']['count']; 43 | $gatewayTcp->lanIp = $workermanConfig['gateway-tcp']['lanIp']; 44 | $gatewayTcp->startPort = $workermanConfig['gateway-tcp']['startPort']; 45 | $gatewayTcp->pingInterval = $workermanConfig['gateway-tcp']['pingInterval']; 46 | $gatewayTcp->pingData = $workermanConfig['gateway-tcp']['pingData']; 47 | $gatewayTcp->registerAddress = $workermanConfig['gateway-tcp']['registerAddress']; 48 | $gatewayTcp->onWorkerStart = $workermanConfig['gateway-tcp']['onWorkerStart'] ?? function ($worker) { 49 | }; 50 | $gatewayTcp->onWorkerStop = $workermanConfig['gateway']['onWorkerStop'] ?? function () { 51 | }; 52 | $gatewayTcp->onConnect = $workermanConfig['gateway']['onConnect'] ?? function () { 53 | }; 54 | $gatewayTcp->onClose = $workermanConfig['gateway']['onClose'] ?? function () { 55 | }; 56 | } 57 | 58 | // bussinessWorker 进程 59 | if ($workermanConfig['business']['enable']) { 60 | $business = new BusinessWorker("{$workermanConfig['business']['protocol']}://{$workermanConfig['business']['host']}:{$workermanConfig['business']['port']}"); 61 | 62 | $business->name = $workermanConfig['business']['name']; 63 | $business->count = $workermanConfig['business']['count']; 64 | $business->registerAddress = $workermanConfig['business']['registerAddress']; 65 | $business->onWorkerStart = $workermanConfig['business']['onWorkerStart'] ?? function (BusinessWorker $worker) { 66 | }; 67 | $business->onWorkerStop = $workermanConfig['business']['onWorkerStop'] ?? function (BusinessWorker $businessWorker) { 68 | }; 69 | 70 | if (empty($workermanConfig['business']['eventHandler']) && !class_exists('Events')) { 71 | if (!class_exists('Events')) { 72 | class Events { 73 | public static function onMessage(string $client_id, string $recv_data) 74 | { 75 | var_dump("client_id $client_id recv_data $recv_data"); 76 | } 77 | }; 78 | } 79 | } 80 | 81 | $business->eventHandler = $workermanConfig['business']['eventHandler'] ?? 'Events'; 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel Octane Workerman 2 | --- 3 | 4 | [![Latest Stable Version](http://poser.pugx.org/jie-anthony/laravel-octane-workerman/v)](https://packagist.org/packages/jie-anthony/laravel-octane-workerman) [![Total Downloads](http://poser.pugx.org/jie-anthony/laravel-octane-workerman/downloads)](https://packagist.org/packages/jie-anthony/laravel-octane-workerman) [![Latest Unstable Version](http://poser.pugx.org/jie-anthony/laravel-octane-workerman/v/unstable)](https://packagist.org/packages/jie-anthony/laravel-octane-workerman) [![License](http://poser.pugx.org/jie-anthony/laravel-octane-workerman/license)](https://packagist.org/packages/jie-anthony/laravel-octane-workerman) [![PHP Version Require](http://poser.pugx.org/jie-anthony/laravel-octane-workerman/require/php)](https://packagist.org/packages/jie-anthony/laravel-octane-workerman) 5 | 6 | 7 | ## Discussions 8 | 9 | 访问地址 https://laravel-workerman.iwnweb.com/ 10 | 11 | 12 | ## Todos 13 | 14 | - [ ] Support Workerman v5 15 | - [x] add make Events command 16 | - [x] add make Socket Command 17 | - [x] add make CustomProcess command 18 | - [x] add global setRegisterAddress of API method 19 | 20 | 21 | ## Screenshot 22 | 23 | Start the laravel project through `gatewayworker` to make the development of laravel in the Internet of Things more convenient. Fetch and communicate with different gateways via API. 24 | 25 |
26 | See the Websocket Gateway and API signal communication Screenshot 27 | 28 | 29 | ![image](https://user-images.githubusercontent.com/10336437/160743947-80837068-5ca6-4ee7-a560-d108878fedbd.png) 30 | 31 | ![image](https://user-images.githubusercontent.com/10336437/160744007-8d0c4af3-487a-41a8-8f9c-bb7bcf4ad118.png) 32 | 33 | ![image](https://user-images.githubusercontent.com/10336437/160744127-979c1531-858e-4869-9ccf-a3b02e582091.png) 34 | 35 | ![image](https://user-images.githubusercontent.com/10336437/160744093-f6c4020a-fbb9-4bf7-a420-0078f354c53c.png) 36 | 37 | ![image](https://user-images.githubusercontent.com/10336437/161367556-01f4cdb5-c51f-4afa-9875-63ca09d83dd7.jpg) 38 |
39 | 40 | ## Installing 41 | 42 | ```shell 43 | $ composer config repositories.laravel-octane-workerman vcs https://github.com/mouyong/laravel-octane-workerman 44 | 45 | # support workerman:gateway and workerman:http command install from https://github.com/mouyong/laravel-octane-workerman 46 | $ composer require jie-anthony/laravel-octane-workerman:dev-master -vvv 47 | 48 | # just support octane:workerman command, install from https://github.com/JieAnthony/laravel-octane-workerman 49 | $ composer require jie-anthony/laravel-octane-workerman -vvv 50 | ``` 51 | 52 | 53 | ## Configuration 54 | 55 | ```shell 56 | php artisan vendor:publish --provider="Laravel\Octane\OctaneServiceProvider" 57 | php artisan vendor:publish --provider="JieAnthony\LaravelOctaneWorkerman\WorkermanGatewayWorkerServiceProvider" 58 | ``` 59 | 60 | configuration edit in `config/workerman.php` 61 | 62 | ## Command parameter 63 | 64 | | option | default | 65 | |--------------------------|---------| 66 | | host | 0.0.0.0 | 67 | | port | 8000 | 68 | | max-requests | 10000 | 69 | | mode | start | 70 | | watch | | 71 | 72 | mode options : ( start / daemon / stop ) 73 | 74 | 75 | ## Useage 76 | 77 | ```shell 78 | php artisan worker start 79 | php artisan worker daemon 80 | php artisan worker reload 81 | php artisan worker stop 82 | php artisan worker status -d 83 | 84 | php artisan make:sockets Sockets 85 | php artisan make:events Events 86 | php artisan make:process CustomProcess 87 | ``` 88 | 89 | ### webman plugin useage 90 | 91 | * [webman plugin](webman_plugin_require_example/README.md) 92 | 93 | 94 | ## Documentation 95 | 96 | * [Workerman](https://www.workerman.net/doc/workerman/) 97 | 98 | 99 | ## websockets 100 | 101 | The tcp `ddos-proxy-http` address 102 | 103 | `ws://127.0.0.1:7000/ws` 104 | 105 | ``` 106 | location /ws { 107 | # the websocket address with http protocol 108 | proxy_pass http://127.0.0.1:7200; 109 | proxy_set_header Upgrade $http_upgrade; 110 | proxy_set_header Connection 'Upgrade'; 111 | } 112 | ``` 113 | test connection to websocket 114 | 115 | ``` 116 | var ws = new WebSocket('ws://127.0.0.1:7000/ws') 117 | ws.onmessage = function (data) { 118 | console.log("server response ws data: " + data) 119 | } 120 | 121 | ws.send('send message test from client') 122 | ``` 123 | 124 | 125 | ### Thanks 126 | 127 | * [Workerman](https://github.com/walkor/Workerman) 128 | * [Laravel](https://github.com/laravel/laravel) 129 | * [Octane](https://github.com/laravel/octane) 130 | * [laravel-octane-workerman](https://github.com/JieAnthony/laravel-octane-workerman) 131 | 132 | ## Contact 133 | 134 | Join QQ Group laravel-octane-gatewayworker 650057913 135 | 136 | laravel-octane-gatewayworker 群聊二维码 137 | 138 | 139 | ## License 140 | 141 | MIT 142 | -------------------------------------------------------------------------------- /src/Process/Monitor.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright walkor 12 | * @link http://www.workerman.net/ 13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | 16 | namespace JieAnthony\LaravelOctaneWorkerman\Process; 17 | 18 | use Workerman\Timer; 19 | use Workerman\Worker; 20 | 21 | /** 22 | * Class Monitor 23 | * @package Process 24 | */ 25 | class Monitor 26 | { 27 | /** 28 | * @var array 29 | */ 30 | protected $_paths = []; 31 | 32 | /** 33 | * @var array 34 | */ 35 | protected $_extensions = []; 36 | 37 | /** 38 | * FileMonitor constructor. 39 | * @param $monitor_dir 40 | * @param $monitor_extensions 41 | * @param $memory_limit 42 | */ 43 | public function __construct($monitor_dir, $monitor_extensions, $memory_limit = null) 44 | { 45 | $this->_paths = (array)$monitor_dir; 46 | $this->_extensions = $monitor_extensions; 47 | if (!Worker::getAllWorkers()) { 48 | return; 49 | } 50 | $disable_functions = explode(',', ini_get('disable_functions')); 51 | if (in_array('exec', $disable_functions, true)) { 52 | echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n"; 53 | } else { 54 | if (!Worker::$daemonize) { 55 | Timer::add(1, function () { 56 | $this->checkAllFilesChange(); 57 | }); 58 | } 59 | } 60 | 61 | $memory_limit = $this->getMemoryLimit($memory_limit); 62 | if ($memory_limit && DIRECTORY_SEPARATOR === '/') { 63 | Timer::add(60, [$this, 'checkMemory'], [$memory_limit]); 64 | } 65 | } 66 | 67 | /** 68 | * @param $monitor_dir 69 | */ 70 | public function checkFilesChange($monitor_dir) 71 | { 72 | static $last_mtime; 73 | if (!$last_mtime) { 74 | $last_mtime = time(); 75 | } 76 | clearstatcache(); 77 | if (!is_dir($monitor_dir)) { 78 | if (!is_file($monitor_dir)) { 79 | return; 80 | } 81 | $iterator = [new \SplFileInfo($monitor_dir)]; 82 | } else { 83 | // recursive traversal directory 84 | $dir_iterator = new \RecursiveDirectoryIterator($monitor_dir, \FilesystemIterator::FOLLOW_SYMLINKS); 85 | $iterator = new \RecursiveIteratorIterator($dir_iterator); 86 | } 87 | foreach ($iterator as $file) { 88 | /** var SplFileInfo $file */ 89 | if (is_dir($file)) { 90 | continue; 91 | } 92 | // check mtime 93 | if ($last_mtime < $file->getMTime() && in_array($file->getExtension(), $this->_extensions, true)) { 94 | $var = 0; 95 | exec(PHP_BINARY . " -l " . $file, $out, $var); 96 | if ($var) { 97 | $last_mtime = $file->getMTime(); 98 | continue; 99 | } 100 | $last_mtime = $file->getMTime(); 101 | // echo ltrim(str_replace(base_path(), '', $file), '/') . " update and reload\n"; 102 | // send SIGUSR1 signal to master process for reload 103 | if (DIRECTORY_SEPARATOR === '/') { 104 | posix_kill(posix_getppid(), SIGUSR1); 105 | } else { 106 | return true; 107 | } 108 | break; 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * @return bool 115 | */ 116 | public function checkAllFilesChange() 117 | { 118 | foreach ($this->_paths as $path) { 119 | if ($this->checkFilesChange($path)) { 120 | return true; 121 | } 122 | } 123 | return false; 124 | } 125 | 126 | /** 127 | * @param $memory_limit 128 | * @return void 129 | */ 130 | public function checkMemory($memory_limit) 131 | { 132 | $ppid = posix_getppid(); 133 | $children_file = "/proc/$ppid/task/$ppid/children"; 134 | if (!is_file($children_file) || !($children = file_get_contents($children_file))) { 135 | return; 136 | } 137 | foreach (explode(' ', $children) as $pid) { 138 | $pid = (int)$pid; 139 | $status_file = "/proc/$pid/status"; 140 | if (!is_file($status_file) || !($status = file_get_contents($status_file))) { 141 | continue; 142 | } 143 | $mem = 0; 144 | if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) { 145 | $mem = $match[1]; 146 | } 147 | $mem = (int)($mem / 1024); 148 | if ($mem >= $memory_limit) { 149 | posix_kill($pid, SIGINT); 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Get memory limit 156 | * @return float 157 | */ 158 | protected function getMemoryLimit($memory_limit) 159 | { 160 | if ($memory_limit === 0) { 161 | return 0; 162 | } 163 | $use_php_ini = false; 164 | if (!$memory_limit) { 165 | $memory_limit = ini_get('memory_limit'); 166 | $use_php_ini = true; 167 | } 168 | 169 | if ($memory_limit == -1) { 170 | return 0; 171 | } 172 | $unit = $memory_limit[strlen($memory_limit) - 1]; 173 | if ($unit == 'G') { 174 | $memory_limit = 1024 * (int)$memory_limit; 175 | } elseif ($unit == 'M') { 176 | $memory_limit = (int)$memory_limit; 177 | } elseif ($unit == 'K') { 178 | $memory_limit = (int)($memory_limit / 1024); 179 | } else { 180 | $memory_limit = (int)($memory_limit / (1024 * 1024)); 181 | } 182 | if ($memory_limit < 30) { 183 | $memory_limit = 30; 184 | } 185 | if ($use_php_ini) { 186 | $memory_limit = (int)(0.8 * $memory_limit); 187 | } 188 | return $memory_limit; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Workerman/WorkermanClient.php: -------------------------------------------------------------------------------- 1 | request instanceof ServerRequestInterface) { 35 | $httpFoundationRequest = $this->toHttpFoundationRequest($context->request); 36 | } else { 37 | $httpFoundationRequest = (new ConvertWorkermanRequestToIlluminateRequest)( 38 | $context->request, 39 | PHP_SAPI 40 | ); 41 | } 42 | 43 | return [ 44 | $httpFoundationRequest, 45 | $context, 46 | ]; 47 | } 48 | 49 | /** 50 | * Determine if the request can be served as a static file. 51 | * 52 | * @param \Illuminate\Http\Request $request 53 | * @param \Laravel\Octane\RequestContext $context 54 | * @return bool 55 | */ 56 | public function canServeRequestAsStaticFile(Request $request, RequestContext $context): bool 57 | { 58 | if (! ($context->publicPath ?? false) || 59 | $request->path() === '/') { 60 | return false; 61 | } 62 | 63 | $publicPath = $context->publicPath; 64 | 65 | $pathToFile = realpath($publicPath.'/'.$request->path()); 66 | 67 | if ($this->isValidFileWithinSymlink($request, $publicPath, $pathToFile)) { 68 | $pathToFile = $publicPath.'/'.$request->path(); 69 | } 70 | 71 | return $this->fileIsServable( 72 | $publicPath, 73 | $pathToFile, 74 | ); 75 | } 76 | 77 | /** 78 | * Determine if the request is for a valid static file within a symlink. 79 | * 80 | * @param \Illuminate\Http\Request $request 81 | * @param string $publicPath 82 | * @param string $pathToFile 83 | * @return bool 84 | */ 85 | private function isValidFileWithinSymlink(Request $request, string $publicPath, string $pathToFile): bool 86 | { 87 | $pathAfterSymlink = $this->pathAfterSymlink($publicPath, $request->path()); 88 | 89 | return $pathAfterSymlink && str_ends_with($pathToFile, $pathAfterSymlink); 90 | } 91 | 92 | /** 93 | * If the given public file is within a symlinked directory, return the path after the symlink. 94 | * 95 | * @param string $publicPath 96 | * @param string $path 97 | * @return string|bool 98 | */ 99 | private function pathAfterSymlink(string $publicPath, string $path) 100 | { 101 | $directories = explode('/', $path); 102 | 103 | while ($directory = array_shift($directories)) { 104 | $publicPath .= '/'.$directory; 105 | 106 | if (is_link($publicPath)) { 107 | return implode('/', $directories); 108 | } 109 | } 110 | 111 | return false; 112 | } 113 | 114 | /** 115 | * Determine if the given file is servable. 116 | * 117 | * @param string $publicPath 118 | * @param string $pathToFile 119 | * @return bool 120 | */ 121 | protected function fileIsServable(string $publicPath, string $pathToFile): bool 122 | { 123 | return $pathToFile && 124 | ! in_array(pathinfo($pathToFile, PATHINFO_EXTENSION), ['php', 'htaccess', 'config']) && 125 | str_starts_with($pathToFile, $publicPath) && 126 | is_file($pathToFile); 127 | } 128 | 129 | /** 130 | * Serve the static file that was requested. 131 | * 132 | * @param \Illuminate\Http\Request $request 133 | * @param \Laravel\Octane\RequestContext $context 134 | * @return void 135 | */ 136 | public function serveStaticFile(Request $request, RequestContext $context): void 137 | { 138 | $publicPath = $context->publicPath; 139 | 140 | $pathToFile = realpath($publicPath.'/'.$request->path()); 141 | 142 | $response = new WorkermanHttpResponse(); 143 | $response->withStatus(200); 144 | $response->withHeader('Content-Type', MimeType::get(pathinfo($request->path(), PATHINFO_EXTENSION))); 145 | $response->withFile($pathToFile); 146 | 147 | $context->connection->send($response); 148 | } 149 | 150 | /** 151 | * Send the response to the server. 152 | * 153 | * @param \Laravel\Octane\RequestContext $context 154 | * @param OctaneResponse $response 155 | * @return void 156 | */ 157 | public function respond(RequestContext $context, OctaneResponse $octaneResponse): void 158 | { 159 | $response = $this->toPsr7Response($octaneResponse->response); 160 | 161 | $context->connection->send(new WorkermanResponse( 162 | $response->getStatusCode(), 163 | $response->getHeaders(), 164 | $response->getBody(), 165 | $response->getProtocolVersion(), 166 | $response->getReasonPhrase() 167 | )); 168 | } 169 | 170 | /** 171 | * Send an error message to the server. 172 | * 173 | * @param \Throwable $e 174 | * @param \Illuminate\Foundation\Application $app 175 | * @param \Illuminate\Http\Request $request 176 | * @param \Laravel\Octane\RequestContext $context 177 | * @return void 178 | */ 179 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 180 | { 181 | $context->connection->send(Octane::formatExceptionForClient( 182 | $e, 183 | $app->make('config')->get('app.debug') 184 | )); 185 | } 186 | 187 | /** 188 | * Stop the underlying server / worker. 189 | * 190 | * @return void 191 | */ 192 | public function stop(): void 193 | { 194 | Worker::stopAll(); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/WebmanConfig.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright walkor 12 | * @link http://www.workerman.net/ 13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | 16 | namespace JieAnthony\LaravelOctaneWorkerman; 17 | 18 | class WebmanConfig 19 | { 20 | /** 21 | * @var array 22 | */ 23 | protected static $_config = []; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected static $_configPath = ''; 29 | 30 | /** 31 | * @var bool 32 | */ 33 | protected static $_loaded = false; 34 | 35 | /** 36 | * @param $config_path 37 | * @param array $exclude_file 38 | */ 39 | public static function load($config_path, $exclude_file = []) 40 | { 41 | static::$_configPath = $config_path; 42 | if (!$config_path) { 43 | return; 44 | } 45 | $dir_iterator = new \RecursiveDirectoryIterator($config_path, \FilesystemIterator::FOLLOW_SYMLINKS); 46 | $iterator = new \RecursiveIteratorIterator($dir_iterator); 47 | foreach ($iterator as $file) { 48 | /** var SplFileInfo $file */ 49 | if (is_dir($file) || $file->getExtension() != 'php' || \in_array($file->getBaseName('.php'), $exclude_file)) { 50 | continue; 51 | } 52 | $app_config_file = $file->getPath() . '/app.php'; 53 | if (!is_file($app_config_file)) { 54 | continue; 55 | } 56 | $relative_path = str_replace($config_path . DIRECTORY_SEPARATOR, '', substr($file, 0, -4)); 57 | $explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relative_path)); 58 | if (count($explode) >= 2) { 59 | $app_config = include $app_config_file; 60 | if (empty($app_config['enable'])) { 61 | continue; 62 | } 63 | } 64 | $config = include $file; 65 | foreach ($explode as $section) { 66 | $tmp = []; 67 | $tmp[$section] = $config; 68 | $config = $tmp; 69 | } 70 | static::$_config = array_replace_recursive(static::$_config, $config); 71 | } 72 | 73 | // Merge database config 74 | foreach (static::$_config['plugin'] ?? [] as $firm => $projects) { 75 | foreach ($projects as $name => $project) { 76 | foreach ($project['database']['connections'] ?? [] as $key => $connection) { 77 | static::$_config['database']['connections']["plugin.$firm.$name.$key"] = $connection; 78 | } 79 | } 80 | } 81 | if (!empty(static::$_config['database']['connections'])) { 82 | static::$_config['database']['default'] = static::$_config['database']['default'] ?? key(static::$_config['database']['connections']); 83 | } 84 | // Merge thinkorm config 85 | foreach (static::$_config['plugin'] ?? [] as $firm => $projects) { 86 | foreach ($projects as $name => $project) { 87 | foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) { 88 | static::$_config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection; 89 | } 90 | } 91 | } 92 | if (!empty(static::$_config['thinkorm']['connections'])) { 93 | static::$_config['thinkorm']['default'] = static::$_config['thinkorm']['default'] ?? key(static::$_config['thinkorm']['connections']); 94 | } 95 | // Merge redis config 96 | foreach (static::$_config['plugin'] ?? [] as $firm => $projects) { 97 | foreach ($projects as $name => $project) { 98 | foreach ($project['redis'] ?? [] as $key => $connection) { 99 | static::$_config['redis']["plugin.$firm.$name.$key"] = $connection; 100 | } 101 | } 102 | } 103 | 104 | static::$_loaded = true; 105 | } 106 | 107 | /** 108 | * @param null $key 109 | * @param null $default 110 | * @return array|mixed|null 111 | */ 112 | public static function get($key = null, $default = null) 113 | { 114 | if ($key === null) { 115 | return static::$_config; 116 | } 117 | $key_array = \explode('.', $key); 118 | $value = static::$_config; 119 | $finded = true; 120 | foreach ($key_array as $index) { 121 | if (!isset($value[$index])) { 122 | if (static::$_loaded) { 123 | return $default; 124 | } 125 | $finded = false; 126 | break; 127 | } 128 | $value = $value[$index]; 129 | } 130 | if ($finded) { 131 | return $value; 132 | } 133 | return static::read($key, $default); 134 | } 135 | 136 | /** 137 | * @param $key 138 | * @param $default 139 | * @return array|mixed|void|null 140 | */ 141 | protected static function read($key, $default = null) 142 | { 143 | $path = static::$_configPath; 144 | if ($path === '') { 145 | return $default; 146 | } 147 | $keys = $key_array = \explode('.', $key); 148 | foreach ($key_array as $index => $section) { 149 | unset($keys[$index]); 150 | if (is_file($file = "$path/$section.php")) { 151 | $config = include $file; 152 | return static::find($keys, $config, $default); 153 | } 154 | if (!is_dir($path = "$path/$section")) { 155 | return $default; 156 | } 157 | } 158 | return $default; 159 | } 160 | 161 | /** 162 | * @param $key_array 163 | * @param $stack 164 | * @param $default 165 | * @return array|mixed 166 | */ 167 | protected static function find($key_array, $stack, $default) 168 | { 169 | if (!is_array($stack)) { 170 | return $default; 171 | } 172 | $value = $stack; 173 | foreach ($key_array as $index) { 174 | if (!isset($value[$index])) { 175 | return $default; 176 | } 177 | $value = $value[$index]; 178 | } 179 | return $value; 180 | } 181 | 182 | 183 | /** 184 | * @param $config_path 185 | * @param array $exclude_file 186 | */ 187 | public static function reload($config_path, $exclude_file = []) 188 | { 189 | static::$_config = []; 190 | static::load($config_path, $exclude_file); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Commands/StartWorkermanGatewayCommand.php: -------------------------------------------------------------------------------- 1 | ensureWorkermanPackageIsInstalled()) { 39 | return 1; 40 | } 41 | 42 | if (!$this->option('host')) { 43 | $host = parse_url(config('octane.workerman.process.http.listen'), PHP_URL_HOST); 44 | 45 | $this->input->setOption('host', $host); 46 | } 47 | 48 | if (!$this->option('port')) { 49 | $port = parse_url(config('octane.workerman.process.http.listen'), PHP_URL_PORT); 50 | 51 | $this->input->setOption('port', $port); 52 | } 53 | 54 | if (in_array($this->argument('mode'), ['start', 'daemon'])) { 55 | $this->writeServerStateFile($serverStateFile, $this->isDaemon()); 56 | } 57 | 58 | return match ($this->argument('mode')) { 59 | default => $this->error('Error workerman server mode'), 60 | 'start', 'daemon' => $this->serverStart($inspector), 61 | 'stop' => $this->serverStop($inspector), 62 | 'reload' => $this->serverReload($inspector), 63 | 'status' => $this->serverStatus($inspector), 64 | }; 65 | } 66 | 67 | public function isDaemon() 68 | { 69 | return $this->argument('mode') === 'daemon'; 70 | } 71 | 72 | protected function serverStart(ServerProcessInspector $inspector) 73 | { 74 | if ($inspector->serverIsRunning()) { 75 | $this->error('Workerman server is already running.'); 76 | 77 | return Command::FAILURE; 78 | } 79 | 80 | if (!$this->isDaemon()) { 81 | return $this->runServer( 82 | $inspector->startServer(), 83 | $inspector, 84 | 'workerman' 85 | ); 86 | } 87 | 88 | $inspector->startDaemonServer(); 89 | 90 | $this->info('The workerman daemon started successfully'); 91 | return Command::SUCCESS; 92 | } 93 | 94 | protected function serverStatus(ServerProcessInspector $inspector) 95 | { 96 | $inspector->getServerStatus(function ($type, $data) { 97 | // master process already exit. 98 | if (trim($data) === 'Press Ctrl+C to quit.') { 99 | $this->info('The workerman server stopped'); 100 | return true; 101 | } 102 | 103 | $this->output->write($data); 104 | }, $this->option('debug')); 105 | 106 | return Command::SUCCESS; 107 | } 108 | 109 | protected function serverReload(ServerProcessInspector $inspector) 110 | { 111 | $inspector->reloadServer(); 112 | 113 | $this->info('The workerman server reload successfully'); 114 | 115 | return Command::SUCCESS; 116 | } 117 | 118 | protected function serverStop(ServerProcessInspector $inspector) 119 | { 120 | $this->info('The workerman server stopped successfully'); 121 | 122 | $inspector->stopServer(); 123 | 124 | return Command::SUCCESS; 125 | } 126 | 127 | public function stopServer() 128 | { 129 | $this->serverStop(app(ServerProcessInspector::class)); 130 | } 131 | 132 | protected function writeServerStateFile(ServerStateFile $serverStateFile, bool $daemon = false) 133 | { 134 | $serverStateFile->writeState([ 135 | 'host' => $this->option('host'), 136 | 'port' => $this->option('port'), 137 | 'daemon' => $daemon, 138 | 'maxRequests' => $this->option('max-requests'), 139 | 'octaneConfig' => config('octane'), 140 | 'publicPath' => public_path(), 141 | 'storagePath' => storage_path(), 142 | 'timezone' => config('app.timezone') 143 | ]); 144 | } 145 | 146 | protected function writeServerOutput($server) 147 | { 148 | [$output, $errorOutput] = $this->getServerOutput($server); 149 | 150 | Str::of($output) 151 | ->explode("\n") 152 | ->filter() 153 | ->each( 154 | fn ($o) => is_array($stream = json_decode($o, true)) 155 | ? $this->handleStream($stream) 156 | : $this->raw($o) 157 | ); 158 | 159 | Str::of($errorOutput) 160 | ->explode("\n") 161 | ->filter() 162 | ->each( 163 | fn ($e) => is_array($stream = json_decode($e, true)) 164 | ? $this->handleStream($stream) 165 | : $this->error($e) 166 | ); 167 | } 168 | 169 | /** 170 | * Returns the list of signals to subscribe. 171 | * 172 | * @return array 173 | */ 174 | public function getSubscribedSignals(): array 175 | { 176 | return [SIGINT, SIGTERM, SIGHUP]; 177 | } 178 | 179 | /** 180 | * The method will be called when the application is signaled. 181 | * 182 | * @param int $signal 183 | * @return void 184 | */ 185 | public function handleSignal(int $signal): void 186 | { 187 | /** @var ServerProcessInspector $inspector */ 188 | $inspector = app(ServerProcessInspector::class); 189 | 190 | if ($this->argument('mode') === 'status' && $this->option('debug') && $signal === SIGINT) { 191 | exit("\n"); 192 | } 193 | 194 | if ($signal === SIGHUP) { 195 | $this->serverReload($inspector); 196 | return; 197 | } 198 | 199 | $this->serverStop($inspector); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /config/workerman.php: -------------------------------------------------------------------------------- 1 | '512M', 5 | 6 | // all suport eventLoopClass @see: https://github.com/walkor/workerman/tree/4.1/Events 7 | // swoole: \Workerman\Events\Swoole:class 8 | 'eventLoopClass' => null, 9 | 10 | 'http' => [ 11 | 'enable' => false, 12 | 'pidFile' => storage_path('logs/laravel-workerman.pid'), 13 | 'stdoutFile' => storage_path('logs/stdout.log'), 14 | 'logFile' => storage_path('logs/workerman.log'), 15 | 'publicPath' => public_path(), 16 | 'maxPackageSize' => 10 * 1024 * 1024, 17 | ], 18 | 19 | 'register' => [ 20 | 'enable' => false, 21 | 'name' => env('APP_NAME', 'laravel-workerman') . ' RegisterWorker', 22 | 'host' => '0.0.0.0', 23 | 'port' => 7100, 24 | ], 25 | 'gateway-websocket' => [ 26 | 'enable' => false, 27 | 'protocol' => 'websocket', // @see https://www.workerman.net/doc/gateway-worker/gateway.html 28 | 'host' => '0.0.0.0', 29 | 'port' => 7200, 30 | 'name' => env('APP_NAME', 'laravel-workerman') . ' WebsocketGatewayWorker', 31 | 'count' => cpu_count(), 32 | 'lanIp' => '127.0.0.1', 33 | 'startPort' => 7201, 34 | 'pingInterval' => 10, 35 | 'pingData' => '{"type":"ping"}', 36 | 'registerAddress' => '127.0.0.1:7100', 37 | 'onWorkerStart' => null, 38 | 'onWorkerStop' => null, 39 | 'onConnect' => null, 40 | 'onClose' => null, 41 | // 'onWorkerStart' => [App\Sockets::class, 'onWorkerStart'], 42 | // 'onWorkerStop' => [App\Sockets::class, 'onWorkerStop'], 43 | // 'onConnect' => [App\Sockets::class, 'onConnect'], 44 | // 'onClose' => [App\Sockets::class, 'onClose'], 45 | ], 46 | 'gateway-tcp' => [ 47 | 'enable' => false, 48 | 'protocol' => 'tcp', // @see https://www.workerman.net/doc/gateway-worker/gateway.html 49 | 'host' => '0.0.0.0', 50 | 'port' => 7300, 51 | 'name' => env('APP_NAME', 'laravel-workerman') . ' TcpGatewayWorker', 52 | 'count' => cpu_count(), 53 | 'lanIp' => '127.0.0.1', 54 | 'startPort' => 7301, 55 | 'pingInterval' => 10, 56 | 'pingData' => '{"type":"ping"}', 57 | 'registerAddress' => '127.0.0.1:7100', 58 | 'onWorkerStart' => null, 59 | 'onWorkerStop' => null, 60 | 'onConnect' => null, 61 | 'onClose' => null, 62 | // 'onWorkerStart' => [App\Sockets::class, 'onWorkerStart'], 63 | // 'onWorkerStop' => [App\Sockets::class, 'onWorkerStop'], 64 | // 'onConnect' => [App\Sockets::class, 'onConnect'], 65 | // 'onClose' => [App\Sockets::class, 'onClose'], 66 | ], 67 | 'business' => [ 68 | 'enable' => false, 69 | 'protocol' => 'http', // @see https://www.workerman.net/doc/gateway-worker/business-worker.html 70 | 'host' => '0.0.0.0', 71 | 'port' => 7400, 72 | 'name' => env('APP_NAME', 'laravel-workerman') . ' BusinessWorker', 73 | 'count' => cpu_count() * 2, 74 | 'registerAddress' => '127.0.0.1:7100', 75 | 'onWorkerStart' => null, 76 | 'onWorkerStop' => null, 77 | 'eventHandler' => null, 78 | // 'onWorkerStart' => [App\Events::class, 'onWorkerStart'], 79 | // 'onWorkerStop' => [App\Events::class, 'onWorkerStop'], 80 | // @see https://www.workerman.net/doc/gateway-worker/business-worker.html 81 | // @see https://www.workerman.net/doc/gateway-worker/on-messsge.html 82 | // 'eventHandler' => App\Events::class, 83 | ], 84 | 'process' => [ 85 | 'database-heartbeat' => [ 86 | 'enable' => false, 87 | 'handler' => JieAnthony\LaravelOctaneWorkerman\Process\DatabaseHeartbeat::class, 88 | 'reloadable' => false, 89 | ], 90 | 'ddos-proxy-http' => [ 91 | 'enable' => false, 92 | 'handler' => JieAnthony\LaravelOctaneWorkerman\Process\DdosProxyHttp::class, 93 | 'listen' => 'tcp://0.0.0.0:7000', 94 | 'context' => null, 95 | ], 96 | 97 | 'http' => [ 98 | 'enable' => true, 99 | 'listen' => 'http://0.0.0.0:7050', 100 | 'count' => cpu_count() * 3, 101 | // 'user' => null, 102 | // 'group' => null, 103 | 'reloadable' => true, 104 | 'reusePort' => true, 105 | // 'transport' => 'tcp', // Transport layer protocol. default tcp 106 | // 'protocol' => null, // Application layer protocol. 107 | 108 | // process business by handler, worker_bind life cycle: https://github.com/mouyong/laravel-octane-workerman/blob/master/src/helpers.php#L243-L252 109 | 'handler' => JieAnthony\LaravelOctaneWorkerman\Process\HttpWorkerProcess::class, 110 | ], 111 | 112 | 'slow_http' => [ 113 | 'enable' => true, 114 | 'listen' => 'http://0.0.0.0:7051', 115 | 'count' => cpu_count() * 3, 116 | // 'user' => null, 117 | // 'group' => null, 118 | 'reloadable' => true, 119 | 'reusePort' => true, 120 | // 'transport' => 'tcp', // Transport layer protocol. default tcp 121 | // 'protocol' => null, // Application layer protocol. 122 | 123 | // process business by handler, worker_bind life cycle: https://github.com/mouyong/laravel-octane-workerman/blob/master/src/helpers.php#L243-L252 124 | 'handler' => JieAnthony\LaravelOctaneWorkerman\Process\HttpWorkerProcess::class, 125 | ], 126 | 127 | /** 128 | * @see https://www.workerman.net/doc/workerman/appendices/about-websocket.html 查看 websocket 文档 129 | * 130 | * @see http://www.websocket-test.com/ 测试连接可用性 131 | * 132 | * ws://x.x.x.x:3000 地址是 listen 配置的地址。可通过 nginx 反向代理隐藏端口 133 | * 134 | * 接收消息使用 Events 类进行接收。生成 Events 类: php artisan make:process Events 135 | */ 136 | 'websocket' => [ 137 | 'enable' => false, 138 | 'listen' => 'websocket://0.0.0.0:7060', 139 | 'count' => cpu_count(), 140 | // 'user' => null, 141 | // 'group' => null, 142 | 'reloadable' => true, 143 | 'reusePort' => true, 144 | // 'transport' => 'tcp', // Transport layer protocol. default tcp 145 | // 'protocol' => null, // Application layer protocol. 146 | 147 | // process business by handler, worker_bind life cycle: https://github.com/mouyong/laravel-octane-workerman/blob/master/src/helpers.php#L243-L252 148 | // 'handler' => App\Events::class, 149 | ], 150 | 151 | 'tcp' => [ 152 | 'enable' => false, 153 | 'listen' => 'tcp://0.0.0.0:7070', 154 | 'count' => cpu_count(), 155 | // 'user' => null, 156 | // 'group' => null, 157 | 'reloadable' => true, 158 | 'reusePort' => true, 159 | // 'transport' => 'tcp', // Transport layer protocol. default tcp 160 | // 'protocol' => null, // Application layer protocol. 161 | 162 | // process business by handler, worker_bind life cycle: https://github.com/mouyong/laravel-octane-workerman/blob/master/src/helpers.php#L243-L252 163 | // 'handler' => App\Events::class, 164 | ], 165 | 166 | 'monitor' => [ 167 | 'enable' => env('APP_ENV') === 'local' && env('APP_DEBUG') === true, 168 | 'handler' => JieAnthony\LaravelOctaneWorkerman\Process\Monitor::class, 169 | 'reloadable' => false, 170 | 'constructor' => [ 171 | // Monitor these directories 172 | 'monitor_dir' => [ 173 | base_path() . '/app', 174 | base_path() . '/bootstrap', 175 | base_path() . '/config', 176 | base_path() . '/database', 177 | base_path() . '/public/**/*.php', 178 | base_path() . '/resources/**/*.php', 179 | base_path() . '/routes', 180 | base_path() . '/composer.lock', 181 | base_path() . '/.env', 182 | base_path() . '/lang', 183 | base_path() . '/process', 184 | ], 185 | // Files with these suffixes will be monitored 186 | 'monitor_extensions' => [ 187 | 'php', 'html', 'htm', 'env' 188 | ] 189 | ] 190 | ], 191 | ], 192 | ]; 193 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 0 ? (int)$count : 4; 147 | 148 | return $count; 149 | } 150 | } 151 | 152 | if (!function_exists('create_laravel_application_for_worker')) { 153 | /** 154 | * 在 worker 内部引入 laravel 与 webman_config 155 | * 156 | * @param \Workerman\Worker|\Laravel\Octane\Worker|null $worker 157 | * @return void 158 | */ 159 | function create_laravel_application_for_worker($worker) 160 | { 161 | defined('LARAVEL_WORKERMAN_START') or define('LARAVEL_WORKERMAN_START', microtime()); 162 | 163 | require_once $_SERVER['APP_BASE_PATH'] . '/vendor/laravel/octane/bin/bootstrap.php'; 164 | 165 | $worker->app = (new \Laravel\Octane\ApplicationFactory($_SERVER['APP_BASE_PATH']))->createApplication(); 166 | 167 | webman_bootstrap($worker); 168 | } 169 | } 170 | 171 | if (!function_exists('request_bind_connection')) { 172 | /** 173 | * @return void 174 | */ 175 | function request_bind_connection() 176 | { 177 | \JieAnthony\LaravelOctaneWorkerman\WebmanRequest::bindInstance(...func_get_args()); 178 | } 179 | } 180 | 181 | if (!function_exists('response_bind_connection')) { 182 | /** 183 | * @return void 184 | */ 185 | function response_bind_connection() 186 | { 187 | \JieAnthony\LaravelOctaneWorkerman\WebmanResponse::bindInstance(...func_get_args()); 188 | } 189 | } 190 | 191 | if (!function_exists('webman_bootstrap')) { 192 | /** 193 | * @param \Workerman\Worker|\Laravel\Octane\Worker|null $worker 194 | * @return void 195 | */ 196 | function webman_bootstrap($worker = null) 197 | { 198 | \JieAnthony\LaravelOctaneWorkerman\WebmanConfig::load(config_path(), ['container']); 199 | 200 | require_once __DIR__ . '/webman_bootstrap.php'; 201 | 202 | webman_route_load(); 203 | } 204 | } 205 | 206 | if (!function_exists('webman_route_load')) { 207 | /** 208 | * @return void 209 | */ 210 | function webman_route_load(?string $pluginName = null, $autoload = true) 211 | { 212 | defined('LARAVEL_ROUTE_START') or define('LARAVEL_ROUTE_START', microtime()); 213 | 214 | foreach (webman_config('plugin', []) as $firm => $projects) { 215 | foreach ($projects as $name => $project) { 216 | if ($pluginName && $name !== $pluginName) { 217 | continue; 218 | } 219 | 220 | $file = config_path() . "/plugin/$firm/$name/route.php"; 221 | 222 | if (!file_exists($file)) { 223 | continue; 224 | } 225 | 226 | if (!$autoload) { 227 | return $file; 228 | } 229 | 230 | require_once $file; 231 | } 232 | } 233 | } 234 | } 235 | 236 | if (!function_exists('worker_bind')) { 237 | /** 238 | * @param $worker 239 | * @param $class 240 | */ 241 | function worker_bind($worker, $class) 242 | { 243 | $callback_map = [ 244 | 'onConnect', 245 | 'onMessage', 246 | 'onClose', 247 | 'onError', 248 | 'onBufferFull', 249 | 'onBufferDrain', 250 | 'onWorkerStop', 251 | 'onWebSocketConnect' 252 | ]; 253 | 254 | foreach ($callback_map as $name) { 255 | if (method_exists($class, $name)) { 256 | $worker->$name = [$class, $name]; 257 | } 258 | } 259 | 260 | if (method_exists($class, 'onWorkerStart')) { 261 | call_user_func([$class, 'onWorkerStart'], $worker); 262 | } 263 | } 264 | } 265 | 266 | if (!function_exists('worker_start')) { 267 | /** 268 | * @param $process_name 269 | * @param $config 270 | * @return void 271 | */ 272 | function worker_start($process_name, $config) 273 | { 274 | $worker = new \Workerman\Worker($config['listen'] ?? null, $config['context'] ?? []); 275 | $property_map = [ 276 | 'count', 277 | 'user', 278 | 'group', 279 | 'reloadable', 280 | 'reusePort', 281 | 'transport', 282 | 'protocol', 283 | ]; 284 | 285 | $worker->name = $process_name; 286 | 287 | foreach ($property_map as $property) { 288 | if (isset($config[$property])) { 289 | $worker->$property = $config[$property]; 290 | } 291 | } 292 | 293 | $worker->onWorkerStart = function ($worker) use ($config) { 294 | create_laravel_application_for_worker($worker); 295 | 296 | foreach ($config['services'] ?? [] as $server) { 297 | if (!class_exists($server['handler'])) { 298 | echo "process error: class {$server['handler']} not exists\r\n"; 299 | continue; 300 | } 301 | 302 | $listen = new \Workerman\Worker($server['listen'] ?? null, $server['context'] ?? []); 303 | if (isset($server['listen'])) { 304 | echo "listen: {$server['listen']}\n"; 305 | } 306 | 307 | $instance = \Illuminate\Container\Container::getInstance()->make($server['handler'], $server['constructor'] ?? []); 308 | 309 | worker_bind($listen, $instance); 310 | 311 | $listen->listen(); 312 | } 313 | 314 | if (isset($config['handler'])) { 315 | if (!class_exists($config['handler'])) { 316 | echo "process error: class {$config['handler']} not exists\r\n"; 317 | return; 318 | } 319 | 320 | $instance = \Illuminate\Container\Container::getInstance()->make($config['handler'], $config['constructor'] ?? []); 321 | 322 | worker_bind($worker, $instance); 323 | } 324 | }; 325 | } 326 | } 327 | --------------------------------------------------------------------------------