初始化代码

This commit is contained in:
2025-12-22 14:34:25 +08:00
parent c2c5ae2fdd
commit a77dbc743f
1510 changed files with 213008 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
<?php
namespace think\swoole;
use think\swoole\coroutine\Context;
use think\swoole\rpc\client\Proxy;
class App extends \think\App
{
public function runningInConsole()
{
return !!Context::getData('_fd');
}
protected function isRpcInterface($abstract)
{
if (interface_exists($abstract) && defined($abstract . '::RPC')) {
return true;
}
return false;
}
public function make(string $abstract, array $vars = [], bool $newInstance = false)
{
if ($this->isRpcInterface($abstract) && !$this->bound($abstract)) {
//rpc接口
$this->bind($abstract, Proxy::getClassName($abstract));
}
return parent::make($abstract, $vars, $newInstance);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace think\swoole;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
class FileWatcher
{
protected $finder;
protected $files = [];
public function __construct($directory, $exclude, $name)
{
$this->finder = new Finder();
$this->finder->files()
->name($name)
->in($directory)
->exclude($exclude);
}
protected function findFiles()
{
$files = [];
/** @var SplFileInfo $f */
foreach ($this->finder as $f) {
$files[$f->getRealpath()] = $f->getMTime();
}
return $files;
}
public function watch(callable $callback)
{
$this->files = $this->findFiles();
swoole_timer_tick(1000, function () use ($callback) {
$files = $this->findFiles();
foreach ($files as $path => $time) {
if (empty($this->files[$path]) || $this->files[$path] != $time) {
call_user_func($callback);
break;
}
}
$this->files = $files;
});
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace think\swoole;
use think\Middleware;
use think\Route;
/**
* Class Http
* @package think\swoole
* @property $request
*/
class Http extends \think\Http
{
/** @var Middleware */
protected static $middleware;
/** @var Route */
protected static $route;
protected function loadMiddleware(): void
{
if (!isset(self::$middleware)) {
parent::loadMiddleware();
self::$middleware = clone $this->app->middleware;
}
$middleware = clone self::$middleware;
$app = $this->app;
$closure = function () use ($app) {
$this->app = $app;
};
$resetMiddleware = $closure->bindTo($middleware, $middleware);
$resetMiddleware();
$this->app->instance("middleware", $middleware);
}
protected function loadRoutes(): void
{
if (!isset(self::$route)) {
parent::loadRoutes();
self::$route = clone $this->app->route;
}
}
protected function dispatchToRoute($request)
{
if (isset(self::$route)) {
$newRoute = clone self::$route;
$app = $this->app;
$closure = function () use ($app) {
$this->app = $app;
};
$resetRouter = $closure->bindTo($newRoute, $newRoute);
$resetRouter();
$this->app->instance("route", $newRoute);
}
return parent::dispatchToRoute($request);
}
}

View File

@@ -0,0 +1,282 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole;
use Closure;
use Exception;
use Swoole\Process;
use Swoole\Server;
use think\App;
use think\console\Output;
use think\exception\Handle;
use think\helper\Str;
use think\swoole\App as SwooleApp;
use think\swoole\concerns\InteractsWithHttp;
use think\swoole\concerns\InteractsWithRpc;
use think\swoole\concerns\InteractsWithServer;
use think\swoole\concerns\InteractsWithSwooleTable;
use think\swoole\concerns\InteractsWithWebsocket;
use think\swoole\pool\Cache;
use think\swoole\pool\Db;
use Throwable;
/**
* Class Manager
*/
class Manager
{
use InteractsWithServer, InteractsWithSwooleTable, InteractsWithHttp, InteractsWithWebsocket, InteractsWithRpc;
/**
* @var App
*/
protected $container;
/**
* @var SwooleApp
*/
protected $app;
/** @var PidManager */
protected $pidManager;
/**
* Server events.
*
* @var array
*/
protected $events = [
'start',
'shutDown',
'workerStart',
'workerStop',
'workerError',
'packet',
'task',
'finish',
'pipeMessage',
'managerStart',
'managerStop',
'request',
];
/**
* Manager constructor.
* @param App $container
* @param PidManager $pidManager
*/
public function __construct(App $container, PidManager $pidManager)
{
$this->container = $container;
$this->pidManager = $pidManager;
}
/**
* 启动服务
*/
public function run(): void
{
$this->initialize();
if ($this->getConfig('hot_update.enable', false)) {
//热更新
$this->addHotUpdateProcess();
}
$this->getServer()->start();
}
/**
* 停止服务
*/
public function stop(): void
{
$this->getServer()->shutdown();
}
/**
* Initialize.
*/
protected function initialize(): void
{
$this->createTables();
$this->prepareWebsocket();
$this->setSwooleServerListeners();
$this->prepareRpc();
}
/**
* 获取配置
* @param string $name
* @param null $default
* @return mixed
*/
public function getConfig(string $name, $default = null)
{
return $this->container->config->get("swoole.{$name}", $default);
}
/**
* 触发事件
* @param $event
* @param $params
*/
protected function triggerEvent(string $event, $params = null): void
{
$this->container->event->trigger("swoole.{$event}", $params);
}
/**
* 监听事件
* @param string $event
* @param $listener
* @param bool $first
*/
protected function onEvent(string $event, $listener, bool $first = false): void
{
$this->container->event->listen("swoole.{$event}", $listener, $first);
}
/**
* @return Server
*/
public function getServer()
{
return $this->container->make(Server::class);
}
/**
* Set swoole server listeners.
*/
protected function setSwooleServerListeners()
{
foreach ($this->events as $event) {
$listener = Str::camel("on_$event");
$callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) {
$this->triggerEvent($event, func_get_args());
};
$this->getServer()->on($event, $callback);
}
}
protected function prepareApplication()
{
if (!$this->app instanceof SwooleApp) {
$this->app = new SwooleApp($this->container->getRootPath());
$this->app->bind(SwooleApp::class, App::class);
//绑定连接池
if ($this->getConfig('pool.db.enable', true)) {
$this->app->bind('db', Db::class);
}
if ($this->getConfig('pool.cache.enable', true)) {
$this->app->bind('cache', Cache::class);
}
$this->app->initialize();
$this->prepareConcretes();
}
}
/**
* 预加载
*/
protected function prepareConcretes()
{
$defaultConcretes = ['db', 'cache', 'event'];
$concretes = array_merge($defaultConcretes, $this->getConfig('concretes', []));
foreach ($concretes as $concrete) {
if ($this->app->has($concrete)) {
$this->app->make($concrete);
}
}
}
/**
* 清除apc、op缓存
*/
protected function clearCache()
{
if (extension_loaded('apc')) {
apc_clear_cache();
}
if (extension_loaded('Zend OPcache')) {
opcache_reset();
}
}
/**
* Set process name.
*
* @param $process
*/
protected function setProcessName($process)
{
// Mac OSX不支持进程重命名
if (stristr(PHP_OS, 'DAR')) {
return;
}
$serverName = 'swoole_http_server';
$appName = $this->container->config->get('app.name', 'ThinkPHP');
$name = sprintf('%s: %s for %s', $serverName, $process, $appName);
swoole_set_process_name($name);
}
/**
* 热更新
*/
protected function addHotUpdateProcess()
{
$process = new Process(function () {
$watcher = new FileWatcher($this->getConfig('hot_update.include', []), $this->getConfig('hot_update.exclude', []), $this->getConfig('hot_update.name', []));
$watcher->watch(function () {
$this->getServer()->reload();
});
}, false, 0);
$this->getServer()->addProcess($process);
}
/**
* 在沙箱中执行
* @param Closure $callable
* @param null $fd
* @param bool $persistent
*/
protected function runInSandbox(Closure $callable, $fd = null, $persistent = false)
{
/** @var Sandbox $sandbox */
$sandbox = $this->app->make(Sandbox::class);
$sandbox->run($callable, $fd, $persistent);
}
/**
* Log server error.
*
* @param Throwable|Exception $e
*/
public function logServerError(Throwable $e)
{
/** @var Handle $handle */
$handle = $this->container->make(Handle::class);
$handle->renderForConsole(new Output(), $e);
$handle->report($e);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace think\swoole;
use RuntimeException;
use Swoole\Process;
use think\helper\Arr;
class PidManager
{
/** @var string */
protected $file;
public function __construct(string $file = null)
{
$this->file = $file ?? (sys_get_temp_dir() . '/swoole.pid');
}
public function create(int $masterPid, int $managerPid)
{
if (!is_writable($this->file)
&& !is_writable(dirname($this->file))
) {
throw new RuntimeException(
sprintf('Pid file "%s" is not writable', $this->file)
);
}
file_put_contents($this->file, $masterPid . ',' . $managerPid);
}
public function getMasterPid()
{
return $this->getPids()['masterPid'];
}
public function getManagerPid()
{
return $this->getPids()['managerPid'];
}
public function getPids(): array
{
$pids = [];
if (is_readable($this->file)) {
$content = file_get_contents($this->file);
$pids = explode(',', $content);
}
return [
'masterPid' => $pids[0] ?? null,
'managerPid' => $pids[1] ?? null,
];
}
/**
* 是否运行中
* @return bool
*/
public function isRunning()
{
$pids = $this->getPids();
if (!count($pids)) {
return false;
}
$masterPid = $pids['masterPid'] ?? null;
$managerPid = $pids['managerPid'] ?? null;
if ($managerPid) {
// Swoole process mode
return $masterPid && $managerPid && Process::kill((int) $managerPid, 0);
}
// Swoole base mode, no manager process
return $masterPid && Process::kill((int) $masterPid, 0);
}
/**
* Kill process.
*
* @param int $sig
* @param int $wait
*
* @return bool
*/
public function killProcess($sig, $wait = 0)
{
Process::kill(
Arr::first($this->getPids()),
$sig
);
if ($wait) {
$start = time();
do {
if (!$this->isRunning()) {
break;
}
usleep(100000);
} while (time() < $start + $wait);
}
return $this->isRunning();
}
public function remove(): bool
{
if (is_writable($this->file)) {
return unlink($this->file);
}
return false;
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace think\swoole;
use Closure;
use RuntimeException;
use Symfony\Component\VarDumper\VarDumper;
use think\App;
use think\Config;
use think\Container;
use think\Event;
use think\Http;
use think\service\PaginatorService;
use think\swoole\contract\ResetterInterface;
use think\swoole\coroutine\Context;
use think\swoole\middleware\ResetVarDumper;
use think\swoole\resetters\ClearInstances;
use think\swoole\resetters\ResetConfig;
use think\swoole\resetters\ResetEvent;
use think\swoole\resetters\ResetService;
use Throwable;
class Sandbox
{
/**
* The app containers in different coroutine environment.
*
* @var array
*/
protected $snapshots = [];
/** @var Manager */
protected $manager;
/** @var App */
protected $app;
/** @var Config */
protected $config;
/** @var Event */
protected $event;
/** @var ResetterInterface[] */
protected $resetters = [];
protected $services = [];
public function __construct(Container $app, Manager $manager)
{
$this->manager = $manager;
$this->setBaseApp($app);
$this->initialize();
}
public function setBaseApp(Container $app)
{
$this->app = $app;
return $this;
}
public function getBaseApp()
{
return $this->app;
}
protected function initialize()
{
Container::setInstance(function () {
return $this->getApplication();
});
$this->app->bind(Http::class, \think\swoole\Http::class);
$this->setInitialConfig();
$this->setInitialServices();
$this->setInitialEvent();
$this->setInitialResetters();
//兼容var-dumper
$this->compatibleVarDumper();
return $this;
}
public function run(Closure $callable, $fd = null, $persistent = false)
{
$this->init($fd);
try {
$this->getApplication()->invoke($callable, [$this]);
} catch (Throwable $e) {
$this->manager->logServerError($e);
} finally {
$this->clear(!$persistent);
}
}
public function init($fd = null)
{
if (!is_null($fd)) {
Context::setData('_fd', $fd);
}
$this->setInstance($app = $this->getApplication());
$this->resetApp($app);
}
public function clear($snapshot = true)
{
if ($snapshot) {
unset($this->snapshots[$this->getSnapshotId()]);
}
Context::clear();
$this->setInstance($this->getBaseApp());
gc_collect_cycles();
}
public function getApplication()
{
$snapshot = $this->getSnapshot();
if ($snapshot instanceof Container) {
return $snapshot;
}
$snapshot = clone $this->getBaseApp();
$this->setSnapshot($snapshot);
return $snapshot;
}
protected function getSnapshotId()
{
if ($fd = Context::getData('_fd')) {
return "fd_" . $fd;
} else {
return Context::getCoroutineId();
}
}
/**
* Get current snapshot.
* @return App|null
*/
public function getSnapshot()
{
return $this->snapshots[$this->getSnapshotId()] ?? null;
}
public function setSnapshot(Container $snapshot)
{
$this->snapshots[$this->getSnapshotId()] = $snapshot;
return $this;
}
public function setInstance(Container $app)
{
$app->instance('app', $app);
$app->instance(Container::class, $app);
}
/**
* Set initial config.
*/
protected function setInitialConfig()
{
$this->config = clone $this->getBaseApp()->config;
}
protected function setInitialEvent()
{
$this->event = clone $this->getBaseApp()->event;
}
/**
* Get config snapshot.
*/
public function getConfig()
{
return $this->config;
}
public function getEvent()
{
return $this->event;
}
public function getServices()
{
return $this->services;
}
protected function setInitialServices()
{
$app = $this->getBaseApp();
$services = [
PaginatorService::class,
];
$services = array_merge($services, $this->config->get('swoole.services', []));
foreach ($services as $service) {
if (class_exists($service) && !in_array($service, $this->services)) {
$serviceObj = new $service($app);
$this->services[$service] = $serviceObj;
}
}
}
/**
* Initialize resetters.
*/
protected function setInitialResetters()
{
$app = $this->getBaseApp();
$resetters = [
ClearInstances::class,
ResetConfig::class,
ResetEvent::class,
ResetService::class,
];
$resetters = array_merge($resetters, $this->config->get('swoole.resetters', []));
foreach ($resetters as $resetter) {
$resetterClass = $app->make($resetter);
if (!$resetterClass instanceof ResetterInterface) {
throw new RuntimeException("{$resetter} must implement " . ResetterInterface::class);
}
$this->resetters[$resetter] = $resetterClass;
}
}
/**
* Reset Application.
*
* @param Container $app
*/
protected function resetApp(Container $app)
{
foreach ($this->resetters as $resetter) {
$resetter->handle($app, $this);
}
}
protected function compatibleVarDumper()
{
if (class_exists(VarDumper::class)) {
$this->app->middleware->add(ResetVarDumper::class);
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole;
use Swoole\Http\Server as HttpServer;
use Swoole\Server;
use Swoole\Websocket\Server as WebsocketServer;
use think\Route;
use think\swoole\command\RpcInterface;
use think\swoole\command\Server as ServerCommand;
use think\swoole\websocket\socketio\Controller;
class Service extends \think\Service
{
protected $isWebsocket = false;
/**
* @var HttpServer | WebsocketServer
*/
protected static $server;
public function register()
{
$this->isWebsocket = $this->app->config->get('swoole.websocket.enable', false);
$this->app->bind(Server::class, function () {
if (is_null(static::$server)) {
$this->createSwooleServer();
}
return static::$server;
});
$this->app->bind("swoole.server", Server::class);
$this->app->bind(PidManager::class, function () {
return new PidManager($this->app->config->get("swoole.server.options.pid_file"));
});
}
public function boot()
{
$this->commands(ServerCommand::class, RpcInterface::class);
if ($this->isWebsocket) {
$this->registerRoutes(function (Route $route) {
$route->group(function () use ($route) {
$route->get('socket.io/', '@upgrade');
$route->post('socket.io/', '@reject');
})
->prefix(Controller::class)
->allowCrossDomain([
'Access-Control-Allow-Credentials' => 'true',
'X-XSS-Protection' => 0,
]);
});
}
}
/**
* Create swoole server.
*/
protected function createSwooleServer()
{
$server = $this->isWebsocket ? WebsocketServer::class : HttpServer::class;
$config = $this->app->config;
$host = $config->get('swoole.server.host');
$port = $config->get('swoole.server.port');
$socketType = $config->get('swoole.server.socket_type', SWOOLE_SOCK_TCP);
$mode = $config->get('swoole.server.mode', SWOOLE_PROCESS);
static::$server = new $server($host, $port, $mode, $socketType);
$options = $config->get('swoole.server.options');
static::$server->set($options);
}
}

View File

@@ -0,0 +1,73 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole;
use Swoole\Table as SwooleTable;
class Table
{
/**
* Registered swoole tables.
*
* @var array
*/
protected $tables = [];
/**
* Add a swoole table to existing tables.
*
* @param string $name
* @param SwooleTable $table
*
* @return Table
*/
public function add(string $name, SwooleTable $table)
{
$this->tables[$name] = $table;
return $this;
}
/**
* Get a swoole table by its name from existing tables.
*
* @param string $name
*
* @return SwooleTable $table
*/
public function get(string $name)
{
return $this->tables[$name] ?? null;
}
/**
* Get all existing swoole tables.
*
* @return array
*/
public function getAll()
{
return $this->tables;
}
/**
* Dynamically access table.
*
* @param string $key
*
* @return SwooleTable
*/
public function __get($key)
{
return $this->get($key);
}
}

View File

@@ -0,0 +1,228 @@
<?php
namespace think\swoole;
use Swoole\Server;
use think\swoole\contract\websocket\ParserInterface;
use think\swoole\coroutine\Context;
use think\swoole\websocket\Room;
/**
* Class Websocket
*/
class Websocket
{
const PUSH_ACTION = 'push';
const EVENT_CONNECT = 'connect';
/**
* @var Server
*/
protected $server;
/**
* @var Room
*/
protected $room;
/**
* @var ParserInterface
*/
protected $parser;
/**
* Websocket constructor.
*
* @param Server $server
* @param Room $room
* @param ParserInterface $parser
*/
public function __construct(Server $server, Room $room, ParserInterface $parser)
{
$this->server = $server;
$this->room = $room;
$this->parser = $parser;
}
/**
* Set broadcast to true.
*/
public function broadcast(): self
{
Context::setData('websocket._broadcast', true);
return $this;
}
/**
* Get broadcast status value.
*/
public function isBroadcast()
{
return Context::getData('websocket._broadcast', false);
}
/**
* Set multiple recipients fd or room names.
*
* @param integer, string, array
*
* @return $this
*/
public function to($values): self
{
$values = is_string($values) || is_integer($values) ? func_get_args() : $values;
$to = Context::getData("websocket._to", []);
foreach ($values as $value) {
if (!in_array($value, $to)) {
$to[] = $value;
}
}
Context::setData("websocket._to", $to);
return $this;
}
/**
* Get push destinations (fd or room name).
*/
public function getTo()
{
return Context::getData("websocket._to", []);
}
/**
* Join sender to multiple rooms.
*
* @param string, array $rooms
*
* @return $this
*/
public function join($rooms): self
{
$rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms;
$this->room->add($this->getSender(), $rooms);
return $this;
}
/**
* Make sender leave multiple rooms.
*
* @param array $rooms
*
* @return $this
*/
public function leave($rooms = []): self
{
$rooms = is_string($rooms) || is_integer($rooms) ? func_get_args() : $rooms;
$this->room->delete($this->getSender(), $rooms);
return $this;
}
/**
* Emit data and reset some status.
*
* @param string
* @param mixed
*
* @return boolean
*/
public function emit(string $event, $data = null): bool
{
$fds = $this->getFds();
$assigned = !empty($this->getTo());
try {
if (empty($fds) && $assigned) {
return false;
}
$result = $this->server->task([
'action' => static::PUSH_ACTION,
'data' => [
'sender' => $this->getSender(),
'descriptors' => $fds,
'broadcast' => $this->isBroadcast(),
'assigned' => $assigned,
'payload' => $this->parser->encode($event, $data),
],
]);
return $result !== false;
} finally {
$this->reset();
}
}
/**
* Close current connection.
*
* @param integer
*
* @return boolean
*/
public function close(int $fd = null)
{
return $this->server->close($fd ?: $this->getSender());
}
/**
* Set sender fd.
*
* @param integer
*
* @return $this
*/
public function setSender(int $fd)
{
Context::setData('websocket._sender', $fd);
return $this;
}
/**
* Get current sender fd.
*/
public function getSender()
{
return Context::getData('websocket._sender');
}
/**
* Get all fds we're going to push data to.
*/
protected function getFds()
{
$to = $this->getTo();
$fds = array_filter($to, function ($value) {
return is_integer($value);
});
$rooms = array_diff($to, $fds);
foreach ($rooms as $room) {
$clients = $this->room->getClients($room);
// fallback fd with wrong type back to fds array
if (empty($clients) && is_numeric($room)) {
$fds[] = $room;
} else {
$fds = array_merge($fds, $clients);
}
}
return array_values(array_unique($fds));
}
protected function reset()
{
Context::removeData("websocket._to");
Context::removeData('websocket._broadcast');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace think\swoole\command;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Helpers;
use Nette\PhpGenerator\PhpFile;
use think\console\Command;
use think\helper\Arr;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\exception\RpcResponseException;
use think\swoole\rpc\client\Client;
use think\swoole\rpc\Error;
use think\swoole\rpc\JsonParser;
use think\swoole\rpc\server\Dispatcher;
class RpcInterface extends Command
{
public function configure()
{
$this->setName('rpc:interface')
->setDescription('Generate Rpc Service Interfaces');
}
public function handle()
{
go(function () {
$clients = $this->app->config->get('swoole.rpc.client', []);
$file = new PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes();
foreach ($clients as $name => $config) {
$client = new Client($config['host'], $config['port']);
$response = $client->sendAndRecv(Dispatcher::ACTION_INTERFACE);
$parserClass = Arr::get($config, 'parser', JsonParser::class);
/** @var ParserInterface $parser */
$parser = new $parserClass;
$result = $parser->decodeResponse($response);
if ($result instanceof Error) {
throw new RpcResponseException($result);
}
$namespace = $file->addNamespace("rpc\\contract\\${name}");
foreach ($result as $interface => $methods) {
$class = $namespace->addInterface($interface);
$class->addConstant("RPC", $name);
foreach ($methods as $methodName => ['parameters' => $parameters, 'returnType' => $returnType, 'comment' => $comment]) {
$method = $class->addMethod($methodName)
->setVisibility(ClassType::VISIBILITY_PUBLIC)
->setComment(Helpers::unformatDocComment($comment))
->setReturnType($returnType);
foreach ($parameters as $parameter) {
$param = $method->addParameter($parameter['name'])
->setTypeHint($parameter['type']);
if (array_key_exists("default", $parameter)) {
$param->setDefaultValue($parameter['default']);
}
}
}
}
}
file_put_contents($this->app->getBasePath() . 'rpc.php', $file);
$this->output->writeln('<info>Succeed!</info>');
});
}
}

View File

@@ -0,0 +1,154 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\swoole\command;
use think\console\Command;
use think\console\input\Argument;
use think\swoole\Manager;
use think\swoole\PidManager;
/**
* Swoole HTTP 命令行支持操作start|stop|restart|reload
* 支持应用配置目录下的swoole.php文件进行参数配置
*/
class Server extends Command
{
public function configure()
{
$this->setName('swoole')
->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload", 'start')
->setDescription('Swoole HTTP Server for ThinkPHP');
}
public function handle()
{
$this->checkEnvironment();
$action = $this->input->getArgument('action');
if (in_array($action, ['start', 'stop', 'reload', 'restart'])) {
$this->app->invokeMethod([$this, $action], [], true);
} else {
$this->output->writeln("<error>Invalid argument action:{$action}, Expected start|stop|restart|reload .</error>");
}
}
/**
* 检查环境
*/
protected function checkEnvironment()
{
if (!extension_loaded('swoole')) {
$this->output->error('Can\'t detect Swoole extension installed.');
exit(1);
}
if (!version_compare(swoole_version(), '4.3.1', 'ge')) {
$this->output->error('Your Swoole version must be higher than `4.3.1`.');
exit(1);
}
}
/**
* 启动server
* @access protected
* @param Manager $manager
* @param PidManager $pidManager
* @return void
*/
protected function start(Manager $manager, PidManager $pidManager)
{
if ($pidManager->isRunning()) {
$this->output->writeln('<error>swoole http server process is already running.</error>');
return;
}
$this->output->writeln('Starting swoole http server...');
$host = $manager->getConfig('server.host');
$port = $manager->getConfig('server.port');
$this->output->writeln("Swoole http server started: <http://{$host}:{$port}>");
$this->output->writeln('You can exit with <info>`CTRL-C`</info>');
$manager->run();
}
/**
* 柔性重启server
* @access protected
* @param PidManager $manager
* @return void
*/
protected function reload(PidManager $manager)
{
if (!$manager->isRunning()) {
$this->output->writeln('<error>no swoole http server process running.</error>');
return;
}
$this->output->writeln('Reloading swoole http server...');
if (!$manager->killProcess(SIGUSR1)) {
$this->output->error('> failure');
return;
}
$this->output->writeln('> success');
}
/**
* 停止server
* @access protected
* @param PidManager $manager
* @return void
*/
protected function stop(PidManager $manager)
{
if (!$manager->isRunning()) {
$this->output->writeln('<error>no swoole http server process running.</error>');
return;
}
$this->output->writeln('Stopping swoole http server...');
$isRunning = $manager->killProcess(SIGTERM, 15);
if ($isRunning) {
$this->output->error('Unable to stop the swoole_http_server process.');
return;
}
$this->output->writeln('> success');
}
/**
* 重启server
* @access protected
* @param Manager $manager
* @param PidManager $pidManager
* @return void
*/
protected function restart(Manager $manager, PidManager $pidManager)
{
if ($pidManager->isRunning()) {
$this->stop($pidManager);
}
$this->start($manager, $pidManager);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace think\swoole\concerns;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Server;
use think\App;
use think\Container;
use think\Cookie;
use think\Event;
use think\exception\Handle;
use think\Http;
use Throwable;
/**
* Trait InteractsWithHttp
* @package think\swoole\concerns
* @property App $app
* @property Container $container
* @method Server getServer()
*/
trait InteractsWithHttp
{
/**
* "onRequest" listener.
*
* @param Request $req
* @param Response $res
*/
public function onRequest($req, $res)
{
$args = func_get_args();
$this->runInSandbox(function (Http $http, Event $event, App $app) use ($args, $req, $res) {
$event->trigger('swoole.request', $args);
$request = $this->prepareRequest($req);
try {
$response = $this->handleRequest($http, $request);
} catch (Throwable $e) {
$response = $this->app
->make(Handle::class)
->render($request, $e);
}
$this->sendResponse($res, $response, $app->cookie);
});
}
protected function handleRequest(Http $http, $request)
{
$level = ob_get_level();
ob_start();
$response = $http->run($request);
$content = $response->getContent();
if (ob_get_level() == 0) {
ob_start();
}
$http->end($response);
if (ob_get_length() > 0) {
$response->content(ob_get_contents() . $content);
}
while (ob_get_level() > $level) {
ob_end_clean();
}
return $response;
}
protected function prepareRequest(Request $req)
{
$header = $req->header ?: [];
$server = $req->server ?: [];
foreach ($header as $key => $value) {
$server["http_" . str_replace('-', '_', $key)] = $value;
}
// 重新实例化请求对象 处理swoole请求数据
/** @var \think\Request $request */
$request = $this->app->make('request', [], true);
return $request->withHeader($header)
->withServer($server)
->withGet($req->get ?: [])
->withPost($req->post ?: [])
->withCookie($req->cookie ?: [])
->withInput($req->rawContent())
->withFiles($req->files ?: [])
->setBaseUrl($req->server['request_uri'])
->setUrl($req->server['request_uri'] . (!empty($req->server['query_string']) ? '?' . $req->server['query_string'] : ''))
->setPathinfo(ltrim($req->server['path_info'], '/'));
}
protected function sendResponse(Response $res, \think\Response $response, Cookie $cookie)
{
// 发送Header
foreach ($response->getHeader() as $key => $val) {
$res->header($key, $val);
}
// 发送状态码
$res->status($response->getCode());
foreach ($cookie->getCookie() as $name => $val) {
list($value, $expire, $option) = $val;
$res->cookie($name, $value, $expire, $option['path'], $option['domain'], $option['secure'] ? true : false, $option['httponly'] ? true : false);
}
$content = $response->getContent();
$res->end($content);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace think\swoole\concerns;
use RuntimeException;
use Swoole\Coroutine\Channel;
trait InteractsWithPool
{
/** @var Channel[] */
protected $pools = [];
protected $connectionCount = [];
/**
* 获取连接池
* @param $name
* @return Channel
*/
protected function getPool($name)
{
if (empty($this->pools[$name])) {
$this->pools[$name] = new Channel($this->getPoolMaxActive($name));
}
return $this->pools[$name];
}
protected function getPoolConnection($name)
{
$pool = $this->getPool($name);
if (!isset($this->connectionCount[$name])) {
$this->connectionCount[$name] = 0;
}
if ($this->connectionCount[$name] < $this->getPoolMaxActive($name)) {
//新建
$connection = $this->createPoolConnection($name);
$this->connectionCount[$name]++;
} else {
$connection = $pool->pop($this->getPoolMaxWaitTime($name));
if ($connection === false) {
throw new RuntimeException(sprintf(
'Borrow the connection timeout in %.2f(s), connections in pool: %d, all connections: %d',
$this->getPoolMaxWaitTime($name),
$pool->length(),
$this->connectionCount[$name] ?? 0
));
}
}
return $this->buildPoolConnection($connection, $pool);
}
abstract protected function buildPoolConnection($connection, Channel $pool);
abstract protected function createPoolConnection(string $name);
abstract protected function getPoolMaxActive($name): int;
abstract protected function getPoolMaxWaitTime($name): int;
public function __destruct()
{
foreach ($this->pools as $pool) {
while (true) {
if ($pool->isEmpty()) {
break;
}
$handler = $pool->pop(0.001);
unset($handler);
}
$pool->close();
}
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace think\swoole\concerns;
use Swoole\Coroutine\Channel;
trait InteractsWithPoolConnector
{
protected $handler;
protected $pool;
protected $release = true;
public function __construct($handler, Channel $pool)
{
$this->handler = $handler;
$this->pool = $pool;
}
public function __call($method, $arguments)
{
return $this->handler->{$method}(...$arguments);
}
public function release()
{
if (!$this->release) {
return;
}
$this->release = false;
if (!$this->pool->isFull()) {
$this->pool->push($this->handler, 0.001);
}
}
public function __destruct()
{
$this->release();
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace think\swoole\concerns;
use Swoole\Server;
use think\App;
use think\Event;
use think\helper\Str;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\rpc\client\Pool;
use think\swoole\rpc\JsonParser;
use think\swoole\rpc\server\Dispatcher;
/**
* Trait InteractsWithRpc
* @package think\swoole\concerns
* @property App $app
* @method Server getServer()
*/
trait InteractsWithRpc
{
protected $rpcEvents = [
'connect',
'receive',
'close',
];
protected $isRpcServer = false;
protected function prepareRpc()
{
if ($this->isRpcServer = $this->getConfig('rpc.server.enable', false)) {
$host = $this->getConfig('server.host');
$port = $this->getConfig('rpc.server.port', 9000);
$rpcServer = $this->getServer()->addlistener($host, $port, SWOOLE_SOCK_TCP);
$rpcServer->set([
'open_eof_check' => true,
'open_eof_split' => true,
'package_eof' => ParserInterface::EOF,
]);
$this->setRpcServerListeners($rpcServer);
}
$this->onEvent('workerStart', function () {
if ($this->isRpcServer) {
$this->bindRpcParser();
$this->bindRpcDispatcher();
}
$this->bindRpcClientPool();
});
}
/**
* @param Server\Port $server
*/
protected function setRpcServerListeners($server)
{
foreach ($this->rpcEvents as $event) {
$listener = Str::camel("on_rpc_$event");
$callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) {
$this->triggerEvent("rpc." . $event, func_get_args());
};
$server->on($event, $callback);
}
}
protected function bindRpcClientPool()
{
if (!empty($clients = $this->getConfig('rpc.client'))) {
$this->app->instance(Pool::class, new Pool($clients));
//引入rpc接口文件
if (file_exists($rpc = $this->app->getBasePath() . 'rpc.php')) {
include_once $rpc;
}
}
}
protected function bindRpcDispatcher()
{
$services = $this->getConfig('rpc.server.services', []);
$this->app->make(Dispatcher::class, [$services]);
}
protected function bindRpcParser()
{
$parserClass = $this->getConfig('rpc.server.parser', JsonParser::class);
$this->app->bind(ParserInterface::class, $parserClass);
$this->app->make(ParserInterface::class);
}
public function onRpcConnect(Server $server, int $fd, int $reactorId)
{
$args = func_get_args();
$this->runInSandbox(function (Event $event, Dispatcher $dispatcher) use ($fd, $server, $args) {
$event->trigger("swoole.rpc.Connect", $args);
}, $fd, true);
}
public function onRpcReceive(Server $server, $fd, $reactorId, $data)
{
$data = rtrim($data, ParserInterface::EOF);
$this->runInSandbox(function (Dispatcher $dispatcher) use ($fd, $data, $server) {
$dispatcher->dispatch($fd, $data);
}, $fd, true);
}
public function onRpcClose(Server $server, int $fd, int $reactorId)
{
$args = func_get_args();
$this->runInSandbox(function (Event $event) use ($args) {
$event->trigger("swoole.rpc.Close", $args);
}, $fd);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace think\swoole\concerns;
use Exception;
use Swoole\Runtime;
use Swoole\Server\Task;
use think\Event;
use think\swoole\PidManager;
/**
* Trait InteractsWithServer
* @package think\swoole\concerns
* @property PidManager $pidManager
*/
trait InteractsWithServer
{
/**
* "onStart" listener.
*/
public function onStart()
{
$this->setProcessName('master process');
$this->pidManager->create($this->getServer()->master_pid, $this->getServer()->manager_pid ?? 0);
$this->triggerEvent("start", func_get_args());
}
/**
* The listener of "managerStart" event.
*
* @return void
*/
public function onManagerStart()
{
$this->setProcessName('manager process');
$this->triggerEvent("managerStart", func_get_args());
}
/**
* "onWorkerStart" listener.
*
* @param \Swoole\Http\Server|mixed $server
*
* @throws Exception
*/
public function onWorkerStart($server)
{
Runtime::enableCoroutine($this->getConfig('coroutine.enable', true), $this->getConfig('coroutine.flags', SWOOLE_HOOK_ALL));
$this->clearCache();
$this->setProcessName($server->taskworker ? 'task process' : 'worker process');
$this->prepareApplication();
$this->bindSwooleTable();
$this->triggerEvent("workerStart");
}
/**
* Set onTask listener.
*
* @param mixed $server
* @param Task $task
*/
public function onTask($server, Task $task)
{
$this->runInSandbox(function (Event $event) use ($task) {
$event->trigger('swoole.task', $task);
});
}
/**
* Set onFinish listener.
*
* @param mixed $server
* @param string $taskId
* @param mixed $data
*/
public function onFinish($server, $taskId, $data)
{
$this->triggerEvent('finish', func_get_args());
}
/**
* Set onShutdown listener.
*/
public function onShutdown()
{
$this->triggerEvent('shutdown');
$this->pidManager->remove();
}
}

View File

@@ -0,0 +1,72 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole\concerns;
use Swoole\Table as SwooleTable;
use think\App;
use think\Container;
use think\swoole\Table;
/**
* Trait InteractsWithSwooleTable
*
* @property Container $container
* @property App $app
*/
trait InteractsWithSwooleTable
{
/**
* @var Table
*/
protected $currentTable;
/**
* Register customized swoole talbes.
*/
protected function createTables()
{
$this->currentTable = new Table();
$this->registerTables();
}
/**
* Register user-defined swoole tables.
*/
protected function registerTables()
{
$tables = $this->container->make('config')->get('swoole.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->currentTable->add($key, $table);
}
}
/**
* Bind swoole table to app container.
*/
protected function bindSwooleTable()
{
$this->app->instance(Table::class, $this->currentTable);
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace think\swoole\concerns;
use Swoole\Http\Request;
use Swoole\Server\Task;
use Swoole\Websocket\Frame;
use Swoole\Websocket\Server;
use think\App;
use think\Container;
use think\Event;
use think\helper\Str;
use think\swoole\contract\websocket\HandlerInterface;
use think\swoole\contract\websocket\ParserInterface;
use think\swoole\contract\websocket\RoomInterface;
use think\swoole\Websocket;
use think\swoole\websocket\Pusher;
use think\swoole\websocket\Room;
use think\swoole\websocket\socketio\Handler;
use think\swoole\websocket\socketio\Parser as SocketioParser;
/**
* Trait InteractsWithWebsocket
* @package think\swoole\concerns
*
* @property App $app
* @property Container $container
* @method \Swoole\Server getServer()
*/
trait InteractsWithWebsocket
{
/**
* @var boolean
*/
protected $isWebsocketServer = false;
/**
* @var RoomInterface
*/
protected $websocketRoom;
/**
* Websocket server events.
*
* @var array
*/
protected $wsEvents = ['open', 'message', 'close'];
/**
* "onOpen" listener.
*
* @param Server $server
* @param Request $req
*/
public function onOpen($server, $req)
{
/** @var Websocket $websocket */
$websocket = $this->app->make(Websocket::class);
$websocket->setSender($req->fd);
$this->runInSandbox(function (Event $event, HandlerInterface $handler) use ($req) {
$request = $this->prepareRequest($req);
if (!$handler->onOpen($req->fd, $request)) {
$event->trigger("swoole.websocket.Connect", $request);
}
}, $req->fd, true);
}
/**
* "onMessage" listener.
*
* @param Server $server
* @param Frame $frame
*/
public function onMessage($server, $frame)
{
/** @var Websocket $websocket */
$websocket = $this->app->make(Websocket::class);
$websocket->setSender($frame->fd);
$this->runInSandbox(function (Event $event, ParserInterface $parser, HandlerInterface $handler) use ($frame) {
if (!$handler->onMessage($frame)) {
$payload = $parser->decode($frame);
['event' => $name, 'data' => $data] = $payload;
$name = Str::studly($name);
if (!in_array($name, ['Close', 'Connect'])) {
$event->trigger("swoole.websocket." . $name, $data);
}
}
}, $frame->fd, true);
}
/**
* "onClose" listener.
*
* @param Server $server
* @param int $fd
* @param int $reactorId
*/
public function onClose($server, $fd, $reactorId)
{
if (!$this->isWebsocketServer($fd) || !$server instanceof Server) {
return;
}
/** @var Websocket $websocket */
$websocket = $this->app->make(Websocket::class);
$websocket->setSender($fd);
$this->runInSandbox(function (Event $event, HandlerInterface $handler) use ($websocket, $fd, $reactorId) {
try {
if (!$handler->onClose($fd, $reactorId)) {
$event->trigger("swoole.websocket.Close");
}
} finally {
// leave all rooms
$websocket->leave();
}
}, $fd);
}
/**
* Prepare settings if websocket is enabled.
*/
protected function prepareWebsocket()
{
if (!$this->isWebsocketServer = $this->getConfig('websocket.enable', false)) {
return;
}
$this->events = array_merge($this->events ?? [], $this->wsEvents);
$this->prepareWebsocketRoom();
$this->onEvent('workerStart', function () {
$this->bindWebsocketRoom();
$this->bindWebsocketParser();
$this->bindWebsocketHandler();
$this->prepareWebsocketListener();
});
}
/**
* Check if it's a websocket fd.
*
* @param int $fd
*
* @return bool
*/
protected function isWebsocketServer(int $fd): bool
{
return $this->getServer()->getClientInfo($fd)['websocket_status'] ?? false;
}
/**
* Prepare websocket room.
*/
protected function prepareWebsocketRoom()
{
// create room instance and initialize
$this->websocketRoom = $this->container->make(Room::class);
$this->websocketRoom->prepare();
}
protected function prepareWebsocketListener()
{
$listeners = $this->getConfig('websocket.listen', []);
foreach ($listeners as $event => $listener) {
$this->app->event->listen('swoole.websocket.' . Str::studly($event), $listener);
}
$subscribers = $this->getConfig('websocket.subscribe', []);
foreach ($subscribers as $subscriber) {
$this->app->event->observe($subscriber, 'swoole.websocket.');
}
//消息推送任务
$this->app->event->listen('swoole.task', function (Task $task, App $app) {
if ($this->isWebsocketPushPayload($task->data)) {
$pusher = $app->make(Pusher::class, $task->data['data']);
$pusher->push();
}
});
}
/**
* Prepare websocket handler for onOpen and onClose callback.
*
* @throws \Exception
*/
protected function bindWebsocketHandler()
{
$handlerClass = $this->getConfig('websocket.handler', Handler::class);
$this->app->bind(HandlerInterface::class, $handlerClass);
$this->app->make(HandlerInterface::class);
}
protected function bindWebsocketParser()
{
$parserClass = $this->getConfig('websocket.parser', SocketioParser::class);
$this->app->bind(ParserInterface::class, $parserClass);
$this->app->make(ParserInterface::class);
}
/**
* Bind room instance to app container.
*/
protected function bindWebsocketRoom(): void
{
$this->app->instance(Room::class, $this->websocketRoom);
}
/**
* Indicates if the payload is websocket push.
*
* @param mixed $payload
*
* @return boolean
*/
public function isWebsocketPushPayload($payload): bool
{
if (!is_array($payload)) {
return false;
}
return $this->isWebsocketServer
&& ($payload['action'] ?? null) === Websocket::PUSH_ACTION
&& array_key_exists('data', $payload);
}
}

View File

@@ -0,0 +1,94 @@
<?php
use think\swoole\websocket\socketio\Handler;
use think\swoole\websocket\socketio\Parser;
return [
'server' => [
'host' => env('SWOOLE_HOST', '127.0.0.1'), // 监听地址
'port' => env('SWOOLE_PORT', 80), // 监听端口
'mode' => SWOOLE_PROCESS, // 运行模式 默认为SWOOLE_PROCESS
'sock_type' => SWOOLE_SOCK_TCP, // sock type 默认为SWOOLE_SOCK_TCP
'options' => [
'pid_file' => runtime_path() . 'swoole.pid',
'log_file' => runtime_path() . 'swoole.log',
'daemonize' => false,
// Normally this value should be 1~4 times larger according to your cpu cores.
'reactor_num' => swoole_cpu_num(),
'worker_num' => swoole_cpu_num(),
'task_worker_num' => swoole_cpu_num(),
'task_enable_coroutine' => true,
'task_max_request' => 3000,
'enable_static_handler' => true,
'document_root' => root_path('public'),
'package_max_length' => 20 * 1024 * 1024,
'buffer_output_size' => 10 * 1024 * 1024,
'socket_buffer_size' => 128 * 1024 * 1024,
'max_request' => 3000,
'send_yield' => true,
],
],
'websocket' => [
'enable' => false,
'handler' => Handler::class,
'parser' => Parser::class,
'ping_interval' => 25000,
'ping_timeout' => 60000,
'room' => [
'type' => 'table',
'table' => [
'room_rows' => 4096,
'room_size' => 2048,
'client_rows' => 8192,
'client_size' => 2048,
],
'redis' => [
],
],
'listen' => [],
'subscribe' => [],
],
'rpc' => [
'server' => [
'enable' => false,
'port' => 9000,
'services' => [
],
],
'client' => [
],
],
'hot_update' => [
'enable' => env('APP_DEBUG', false),
'name' => ['*.php'],
'include' => [app_path()],
'exclude' => [],
],
//连接池
'pool' => [
'db' => [
'enable' => true,
'max_active' => 3,
'max_wait_time' => 5,
],
'cache' => [
'enable' => true,
'max_active' => 3,
'max_wait_time' => 5,
],
],
'coroutine' => [
'enable' => true,
'flags' => SWOOLE_HOOK_ALL,
],
'tables' => [],
//每个worker里需要预加载以共用的实例
'concretes' => [],
//重置器
'resetters' => [],
//每次请求前需要清空的实例
'instances' => [],
//每次请求前需要重新执行的服务
'services' => [],
];

View File

@@ -0,0 +1,17 @@
<?php
namespace think\swoole\contract;
use think\Container;
use think\swoole\Sandbox;
interface ResetterInterface
{
/**
* "handle" function for resetting app.
*
* @param Container $app
* @param Sandbox $sandbox
*/
public function handle(Container $app, Sandbox $sandbox);
}

View File

@@ -0,0 +1,39 @@
<?php
namespace think\swoole\contract\rpc;
use think\swoole\rpc\Protocol;
interface ParserInterface
{
const EOF = "\r\n\r\n";
/**
* @param Protocol $protocol
*
* @return string
*/
public function encode(Protocol $protocol): string;
/**
* @param string $string
*
* @return Protocol
*/
public function decode(string $string): Protocol;
/**
* @param string $string
*
* @return mixed
*/
public function decodeResponse(string $string);
/**
* @param mixed $result
*
* @return string
*/
public function encodeResponse($result): string;
}

View File

@@ -0,0 +1,42 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole\contract\websocket;
use Swoole\Websocket\Frame;
use think\Request;
interface HandlerInterface
{
/**
* "onOpen" listener.
*
* @param int $fd
* @param Request $request
*/
public function onOpen($fd, Request $request);
/**
* "onMessage" listener.
* only triggered when event handler not found
*
* @param Frame $frame
*/
public function onMessage(Frame $frame);
/**
* "onClose" listener.
*
* @param int $fd
* @param int $reactorId
*/
public function onClose($fd, $reactorId);
}

View File

@@ -0,0 +1,29 @@
<?php
namespace think\swoole\contract\websocket;
use Swoole\WebSocket\Frame;
interface ParserInterface
{
/**
* Encode output payload for websocket push.
*
* @param string $event
* @param mixed $data
*
* @return mixed
*/
public function encode(string $event, $data);
/**
* Input message on websocket connected.
* Define and return event name and payload data here.
*
* @param Frame $frame
*
* @return array
*/
public function decode($frame);
}

View File

@@ -0,0 +1,61 @@
<?php
namespace think\swoole\contract\websocket;
interface RoomInterface
{
/**
* Rooms key
*
* @const string
*/
public const ROOMS_KEY = 'rooms';
/**
* Descriptors key
*
* @const string
*/
public const DESCRIPTORS_KEY = 'fds';
/**
* Do some init stuffs before workers started.
*
* @return RoomInterface
*/
public function prepare(): RoomInterface;
/**
* Add multiple socket fds to a room.
*
* @param int fd
* @param array|string rooms
*/
public function add(int $fd, $rooms);
/**
* Delete multiple socket fds from a room.
*
* @param int fd
* @param array|string rooms
*/
public function delete(int $fd, $rooms);
/**
* Get all sockets by a room key.
*
* @param string room
*
* @return array
*/
public function getClients(string $room);
/**
* Get all rooms by a fd.
*
* @param int fd
*
* @return array
*/
public function getRooms(int $fd);
}

View File

@@ -0,0 +1,96 @@
<?php
namespace think\swoole\coroutine;
use Closure;
use Swoole\Coroutine;
class Context
{
/**
* The data in different coroutine environment.
*
* @var array
*/
protected static $data = [];
/**
* Get data by current coroutine id.
*
* @param string $key
*
* @param null $default
* @return mixed|null
*/
public static function getData(string $key, $default = null)
{
return static::$data[static::getCoroutineId()][$key] ?? $default;
}
public static function hasData(string $key)
{
return isset(static::$data[static::getCoroutineId()]) && array_key_exists($key, static::$data[static::getCoroutineId()]);
}
public static function rememberData(string $key, $value)
{
if (self::hasData($key)) {
return self::getData($key);
}
if ($value instanceof Closure) {
// 获取缓存数据
$value = $value();
}
self::setData($key, $value);
return $value;
}
/**
* Set data by current coroutine id.
*
* @param string $key
* @param $value
*/
public static function setData(string $key, $value)
{
static::$data[static::getCoroutineId()][$key] = $value;
}
/**
* Remove data by current coroutine id.
*
* @param string $key
*/
public static function removeData(string $key)
{
unset(static::$data[static::getCoroutineId()][$key]);
}
/**
* Get data keys by current coroutine id.
*/
public static function getDataKeys()
{
return array_keys(static::$data[static::getCoroutineId()] ?? []);
}
/**
* Clear data by current coroutine id.
*/
public static function clear()
{
unset(static::$data[static::getCoroutineId()]);
}
/**
* Get current coroutine id.
*/
public static function getCoroutineId()
{
return Coroutine::getuid();
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace think\swoole\exception;
use Exception;
class RpcClientException extends Exception
{
}

View File

@@ -0,0 +1,22 @@
<?php
namespace think\swoole\exception;
use Exception;
use think\swoole\rpc\Error;
class RpcResponseException extends Exception
{
protected $error;
public function __construct(Error $error)
{
parent::__construct($error->getMessage(), $error->getCode());
$this->error = $error;
}
public function getError()
{
return $this->error;
}
}

View File

@@ -0,0 +1,22 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace think\swoole\facade;
use think\Facade;
class Server extends Facade
{
protected static function getFacadeClass()
{
return 'swoole.server';
}
}

View File

@@ -0,0 +1,20 @@
<?php
if (!function_exists('swoole_cpu_num')) {
function swoole_cpu_num(): int
{
return 1;
}
}
if (!defined('SWOOLE_SOCK_TCP')) {
define('SWOOLE_SOCK_TCP', 1);
}
if (!defined('SWOOLE_PROCESS')) {
define('SWOOLE_PROCESS', 3);
}
if (!defined('SWOOLE_HOOK_ALL')) {
define('SWOOLE_HOOK_ALL', 1879048191);
}

View File

@@ -0,0 +1,32 @@
<?php
namespace think\swoole\middleware;
use Closure;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\VarDumper;
use think\Request;
class ResetVarDumper
{
protected $cloner;
public function __construct()
{
$this->cloner = new VarCloner();
$this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO);
}
public function handle(Request $request, Closure $next)
{
$prevHandler = VarDumper::setHandler(function ($var) {
$dumper = new HtmlDumper();
$dumper->dump($this->cloner->cloneVar($var));
});
$response = $next($request);
VarDumper::setHandler($prevHandler);
return $response;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace think\swoole\pool;
use Swoole\Coroutine\Channel;
use think\swoole\concerns\InteractsWithPool;
use think\swoole\coroutine\Context;
use think\swoole\pool\cache\Store;
class Cache extends \think\Cache
{
use InteractsWithPool;
protected function getPoolMaxActive($name): int
{
return $this->app->config->get('swoole.pool.cache.max_active', 3);
}
protected function getPoolMaxWaitTime($name): int
{
return $this->app->config->get('swoole.pool.cache.max_wait_time', 3);
}
/**
* 获取驱动实例
* @param null|string $name
* @return mixed
*/
protected function driver(string $name = null)
{
$name = $name ?: $this->getDefaultDriver();
return Context::rememberData("cache.store.{$name}", function () use ($name) {
return $this->getPoolConnection($name);
});
}
protected function buildPoolConnection($connection, Channel $pool)
{
return new Store($connection, $pool);
}
protected function createPoolConnection(string $name)
{
return $this->createDriver($name);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace think\swoole\pool;
use Swoole\Coroutine\Channel;
use think\Config;
use think\db\ConnectionInterface;
use think\swoole\concerns\InteractsWithPool;
use think\swoole\coroutine\Context;
use think\swoole\pool\db\Connection;
/**
* Class Db
* @package think\swoole\pool
* @property Config $config
*/
class Db extends \think\Db
{
use InteractsWithPool;
protected function getPoolMaxActive($name): int
{
return $this->config->get('swoole.pool.db.max_active', 3);
}
protected function getPoolMaxWaitTime($name): int
{
return $this->config->get('swoole.pool.db.max_wait_time', 3);
}
/**
* 创建数据库连接实例
* @access protected
* @param string|null $name 连接标识
* @param bool $force 强制重新连接
* @return ConnectionInterface
*/
protected function instance(string $name = null, bool $force = false): ConnectionInterface
{
if (empty($name)) {
$name = $this->getConfig('default', 'mysql');
}
if ($force) {
return $this->createConnection($name);
}
return Context::rememberData("db.connection.{$name}", function () use ($name) {
return $this->getPoolConnection($name);
});
}
protected function buildPoolConnection($connection, Channel $pool)
{
return new Connection($connection, $pool);
}
protected function createPoolConnection(string $name)
{
return $this->createConnection($name);
}
protected function getConnectionConfig(string $name): array
{
$config = parent::getConnectionConfig($name);
//打开断线重连
$config['break_reconnect'] = true;
return $config;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace think\swoole\pool\cache;
use think\swoole\concerns\InteractsWithPoolConnector;
class Store
{
use InteractsWithPoolConnector;
}

View File

@@ -0,0 +1,233 @@
<?php
namespace think\swoole\pool\db;
use Psr\SimpleCache\CacheInterface;
use think\db\BaseQuery;
use think\db\ConnectionInterface;
use think\DbManager;
use think\swoole\concerns\InteractsWithPoolConnector;
/**
* Class Connection
* @package think\swoole\pool\db
* @property ConnectionInterface $handler
*/
class Connection implements ConnectionInterface
{
use InteractsWithPoolConnector;
/**
* 获取当前连接器类对应的Query类
* @access public
* @return string
*/
public function getQueryClass(): string
{
return $this->handler->getQueryClass();
}
/**
* 连接数据库方法
* @access public
* @param array $config 接参数
* @param integer $linkNum 连接序号
* @return mixed
*/
public function connect(array $config = [], $linkNum = 0)
{
return $this->handler->connect($config, $linkNum);
}
/**
* 设置当前的数据库Db对象
* @access public
* @param DbManager $db
* @return void
*/
public function setDb(DbManager $db)
{
$this->handler->setDb($db);
}
/**
* 设置当前的缓存对象
* @access public
* @param CacheInterface $cache
* @return void
*/
public function setCache(CacheInterface $cache)
{
$this->handler->setCache($cache);
}
/**
* 获取数据库的配置参数
* @access public
* @param string $config 配置名称
* @return mixed
*/
public function getConfig(string $config = '')
{
return $this->handler->getConfig($config);
}
/**
* 关闭数据库(或者重新连接)
* @access public
*/
public function close()
{
return $this->handler->close();
}
/**
* 查找单条记录
* @access public
* @param BaseQuery $query 查询对象
* @return array
*/
public function find(BaseQuery $query): array
{
return $this->handler->find($query);
}
/**
* 查找记录
* @access public
* @param BaseQuery $query 查询对象
* @return array
*/
public function select(BaseQuery $query): array
{
return $this->handler->select($query);
}
/**
* 插入记录
* @access public
* @param BaseQuery $query 查询对象
* @param boolean $getLastInsID 返回自增主键
* @return mixed
*/
public function insert(BaseQuery $query, bool $getLastInsID = false)
{
return $this->handler->insert($query, $getLastInsID);
}
/**
* 批量插入记录
* @access public
* @param BaseQuery $query 查询对象
* @param mixed $dataSet 数据集
* @return integer
* @throws \Exception
* @throws \Throwable
*/
public function insertAll(BaseQuery $query, array $dataSet = []): int
{
return $this->handler->insertAll($query, $dataSet);
}
/**
* 更新记录
* @access public
* @param BaseQuery $query 查询对象
* @return integer
*/
public function update(BaseQuery $query): int
{
return $this->handler->update($query);
}
/**
* 删除记录
* @access public
* @param BaseQuery $query 查询对象
* @return int
*/
public function delete(BaseQuery $query): int
{
return $this->handler->delete($query);
}
/**
* 得到某个字段的值
* @access public
* @param BaseQuery $query 查询对象
* @param string $field 字段名
* @param mixed $default 默认值
* @return mixed
*/
public function value(BaseQuery $query, string $field, $default = null)
{
return $this->handler->value($query, $field, $default);
}
/**
* 得到某个列的数组
* @access public
* @param BaseQuery $query 查询对象
* @param string $column 字段名 多个字段用逗号分隔
* @param string $key 索引
* @return array
*/
public function column(BaseQuery $query, string $column, string $key = ''): array
{
return $this->handler->column($query, $column, $key);
}
/**
* 执行数据库事务
* @access public
* @param callable $callback 数据操作方法回调
* @return mixed
* @throws \Throwable
*/
public function transaction(callable $callback)
{
return $this->handler->transaction($callback);
}
/**
* 启动事务
* @access public
* @return void
* @throws \Exception
*/
public function startTrans()
{
$this->handler->startTrans();
}
/**
* 用于非自动提交状态下面的查询提交
* @access public
* @return void
*/
public function commit()
{
$this->handler->commit();
}
/**
* 事务回滚
* @access public
* @return void
*/
public function rollback()
{
$this->handler->commit();
}
/**
* 获取最近一次查询的sql语句
* @access public
* @return string
*/
public function getLastSql(): string
{
return $this->handler->getLastSql();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace think\swoole\resetters;
use think\Container;
use think\swoole\Sandbox;
use think\swoole\contract\ResetterInterface;
class ClearInstances implements ResetterInterface
{
public function handle(Container $app, Sandbox $sandbox)
{
$instances = ['log'];
$instances = array_merge($instances, $sandbox->getConfig()->get('swoole.instances', []));
foreach ($instances as $instance) {
$app->delete($instance);
}
return $app;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace think\swoole\resetters;
use think\Container;
use think\swoole\Sandbox;
use think\swoole\contract\ResetterInterface;
class ResetConfig implements ResetterInterface
{
public function handle(Container $app, Sandbox $sandbox)
{
$app->instance('config', clone $sandbox->getConfig());
return $app;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace think\swoole\resetters;
use think\Container;
use think\swoole\Sandbox;
use think\swoole\contract\ResetterInterface;
/**
* Class ResetEvent
* @package think\swoole\resetters
* @property Container $app;
*/
class ResetEvent implements ResetterInterface
{
public function handle(Container $app, Sandbox $sandbox)
{
$event = clone $sandbox->getEvent();
$closure = function () use ($app) {
$this->app = $app;
};
$resetEvent = $closure->bindTo($event, $event);
$resetEvent();
$app->instance('event', $event);
return $app;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace think\swoole\resetters;
use think\Container;
use think\swoole\contract\ResetterInterface;
use think\swoole\Sandbox;
/**
* Class ResetService
* @package think\swoole\resetters
* @property Container $app;
*/
class ResetService implements ResetterInterface
{
/**
* "handle" function for resetting app.
*
* @param Container $app
* @param Sandbox $sandbox
*/
public function handle(Container $app, Sandbox $sandbox)
{
foreach ($sandbox->getServices() as $service) {
$this->rebindServiceContainer($app, $service);
if (method_exists($service, 'register')) {
$service->register();
}
if (method_exists($service, 'boot')) {
$app->invoke([$service, 'boot']);
}
}
}
protected function rebindServiceContainer($app, $service)
{
$closure = function () use ($app) {
$this->app = $app;
};
$resetService = $closure->bindTo($service, $service);
$resetService();
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace think\swoole\rpc;
class Error implements \JsonSerializable
{
/**
* @var int
*/
protected $code = 0;
/**
* @var string
*/
protected $message = '';
/**
* @var mixed
*/
protected $data;
/**
* @param int $code
* @param string $message
* @param mixed $data
*
* @return Error
*/
public static function make(int $code, string $message, $data = null): self
{
$instance = new static();
$instance->code = $code;
$instance->message = $message;
$instance->data = $data;
return $instance;
}
/**
* @return int
*/
public function getCode(): int
{
return $this->code;
}
/**
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* @return mixed
*/
public function getData()
{
return $this->data;
}
public function jsonSerialize()
{
return [
'code' => $this->code,
'message' => $this->message,
'data' => $this->data,
];
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace think\swoole\rpc;
use Exception;
use think\swoole\contract\rpc\ParserInterface;
class JsonParser implements ParserInterface
{
/**
* Json-rpc version
*/
const VERSION = '2.0';
const DELIMITER = "@";
/**
* @param Protocol $protocol
*
* @return string
*/
public function encode(Protocol $protocol): string
{
$interface = $protocol->getInterface();
$methodName = $protocol->getMethod();
$method = $interface . self::DELIMITER . $methodName;
$data = [
'jsonrpc' => self::VERSION,
'method' => $method,
'params' => $protocol->getParams(),
'id' => '',
];
$string = json_encode($data, JSON_UNESCAPED_UNICODE);
return $string;
}
/**
* @param string $string
*
* @return Protocol
*/
public function decode(string $string): Protocol
{
$data = json_decode($string, true);
$error = json_last_error();
if ($error != JSON_ERROR_NONE) {
throw new Exception(
sprintf('Data(%s) is not json format!', $string)
);
}
$method = $data['method'] ?? '';
$params = $data['params'] ?? [];
if (empty($method)) {
throw new Exception(
sprintf('Method(%s) cant not be empty!', $string)
);
}
$methodAry = explode(self::DELIMITER, $method);
if (count($methodAry) < 2) {
throw new Exception(
sprintf('Method(%s) is bad format!', $method)
);
}
[$interfaceClass, $methodName] = $methodAry;
if (empty($interfaceClass) || empty($methodName)) {
throw new Exception(
sprintf('Interface(%s) or Method(%s) can not be empty!', $interfaceClass, $method)
);
}
return Protocol::make($interfaceClass, $methodName, $params);
}
/**
* @param string $string
*
* @return mixed
*/
public function decodeResponse(string $string)
{
$data = json_decode($string, true);
if (array_key_exists('result', $data)) {
return $data['result'];
}
$code = $data['error']['code'] ?? 0;
$message = $data['error']['message'] ?? '';
$data = $data['error']['data'] ?? null;
return Error::make($code, $message, $data);
}
/**
* @param mixed $result
*
* @return string
*/
public function encodeResponse($result): string
{
$data = [
'jsonrpc' => self::VERSION,
];
if ($result instanceof Error) {
$data['error'] = $result;
} else {
$data['result'] = $result;
}
$string = json_encode($data);
return $string;
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace think\swoole\rpc;
class Protocol
{
/**
* @var string
*/
private $interface = '';
/**
* @var string
*/
private $method = '';
/**
* @var array
*/
private $params = [];
/**
* Replace constructor
*
* @param string $interface
* @param string $method
* @param array $params
*
* @return Protocol
*/
public static function make(string $interface, string $method, array $params)
{
$instance = new static();
$instance->interface = $interface;
$instance->method = $method;
$instance->params = $params;
return $instance;
}
/**
* @return string
*/
public function getInterface(): string
{
return $this->interface;
}
/**
* @return string
*/
public function getMethod(): string
{
return $this->method;
}
/**
* @return array
*/
public function getParams(): array
{
return $this->params;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace think\swoole\rpc\client;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\exception\RpcClientException;
/**
* Class Client
* @package think\swoole\rpc\client
*/
class Client
{
protected $host;
protected $port;
protected $timeout;
protected $options;
/** @var \Swoole\Coroutine\Client */
protected $handler;
public function __construct($host, $port, $timeout = 0.5, $options = [])
{
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
$this->options = $options;
$this->connect();
}
public function sendAndRecv(string $data, bool $reconnect = false)
{
if ($reconnect) {
$this->connect();
}
try {
if (!$this->send($data)) {
throw new RpcClientException(swoole_strerror($this->handler->errCode), $this->handler->errCode);
}
$result = $this->handler->recv();
if ($result === false || empty($result)) {
throw new RpcClientException(swoole_strerror($this->handler->errCode), $this->handler->errCode);
}
return $result;
} catch (RpcClientException $e) {
if ($reconnect) {
throw $e;
}
return $this->sendAndRecv($data, true);
}
}
public function send($data)
{
return $this->handler->send($data . ParserInterface::EOF);
}
protected function connect()
{
$client = new \Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
$client->set([
'open_eof_check' => true,
'open_eof_split' => true,
'package_eof' => ParserInterface::EOF,
]);
if (!$client->connect($this->host, $this->port, $this->timeout)) {
throw new RpcClientException(
sprintf('Connect failed host=%s port=%d', $this->host, $this->port)
);
}
$this->handler = $client;
}
public function __destruct()
{
if ($this->handler) {
$this->handler->close();
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace think\swoole\rpc\client;
use think\swoole\concerns\InteractsWithPoolConnector;
/**
* Class Connection
* @package think\swoole\rpc\client
* @mixin Client
*/
class Connection
{
use InteractsWithPoolConnector;
}

View File

@@ -0,0 +1,57 @@
<?php
namespace think\swoole\rpc\client;
use Swoole\Coroutine\Channel;
use think\helper\Arr;
use think\swoole\concerns\InteractsWithPool;
class Pool
{
use InteractsWithPool;
protected $clients;
public function __construct($clients)
{
$this->clients = $clients;
}
protected function getPoolMaxActive($name)
{
return $this->getClientConfig($name, 'max_active', 3);
}
protected function getPoolMaxWaitTime($name)
{
return $this->getClientConfig($name, 'max_wait_time', 3);
}
public function getClientConfig($client, $name, $default = null)
{
return Arr::get($this->clients, $client . "." . $name, $default);
}
/**
* @param $name
* @return Connection
*/
public function connect($name)
{
return $this->getPoolConnection($name);
}
protected function buildPoolConnection($client, Channel $pool)
{
return new Connection($client, $pool);
}
protected function createPoolConnection(string $name)
{
$host = $this->getClientConfig($name, 'host', '127.0.0.1');
$port = $this->getClientConfig($name, 'port', 9000);
$timeout = $this->getClientConfig($name, 'timeout', 0.5);
return new Client($host, $port, $timeout);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace think\swoole\rpc\client;
use InvalidArgumentException;
use Nette\PhpGenerator\Factory;
use Nette\PhpGenerator\PhpFile;
use ReflectionClass;
use RuntimeException;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\exception\RpcResponseException;
use think\swoole\rpc\Error;
use think\swoole\rpc\JsonParser;
use think\swoole\rpc\Protocol;
class Proxy
{
protected $client;
protected $interface;
/** @var Pool */
protected $pool;
/** @var ParserInterface */
protected $parser;
public function __construct(Pool $pool)
{
$this->pool = $pool;
$parserClass = $this->pool->getClientConfig($this->client, 'parser', JsonParser::class);
$this->parser = new $parserClass;
}
protected function proxyCall($method, $params)
{
$protocol = Protocol::make($this->interface, $method, $params);
$data = $this->parser->encode($protocol);
$client = $this->pool->connect($this->client);
$response = $client->sendAndRecv($data);
$client->release();
$result = $this->parser->decodeResponse($response);
if ($result instanceof Error) {
throw new RpcResponseException($result);
}
return $result;
}
public static function getClassName($interface)
{
if (!interface_exists($interface)) {
throw new InvalidArgumentException(
sprintf('%s must be exist interface!', $interface)
);
}
$name = constant($interface . "::RPC");
$proxyName = class_basename($interface) . "Service";
$className = "rpc\\service\\${name}\\{$proxyName}";
if (!class_exists($className, false)) {
$file = new PhpFile;
$namespace = $file->addNamespace("rpc\\service\\${name}");
$namespace->addUse(Proxy::class);
$namespace->addUse($interface);
$class = $namespace->addClass($proxyName);
$class->setExtends(Proxy::class);
$class->addImplement($interface);
$class->addProperty('client', $name);
$class->addProperty('interface', class_basename($interface));
$reflection = new ReflectionClass($interface);
foreach ($reflection->getMethods() as $methodRef) {
$method = (new Factory)->fromMethodReflection($methodRef);
$method->setBody("return \$this->proxyCall('{$methodRef->getName()}', func_get_args());");
$class->addMember($method);
}
if (function_exists('eval')) {
eval($file);
} else {
$proxyFile = sprintf('%s/%s.php', sys_get_temp_dir(), $proxyName);
$result = file_put_contents($proxyFile, $file);
if ($result === false) {
throw new RuntimeException(sprintf('Proxy file(%s) generate fail', $proxyFile));
}
require $proxyFile;
unlink($proxyFile);
}
}
return $className;
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace think\swoole\rpc\server;
use Exception;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use Swoole\Server;
use think\App;
use think\swoole\contract\rpc\ParserInterface;
use think\swoole\rpc\Error;
use Throwable;
class Dispatcher
{
const ACTION_INTERFACE = '@action_interface';
/**
* Parser error
*/
const PARSER_ERROR = -32700;
/**
* Invalid Request
*/
const INVALID_REQUEST = -32600;
/**
* Method not found
*/
const METHOD_NOT_FOUND = -32601;
/**
* Invalid params
*/
const INVALID_PARAMS = -32602;
/**
* Internal error
*/
const INTERNAL_ERROR = -32603;
protected $app;
protected $parser;
protected $services;
protected $server;
public function __construct(App $app, ParserInterface $parser, Server $server, $services)
{
$this->app = $app;
$this->parser = $parser;
$this->server = $server;
$this->prepareServices($services);
}
/**
* 获取服务接口
* @param $services
* @throws \ReflectionException
*/
protected function prepareServices($services)
{
foreach ($services as $className) {
$reflectionClass = new ReflectionClass($className);
$interfaces = $reflectionClass->getInterfaceNames();
foreach ($interfaces as $interface) {
$this->services[class_basename($interface)] = [
'interface' => $interface,
'class' => $className,
];
}
}
}
/**
* 获取接口信息
* @return array
*/
protected function getInterfaces()
{
$interfaces = [];
foreach ($this->services as $key => ['interface' => $interface]) {
$interfaces[$key] = $this->getMethods($interface);
}
return $interfaces;
}
protected function getMethods($interface)
{
$methods = [];
$reflection = new ReflectionClass($interface);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$returnType = $method->getReturnType();
if ($returnType instanceof ReflectionNamedType) {
$returnType = $returnType->getName();
}
$methods[$method->getName()] = [
'parameters' => $this->getParameters($method),
'returnType' => $returnType,
'comment' => $method->getDocComment(),
];
}
return $methods;
}
protected function getParameters(ReflectionMethod $method)
{
$parameters = [];
foreach ($method->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType) {
$type = $type->getName();
}
$param = [
'name' => $parameter->getName(),
'type' => $type,
];
if ($parameter->isOptional()) {
$param['default'] = $parameter->getDefaultValue();
}
$parameters[] = $param;
}
return $parameters;
}
/**
* 调度
* @param int $fd
* @param string $data
*/
public function dispatch(int $fd, string $data)
{
try {
if ($data === Dispatcher::ACTION_INTERFACE) {
$result = $this->getInterfaces();
} else {
$protocol = $this->parser->decode($data);
$interface = $protocol->getInterface();
$method = $protocol->getMethod();
$params = $protocol->getParams();
$service = $this->services[$interface] ?? null;
if (empty($service)) {
throw new Exception(
sprintf('Service %s is not founded!', $interface),
self::INVALID_REQUEST
);
}
$result = $this->app->invoke([$this->app->make($service['class']), $method], $params);
}
} catch (Throwable | Exception $e) {
$result = Error::make($e->getCode(), $e->getMessage());
}
$data = $this->parser->encodeResponse($result);
$this->server->send($fd, $data . ParserInterface::EOF);
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace think\swoole\websocket;
use Swoole\Server;
/**
* Class Pusher
*/
class Pusher
{
/**
* @var Server
*/
protected $server;
/**
* @var int
*/
protected $sender;
/**
* @var array
*/
protected $descriptors;
/**
* @var bool
*/
protected $broadcast;
/**
* @var bool
*/
protected $assigned;
/**
* @var string
*/
protected $payload;
/**
* Push constructor.
*
* @param Server $server
* @param int $sender
* @param array $descriptors
* @param bool $broadcast
* @param bool $assigned
* @param string $payload
*/
public function __construct(
Server $server,
string $payload,
int $sender = 0,
array $descriptors = [],
bool $broadcast = false,
bool $assigned = false
)
{
$this->sender = $sender;
$this->descriptors = $descriptors;
$this->broadcast = $broadcast;
$this->assigned = $assigned;
$this->payload = $payload;
$this->server = $server;
}
/**
* @return int
*/
public function getSender(): int
{
return $this->sender;
}
/**
* @return array
*/
public function getDescriptors(): array
{
return $this->descriptors;
}
/**
* @param int $descriptor
*
* @return self
*/
public function addDescriptor($descriptor): self
{
return $this->addDescriptors([$descriptor]);
}
/**
* @param array $descriptors
*
* @return self
*/
public function addDescriptors(array $descriptors): self
{
$this->descriptors = array_values(
array_unique(
array_merge($this->descriptors, $descriptors)
)
);
return $this;
}
/**
* @param int $descriptor
*
* @return bool
*/
public function hasDescriptor(int $descriptor): bool
{
return in_array($descriptor, $this->descriptors);
}
/**
* @return bool
*/
public function isBroadcast(): bool
{
return $this->broadcast;
}
/**
* @return bool
*/
public function isAssigned(): bool
{
return $this->assigned;
}
/**
* @return string
*/
public function getPayload(): string
{
return $this->payload;
}
/**
* @return bool
*/
public function shouldBroadcast(): bool
{
return $this->broadcast && empty($this->descriptors) && !$this->assigned;
}
/**
* Returns all descriptors that are websocket
*
* @return array
*/
protected function getWebsocketConnections(): array
{
return array_filter(iterator_to_array($this->server->connections), function ($fd) {
return (bool) $this->server->getClientInfo($fd)['websocket_status'] ?? false;
});
}
/**
* @param int $fd
*
* @return bool
*/
protected function shouldPushToDescriptor(int $fd): bool
{
if (!$this->server->exist($fd)) {
return false;
}
return $this->broadcast ? $this->sender !== (int) $fd : true;
}
/**
* Push message to related descriptors
* @return void
*/
public function push(): void
{
// attach sender if not broadcast
if (!$this->broadcast && $this->sender && !$this->hasDescriptor($this->sender)) {
$this->addDescriptor($this->sender);
}
// check if to broadcast to other clients
if ($this->shouldBroadcast()) {
$this->addDescriptors($this->getWebsocketConnections());
}
// push message to designated fds
foreach ($this->descriptors as $descriptor) {
if ($this->shouldPushToDescriptor($descriptor)) {
$this->server->push($descriptor, $this->payload);
}
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace think\swoole\websocket;
use think\Manager;
use think\swoole\websocket\room\Table;
/**
* Class Room
* @package think\swoole\websocket
* @mixin Table
*/
class Room extends Manager
{
protected $namespace = "\\think\\swoole\\websocket\\room\\";
protected function resolveConfig(string $name)
{
return $this->app->config->get("swoole.websocket.room.{$name}", []);
}
/**
* 默认驱动
* @return string|null
*/
public function getDefaultDriver()
{
return $this->app->config->get('swoole.websocket.room.type', 'table');
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace think\swoole\websocket;
use Swoole\Websocket\Frame;
use think\swoole\contract\websocket\ParserInterface;
class SimpleParser implements ParserInterface
{
/**
* Encode output payload for websocket push.
*
* @param string $event
* @param mixed $data
*
* @return mixed
*/
public function encode(string $event, $data)
{
return json_encode(
[
'event' => $event,
'data' => $data,
]
);
}
/**
* Input message on websocket connected.
* Define and return event name and payload data here.
*
* @param Frame $frame
*
* @return array
*/
public function decode($frame)
{
$data = json_decode($frame->data, true);
return [
'event' => $data['event'] ?? null,
'data' => $data['data'] ?? null,
];
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace think\swoole\websocket\room;
use InvalidArgumentException;
use Redis as PHPRedis;
use think\helper\Arr;
use think\swoole\contract\websocket\RoomInterface;
/**
* Class RedisRoom
*/
class Redis implements RoomInterface
{
/**
* @var PHPRedis
*/
protected $redis;
/**
* @var array
*/
protected $config;
/**
* @var string
*/
protected $prefix = 'swoole:';
/**
* RedisRoom constructor.
*
* @param array $config
*/
public function __construct(array $config)
{
$this->config = $config;
if ($prefix = Arr::get($this->config, 'prefix')) {
$this->prefix = $prefix;
}
}
/**
* @return RoomInterface
*/
public function prepare(): RoomInterface
{
$this->cleanRooms();
//关闭redis
$this->redis->close();
$this->redis = null;
return $this;
}
/**
* Set redis client.
*
*/
protected function getRedis()
{
if (!$this->redis) {
$host = Arr::get($this->config, 'host', '127.0.0.1');
$port = Arr::get($this->config, 'port', 6379);
$this->redis = new PHPRedis();
$this->redis->pconnect($host, $port);
}
return $this->redis;
}
/**
* Add multiple socket fds to a room.
*
* @param int fd
* @param array|string rooms
*/
public function add(int $fd, $rooms)
{
$rooms = is_array($rooms) ? $rooms : [$rooms];
$this->addValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY);
foreach ($rooms as $room) {
$this->addValue($room, [$fd], RoomInterface::ROOMS_KEY);
}
}
/**
* Delete multiple socket fds from a room.
*
* @param int fd
* @param array|string rooms
*/
public function delete(int $fd, $rooms)
{
$rooms = is_array($rooms) ? $rooms : [$rooms];
$rooms = count($rooms) ? $rooms : $this->getRooms($fd);
$this->removeValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY);
foreach ($rooms as $room) {
$this->removeValue($room, [$fd], RoomInterface::ROOMS_KEY);
}
}
/**
* Add value to redis.
*
* @param $key
* @param array $values
* @param string $table
*
* @return $this
*/
protected function addValue($key, array $values, string $table)
{
$this->checkTable($table);
$redisKey = $this->getKey($key, $table);
$pipe = $this->getRedis()->multi(PHPRedis::PIPELINE);
foreach ($values as $value) {
$pipe->sadd($redisKey, $value);
}
$pipe->exec();
return $this;
}
/**
* Remove value from reddis.
*
* @param $key
* @param array $values
* @param string $table
*
* @return $this
*/
protected function removeValue($key, array $values, string $table)
{
$this->checkTable($table);
$redisKey = $this->getKey($key, $table);
$pipe = $this->getRedis()->multi(PHPRedis::PIPELINE);
foreach ($values as $value) {
$pipe->srem($redisKey, $value);
}
$pipe->exec();
return $this;
}
/**
* Get all sockets by a room key.
*
* @param string room
*
* @return array
*/
public function getClients(string $room)
{
return $this->getValue($room, RoomInterface::ROOMS_KEY) ?? [];
}
/**
* Get all rooms by a fd.
*
* @param int fd
*
* @return array
*/
public function getRooms(int $fd)
{
return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?? [];
}
/**
* Check table for rooms and descriptors.
*
* @param string $table
*/
protected function checkTable(string $table)
{
if (!in_array($table, [RoomInterface::ROOMS_KEY, RoomInterface::DESCRIPTORS_KEY])) {
throw new InvalidArgumentException("Invalid table name: `{$table}`.");
}
}
/**
* Get value.
*
* @param string $key
* @param string $table
*
* @return array
*/
protected function getValue(string $key, string $table)
{
$this->checkTable($table);
return $this->getRedis()->smembers($this->getKey($key, $table));
}
/**
* Get key.
*
* @param string $key
* @param string $table
*
* @return string
*/
protected function getKey(string $key, string $table)
{
return "{$this->prefix}{$table}:{$key}";
}
/**
* Clean all rooms.
*/
protected function cleanRooms(): void
{
if (count($keys = $this->getRedis()->keys("{$this->prefix}*"))) {
$this->getRedis()->del($keys);
}
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace think\swoole\websocket\room;
use InvalidArgumentException;
use Swoole\Table as SwooleTable;
use think\swoole\contract\websocket\RoomInterface;
class Table implements RoomInterface
{
/**
* @var array
*/
protected $config = [
'room_rows' => 4096,
'room_size' => 2048,
'client_rows' => 8192,
'client_size' => 2048,
];
/**
* @var SwooleTable
*/
protected $rooms;
/**
* @var SwooleTable
*/
protected $fds;
/**
* TableRoom constructor.
*
* @param array $config
*/
public function __construct(array $config)
{
$this->config = array_merge($this->config, $config);
}
/**
* Do some init stuffs before workers started.
*
* @return RoomInterface
*/
public function prepare(): RoomInterface
{
$this->initRoomsTable();
$this->initFdsTable();
return $this;
}
/**
* Add multiple socket fds to a room.
*
* @param int fd
* @param array|string rooms
*/
public function add(int $fd, $roomNames)
{
$rooms = $this->getRooms($fd);
$roomNames = is_array($roomNames) ? $roomNames : [$roomNames];
foreach ($roomNames as $room) {
$fds = $this->getClients($room);
if (in_array($fd, $fds)) {
continue;
}
$fds[] = $fd;
$rooms[] = $room;
$this->setClients($room, $fds);
}
$this->setRooms($fd, $rooms);
}
/**
* Delete multiple socket fds from a room.
*
* @param int fd
* @param array|string rooms
*/
public function delete(int $fd, $roomNames = [])
{
$allRooms = $this->getRooms($fd);
$roomNames = is_array($roomNames) ? $roomNames : [$roomNames];
$rooms = count($roomNames) ? $roomNames : $allRooms;
$removeRooms = [];
foreach ($rooms as $room) {
$fds = $this->getClients($room);
if (!in_array($fd, $fds)) {
continue;
}
$this->setClients($room, array_values(array_diff($fds, [$fd])));
$removeRooms[] = $room;
}
$this->setRooms($fd, collect($allRooms)->diff($removeRooms)->values()->toArray());
}
/**
* Get all sockets by a room key.
*
* @param string room
*
* @return array
*/
public function getClients(string $room)
{
return $this->getValue($room, RoomInterface::ROOMS_KEY) ?? [];
}
/**
* Get all rooms by a fd.
*
* @param int fd
*
* @return array
*/
public function getRooms(int $fd)
{
return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?? [];
}
/**
* @param string $room
* @param array $fds
*
* @return $this
*/
protected function setClients(string $room, array $fds)
{
return $this->setValue($room, $fds, RoomInterface::ROOMS_KEY);
}
/**
* @param int $fd
* @param array $rooms
*
* @return $this
*/
protected function setRooms(int $fd, array $rooms)
{
return $this->setValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY);
}
/**
* Init rooms table
*/
protected function initRoomsTable(): void
{
$this->rooms = new SwooleTable($this->config['room_rows']);
$this->rooms->column('value', SwooleTable::TYPE_STRING, $this->config['room_size']);
$this->rooms->create();
}
/**
* Init descriptors table
*/
protected function initFdsTable()
{
$this->fds = new SwooleTable($this->config['client_rows']);
$this->fds->column('value', SwooleTable::TYPE_STRING, $this->config['client_size']);
$this->fds->create();
}
/**
* Set value to table
*
* @param $key
* @param array $value
* @param string $table
*
* @return $this
*/
public function setValue($key, array $value, string $table)
{
$this->checkTable($table);
$this->$table->set($key, ['value' => json_encode($value)]);
return $this;
}
/**
* Get value from table
*
* @param string $key
* @param string $table
*
* @return array|mixed
*/
public function getValue(string $key, string $table)
{
$this->checkTable($table);
$value = $this->$table->get($key);
return $value ? json_decode($value['value'], true) : [];
}
/**
* Check table for exists
*
* @param string $table
*/
protected function checkTable(string $table)
{
if (!property_exists($this, $table) || !$this->$table instanceof SwooleTable) {
throw new InvalidArgumentException("Invalid table name: `{$table}`.");
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace think\swoole\websocket\socketio;
use think\Config;
use think\Cookie;
use think\Request;
class Controller
{
protected $transports = ['polling', 'websocket'];
public function upgrade(Request $request, Config $config, Cookie $cookie)
{
if (!in_array($request->param('transport'), $this->transports)) {
return json(
[
'code' => 0,
'message' => 'Transport unknown',
],
400
);
}
if ($request->has('sid')) {
$response = response('1:6');
} else {
$sid = base64_encode(uniqid());
$payload = json_encode(
[
'sid' => $sid,
'upgrades' => ['websocket'],
'pingInterval' => $config->get('swoole.websocket.ping_interval'),
'pingTimeout' => $config->get('swoole.websocket.ping_timeout'),
]
);
$cookie->set('io', $sid);
$response = response('97:0' . $payload . '2:40');
}
return $response->contentType('text/plain');
}
public function reject(Request $request)
{
return json(
[
'code' => 3,
'message' => 'Bad request',
],
400
);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace think\swoole\websocket\socketio;
use Swoole\Server;
use Swoole\Websocket\Frame;
use Swoole\WebSocket\Server as WebsocketServer;
use think\Config;
use think\Request;
use think\swoole\contract\websocket\HandlerInterface;
class Handler implements HandlerInterface
{
/** @var WebsocketServer */
protected $server;
/** @var Config */
protected $config;
public function __construct(Server $server, Config $config)
{
$this->server = $server;
$this->config = $config;
}
/**
* "onOpen" listener.
*
* @param int $fd
* @param Request $request
*/
public function onOpen($fd, Request $request)
{
if (!$request->param('sid')) {
$payload = json_encode(
[
'sid' => base64_encode(uniqid()),
'upgrades' => [],
'pingInterval' => $this->config->get('swoole.websocket.ping_interval'),
'pingTimeout' => $this->config->get('swoole.websocket.ping_timeout'),
]
);
$initPayload = Packet::OPEN . $payload;
$connectPayload = Packet::MESSAGE . Packet::CONNECT;
$this->server->push($fd, $initPayload);
$this->server->push($fd, $connectPayload);
}
}
/**
* "onMessage" listener.
* only triggered when event handler not found
*
* @param Frame $frame
* @return bool
*/
public function onMessage(Frame $frame)
{
$packet = $frame->data;
if (Packet::getPayload($packet)) {
return false;
}
$this->checkHeartbeat($frame->fd, $packet);
return true;
}
/**
* "onClose" listener.
*
* @param int $fd
* @param int $reactorId
*/
public function onClose($fd, $reactorId)
{
return;
}
protected function checkHeartbeat($fd, $packet)
{
$packetLength = strlen($packet);
$payload = '';
if ($isPing = Packet::isSocketType($packet, 'ping')) {
$payload .= Packet::PONG;
}
if ($isPing && $packetLength > 1) {
$payload .= substr($packet, 1, $packetLength - 1);
}
if ($isPing) {
$this->server->push($fd, $payload);
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace think\swoole\websocket\socketio;
/**
* Class Packet
*/
class Packet
{
/**
* Socket.io packet type `open`.
*/
const OPEN = 0;
/**
* Socket.io packet type `close`.
*/
const CLOSE = 1;
/**
* Socket.io packet type `ping`.
*/
const PING = 2;
/**
* Socket.io packet type `pong`.
*/
const PONG = 3;
/**
* Socket.io packet type `message`.
*/
const MESSAGE = 4;
/**
* Socket.io packet type 'upgrade'
*/
const UPGRADE = 5;
/**
* Socket.io packet type `noop`.
*/
const NOOP = 6;
/**
* Engine.io packet type `connect`.
*/
const CONNECT = 0;
/**
* Engine.io packet type `disconnect`.
*/
const DISCONNECT = 1;
/**
* Engine.io packet type `event`.
*/
const EVENT = 2;
/**
* Engine.io packet type `ack`.
*/
const ACK = 3;
/**
* Engine.io packet type `error`.
*/
const ERROR = 4;
/**
* Engine.io packet type 'binary event'
*/
const BINARY_EVENT = 5;
/**
* Engine.io packet type `binary ack`. For acks with binary arguments.
*/
const BINARY_ACK = 6;
/**
* Socket.io packet types.
*/
public static $socketTypes = [
0 => 'OPEN',
1 => 'CLOSE',
2 => 'PING',
3 => 'PONG',
4 => 'MESSAGE',
5 => 'UPGRADE',
6 => 'NOOP',
];
/**
* Engine.io packet types.
*/
public static $engineTypes = [
0 => 'CONNECT',
1 => 'DISCONNECT',
2 => 'EVENT',
3 => 'ACK',
4 => 'ERROR',
5 => 'BINARY_EVENT',
6 => 'BINARY_ACK',
];
/**
* Get socket packet type of a raw payload.
*
* @param string $packet
*
* @return int|null
*/
public static function getSocketType(string $packet)
{
$type = $packet[0] ?? null;
if (!array_key_exists($type, static::$socketTypes)) {
return;
}
return (int) $type;
}
/**
* Get data packet from a raw payload.
*
* @param string $packet
*
* @return array|null
*/
public static function getPayload(string $packet)
{
$packet = trim($packet);
$start = strpos($packet, '[');
if ($start === false || substr($packet, -1) !== ']') {
return;
}
$data = substr($packet, $start, strlen($packet) - $start);
$data = json_decode($data, true);
if (is_null($data)) {
return;
}
return [
'event' => $data[0],
'data' => $data[1] ?? null,
];
}
/**
* Return if a socket packet belongs to specific type.
*
* @param $packet
* @param string $typeName
*
* @return bool
*/
public static function isSocketType($packet, string $typeName)
{
$type = array_search(strtoupper($typeName), static::$socketTypes);
if ($type === false) {
return false;
}
return static::getSocketType($packet) === $type;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace think\swoole\websocket\socketio;
use think\swoole\contract\websocket\ParserInterface;
class Parser implements ParserInterface
{
/**
* Encode output payload for websocket push.
*
* @param string $event
* @param mixed $data
*
* @return mixed
*/
public function encode(string $event, $data)
{
$packet = Packet::MESSAGE . Packet::EVENT;
$shouldEncode = is_array($data) || is_object($data);
$data = $shouldEncode ? json_encode($data) : $data;
$format = $shouldEncode ? '["%s",%s]' : '["%s","%s"]';
return $packet . sprintf($format, $event, $data);
}
/**
* Decode message from websocket client.
* Define and return payload here.
*
* @param \Swoole\Websocket\Frame $frame
*
* @return array
*/
public function decode($frame)
{
$payload = Packet::getPayload($frame->data);
return [
'event' => $payload['event'] ?? null,
'data' => $payload['data'] ?? null,
];
}
}