├── tests ├── stub │ ├── .env │ ├── runtime │ │ └── .gitignore │ ├── config │ │ ├── app.php │ │ └── cache.php │ ├── think │ └── app │ │ └── controller │ │ └── Index.php ├── Pest.php └── unit │ └── watcher │ └── FswatchTest.php ├── .gitignore ├── src ├── message │ ├── ReloadMessage.php │ └── PushMessage.php ├── rpc │ ├── client │ │ ├── Service.php │ │ ├── Connector.php │ │ ├── Proxy.php │ │ └── Gateway.php │ ├── File.php │ ├── Sendfile.php │ ├── server │ │ ├── Channel.php │ │ └── Dispatcher.php │ ├── Packer.php │ ├── Error.php │ ├── Protocol.php │ └── JsonParser.php ├── exception │ ├── RpcClientException.php │ └── RpcResponseException.php ├── watcher │ ├── Driver.php │ └── driver │ │ ├── Scan.php │ │ ├── Fswatch.php │ │ └── Find.php ├── contract │ ├── LockInterface.php │ ├── ResetterInterface.php │ ├── websocket │ │ ├── HandlerInterface.php │ │ └── RoomInterface.php │ └── rpc │ │ └── ParserInterface.php ├── websocket │ ├── Event.php │ ├── Room.php │ ├── middleware │ │ └── SessionInit.php │ ├── socketio │ │ ├── EnginePacket.php │ │ ├── Packet.php │ │ └── Handler.php │ ├── Handler.php │ ├── Pusher.php │ └── room │ │ ├── Table.php │ │ └── Redis.php ├── Http.php ├── pool │ ├── Cache.php │ ├── Db.php │ ├── Connector.php │ ├── Client.php │ ├── proxy │ │ ├── Store.php │ │ └── Connection.php │ └── Proxy.php ├── resetters │ ├── ResetConfig.php │ ├── ResetModel.php │ ├── ClearInstances.php │ ├── ResetEvent.php │ ├── ResetPaginator.php │ └── ResetService.php ├── App.php ├── response │ ├── Iterator.php │ ├── Websocket.php │ └── File.php ├── middleware │ ├── InteractsWithVarDumper.php │ ├── TraceRpcServer.php │ └── TraceRpcClient.php ├── Ipc.php ├── concerns │ ├── ModifyProperty.php │ ├── InteractsWithLock.php │ ├── WithMiddleware.php │ ├── InteractsWithPools.php │ ├── WithContainer.php │ ├── WithRpcClient.php │ ├── InteractsWithSwooleTable.php │ ├── InteractsWithRpcConnector.php │ ├── InteractsWithTracing.php │ ├── WithApplication.php │ ├── InteractsWithQueue.php │ ├── InteractsWithRpcClient.php │ ├── InteractsWithRpcServer.php │ ├── InteractsWithServer.php │ └── InteractsWithWebsocket.php ├── Lock.php ├── coroutine │ ├── Barrier.php │ └── Context.php ├── packet │ ├── Buffer.php │ └── File.php ├── Service.php ├── lock │ └── Table.php ├── ipc │ ├── Driver.php │ └── driver │ │ ├── Redis.php │ │ └── UnixSocket.php ├── Watcher.php ├── helpers.php ├── Pool.php ├── command │ ├── Server.php │ └── RpcInterface.php ├── Manager.php ├── Middleware.php ├── Table.php ├── config │ └── swoole.php ├── Websocket.php └── Sandbox.php ├── phpunit.xml ├── phpstan.neon ├── composer.json ├── README.md └── LICENSE /tests/stub/.env: -------------------------------------------------------------------------------- 1 | APP_DEBUG=true 2 | -------------------------------------------------------------------------------- /tests/stub/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | composer.lock 4 | vendor 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /tests/stub/config/app.php: -------------------------------------------------------------------------------- 1 | 'error', 4 | ]; 5 | -------------------------------------------------------------------------------- /src/message/ReloadMessage.php: -------------------------------------------------------------------------------- 1 | initialize(); 7 | -------------------------------------------------------------------------------- /src/rpc/client/Service.php: -------------------------------------------------------------------------------- 1 | 'file', 4 | 'stores' => [ 5 | 'file' => [ 6 | 'type' => 'File', 7 | ], 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /src/watcher/Driver.php: -------------------------------------------------------------------------------- 1 | console->addCommands([\think\swoole\command\Server::class]); 11 | 12 | $app->console->run(); 13 | -------------------------------------------------------------------------------- /src/message/PushMessage.php: -------------------------------------------------------------------------------- 1 | fd = $fd; 13 | $this->data = $data; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/websocket/Event.php: -------------------------------------------------------------------------------- 1 | type = $type; 13 | $this->data = $data; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | post()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pool/Cache.php: -------------------------------------------------------------------------------- 1 | app->config->get('swoole.pool.cache', [])); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/rpc/File.php: -------------------------------------------------------------------------------- 1 | getPathname())) { 14 | unlink($this->getPathname()); 15 | } 16 | } catch (Throwable $e) { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/resetters/ResetConfig.php: -------------------------------------------------------------------------------- 1 | instance('config', clone $sandbox->getConfig()); 15 | 16 | return $app; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/App.php: -------------------------------------------------------------------------------- 1 | inConsole = $inConsole; 12 | } 13 | 14 | public function runningInConsole(): bool 15 | { 16 | return $this->inConsole; 17 | } 18 | 19 | public function clearInstances() 20 | { 21 | $this->instances = []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/response/Iterator.php: -------------------------------------------------------------------------------- 1 | iterator = $iterator; 16 | } 17 | 18 | public function getIterator(): Traversable 19 | { 20 | return $this->iterator; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/middleware/InteractsWithVarDumper.php: -------------------------------------------------------------------------------- 1 | getMessage(), $error->getCode()); 15 | $this->error = $error; 16 | } 17 | 18 | public function getError() 19 | { 20 | return $this->error; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Ipc.php: -------------------------------------------------------------------------------- 1 | app->config->get("swoole.ipc.{$name}", []); 17 | } 18 | 19 | public function getDefaultDriver() 20 | { 21 | return $this->app->config->get('swoole.ipc.type', 'unix_socket'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/concerns/ModifyProperty.php: -------------------------------------------------------------------------------- 1 | hasProperty($property)) { 13 | $reflectProperty = $reflectObject->getProperty($property); 14 | $reflectProperty->setAccessible(true); 15 | $reflectProperty->setValue($object, $value); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/resetters/ResetModel.php: -------------------------------------------------------------------------------- 1 | getApplication()->invoke(...$args); 18 | }); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Lock.php: -------------------------------------------------------------------------------- 1 | app->config->get("swoole.lock.{$name}", []); 15 | } 16 | 17 | /** 18 | * 默认驱动 19 | * @return string|null 20 | */ 21 | public function getDefaultDriver() 22 | { 23 | return $this->app->config->get('swoole.lock.type', 'table'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/rpc/Sendfile.php: -------------------------------------------------------------------------------- 1 | getPathname(), 'rb'); 10 | if ($handle) { 11 | try { 12 | yield pack(Packer::HEADER_PACK, $file->getSize(), Packer::TYPE_FILE); 13 | while (!feof($handle)) { 14 | yield fread($handle, 8192); 15 | } 16 | } finally { 17 | fclose($handle); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/coroutine/Barrier.php: -------------------------------------------------------------------------------- 1 | close(); 16 | }); 17 | 18 | call_user_func_array($func, $params); 19 | }, ...$params); 20 | 21 | $channel->pop(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests 6 | scanFiles: 7 | - vendor/topthink/framework/src/helper.php 8 | scanDirectories: 9 | - vendor/swoole/ide-helper/src/swoole_library/src 10 | treatPhpDocTypesAsCertain: false 11 | universalObjectCratesClasses: 12 | - PHPUnit\Framework\TestCase 13 | ignoreErrors: 14 | - 15 | identifier: while.alwaysTrue 16 | path: src\concerns\InteractsWithQueue.php 17 | - 18 | identifier: if.alwaysFalse 19 | path: src\concerns\InteractsWithWebsocket.php 20 | - 21 | identifier: trait.unused 22 | - 23 | identifier: argument.type 24 | path: src\config\swoole.php 25 | -------------------------------------------------------------------------------- /src/resetters/ClearInstances.php: -------------------------------------------------------------------------------- 1 | getConfig()->get('swoole.instances', [])); 16 | 17 | foreach ($instances as $instance) { 18 | $app->delete($instance); 19 | } 20 | 21 | return $app; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/resetters/ResetEvent.php: -------------------------------------------------------------------------------- 1 | getEvent(); 21 | $this->modifyProperty($event, $app); 22 | $app->instance('event', $event); 23 | 24 | return $app; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/contract/websocket/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | length = $length; 13 | } 14 | 15 | public function write(&$data) 16 | { 17 | $size = strlen($this->data); 18 | $string = substr($data, 0, $this->length - $size); 19 | 20 | $this->data .= $string; 21 | 22 | if (strlen($data) >= $this->length - $size) { 23 | $data = substr($data, $this->length - $size); 24 | 25 | return $this->data; 26 | } else { 27 | $data = ''; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/websocket/Room.php: -------------------------------------------------------------------------------- 1 | app->config->get("swoole.websocket.room.{$name}", []); 20 | } 21 | 22 | /** 23 | * 默认驱动 24 | * @return string|null 25 | */ 26 | public function getDefaultDriver() 27 | { 28 | return $this->app->config->get('swoole.websocket.room.type', 'table'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithLock.php: -------------------------------------------------------------------------------- 1 | getConfig('lock.enable', false)) { 25 | $this->lock = $this->container->make(Lock::class); 26 | $this->lock->prepare(); 27 | 28 | $this->onEvent('workerStart', function () { 29 | $this->app->instance(Lock::class, $this->lock); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/contract/rpc/ParserInterface.php: -------------------------------------------------------------------------------- 1 | close(); 26 | } 27 | } 28 | }, $this->config->get('swoole.pool.db', [])); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/rpc/server/Channel.php: -------------------------------------------------------------------------------- 1 | queue = new Coroutine\Channel(1); 18 | Coroutine::create(function () use ($handler) { 19 | $this->queue->push($handler); 20 | }); 21 | } 22 | 23 | /** 24 | * @return File|Buffer 25 | */ 26 | public function pop() 27 | { 28 | return $this->queue->pop(); 29 | } 30 | 31 | public function push($handle) 32 | { 33 | return $this->queue->push($handle); 34 | } 35 | 36 | public function close() 37 | { 38 | return $this->queue->close(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/resetters/ResetPaginator.php: -------------------------------------------------------------------------------- 1 | getApplication()->request->baseUrl(); 17 | }); 18 | 19 | Paginator::currentPageResolver(function ($varPage = 'page') use ($sandbox) { 20 | 21 | $page = $sandbox->getApplication()->request->param($varPage); 22 | 23 | if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { 24 | return (int) $page; 25 | } 26 | 27 | return 1; 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/packet/File.php: -------------------------------------------------------------------------------- 1 | length = $length; 14 | } 15 | 16 | public function write(&$data) 17 | { 18 | if (!$this->handle) { 19 | $this->name = tempnam(sys_get_temp_dir(), 'swoole_rpc_'); 20 | $this->handle = fopen($this->name, 'ab'); 21 | } 22 | 23 | $size = fstat($this->handle)['size']; 24 | $string = substr($data, 0, $this->length - $size); 25 | 26 | fwrite($this->handle, $string); 27 | 28 | if (strlen($data) >= $this->length - $size) { 29 | fclose($this->handle); 30 | $data = substr($data, $this->length - $size); 31 | 32 | return new \think\swoole\rpc\File($this->name); 33 | } else { 34 | $data = ''; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\swoole; 13 | 14 | use think\swoole\command\RpcInterface; 15 | use think\swoole\command\Server as ServerCommand; 16 | 17 | class Service extends \think\Service 18 | { 19 | 20 | public function boot() 21 | { 22 | $this->commands( 23 | ServerCommand::class, 24 | RpcInterface::class, 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/resetters/ResetService.php: -------------------------------------------------------------------------------- 1 | getServices() as $service) { 27 | $this->modifyProperty($service, $app); 28 | if (method_exists($service, 'register')) { 29 | $service->register(); 30 | } 31 | if (method_exists($service, 'boot')) { 32 | $app->invoke([$service, 'boot']); 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/lock/Table.php: -------------------------------------------------------------------------------- 1 | locks = new SwooleTable(1024); 19 | $this->locks->column('time', SwooleTable::TYPE_INT); 20 | $this->locks->create(); 21 | } 22 | 23 | public function lock($name, $expire = 60) 24 | { 25 | $time = time(); 26 | 27 | while (true) { 28 | $lock = $this->locks->get($name); 29 | if (!$lock || $lock['time'] <= $time - $expire) { 30 | $this->locks->set($name, ['time' => time()]); 31 | return true; 32 | } else { 33 | usleep(500); 34 | continue; 35 | } 36 | } 37 | } 38 | 39 | public function unlock($name) 40 | { 41 | $this->locks->del($name); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/pool/Connector.php: -------------------------------------------------------------------------------- 1 | connector = $connector; 15 | } 16 | 17 | public function setChecker($checker) 18 | { 19 | $this->checker = $checker; 20 | } 21 | 22 | public function connect(array $config) 23 | { 24 | return call_user_func($this->connector, $config); 25 | } 26 | 27 | public function disconnect($connection) 28 | { 29 | 30 | } 31 | 32 | public function isConnected($connection): bool 33 | { 34 | if ($this->checker) { 35 | return call_user_func($this->checker, $connection); 36 | } 37 | return true; 38 | } 39 | 40 | public function reset($connection, array $config) 41 | { 42 | 43 | } 44 | 45 | public function validate($connection): bool 46 | { 47 | return true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ipc/Driver.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 21 | $this->config = $config; 22 | } 23 | 24 | public function listenMessage($workerId) 25 | { 26 | $this->workerId = $workerId; 27 | 28 | $this->subscribe(); 29 | } 30 | 31 | public function sendMessage($workerId, $message) 32 | { 33 | if ($workerId === $this->workerId) { 34 | $this->manager->triggerEvent('message', $message); 35 | } else { 36 | $this->publish($workerId, $message); 37 | } 38 | } 39 | 40 | abstract public function getType(); 41 | 42 | abstract public function prepare(Pool $pool); 43 | 44 | abstract public function subscribe(); 45 | 46 | abstract public function publish($workerId, $message); 47 | } 48 | -------------------------------------------------------------------------------- /src/concerns/WithMiddleware.php: -------------------------------------------------------------------------------- 1 | middleware[] = [ 18 | 'middleware' => [$middleware, $params], 19 | 'options' => &$options, 20 | ]; 21 | 22 | return new class($options) { 23 | protected $options; 24 | 25 | public function __construct(array &$options) 26 | { 27 | $this->options = &$options; 28 | } 29 | 30 | public function only($methods) 31 | { 32 | $this->options['only'] = is_array($methods) ? $methods : func_get_args(); 33 | return $this; 34 | } 35 | 36 | public function except($methods) 37 | { 38 | $this->options['except'] = is_array($methods) ? $methods : func_get_args(); 39 | 40 | return $this; 41 | } 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Watcher.php: -------------------------------------------------------------------------------- 1 | app->config->get('swoole.hot_update.' . $name, $default); 15 | } 16 | 17 | /** 18 | * @param $name 19 | * @return \think\swoole\watcher\Driver 20 | */ 21 | public function monitor($name = null) 22 | { 23 | return $this->driver($name); 24 | } 25 | 26 | protected function resolveParams($name): array 27 | { 28 | return [ 29 | [ 30 | 'directory' => array_filter($this->getConfig('include', []), function ($dir) { 31 | return is_dir($dir); 32 | }), 33 | 'exclude' => $this->getConfig('exclude', []), 34 | 'name' => $this->getConfig('name', []), 35 | ], 36 | ]; 37 | } 38 | 39 | public function getDefaultDriver() 40 | { 41 | return $this->getConfig('type', 'scan'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/middleware/TraceRpcServer.php: -------------------------------------------------------------------------------- 1 | tracer = $tracer; 20 | } 21 | 22 | public function handle(Protocol $protocol, $next) 23 | { 24 | $context = $this->tracer->extract(TEXT_MAP, $protocol->getContext()); 25 | $scope = $this->tracer->startActiveSpan( 26 | 'rpc.server:' . $protocol->getInterface() . '@' . $protocol->getMethod(), 27 | [ 28 | 'child_of' => $context, 29 | 'tags' => [ 30 | SPAN_KIND => SPAN_KIND_RPC_SERVER, 31 | ], 32 | ] 33 | ); 34 | $span = $scope->getSpan(); 35 | 36 | try { 37 | return $next($protocol); 38 | } catch (Throwable $e) { 39 | $span->setTag(ERROR, $e); 40 | throw $e; 41 | } finally { 42 | $scope->close(); 43 | $this->tracer->flush(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/contract/websocket/RoomInterface.php: -------------------------------------------------------------------------------- 1 | setContentDisposition($disposition, $name); 38 | } 39 | 40 | return $response; 41 | } 42 | 43 | function file(string $filename) 44 | { 45 | return new File($filename); 46 | } 47 | 48 | function iterator(Traversable $iterator) 49 | { 50 | return new Iterator($iterator); 51 | } 52 | 53 | function websocket() 54 | { 55 | return new Websocket(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/websocket/middleware/SessionInit.php: -------------------------------------------------------------------------------- 1 | app = $app; 22 | $this->session = $session; 23 | } 24 | 25 | /** 26 | * Session初始化 27 | * @access public 28 | * @param Request $request 29 | * @param Closure $next 30 | * @return Response 31 | */ 32 | public function handle($request, Closure $next) 33 | { 34 | // Session初始化 35 | $varSessionId = $this->app->config->get('session.var_session_id'); 36 | $cookieName = $this->session->getName(); 37 | 38 | if ($varSessionId && $request->request($varSessionId)) { 39 | $sessionId = $request->request($varSessionId); 40 | } else { 41 | $sessionId = $request->cookie($cookieName); 42 | } 43 | 44 | if ($sessionId) { 45 | $this->session->setId($sessionId); 46 | } 47 | 48 | $this->session->init(); 49 | 50 | $request->withSession($this->session); 51 | 52 | return $next($request); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithPools.php: -------------------------------------------------------------------------------- 1 | app->make(Pool::class); 24 | } 25 | 26 | protected function preparePools() 27 | { 28 | $createPools = function () { 29 | $pools = $this->getPools(); 30 | 31 | foreach ($this->getConfig('pool', []) as $name => $config) { 32 | $type = Arr::pull($config, 'type'); 33 | if ($type && is_subclass_of($type, ConnectorInterface::class)) { 34 | $pool = new ConnectionPool( 35 | Pool::pullPoolConfig($config), 36 | $this->app->make($type), 37 | $config 38 | ); 39 | $pools->add($name, $pool); 40 | //注入到app 41 | $this->app->instance("swoole.pool.{$name}", $pool); 42 | } 43 | } 44 | }; 45 | 46 | $this->onEvent('workerStart', $createPools); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/watcher/FswatchTest.php: -------------------------------------------------------------------------------- 1 | config->set([ 11 | 'hot_update' => [ 12 | 'name' => ['*.txt'], 13 | 'include' => [runtime_path()], 14 | 'exclude' => [], 15 | ], 16 | ], 'swoole'); 17 | }); 18 | 19 | it('test fswatch watcher', function ($type) { 20 | run(function () use ($type) { 21 | $monitor = app(Watcher::class)->monitor($type); 22 | expect($monitor)->toBeInstanceOf(Driver::class); 23 | 24 | $changes = []; 25 | Coroutine::create(function () use (&$changes, $monitor) { 26 | $monitor->watch(function ($data) use (&$changes) { 27 | $changes = array_merge($changes, $data); 28 | }); 29 | }); 30 | Timer::after(500, function () { 31 | file_put_contents(runtime_path() . 'some.css', 'test'); 32 | file_put_contents(runtime_path() . 'test.txt', 'test'); 33 | }); 34 | 35 | sleep(3); 36 | 37 | expect($changes)->toBe([runtime_path() . 'test.txt']); 38 | $monitor->stop(); 39 | }); 40 | })->with([ 41 | 'find', 42 | 'fswatch', 43 | 'scan', 44 | ])->after(function () { 45 | @unlink(runtime_path() . 'test.css'); 46 | @unlink(runtime_path() . 'test.txt'); 47 | }); 48 | -------------------------------------------------------------------------------- /src/rpc/Packer.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public static function unpack($data) 28 | { 29 | $header = unpack(self::HEADER_STRUCT, substr($data, 0, self::HEADER_SIZE)); 30 | if ($header === false) { 31 | throw new RuntimeException('Invalid Header'); 32 | } 33 | 34 | switch ($header['type']) { 35 | case Packer::TYPE_BUFFER: 36 | $handler = new Buffer($header['length']); 37 | break; 38 | case Packer::TYPE_FILE: 39 | $handler = new File($header['length']); 40 | break; 41 | default: 42 | throw new RuntimeException("unsupported data type: [{$header['type']}"); 43 | } 44 | 45 | $data = substr($data, self::HEADER_SIZE); 46 | 47 | return [$handler, $data]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/middleware/TraceRpcClient.php: -------------------------------------------------------------------------------- 1 | tracer = $tracer; 21 | } 22 | 23 | public function handle(Protocol $protocol, $next) 24 | { 25 | $scope = $this->tracer->startActiveSpan( 26 | 'rpc.client:' . $protocol->getInterface() . '@' . $protocol->getMethod(), 27 | [ 28 | 'tags' => [ 29 | SPAN_KIND => SPAN_KIND_RPC_CLIENT, 30 | ], 31 | ] 32 | ); 33 | $span = $scope->getSpan(); 34 | $context = $protocol->getContext(); 35 | $this->tracer->inject($span->getContext(), TEXT_MAP, $context); 36 | $protocol->setContext($context); 37 | 38 | try { 39 | return $next($protocol); 40 | } catch (Throwable $e) { 41 | if (!$e instanceof RpcResponseException) { 42 | $span->setTag(ERROR, $e); 43 | } 44 | throw $e; 45 | } finally { 46 | $scope->close(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/rpc/Error.php: -------------------------------------------------------------------------------- 1 | code = $code; 34 | $instance->message = $message; 35 | $instance->data = $data; 36 | 37 | return $instance; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getCode(): int 44 | { 45 | return $this->code; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getMessage(): string 52 | { 53 | return $this->message; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getData() 60 | { 61 | return $this->data; 62 | } 63 | 64 | #[\ReturnTypeWillChange] 65 | public function jsonSerialize() 66 | { 67 | return [ 68 | 'code' => $this->code, 69 | 'message' => $this->message, 70 | 'data' => $this->data, 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/response/Websocket.php: -------------------------------------------------------------------------------- 1 | listeners['Open'] = $listener; 15 | return $this; 16 | } 17 | 18 | public function onMessage($listener) 19 | { 20 | $this->listeners['Message'] = $listener; 21 | return $this; 22 | } 23 | 24 | public function onEvent($listener) 25 | { 26 | $this->listeners['Event'] = $listener; 27 | return $this; 28 | } 29 | 30 | public function onClose($listener) 31 | { 32 | $this->listeners['Close'] = $listener; 33 | return $this; 34 | } 35 | 36 | public function onConnect($listener) 37 | { 38 | $this->listeners['Connect'] = $listener; 39 | return $this; 40 | } 41 | 42 | public function onDisconnect($listener) 43 | { 44 | $this->listeners['Disconnect'] = $listener; 45 | return $this; 46 | } 47 | 48 | public function onPing($listener) 49 | { 50 | $this->listeners['Ping'] = $listener; 51 | return $this; 52 | } 53 | 54 | public function onPong($listener) 55 | { 56 | $this->listeners['Pong'] = $listener; 57 | return $this; 58 | } 59 | 60 | public function subscribe(Event $event) 61 | { 62 | foreach ($this->listeners as $eventName => $listener) { 63 | $event->listen('swoole.websocket.' . $eventName, $listener); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/watcher/driver/Scan.php: -------------------------------------------------------------------------------- 1 | finder = new Finder(); 19 | $this->finder 20 | ->files() 21 | ->name($config['name']) 22 | ->in($config['directory']) 23 | ->exclude($config['exclude']); 24 | } 25 | 26 | protected function findFiles() 27 | { 28 | $files = []; 29 | /** @var SplFileInfo $f */ 30 | foreach ($this->finder as $f) { 31 | $files[$f->getRealpath()] = $f->getMTime(); 32 | } 33 | return $files; 34 | } 35 | 36 | public function watch(callable $callback) 37 | { 38 | $this->files = $this->findFiles(); 39 | 40 | $this->timer = Timer::tick(2000, function () use ($callback) { 41 | 42 | $files = $this->findFiles(); 43 | 44 | foreach ($files as $path => $time) { 45 | if (empty($this->files[$path]) || $this->files[$path] != $time) { 46 | call_user_func($callback, [$path]); 47 | break; 48 | } 49 | } 50 | 51 | $this->files = $files; 52 | }); 53 | } 54 | 55 | public function stop() 56 | { 57 | if ($this->timer) { 58 | Timer::clear($this->timer); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/websocket/socketio/EnginePacket.php: -------------------------------------------------------------------------------- 1 | type = $type; 49 | $this->data = $data; 50 | } 51 | 52 | public static function open($payload) 53 | { 54 | return new self(self::OPEN, $payload); 55 | } 56 | 57 | public static function pong($payload = '') 58 | { 59 | return new self(self::PONG, $payload); 60 | } 61 | 62 | public static function ping() 63 | { 64 | return new self(self::PING); 65 | } 66 | 67 | public static function message($payload) 68 | { 69 | return new self(self::MESSAGE, $payload); 70 | } 71 | 72 | public static function fromString(string $packet) 73 | { 74 | return new self(substr($packet, 0, 1), substr($packet, 1) ?: ''); 75 | } 76 | 77 | public function toString() 78 | { 79 | return $this->type . $this->data; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/concerns/WithContainer.php: -------------------------------------------------------------------------------- 1 | container = $container; 24 | } 25 | 26 | protected function getContainer() 27 | { 28 | return $this->container; 29 | } 30 | 31 | /** 32 | * 获取配置 33 | * @param string $name 34 | * @param mixed $default 35 | * @return mixed 36 | */ 37 | public function getConfig(string $name, $default = null) 38 | { 39 | return $this->container->config->get("swoole.{$name}", $default); 40 | } 41 | 42 | /** 43 | * 触发事件 44 | * @param string $event 45 | * @param null $params 46 | */ 47 | public function triggerEvent(string $event, $params = null): void 48 | { 49 | $this->container->event->trigger("swoole.{$event}", $params); 50 | } 51 | 52 | /** 53 | * 监听事件 54 | * @param string $event 55 | * @param $listener 56 | * @param bool $first 57 | */ 58 | public function onEvent(string $event, $listener, bool $first = false): void 59 | { 60 | $this->container->event->listen("swoole.{$event}", $listener, $first); 61 | } 62 | 63 | /** 64 | * Log server error. 65 | * 66 | * @param Throwable $e 67 | */ 68 | public function logServerError(Throwable $e) 69 | { 70 | /** @var Handle $handle */ 71 | $handle = $this->container->make(Handle::class); 72 | 73 | $handle->report($e); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Pool.php: -------------------------------------------------------------------------------- 1 | init(); 21 | $this->pools[$name] = $pool; 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * @param string $name 28 | * 29 | * @return ConnectionPool 30 | */ 31 | public function get(string $name) 32 | { 33 | return $this->pools[$name] ?? null; 34 | } 35 | 36 | public function close(string $key) 37 | { 38 | return $this->pools[$key]->close(); 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getAll() 45 | { 46 | return $this->pools; 47 | } 48 | 49 | public function closeAll() 50 | { 51 | foreach ($this->pools as $pool) { 52 | $pool->close(); 53 | } 54 | } 55 | 56 | /** 57 | * @param string $key 58 | * 59 | * @return ConnectionPool 60 | */ 61 | public function __get($key) 62 | { 63 | return $this->get($key); 64 | } 65 | 66 | public static function pullPoolConfig(&$config) 67 | { 68 | return [ 69 | 'minActive' => Arr::pull($config, 'min_active', 0), 70 | 'maxActive' => Arr::pull($config, 'max_active', 10), 71 | 'maxWaitTime' => Arr::pull($config, 'max_wait_time', 5), 72 | 'maxIdleTime' => Arr::pull($config, 'max_idle_time', 20), 73 | 'idleCheckInterval' => Arr::pull($config, 'idle_check_interval', 10), 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/websocket/Handler.php: -------------------------------------------------------------------------------- 1 | event = $event; 18 | } 19 | 20 | /** 21 | * "onOpen" listener. 22 | * 23 | * @param Request $request 24 | */ 25 | public function onOpen(Request $request) 26 | { 27 | $this->event->trigger('swoole.websocket.Open', $request); 28 | } 29 | 30 | /** 31 | * "onMessage" listener. 32 | * 33 | * @param Frame $frame 34 | */ 35 | public function onMessage(Frame $frame) 36 | { 37 | $this->event->trigger('swoole.websocket.Message', $frame); 38 | 39 | $event = $this->decode($frame->data); 40 | if ($event) { 41 | $this->event->trigger('swoole.websocket.Event', $event); 42 | } 43 | } 44 | 45 | /** 46 | * "onClose" listener. 47 | */ 48 | public function onClose() 49 | { 50 | $this->event->trigger('swoole.websocket.Close'); 51 | } 52 | 53 | protected function decode($payload) 54 | { 55 | $data = json_decode($payload, true); 56 | if (!empty($data['type'])) { 57 | return new WsEvent($data['type'], $data['data'] ?? null); 58 | } 59 | return null; 60 | } 61 | 62 | public function encodeMessage($message) 63 | { 64 | if ($message instanceof WsEvent) { 65 | return json_encode([ 66 | 'type' => $message->type, 67 | 'data' => $message->data, 68 | ]); 69 | } 70 | return $message; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/pool/Client.php: -------------------------------------------------------------------------------- 1 | set($config); 25 | 26 | $client->connect($host, $port, $timeout); 27 | 28 | return $client; 29 | } 30 | 31 | /** 32 | * Disconnect and free resources 33 | * @param \Swoole\Coroutine\Client $connection 34 | * @return mixed 35 | */ 36 | public function disconnect($connection) 37 | { 38 | $connection->close(); 39 | } 40 | 41 | /** 42 | * Whether the connection is established 43 | * @param \Swoole\Coroutine\Client $connection 44 | * @return bool 45 | */ 46 | public function isConnected($connection): bool 47 | { 48 | return $connection->isConnected() && $connection->peek() !== ''; 49 | } 50 | 51 | /** 52 | * Reset the connection 53 | * @param \Swoole\Coroutine\Client $connection 54 | * @param array $config 55 | * @return mixed 56 | */ 57 | public function reset($connection, array $config) 58 | { 59 | } 60 | 61 | /** 62 | * Validate the connection 63 | * 64 | * @param mixed $connection 65 | * @return bool 66 | */ 67 | public function validate($connection): bool 68 | { 69 | return $connection instanceof \Swoole\Coroutine\Client; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/websocket/Pusher.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 29 | $this->room = $room; 30 | $this->handler = $handler; 31 | } 32 | 33 | public function to(...$values) 34 | { 35 | foreach ($values as $value) { 36 | if (is_array($value)) { 37 | $this->to(...$value); 38 | } elseif (!in_array($value, $this->to)) { 39 | $this->to[] = $value; 40 | } 41 | } 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Push message to related descriptors 48 | * @param $data 49 | * @return void 50 | */ 51 | public function push($data): void 52 | { 53 | $fds = []; 54 | 55 | foreach ($this->to as $room) { 56 | $clients = $this->room->getClients($room); 57 | if (!empty($clients)) { 58 | $fds = array_merge($fds, $clients); 59 | } 60 | } 61 | 62 | foreach (array_unique($fds) as $fd) { 63 | [$workerId, $fd] = explode('.', $fd); 64 | $data = $this->handler->encodeMessage($data); 65 | $this->manager->sendMessage((int) $workerId, new PushMessage((int) $fd, $data)); 66 | } 67 | } 68 | 69 | public function emit(string $event, ...$data): void 70 | { 71 | $this->push(new Event($event, $data)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "topthink/think-swoole", 3 | "description": "Swoole extend for thinkphp", 4 | "license": "Apache-2.0", 5 | "authors": [ 6 | { 7 | "name": "liu21st", 8 | "email": "liu21st@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.0", 13 | "ext-json": "*", 14 | "ext-swoole": "^4.0|^5.0|^6.0", 15 | "topthink/framework": "^6.0|^8.0", 16 | "nette/php-generator": "^4.0", 17 | "open-smf/connection-pool": ">=1.0", 18 | "stechstudio/backoff": "^1.2", 19 | "symfony/finder": ">=4.3", 20 | "symfony/process": ">=4.2", 21 | "swoole/ide-helper": "^5.0" 22 | }, 23 | "require-dev": { 24 | "topthink/think-tracing": "^1.0", 25 | "topthink/think-queue": "^3.0", 26 | "phpstan/phpstan": "^2.0", 27 | "pestphp/pest": "^3.7" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "think\\swoole\\": "src" 32 | }, 33 | "files": [ 34 | "src/helpers.php" 35 | ] 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "app\\": "tests/stub/app" 40 | } 41 | }, 42 | "extra": { 43 | "think": { 44 | "services": [ 45 | "think\\swoole\\Service" 46 | ], 47 | "config": { 48 | "swoole": "src/config/swoole.php" 49 | } 50 | } 51 | }, 52 | "config": { 53 | "preferred-install": "dist", 54 | "sort-packages": true, 55 | "platform-check": false, 56 | "platform": { 57 | "ext-swoole": "5.0.0", 58 | "ext-fileinfo": "1.0.4" 59 | }, 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true 62 | } 63 | }, 64 | "scripts": { 65 | "analyze": "phpstan --memory-limit=1G", 66 | "test": "pest --colors=always" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/command/Server.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\swoole\command; 13 | 14 | use think\console\Command; 15 | use think\console\input\Option; 16 | use think\swoole\Manager; 17 | 18 | class Server extends Command 19 | { 20 | public function configure() 21 | { 22 | $this->setName('swoole') 23 | ->addOption( 24 | 'env', 25 | 'E', 26 | Option::VALUE_REQUIRED, 27 | 'Environment name', 28 | '' 29 | ) 30 | ->setDescription('Swoole Server for ThinkPHP'); 31 | } 32 | 33 | public function handle(Manager $manager) 34 | { 35 | $this->checkEnvironment(); 36 | 37 | $this->output->writeln('Starting swoole server...'); 38 | 39 | $this->output->writeln('You can exit with `CTRL-C`'); 40 | 41 | $envName = $this->input->getOption('env'); 42 | $manager->start($envName); 43 | } 44 | 45 | /** 46 | * 检查环境 47 | */ 48 | protected function checkEnvironment() 49 | { 50 | if (!extension_loaded('swoole')) { 51 | $this->output->error('Can\'t detect Swoole extension installed.'); 52 | 53 | exit(1); 54 | } 55 | 56 | if (!version_compare(swoole_version(), '4.6.0', 'ge')) { 57 | $this->output->error('Your Swoole version must be higher than `4.6.0`.'); 58 | 59 | exit(1); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\swoole; 13 | 14 | use think\swoole\concerns\InteractsWithHttp; 15 | use think\swoole\concerns\InteractsWithLock; 16 | use think\swoole\concerns\InteractsWithPools; 17 | use think\swoole\concerns\InteractsWithQueue; 18 | use think\swoole\concerns\InteractsWithRpcClient; 19 | use think\swoole\concerns\InteractsWithRpcServer; 20 | use think\swoole\concerns\InteractsWithServer; 21 | use think\swoole\concerns\InteractsWithSwooleTable; 22 | use think\swoole\concerns\InteractsWithTracing; 23 | use think\swoole\concerns\WithApplication; 24 | use think\swoole\concerns\WithContainer; 25 | 26 | /** 27 | * Class Manager 28 | */ 29 | class Manager 30 | { 31 | use InteractsWithServer, 32 | InteractsWithSwooleTable, 33 | InteractsWithHttp, 34 | InteractsWithPools, 35 | InteractsWithRpcClient, 36 | InteractsWithRpcServer, 37 | InteractsWithQueue, 38 | InteractsWithTracing, 39 | InteractsWithLock, 40 | WithContainer, 41 | WithApplication; 42 | 43 | /** 44 | * Initialize. 45 | */ 46 | protected function initialize(): void 47 | { 48 | $this->prepareTables(); 49 | $this->preparePools(); 50 | $this->prepareHttp(); 51 | $this->prepareRpcServer(); 52 | $this->prepareQueue(); 53 | $this->prepareRpcClient(); 54 | $this->prepareTracing(); 55 | $this->prepareLock(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Middleware.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | 27 | foreach ($middlewares as $middleware) { 28 | $this->queue[] = $this->buildMiddleware($middleware); 29 | } 30 | } 31 | 32 | public static function make(App $app, $middlewares = []) 33 | { 34 | return new self($app, $middlewares); 35 | } 36 | 37 | /** 38 | * 调度管道 39 | * @return Pipeline 40 | */ 41 | public function pipeline() 42 | { 43 | return (new Pipeline()) 44 | ->through(array_map(function ($middleware) { 45 | return function ($request, $next) use ($middleware) { 46 | [$call, $params] = $middleware; 47 | 48 | if (is_array($call) && is_string($call[0])) { 49 | $call = [$this->app->make($call[0]), $call[1]]; 50 | } 51 | return call_user_func($call, $request, $next, ...$params); 52 | }; 53 | }, $this->queue)); 54 | } 55 | 56 | /** 57 | * 解析中间件 58 | * @param mixed $middleware 59 | * @return array 60 | */ 61 | protected function buildMiddleware($middleware): array 62 | { 63 | if (is_array($middleware)) { 64 | [$middleware, $params] = $middleware; 65 | } 66 | 67 | if ($middleware instanceof Closure) { 68 | return [$middleware, $params ?? []]; 69 | } 70 | 71 | if (!is_string($middleware)) { 72 | throw new InvalidArgumentException('The middleware is invalid'); 73 | } 74 | 75 | return [[$middleware, 'handle'], $params ?? []]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Table.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\swoole; 13 | 14 | use Swoole\Table as SwooleTable; 15 | 16 | class Table 17 | { 18 | public const TYPE_INT = 1; 19 | 20 | public const TYPE_STRING = 3; 21 | 22 | public const TYPE_FLOAT = 2; 23 | 24 | /** 25 | * Registered swoole tables. 26 | * 27 | * @var array 28 | */ 29 | protected $tables = []; 30 | 31 | /** 32 | * Add a swoole table to existing tables. 33 | * 34 | * @param string $name 35 | * @param SwooleTable $table 36 | * 37 | * @return Table 38 | */ 39 | public function add(string $name, SwooleTable $table) 40 | { 41 | $this->tables[$name] = $table; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Get a swoole table by its name from existing tables. 48 | * 49 | * @param string $name 50 | * 51 | * @return SwooleTable $table 52 | */ 53 | public function get(string $name) 54 | { 55 | return $this->tables[$name] ?? null; 56 | } 57 | 58 | /** 59 | * Get all existing swoole tables. 60 | * 61 | * @return array 62 | */ 63 | public function getAll() 64 | { 65 | return $this->tables; 66 | } 67 | 68 | /** 69 | * Dynamically access table. 70 | * 71 | * @param string $key 72 | * 73 | * @return SwooleTable 74 | */ 75 | public function __get(string $key) 76 | { 77 | return $this->get($key); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ipc/driver/Redis.php: -------------------------------------------------------------------------------- 1 | config; 32 | 33 | $this->pool = new ConnectionPool( 34 | Pool::pullPoolConfig($config), 35 | new PhpRedisConnector(), 36 | $config 37 | ); 38 | 39 | $this->manager->getPools()->add('ipc.redis', $this->pool); 40 | 41 | Coroutine::create(function () { 42 | $this->runWithRedis(function (PHPRedis $redis) { 43 | $redis->setOption(PHPRedis::OPT_READ_TIMEOUT, -1); 44 | $redis->subscribe([$this->getPrefix() . $this->workerId], function ($redis, $channel, $message) { 45 | $this->manager->triggerEvent('message', unserialize($message)); 46 | }); 47 | }); 48 | }); 49 | } 50 | 51 | public function publish($workerId, $message) 52 | { 53 | $this->runWithRedis(function (PHPRedis $redis) use ($message, $workerId) { 54 | $redis->publish($this->getPrefix() . $workerId, serialize($message)); 55 | }); 56 | } 57 | 58 | protected function getPrefix() 59 | { 60 | return Arr::get($this->config, 'prefix', 'swoole:ipc:'); 61 | } 62 | 63 | protected function runWithRedis(Closure $callable) 64 | { 65 | $redis = $this->pool->borrow(); 66 | try { 67 | return $callable($redis); 68 | } finally { 69 | $this->pool->return($redis); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/concerns/WithRpcClient.php: -------------------------------------------------------------------------------- 1 | bindRpcInterface(); 33 | run(function () { 34 | $this->app->invoke([$this, 'handle']); 35 | }); 36 | } 37 | 38 | protected function bindRpcInterface() 39 | { 40 | //引入rpc接口文件 41 | if (file_exists($rpc = $this->app->getBasePath() . 'rpc.php')) { 42 | $rpcServices = (array) include $rpc; 43 | 44 | //绑定rpc接口 45 | try { 46 | foreach ($rpcServices as $name => $abstracts) { 47 | 48 | $config = $this->app->config->get("swoole.rpc.client.{$name}", []); 49 | 50 | $parserClass = Arr::pull($config, 'parser', JsonParser::class); 51 | $tries = Arr::pull($config, 'tries', 2); 52 | $middleware = Arr::pull($config, 'middleware', []); 53 | 54 | $parser = $this->app->make($parserClass); 55 | $gateway = new Gateway($config, $parser, $tries); 56 | 57 | foreach ($abstracts as $abstract) { 58 | $this->app->bind($abstract, function (App $app) use ($middleware, $gateway, $name, $abstract) { 59 | return $app->invokeClass(Proxy::getClassName($name, $abstract), [$gateway, $middleware]); 60 | }); 61 | } 62 | } 63 | } catch (Throwable $e) { 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithSwooleTable.php: -------------------------------------------------------------------------------- 1 | 10 | // +---------------------------------------------------------------------- 11 | 12 | namespace think\swoole\concerns; 13 | 14 | use Swoole\Table as SwooleTable; 15 | use think\App; 16 | use think\swoole\Table; 17 | 18 | /** 19 | * Trait InteractsWithSwooleTable 20 | * 21 | * @property App $container 22 | * @property App $app 23 | */ 24 | trait InteractsWithSwooleTable 25 | { 26 | /** 27 | * @var Table 28 | */ 29 | protected $currentTable; 30 | 31 | /** 32 | * Register customized swoole tables. 33 | */ 34 | protected function prepareTables() 35 | { 36 | $this->currentTable = new Table(); 37 | $this->registerTables(); 38 | $this->onEvent('workerStart', function () { 39 | $this->app->instance(Table::class, $this->currentTable); 40 | foreach ($this->currentTable->getAll() as $name => $table) { 41 | $this->app->instance("swoole.table.{$name}", $table); 42 | } 43 | }); 44 | } 45 | 46 | /** 47 | * Register user-defined swoole tables. 48 | */ 49 | protected function registerTables() 50 | { 51 | $tables = $this->container->make('config')->get('swoole.tables', []); 52 | 53 | foreach ($tables as $key => $value) { 54 | $table = new SwooleTable($value['size']); 55 | $columns = $value['columns'] ?? []; 56 | foreach ($columns as $column) { 57 | if (isset($column['size'])) { 58 | $table->column($column['name'], $column['type'], $column['size']); 59 | } else { 60 | $table->column($column['name'], $column['type']); 61 | } 62 | } 63 | $table->create(); 64 | 65 | $this->currentTable->add($key, $table); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/watcher/driver/Fswatch.php: -------------------------------------------------------------------------------- 1 | directory = $config['directory']; 27 | 28 | if (!empty($config['name'])) { 29 | foreach ($config['name'] as $value) { 30 | $this->matchRegexps[] = Glob::toRegex($value); 31 | } 32 | } 33 | } 34 | 35 | public function watch(callable $callback) 36 | { 37 | $command = $this->getCommand(); 38 | $this->process = new Process($command, timeout: 0); 39 | try { 40 | $this->process->run(function ($type, $data) use ($callback) { 41 | $files = array_unique(array_filter(explode("\n", $data))); 42 | if (!empty($this->matchRegexps)) { 43 | $files = array_filter($files, function ($file) { 44 | $filename = basename($file); 45 | foreach ($this->matchRegexps as $regex) { 46 | if (preg_match($regex, $filename)) { 47 | return true; 48 | } 49 | } 50 | return false; 51 | }); 52 | } 53 | if (!empty($files)) { 54 | $callback($files); 55 | } 56 | }); 57 | } catch (Throwable) { 58 | 59 | } 60 | } 61 | 62 | protected function getCommand() 63 | { 64 | $command = ["fswatch", "--format=%p", '-r', '--event=Created', '--event=Updated', '--event=Removed', '--event=Renamed']; 65 | 66 | return [...$command, ...$this->directory]; 67 | } 68 | 69 | public function stop() 70 | { 71 | if ($this->process) { 72 | $this->process->stop(); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/rpc/Protocol.php: -------------------------------------------------------------------------------- 1 | interface = $interface; 44 | $instance->method = $method; 45 | $instance->params = $params; 46 | $instance->context = $context; 47 | 48 | return $instance; 49 | } 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getInterface(): string 55 | { 56 | return $this->interface; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getMethod(): string 63 | { 64 | return $this->method; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getParams(): array 71 | { 72 | return $this->params; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | public function getContext(): array 79 | { 80 | return $this->context; 81 | } 82 | 83 | /** 84 | * @param string $interface 85 | */ 86 | public function setInterface(string $interface): void 87 | { 88 | $this->interface = $interface; 89 | } 90 | 91 | /** 92 | * @param string $method 93 | */ 94 | public function setMethod(string $method): void 95 | { 96 | $this->method = $method; 97 | } 98 | 99 | /** 100 | * @param array $params 101 | */ 102 | public function setParams(array $params): void 103 | { 104 | $this->params = $params; 105 | } 106 | 107 | /** 108 | * @param array $context 109 | */ 110 | public function setContext(array $context): void 111 | { 112 | $this->context = $context; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithRpcConnector.php: -------------------------------------------------------------------------------- 1 | recv()) { 22 | begin: 23 | if (empty($handler)) { 24 | [$handler, $data] = Packer::unpack($data); 25 | } 26 | 27 | $response = $handler->write($data); 28 | 29 | if (!empty($response)) { 30 | $handler = null; 31 | 32 | if ($response instanceof File) { 33 | $file = $response; 34 | } else { 35 | $result = $decoder($response); 36 | if ($result === Protocol::FILE) { 37 | $result = $file; 38 | } 39 | return $result; 40 | } 41 | } 42 | 43 | if (!empty($data)) { 44 | goto begin; 45 | } 46 | } 47 | 48 | if ($data === '') { 49 | throw new RpcClientException('Connection is closed. ' . $client->errMsg, $client->errCode); 50 | } 51 | if ($data === false) { 52 | throw new RpcClientException('Error receiving data, errno=' . $client->errCode . ' errmsg=' . swoole_strerror($client->errCode), $client->errCode); 53 | } 54 | } 55 | 56 | public function sendAndRecv($data, callable $decoder) 57 | { 58 | if (!$data instanceof Generator) { 59 | $data = [$data]; 60 | } 61 | 62 | return $this->runWithClient(function (Client $client) use ($decoder, $data) { 63 | try { 64 | foreach ($data as $string) { 65 | if (!empty($string)) { 66 | if ($client->send($string) === false) { 67 | throw new RpcClientException('Send data failed. ' . $client->errMsg, $client->errCode); 68 | } 69 | } 70 | } 71 | return $this->recv($client, $decoder); 72 | } catch (RpcClientException $e) { 73 | $client->close(); 74 | throw $e; 75 | } 76 | }); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithTracing.php: -------------------------------------------------------------------------------- 1 | container->config->get('tracing.tracers'); 21 | $hasAsync = false; 22 | foreach ($tracers as $name => $tracer) { 23 | if (Arr::get($tracer, 'async', false)) { 24 | $this->addWorker(function () use ($name) { 25 | $tracer = $this->app->make(Tracer::class)->tracer($name); 26 | 27 | $tracer->report(); 28 | }, "tracing [{$name}]"); 29 | $hasAsync = true; 30 | } 31 | } 32 | 33 | if ($hasAsync) { 34 | $this->onEvent('workerStart', function () { 35 | $this->bindTracingRedisPool(); 36 | $this->bindTracingRedisReporter(); 37 | }); 38 | } 39 | } 40 | } 41 | 42 | protected function bindTracingRedisReporter() 43 | { 44 | $this->getApplication()->bind(RedisReporter::class, function ($name) { 45 | 46 | $pool = $this->getPools()->get("tracing.redis"); 47 | 48 | $redis = new class($pool) { 49 | protected $pool; 50 | 51 | public function __construct($pool) 52 | { 53 | $this->pool = $pool; 54 | } 55 | 56 | public function __call($name, $arguments) 57 | { 58 | $client = $this->pool->borrow(); 59 | try { 60 | return call_user_func_array([$client, $name], $arguments); 61 | } finally { 62 | $this->pool->return($client); 63 | } 64 | } 65 | }; 66 | 67 | return new RedisReporter($name, $redis); 68 | }); 69 | } 70 | 71 | protected function bindTracingRedisPool() 72 | { 73 | $config = $this->container->config->get('tracing.redis'); 74 | 75 | $pool = new ConnectionPool( 76 | Pool::pullPoolConfig($config), 77 | new PhpRedisConnector(), 78 | $config 79 | ); 80 | $this->getPools()->add("tracing.redis", $pool); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/concerns/WithApplication.php: -------------------------------------------------------------------------------- 1 | app instanceof SwooleApp) { 31 | $this->app = new SwooleApp($this->container->getRootPath()); 32 | $this->app->setEnvName($envName); 33 | $this->app->bind(SwooleApp::class, App::class); 34 | $this->app->bind(Manager::class, $this); 35 | //绑定连接池 36 | if ($this->getConfig('pool.db.enable', true)) { 37 | $this->app->bind('db', Db::class); 38 | $this->app->resolving(Db::class, function (Db $db) { 39 | $db->setLog(function ($type, $log) { 40 | Container::getInstance()->make(Log::class)->log($type, $log); 41 | }); 42 | }); 43 | } 44 | if ($this->getConfig('pool.cache.enable', true)) { 45 | $this->app->bind('cache', Cache::class); 46 | } 47 | $this->app->initialize(); 48 | $this->app->instance('request', $this->container->request); 49 | $this->prepareConcretes(); 50 | } 51 | } 52 | 53 | /** 54 | * 预加载 55 | */ 56 | protected function prepareConcretes() 57 | { 58 | $defaultConcretes = ['db', 'cache', 'event']; 59 | 60 | $concretes = array_merge($defaultConcretes, $this->getConfig('concretes', [])); 61 | 62 | foreach ($concretes as $concrete) { 63 | $this->app->make($concrete); 64 | } 65 | } 66 | 67 | public function getApplication() 68 | { 69 | return $this->app; 70 | } 71 | 72 | /** 73 | * 获取沙箱 74 | * @return Sandbox 75 | */ 76 | protected function getSandbox() 77 | { 78 | return $this->app->make(Sandbox::class); 79 | } 80 | 81 | /** 82 | * 在沙箱中执行 83 | * @param Closure $callable 84 | */ 85 | public function runInSandbox(Closure $callable) 86 | { 87 | try { 88 | $this->getSandbox()->run($callable); 89 | } catch (Throwable $e) { 90 | $this->logServerError($e); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithQueue.php: -------------------------------------------------------------------------------- 1 | getConfig('queue.workers', []); 17 | 18 | foreach ($workers as $queue => $options) { 19 | 20 | if (strpos($queue, '@') !== false) { 21 | [$queue, $connection] = explode('@', $queue); 22 | } else { 23 | $connection = null; 24 | } 25 | 26 | $workerNum = Arr::get($options, 'worker_num', 1); 27 | 28 | $this->addBatchWorker($workerNum, function (Process\Pool $pool) use ($options, $connection, $queue) { 29 | $delay = Arr::get($options, 'delay', 0); 30 | $sleep = Arr::get($options, 'sleep', 3); 31 | $tries = Arr::get($options, 'tries', 0); 32 | $timeout = Arr::get($options, 'timeout', 60); 33 | 34 | /** @var Worker $worker */ 35 | $worker = $this->app->make(Worker::class); 36 | 37 | while (true) { 38 | $timer = Timer::after($timeout * 1000, function () use ($pool) { 39 | //关闭协程死锁检查 40 | Coroutine::set([ 41 | 'enable_deadlock_check' => false, 42 | ]); 43 | $pool->getProcess()->exit(); 44 | }); 45 | 46 | $this->runWithBarrier([$this, 'runInSandbox'], function () use ($connection, $queue, $delay, $sleep, $tries, $worker) { 47 | $worker->runNextJob($connection, $queue, $delay, $sleep, $tries); 48 | }); 49 | 50 | Timer::clear($timer); 51 | } 52 | }, "queue [$queue]"); 53 | } 54 | } 55 | 56 | public function prepareQueue() 57 | { 58 | if ($this->getConfig('queue.enable', false)) { 59 | $this->listenForEvents(); 60 | $this->createQueueWorkers(); 61 | } 62 | } 63 | 64 | /** 65 | * 注册事件 66 | */ 67 | protected function listenForEvents() 68 | { 69 | $this->container->event->listen(JobFailed::class, function (JobFailed $event) { 70 | $this->logFailedJob($event); 71 | }); 72 | } 73 | 74 | /** 75 | * 记录失败任务 76 | * @param JobFailed $event 77 | */ 78 | protected function logFailedJob(JobFailed $event) 79 | { 80 | $this->container['queue.failer']->log( 81 | $event->connection, 82 | $event->job->getQueue(), 83 | $event->job->getRawBody(), 84 | $event->exception 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/watcher/driver/Find.php: -------------------------------------------------------------------------------- 1 | directory = $config['directory']; 30 | $this->exclude = $config['exclude']; 31 | $this->name = $config['name']; 32 | } 33 | 34 | public function watch(callable $callback) 35 | { 36 | $ms = 2000; 37 | $seconds = ceil(($ms + 1000) / 1000); 38 | $minutes = sprintf('-%.2f', $seconds / 60); 39 | 40 | $dest = implode(' ', $this->directory); 41 | 42 | $name = empty($this->name) ? '' : ' \( ' . join(' -o ', array_map(fn($v) => "-name \"{$v}\"", $this->name)) . ' \)'; 43 | $notName = ''; 44 | $notPath = ''; 45 | if (!empty($this->exclude)) { 46 | $excludeDirs = $excludeFiles = []; 47 | foreach ($this->exclude as $directory) { 48 | $directory = rtrim($directory, '/'); 49 | if (is_dir($directory)) { 50 | $excludeDirs[] = $directory; 51 | } else { 52 | $excludeFiles[] = $directory; 53 | } 54 | } 55 | 56 | if (!empty($excludeFiles)) { 57 | $notPath = ' -not \( ' . join(' -and ', array_map(fn($v) => "-name \"{$v}\"", $excludeFiles)) . ' \)'; 58 | } 59 | 60 | if (!empty($excludeDirs)) { 61 | $notPath = ' -not \( ' . join(' -and ', array_map(fn($v) => "-path \"{$v}/*\"", $excludeDirs)) . ' \)'; 62 | } 63 | } 64 | 65 | $command = "find {$dest}{$name}{$notName}{$notPath} -mmin {$minutes} -type f -print"; 66 | 67 | $this->timer = Timer::tick($ms, function () use ($callback, $command) { 68 | $ret = System::exec($command); 69 | if ($ret['code'] === 0 && strlen($ret['output'])) { 70 | $stdout = trim($ret['output']); 71 | if (!empty($stdout)) { 72 | $files = array_filter(explode("\n", $stdout)); 73 | call_user_func($callback, $files); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | public function stop() 80 | { 81 | if ($this->timer) { 82 | Timer::clear($this->timer); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/pool/proxy/Store.php: -------------------------------------------------------------------------------- 1 | __call(__FUNCTION__, func_get_args()); 17 | } 18 | 19 | /** 20 | * @inheritDoc 21 | */ 22 | public function get($name, $default = null): mixed 23 | { 24 | return $this->__call(__FUNCTION__, func_get_args()); 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function set($name, $value, $expire = null): bool 31 | { 32 | return $this->__call(__FUNCTION__, func_get_args()); 33 | } 34 | 35 | /** 36 | * @inheritDoc 37 | */ 38 | public function inc($name, $step = 1) 39 | { 40 | return $this->__call(__FUNCTION__, func_get_args()); 41 | } 42 | 43 | /** 44 | * @inheritDoc 45 | */ 46 | public function dec($name, $step = 1) 47 | { 48 | return $this->__call(__FUNCTION__, func_get_args()); 49 | } 50 | 51 | /** 52 | * @inheritDoc 53 | */ 54 | public function delete($name): bool 55 | { 56 | return $this->__call(__FUNCTION__, func_get_args()); 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function clear(): bool 63 | { 64 | return $this->__call(__FUNCTION__, func_get_args()); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function clearTag($keys) 71 | { 72 | $this->__call(__FUNCTION__, func_get_args()); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function getMultiple($keys, $default = null): iterable 79 | { 80 | return $this->__call(__FUNCTION__, func_get_args()); 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function setMultiple($values, $ttl = null): bool 87 | { 88 | return $this->__call(__FUNCTION__, func_get_args()); 89 | } 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | public function deleteMultiple($keys): bool 95 | { 96 | return $this->__call(__FUNCTION__, func_get_args()); 97 | } 98 | 99 | /** 100 | * @inheritDoc 101 | */ 102 | public function tag($name): TagSet 103 | { 104 | return $this->__call(__FUNCTION__, func_get_args()); 105 | } 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | public function pull($name) 111 | { 112 | return $this->__call(__FUNCTION__, func_get_args()); 113 | } 114 | 115 | /** 116 | * @inheritDoc 117 | */ 118 | public function remember($name, $value, $expire = null) 119 | { 120 | return $this->__call(__FUNCTION__, func_get_args()); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/config/swoole.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'enable' => true, 6 | 'host' => '0.0.0.0', 7 | 'port' => 8080, 8 | 'worker_num' => swoole_cpu_num(), 9 | 'options' => [], 10 | ], 11 | 'websocket' => [ 12 | 'enable' => false, 13 | 'route' => true, 14 | 'handler' => \think\swoole\websocket\Handler::class, 15 | 'ping_interval' => 25000, 16 | 'ping_timeout' => 60000, 17 | 'room' => [ 18 | 'type' => 'table', 19 | 'table' => [ 20 | 'room_rows' => 8192, 21 | 'room_size' => 2048, 22 | 'client_rows' => 4096, 23 | 'client_size' => 2048, 24 | ], 25 | 'redis' => [ 26 | 'host' => '127.0.0.1', 27 | 'port' => 6379, 28 | 'max_active' => 3, 29 | 'max_wait_time' => 5, 30 | ], 31 | ], 32 | 'listen' => [], 33 | 'subscribe' => [], 34 | ], 35 | 'rpc' => [ 36 | 'server' => [ 37 | 'enable' => false, 38 | 'host' => '0.0.0.0', 39 | 'port' => 9000, 40 | 'worker_num' => swoole_cpu_num(), 41 | 'services' => [], 42 | ], 43 | 'client' => [], 44 | ], 45 | //队列 46 | 'queue' => [ 47 | 'enable' => false, 48 | 'workers' => [], 49 | ], 50 | 'hot_update' => [ 51 | 'enable' => env('APP_DEBUG', false), 52 | 'name' => ['*.php'], 53 | 'include' => [app_path()], 54 | 'exclude' => [], 55 | ], 56 | //连接池 57 | 'pool' => [ 58 | 'db' => [ 59 | 'enable' => true, 60 | 'max_active' => 3, 61 | 'max_wait_time' => 5, 62 | ], 63 | 'cache' => [ 64 | 'enable' => true, 65 | 'max_active' => 3, 66 | 'max_wait_time' => 5, 67 | ], 68 | //自定义连接池 69 | ], 70 | 'ipc' => [ 71 | 'type' => 'unix_socket', 72 | 'redis' => [ 73 | 'host' => '127.0.0.1', 74 | 'port' => 6379, 75 | 'max_active' => 3, 76 | 'max_wait_time' => 5, 77 | ], 78 | ], 79 | //锁 80 | 'lock' => [ 81 | 'enable' => false, 82 | 'type' => 'table', 83 | 'redis' => [ 84 | 'host' => '127.0.0.1', 85 | 'port' => 6379, 86 | 'max_active' => 3, 87 | 'max_wait_time' => 5, 88 | ], 89 | ], 90 | 'tables' => [], 91 | //每个worker里需要预加载以共用的实例 92 | 'concretes' => [], 93 | //重置器 94 | 'resetters' => [], 95 | //每次请求前需要清空的实例 96 | 'instances' => [], 97 | //每次请求前需要重新执行的服务 98 | 'services' => [], 99 | ]; 100 | -------------------------------------------------------------------------------- /src/pool/Proxy.php: -------------------------------------------------------------------------------- 1 | released = new WeakMap(); 35 | $this->disconnected = new WeakMap(); 36 | 37 | if ($connector instanceof Closure) { 38 | $connector = new Connector($connector); 39 | } 40 | 41 | if ($connector instanceof Connector) { 42 | $connector->setChecker(function ($connection) { 43 | return !isset($this->disconnected[$connection]); 44 | }); 45 | } 46 | 47 | $this->pool = new ConnectionPool( 48 | Pool::pullPoolConfig($config), 49 | $connector, 50 | $connectionConfig 51 | ); 52 | 53 | $this->pool->init(); 54 | } 55 | 56 | protected function getPoolConnection() 57 | { 58 | return Context::rememberData('connection.' . spl_object_id($this), function () { 59 | $connection = $this->pool->borrow(); 60 | 61 | $this->released[$connection] = false; 62 | 63 | Coroutine::defer(function () use ($connection) { 64 | //自动释放 65 | $this->releaseConnection($connection); 66 | }); 67 | 68 | return $connection; 69 | }); 70 | } 71 | 72 | protected function releaseConnection($connection) 73 | { 74 | if ($this->released[$connection] ?? false) { 75 | return; 76 | } 77 | $this->released[$connection] = true; 78 | $this->pool->return($connection); 79 | } 80 | 81 | public function release() 82 | { 83 | $connection = $this->getPoolConnection(); 84 | $this->releaseConnection($connection); 85 | } 86 | 87 | public function __call($method, $arguments) 88 | { 89 | $connection = $this->getPoolConnection(); 90 | if ($this->released[$connection] ?? false) { 91 | throw new RuntimeException('Connection already has been released!'); 92 | } 93 | 94 | try { 95 | return $connection->{$method}(...$arguments); 96 | } catch (Throwable $e) { 97 | $this->disconnected[$connection] = true; 98 | throw $e; 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/rpc/client/Proxy.php: -------------------------------------------------------------------------------- 1 | app = $app; 30 | $this->gateway = $gateway; 31 | $this->middleware = $middleware; 32 | } 33 | 34 | final protected function proxyCall($method, $params) 35 | { 36 | $protocol = Protocol::make($this->interface, $method, $params, $this->context); 37 | 38 | return Middleware::make($this->app, $this->middleware) 39 | ->pipeline() 40 | ->send($protocol) 41 | ->then(function (Protocol $protocol) { 42 | return $this->gateway->call($protocol); 43 | }); 44 | } 45 | 46 | final public function withContext($context): self 47 | { 48 | $this->context = $context; 49 | return $this; 50 | } 51 | 52 | final public static function getClassName($client, $interface) 53 | { 54 | if (!interface_exists($interface)) { 55 | throw new InvalidArgumentException( 56 | sprintf('%s must be exist interface!', $interface) 57 | ); 58 | } 59 | 60 | $proxyName = class_basename($interface) . "Service"; 61 | $className = "rpc\\service\\{$client}\\{$proxyName}"; 62 | 63 | if (!class_exists($className, false)) { 64 | 65 | $namespace = new PhpNamespace("rpc\\service\\{$client}"); 66 | $namespace->addUse(Proxy::class); 67 | $namespace->addUse($interface); 68 | 69 | $class = $namespace->addClass($proxyName); 70 | 71 | $class->setExtends(Proxy::class); 72 | $class->addImplement($interface); 73 | $class->addProperty('interface', class_basename($interface)); 74 | 75 | $reflection = new ReflectionClass($interface); 76 | 77 | foreach ($reflection->getMethods() as $methodRef) { 78 | if ($methodRef->getDeclaringClass()->name == Service::class) { 79 | continue; 80 | } 81 | $method = (new Factory)->fromMethodReflection($methodRef); 82 | $body = "\$this->proxyCall('{$methodRef->getName()}', func_get_args());"; 83 | if ($method->getReturnType() != 'void') { 84 | $body = "return {$body}"; 85 | } 86 | $method->setBody($body); 87 | $class->addMember($method); 88 | } 89 | 90 | eval($namespace); 91 | } 92 | return $className; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/response/File.php: -------------------------------------------------------------------------------- 1 | 'application/octet-stream', 17 | 'Accept-Ranges' => 'bytes', 18 | ]; 19 | 20 | /** 21 | * @var SplFileInfo 22 | */ 23 | protected $file; 24 | 25 | public function __construct($file, ?string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true) 26 | { 27 | $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified, $autoContentType); 28 | } 29 | 30 | public function getFile() 31 | { 32 | return $this->file; 33 | } 34 | 35 | public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true) 36 | { 37 | if (!$file instanceof SplFileInfo) { 38 | $file = new SplFileInfo((string) $file); 39 | } 40 | 41 | if (!$file->isReadable()) { 42 | throw new RuntimeException('File must be readable.'); 43 | } 44 | 45 | $this->header['Content-Length'] = $file->getSize(); 46 | 47 | $this->file = $file; 48 | 49 | if ($autoEtag) { 50 | $this->setAutoEtag(); 51 | } 52 | 53 | if ($autoLastModified) { 54 | $this->setAutoLastModified(); 55 | } 56 | 57 | if ($contentDisposition) { 58 | $this->setContentDisposition($contentDisposition); 59 | } 60 | 61 | if ($autoContentType) { 62 | $this->setAutoContentType(); 63 | } 64 | 65 | return $this; 66 | } 67 | 68 | public function setAutoContentType() 69 | { 70 | $finfo = finfo_open(FILEINFO_MIME_TYPE); 71 | 72 | $mimeType = finfo_file($finfo, $this->file->getPathname()); 73 | if ($mimeType) { 74 | $this->header['Content-Type'] = $mimeType; 75 | } 76 | } 77 | 78 | public function setContentDisposition(string $disposition, string $filename = '') 79 | { 80 | if ('' === $filename) { 81 | $filename = $this->file->getFilename(); 82 | } 83 | 84 | $this->header['Content-Disposition'] = "{$disposition}; filename=\"{$filename}\""; 85 | 86 | return $this; 87 | } 88 | 89 | public function setAutoLastModified() 90 | { 91 | $mTime = $this->file->getMTime(); 92 | if ($mTime) { 93 | $date = DateTime::createFromFormat('U', (string) $mTime); 94 | $this->lastModified($date->format('D, d M Y H:i:s') . ' GMT'); 95 | } 96 | return $this; 97 | } 98 | 99 | public function setAutoEtag() 100 | { 101 | $eTag = "W/\"" . sha1_file($this->file->getPathname()) . "\""; 102 | 103 | return $this->eTag($eTag); 104 | } 105 | 106 | protected function sendData(string $data): void 107 | { 108 | readfile($this->file->getPathname()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ipc/driver/UnixSocket.php: -------------------------------------------------------------------------------- 1 | getSocket($this->workerId), function (Coroutine\Socket $socket) { 35 | $data = $socket->recv(); 36 | $length = strlen($data); 37 | if ($length == self::HEADER_SIZE) { 38 | $header = unpack(self::HEADER_STRUCT, $data); 39 | if ($header) { 40 | $this->packets[$header['worker']] = new Buffer($header['length']); 41 | } 42 | } elseif ($length > self::HEADER_SIZE) { 43 | $header = unpack(self::HEADER_STRUCT, substr($data, 0, self::HEADER_SIZE)); 44 | if ($header && !empty($this->packets[$header['worker']])) { 45 | $packet = $this->packets[$header['worker']]; 46 | $data = substr($data, self::HEADER_SIZE); 47 | $response = $packet->write($data); 48 | if ($response) { 49 | unset($this->packets[$header['worker']]); 50 | Coroutine::create(function () use ($response) { 51 | try { 52 | $message = unserialize($response); 53 | $this->manager->triggerEvent('message', $message); 54 | } catch (Throwable $e) { 55 | $this->manager->logServerError($e); 56 | } 57 | }); 58 | } 59 | } 60 | } 61 | }); 62 | } 63 | 64 | public function publish($workerId, $message) 65 | { 66 | Barrier::run(function () use ($workerId, $message) { 67 | $socket = $this->getSocket($workerId); 68 | 69 | $data = serialize($message); 70 | 71 | $header = pack(self::HEADER_PACK, $workerId, strlen($data)); 72 | 73 | if (!$socket->send($header)) { 74 | return; 75 | } 76 | 77 | $dataSize = strlen($data); 78 | $chunkSize = 1024 * 32; 79 | $sendSize = 0; 80 | 81 | do { 82 | if (!$socket->send($header . substr($data, $sendSize, $chunkSize))) { 83 | break; 84 | } 85 | } while (($sendSize += $chunkSize) < $dataSize); 86 | }); 87 | } 88 | 89 | /** 90 | * @param $workerId 91 | * @return \Swoole\Coroutine\Socket 92 | */ 93 | protected function getSocket($workerId) 94 | { 95 | return $this->manager->getPool()->getProcess($workerId)->exportSocket(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/command/RpcInterface.php: -------------------------------------------------------------------------------- 1 | setName('rpc:interface') 22 | ->setDescription('Generate Rpc Service Interfaces'); 23 | } 24 | 25 | public function handle() 26 | { 27 | run(function () { 28 | $file = new PhpFile; 29 | $file->addComment('This file is auto-generated.'); 30 | $file->setStrictTypes(); 31 | $services = []; 32 | 33 | $clients = $this->app->config->get('swoole.rpc.client', []); 34 | 35 | foreach ($clients as $name => $config) { 36 | 37 | $parserClass = Arr::get($config, 'parser', JsonParser::class); 38 | /** @var ParserInterface $parser */ 39 | $parser = new $parserClass; 40 | 41 | $gateway = new Gateway($config, $parser, Arr::get($config, 'tries', 2)); 42 | 43 | $result = $gateway->getServices(); 44 | 45 | $namespace = $file->addNamespace("rpc\\contract\\{$name}"); 46 | 47 | $namespace->addUse(Service::class); 48 | 49 | foreach ($result as $interface => $methods) { 50 | 51 | $services[$name][] = $namespace->getName() . "\\{$interface}"; 52 | 53 | $class = $namespace->addInterface($interface); 54 | 55 | $class->addExtend(Service::class); 56 | 57 | foreach ($methods as $methodName => ['parameters' => $parameters, 'returnType' => $returnType, 'comment' => $comment]) { 58 | $method = $class->addMethod($methodName) 59 | ->setVisibility(ClassType::VISIBILITY_PUBLIC) 60 | ->setComment(Helpers::unformatDocComment($comment)) 61 | ->setReturnType($returnType); 62 | 63 | foreach ($parameters as $parameter) { 64 | if ($parameter['type'] && (class_exists($parameter['type']) || interface_exists($parameter['type']))) { 65 | $namespace->addUse($parameter['type']); 66 | } 67 | $param = $method->addParameter($parameter['name']) 68 | ->setType($parameter['type']); 69 | 70 | if (array_key_exists('default', $parameter)) { 71 | $param->setDefaultValue($parameter['default']); 72 | } 73 | 74 | if (array_key_exists('nullable', $parameter)) { 75 | $param->setNullable(); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | $dumper = new Dumper(); 83 | 84 | $services = 'return ' . $dumper->dump($services) . ';'; 85 | 86 | file_put_contents($this->app->getBasePath() . 'rpc.php', $file . $services); 87 | 88 | $this->output->writeln('Succeed!'); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithRpcClient.php: -------------------------------------------------------------------------------- 1 | onEvent('workerStart', function () { 27 | $this->bindRpcClientPool(); 28 | $this->bindRpcInterface(); 29 | }); 30 | } 31 | 32 | protected function bindRpcInterface() 33 | { 34 | //引入rpc接口文件 35 | if (file_exists($rpc = $this->container->getBasePath() . 'rpc.php')) { 36 | 37 | $rpcServices = (array) include $rpc; 38 | 39 | //绑定rpc接口 40 | try { 41 | foreach ($rpcServices as $name => $abstracts) { 42 | $parserClass = $this->getConfig("rpc.client.{$name}.parser", JsonParser::class); 43 | $tries = $this->getConfig("rpc.client.{$name}.tries", 2); 44 | $middleware = $this->getConfig("rpc.client.{$name}.middleware", []); 45 | 46 | $parser = $this->getApplication()->make($parserClass); 47 | $gateway = new Gateway($this->createRpcConnector($name), $parser, $tries); 48 | 49 | foreach ($abstracts as $abstract) { 50 | $this->getApplication() 51 | ->bind($abstract, function (App $app) use ($middleware, $gateway, $name, $abstract) { 52 | return $app->invokeClass(Proxy::getClassName($name, $abstract), [$gateway, $middleware]); 53 | }); 54 | } 55 | } 56 | } catch (Throwable $e) { 57 | } 58 | } 59 | } 60 | 61 | protected function bindRpcClientPool() 62 | { 63 | if (!empty($clients = $this->getConfig('rpc.client'))) { 64 | //创建client连接池 65 | foreach ($clients as $name => $config) { 66 | $pool = new ConnectionPool( 67 | Pool::pullPoolConfig($config), 68 | new Client(), 69 | $config 70 | ); 71 | $this->getPools()->add("rpc.client.{$name}", $pool); 72 | } 73 | } 74 | } 75 | 76 | protected function createRpcConnector($name) 77 | { 78 | $pool = $this->getPools()->get("rpc.client.{$name}"); 79 | 80 | return new class($pool) implements Connector { 81 | 82 | use InteractsWithRpcConnector; 83 | 84 | protected $pool; 85 | 86 | public function __construct(ConnectionPool $pool) 87 | { 88 | $this->pool = $pool; 89 | } 90 | 91 | protected function runWithClient($callback) 92 | { 93 | /** @var \Swoole\Coroutine\Client $client */ 94 | $client = $this->pool->borrow(); 95 | try { 96 | return $callback($client); 97 | } finally { 98 | $this->pool->return($client); 99 | } 100 | } 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/websocket/socketio/Packet.php: -------------------------------------------------------------------------------- 1 | type = $type; 53 | } 54 | 55 | public static function create($type, array $decoded = []) 56 | { 57 | $new = new self($type); 58 | $new->id = $decoded['id'] ?? null; 59 | if (isset($decoded['nsp'])) { 60 | $new->nsp = $decoded['nsp'] ?: '/'; 61 | } else { 62 | $new->nsp = '/'; 63 | } 64 | $new->data = $decoded['data'] ?? null; 65 | return $new; 66 | } 67 | 68 | public function toString() 69 | { 70 | $str = '' . $this->type; 71 | if ($this->nsp && '/' !== $this->nsp) { 72 | $str .= $this->nsp . ','; 73 | } 74 | 75 | if ($this->id !== null) { 76 | $str .= $this->id; 77 | } 78 | 79 | if (null !== $this->data) { 80 | $str .= json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 81 | } 82 | return $str; 83 | } 84 | 85 | public static function fromString(string $str) 86 | { 87 | $i = 0; 88 | 89 | $packet = new Packet((int) substr($str, 0, 1)); 90 | 91 | // look up namespace (if any) 92 | if ('/' === substr($str, $i + 1, 1)) { 93 | $nsp = ''; 94 | while (++$i) { 95 | $c = substr($str, $i, 1); 96 | if (',' === $c) { 97 | break; 98 | } 99 | $nsp .= $c; 100 | if ($i === strlen($str)) { 101 | break; 102 | } 103 | } 104 | $packet->nsp = $nsp; 105 | } else { 106 | $packet->nsp = '/'; 107 | } 108 | 109 | // look up id 110 | $next = substr($str, $i + 1, 1); 111 | if ('' !== $next && is_numeric($next)) { 112 | $id = ''; 113 | while (++$i) { 114 | $c = substr($str, $i, 1); 115 | if (null == $c || !is_numeric($c)) { 116 | --$i; 117 | break; 118 | } 119 | $id .= substr($str, $i, 1); 120 | if ($i === strlen($str)) { 121 | break; 122 | } 123 | } 124 | $packet->id = intval($id); 125 | } 126 | 127 | // look up json data 128 | if (substr($str, ++$i, 1)) { 129 | $packet->data = json_decode(substr($str, $i), true); 130 | } 131 | 132 | return $packet; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/rpc/JsonParser.php: -------------------------------------------------------------------------------- 1 | getInterface(); 26 | $methodName = $protocol->getMethod(); 27 | 28 | $method = $interface . self::DELIMITER . $methodName; 29 | $data = [ 30 | 'jsonrpc' => self::VERSION, 31 | 'method' => $method, 32 | 'params' => $protocol->getParams(), 33 | 'context' => $protocol->getContext(), 34 | 'id' => '', 35 | ]; 36 | 37 | return json_encode($data, JSON_UNESCAPED_UNICODE); 38 | } 39 | 40 | /** 41 | * @param string $string 42 | * 43 | * @return Protocol 44 | */ 45 | public function decode(string $string): Protocol 46 | { 47 | $data = json_decode($string, true); 48 | 49 | $error = json_last_error(); 50 | if ($error != JSON_ERROR_NONE) { 51 | throw new Exception( 52 | sprintf('Data(%s) is not json format!', $string), 53 | Dispatcher::PARSER_ERROR 54 | ); 55 | } 56 | 57 | $method = $data['method'] ?? ''; 58 | $params = $data['params'] ?? []; 59 | $context = $data['context'] ?? []; 60 | 61 | if (empty($method)) { 62 | throw new Exception( 63 | sprintf('Method(%s) cant not be empty!', $string), 64 | Dispatcher::INVALID_PARAMS 65 | ); 66 | } 67 | 68 | $methodAry = explode(self::DELIMITER, $method); 69 | if (count($methodAry) < 2) { 70 | throw new Exception( 71 | sprintf('Method(%s) is bad format!', $method), 72 | Dispatcher::INVALID_PARAMS 73 | ); 74 | } 75 | 76 | [$interfaceClass, $methodName] = $methodAry; 77 | 78 | if (empty($interfaceClass) || empty($methodName)) { 79 | throw new Exception( 80 | sprintf('Interface(%s) or Method(%s) can not be empty!', $interfaceClass, $method), 81 | Dispatcher::INVALID_PARAMS 82 | ); 83 | } 84 | 85 | return Protocol::make($interfaceClass, $methodName, $params, $context); 86 | } 87 | 88 | /** 89 | * @param string $string 90 | * 91 | * @return mixed 92 | */ 93 | public function decodeResponse(string $string) 94 | { 95 | $data = json_decode($string, true); 96 | 97 | if (array_key_exists('result', $data)) { 98 | return $data['result']; 99 | } 100 | 101 | $code = $data['error']['code'] ?? 0; 102 | $message = $data['error']['message'] ?? ''; 103 | $data = $data['error']['data'] ?? null; 104 | 105 | return Error::make($code, $message, $data); 106 | } 107 | 108 | /** 109 | * @param mixed $result 110 | * 111 | * @return string 112 | */ 113 | public function encodeResponse($result): string 114 | { 115 | $data = [ 116 | 'jsonrpc' => self::VERSION, 117 | 'id' => '', 118 | ]; 119 | 120 | if ($result instanceof Error) { 121 | $data['error'] = $result; 122 | } else { 123 | $data['result'] = $result; 124 | } 125 | 126 | return json_encode($data); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Websocket.php: -------------------------------------------------------------------------------- 1 | app = $app; 51 | $this->room = $room; 52 | $this->event = $event; 53 | } 54 | 55 | /** 56 | * @return Pusher 57 | */ 58 | protected function makePusher() 59 | { 60 | return $this->app->invokeClass(Pusher::class); 61 | } 62 | 63 | public function to(...$values) 64 | { 65 | return $this->makePusher()->to(...$values); 66 | } 67 | 68 | public function push($data) 69 | { 70 | $this->makePusher()->to($this->getSender())->push($data); 71 | } 72 | 73 | public function emit(string $event, ...$data) 74 | { 75 | $this->makePusher()->to($this->getSender())->emit($event, ...$data); 76 | } 77 | 78 | /** 79 | * Join sender to multiple rooms. 80 | * 81 | * @param string|integer|array $rooms 82 | * 83 | * @return $this 84 | */ 85 | public function join($rooms): self 86 | { 87 | $rooms = is_string($rooms) || is_int($rooms) ? func_get_args() : $rooms; 88 | 89 | $this->room->add($this->getSender(), $rooms); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Make sender leave multiple rooms. 96 | * 97 | * @param array|string|integer $rooms 98 | * 99 | * @return $this 100 | */ 101 | public function leave($rooms = []): self 102 | { 103 | $rooms = is_string($rooms) || is_int($rooms) ? func_get_args() : $rooms; 104 | 105 | $this->room->delete($this->getSender(), $rooms); 106 | 107 | return $this; 108 | } 109 | 110 | public function setConnected($connected) 111 | { 112 | $this->connected = $connected; 113 | } 114 | 115 | public function isEstablished() 116 | { 117 | return $this->connected; 118 | } 119 | 120 | /** 121 | * Close current connection. 122 | */ 123 | public function close() 124 | { 125 | if ($this->client) { 126 | $this->client->close(); 127 | } 128 | } 129 | 130 | /** 131 | * @param Response $response 132 | */ 133 | public function setClient($response) 134 | { 135 | $this->client = $response; 136 | } 137 | 138 | /** 139 | * Set sender fd. 140 | * 141 | * @param string $fd 142 | * 143 | * @return $this 144 | */ 145 | public function setSender(string $fd) 146 | { 147 | $this->sender = $fd; 148 | return $this; 149 | } 150 | 151 | /** 152 | * Get current sender fd. 153 | */ 154 | public function getSender() 155 | { 156 | if (empty($this->sender)) { 157 | throw new RuntimeException('Cannot use websocket as current client before handshake!'); 158 | } 159 | return $this->sender; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ThinkPHP Swoole 扩展 2 | =============== 3 | 4 | 交流群:787100169 [![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://jq.qq.com/?_wv=1027&k=VRcdnUKL) 5 | 6 | ## 安装 7 | 8 | 首先按照Swoole官网说明安装swoole扩展,然后使用 9 | 10 | ~~~ 11 | composer require topthink/think-swoole 12 | ~~~ 13 | 14 | 安装swoole扩展。 15 | 16 | ## 使用方法 17 | 18 | 直接在命令行下启动HTTP服务端。 19 | 20 | ~~~ 21 | php think swoole 22 | ~~~ 23 | 24 | 启动完成后,默认会在0.0.0.0:8080启动一个HTTP Server,可以直接访问当前的应用。 25 | 26 | swoole的相关参数可以在`config/swoole.php`里面配置(具体参考配置文件内容)。 27 | 28 | 如果需要使用守护进程方式运行,建议使用supervisor来管理进程 29 | 30 | ## 访问静态文件 31 | > 4.0开始协程风格服务端默认不支持静态文件访问,建议使用nginx来支持静态文件访问,也可使用路由输出文件内容,下面是示例,可参照修改 32 | 1. 添加静态文件路由: 33 | 34 | ```php 35 | Route::get('static/:path', function (string $path) { 36 | $filename = public_path() . $path; 37 | return new \think\swoole\response\File($filename); 38 | })->pattern(['path' => '.*\.\w+$']); 39 | ``` 40 | 41 | 2. 访问路由 `http://localhost/static/文件路径` 42 | 43 | ## 队列支持 44 | 45 | > 4.0开始协程风格服务端没有task进程了,使用think-queue代替 46 | 47 | 使用方法见 [think-queue](https://github.com/top-think/think-queue) 48 | 49 | 以下配置代替think-queue里的最后一步:`监听任务并执行`,无需另外起进程执行队列 50 | 51 | ```php 52 | return [ 53 | // ... 54 | 'queue' => [ 55 | 'enable' => true, 56 | //键名是队列名称 57 | 'workers' => [ 58 | //下面参数是不设置时的默认配置 59 | 'default' => [ 60 | 'delay' => 0, 61 | 'sleep' => 3, 62 | 'tries' => 0, 63 | 'timeout' => 60, 64 | 'worker_num' => 1, 65 | ], 66 | //使用@符号后面可指定队列使用驱动 67 | 'default@connection' => [ 68 | //此处可不设置任何参数,使用上面的默认配置 69 | ], 70 | ], 71 | ], 72 | // ... 73 | ]; 74 | 75 | ``` 76 | 77 | ### websocket 78 | 79 | > 新增路由调度的方式,方便实现多个websocket服务 80 | 81 | #### 配置 82 | 83 | ``` 84 | swoole.websocket.route = true 时开启 85 | ``` 86 | 87 | #### 路由定义 88 | ```php 89 | Route::get('path1','controller/action1'); 90 | Route::get('path2','controller/action2'); 91 | ``` 92 | 93 | #### 控制器 94 | 95 | ```php 96 | use \think\swoole\Websocket; 97 | use \think\swoole\websocket\Event; 98 | use \Swoole\WebSocket\Frame; 99 | use \think\swoole\websocket\Room; 100 | 101 | class Controller { 102 | 103 | public function action1(){//不可以在这里注入websocket对象 104 | 105 | return \think\swoole\helper\websocket() 106 | ->onOpen(...) 107 | ->onMessage(function(Websocket $websocket, Frame $frame){ //只可在事件响应这里注入websocket对象 108 | //... 109 | $websocket->join('room_key'); //将当前连接加入到某个room,后续可以向该room发送消息 这个room里的都可以收到 110 | //比如room_key可以直接使用这个用户的id,然后其他地方需要给某个用户发送消息,直接向这个room发送消息即可 111 | //... 112 | $websocket->push('message'); //给当前连接发送消息 113 | //... 114 | $websocket->emit('event_name', 'message'); //给当前连接发送事件 115 | //... 116 | $websocket->to('room_key')->push('message'); //给指定room的所有连接发送消息 在http请求的控制器中也可以注入Websocket对象这样发消息 117 | //... 118 | }) 119 | ->onClose(...); 120 | } 121 | 122 | public function action2(){ 123 | 124 | return \think\swoole\helper\websocket() 125 | ->onOpen(...) 126 | ->onMessage(function(Websocket $websocket, Frame $frame){ 127 | //... 128 | }) 129 | ->onClose(...); 130 | } 131 | } 132 | ``` 133 | 134 | ### 流式输出 135 | 136 | ```php 137 | 138 | class Controller { 139 | 140 | public function action(){ 141 | return \think\swoole\helper\iterator(value(function(){ 142 | foreach(range(1,10) as $i) 143 | yield $i; 144 | sleep(1);//模拟等待 145 | } 146 | })); 147 | } 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithRpcServer.php: -------------------------------------------------------------------------------- 1 | bindRpcParser(); 29 | $this->bindRpcDispatcher(); 30 | 31 | $host = $this->getConfig('rpc.server.host', '0.0.0.0'); 32 | $port = $this->getConfig('rpc.server.port', 9000); 33 | 34 | $server = new Server($host, $port, false, true); 35 | 36 | $server->handle(function (Connection $conn) { 37 | 38 | $this->runInSandbox(function (App $app, Dispatcher $dispatcher) use ($conn) { 39 | $files = []; 40 | while (true) { 41 | //接收数据 42 | $data = $conn->recv(); 43 | 44 | if ($data === '' || $data === false) { 45 | break; 46 | } 47 | begin: 48 | if (!isset($handler)) { 49 | try { 50 | [$handler, $data] = Packer::unpack($data); 51 | } catch (Throwable $e) { 52 | //错误的包头 53 | $result = Error::make(Dispatcher::INVALID_REQUEST, $e->getMessage()); 54 | $this->runWithBarrier(function () use ($dispatcher, $app, $conn, $result) { 55 | $dispatcher->dispatch($app, $conn, $result); 56 | }); 57 | break; 58 | } 59 | } 60 | 61 | $result = $handler->write($data); 62 | 63 | if (!empty($result)) { 64 | $handler = null; 65 | if ($result instanceof File) { 66 | $files[] = $result; 67 | } else { 68 | $this->runWithBarrier(function () use ($dispatcher, $app, $conn, $result, $files) { 69 | $dispatcher->dispatch($app, $conn, $result, $files); 70 | }); 71 | $files = []; 72 | } 73 | } 74 | 75 | if (!empty($data)) { 76 | goto begin; 77 | } 78 | } 79 | 80 | $conn->close(); 81 | }); 82 | }); 83 | 84 | $server->start(); 85 | } 86 | 87 | protected function prepareRpcServer() 88 | { 89 | if ($this->getConfig('rpc.server.enable', false)) { 90 | 91 | $workerNum = $this->getConfig('rpc.server.worker_num', 1); 92 | 93 | $this->addBatchWorker($workerNum, [$this, 'createRpcServer'], 'rpc server'); 94 | } 95 | } 96 | 97 | protected function bindRpcDispatcher() 98 | { 99 | $services = $this->getConfig('rpc.server.services', []); 100 | $middleware = $this->getConfig('rpc.server.middleware', []); 101 | 102 | $this->app->make(Dispatcher::class, [$services, $middleware]); 103 | } 104 | 105 | protected function bindRpcParser() 106 | { 107 | $parserClass = $this->getConfig('rpc.server.parser', JsonParser::class); 108 | 109 | $this->app->bind(ParserInterface::class, $parserClass); 110 | $this->app->make(ParserInterface::class); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/coroutine/Context.php: -------------------------------------------------------------------------------- 1 | offsetGet($key); 41 | } 42 | return $default; 43 | } 44 | 45 | /** 46 | * 判断是否存在临时数据 47 | * @param string $key 48 | * @return bool 49 | */ 50 | public static function hasData(string $key) 51 | { 52 | return self::getDataObject()->offsetExists($key); 53 | } 54 | 55 | /** 56 | * 写入临时数据 57 | * @param string $key 58 | * @param $value 59 | */ 60 | public static function setData(string $key, $value) 61 | { 62 | self::getDataObject()->offsetSet($key, $value); 63 | } 64 | 65 | /** 66 | * 删除数据 67 | * @param string $key 68 | */ 69 | public static function removeData(string $key) 70 | { 71 | if (self::hasData($key)) { 72 | self::getDataObject()->offsetUnset($key); 73 | } 74 | } 75 | 76 | /** 77 | * 如果不存在则写入数据 78 | * @param string $key 79 | * @param $value 80 | * @return mixed|null 81 | */ 82 | public static function rememberData(string $key, $value) 83 | { 84 | if (self::hasData($key)) { 85 | return self::getData($key); 86 | } 87 | 88 | if ($value instanceof Closure) { 89 | // 获取缓存数据 90 | $value = $value(); 91 | } 92 | 93 | self::setData($key, $value); 94 | 95 | return $value; 96 | } 97 | 98 | /** 99 | * @internal 100 | * 清空数据 101 | */ 102 | public static function clear() 103 | { 104 | self::getDataObject()->exchangeArray([]); 105 | } 106 | 107 | /** 108 | * 获取当前协程ID 109 | * @return mixed 110 | * @deprecated 111 | */ 112 | public static function getCoroutineId() 113 | { 114 | return Coroutine::getCid(); 115 | } 116 | 117 | /** 118 | * 获取当前协程ID 119 | * @return mixed 120 | */ 121 | public static function getId() 122 | { 123 | return Coroutine::getCid(); 124 | } 125 | 126 | /** 127 | * 获取父级协程ID 128 | * @param int $id 129 | * @return mixed 130 | */ 131 | public static function getPid($id = 0) 132 | { 133 | if (self::get($id)->offsetExists('#pid')) { 134 | return self::get($id)->offsetGet('#pid'); 135 | } 136 | return Coroutine::getPcid($id); 137 | } 138 | 139 | /** 140 | * 绑定父级协程ID 141 | * @param $id 142 | */ 143 | public static function attach($id) 144 | { 145 | self::get()->offsetSet('#pid', $id); 146 | } 147 | 148 | /** 149 | * 获取根协程ID 150 | * @param bool $init 151 | * @return mixed 152 | */ 153 | public static function getRootId($init = false) 154 | { 155 | if ($init) { 156 | self::get()->offsetSet('#root', true); 157 | return self::getId(); 158 | } else { 159 | $cid = self::getId(); 160 | while (!self::get($cid)->offsetExists('#root')) { 161 | $cid = self::getPid($cid); 162 | 163 | if ($cid < 1) { 164 | break; 165 | } 166 | } 167 | 168 | return $cid; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/rpc/client/Gateway.php: -------------------------------------------------------------------------------- 1 | createDefaultConnector($connector); 39 | } 40 | $this->connector = $connector; 41 | $this->parser = $parser; 42 | $this->tries = $tries; 43 | } 44 | 45 | protected function encodeData(Protocol $protocol) 46 | { 47 | $params = $protocol->getParams(); 48 | 49 | //有文件,先传输 50 | foreach ($params as $index => $param) { 51 | if ($param instanceof File) { 52 | yield from $this->fread($param); 53 | $params[$index] = Protocol::FILE; 54 | } 55 | } 56 | 57 | $protocol->setParams($params); 58 | 59 | $data = $this->parser->encode($protocol); 60 | 61 | yield Packer::pack($data); 62 | } 63 | 64 | protected function decodeResponse($response) 65 | { 66 | $result = $this->parser->decodeResponse($response); 67 | 68 | if ($result instanceof Error) { 69 | throw new RpcResponseException($result); 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | protected function sendAndRecv($data) 76 | { 77 | return $this->connector->sendAndRecv($data, Closure::fromCallable([$this, 'decodeResponse'])); 78 | } 79 | 80 | public function call(Protocol $protocol) 81 | { 82 | if ($this->tries > 1) { 83 | $result = backoff(function () use ($protocol) { 84 | try { 85 | return $this->sendAndRecv($this->encodeData($protocol)); 86 | } catch (RpcResponseException $e) { 87 | return $e; 88 | } 89 | }, $this->tries); 90 | 91 | if ($result instanceof RpcResponseException) { 92 | throw $result; 93 | } 94 | 95 | return $result; 96 | } else { 97 | return $this->sendAndRecv($this->encodeData($protocol)); 98 | } 99 | } 100 | 101 | public function getServices() 102 | { 103 | return $this->sendAndRecv(Packer::pack(Protocol::ACTION_INTERFACE)); 104 | } 105 | 106 | protected function createDefaultConnector($config) 107 | { 108 | return new class($config) implements Connector { 109 | 110 | use InteractsWithRpcConnector; 111 | 112 | /** @var Client */ 113 | protected $client; 114 | protected $config; 115 | 116 | /** 117 | * constructor. 118 | * @param array $config 119 | */ 120 | public function __construct($config) 121 | { 122 | $this->config = $config; 123 | } 124 | 125 | protected function isConnected(): bool 126 | { 127 | return $this->client && $this->client->isConnected() && $this->client->peek() !== ''; 128 | } 129 | 130 | protected function getClient() 131 | { 132 | if (!$this->isConnected()) { 133 | $client = new Client(SWOOLE_SOCK_TCP); 134 | 135 | $config = $this->config; 136 | 137 | $host = Arr::pull($config, 'host'); 138 | $port = Arr::pull($config, 'port'); 139 | $timeout = Arr::pull($config, 'timeout', 5); 140 | 141 | $client->set($config); 142 | 143 | if (!$client->connect($host, $port, $timeout)) { 144 | throw new RpcClientException( 145 | sprintf('Connect failed host=%s port=%d', $host, $port) 146 | ); 147 | } 148 | 149 | $this->client = $client; 150 | } 151 | return $this->client; 152 | } 153 | 154 | protected function runWithClient($callback) 155 | { 156 | return $callback($this->getClient()); 157 | } 158 | }; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithServer.php: -------------------------------------------------------------------------------- 1 | addWorker($func, $name ? "{$name} #{$i}" : null); 41 | } 42 | return $this; 43 | } 44 | 45 | public function addWorker(callable $func, $name = null): self 46 | { 47 | $this->startFuncMap[] = [$func, $name]; 48 | return $this; 49 | } 50 | 51 | /** 52 | * 启动服务 53 | * @param string $envName 环境变量标识 54 | */ 55 | public function start(string $envName): void 56 | { 57 | $this->setProcessName('manager process'); 58 | 59 | //协程配置 60 | Coroutine::set($this->getConfig('coroutine', [])); 61 | 62 | $this->initialize(); 63 | $this->triggerEvent('init'); 64 | 65 | //热更新 66 | if ($this->getConfig('hot_update.enable', false)) { 67 | $this->addHotUpdateProcess(); 68 | } 69 | 70 | $workerNum = count($this->startFuncMap); 71 | 72 | $pool = $this->createPool($workerNum); 73 | 74 | $pool->on(Constant::EVENT_WORKER_START, function ($pool, $workerId) use ($envName) { 75 | 76 | Runtime::enableCoroutine(); 77 | 78 | $this->pool = $pool; 79 | $this->workerId = $workerId; 80 | 81 | [$func, $name] = $this->startFuncMap[$workerId]; 82 | 83 | if ($name) { 84 | $this->setProcessName($name); 85 | } 86 | 87 | $this->clearCache(); 88 | $this->prepareApplication($envName); 89 | 90 | $this->ipc->listenMessage($workerId); 91 | 92 | Process::signal(SIGTERM, function () { 93 | $this->stopWorker(); 94 | }); 95 | 96 | $this->onEvent('message', function ($message) { 97 | if ($message instanceof ReloadMessage) { 98 | $this->stopWorker(); 99 | } 100 | }); 101 | 102 | $this->triggerEvent(Constant::EVENT_WORKER_START, $name); 103 | 104 | $func($pool, $workerId); 105 | }); 106 | 107 | $pool->start(); 108 | } 109 | 110 | protected function stopWorker() 111 | { 112 | $this->triggerEvent('beforeWorkerStop'); 113 | $this->pool->getProcess()->exit(); 114 | } 115 | 116 | public function getWorkerId() 117 | { 118 | return $this->workerId; 119 | } 120 | 121 | /** 122 | * 获取当前工作进程池对象 123 | * @return Pool 124 | */ 125 | public function getPool() 126 | { 127 | return $this->pool; 128 | } 129 | 130 | public function sendMessage($workerId, $message) 131 | { 132 | $this->ipc->sendMessage($workerId, $message); 133 | } 134 | 135 | protected function createPool($workerNum) 136 | { 137 | $this->ipc = $this->container->make(Ipc::class); 138 | 139 | $pool = new Pool($workerNum, $this->ipc->getType(), 0, true); 140 | 141 | $this->ipc->prepare($pool); 142 | 143 | return $pool; 144 | } 145 | 146 | public function runWithBarrier(callable $func, ...$params) 147 | { 148 | Barrier::run($func, ...$params); 149 | } 150 | 151 | /** 152 | * 热更新 153 | */ 154 | protected function addHotUpdateProcess() 155 | { 156 | //热更新时关闭协程死锁检查 157 | Coroutine::set([ 158 | 'enable_deadlock_check' => false, 159 | ]); 160 | 161 | $this->addWorker(function () { 162 | $watcher = $this->container->make(Watcher::class); 163 | $watcher->watch(function () { 164 | foreach ($this->startFuncMap as $workerId => $func) { 165 | if ($workerId != $this->workerId) { 166 | $this->sendMessage($workerId, new ReloadMessage); 167 | } 168 | } 169 | }); 170 | }, 'hot update'); 171 | } 172 | 173 | /** 174 | * 清除apc、op缓存 175 | */ 176 | protected function clearCache() 177 | { 178 | if (extension_loaded('apc')) { 179 | apc_clear_cache(); 180 | } 181 | 182 | if (extension_loaded('Zend OPcache')) { 183 | opcache_reset(); 184 | } 185 | } 186 | 187 | /** 188 | * Set process name. 189 | * 190 | * @param $process 191 | */ 192 | protected function setProcessName($process) 193 | { 194 | $appName = $this->container->config->get('app.name', 'ThinkPHP'); 195 | 196 | $name = sprintf('swoole: %s process for %s', $process, $appName); 197 | 198 | @cli_set_process_title($name); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/websocket/room/Table.php: -------------------------------------------------------------------------------- 1 | 8192, 16 | 'room_size' => 2048, 17 | 'client_rows' => 4096, 18 | 'client_size' => 2048, 19 | ]; 20 | 21 | /** 22 | * @var SwooleTable 23 | */ 24 | protected $rooms; 25 | 26 | /** 27 | * @var SwooleTable 28 | */ 29 | protected $fds; 30 | 31 | /** 32 | * TableRoom constructor. 33 | * 34 | * @param array $config 35 | */ 36 | public function __construct(array $config) 37 | { 38 | $this->config = array_merge($this->config, $config); 39 | } 40 | 41 | /** 42 | * Do some init stuffs before workers started. 43 | * 44 | * @return RoomInterface 45 | */ 46 | public function prepare(): RoomInterface 47 | { 48 | $this->initRoomsTable(); 49 | $this->initFdsTable(); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Add multiple socket fds to a room. 56 | * 57 | * @param string $fd 58 | * @param array|string $roomNames 59 | */ 60 | public function add($fd, $roomNames) 61 | { 62 | $rooms = $this->getRooms($fd); 63 | $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; 64 | 65 | foreach ($roomNames as $room) { 66 | $fds = $this->getClients($room); 67 | 68 | if (in_array($fd, $fds)) { 69 | continue; 70 | } 71 | 72 | $fds[] = $fd; 73 | $rooms[] = $room; 74 | 75 | $this->setClients($room, $fds); 76 | } 77 | 78 | $this->setRooms($fd, $rooms); 79 | } 80 | 81 | /** 82 | * Delete multiple socket fds from a room. 83 | * 84 | * @param string $fd 85 | * @param array|string $roomNames 86 | */ 87 | public function delete($fd, $roomNames = []) 88 | { 89 | $allRooms = $this->getRooms($fd); 90 | $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; 91 | $rooms = count($roomNames) ? $roomNames : $allRooms; 92 | 93 | $removeRooms = []; 94 | foreach ($rooms as $room) { 95 | $fds = $this->getClients($room); 96 | 97 | if (!in_array($fd, $fds)) { 98 | continue; 99 | } 100 | 101 | $this->setClients($room, array_values(array_diff($fds, [$fd]))); 102 | $removeRooms[] = $room; 103 | } 104 | 105 | $this->setRooms($fd, collect($allRooms)->diff($removeRooms)->values()->toArray()); 106 | } 107 | 108 | /** 109 | * Get all sockets by a room key. 110 | * 111 | * @param string $room 112 | * 113 | * @return array 114 | */ 115 | public function getClients(string $room) 116 | { 117 | return $this->getValue($room, RoomInterface::ROOMS_KEY) ?? []; 118 | } 119 | 120 | /** 121 | * Get all rooms by a fd. 122 | * 123 | * @param string $fd 124 | * 125 | * @return array 126 | */ 127 | public function getRooms($fd) 128 | { 129 | return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?? []; 130 | } 131 | 132 | /** 133 | * @param string $room 134 | * @param array $fds 135 | * 136 | * @return $this 137 | */ 138 | protected function setClients(string $room, array $fds) 139 | { 140 | return $this->setValue($room, $fds, RoomInterface::ROOMS_KEY); 141 | } 142 | 143 | /** 144 | * @param string $fd 145 | * @param array $rooms 146 | * 147 | * @return $this 148 | */ 149 | protected function setRooms($fd, array $rooms) 150 | { 151 | return $this->setValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); 152 | } 153 | 154 | /** 155 | * Init rooms table 156 | */ 157 | protected function initRoomsTable(): void 158 | { 159 | $this->rooms = new SwooleTable($this->config['room_rows']); 160 | $this->rooms->column('value', SwooleTable::TYPE_STRING, $this->config['room_size']); 161 | $this->rooms->create(); 162 | } 163 | 164 | /** 165 | * Init descriptors table 166 | */ 167 | protected function initFdsTable() 168 | { 169 | $this->fds = new SwooleTable($this->config['client_rows']); 170 | $this->fds->column('value', SwooleTable::TYPE_STRING, $this->config['client_size']); 171 | $this->fds->create(); 172 | } 173 | 174 | /** 175 | * Set value to table 176 | * 177 | * @param $key 178 | * @param array $value 179 | * @param string $table 180 | * 181 | * @return $this 182 | */ 183 | public function setValue($key, array $value, string $table) 184 | { 185 | $this->checkTable($table); 186 | 187 | if (empty($value)) { 188 | $this->$table->del($key); 189 | } else { 190 | $this->$table->set($key, ['value' => json_encode($value)]); 191 | } 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get value from table 198 | * 199 | * @param string $key 200 | * @param string $table 201 | * 202 | * @return array|mixed 203 | */ 204 | public function getValue(string $key, string $table) 205 | { 206 | $this->checkTable($table); 207 | 208 | $value = $this->$table->get($key); 209 | 210 | return $value ? json_decode($value['value'], true) : []; 211 | } 212 | 213 | /** 214 | * Check table for exists 215 | * 216 | * @param string $table 217 | */ 218 | protected function checkTable(string $table) 219 | { 220 | if (!property_exists($this, $table) || !$this->$table instanceof SwooleTable) { 221 | throw new InvalidArgumentException("Invalid table name: `{$table}`."); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/pool/proxy/Connection.php: -------------------------------------------------------------------------------- 1 | __call(__FUNCTION__, func_get_args()); 27 | } 28 | 29 | /** 30 | * 连接数据库方法 31 | * @access public 32 | * @param array $config 接参数 33 | * @param integer $linkNum 连接序号 34 | * @return mixed 35 | */ 36 | public function connect(array $config = [], $linkNum = 0) 37 | { 38 | return $this->__call(__FUNCTION__, func_get_args()); 39 | } 40 | 41 | /** 42 | * 设置当前的数据库Db对象 43 | * @access public 44 | * @param DbManager $db 45 | * @return void 46 | */ 47 | public function setDb(DbManager $db) 48 | { 49 | $this->__call(__FUNCTION__, func_get_args()); 50 | } 51 | 52 | /** 53 | * 设置当前的缓存对象 54 | * @access public 55 | * @param CacheInterface $cache 56 | * @return void 57 | */ 58 | public function setCache(CacheInterface $cache) 59 | { 60 | $this->__call(__FUNCTION__, func_get_args()); 61 | } 62 | 63 | /** 64 | * 获取数据库的配置参数 65 | * @access public 66 | * @param string $config 配置名称 67 | * @return mixed 68 | */ 69 | public function getConfig(string $config = '') 70 | { 71 | return $this->__call(__FUNCTION__, func_get_args()); 72 | } 73 | 74 | /** 75 | * 关闭数据库(或者重新连接) 76 | * @access public 77 | */ 78 | public function close() 79 | { 80 | return $this->__call(__FUNCTION__, func_get_args()); 81 | } 82 | 83 | /** 84 | * 查找单条记录 85 | * @access public 86 | * @param BaseQuery $query 查询对象 87 | * @return array 88 | */ 89 | public function find(BaseQuery $query): array 90 | { 91 | return $this->__call(__FUNCTION__, func_get_args()); 92 | } 93 | 94 | /** 95 | * 查找记录 96 | * @access public 97 | * @param BaseQuery $query 查询对象 98 | * @return array 99 | */ 100 | public function select(BaseQuery $query): array 101 | { 102 | return $this->__call(__FUNCTION__, func_get_args()); 103 | } 104 | 105 | /** 106 | * 插入记录 107 | * @access public 108 | * @param BaseQuery $query 查询对象 109 | * @param boolean $getLastInsID 返回自增主键 110 | * @return mixed 111 | */ 112 | public function insert(BaseQuery $query, bool $getLastInsID = false) 113 | { 114 | return $this->__call(__FUNCTION__, func_get_args()); 115 | } 116 | 117 | /** 118 | * 批量插入记录 119 | * @access public 120 | * @param BaseQuery $query 查询对象 121 | * @param array $dataSet 数据集 122 | * @return integer 123 | * @throws \Exception 124 | * @throws \Throwable 125 | */ 126 | public function insertAll(BaseQuery $query, array $dataSet = []): int 127 | { 128 | return $this->__call(__FUNCTION__, func_get_args()); 129 | } 130 | 131 | /** 132 | * 更新记录 133 | * @access public 134 | * @param BaseQuery $query 查询对象 135 | * @return integer 136 | */ 137 | public function update(BaseQuery $query): int 138 | { 139 | return $this->__call(__FUNCTION__, func_get_args()); 140 | } 141 | 142 | /** 143 | * 删除记录 144 | * @access public 145 | * @param BaseQuery $query 查询对象 146 | * @return int 147 | */ 148 | public function delete(BaseQuery $query): int 149 | { 150 | return $this->__call(__FUNCTION__, func_get_args()); 151 | } 152 | 153 | /** 154 | * 得到某个字段的值 155 | * @access public 156 | * @param BaseQuery $query 查询对象 157 | * @param string $field 字段名 158 | * @param mixed $default 默认值 159 | * @return mixed 160 | */ 161 | public function value(BaseQuery $query, string $field, $default = null) 162 | { 163 | return $this->__call(__FUNCTION__, func_get_args()); 164 | } 165 | 166 | /** 167 | * 得到某个列的数组 168 | * @access public 169 | * @param BaseQuery $query 查询对象 170 | * @param string|array $column 字段名 多个字段用逗号分隔 171 | * @param string $key 索引 172 | * @return array 173 | */ 174 | public function column(BaseQuery $query, $column, string $key = ''): array 175 | { 176 | return $this->__call(__FUNCTION__, func_get_args()); 177 | } 178 | 179 | /** 180 | * 执行数据库事务 181 | * @access public 182 | * @param callable $callback 数据操作方法回调 183 | * @return mixed 184 | * @throws \Throwable 185 | */ 186 | public function transaction(callable $callback) 187 | { 188 | return $this->__call(__FUNCTION__, func_get_args()); 189 | } 190 | 191 | /** 192 | * 启动事务 193 | * @access public 194 | * @return void 195 | * @throws \Exception 196 | */ 197 | public function startTrans() 198 | { 199 | $this->__call(__FUNCTION__, func_get_args()); 200 | } 201 | 202 | /** 203 | * 用于非自动提交状态下面的查询提交 204 | * @access public 205 | * @return void 206 | */ 207 | public function commit() 208 | { 209 | $this->__call(__FUNCTION__, func_get_args()); 210 | } 211 | 212 | /** 213 | * 事务回滚 214 | * @access public 215 | * @return void 216 | */ 217 | public function rollback() 218 | { 219 | $this->__call(__FUNCTION__, func_get_args()); 220 | } 221 | 222 | /** 223 | * 获取最近一次查询的sql语句 224 | * @access public 225 | * @return string 226 | */ 227 | public function getLastSql(): string 228 | { 229 | return $this->__call(__FUNCTION__, func_get_args()); 230 | } 231 | 232 | public function table($table) 233 | { 234 | return $this->__call(__FUNCTION__, func_get_args()); 235 | } 236 | 237 | public function name($name) 238 | { 239 | return $this->__call(__FUNCTION__, func_get_args()); 240 | } 241 | 242 | public function getLastInsID(BaseQuery $query, ?string $sequence = null) 243 | { 244 | return $this->__call(__FUNCTION__, func_get_args()); 245 | } 246 | 247 | public function getTableFields(string $tableName): array 248 | { 249 | return $this->__call(__FUNCTION__, func_get_args()); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/websocket/socketio/Handler.php: -------------------------------------------------------------------------------- 1 | event = $event; 35 | $this->config = $config; 36 | $this->websocket = $websocket; 37 | $this->pingInterval = $this->config->get('swoole.websocket.ping_interval', 25000); 38 | $this->pingTimeout = $this->config->get('swoole.websocket.ping_timeout', 60000); 39 | } 40 | 41 | /** 42 | * "onOpen" listener. 43 | * 44 | * @param Request $request 45 | */ 46 | public function onOpen(Request $request) 47 | { 48 | $this->eio = $request->param('EIO'); 49 | 50 | $payload = json_encode( 51 | [ 52 | 'sid' => base64_encode(uniqid()), 53 | 'upgrades' => [], 54 | 'pingInterval' => $this->pingInterval, 55 | 'pingTimeout' => $this->pingTimeout, 56 | ] 57 | ); 58 | 59 | $this->push(EnginePacket::open($payload)); 60 | 61 | $this->event->trigger('swoole.websocket.Open', $request); 62 | 63 | if ($this->eio < 4) { 64 | $this->resetPingTimeout($this->pingInterval + $this->pingTimeout); 65 | $this->onConnect(); 66 | } else { 67 | $this->schedulePing(); 68 | } 69 | } 70 | 71 | /** 72 | * "onMessage" listener. 73 | * 74 | * @param Frame $frame 75 | */ 76 | public function onMessage(Frame $frame) 77 | { 78 | $enginePacket = EnginePacket::fromString($frame->data); 79 | 80 | $this->event->trigger('swoole.websocket.Message', $enginePacket); 81 | 82 | $this->resetPingTimeout($this->pingInterval + $this->pingTimeout); 83 | 84 | switch ($enginePacket->type) { 85 | case EnginePacket::MESSAGE: 86 | $packet = Packet::fromString($enginePacket->data); 87 | switch ($packet->type) { 88 | case Packet::CONNECT: 89 | $this->onConnect($packet->data); 90 | break; 91 | case Packet::EVENT: 92 | $type = array_shift($packet->data); 93 | $data = $packet->data; 94 | $result = $this->event->trigger('swoole.websocket.Event', new WsEvent($type, $data)); 95 | 96 | if ($packet->id !== null) { 97 | $responsePacket = Packet::create(Packet::ACK, [ 98 | 'id' => $packet->id, 99 | 'nsp' => $packet->nsp, 100 | 'data' => $result, 101 | ]); 102 | 103 | $this->push($responsePacket); 104 | } 105 | break; 106 | case Packet::DISCONNECT: 107 | $this->event->trigger('swoole.websocket.Disconnect'); 108 | $this->websocket->close(); 109 | break; 110 | default: 111 | $this->websocket->close(); 112 | break; 113 | } 114 | break; 115 | case EnginePacket::PING: 116 | $this->event->trigger('swoole.websocket.Ping'); 117 | $this->push(EnginePacket::pong($enginePacket->data)); 118 | break; 119 | case EnginePacket::PONG: 120 | $this->event->trigger('swoole.websocket.Pong'); 121 | $this->schedulePing(); 122 | break; 123 | default: 124 | $this->websocket->close(); 125 | break; 126 | } 127 | } 128 | 129 | /** 130 | * "onClose" listener. 131 | */ 132 | public function onClose() 133 | { 134 | Timer::clear($this->pingTimeoutTimer); 135 | Timer::clear($this->pingIntervalTimer); 136 | $this->event->trigger('swoole.websocket.Close'); 137 | } 138 | 139 | protected function onConnect($data = null) 140 | { 141 | try { 142 | $this->event->trigger('swoole.websocket.Connect', $data); 143 | $packet = Packet::create(Packet::CONNECT); 144 | if ($this->eio >= 4) { 145 | $packet->data = ['sid' => base64_encode(uniqid())]; 146 | } 147 | } catch (Exception $exception) { 148 | $packet = Packet::create(Packet::CONNECT_ERROR, [ 149 | 'data' => ['message' => $exception->getMessage()], 150 | ]); 151 | } 152 | 153 | $this->push($packet); 154 | } 155 | 156 | protected function resetPingTimeout($timeout) 157 | { 158 | Timer::clear($this->pingTimeoutTimer); 159 | $this->pingTimeoutTimer = Timer::after($timeout, function () { 160 | $this->websocket->close(); 161 | }); 162 | } 163 | 164 | protected function schedulePing() 165 | { 166 | Timer::clear($this->pingIntervalTimer); 167 | $this->pingIntervalTimer = Timer::after($this->pingInterval, function () { 168 | $this->push(EnginePacket::ping()); 169 | $this->resetPingTimeout($this->pingTimeout); 170 | }); 171 | } 172 | 173 | public function encodeMessage($message) 174 | { 175 | if ($message instanceof WsEvent) { 176 | $message = Packet::create(Packet::EVENT, [ 177 | 'data' => array_merge([$message->type], $message->data), 178 | ]); 179 | } 180 | 181 | if ($message instanceof Packet) { 182 | $message = EnginePacket::message($message->toString()); 183 | } 184 | 185 | if ($message instanceof EnginePacket) { 186 | $message = $message->toString(); 187 | } 188 | 189 | return $message; 190 | } 191 | 192 | protected function push($data) 193 | { 194 | $this->websocket->push($data); 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/Sandbox.php: -------------------------------------------------------------------------------- 1 | setBaseApp($app); 47 | $this->initialize(); 48 | } 49 | 50 | public function setBaseApp(App $app) 51 | { 52 | $this->app = $app; 53 | 54 | return $this; 55 | } 56 | 57 | public function getBaseApp() 58 | { 59 | return $this->app; 60 | } 61 | 62 | protected function initialize() 63 | { 64 | Container::setInstance(function () { 65 | return $this->getApplication(); 66 | }); 67 | 68 | $this->setInitialConfig(); 69 | $this->setInitialServices(); 70 | $this->setInitialEvent(); 71 | $this->setInitialResetters(); 72 | 73 | return $this; 74 | } 75 | 76 | public function run(Closure $callable) 77 | { 78 | $this->init(); 79 | $app = $this->getApplication(); 80 | try { 81 | $app->invoke($callable, [$this]); 82 | } catch (Throwable $e) { 83 | $app->make(Handle::class)->report($e); 84 | } finally { 85 | $this->clear(); 86 | } 87 | } 88 | 89 | public function init() 90 | { 91 | $app = $this->getApplication(true); 92 | $this->setInstance($app); 93 | $this->resetApp($app); 94 | } 95 | 96 | public function clear() 97 | { 98 | if ($app = $this->getSnapshot()) { 99 | $app->clearInstances(); 100 | unset($this->snapshots[$this->getSnapshotId()]); 101 | } 102 | 103 | Context::clear(); 104 | $this->setInstance($this->getBaseApp()); 105 | } 106 | 107 | public function getApplication($init = false) 108 | { 109 | $snapshot = $this->getSnapshot($init); 110 | if ($snapshot instanceof App) { 111 | return $snapshot; 112 | } 113 | 114 | if ($init) { 115 | $snapshot = clone $this->getBaseApp(); 116 | $this->setSnapshot($snapshot); 117 | 118 | return $snapshot; 119 | } 120 | throw new InvalidArgumentException('The app object has not been initialized'); 121 | } 122 | 123 | protected function getSnapshotId($init = false) 124 | { 125 | return Context::getRootId($init); 126 | } 127 | 128 | /** 129 | * Get current snapshot. 130 | * @return App|null 131 | */ 132 | public function getSnapshot($init = false) 133 | { 134 | return $this->snapshots[$this->getSnapshotId($init)] ?? null; 135 | } 136 | 137 | public function setSnapshot(App $snapshot) 138 | { 139 | $this->snapshots[$this->getSnapshotId()] = $snapshot; 140 | 141 | return $this; 142 | } 143 | 144 | public function setInstance(App $app) 145 | { 146 | $app->instance('app', $app); 147 | $app->instance(Container::class, $app); 148 | 149 | $reflectObject = new ReflectionObject($app); 150 | $reflectProperty = $reflectObject->getProperty('services'); 151 | $reflectProperty->setAccessible(true); 152 | $services = $reflectProperty->getValue($app); 153 | 154 | foreach ($services as $service) { 155 | $this->modifyProperty($service, $app); 156 | } 157 | } 158 | 159 | /** 160 | * Set initial config. 161 | */ 162 | protected function setInitialConfig() 163 | { 164 | $this->config = clone $this->getBaseApp()->config; 165 | } 166 | 167 | protected function setInitialEvent() 168 | { 169 | $this->event = clone $this->getBaseApp()->event; 170 | } 171 | 172 | /** 173 | * Get config snapshot. 174 | */ 175 | public function getConfig() 176 | { 177 | return $this->config; 178 | } 179 | 180 | public function getEvent() 181 | { 182 | return $this->event; 183 | } 184 | 185 | public function getServices() 186 | { 187 | return $this->services; 188 | } 189 | 190 | protected function setInitialServices() 191 | { 192 | $app = $this->getBaseApp(); 193 | 194 | $services = $this->config->get('swoole.services', []); 195 | 196 | foreach ($services as $service) { 197 | if (class_exists($service) && !in_array($service, $this->services)) { 198 | $serviceObj = new $service($app); 199 | $this->services[$service] = $serviceObj; 200 | } 201 | } 202 | } 203 | 204 | /** 205 | * Initialize resetters. 206 | */ 207 | protected function setInitialResetters() 208 | { 209 | $app = $this->getBaseApp(); 210 | 211 | $resetters = [ 212 | ClearInstances::class, 213 | ResetConfig::class, 214 | ResetEvent::class, 215 | ResetService::class, 216 | ResetModel::class, 217 | ResetPaginator::class, 218 | ]; 219 | 220 | $resetters = array_merge($resetters, $this->config->get('swoole.resetters', [])); 221 | 222 | foreach ($resetters as $resetter) { 223 | $resetterClass = $app->make($resetter); 224 | if (!$resetterClass instanceof ResetterInterface) { 225 | throw new RuntimeException("{$resetter} must implement " . ResetterInterface::class); 226 | } 227 | $this->resetters[$resetter] = $resetterClass; 228 | } 229 | } 230 | 231 | /** 232 | * Reset Application. 233 | * 234 | * @param App $app 235 | */ 236 | protected function resetApp(App $app) 237 | { 238 | foreach ($this->resetters as $resetter) { 239 | $resetter->handle($app, $this); 240 | } 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/websocket/room/Redis.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 44 | $this->config = $config; 45 | 46 | if ($prefix = Arr::get($this->config, 'prefix')) { 47 | $this->prefix = $prefix; 48 | } 49 | } 50 | 51 | /** 52 | * @return RoomInterface 53 | */ 54 | public function prepare(): RoomInterface 55 | { 56 | $this->initData(); 57 | $this->prepareRedis(); 58 | return $this; 59 | } 60 | 61 | protected function prepareRedis() 62 | { 63 | $this->manager->onEvent('workerStart', function () { 64 | $config = $this->config; 65 | $this->pool = new ConnectionPool( 66 | Pool::pullPoolConfig($config), 67 | new PhpRedisConnector(), 68 | $config 69 | ); 70 | $this->manager->getPools()->add('websocket.room', $this->pool); 71 | }); 72 | } 73 | 74 | protected function initData() 75 | { 76 | $connector = new PhpRedisConnector(); 77 | 78 | $connection = $connector->connect($this->config); 79 | 80 | if (count($keys = $connection->keys("{$this->prefix}*"))) { 81 | $connection->del($keys); 82 | } 83 | 84 | $connector->disconnect($connection); 85 | } 86 | 87 | /** 88 | * Add multiple socket fds to a room. 89 | * 90 | * @param string $fd 91 | * @param array|string $roomNames 92 | */ 93 | public function add($fd, $roomNames) 94 | { 95 | $rooms = is_array($roomNames) ? $roomNames : [$roomNames]; 96 | 97 | $this->addValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); 98 | 99 | foreach ($rooms as $room) { 100 | $this->addValue($room, [$fd], RoomInterface::ROOMS_KEY); 101 | } 102 | } 103 | 104 | /** 105 | * Delete multiple socket fds from a room. 106 | * 107 | * @param string $fd 108 | * @param array|string $rooms 109 | */ 110 | public function delete($fd, $rooms) 111 | { 112 | $rooms = is_array($rooms) ? $rooms : [$rooms]; 113 | $rooms = count($rooms) ? $rooms : $this->getRooms($fd); 114 | 115 | $this->removeValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); 116 | 117 | foreach ($rooms as $room) { 118 | $this->removeValue($room, [$fd], RoomInterface::ROOMS_KEY); 119 | } 120 | } 121 | 122 | protected function runWithRedis(\Closure $callable) 123 | { 124 | $redis = $this->pool->borrow(); 125 | try { 126 | return $callable($redis); 127 | } finally { 128 | $this->pool->return($redis); 129 | } 130 | } 131 | 132 | /** 133 | * Add value to redis. 134 | * 135 | * @param $key 136 | * @param array $values 137 | * @param string $table 138 | * 139 | * @return $this 140 | */ 141 | protected function addValue($key, array $values, string $table) 142 | { 143 | $this->checkTable($table); 144 | $redisKey = $this->getKey($key, $table); 145 | 146 | $this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) { 147 | $pipe = $redis->multi(PHPRedis::PIPELINE); 148 | 149 | foreach ($values as $value) { 150 | $pipe->sadd($redisKey, $value); 151 | } 152 | 153 | $pipe->exec(); 154 | }); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * Remove value from redis. 161 | * 162 | * @param $key 163 | * @param array $values 164 | * @param string $table 165 | * 166 | * @return $this 167 | */ 168 | protected function removeValue($key, array $values, string $table) 169 | { 170 | $this->checkTable($table); 171 | $redisKey = $this->getKey($key, $table); 172 | 173 | $this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) { 174 | $pipe = $redis->multi(PHPRedis::PIPELINE); 175 | foreach ($values as $value) { 176 | $pipe->srem($redisKey, $value); 177 | } 178 | $pipe->exec(); 179 | }); 180 | 181 | return $this; 182 | } 183 | 184 | /** 185 | * Get all sockets by a room key. 186 | * 187 | * @param string $room 188 | * 189 | * @return array 190 | */ 191 | public function getClients(string $room) 192 | { 193 | return $this->getValue($room, RoomInterface::ROOMS_KEY) ?: []; 194 | } 195 | 196 | /** 197 | * Get all rooms by a fd. 198 | * 199 | * @param string $fd 200 | * 201 | * @return array 202 | */ 203 | public function getRooms($fd) 204 | { 205 | return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?: []; 206 | } 207 | 208 | /** 209 | * Check table for rooms and descriptors. 210 | * 211 | * @param string $table 212 | */ 213 | protected function checkTable(string $table) 214 | { 215 | if (!in_array($table, [RoomInterface::ROOMS_KEY, RoomInterface::DESCRIPTORS_KEY])) { 216 | throw new InvalidArgumentException("Invalid table name: `{$table}`."); 217 | } 218 | } 219 | 220 | /** 221 | * Get value. 222 | * 223 | * @param string $key 224 | * @param string $table 225 | * 226 | * @return array 227 | */ 228 | protected function getValue(string $key, string $table) 229 | { 230 | $this->checkTable($table); 231 | 232 | return $this->runWithRedis(function (PHPRedis $redis) use ($table, $key) { 233 | return $redis->smembers($this->getKey($key, $table)); 234 | }); 235 | } 236 | 237 | /** 238 | * Get key. 239 | * 240 | * @param string $key 241 | * @param string $table 242 | * 243 | * @return string 244 | */ 245 | protected function getKey(string $key, string $table) 246 | { 247 | return "{$this->prefix}{$table}:{$key}"; 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /src/rpc/server/Dispatcher.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 58 | $this->prepareServices($services); 59 | $this->middleware = $middleware; 60 | } 61 | 62 | /** 63 | * 获取服务接口 64 | * @param $services 65 | * @throws ReflectionException 66 | */ 67 | protected function prepareServices($services) 68 | { 69 | foreach ($services as $className) { 70 | $reflectionClass = new ReflectionClass($className); 71 | $interfaces = $reflectionClass->getInterfaceNames(); 72 | if (!empty($interfaces)) { 73 | foreach ($interfaces as $interface) { 74 | $this->services[class_basename($interface)] = [ 75 | 'interface' => $interface, 76 | 'class' => $className, 77 | ]; 78 | } 79 | } else { 80 | $this->services[class_basename($className)] = [ 81 | 'interface' => $className, 82 | 'class' => $className, 83 | ]; 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * 获取接口信息 90 | * @return array 91 | */ 92 | protected function getInterfaces() 93 | { 94 | $interfaces = []; 95 | foreach ($this->services as $key => ['interface' => $interface]) { 96 | $interfaces[$key] = $this->getMethods($interface); 97 | } 98 | return $interfaces; 99 | } 100 | 101 | protected function getMethods($interface) 102 | { 103 | $methods = []; 104 | 105 | $reflection = new ReflectionClass($interface); 106 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 107 | if ($method->isConstructor() || $method->isDestructor()) { 108 | continue; 109 | } 110 | $returnType = $method->getReturnType(); 111 | if ($returnType instanceof ReflectionNamedType) { 112 | $returnType = $returnType->getName(); 113 | } 114 | $methods[$method->getName()] = [ 115 | 'parameters' => $this->getParameters($method), 116 | 'returnType' => $returnType, 117 | 'comment' => $method->getDocComment(), 118 | ]; 119 | } 120 | return $methods; 121 | } 122 | 123 | protected function getParameters(ReflectionMethod $method) 124 | { 125 | $parameters = []; 126 | foreach ($method->getParameters() as $parameter) { 127 | $type = $parameter->getType(); 128 | if ($type instanceof ReflectionNamedType) { 129 | $type = $type->getName(); 130 | } 131 | $param = [ 132 | 'name' => $parameter->getName(), 133 | 'type' => $type, 134 | ]; 135 | 136 | if ($parameter->isOptional()) { 137 | $param['default'] = $parameter->getDefaultValue(); 138 | } 139 | 140 | if ($parameter->allowsNull()) { 141 | $param['nullable'] = true; 142 | } 143 | 144 | $parameters[] = $param; 145 | } 146 | return $parameters; 147 | } 148 | 149 | /** 150 | * 调度 151 | * @param App $app 152 | * @param Connection $conn 153 | * @param string|Error $data 154 | * @param array $files 155 | */ 156 | public function dispatch(App $app, Connection $conn, $data, $files = []) 157 | { 158 | try { 159 | switch (true) { 160 | case $data instanceof Error: 161 | $result = $data; 162 | break; 163 | case $data === Protocol::ACTION_INTERFACE: 164 | $result = $this->getInterfaces(); 165 | break; 166 | default: 167 | $protocol = $this->parser->decode($data); 168 | $result = $this->dispatchWithMiddleware($app, $protocol, $files); 169 | } 170 | } catch (Throwable $e) { 171 | $result = Error::make($e->getCode(), $e->getMessage()); 172 | } 173 | 174 | //传输文件 175 | if ($result instanceof \think\File) { 176 | foreach ($this->fread($result) as $string) { 177 | if (!empty($string)) { 178 | $conn->send($string); 179 | } 180 | } 181 | $result = Protocol::FILE; 182 | } 183 | 184 | $data = $this->parser->encodeResponse($result); 185 | 186 | $conn->send(Packer::pack($data)); 187 | } 188 | 189 | protected function dispatchWithMiddleware(App $app, Protocol $protocol, $files) 190 | { 191 | $interface = $protocol->getInterface(); 192 | $method = $protocol->getMethod(); 193 | $params = $protocol->getParams(); 194 | 195 | //文件参数 196 | foreach ($params as $index => $param) { 197 | if ($param === Protocol::FILE) { 198 | $params[$index] = array_shift($files); 199 | } 200 | } 201 | 202 | $service = $this->services[$interface] ?? null; 203 | if (empty($service)) { 204 | throw new RuntimeException( 205 | sprintf('Service %s is not founded!', $interface), 206 | self::METHOD_NOT_FOUND 207 | ); 208 | } 209 | 210 | $instance = $app->make($service['class']); 211 | $middlewares = array_merge($this->middleware, $this->getServiceMiddlewares($instance, $method)); 212 | 213 | return Middleware::make($app, $middlewares) 214 | ->pipeline() 215 | ->send($protocol) 216 | ->then(function () use ($instance, $method, $params) { 217 | return call_user_func_array([$instance, $method], $params); 218 | }); 219 | } 220 | 221 | protected function getServiceMiddlewares($service, $method) 222 | { 223 | $middlewares = []; 224 | 225 | $class = new ReflectionClass($service); 226 | 227 | if ($class->hasProperty('middleware')) { 228 | $reflectionProperty = $class->getProperty('middleware'); 229 | $reflectionProperty->setAccessible(true); 230 | 231 | foreach ($reflectionProperty->getValue($service) as $key => $val) { 232 | if (!is_int($key)) { 233 | $middleware = $key; 234 | $options = $val; 235 | } elseif (isset($val['middleware'])) { 236 | $middleware = $val['middleware']; 237 | $options = $val['options'] ?? []; 238 | } else { 239 | $middleware = $val; 240 | $options = []; 241 | } 242 | 243 | if ((isset($options['only']) && !in_array($method, (array) $options['only'])) || 244 | (!empty($options['except']) && in_array($method, (array) $options['except']))) { 245 | continue; 246 | } 247 | 248 | if (is_string($middleware) && strpos($middleware, ':')) { 249 | $middleware = explode(':', $middleware); 250 | if (count($middleware) > 1) { 251 | $middleware = [$middleware[0], array_slice($middleware, 1)]; 252 | } 253 | } 254 | 255 | $middlewares[] = $middleware; 256 | } 257 | } 258 | 259 | return $middlewares; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/concerns/InteractsWithWebsocket.php: -------------------------------------------------------------------------------- 1 | runInSandbox(function (App $app, Http $http, Event $event) use ($req, $res) { 58 | $request = $this->prepareRequest($req); 59 | 60 | //路由调度 61 | $useRoute = $this->getConfig('websocket.route', false); 62 | if ($useRoute) { 63 | $response = $http->run($request); 64 | if (!$response instanceof \think\swoole\response\Websocket) { 65 | $res->close(); 66 | return; 67 | } 68 | $event->subscribe([$response]); 69 | } else { 70 | $request = $this->setRequestThroughMiddleware($app, $request); 71 | } 72 | 73 | //自动处理Sec-WebSocket-Protocol 74 | if ($protocol = $request->header('sec-websocket-protocol')) { 75 | $res->header('sec-websocket-protocol', $protocol); 76 | } 77 | 78 | $res->upgrade(); 79 | 80 | $websocket = $app->make(Websocket::class, [], true); 81 | $app->instance(Websocket::class, $websocket); 82 | 83 | $websocket->setClient($res); 84 | 85 | $fd = $this->wsIdAtomic->add(); 86 | 87 | $this->wsMessageChannel[$fd] = new Channel(1); 88 | 89 | Coroutine::create(function () use ($websocket, $res, $fd) { 90 | //推送消息 91 | while ($message = $this->wsMessageChannel[$fd]->pop()) { 92 | $websocket->setConnected($res->push($message)); 93 | } 94 | }); 95 | 96 | try { 97 | $id = "{$this->workerId}.{$fd}"; 98 | 99 | $websocket->setSender($id); 100 | $websocket->join($id); 101 | 102 | $handler = $app->make(HandlerInterface::class); 103 | 104 | $this->runWithBarrier(function () use ($request, $handler) { 105 | try { 106 | $handler->onOpen($request); 107 | } catch (Throwable $e) { 108 | $this->logServerError($e); 109 | } 110 | }); 111 | 112 | $this->runWithBarrier(function () use ($handler, $res) { 113 | 114 | $cid = Coroutine::getCid(); 115 | $messages = 0; 116 | $wait = false; 117 | 118 | $frame = null; 119 | while (true) { 120 | /** @var Frame|false|string $recv */ 121 | $recv = $res->recv(); 122 | if ($recv === '' || $recv === false || $recv instanceof CloseFrame) { 123 | break; 124 | } 125 | 126 | if (empty($frame)) { 127 | $frame = new Frame(); 128 | $frame->opcode = $recv->opcode; 129 | $frame->flags = $recv->flags; 130 | $frame->fd = $recv->fd; 131 | $frame->finish = false; 132 | } 133 | 134 | $frame->data .= $recv->data; 135 | 136 | $frame->finish = $recv->finish; 137 | 138 | if ($frame->finish) { 139 | Coroutine::create(function () use (&$wait, &$messages, $cid, $frame, $handler) { 140 | ++$messages; 141 | Coroutine::defer(function () use (&$wait, &$messages, $cid) { 142 | --$messages; 143 | if ($wait) { 144 | Coroutine::resume($cid); 145 | } 146 | }); 147 | try { 148 | $handler->onMessage($frame); 149 | } catch (Throwable $e) { 150 | $this->logServerError($e); 151 | } 152 | }); 153 | $frame = null; 154 | } 155 | } 156 | 157 | //等待消息执行完毕 158 | while ($messages > 0) { 159 | $wait = true; 160 | Coroutine::yield(); 161 | } 162 | }); 163 | 164 | $this->runWithBarrier(function () use ($handler) { 165 | try { 166 | $handler->onClose(); 167 | } catch (Throwable $e) { 168 | $this->logServerError($e); 169 | } 170 | }); 171 | //关闭连接 172 | $res->close(); 173 | } finally { 174 | // leave all rooms 175 | $websocket->leave(); 176 | if (isset($this->wsMessageChannel[$fd])) { 177 | $this->wsMessageChannel[$fd]->close(); 178 | unset($this->wsMessageChannel[$fd]); 179 | } 180 | $websocket->setConnected(false); 181 | } 182 | }); 183 | } 184 | 185 | /** 186 | * @param App $app 187 | * @param \think\Request $request 188 | * @return \think\Request 189 | */ 190 | protected function setRequestThroughMiddleware(App $app, \think\Request $request) 191 | { 192 | $app->instance('request', $request); 193 | return Middleware::make($app, $this->getConfig('websocket.middleware', [])) 194 | ->pipeline() 195 | ->send($request) 196 | ->then(function ($request) { 197 | return $request; 198 | }); 199 | } 200 | 201 | /** 202 | * Prepare settings if websocket is enabled. 203 | */ 204 | protected function prepareWebsocket() 205 | { 206 | $this->prepareWebsocketIdAtomic(); 207 | $this->prepareWebsocketRoom(); 208 | 209 | $this->onEvent('message', function ($message) { 210 | if ($message instanceof PushMessage) { 211 | if (isset($this->wsMessageChannel[$message->fd])) { 212 | $this->wsMessageChannel[$message->fd]->push($message->data); 213 | } 214 | } 215 | }); 216 | 217 | $this->onEvent('workerStart', function () { 218 | $this->bindWebsocketRoom(); 219 | $this->bindWebsocketHandler(); 220 | $this->prepareWebsocketListener(); 221 | }); 222 | } 223 | 224 | protected function prepareWebsocketIdAtomic() 225 | { 226 | $this->wsIdAtomic = new Atomic(); 227 | } 228 | 229 | /** 230 | * Prepare websocket room. 231 | */ 232 | protected function prepareWebsocketRoom() 233 | { 234 | $this->wsRoom = $this->container->make(Room::class); 235 | $this->wsRoom->prepare(); 236 | } 237 | 238 | protected function prepareWebsocketListener() 239 | { 240 | $listeners = $this->getConfig('websocket.listen', []); 241 | 242 | foreach ($listeners as $event => $listener) { 243 | $this->app->event->listen('swoole.websocket.' . Str::studly($event), $listener); 244 | } 245 | 246 | $subscribers = $this->getConfig('websocket.subscribe', []); 247 | 248 | foreach ($subscribers as $subscriber) { 249 | $this->app->event->observe($subscriber, 'swoole.websocket.'); 250 | } 251 | } 252 | 253 | /** 254 | * Prepare websocket handler for onOpen and onClose callback 255 | */ 256 | protected function bindWebsocketHandler() 257 | { 258 | $handlerClass = $this->getConfig('websocket.handler'); 259 | $this->app->bind(HandlerInterface::class, $handlerClass); 260 | } 261 | 262 | /** 263 | * Bind room instance to app container. 264 | */ 265 | protected function bindWebsocketRoom(): void 266 | { 267 | $this->app->instance(Room::class, $this->wsRoom); 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------