├── 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://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 |
--------------------------------------------------------------------------------