diff --git a/.travis.yml b/.travis.yml index 6af4ba4f..83809e71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,26 +4,6 @@ sudo: false matrix: include: - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/framework:5.1.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/framework:5.2.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/framework:5.3.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/framework:5.4.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/framework:5.5.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.1.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.2.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.3.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.4.* - - php: 7.0 - env: FRAMEWORK_VERSION=laravel/lumen-framework:5.5.* - php: 7.1 env: FRAMEWORK_VERSION=laravel/framework:5.1.* - php: 7.1 @@ -66,10 +46,10 @@ matrix: env: FRAMEWORK_VERSION=laravel/lumen-framework:5.5.* before_install: - - pecl install swoole + - printf "\n" | pecl install swoole install: - composer require "${FRAMEWORK_VERSION}" --no-update -n - travis_retry composer install --no-suggest --prefer-dist -n -o -script: vendor/bin/phpunit \ No newline at end of file +script: vendor/bin/phpunit diff --git a/composer.json b/composer.json index 2f5bd938..b1738e47 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ } ], "require": { + "php": "^7.1", "illuminate/console": "~5.1", "illuminate/contracts": "~5.1", "illuminate/http": "~5.1", @@ -21,7 +22,9 @@ }, "require-dev": { "laravel/lumen-framework": "~5.1", - "phpunit/phpunit": "~6.0" + "phpunit/phpunit": "^6.1", + "mockery/mockery": "~1.0", + "codedungeon/phpunit-result-printer": "^0.14.0" }, "autoload": { "psr-4": { diff --git a/config/swoole_http.php b/config/swoole_http.php index cf52775a..45bd979f 100644 --- a/config/swoole_http.php +++ b/config/swoole_http.php @@ -1,5 +1,7 @@ [ 'pid_file' => env('SWOOLE_HTTP_PID_FILE', base_path('storage/logs/swoole_http.pid')), 'log_file' => env('SWOOLE_HTTP_LOG_FILE', base_path('storage/logs/swoole_http.log')), - 'daemonize' => env('SWOOLE_HTTP_DAEMONIZE', 0), + 'daemonize' => env('SWOOLE_HTTP_DAEMONIZE', false), + 'task_worker_num' => env('SWOOLE_HTTP_TASK_WORKER_NUM', 4) ], ], + + 'websocket' => [ + 'enabled' => env('SWOOLE_HTTP_WEBSOCKET', false), + ], + + /* + |-------------------------------------------------------------------------- + | Providers here will be registered on every request. + |-------------------------------------------------------------------------- + */ 'providers' => [ // App\Providers\AuthServiceProvider::class, + ], + + /* + |-------------------------------------------------------------------------- + | Define your swoole tables here. + | + | @see https://wiki.swoole.com/wiki/page/p-table.html + |-------------------------------------------------------------------------- + */ + 'tables' => [ + // 'table_name' => [ + // 'size' => 1024, + // 'columns' => [ + // ['name' => 'column_name', 'type' => Table::TYPE_STRING, 'size' => 1024], + // ] + // ], ] ]; diff --git a/config/swoole_websocket.php b/config/swoole_websocket.php new file mode 100644 index 00000000..bd04c0a8 --- /dev/null +++ b/config/swoole_websocket.php @@ -0,0 +1,59 @@ + SwooleTW\Http\Websocket\WebsocketHandler::class, + + /* + |-------------------------------------------------------------------------- + | Websocket handlers mapping for onMessage callback + |-------------------------------------------------------------------------- + */ + 'handlers' => [ + // 'event_name' => 'App\Handlers\ExampleHandler@function', + ], + + /* + |-------------------------------------------------------------------------- + | Default websocket driver + |-------------------------------------------------------------------------- + */ + 'default' => 'table', + + /* + |-------------------------------------------------------------------------- + | Default message formatter + | Replace it if you want to customize your websocket payload + |-------------------------------------------------------------------------- + */ + 'formatter' => SwooleTW\Http\Websocket\Formatters\DefaultFormatter::class, + + /* + |-------------------------------------------------------------------------- + | Drivers mapping + |-------------------------------------------------------------------------- + */ + 'drivers' => [ + 'table' => SwooleTW\Http\Websocket\Rooms\TableRoom::class, + ], + + /* + |-------------------------------------------------------------------------- + | Drivers settings + |-------------------------------------------------------------------------- + */ + 'settings' => [ + + 'table' => [ + 'room_rows' => 4096, + 'room_size' => 2048, + 'client_rows' => 8192, + 'client_size' => 2048 + ] + ], +]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8f339800..6c4a1dbb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,7 @@ stopOnError="false" stopOnFailure="false" verbose="true" -> + printerClass="Codedungeon\PHPUnitPrettyResultPrinter\Printer"> ./tests diff --git a/src/Commands/HttpServerCommand.php b/src/Commands/HttpServerCommand.php index ed62e4b6..d5303c3c 100644 --- a/src/Commands/HttpServerCommand.php +++ b/src/Commands/HttpServerCommand.php @@ -176,13 +176,21 @@ protected function showInfos() { $pid = $this->getPid(); $isRunning = $this->isRunning($pid); + $host = $this->configs['server']['host']; + $port = $this->configs['server']['port']; + $isWebsocket = $this->configs['websocket']['enabled']; + $logFile = $this->configs['server']['options']['log_file']; $this->table(['Name', 'Value'], [ ['PHP Version', 'Version' => phpversion()], ['Swoole Version', 'Version' => swoole_version()], ['Laravel Version', $this->getApplication()->getVersion()], ['Server Status', $isRunning ? 'Online' : 'Offline'], + ['Listen IP', $host], + ['Listen Port', $port], + ['Websocket Mode', $isWebsocket ? 'On' : 'Off'], ['PID', $isRunning ? $pid : 'None'], + ['Log Path', $logFile], ]); } diff --git a/src/HttpServiceProvider.php b/src/HttpServiceProvider.php index db7d5b9b..79f8f5bc 100644 --- a/src/HttpServiceProvider.php +++ b/src/HttpServiceProvider.php @@ -2,8 +2,8 @@ namespace SwooleTW\Http; -use SwooleTW\Http\Commands\HttpServerCommand; use Illuminate\Support\ServiceProvider; +use SwooleTW\Http\Commands\HttpServerCommand; abstract class HttpServiceProvider extends ServiceProvider { @@ -21,7 +21,7 @@ abstract class HttpServiceProvider extends ServiceProvider */ public function register() { - $this->mergeConfig(); + $this->mergeConfigs(); $this->registerManager(); $this->registerCommands(); } @@ -41,16 +41,18 @@ abstract protected function registerManager(); public function boot() { $this->publishes([ - __DIR__ . '/../config/swoole_http.php' => base_path('config/swoole_http.php') + __DIR__ . '/../config/swoole_http.php' => base_path('config/swoole_http.php'), + __DIR__ . '/../config/swoole_websocket.php' => base_path('config/swoole_websocket.php') ], 'config'); } /** * Merge configurations. */ - protected function mergeConfig() + protected function mergeConfigs() { $this->mergeConfigFrom(__DIR__ . '/../config/swoole_http.php', 'swoole_http'); + $this->mergeConfigFrom(__DIR__ . '/../config/swoole_websocket.php', 'swoole_websocket'); } /** diff --git a/src/Server/Facades/Server.php b/src/Server/Facades/Server.php new file mode 100644 index 00000000..7a34102a --- /dev/null +++ b/src/Server/Facades/Server.php @@ -0,0 +1,18 @@ +setProcessName('manager process'); - $this->createSwooleHttpServer(); - $this->configureSwooleHttpServer(); - $this->setSwooleHttpServerListeners(); + $this->createTables(); + $this->prepareWebsocket(); + $this->createSwooleServer(); + $this->configureSwooleServer(); + $this->setSwooleServerListeners(); + } + + /** + * Prepare settings if websocket is enabled. + */ + protected function createTables() + { + $this->table = new Table; + $this->registerTables(); + } + + /** + * Prepare settings if websocket is enabled. + */ + protected function prepareWebsocket() + { + $isWebsocket = $this->container['config']->get('swoole_http.websocket.enabled'); + $formatter = $this->container['config']->get('swoole_websocket.formatter'); + + if ($isWebsocket) { + array_push($this->events, ...$this->wsEvents); + $this->isWebsocket = true; + $this->setFormatter(new $formatter); + $this->setWebsocketHandler(); + $this->setWebsocketRoom(); + } } /** - * Creates swoole_http_server. + * Create swoole server. */ - protected function createSwooleHttpServer() + protected function createSwooleServer() { + $server = $this->isWebsocket ? WebsocketServer::class : HttpServer::class; $host = $this->container['config']->get('swoole_http.server.host'); $port = $this->container['config']->get('swoole_http.server.port'); - $this->server = new Server($host, $port); + $this->server = new $server($host, $port); } /** - * Sets swoole_http_server configurations. + * Set swoole server configurations. */ - protected function configureSwooleHttpServer() + protected function configureSwooleServer() { $config = $this->container['config']->get('swoole_http.server.options'); @@ -120,9 +163,9 @@ protected function configureSwooleHttpServer() } /** - * Sets swoole_http_server listeners. + * Set swoole server listeners. */ - protected function setSwooleHttpServerListeners() + protected function setSwooleServerListeners() { foreach ($this->events as $event) { $listener = 'on' . ucfirst($event); @@ -131,7 +174,7 @@ protected function setSwooleHttpServerListeners() $this->server->on($event, [$this, $listener]); } else { $this->server->on($event, function () use ($event) { - $event = sprintf('http.%s', $event); + $event = sprintf('swoole.%s', $event); $this->container['events']->fire($event, func_get_args()); }); @@ -147,7 +190,7 @@ public function onStart() $this->setProcessName('master process'); $this->createPidFile(); - $this->container['events']->fire('http.start', func_get_args()); + $this->container['events']->fire('swoole.start', func_get_args()); } /** @@ -158,11 +201,20 @@ public function onWorkerStart() $this->clearCache(); $this->setProcessName('worker process'); - $this->container['events']->fire('http.workerStart', func_get_args()); + $this->container['events']->fire('swoole.workerStart', func_get_args()); + + // clear events instance in case of repeated listeners in worker process + Facade::clearResolvedInstance('events'); $this->createApplication(); $this->setLaravelApp(); - $this->bindSwooleHttpServer(); + $this->bindSwooleServer(); + $this->bindSwooleTable(); + + if ($this->isWebsocket) { + $this->bindRoom(); + $this->bindWebsocket(); + } } /** @@ -173,27 +225,51 @@ public function onWorkerStart() */ public function onRequest($swooleRequest, $swooleResponse) { - $this->container['events']->fire('http.onRequest'); + $this->container['events']->fire('swoole.request'); // Reset user-customized providers $this->getApplication()->resetProviders(); - $illuminateRequest = Request::make($swooleRequest)->toIlluminate(); - $illuminateResponse = $this->getApplication()->run($illuminateRequest); - $response = Response::make($illuminateResponse, $swooleResponse); - // To prevent 'connection[...] is closed' error. - if (! $this->server->exist($swooleRequest->fd)) { - return; + try { + $illuminateResponse = $this->getApplication()->run($illuminateRequest); + $response = Response::make($illuminateResponse, $swooleResponse); + $response->send(); + } catch (Exception $e) { + $this->logServerError($e); + + try { + $swooleResponse->status(500); + $swooleResponse->end('Oops! An unexpected error occurred.'); + } catch (Exception $e) { + // Catch: zm_deactivate_swoole: Fatal error: Uncaught exception + // 'ErrorException' with message 'swoole_http_response::status(): + // http client#2 is not exist. + } + } + } + + /** + * Set onTask listener. + */ + public function onTask(HttpServer $server, $taskId, $fromId, $data) + { + $this->container['events']->fire('swoole.task', func_get_args()); + + // push websocket message + if ($this->isWebsocket + && array_key_exists('action', $data) + && $data['action'] === Websocket::PUSH_ACTION) { + $this->pushMessage($server, $data['data'] ?? []); } - $response->send(); - - // Unset request and response. - $response = null; - $swooleRequest = null; - $swooleResponse = null; - $illuminateRequest = null; - $illuminateResponse = null; + } + + /** + * Set onFinish listener. + */ + public function onFinish(HttpServer $server, $taskId, $data) + { + // task worker callback } /** @@ -203,7 +279,7 @@ public function onShutdown() { $this->removePidFile(); - $this->container['events']->fire('http.showdown', func_get_args()); + $this->container['events']->fire('swoole.shutdown', func_get_args()); } /** @@ -239,13 +315,23 @@ protected function setLaravelApp() /** * Bind swoole server to Laravel app container. */ - protected function bindSwooleHttpServer() + protected function bindSwooleServer() { $this->app->singleton('swoole.server', function () { return $this->server; }); } + /** + * Bind swoole table to Laravel app container. + */ + protected function bindSwooleTable() + { + $this->app->singleton('swoole.table', function () { + return $this->table; + }); + } + /** * Gets pid file path. * @@ -290,7 +376,7 @@ protected function clearCache() } /** - * Sets process name. + * Set process name. * * @param $process */ @@ -306,4 +392,47 @@ protected function setProcessName($process) swoole_set_process_name($name); } + + /** + * Log server error. + * + * @param Exception + */ + protected function logServerError(Exception $e) + { + $logFile = $this->container['config']->get('swoole_http.server.options.log_file'); + + try { + $output = fopen($logFile ,'w'); + } catch (Exception $e) { + $output = STDOUT; + } + + $prefix = sprintf("[%s #%d *%d]\tERROR\t", date('Y-m-d H:i:s'), $this->server->master_pid, $this->server->worker_id); + + fwrite($output, sprintf('%s%s(%d): %s', $prefix, $e->getFile(), $e->getLine(), $e->getMessage()) . PHP_EOL); + } + + /** + * Register user-defined swoole tables. + */ + protected function registerTables() + { + $tables = $this->container['config']->get('swoole_http.tables') ?? []; + + foreach ($tables as $key => $value) { + $table = new SwooleTable($value['size']); + $columns = $value['columns'] ?? []; + foreach ($columns as $column) { + if (isset($column['size'])) { + $table->column($column['name'], $column['type'], $column['size']); + } else { + $table->column($column['name'], $column['type']); + } + } + $table->create(); + + $this->table->add($key, $table); + } + } } diff --git a/src/Server/Table.php b/src/Server/Table.php new file mode 100644 index 00000000..71ea0db0 --- /dev/null +++ b/src/Server/Table.php @@ -0,0 +1,27 @@ +tables[$name] = $table; + + return $this; + } + + public function get(string $name) + { + return $this->tables[$name] ?? null; + } + + public function getAll() + { + return $this->tables; + } +} diff --git a/src/Websocket/CanWebsocket.php b/src/Websocket/CanWebsocket.php new file mode 100644 index 00000000..f68caa3d --- /dev/null +++ b/src/Websocket/CanWebsocket.php @@ -0,0 +1,227 @@ +toIlluminate(); + + try { + $this->websocketHandler->onOpen($swooleRequest->fd, $illuminateRequest); + } catch (Exception $e) { + $this->logServerError($e); + } + } + + /** + * "onMessage" listener. + * + * @param \Swoole\Websocket\Server $server + * @param \Swoole\Websocket\Frame $frame + */ + public function onMessage(Server $server, Frame $frame) + { + $this->app['swoole.websocket']->setSender($frame->fd); + + $payload = $this->formatter->input($frame); + $handler = $this->container['config']->get("swoole_websocket.handlers.{$payload['event']}"); + + try { + if ($handler) { + $this->app->call($handler, [$frame->fd, $payload['data']]); + } else { + $this->websocketHandler->onMessage($frame); + } + } catch (Exception $e) { + $this->logServerError($e); + } + } + + /** + * "onClose" listener. + * + * @param \Swoole\Websocket\Server $server + * @param int $fd + * @param int $reactorId + */ + public function onClose(Server $server, $fd, $reactorId) + { + if (! $this->isWebsocket($fd)) { + return; + } + + try { + $this->websocketHandler->onClose($fd, $reactorId); + } catch (Exception $e) { + $this->logServerError($e); + } + + $this->app['swoole.websocket']->setSender($fd)->leaveAll(); + } + + /** + * Push websocket message to clients. + * + * @param \Swoole\Websocket\Server $server + * @param mixed $data + */ + public function pushMessage(Server $server, array $data) + { + $opcode = $data['opcode'] ?? 1; + $sender = $data['sender'] ?? 0; + $fds = $data['fds'] ?? []; + $broadcast = $data['broadcast'] ?? false; + $event = $data['event'] ?? null; + $message = $this->formatter->output($event, $data['message']); + + // attach sender if not broadcast + if (! $broadcast && ! in_array($sender, $fds)) { + $fds[] = $sender; + } + + // check if to broadcast all clients + if ($broadcast && empty($fds)) { + foreach ($server->connections as $fd) { + if ($this->isWebsocket($fd)) { + $fds[] = $fd; + } + } + } + + foreach ($fds as $fd) { + if ($broadcast && $sender === (integer) $fd) { + continue; + } + $server->push($fd, $message, $opcode); + } + } + + /** + * Set message formatter for websocket. + * + * @param \SwooleTW\Http\Websocket\Formatter\FormatterContract $formatter + */ + public function setFormatter(FormatterContract $formatter) + { + $this->formatter = $formatter; + + return $this; + } + + /** + * Get message formatter for websocket. + */ + public function getFormatter() + { + return $this->formatter; + } + + /** + * Check if is a websocket fd. + */ + protected function isWebsocket(int $fd) + { + $info = $this->server->connection_info($fd); + + return array_key_exists('websocket_status', $info) && $info['websocket_status']; + } + + /** + * Set websocket handler for onOpen and onClose callback. + */ + protected function setWebsocketRoom() + { + $driver = $this->container['config']->get('swoole_websocket.default'); + $configs = $this->container['config']->get("swoole_websocket.settings.{$driver}"); + $className = $this->container['config']->get("swoole_websocket.drivers.{$driver}"); + + $this->websocketRoom = new $className($configs); + $this->websocketRoom->prepare(); + } + + /** + * Set websocket handler for onOpen and onClose callback. + */ + protected function setWebsocketHandler() + { + $handlerClass = $this->container['config']->get('swoole_websocket.handler'); + + if (! $handlerClass) { + throw new Exception('websocket handler not set in swoole_websocket config'); + } + + $handler = $this->container->make($handlerClass); + + if (! $handler instanceof HandlerContract) { + throw new Exception(sprintf('%s must implement %s', get_class($handler), HandlerContract::class)); + } + + $this->websocketHandler = $handler; + } + + /** + * Bind room instance to Laravel app container. + */ + protected function bindRoom() + { + $this->app->singleton(RoomContract::class, function ($app) { + return $this->websocketRoom; + }); + $this->app->alias(RoomContract::class, 'swoole.room'); + } + + /** + * Bind websocket instance to Laravel app container. + */ + protected function bindWebsocket() + { + $this->app->singleton(Websocket::class, function ($app) { + return new Websocket($this->websocketRoom); + }); + $this->app->alias(Websocket::class, 'swoole.websocket'); + } +} diff --git a/src/Websocket/Facades/Room.php b/src/Websocket/Facades/Room.php new file mode 100644 index 00000000..e0a26fd4 --- /dev/null +++ b/src/Websocket/Facades/Room.php @@ -0,0 +1,18 @@ +data, true); + + return [ + 'event' => $data['event'] ?? null, + 'data' => $data['data'] ?? null + ]; + } + + /** + * Output message for websocket push. + * + * @return mixed + */ + public function output($event, $data) + { + return json_encode([ + 'event' => $event, + 'data' => $data + ]); + } +} diff --git a/src/Websocket/Formatters/FormatterContract.php b/src/Websocket/Formatters/FormatterContract.php new file mode 100644 index 00000000..462e4d31 --- /dev/null +++ b/src/Websocket/Formatters/FormatterContract.php @@ -0,0 +1,21 @@ +config = $config; + } + + public function prepare() + { + $this->initRoomsTable(); + $this->initSidsTable(); + } + + public function add(int $fd, string $room) + { + $this->addAll($fd, [$room]); + } + + public function addAll(int $fd, array $roomNames) + { + $rooms = $this->getRooms($fd); + + foreach ($roomNames as $room) { + $room = $this->encode($room); + $sids = $this->getClients($room, false); + + if (in_array($fd, $sids)) { + continue; + } + + $sids[] = $fd; + $rooms[] = $room; + + $this->setClients($room, $sids); + } + + $this->setRooms($fd, $rooms); + } + + public function delete(int $fd, string $room) + { + $this->deleteAll($fd, [$room]); + } + + public function deleteAll(int $fd, array $roomNames = []) + { + $allRooms = $this->getRooms($fd); + $rooms = count($roomNames) ? $this->encode($roomNames) : $allRooms; + + $removeRooms = []; + foreach ($rooms as $room) { + $sids = $this->getClients($room, false); + + if (! in_array($fd, $sids)) { + continue; + } + + $this->setClients($room, array_values(array_diff($sids, [$fd])), 'rooms'); + $removeRooms[] = $room; + } + + $this->setRooms($fd, array_values(array_diff($allRooms, $removeRooms)), 'sids'); + } + + public function getClients(string $room, $hash = true) + { + if ($hash) { + $room = $this->encode($room); + } + + return $this->getValue($room, 'rooms'); + } + + public function getRooms(int $fd) + { + return $this->getValue($fd, 'sids'); + } + + protected function setClients(string $room, array $sids) + { + return $this->setValue($room, $sids, 'rooms'); + } + + protected function setRooms(int $fd, array $rooms) + { + return $this->setValue($fd, $rooms, 'sids'); + } + + protected function initRoomsTable() + { + $this->rooms = new Table($this->config['room_rows']); + $this->rooms->column('value', Table::TYPE_STRING, $this->config['room_size']); + $this->rooms->create(); + } + + protected function initSidsTable() + { + $this->sids = new Table($this->config['client_rows']); + $this->sids->column('value', Table::TYPE_STRING, $this->config['client_size']); + $this->sids->create(); + } + + protected function encode($keys) + { + if (is_array($keys)) { + return array_map(function ($key) { + return md5($key); + }, $keys); + } + + return md5($keys); + } + + public function setValue($key, array $value, string $table) + { + $this->checkTable($table); + + $this->$table->set($key, [ + 'value' => json_encode($value) + ]); + + return $this; + } + + public function getValue(string $key, string $table) + { + $this->checkTable($table); + + $value = $this->$table->get($key); + + return $value ? json_decode($value['value'], true) : []; + } + + protected function checkTable(string $table) + { + if (! property_exists($this, $table) || ! $this->$table instanceof Table) { + throw new \InvalidArgumentException('invalid table name.'); + } + } +} diff --git a/src/Websocket/Websocket.php b/src/Websocket/Websocket.php new file mode 100644 index 00000000..ffedd86a --- /dev/null +++ b/src/Websocket/Websocket.php @@ -0,0 +1,172 @@ +room = $room; + } + + public function broadcast() + { + $this->isBroadcast = true; + + return $this; + } + + /** + * @param integer, string (fd or room) + */ + public function to($value) + { + $this->toAll([$value]); + + return $this; + } + + /** + * @param array (fds or rooms) + */ + public function toAll(array $values) + { + foreach ($values as $value) { + if (! in_array($value, $this->to)) { + $this->to[] = $value; + } + } + + return $this; + } + + /** + * @param string + */ + public function join(string $room) + { + $this->room->add($this->sender, $room); + + return $this; + } + + /** + * @param array + */ + public function joinAll(array $rooms) + { + $this->room->addAll($this->sender, $rooms); + + return $this; + } + + /** + * @param string + */ + public function leave(string $room) + { + $this->room->delete($this->sender, $room); + + return $this; + } + + /** + * @param array + */ + public function leaveAll(array $rooms = []) + { + $this->room->deleteAll($this->sender, $rooms); + + return $this; + } + + public function on(string $event, callable $callback) + { + // + } + + public function emit(string $event, $data) + { + app('swoole.server')->task([ + 'action' => static::PUSH_ACTION, + 'data' => [ + 'sender' => $this->sender, + 'fds' => $this->getFds(), + 'broadcast' => $this->isBroadcast, + 'event' => $event, + 'message' => $data + ] + ]); + + $this->cleanData(); + } + + public function in(string $room) + { + $this->join($room); + + return $this; + } + + public function setSender(int $fd) + { + $this->sender = $fd; + + return $this; + } + + public function getSender() + { + return $this->sender; + } + + public function getIsBroadcast() + { + return $this->isBroadcast; + } + + public function getTo() + { + return $this->to; + } + + protected function getFds() + { + $fds = array_filter($this->to, function ($value) { + return is_integer($value); + }); + $rooms = array_diff($this->to, $fds); + + foreach ($rooms as $room) { + $fds = array_unique(array_merge($fds, $this->room->getClients($room))); + } + + return array_values($fds); + } + + protected function cleanData() + { + $this->isBroadcast = false; + $this->sender = null; + $this->to = []; + } +} diff --git a/src/Websocket/WebsocketHandler.php b/src/Websocket/WebsocketHandler.php new file mode 100644 index 00000000..5db9551c --- /dev/null +++ b/src/Websocket/WebsocketHandler.php @@ -0,0 +1,43 @@ +fire('swoole.onOpen', compact('fd', 'request')); + } + + /** + * "onMessage" listener. + * only triggered when event handler not found + * + * @param \Swoole\Websocket\Frame $frame + */ + public function onMessage(Frame $frame) + { + app('events')->fire('swoole.onMessage', compact('frame')); + } + + /** + * "onClose" listener. + * + * @param int $fd + * @param int $reactorId + */ + public function onClose($fd, $reactorId) + { + app('events')->fire('swoole.onClose', compact('fd', 'reactorId')); + } +} diff --git a/tests/Server/TableTest.php b/tests/Server/TableTest.php new file mode 100644 index 00000000..9af19080 --- /dev/null +++ b/tests/Server/TableTest.php @@ -0,0 +1,34 @@ +add($name = 'foo', $swooleTable); + + $this->assertSame($swooleTable, $table->get($name)); + } + + public function testGetAll() + { + $swooleTable = m::mock(SwooleTable::class); + + $table = new Table; + $table->add($foo = 'foo', $swooleTable); + $table->add($bar = 'bar', $swooleTable); + + $this->assertSame(2, count($table->getAll())); + $this->assertSame($swooleTable, $table->getAll()[$foo]); + $this->assertSame($swooleTable, $table->getAll()[$bar]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index f03bbb42..71615442 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,10 @@ class TestCase extends BaseTestCase { - + public function tearDown() + { + $this->addToAssertionCount( + \Mockery::getContainer()->mockery_getExpectationCount() + ); + } } diff --git a/tests/Websocket/TableRoomTest.php b/tests/Websocket/TableRoomTest.php new file mode 100644 index 00000000..62b257ef --- /dev/null +++ b/tests/Websocket/TableRoomTest.php @@ -0,0 +1,122 @@ + 4096, + 'room_size' => 2048, + 'client_rows' => 8192, + 'client_size' => 2048 + ]; + $this->tableRoom = new TableRoom($config); + $this->tableRoom->prepare(); + } + + public function testPrepare() + { + $reflection = new \ReflectionClass($this->tableRoom); + $method = $reflection->getMethod('prepare'); + $method->invoke($this->tableRoom); + + $rooms = $reflection->getProperty('rooms'); + $rooms->setAccessible(true); + + $sids = $reflection->getProperty('sids'); + $sids->setAccessible(true); + + $this->assertInstanceOf(Table::class, $rooms->getValue($this->tableRoom)); + $this->assertInstanceOf(Table::class, $sids->getValue($this->tableRoom)); + } + + public function testInvalidTableName() + { + $this->expectException(\InvalidArgumentException::class); + + $this->tableRoom->getValue(1, 'foo'); + } + + public function testSetValue() + { + $this->tableRoom->setValue($key = 1, $value = ['foo', 'bar'], $table = 'sids'); + + $this->assertSame($value, $this->tableRoom->getValue($key, $table)); + } + + public function testAddAll() + { + $this->tableRoom->addAll($key = 1, $values = ['foo', 'bar']); + + $this->assertSame($this->encode($values), $this->tableRoom->getValue($key, $table = 'sids')); + $this->assertSame([$key], $this->tableRoom->getValue($this->encode('foo'), 'rooms')); + $this->assertSame([$key], $this->tableRoom->getValue($this->encode('bar'), 'rooms')); + } + + public function testAdd() + { + $this->tableRoom->add($key = 1, $value = 'foo'); + + $this->assertSame([$this->encode($value)], $this->tableRoom->getValue($key, $table = 'sids')); + $this->assertSame([$key], $this->tableRoom->getValue($this->encode($value), 'rooms')); + } + + public function testDeleteAll() + { + $this->tableRoom->addAll($key = 1, $values = ['foo', 'bar']); + $this->tableRoom->deleteAll($key); + + $this->assertSame([], $this->tableRoom->getValue($key, $table = 'sids')); + $this->assertSame([], $this->tableRoom->getValue($this->encode('foo'), 'rooms')); + $this->assertSame([], $this->tableRoom->getValue($this->encode('bar'), 'rooms')); + } + + public function testDelete() + { + $this->tableRoom->addAll($key = 1, $values = ['foo', 'bar']); + $this->tableRoom->delete($key, 'foo'); + + $this->assertSame([$this->encode('bar')], $this->tableRoom->getValue($key, $table = 'sids')); + $this->assertSame([], $this->tableRoom->getValue($this->encode('foo'), 'rooms')); + $this->assertSame([$key], $this->tableRoom->getValue($this->encode('bar'), 'rooms')); + } + + public function testGetRooms() + { + $this->tableRoom->addAll($key = 1, $values = ['foo', 'bar']); + + $this->assertSame( + $this->tableRoom->getValue($key, $table = 'sids'), + $this->tableRoom->getRooms($key) + ); + } + + public function testGetClients() + { + $keys = [1, 2]; + $this->tableRoom->add($keys[0], $room = 'foo'); + $this->tableRoom->add($keys[1], $room); + + $this->assertSame( + $this->tableRoom->getValue($this->encode($room), $table = 'rooms'), + $this->tableRoom->getClients($room) + ); + } + + protected function encode($keys) + { + $reflection = new \ReflectionClass($this->tableRoom); + $method = $reflection->getMethod('encode'); + $method->setAccessible(true); + + return $method->invokeArgs($this->tableRoom, [$keys]); + } +} diff --git a/tests/Websocket/WebsocketTest.php b/tests/Websocket/WebsocketTest.php new file mode 100644 index 00000000..893ef193 --- /dev/null +++ b/tests/Websocket/WebsocketTest.php @@ -0,0 +1,93 @@ +getWebsocket(); + $this->assertFalse($websocket->getIsBroadcast()); + + $websocket->broadcast(); + $this->assertTrue($websocket->getIsBroadcast()); + } + + public function testSetTo() + { + $websocket = $this->getWebsocket()->to($foo = 'foo'); + $this->assertTrue(in_array($foo, $websocket->getTo())); + + $websocket->toAll($bar = ['foo', 'bar', 'seafood']); + $this->assertSame($bar, $websocket->getTo()); + } + + public function testSetSender() + { + $websocket = $this->getWebsocket()->setSender($fd = 1); + $this->assertSame($fd, $websocket->getSender()); + } + + public function testJoin() + { + $room = m::mock(RoomContract::class); + $room->shouldReceive('add') + ->with($sender = 1, $name = 'room') + ->once(); + + $websocket = $this->getWebsocket($room) + ->setSender($sender) + ->join($name); + } + + public function testJoinAll() + { + $room = m::mock(RoomContract::class); + $room->shouldReceive('addAll') + ->with($sender = 1, $names = ['room1', 'room2']) + ->once(); + + $websocket = $this->getWebsocket($room) + ->setSender($sender) + ->joinAll($names); + } + + public function testLeave() + { + $room = m::mock(RoomContract::class); + $room->shouldReceive('delete') + ->with($sender = 1, $name = 'room') + ->once(); + + $websocket = $this->getWebsocket($room) + ->setSender($sender) + ->leave($name); + } + + public function testLeaveAll() + { + $room = m::mock(RoomContract::class); + $room->shouldReceive('deleteAll') + ->with($sender = 1, $names = ['room1', 'room2']) + ->once(); + + $websocket = $this->getWebsocket($room) + ->setSender($sender) + ->leaveAll($names); + } + + protected function getWebsocket(RoomContract $room = null) + { + return new Websocket($room ?? m::mock(RoomContract::class)); + } +}