add
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use think\swoole\coroutine\Context;
|
||||
|
||||
class App extends \think\App
|
||||
{
|
||||
public function runningInConsole()
|
||||
{
|
||||
return Context::hasData('_fd');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use Swoole\Timer;
|
||||
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();
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use think\Middleware;
|
||||
use think\Route;
|
||||
use think\swoole\concerns\ModifyProperty;
|
||||
|
||||
/**
|
||||
* Class Http
|
||||
* @package think\swoole
|
||||
* @property $request
|
||||
*/
|
||||
class Http extends \think\Http
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
/** @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;
|
||||
$this->modifyProperty(self::$middleware, null);
|
||||
}
|
||||
|
||||
$middleware = clone self::$middleware;
|
||||
$this->modifyProperty($middleware, $this->app);
|
||||
$this->app->instance("middleware", $middleware);
|
||||
}
|
||||
|
||||
protected function loadRoutes(): void
|
||||
{
|
||||
if (!isset(self::$route)) {
|
||||
parent::loadRoutes();
|
||||
self::$route = clone $this->app->route;
|
||||
$this->modifyProperty(self::$route, null);
|
||||
$this->modifyProperty(self::$route, null, 'request');
|
||||
}
|
||||
}
|
||||
|
||||
protected function dispatchToRoute($request)
|
||||
{
|
||||
if (isset(self::$route)) {
|
||||
$newRoute = clone self::$route;
|
||||
$this->modifyProperty($newRoute, $this->app);
|
||||
$this->app->instance("route", $newRoute);
|
||||
}
|
||||
|
||||
return parent::dispatchToRoute($request);
|
||||
}
|
||||
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?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 think\App;
|
||||
use think\swoole\concerns\InteractsWithHttp;
|
||||
use think\swoole\concerns\InteractsWithPools;
|
||||
use think\swoole\concerns\InteractsWithRpcServer;
|
||||
use think\swoole\concerns\InteractsWithRpcClient;
|
||||
use think\swoole\concerns\InteractsWithServer;
|
||||
use think\swoole\concerns\InteractsWithSwooleTable;
|
||||
use think\swoole\concerns\InteractsWithWebsocket;
|
||||
use think\swoole\concerns\WithApplication;
|
||||
|
||||
/**
|
||||
* Class Manager
|
||||
*/
|
||||
class Manager
|
||||
{
|
||||
use InteractsWithServer,
|
||||
InteractsWithSwooleTable,
|
||||
InteractsWithHttp,
|
||||
InteractsWithWebsocket,
|
||||
InteractsWithPools,
|
||||
InteractsWithRpcClient,
|
||||
InteractsWithRpcServer,
|
||||
WithApplication;
|
||||
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Server events.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $events = [
|
||||
'start',
|
||||
'shutDown',
|
||||
'workerStart',
|
||||
'workerStop',
|
||||
'workerError',
|
||||
'workerExit',
|
||||
'packet',
|
||||
'task',
|
||||
'finish',
|
||||
'pipeMessage',
|
||||
'managerStart',
|
||||
'managerStop',
|
||||
'request',
|
||||
];
|
||||
|
||||
/**
|
||||
* Manager constructor.
|
||||
* @param App $container
|
||||
*/
|
||||
public function __construct(App $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->prepareTables();
|
||||
$this->preparePools();
|
||||
$this->prepareWebsocket();
|
||||
$this->setSwooleServerListeners();
|
||||
$this->prepareRpcServer();
|
||||
$this->prepareRpcClient();
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use Swoole\Process;
|
||||
|
||||
class PidManager
|
||||
{
|
||||
/** @var string */
|
||||
protected $file;
|
||||
|
||||
public function __construct(string $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
public function getPid()
|
||||
{
|
||||
if (is_readable($this->file)) {
|
||||
return (int) file_get_contents($this->file);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否运行中
|
||||
* @return bool
|
||||
*/
|
||||
public function isRunning()
|
||||
{
|
||||
$pid = $this->getPid();
|
||||
|
||||
return $pid > 0 && Process::kill($pid, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill process.
|
||||
*
|
||||
* @param int $sig
|
||||
* @param int $wait
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function killProcess($sig, $wait = 0)
|
||||
{
|
||||
$pid = $this->getPid();
|
||||
$pid > 0 && Process::kill($pid, $sig);
|
||||
|
||||
if ($wait) {
|
||||
$start = time();
|
||||
|
||||
do {
|
||||
if (!$this->isRunning()) {
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
} while (time() < $start + $wait);
|
||||
}
|
||||
|
||||
return $this->isRunning();
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use Smf\ConnectionPool\ConnectionPool;
|
||||
use think\helper\Arr;
|
||||
|
||||
class Pool
|
||||
{
|
||||
protected $pools = [];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param ConnectionPool $pool
|
||||
*
|
||||
* @return Pool
|
||||
*/
|
||||
public function add(string $name, ConnectionPool $pool)
|
||||
{
|
||||
$pool->init();
|
||||
$this->pools[$name] = $pool;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return ConnectionPool
|
||||
*/
|
||||
public function get(string $name)
|
||||
{
|
||||
return $this->pools[$name] ?? null;
|
||||
}
|
||||
|
||||
public function close(string $key)
|
||||
{
|
||||
return $this->pools[$key]->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->pools;
|
||||
}
|
||||
|
||||
public function closeAll()
|
||||
{
|
||||
foreach ($this->pools as $pool) {
|
||||
$pool->close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return ConnectionPool
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->get($key);
|
||||
}
|
||||
|
||||
public static function pullPoolConfig(&$config)
|
||||
{
|
||||
return [
|
||||
'minActive' => Arr::pull($config, 'min_active', 0),
|
||||
'maxActive' => Arr::pull($config, 'max_active', 10),
|
||||
'maxWaitTime' => Arr::pull($config, 'max_wait_time', 5),
|
||||
'maxIdleTime' => Arr::pull($config, 'max_idle_time', 20),
|
||||
'idleCheckInterval' => Arr::pull($config, 'idle_check_interval', 10),
|
||||
];
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Server;
|
||||
use Swoole\Server\Port;
|
||||
use think\App;
|
||||
use think\Event;
|
||||
use think\helper\Str;
|
||||
use think\swoole\concerns\InteractsWithPools;
|
||||
use think\swoole\concerns\InteractsWithRpcClient;
|
||||
use think\swoole\concerns\InteractsWithServer;
|
||||
use think\swoole\concerns\InteractsWithSwooleTable;
|
||||
use think\swoole\concerns\WithApplication;
|
||||
use think\swoole\contract\rpc\ParserInterface;
|
||||
use think\swoole\rpc\JsonParser;
|
||||
use think\swoole\rpc\Packer;
|
||||
use think\swoole\rpc\server\Channel;
|
||||
use think\swoole\rpc\server\Dispatcher;
|
||||
use Throwable;
|
||||
|
||||
class RpcManager
|
||||
{
|
||||
use InteractsWithServer,
|
||||
InteractsWithSwooleTable,
|
||||
InteractsWithPools,
|
||||
InteractsWithRpcClient,
|
||||
WithApplication;
|
||||
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Server events.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $events = [
|
||||
'start',
|
||||
'shutDown',
|
||||
'workerStart',
|
||||
'workerStop',
|
||||
'workerError',
|
||||
'workerExit',
|
||||
'packet',
|
||||
'task',
|
||||
'finish',
|
||||
'pipeMessage',
|
||||
'managerStart',
|
||||
'managerStop',
|
||||
];
|
||||
|
||||
protected $rpcEvents = [
|
||||
'connect',
|
||||
'receive',
|
||||
'close',
|
||||
];
|
||||
|
||||
/** @var Channel[] */
|
||||
protected $channels = [];
|
||||
|
||||
/**
|
||||
* Manager constructor.
|
||||
* @param App $container
|
||||
*/
|
||||
public function __construct(App $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize.
|
||||
*/
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->events = array_merge($this->events ?? [], $this->rpcEvents);
|
||||
$this->prepareTables();
|
||||
$this->preparePools();
|
||||
$this->setSwooleServerListeners();
|
||||
$this->prepareRpcServer();
|
||||
$this->prepareRpcClient();
|
||||
}
|
||||
|
||||
protected function prepareRpcServer()
|
||||
{
|
||||
$this->onEvent('workerStart', function () {
|
||||
$this->bindRpcParser();
|
||||
$this->bindRpcDispatcher();
|
||||
});
|
||||
}
|
||||
|
||||
public function attachToServer(Port $port)
|
||||
{
|
||||
$port->set([]);
|
||||
foreach ($this->rpcEvents as $event) {
|
||||
$listener = Str::camel("on_$event");
|
||||
$callback = method_exists($this, $listener) ? [$this, $listener] : function () use ($event) {
|
||||
$this->triggerEvent("rpc." . $event, func_get_args());
|
||||
};
|
||||
|
||||
$port->on($event, $callback);
|
||||
}
|
||||
|
||||
$this->onEvent('workerStart', function (App $app) {
|
||||
$this->app = $app;
|
||||
});
|
||||
$this->prepareRpcServer();
|
||||
}
|
||||
|
||||
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 onConnect(Server $server, int $fd, int $reactorId)
|
||||
{
|
||||
$args = func_get_args();
|
||||
$this->runInSandbox(function (Event $event) use ($args) {
|
||||
$event->trigger("swoole.rpc.Connect", $args);
|
||||
}, $fd, true);
|
||||
}
|
||||
|
||||
protected function recv(Server $server, $fd, $data, $callback)
|
||||
{
|
||||
if (!isset($this->channels[$fd]) || empty($handle = $this->channels[$fd]->pop())) {
|
||||
//解析包头
|
||||
try {
|
||||
[$header, $data] = Packer::unpack($data);
|
||||
} catch (Throwable $e) {
|
||||
//错误的包头
|
||||
return $server->close($fd);
|
||||
}
|
||||
|
||||
$this->channels[$fd] = new Channel($header);
|
||||
|
||||
$handle = $this->channels[$fd]->pop();
|
||||
}
|
||||
|
||||
$result = $handle->write($data);
|
||||
|
||||
if (!empty($result)) {
|
||||
Coroutine::create($callback, $result);
|
||||
$this->channels[$fd]->close();
|
||||
} else {
|
||||
$this->channels[$fd]->push($handle);
|
||||
}
|
||||
|
||||
if (!empty($data)) {
|
||||
$this->recv($server, $fd, $data, $callback);
|
||||
}
|
||||
}
|
||||
|
||||
public function onReceive(Server $server, $fd, $reactorId, $data)
|
||||
{
|
||||
$this->recv($server, $fd, $data, function ($data) use ($fd, $server) {
|
||||
$this->runInSandbox(function (Dispatcher $dispatcher) use ($fd, $data) {
|
||||
$dispatcher->dispatch($fd, $data);
|
||||
}, $fd, true);
|
||||
});
|
||||
}
|
||||
|
||||
public function onClose(Server $server, int $fd, int $reactorId)
|
||||
{
|
||||
unset($this->channels[$fd]);
|
||||
$args = func_get_args();
|
||||
$this->runInSandbox(function (Event $event) use ($args) {
|
||||
$event->trigger("swoole.rpc.Close", $args);
|
||||
}, $fd);
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole;
|
||||
|
||||
use Closure;
|
||||
use ReflectionObject;
|
||||
use RuntimeException;
|
||||
use think\App;
|
||||
use think\Config;
|
||||
use think\Container;
|
||||
use think\Event;
|
||||
use think\Http;
|
||||
use think\swoole\concerns\ModifyProperty;
|
||||
use think\swoole\contract\ResetterInterface;
|
||||
use think\swoole\coroutine\Context;
|
||||
use think\swoole\resetters\ClearInstances;
|
||||
use think\swoole\resetters\ResetConfig;
|
||||
use think\swoole\resetters\ResetEvent;
|
||||
use think\swoole\resetters\ResetService;
|
||||
use Throwable;
|
||||
|
||||
class Sandbox
|
||||
{
|
||||
use ModifyProperty;
|
||||
/**
|
||||
* The app containers in different coroutine environment.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $snapshots = [];
|
||||
|
||||
/** @var App */
|
||||
protected $app;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var Event */
|
||||
protected $event;
|
||||
|
||||
/** @var ResetterInterface[] */
|
||||
protected $resetters = [];
|
||||
protected $services = [];
|
||||
|
||||
public function __construct(Container $app)
|
||||
{
|
||||
$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();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(Closure $callable, $fd = null, $persistent = false)
|
||||
{
|
||||
$this->init($fd);
|
||||
|
||||
try {
|
||||
$this->getApplication()->invoke($callable, [$this]);
|
||||
} catch (Throwable $e) {
|
||||
throw $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());
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$reflectObject = new ReflectionObject($app);
|
||||
$reflectProperty = $reflectObject->getProperty('services');
|
||||
$reflectProperty->setAccessible(true);
|
||||
$services = $reflectProperty->getValue($app);
|
||||
|
||||
foreach ($services as $service) {
|
||||
$this->modifyProperty($service, $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 = $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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -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;
|
||||
|
||||
use think\Route;
|
||||
use think\swoole\command\Rpc;
|
||||
use think\swoole\command\RpcInterface;
|
||||
use think\swoole\command\Server as ServerCommand;
|
||||
use think\swoole\websocket\socketio\Controller;
|
||||
|
||||
class Service extends \think\Service
|
||||
{
|
||||
|
||||
public function boot()
|
||||
{
|
||||
$this->commands(ServerCommand::class, RpcInterface::class, Rpc::class);
|
||||
|
||||
if ($this->app->config->get('swoole.websocket.enable', false)) {
|
||||
$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,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+228
@@ -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');
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\Output;
|
||||
use think\swoole\PidManager;
|
||||
use think\swoole\RpcManager;
|
||||
|
||||
class Rpc extends Command
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('swoole:rpc')
|
||||
->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload", 'start')
|
||||
->setDescription('Swoole RPC Server for ThinkPHP');
|
||||
}
|
||||
|
||||
protected function initialize(Input $input, Output $output)
|
||||
{
|
||||
$this->app->bind(\Swoole\Server::class, function () {
|
||||
return $this->createSwooleServer();
|
||||
});
|
||||
|
||||
$this->app->bind(PidManager::class, function () {
|
||||
return new PidManager($this->app->config->get("swoole.server.options.pid_file"));
|
||||
});
|
||||
}
|
||||
|
||||
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 RpcManager $manager
|
||||
* @param PidManager $pidManager
|
||||
* @return void
|
||||
*/
|
||||
protected function start(RpcManager $manager, PidManager $pidManager)
|
||||
{
|
||||
if ($pidManager->isRunning()) {
|
||||
$this->output->writeln('<error>swoole rpc server process is already running.</error>');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln('Starting swoole rpc server...');
|
||||
|
||||
$host = $this->app->config->get('swoole.server.host');
|
||||
$port = $this->app->config->get('swoole.rpc.server.port');
|
||||
|
||||
$this->output->writeln("Swoole rpc server started: <tcp://{$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 rpc server process running.</error>');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln('Reloading swoole rpc 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 rpc server process running.</error>');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln('Stopping swoole rpc server...');
|
||||
|
||||
$isRunning = $manager->killProcess(SIGTERM, 15);
|
||||
|
||||
if ($isRunning) {
|
||||
$this->output->error('Unable to stop the rpc process.');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln('> success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启server
|
||||
* @access protected
|
||||
* @param RpcManager $manager
|
||||
* @param PidManager $pidManager
|
||||
* @return void
|
||||
*/
|
||||
protected function restart(RpcManager $manager, PidManager $pidManager)
|
||||
{
|
||||
if ($pidManager->isRunning()) {
|
||||
$this->stop($pidManager);
|
||||
}
|
||||
|
||||
$this->start($manager, $pidManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create swoole server.
|
||||
*/
|
||||
protected function createSwooleServer()
|
||||
{
|
||||
$config = $this->app->config;
|
||||
$host = $config->get('swoole.server.host');
|
||||
$port = $config->get('swoole.rpc.server.port');
|
||||
$socketType = $config->get('swoole.server.socket_type', SWOOLE_SOCK_TCP);
|
||||
$mode = $config->get('swoole.server.mode', SWOOLE_PROCESS);
|
||||
|
||||
/** @var \Swoole\Server $server */
|
||||
$server = new \Swoole\Server($host, $port, $mode, $socketType);
|
||||
|
||||
$options = $config->get('swoole.server.options');
|
||||
|
||||
$server->set($options);
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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\rpc\client\Gateway;
|
||||
use think\swoole\rpc\JsonParser;
|
||||
|
||||
class RpcInterface extends Command
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('rpc:interface')
|
||||
->setDescription('Generate Rpc Service Interfaces');
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$clients = $this->app->config->get('swoole.rpc.client', []);
|
||||
|
||||
$file = new PhpFile;
|
||||
$file->addComment('This file is auto-generated.');
|
||||
$file->setStrictTypes();
|
||||
$services = [];
|
||||
foreach ($clients as $name => $config) {
|
||||
|
||||
$parserClass = Arr::get($config, 'parser', JsonParser::class);
|
||||
/** @var ParserInterface $parser */
|
||||
$parser = new $parserClass;
|
||||
|
||||
$gateway = new Gateway($config, $parser);
|
||||
|
||||
$result = $gateway->getServices();
|
||||
|
||||
$namespace = $file->addNamespace("rpc\\contract\\${name}");
|
||||
|
||||
foreach ($result as $interface => $methods) {
|
||||
|
||||
$services[$name][] = $namespace->getName() . "\\{$interface}";
|
||||
|
||||
$class = $namespace->addInterface($interface);
|
||||
|
||||
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) {
|
||||
if ($parameter['type'] && (class_exists($parameter['type']) || interface_exists($parameter['type']))) {
|
||||
$namespace->addUse($parameter['type']);
|
||||
}
|
||||
$param = $method->addParameter($parameter['name'])
|
||||
->setTypeHint($parameter['type']);
|
||||
|
||||
if (array_key_exists("default", $parameter)) {
|
||||
$param->setDefaultValue($parameter['default']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$services = "return " . Helpers::dump($services) . ";";
|
||||
|
||||
file_put_contents($this->app->getBasePath() . 'rpc.php', $file . $services);
|
||||
|
||||
$this->output->writeln('<info>Succeed!</info>');
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<?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 Swoole\Http\Server as HttpServer;
|
||||
use Swoole\WebSocket\Server as WebsocketServer;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\Output;
|
||||
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');
|
||||
}
|
||||
|
||||
protected function initialize(Input $input, Output $output)
|
||||
{
|
||||
$this->app->bind(\Swoole\Server::class, function () {
|
||||
return $this->createSwooleServer();
|
||||
});
|
||||
|
||||
$this->app->bind(PidManager::class, function () {
|
||||
return new PidManager($this->app->config->get("swoole.server.options.pid_file"));
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create swoole server.
|
||||
*/
|
||||
protected function createSwooleServer()
|
||||
{
|
||||
|
||||
$isWebsocket = $this->app->config->get('swoole.websocket.enable', false);
|
||||
|
||||
$serverClass = $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);
|
||||
|
||||
/** @var \Swoole\Server $server */
|
||||
$server = new $serverClass($host, $port, $mode, $socketType);
|
||||
|
||||
$options = $config->get('swoole.server.options');
|
||||
|
||||
$server->set($options);
|
||||
return $server;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\Http\Response;
|
||||
use Swoole\Server;
|
||||
use Symfony\Component\VarDumper\VarDumper;
|
||||
use think\App;
|
||||
use think\Container;
|
||||
use think\Cookie;
|
||||
use think\Event;
|
||||
use think\exception\Handle;
|
||||
use think\Http;
|
||||
use think\Middleware;
|
||||
use think\swoole\middleware\ResetVarDumper;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithHttp
|
||||
* @package think\swoole\concerns
|
||||
* @property App $app
|
||||
* @property Container $container
|
||||
* @method Server getServer()
|
||||
*/
|
||||
trait InteractsWithHttp
|
||||
{
|
||||
public static $statusTexts = [
|
||||
100 => 'Continue',
|
||||
101 => 'Switching Protocols',
|
||||
102 => 'Processing', // RFC2518
|
||||
103 => 'Early Hints',
|
||||
200 => 'OK',
|
||||
201 => 'Created',
|
||||
202 => 'Accepted',
|
||||
203 => 'Non-Authoritative Information',
|
||||
204 => 'No Content',
|
||||
205 => 'Reset Content',
|
||||
206 => 'Partial Content',
|
||||
207 => 'Multi-Status', // RFC4918
|
||||
208 => 'Already Reported', // RFC5842
|
||||
226 => 'IM Used', // RFC3229
|
||||
300 => 'Multiple Choices',
|
||||
301 => 'Moved Permanently',
|
||||
302 => 'Found',
|
||||
303 => 'See Other',
|
||||
304 => 'Not Modified',
|
||||
305 => 'Use Proxy',
|
||||
307 => 'Temporary Redirect',
|
||||
308 => 'Permanent Redirect', // RFC7238
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
402 => 'Payment Required',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
406 => 'Not Acceptable',
|
||||
407 => 'Proxy Authentication Required',
|
||||
408 => 'Request Timeout',
|
||||
409 => 'Conflict',
|
||||
410 => 'Gone',
|
||||
411 => 'Length Required',
|
||||
412 => 'Precondition Failed',
|
||||
413 => 'Payload Too Large',
|
||||
414 => 'URI Too Long',
|
||||
415 => 'Unsupported Media Type',
|
||||
416 => 'Range Not Satisfiable',
|
||||
417 => 'Expectation Failed',
|
||||
418 => 'I\'m a teapot', // RFC2324
|
||||
421 => 'Misdirected Request', // RFC7540
|
||||
422 => 'Unprocessable Entity', // RFC4918
|
||||
423 => 'Locked', // RFC4918
|
||||
424 => 'Failed Dependency', // RFC4918
|
||||
425 => 'Too Early', // RFC-ietf-httpbis-replay-04
|
||||
426 => 'Upgrade Required', // RFC2817
|
||||
428 => 'Precondition Required', // RFC6585
|
||||
429 => 'Too Many Requests', // RFC6585
|
||||
431 => 'Request Header Fields Too Large', // RFC6585
|
||||
449 => 'Retry With',
|
||||
451 => 'Unavailable For Legal Reasons', // RFC7725
|
||||
500 => 'Internal Server Error',
|
||||
501 => 'Not Implemented',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
504 => 'Gateway Timeout',
|
||||
505 => 'HTTP Version Not Supported',
|
||||
506 => 'Variant Also Negotiates', // RFC2295
|
||||
507 => 'Insufficient Storage', // RFC4918
|
||||
508 => 'Loop Detected', // RFC5842
|
||||
510 => 'Not Extended', // RFC2774
|
||||
511 => 'Network Authentication Required', // RFC6585
|
||||
];
|
||||
|
||||
/**
|
||||
* "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, Middleware $middleware) use ($args, $req, $res) {
|
||||
$event->trigger('swoole.request', $args);
|
||||
|
||||
//兼容var-dumper
|
||||
if (class_exists(VarDumper::class)) {
|
||||
$middleware->add(ResetVarDumper::class);
|
||||
}
|
||||
|
||||
$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 ?: [])
|
||||
->withFiles($req->files ?: [])
|
||||
->withInput($req->rawContent())
|
||||
->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);
|
||||
}
|
||||
|
||||
// 发送状态码
|
||||
$code = $response->getCode();
|
||||
$res->status($code, isset(self::$statusTexts[$code]) ? self::$statusTexts[$code] : 'unknown status');
|
||||
|
||||
foreach ($cookie->getCookie() as $name => $val) {
|
||||
[$value, $expire, $option] = $val;
|
||||
|
||||
$res->cookie($name, $value, $expire, $option['path'], $option['domain'], $option['secure'] ? true : false, $option['httponly'] ? true : false);
|
||||
}
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
$this->sendByChunk($res, $content);
|
||||
}
|
||||
|
||||
protected function sendByChunk(Response $res, $content)
|
||||
{
|
||||
if (!empty($content)) {
|
||||
$chunkSize = 8192;
|
||||
|
||||
foreach (str_split($content, $chunkSize) as $chunk) {
|
||||
$res->write($chunk);
|
||||
}
|
||||
}
|
||||
$res->end();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Exception;
|
||||
use Smf\ConnectionPool\ConnectionPool;
|
||||
use Smf\ConnectionPool\Connectors\ConnectorInterface;
|
||||
use Swoole\Server;
|
||||
use think\App;
|
||||
use think\helper\Arr;
|
||||
use think\swoole\Pool;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithRpc
|
||||
* @package think\swoole\concerns
|
||||
* @property App $app
|
||||
* @method Server getServer()
|
||||
*/
|
||||
trait InteractsWithPools
|
||||
{
|
||||
/**
|
||||
* @return Pool
|
||||
*/
|
||||
public function getPools()
|
||||
{
|
||||
return $this->app->make(Pool::class);
|
||||
}
|
||||
|
||||
protected function preparePools()
|
||||
{
|
||||
$createPools = function () {
|
||||
/** @var Pool $pool */
|
||||
$pools = $this->getPools();
|
||||
|
||||
foreach ($this->getConfig('pool', []) as $name => $config) {
|
||||
$type = Arr::pull($config, 'type');
|
||||
if ($type && is_subclass_of($type, ConnectorInterface::class)) {
|
||||
$pool = new ConnectionPool(
|
||||
Pool::pullPoolConfig($config),
|
||||
$this->app->make($type),
|
||||
$config
|
||||
);
|
||||
$pools->add($name, $pool);
|
||||
//注入到app
|
||||
$this->app->instance("swoole.pool.{$name}", $pool);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$closePools = function () {
|
||||
try {
|
||||
$this->getPools()->closeAll();
|
||||
} catch (Exception | Throwable $e) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$this->onEvent('workerStart', $createPools);
|
||||
$this->onEvent('workerStop', $closePools);
|
||||
$this->onEvent('workerError', $closePools);
|
||||
$this->onEvent('workerExit', $closePools);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Smf\ConnectionPool\ConnectionPool;
|
||||
use Swoole\Server;
|
||||
use think\App;
|
||||
use think\swoole\exception\RpcClientException;
|
||||
use think\swoole\Pool;
|
||||
use think\swoole\pool\Client;
|
||||
use think\swoole\rpc\client\Connector;
|
||||
use think\swoole\rpc\client\Gateway;
|
||||
use think\swoole\rpc\client\Proxy;
|
||||
use think\swoole\rpc\JsonParser;
|
||||
use think\swoole\rpc\Packer;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithRpcClient
|
||||
* @package think\swoole\concerns
|
||||
* @property App $app
|
||||
* @property App $container
|
||||
* @method Server getServer()
|
||||
* @method Pool getPools()
|
||||
*/
|
||||
trait InteractsWithRpcClient
|
||||
{
|
||||
protected $rpcServices = [];
|
||||
|
||||
protected function prepareRpcClient()
|
||||
{
|
||||
//引入rpc接口文件
|
||||
if (file_exists($rpc = $this->container->getBasePath() . 'rpc.php')) {
|
||||
$this->rpcServices = (array) include $rpc;
|
||||
}
|
||||
|
||||
$this->onEvent('workerStart', function () {
|
||||
$this->bindRpcClientPool();
|
||||
});
|
||||
}
|
||||
|
||||
protected function bindRpcClientPool()
|
||||
{
|
||||
if (!empty($clients = $this->getConfig('rpc.client'))) {
|
||||
//创建client连接池
|
||||
foreach ($clients as $name => $config) {
|
||||
$pool = new ConnectionPool(
|
||||
Pool::pullPoolConfig($config),
|
||||
new Client(),
|
||||
array_merge(
|
||||
$config,
|
||||
[
|
||||
'open_length_check' => true,
|
||||
'package_length_type' => Packer::HEADER_PACK,
|
||||
'package_length_offset' => 0,
|
||||
'package_body_offset' => 8,
|
||||
]
|
||||
)
|
||||
);
|
||||
$this->getPools()->add("rpc.client.{$name}", $pool);
|
||||
}
|
||||
|
||||
//绑定rpc接口
|
||||
try {
|
||||
foreach ($this->rpcServices as $name => $abstracts) {
|
||||
$parserClass = $this->getConfig("rpc.client.{$name}.parser", JsonParser::class);
|
||||
$parser = $this->app->make($parserClass);
|
||||
$gateway = new Gateway($this->createRpcConnector($name), $parser);
|
||||
|
||||
foreach ($abstracts as $abstract) {
|
||||
$this->app->bind($abstract, function () use ($gateway, $name, $abstract) {
|
||||
return $this->app->invokeClass(Proxy::getClassName($name, $abstract), [$gateway]);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (\Exception | \Throwable $e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function createRpcConnector($name)
|
||||
{
|
||||
$pool = $this->getPools()->get("rpc.client.{$name}");
|
||||
|
||||
return new class($pool) implements Connector {
|
||||
protected $pool;
|
||||
|
||||
public function __construct(ConnectionPool $pool)
|
||||
{
|
||||
$this->pool = $pool;
|
||||
}
|
||||
|
||||
public function sendAndRecv($data)
|
||||
{
|
||||
if (!$data instanceof \Generator) {
|
||||
$data = [$data];
|
||||
}
|
||||
|
||||
/** @var \Swoole\Coroutine\Client $client */
|
||||
$client = $this->pool->borrow();
|
||||
|
||||
try {
|
||||
foreach ($data as $string) {
|
||||
if (!$client->send($string)) {
|
||||
$this->onError($client);
|
||||
}
|
||||
}
|
||||
|
||||
$response = $client->recv();
|
||||
|
||||
if ($response === false || empty($response)) {
|
||||
$this->onError($client);
|
||||
}
|
||||
|
||||
return $response;
|
||||
} finally {
|
||||
$this->pool->return($client);
|
||||
}
|
||||
}
|
||||
|
||||
protected function onError(\Swoole\Coroutine\Client $client)
|
||||
{
|
||||
$client->close();
|
||||
throw new RpcClientException(swoole_strerror($client->errCode), $client->errCode);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Swoole\Server;
|
||||
use think\App;
|
||||
use think\swoole\Pool;
|
||||
use think\swoole\RpcManager;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithRpc
|
||||
* @package think\swoole\concerns
|
||||
* @property App $app
|
||||
* @property App $container
|
||||
* @method Server getServer()
|
||||
* @method Pool getPools()
|
||||
*/
|
||||
trait InteractsWithRpcServer
|
||||
{
|
||||
|
||||
protected function prepareRpcServer()
|
||||
{
|
||||
if ($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);
|
||||
|
||||
/** @var RpcManager $rpcManager */
|
||||
$rpcManager = $this->container->make(RpcManager::class);
|
||||
|
||||
$rpcManager->attachToServer($rpcServer);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Exception;
|
||||
use Swoole\Process;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\Server;
|
||||
use Swoole\Server\Task;
|
||||
use think\App;
|
||||
use think\console\Output;
|
||||
use think\Event;
|
||||
use think\exception\Handle;
|
||||
use think\helper\Str;
|
||||
use think\swoole\FileWatcher;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithServer
|
||||
* @package think\swoole\concerns
|
||||
* @property App $container
|
||||
*/
|
||||
trait InteractsWithServer
|
||||
{
|
||||
|
||||
/**
|
||||
* 启动服务
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->getServer()->set([
|
||||
'task_enable_coroutine' => true,
|
||||
'send_yield' => true,
|
||||
'reload_async' => true,
|
||||
'enable_coroutine' => true,
|
||||
'max_request' => 0,
|
||||
'task_max_request' => 0,
|
||||
]);
|
||||
$this->initialize();
|
||||
$this->triggerEvent('init');
|
||||
|
||||
//热更新
|
||||
if ($this->getConfig('hot_update.enable', false)) {
|
||||
$this->addHotUpdateProcess();
|
||||
}
|
||||
|
||||
$this->getServer()->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止服务
|
||||
*/
|
||||
public function stop(): void
|
||||
{
|
||||
$this->getServer()->shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* "onStart" listener.
|
||||
*/
|
||||
public function onStart()
|
||||
{
|
||||
$this->setProcessName('master process');
|
||||
|
||||
$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->triggerEvent("workerStart", $this->app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}, $task->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set onShutdown listener.
|
||||
*/
|
||||
public function onShutdown()
|
||||
{
|
||||
$this->triggerEvent('shutdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 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->addProcess($process);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add process to http server
|
||||
*
|
||||
* @param Process $process
|
||||
*/
|
||||
public function addProcess(Process $process): void
|
||||
{
|
||||
$this->getServer()->addProcess($process);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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 tables.
|
||||
*/
|
||||
protected function prepareTables()
|
||||
{
|
||||
$this->currentTable = new Table();
|
||||
$this->registerTables();
|
||||
$this->onEvent('workerStart', function () {
|
||||
$this->app->instance(Table::class, $this->currentTable);
|
||||
foreach ($this->currentTable->getAll() as $name => $table) {
|
||||
$this->app->instance("swoole.table.{$name}", $table);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
<?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\Pipeline;
|
||||
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, App $app) use ($req) {
|
||||
$request = $this->prepareRequest($req);
|
||||
$request = $this->setRequestThroughMiddleware($app, $request);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param App $app
|
||||
* @param \think\Request $request
|
||||
* @return \think\Request
|
||||
*/
|
||||
protected function setRequestThroughMiddleware(App $app, \think\Request $request)
|
||||
{
|
||||
$middleware = $this->getConfig('websocket.middleware', []);
|
||||
|
||||
return (new Pipeline())
|
||||
->send($request)
|
||||
->through(array_map(function ($middleware) use ($app) {
|
||||
return function ($request, $next) use ($app, $middleware) {
|
||||
if (is_array($middleware)) {
|
||||
list($middleware, $param) = $middleware;
|
||||
}
|
||||
if (is_string($middleware)) {
|
||||
$middleware = [$app->make($middleware), 'handle'];
|
||||
}
|
||||
return call_user_func($middleware, $request, $next, $param ?? null);
|
||||
};
|
||||
}, $middleware))
|
||||
->then(function ($request) {
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use ReflectionObject;
|
||||
|
||||
trait ModifyProperty
|
||||
{
|
||||
protected function modifyProperty($object, $value, $property = 'app')
|
||||
{
|
||||
$reflectObject = new ReflectionObject($object);
|
||||
if ($reflectObject->hasProperty($property)) {
|
||||
$reflectProperty = $reflectObject->getProperty($property);
|
||||
$reflectProperty->setAccessible(true);
|
||||
$reflectProperty->setValue($object, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\concerns;
|
||||
|
||||
use Closure;
|
||||
use Swoole\Server;
|
||||
use think\App;
|
||||
use think\swoole\App as SwooleApp;
|
||||
use think\swoole\pool\Cache;
|
||||
use think\swoole\pool\Db;
|
||||
use think\swoole\Sandbox;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Trait WithApplication
|
||||
* @package think\swoole\concerns
|
||||
* @property App $container
|
||||
*/
|
||||
trait WithApplication
|
||||
{
|
||||
|
||||
/**
|
||||
* @var SwooleApp
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
* @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
|
||||
*/
|
||||
public function onEvent(string $event, $listener, bool $first = false): void
|
||||
{
|
||||
$this->container->event->listen("swoole.{$event}", $listener, $first);
|
||||
}
|
||||
|
||||
protected function prepareApplication()
|
||||
{
|
||||
if (!$this->app instanceof SwooleApp) {
|
||||
$this->app = new SwooleApp($this->container->getRootPath());
|
||||
$this->app->bind(SwooleApp::class, App::class);
|
||||
$this->app->bind(Server::class, $this->getServer());
|
||||
$this->app->bind("swoole.server", Server::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取沙箱
|
||||
* @return Sandbox
|
||||
*/
|
||||
protected function getSandbox()
|
||||
{
|
||||
return $this->app->make(Sandbox::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在沙箱中执行
|
||||
* @param Closure $callable
|
||||
* @param null $fd
|
||||
* @param bool $persistent
|
||||
*/
|
||||
protected function runInSandbox(Closure $callable, $fd = null, $persistent = false)
|
||||
{
|
||||
try {
|
||||
$this->getSandbox()->run($callable, $fd, $persistent);
|
||||
} catch (Throwable $e) {
|
||||
$this->logServerError($e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(),
|
||||
'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,
|
||||
],
|
||||
],
|
||||
'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' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'max_active' => 3,
|
||||
'max_wait_time' => 5,
|
||||
],
|
||||
],
|
||||
'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' => [],
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\coroutine;
|
||||
|
||||
use ArrayObject;
|
||||
use Closure;
|
||||
use Swoole\Coroutine;
|
||||
|
||||
class Context
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取协程上下文
|
||||
* @param null $cid
|
||||
* @return Coroutine\Context
|
||||
*/
|
||||
public static function get($cid = null)
|
||||
{
|
||||
return Coroutine::getContext($cid);
|
||||
}
|
||||
|
||||
public static function getDataObject()
|
||||
{
|
||||
$context = self::get();
|
||||
if (!isset($context['data'])) {
|
||||
$context['data'] = new ArrayObject();
|
||||
}
|
||||
return $context['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前协程临时数据
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function getData(string $key, $default = null)
|
||||
{
|
||||
if (self::hasData($key)) {
|
||||
return self::getDataObject()->offsetGet($key);
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在临时数据
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasData(string $key)
|
||||
{
|
||||
return self::getDataObject()->offsetExists($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入临时数据
|
||||
* @param string $key
|
||||
* @param $value
|
||||
*/
|
||||
public static function setData(string $key, $value)
|
||||
{
|
||||
self::getDataObject()->offsetSet($key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param string $key
|
||||
*/
|
||||
public static function removeData(string $key)
|
||||
{
|
||||
if (self::hasData($key)) {
|
||||
self::getDataObject()->offsetUnset($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果不存在则写入数据
|
||||
* @param string $key
|
||||
* @param $value
|
||||
* @return mixed|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* 清空数据
|
||||
*/
|
||||
public static function clear()
|
||||
{
|
||||
self::getDataObject()->exchangeArray([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前协程ID
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getCoroutineId()
|
||||
{
|
||||
return Coroutine::getuid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class RpcClientException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
+20
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool;
|
||||
|
||||
use think\swoole\pool\proxy\Store;
|
||||
|
||||
class Cache extends \think\Cache
|
||||
{
|
||||
protected function createDriver(string $name)
|
||||
{
|
||||
return new Store(function () use ($name) {
|
||||
return parent::createDriver($name);
|
||||
}, $this->app->config->get('swoole.pool.cache', []));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool;
|
||||
|
||||
use Smf\ConnectionPool\Connectors\ConnectorInterface;
|
||||
use think\helper\Arr;
|
||||
|
||||
class Client implements ConnectorInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Connect to the specified Server and returns the connection resource
|
||||
* @param array $config
|
||||
* @return \Swoole\Coroutine\Client
|
||||
*/
|
||||
public function connect(array $config)
|
||||
{
|
||||
$client = new \Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
|
||||
|
||||
$host = Arr::pull($config, 'host');
|
||||
$port = Arr::pull($config, 'port');
|
||||
$timeout = Arr::pull($config, 'timeout');
|
||||
|
||||
$client->set($config);
|
||||
|
||||
$client->connect($host, $port, $timeout);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and free resources
|
||||
* @param \Swoole\Coroutine\Client $connection
|
||||
* @return mixed
|
||||
*/
|
||||
public function disconnect($connection)
|
||||
{
|
||||
$connection->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the connection is established
|
||||
* @param \Swoole\Coroutine\Client $connection
|
||||
* @return bool
|
||||
*/
|
||||
public function isConnected($connection): bool
|
||||
{
|
||||
return $connection->isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the connection
|
||||
* @param \Swoole\Coroutine\Client $connection
|
||||
* @param array $config
|
||||
* @return mixed
|
||||
*/
|
||||
public function reset($connection, array $config)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the connection
|
||||
*
|
||||
* @param \Swoole\Coroutine\Client $connection
|
||||
* @return bool
|
||||
*/
|
||||
public function validate($connection): bool
|
||||
{
|
||||
return $connection instanceof \Swoole\Coroutine\Client;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool;
|
||||
|
||||
use think\Config;
|
||||
use think\db\ConnectionInterface;
|
||||
use think\swoole\pool\proxy\Connection;
|
||||
|
||||
/**
|
||||
* Class Db
|
||||
* @package think\swoole\pool
|
||||
* @property Config $config
|
||||
*/
|
||||
class Db extends \think\Db
|
||||
{
|
||||
|
||||
protected function createConnection(string $name): ConnectionInterface
|
||||
{
|
||||
return new Connection(function () use ($name) {
|
||||
return parent::createConnection($name);
|
||||
}, $this->config->get('swoole.pool.db', []));
|
||||
}
|
||||
|
||||
protected function getConnectionConfig(string $name): array
|
||||
{
|
||||
$config = parent::getConnectionConfig($name);
|
||||
|
||||
//打开断线重连
|
||||
$config['break_reconnect'] = true;
|
||||
return $config;
|
||||
}
|
||||
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool;
|
||||
|
||||
use Closure;
|
||||
use RuntimeException;
|
||||
use Smf\ConnectionPool\ConnectionPool;
|
||||
use Smf\ConnectionPool\Connectors\ConnectorInterface;
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Event;
|
||||
use think\swoole\coroutine\Context;
|
||||
use think\swoole\Pool;
|
||||
|
||||
abstract class Proxy
|
||||
{
|
||||
const KEY_RELEASED = '__released';
|
||||
|
||||
protected $pool;
|
||||
|
||||
/**
|
||||
* Proxy constructor.
|
||||
* @param Closure $creator
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct($creator, $config)
|
||||
{
|
||||
$this->pool = new ConnectionPool(
|
||||
Pool::pullPoolConfig($config),
|
||||
new class($creator) implements ConnectorInterface {
|
||||
|
||||
protected $creator;
|
||||
|
||||
public function __construct($creator)
|
||||
{
|
||||
$this->creator = $creator;
|
||||
}
|
||||
|
||||
public function connect(array $config)
|
||||
{
|
||||
return call_user_func($this->creator);
|
||||
}
|
||||
|
||||
public function disconnect($connection)
|
||||
{
|
||||
//强制回收内存,完成连接释放
|
||||
Event::defer(function () {
|
||||
gc_collect_cycles();
|
||||
});
|
||||
}
|
||||
|
||||
public function isConnected($connection): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reset($connection, array $config)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function validate($connection): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
$this->pool->init();
|
||||
}
|
||||
|
||||
protected function getPoolConnection()
|
||||
{
|
||||
return Context::rememberData("connection." . spl_object_id($this), function () {
|
||||
$connection = $this->pool->borrow();
|
||||
|
||||
$connection->{static::KEY_RELEASED} = false;
|
||||
|
||||
Coroutine::defer(function () use ($connection) {
|
||||
//自动归还
|
||||
$connection->{static::KEY_RELEASED} = true;
|
||||
$this->pool->return($connection);
|
||||
});
|
||||
|
||||
return $connection;
|
||||
});
|
||||
}
|
||||
|
||||
public function release()
|
||||
{
|
||||
$connection = $this->getPoolConnection();
|
||||
if ($connection->{static::KEY_RELEASED}) {
|
||||
return;
|
||||
}
|
||||
$this->pool->return($connection);
|
||||
}
|
||||
|
||||
public function __call($method, $arguments)
|
||||
{
|
||||
$connection = $this->getPoolConnection();
|
||||
if ($connection->{static::KEY_RELEASED}) {
|
||||
throw new RuntimeException("Connection already has been released!");
|
||||
}
|
||||
|
||||
return $connection->{$method}(...$arguments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool\proxy;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use think\db\BaseQuery;
|
||||
use think\db\ConnectionInterface;
|
||||
use think\DbManager;
|
||||
use think\swoole\pool\Proxy;
|
||||
|
||||
/**
|
||||
* Class Connection
|
||||
* @package think\swoole\pool\db
|
||||
* @property ConnectionInterface $handler
|
||||
*/
|
||||
class Connection extends Proxy implements ConnectionInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* 获取当前连接器类对应的Query类
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getQueryClass(): string
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接数据库方法
|
||||
* @access public
|
||||
* @param array $config 接参数
|
||||
* @param integer $linkNum 连接序号
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(array $config = [], $linkNum = 0)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前的数据库Db对象
|
||||
* @access public
|
||||
* @param DbManager $db
|
||||
* @return void
|
||||
*/
|
||||
public function setDb(DbManager $db)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前的缓存对象
|
||||
* @access public
|
||||
* @param CacheInterface $cache
|
||||
* @return void
|
||||
*/
|
||||
public function setCache(CacheInterface $cache)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库的配置参数
|
||||
* @access public
|
||||
* @param string $config 配置名称
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfig(string $config = '')
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭数据库(或者重新连接)
|
||||
* @access public
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找单条记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @return array
|
||||
*/
|
||||
public function find(BaseQuery $query): array
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @return array
|
||||
*/
|
||||
public function select(BaseQuery $query): array
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @param boolean $getLastInsID 返回自增主键
|
||||
* @return mixed
|
||||
*/
|
||||
public function insert(BaseQuery $query, bool $getLastInsID = false)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @param mixed $dataSet 数据集
|
||||
* @return integer
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function insertAll(BaseQuery $query, array $dataSet = []): int
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @return integer
|
||||
*/
|
||||
public function update(BaseQuery $query): int
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记录
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @return int
|
||||
*/
|
||||
public function delete(BaseQuery $query): int
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到某个字段的值
|
||||
* @access public
|
||||
* @param BaseQuery $query 查询对象
|
||||
* @param string $field 字段名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed
|
||||
*/
|
||||
public function value(BaseQuery $query, string $field, $default = null)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到某个列的数组
|
||||
* @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->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库事务
|
||||
* @access public
|
||||
* @param callable $callback 数据操作方法回调
|
||||
* @return mixed
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function transaction(callable $callback)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动事务
|
||||
* @access public
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function startTrans()
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于非自动提交状态下面的查询提交
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function commit()
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务回滚
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
public function rollback()
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近一次查询的sql语句
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
public function getLastSql(): string
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
public function table($table)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
public function name($name)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\pool\proxy;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use think\contract\CacheHandlerInterface;
|
||||
use think\swoole\pool\Proxy;
|
||||
|
||||
class Store extends Proxy implements CacheHandlerInterface, CacheInterface
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function get($name, $default = null)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function set($name, $value, $expire = null)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function inc(string $name, int $step = 1)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function dec(string $name, int $step = 1)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function delete($name)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function clearTag(array $keys)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getMultiple($keys, $default = null)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setMultiple($values, $ttl = null)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function deleteMultiple($keys)
|
||||
{
|
||||
return $this->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\resetters;
|
||||
|
||||
use think\Container;
|
||||
use think\swoole\concerns\ModifyProperty;
|
||||
use think\swoole\Sandbox;
|
||||
use think\swoole\contract\ResetterInterface;
|
||||
|
||||
/**
|
||||
* Class ResetEvent
|
||||
* @package think\swoole\resetters
|
||||
* @property Container $app;
|
||||
*/
|
||||
class ResetEvent implements ResetterInterface
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
public function handle(Container $app, Sandbox $sandbox)
|
||||
{
|
||||
$event = clone $sandbox->getEvent();
|
||||
$this->modifyProperty($event, $app);
|
||||
$app->instance('event', $event);
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\resetters;
|
||||
|
||||
use think\Container;
|
||||
use think\swoole\concerns\ModifyProperty;
|
||||
use think\swoole\contract\ResetterInterface;
|
||||
use think\swoole\Sandbox;
|
||||
|
||||
/**
|
||||
* Class ResetService
|
||||
* @package think\swoole\resetters
|
||||
* @property Container $app;
|
||||
*/
|
||||
class ResetService implements ResetterInterface
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
/**
|
||||
* "handle" function for resetting app.
|
||||
*
|
||||
* @param Container $app
|
||||
* @param Sandbox $sandbox
|
||||
*/
|
||||
public function handle(Container $app, Sandbox $sandbox)
|
||||
{
|
||||
foreach ($sandbox->getServices() as $service) {
|
||||
$this->modifyProperty($service, $app);
|
||||
if (method_exists($service, 'register')) {
|
||||
$service->register();
|
||||
}
|
||||
if (method_exists($service, 'boot')) {
|
||||
$app->invoke([$service, 'boot']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+72
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class File extends \think\File
|
||||
{
|
||||
public function __destruct()
|
||||
{
|
||||
//销毁时删除临时文件
|
||||
try {
|
||||
if (file_exists($this->getPathname())) {
|
||||
unlink($this->getPathname());
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc;
|
||||
|
||||
class Packer
|
||||
{
|
||||
const HEADER_SIZE = 8;
|
||||
const HEADER_STRUCT = "Nlength/Ntype";
|
||||
const HEADER_PACK = "NN";
|
||||
|
||||
const TYPE_BUFFER = 0;
|
||||
const TYPE_FILE = 1;
|
||||
|
||||
public static function pack($data, $type = self::TYPE_BUFFER)
|
||||
{
|
||||
return pack(self::HEADER_PACK, strlen($data), $type) . $data;
|
||||
}
|
||||
|
||||
public static function unpack($data)
|
||||
{
|
||||
$header = unpack(self::HEADER_STRUCT, substr($data, 0, self::HEADER_SIZE));
|
||||
$data = substr($data, self::HEADER_SIZE);
|
||||
|
||||
return [$header, $data];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc;
|
||||
|
||||
class Protocol
|
||||
{
|
||||
const ACTION_INTERFACE = '@action_interface';
|
||||
const FILE = '@file';
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\client;
|
||||
|
||||
use Generator;
|
||||
|
||||
interface Connector
|
||||
{
|
||||
/**
|
||||
* @param Generator|string $data
|
||||
* @return string
|
||||
*/
|
||||
public function sendAndRecv($data);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\client;
|
||||
|
||||
use RuntimeException;
|
||||
use Swoole\Client;
|
||||
use Swoole\Coroutine;
|
||||
use think\File;
|
||||
use think\helper\Arr;
|
||||
use think\swoole\contract\rpc\ParserInterface;
|
||||
use think\swoole\exception\RpcClientException;
|
||||
use think\swoole\exception\RpcResponseException;
|
||||
use think\swoole\rpc\Error;
|
||||
use think\swoole\rpc\Packer;
|
||||
use think\swoole\rpc\Protocol;
|
||||
|
||||
class Gateway
|
||||
{
|
||||
protected $connector;
|
||||
|
||||
/** @var ParserInterface */
|
||||
protected $parser;
|
||||
|
||||
/**
|
||||
* Gateway constructor.
|
||||
* @param Connector|array $connector
|
||||
* @param ParserInterface $parser
|
||||
*/
|
||||
public function __construct($connector, ParserInterface $parser)
|
||||
{
|
||||
if (is_array($connector)) {
|
||||
$connector = $this->createDefaultConnector($connector);
|
||||
}
|
||||
$this->connector = $connector;
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
protected function encodeData(Protocol $protocol)
|
||||
{
|
||||
$params = $protocol->getParams();
|
||||
|
||||
//有文件,先传输
|
||||
foreach ($params as $index => $param) {
|
||||
if ($param instanceof File) {
|
||||
$handle = fopen($param->getPathname(), "rb");
|
||||
yield pack(Packer::HEADER_PACK, $param->getSize(), Packer::TYPE_FILE);
|
||||
while (!feof($handle)) {
|
||||
yield fread($handle, 8192);
|
||||
}
|
||||
fclose($handle);
|
||||
$params[$index] = Protocol::FILE;
|
||||
}
|
||||
}
|
||||
|
||||
$protocol = Protocol::make($protocol->getInterface(), $protocol->getMethod(), $params);
|
||||
|
||||
$data = $this->parser->encode($protocol);
|
||||
|
||||
yield Packer::pack($data);
|
||||
}
|
||||
|
||||
protected function decodeResponse($response)
|
||||
{
|
||||
[, $response] = Packer::unpack($response);
|
||||
$result = $this->parser->decodeResponse($response);
|
||||
|
||||
if ($result instanceof Error) {
|
||||
throw new RpcResponseException($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function sendAndRecv(Protocol $protocol)
|
||||
{
|
||||
$response = $this->connector->sendAndRecv($this->encodeData($protocol));
|
||||
|
||||
return $this->decodeResponse($response);
|
||||
}
|
||||
|
||||
public function getServices()
|
||||
{
|
||||
$response = $this->connector->sendAndRecv(Packer::pack(Protocol::ACTION_INTERFACE));
|
||||
|
||||
return $this->decodeResponse($response);
|
||||
}
|
||||
|
||||
protected function createDefaultConnector($config)
|
||||
{
|
||||
$class = Coroutine::getCid() > -1 ? Coroutine\Client::class : Client::class;
|
||||
|
||||
/** @var Client|Coroutine\Client $client */
|
||||
$client = new $class(SWOOLE_SOCK_TCP);
|
||||
|
||||
$host = Arr::pull($config, 'host');
|
||||
$port = Arr::pull($config, 'port');
|
||||
$timeout = Arr::pull($config, 'timeout');
|
||||
|
||||
$client->set(array_merge($config, [
|
||||
'open_length_check' => true,
|
||||
'package_length_type' => Packer::HEADER_PACK,
|
||||
'package_length_offset' => 0,
|
||||
'package_body_offset' => 8,
|
||||
]));
|
||||
|
||||
if (!$client->connect($host, $port, $timeout)) {
|
||||
throw new RuntimeException(
|
||||
sprintf('Connect failed host=%s port=%d', $host, $port)
|
||||
);
|
||||
}
|
||||
|
||||
return new class($client) implements Connector {
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* constructor.
|
||||
* @param Client|Coroutine\Client $client
|
||||
*/
|
||||
public function __construct($client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function sendAndRecv($data)
|
||||
{
|
||||
if (!$data instanceof \Generator) {
|
||||
$data = [$data];
|
||||
}
|
||||
|
||||
foreach ($data as $string) {
|
||||
if (!$this->client->send($string)) {
|
||||
throw new RpcClientException(swoole_strerror($this->client->errCode), $this->client->errCode);
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->client->recv();
|
||||
|
||||
if ($response === false || empty($response)) {
|
||||
throw new RpcClientException(swoole_strerror($this->client->errCode), $this->client->errCode);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\client;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Nette\PhpGenerator\Factory;
|
||||
use Nette\PhpGenerator\PhpNamespace;
|
||||
use ReflectionClass;
|
||||
use think\swoole\rpc\Protocol;
|
||||
|
||||
abstract class Proxy
|
||||
{
|
||||
protected $interface;
|
||||
|
||||
/** @var Gateway */
|
||||
protected $gateway;
|
||||
|
||||
final public function __construct(Gateway $gateway)
|
||||
{
|
||||
$this->gateway = $gateway;
|
||||
}
|
||||
|
||||
final protected function proxyCall($method, $params)
|
||||
{
|
||||
$protocol = Protocol::make($this->interface, $method, $params);
|
||||
|
||||
return $this->gateway->sendAndRecv($protocol);
|
||||
}
|
||||
|
||||
final public static function getClassName($client, $interface)
|
||||
{
|
||||
if (!interface_exists($interface)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('%s must be exist interface!', $interface)
|
||||
);
|
||||
}
|
||||
|
||||
$proxyName = class_basename($interface) . "Service";
|
||||
$className = "rpc\\service\\{$client}\\{$proxyName}";
|
||||
|
||||
if (!class_exists($className, false)) {
|
||||
|
||||
$namespace = new PhpNamespace("rpc\\service\\{$client}");
|
||||
$namespace->addUse(Proxy::class);
|
||||
$namespace->addUse($interface);
|
||||
|
||||
$class = $namespace->addClass($proxyName);
|
||||
|
||||
$class->setExtends(Proxy::class);
|
||||
$class->addImplement($interface);
|
||||
$class->addProperty('interface', class_basename($interface));
|
||||
|
||||
$reflection = new ReflectionClass($interface);
|
||||
|
||||
foreach ($reflection->getMethods() as $methodRef) {
|
||||
$method = (new Factory)->fromMethodReflection($methodRef);
|
||||
$body = "\$this->proxyCall('{$methodRef->getName()}', func_get_args());";
|
||||
if ($method->getReturnType() != 'void') {
|
||||
$body = "return {$body}";
|
||||
}
|
||||
$method->setBody($body);
|
||||
$class->addMember($method);
|
||||
}
|
||||
|
||||
eval($namespace);
|
||||
}
|
||||
return $className;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\server;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use think\swoole\rpc\Packer;
|
||||
use think\swoole\rpc\server\channel\Buffer;
|
||||
use think\swoole\rpc\server\channel\File;
|
||||
|
||||
class Channel
|
||||
{
|
||||
protected $header;
|
||||
|
||||
protected $queue;
|
||||
|
||||
public function __construct($header)
|
||||
{
|
||||
$this->header = $header;
|
||||
$this->queue = new Coroutine\Channel(1);
|
||||
|
||||
Coroutine::create(function () use ($header) {
|
||||
switch ($header['type']) {
|
||||
case Packer::TYPE_BUFFER:
|
||||
$type = Buffer::class;
|
||||
break;
|
||||
case Packer::TYPE_FILE:
|
||||
$type = File::class;
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException('not support data type');
|
||||
}
|
||||
|
||||
$handle = new $type($header['length']);
|
||||
$this->queue->push($handle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return File|Buffer
|
||||
*/
|
||||
public function pop()
|
||||
{
|
||||
return $this->queue->pop();
|
||||
}
|
||||
|
||||
public function push($handle)
|
||||
{
|
||||
return $this->queue->push($handle);
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
return $this->queue->close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?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 think\swoole\rpc\File;
|
||||
use think\swoole\rpc\Packer;
|
||||
use think\swoole\rpc\Protocol;
|
||||
use Throwable;
|
||||
|
||||
class Dispatcher
|
||||
{
|
||||
/**
|
||||
* 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;
|
||||
|
||||
protected $files = [];
|
||||
|
||||
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|File $data
|
||||
*/
|
||||
public function dispatch(int $fd, $data)
|
||||
{
|
||||
if ($data instanceof File) {
|
||||
$this->files[$fd][] = $data;
|
||||
} else {
|
||||
try {
|
||||
if ($data === Protocol::ACTION_INTERFACE) {
|
||||
$result = $this->getInterfaces();
|
||||
} else {
|
||||
$protocol = $this->parser->decode($data);
|
||||
|
||||
$interface = $protocol->getInterface();
|
||||
$method = $protocol->getMethod();
|
||||
$params = $protocol->getParams();
|
||||
|
||||
//文件参数
|
||||
foreach ($params as $index => $param) {
|
||||
if ($param === Protocol::FILE) {
|
||||
$params[$index] = array_shift($this->files[$fd]);
|
||||
}
|
||||
}
|
||||
|
||||
$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, Packer::pack($data));
|
||||
//清空文件缓存
|
||||
unset($this->files[$fd]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\server\channel;
|
||||
|
||||
class Buffer
|
||||
{
|
||||
protected $data = '';
|
||||
protected $length;
|
||||
|
||||
public function __construct($length)
|
||||
{
|
||||
$this->length = $length;
|
||||
}
|
||||
|
||||
public function write(&$data)
|
||||
{
|
||||
$size = strlen($this->data);
|
||||
$string = substr($data, 0, $this->length - $size);
|
||||
|
||||
$this->data .= $string;
|
||||
|
||||
if (strlen($data) >= $this->length - $size) {
|
||||
$data = substr($data, $this->length - $size);
|
||||
|
||||
return $this->data;
|
||||
} else {
|
||||
$data = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\rpc\server\channel;
|
||||
|
||||
class File
|
||||
{
|
||||
protected $name;
|
||||
protected $handle;
|
||||
protected $length;
|
||||
|
||||
public function __construct($length)
|
||||
{
|
||||
$this->name = tempnam(sys_get_temp_dir(), "swoole_rpc_");
|
||||
$this->handle = fopen($this->name, 'ab');
|
||||
$this->length = $length;
|
||||
}
|
||||
|
||||
public function write(&$data)
|
||||
{
|
||||
$size = fstat($this->handle)['size'];
|
||||
$string = substr($data, 0, $this->length - $size);
|
||||
|
||||
fwrite($this->handle, $string);
|
||||
|
||||
if (strlen($data) >= $this->length - $size) {
|
||||
fclose($this->handle);
|
||||
$data = substr($data, $this->length - $size);
|
||||
|
||||
return new \think\swoole\rpc\File($this->name);
|
||||
} else {
|
||||
$data = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\websocket\middleware;
|
||||
|
||||
use Closure;
|
||||
use think\App;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use think\Session;
|
||||
|
||||
class SessionInit
|
||||
{
|
||||
/** @var App */
|
||||
protected $app;
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
public function __construct(App $app, Session $session)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session初始化
|
||||
* @access public
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return Response
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Session初始化
|
||||
$varSessionId = $this->app->config->get('session.var_session_id');
|
||||
$cookieName = $this->session->getName();
|
||||
|
||||
if ($varSessionId && $request->request($varSessionId)) {
|
||||
$sessionId = $request->request($varSessionId);
|
||||
} else {
|
||||
$sessionId = $request->cookie($cookieName);
|
||||
}
|
||||
|
||||
if ($sessionId) {
|
||||
$this->session->setId($sessionId);
|
||||
}
|
||||
|
||||
$this->session->init();
|
||||
|
||||
$request->withSession($this->session);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
namespace think\swoole\websocket\room;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Redis as PHPRedis;
|
||||
use Smf\ConnectionPool\ConnectionPool;
|
||||
use Smf\ConnectionPool\Connectors\PhpRedisConnector;
|
||||
use think\helper\Arr;
|
||||
use think\swoole\contract\websocket\RoomInterface;
|
||||
use think\swoole\Manager;
|
||||
use think\swoole\Pool;
|
||||
|
||||
/**
|
||||
* Class RedisRoom
|
||||
*/
|
||||
class Redis implements RoomInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix = 'swoole:';
|
||||
|
||||
/** @var Manager */
|
||||
protected $manager;
|
||||
|
||||
/** @var ConnectionPool */
|
||||
protected $pool;
|
||||
|
||||
/**
|
||||
* RedisRoom constructor.
|
||||
*
|
||||
* @param Manager $manager
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(Manager $manager, array $config)
|
||||
{
|
||||
$this->manager = $manager;
|
||||
$this->config = $config;
|
||||
|
||||
if ($prefix = Arr::get($this->config, 'prefix')) {
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RoomInterface
|
||||
*/
|
||||
public function prepare(): RoomInterface
|
||||
{
|
||||
$this->initData();
|
||||
$this->prepareRedis();
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function prepareRedis()
|
||||
{
|
||||
$this->manager->onEvent('workerStart', function () {
|
||||
$config = $this->config;
|
||||
$this->pool = new ConnectionPool(
|
||||
Pool::pullPoolConfig($config),
|
||||
new PhpRedisConnector(),
|
||||
$config
|
||||
);
|
||||
$this->manager->getPools()->add("websocket.room", $this->pool);
|
||||
});
|
||||
}
|
||||
|
||||
protected function initData()
|
||||
{
|
||||
$connector = new PhpRedisConnector();
|
||||
|
||||
$connection = $connector->connect($this->config);
|
||||
|
||||
if (count($keys = $connection->keys("{$this->prefix}*"))) {
|
||||
$connection->del($keys);
|
||||
}
|
||||
|
||||
$connector->disconnect($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
protected function runWithRedis(\Closure $callable)
|
||||
{
|
||||
$redis = $this->pool->borrow();
|
||||
try {
|
||||
return $callable($redis);
|
||||
} finally {
|
||||
$this->pool->return($redis);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
$this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) {
|
||||
$pipe = $redis->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);
|
||||
|
||||
$this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) {
|
||||
$pipe = $redis->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->runWithRedis(function (PHPRedis $redis) use ($table, $key) {
|
||||
return $redis->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}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}`.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user