From 2d2ac3c8d15a197e8e34049d45ddbe68633599c7 Mon Sep 17 00:00:00 2001 From: chuan <2154243450@qq.com> Date: Sun, 27 Apr 2025 14:02:59 +0800 Subject: [PATCH] init --- .gitignore | 4 + app/Events.php | 84 + app/start_businessworker.php | 40 + app/start_gateway.php | 78 + app/start_register.php | 28 + composer.json | 6 + composer.lock | 148 + start.php | 37 + start_for_win.bat | 2 + vendor/autoload.php | 25 + vendor/composer/ClassLoader.php | 579 ++++ vendor/composer/InstalledVersions.php | 378 +++ vendor/composer/LICENSE | 21 + vendor/composer/autoload_classmap.php | 10 + vendor/composer/autoload_namespaces.php | 9 + vendor/composer/autoload_psr4.php | 11 + vendor/composer/autoload_real.php | 38 + vendor/composer/autoload_static.php | 44 + vendor/composer/installed.json | 141 + vendor/composer/installed.php | 41 + vendor/composer/platform_check.php | 26 + .../gateway-worker/.github/FUNDING.yml | 4 + vendor/workerman/gateway-worker/.gitignore | 7 + .../workerman/gateway-worker/MIT-LICENSE.txt | 21 + vendor/workerman/gateway-worker/README.md | 36 + vendor/workerman/gateway-worker/composer.json | 18 + .../gateway-worker/src/BusinessWorker.php | 515 +++ .../workerman/gateway-worker/src/Gateway.php | 1216 ++++++++ .../gateway-worker/src/Lib/Context.php | 136 + .../workerman/gateway-worker/src/Lib/Db.php | 76 + .../gateway-worker/src/Lib/DbConnection.php | 1979 ++++++++++++ .../gateway-worker/src/Lib/Gateway.php | 1428 +++++++++ .../src/Protocols/GatewayProtocol.php | 228 ++ .../workerman/gateway-worker/src/Register.php | 194 ++ .../workerman/workerman/.github/FUNDING.yml | 4 + vendor/workerman/workerman/.gitignore | 6 + vendor/workerman/workerman/Autoloader.php | 69 + .../Connection/AsyncTcpConnection.php | 382 +++ .../Connection/AsyncUdpConnection.php | 203 ++ .../Connection/ConnectionInterface.php | 126 + .../workerman/Connection/TcpConnection.php | 1004 ++++++ .../workerman/Connection/UdpConnection.php | 208 ++ vendor/workerman/workerman/Events/Ev.php | 189 ++ vendor/workerman/workerman/Events/Event.php | 215 ++ .../workerman/Events/EventInterface.php | 107 + .../workerman/workerman/Events/Libevent.php | 225 ++ .../workerman/workerman/Events/React/Base.php | 264 ++ .../workerman/Events/React/ExtEventLoop.php | 27 + .../Events/React/ExtLibEventLoop.php | 27 + .../Events/React/StreamSelectLoop.php | 26 + vendor/workerman/workerman/Events/Select.php | 357 +++ vendor/workerman/workerman/Events/Swoole.php | 285 ++ vendor/workerman/workerman/Events/Uv.php | 260 ++ vendor/workerman/workerman/Lib/Constants.php | 44 + vendor/workerman/workerman/Lib/Timer.php | 22 + vendor/workerman/workerman/MIT-LICENSE.txt | 21 + .../workerman/workerman/Protocols/Frame.php | 61 + vendor/workerman/workerman/Protocols/Http.php | 323 ++ .../workerman/Protocols/Http/Chunk.php | 48 + .../workerman/Protocols/Http/Request.php | 694 +++++ .../workerman/Protocols/Http/Response.php | 458 +++ .../Protocols/Http/ServerSentEvents.php | 64 + .../workerman/Protocols/Http/Session.php | 461 +++ .../Http/Session/FileSessionHandler.php | 183 ++ .../Session/RedisClusterSessionHandler.php | 46 + .../Http/Session/RedisSessionHandler.php | 154 + .../Http/Session/SessionHandlerInterface.php | 114 + .../workerman/Protocols/Http/mime.types | 90 + .../workerman/Protocols/ProtocolInterface.php | 52 + vendor/workerman/workerman/Protocols/Text.php | 70 + .../workerman/Protocols/Websocket.php | 562 ++++ vendor/workerman/workerman/Protocols/Ws.php | 432 +++ vendor/workerman/workerman/README.md | 342 ++ vendor/workerman/workerman/Timer.php | 220 ++ vendor/workerman/workerman/Worker.php | 2756 +++++++++++++++++ vendor/workerman/workerman/composer.json | 38 + 76 files changed, 18847 insertions(+) create mode 100644 .gitignore create mode 100644 app/Events.php create mode 100644 app/start_businessworker.php create mode 100644 app/start_gateway.php create mode 100644 app/start_register.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 start.php create mode 100644 start_for_win.bat create mode 100644 vendor/autoload.php create mode 100644 vendor/composer/ClassLoader.php create mode 100644 vendor/composer/InstalledVersions.php create mode 100644 vendor/composer/LICENSE create mode 100644 vendor/composer/autoload_classmap.php create mode 100644 vendor/composer/autoload_namespaces.php create mode 100644 vendor/composer/autoload_psr4.php create mode 100644 vendor/composer/autoload_real.php create mode 100644 vendor/composer/autoload_static.php create mode 100644 vendor/composer/installed.json create mode 100644 vendor/composer/installed.php create mode 100644 vendor/composer/platform_check.php create mode 100644 vendor/workerman/gateway-worker/.github/FUNDING.yml create mode 100644 vendor/workerman/gateway-worker/.gitignore create mode 100644 vendor/workerman/gateway-worker/MIT-LICENSE.txt create mode 100644 vendor/workerman/gateway-worker/README.md create mode 100644 vendor/workerman/gateway-worker/composer.json create mode 100644 vendor/workerman/gateway-worker/src/BusinessWorker.php create mode 100644 vendor/workerman/gateway-worker/src/Gateway.php create mode 100644 vendor/workerman/gateway-worker/src/Lib/Context.php create mode 100644 vendor/workerman/gateway-worker/src/Lib/Db.php create mode 100644 vendor/workerman/gateway-worker/src/Lib/DbConnection.php create mode 100644 vendor/workerman/gateway-worker/src/Lib/Gateway.php create mode 100644 vendor/workerman/gateway-worker/src/Protocols/GatewayProtocol.php create mode 100644 vendor/workerman/gateway-worker/src/Register.php create mode 100644 vendor/workerman/workerman/.github/FUNDING.yml create mode 100644 vendor/workerman/workerman/.gitignore create mode 100644 vendor/workerman/workerman/Autoloader.php create mode 100644 vendor/workerman/workerman/Connection/AsyncTcpConnection.php create mode 100644 vendor/workerman/workerman/Connection/AsyncUdpConnection.php create mode 100644 vendor/workerman/workerman/Connection/ConnectionInterface.php create mode 100644 vendor/workerman/workerman/Connection/TcpConnection.php create mode 100644 vendor/workerman/workerman/Connection/UdpConnection.php create mode 100644 vendor/workerman/workerman/Events/Ev.php create mode 100644 vendor/workerman/workerman/Events/Event.php create mode 100644 vendor/workerman/workerman/Events/EventInterface.php create mode 100644 vendor/workerman/workerman/Events/Libevent.php create mode 100644 vendor/workerman/workerman/Events/React/Base.php create mode 100644 vendor/workerman/workerman/Events/React/ExtEventLoop.php create mode 100644 vendor/workerman/workerman/Events/React/ExtLibEventLoop.php create mode 100644 vendor/workerman/workerman/Events/React/StreamSelectLoop.php create mode 100644 vendor/workerman/workerman/Events/Select.php create mode 100644 vendor/workerman/workerman/Events/Swoole.php create mode 100644 vendor/workerman/workerman/Events/Uv.php create mode 100644 vendor/workerman/workerman/Lib/Constants.php create mode 100644 vendor/workerman/workerman/Lib/Timer.php create mode 100644 vendor/workerman/workerman/MIT-LICENSE.txt create mode 100644 vendor/workerman/workerman/Protocols/Frame.php create mode 100644 vendor/workerman/workerman/Protocols/Http.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Chunk.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Request.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Response.php create mode 100644 vendor/workerman/workerman/Protocols/Http/ServerSentEvents.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Session.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Session/FileSessionHandler.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Session/RedisClusterSessionHandler.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Session/RedisSessionHandler.php create mode 100644 vendor/workerman/workerman/Protocols/Http/Session/SessionHandlerInterface.php create mode 100644 vendor/workerman/workerman/Protocols/Http/mime.types create mode 100644 vendor/workerman/workerman/Protocols/ProtocolInterface.php create mode 100644 vendor/workerman/workerman/Protocols/Text.php create mode 100644 vendor/workerman/workerman/Protocols/Websocket.php create mode 100644 vendor/workerman/workerman/Protocols/Ws.php create mode 100644 vendor/workerman/workerman/README.md create mode 100644 vendor/workerman/workerman/Timer.php create mode 100644 vendor/workerman/workerman/Worker.php create mode 100644 vendor/workerman/workerman/composer.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27c4e75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/.vscode +*.log +.env \ No newline at end of file diff --git a/app/Events.php b/app/Events.php new file mode 100644 index 0000000..575d5a7 --- /dev/null +++ b/app/Events.php @@ -0,0 +1,84 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * 用于检测业务代码死循环或者长时间阻塞等问题 + * 如果发现业务卡死,可以将下面declare打开(去掉//注释),并执行php start.php reload + * 然后观察一段时间workerman.log看是否有process_timeout异常 + */ +//declare(ticks=1); + +use \GatewayWorker\Lib\Gateway; + +/** + * 主逻辑 + * 主要是处理 onConnect onMessage onClose 三个方法 + * onConnect 和 onClose 如果不需要可以不用实现并删除 + */ +class Events +{ + /** + * 当客户端连接时触发 + * 如果业务不需此回调可以删除onConnect + * + * @param int $client_id 连接id + */ + public static function onConnect($client_id) + { + echo "客户端连接成功了 === client_id = ".$client_id . PHP_EOL; + // 向当前client_id发送数据 + Gateway::sendToClient($client_id, "你的连接ID为: $client_id "); + } + + /** + * 当客户端发来消息时触发 + * @param int $client_id 连接id + * @param mixed $message 具体消息 + */ + public static function onMessage($client_id, $message) + { + echo "客户端".$client_id."发送的消息 === ".$message . PHP_EOL; + + $data = json_decode($message, true); + $type = isset($data['type']) ? $data['type'] : ''; + $user_id = isset($data['uid']) ? $data['uid'] : 0; + + // 将用户ID与连接绑定 + if ($user_id > 0) { + Gateway::bindUid($client_id, $user_id); + } + + // 根据消息类类型处理逻辑 + switch ($type) { + case 'pop-up': // 弹窗通知 + echo "弹窗通知事件"; + $res = Gateway::sendToAll("广播弹窗消息"); + var_dump($res); + break; + default: + echo "客服端发送的消息type无法处理...".PHP_EOL; + Gateway::sendToCurrentClient("客服端发送的消息type无法处理"); + } + } + + /** + * 当用户断开连接时触发 + * @param int $client_id 连接id + */ + public static function onClose($client_id) + { + // 向所有人发送 +// GateWay::sendToAll("$client_id logout\r\n"); + } +} diff --git a/app/start_businessworker.php b/app/start_businessworker.php new file mode 100644 index 0000000..b3c86c2 --- /dev/null +++ b/app/start_businessworker.php @@ -0,0 +1,40 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \Workerman\WebServer; +use \GatewayWorker\Gateway; +use \GatewayWorker\BusinessWorker; +use \Workerman\Autoloader; + +// 自动加载类 +require_once __DIR__ . '/../vendor/autoload.php'; +require_once __DIR__ . '/Events.php'; + +// bussinessWorker 进程 +$worker = new BusinessWorker(); +// worker名称 +$worker->name = 'PHPBusinessWorker'; +// bussinessWorker进程数量 +$worker->count = 4; +// 服务注册地址 +$worker->registerAddress = '127.0.0.1:1238'; + +$worker->eventHandler = 'Events'; // 绑定事件处理类, 如果类带有命名空间,则需要把命名空间加上, 默认值是Events + +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} + diff --git a/app/start_gateway.php b/app/start_gateway.php new file mode 100644 index 0000000..3f5e494 --- /dev/null +++ b/app/start_gateway.php @@ -0,0 +1,78 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \Workerman\WebServer; +use \GatewayWorker\Gateway; +use \GatewayWorker\BusinessWorker; +use \Workerman\Autoloader; + +// 自动加载类 +require_once __DIR__ . '/../vendor/autoload.php'; + +// gateway 进程,这里使用Text协议,可以用telnet测试 +$gateway = new Gateway("websocket://0.0.0.0:8282"); +$gateway->transport = 'ssl'; // 启用 SSL + +// gateway名称,status方便查看 +$gateway->name = 'PHPGateway'; +// gateway进程数,一般设置2个就足够 +$gateway->count = 2; +// 本机内网ip,分布式部署时使用内网ip +$gateway->lanIp = '127.0.0.1'; +// 内部通讯起始端口,假如$gateway->count=2,起始端口为2900 +// 则一般会使用2900 2901 2个端口作为内部通讯端口 +$gateway->startPort = 2900; +// 服务注册地址 +$gateway->registerAddress = '127.0.0.1:1238'; + +// 心跳间隔 30秒 +$gateway->pingInterval = 20; +// 心跳数据 +$gateway->pingData = '{"type":"pong "}'; + + +// 当客户端连接上来时,设置连接的onWebSocketConnect,即在websocket握手时的回调 +$gateway->onConnect = function($connection) +{ + $connection->onWebSocketConnect = function($connection , $http_header) + { + // 设置允许跨域 +// header('Access-Control-Allow-Origin: *' ); +// header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS'); +// header('Access-Control-Allow-Headers: Content-Type,Authorization,Accpet,Token,Language'); +// header('Access-Control-Max-Age: 86400'); +// header('Access-Control-Allow-Credentials: true'); + + $connection->headers['Access-Control-Allow-Origin'] = '*'; + $connection->headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS'; + $connection->headers['Access-Control-Allow-Headers'] = 'Content-Type'; + + // 可以在这里判断连接来源是否合法,不合法就关掉连接 + // $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接 +// if($_SERVER['HTTP_ORIGIN'] != 'http://kedou.workerman.net') +// { +// $connection->close(); +// } + // onWebSocketConnect 里面$_GET $_SERVER是可用的 + // var_dump($_GET, $_SERVER); + }; +}; + + +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} + diff --git a/app/start_register.php b/app/start_register.php new file mode 100644 index 0000000..4a7047b --- /dev/null +++ b/app/start_register.php @@ -0,0 +1,28 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \GatewayWorker\Register; + +// 自动加载类 +require_once __DIR__ . '/../vendor/autoload.php'; + +// register 必须是text协议,切记不能将register端口开放给外网 (如需允许外网访问,需设置为 text://0.0.0.0:1238) +$register = new Register('text://127.0.0.1:1238'); + +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..56b66db --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "workerman/gateway-worker": "^3.1", + "ext-json": "*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..c5aebe5 --- /dev/null +++ b/composer.lock @@ -0,0 +1,148 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c0ed3867bae35883559b7367d2aee5a0", + "packages": [ + { + "name": "workerman/gateway-worker", + "version": "v3.1.18", + "source": { + "type": "git", + "url": "https://github.com/walkor/GatewayWorker.git", + "reference": "8d371770cb0dbd8166b94d6049a6a497c13476df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/8d371770cb0dbd8166b94d6049a6a497c13476df", + "reference": "8d371770cb0dbd8166b94d6049a6a497c13476df", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0", + "workerman/workerman": "^4.0.30" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "GatewayWorker\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "keywords": [ + "communication", + "distributed" + ], + "support": { + "issues": "https://github.com/walkor/GatewayWorker/issues", + "source": "https://github.com/walkor/GatewayWorker/tree/v3.1.18" + }, + "funding": [ + { + "url": "https://opencollective.com/walkor", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "time": "2024-12-01T09:51:00+00:00" + }, + { + "name": "workerman/workerman", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "cafb5a43d93d7d30a16b32a57948581cca993562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/cafb5a43d93d7d30a16b32a57948581cca993562", + "reference": "cafb5a43d93d7d30a16b32a57948581cca993562", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", + "homepage": "http://www.workerman.net", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://wenda.workerman.net/", + "issues": "https://github.com/walkor/workerman/issues", + "source": "https://github.com/walkor/workerman", + "wiki": "http://doc.workerman.net/" + }, + "funding": [ + { + "url": "https://opencollective.com/workerman", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "time": "2024-11-24T11:45:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/start.php b/start.php new file mode 100644 index 0000000..75f5816 --- /dev/null +++ b/start.php @@ -0,0 +1,37 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..6d29bff --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,378 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = strtr(__DIR__, '\\', '/'); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/workerman/workerman'), + 'GatewayWorker\\' => array($vendorDir . '/workerman/gateway-worker/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..4735f36 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..6bd09fe --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,44 @@ + + array ( + 'Workerman\\' => 10, + ), + 'G' => + array ( + 'GatewayWorker\\' => 14, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Workerman\\' => + array ( + 0 => __DIR__ . '/..' . '/workerman/workerman', + ), + 'GatewayWorker\\' => + array ( + 0 => __DIR__ . '/..' . '/workerman/gateway-worker/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit9c794a7c09bf62eba0cfbd5ecda2bf9b::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit9c794a7c09bf62eba0cfbd5ecda2bf9b::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit9c794a7c09bf62eba0cfbd5ecda2bf9b::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..3c83f0e --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,141 @@ +{ + "packages": [ + { + "name": "workerman/gateway-worker", + "version": "v3.1.18", + "version_normalized": "3.1.18.0", + "source": { + "type": "git", + "url": "https://github.com/walkor/GatewayWorker.git", + "reference": "8d371770cb0dbd8166b94d6049a6a497c13476df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/8d371770cb0dbd8166b94d6049a6a497c13476df", + "reference": "8d371770cb0dbd8166b94d6049a6a497c13476df", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0", + "workerman/workerman": "^4.0.30" + }, + "time": "2024-12-01T09:51:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GatewayWorker\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "keywords": [ + "communication", + "distributed" + ], + "support": { + "issues": "https://github.com/walkor/GatewayWorker/issues", + "source": "https://github.com/walkor/GatewayWorker/tree/v3.1.18" + }, + "funding": [ + { + "url": "https://opencollective.com/walkor", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "install-path": "../workerman/gateway-worker" + }, + { + "name": "workerman/workerman", + "version": "v4.2.1", + "version_normalized": "4.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "cafb5a43d93d7d30a16b32a57948581cca993562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/cafb5a43d93d7d30a16b32a57948581cca993562", + "reference": "cafb5a43d93d7d30a16b32a57948581cca993562", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.0" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "time": "2024-11-24T11:45:37+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Workerman\\": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", + "homepage": "http://www.workerman.net", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://wenda.workerman.net/", + "issues": "https://github.com/walkor/workerman/issues", + "source": "https://github.com/walkor/workerman", + "wiki": "http://doc.workerman.net/" + }, + "funding": [ + { + "url": "https://opencollective.com/workerman", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "install-path": "../workerman/workerman" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 0000000..ac2888f --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,41 @@ + array( + 'name' => '__root__', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'workerman/gateway-worker' => array( + 'pretty_version' => 'v3.1.18', + 'version' => '3.1.18.0', + 'reference' => '8d371770cb0dbd8166b94d6049a6a497c13476df', + 'type' => 'library', + 'install_path' => __DIR__ . '/../workerman/gateway-worker', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'workerman/workerman' => array( + 'pretty_version' => 'v4.2.1', + 'version' => '4.2.1.0', + 'reference' => 'cafb5a43d93d7d30a16b32a57948581cca993562', + 'type' => 'library', + 'install_path' => __DIR__ . '/../workerman/workerman', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 0000000..adfb472 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 80000)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/vendor/workerman/gateway-worker/.github/FUNDING.yml b/vendor/workerman/gateway-worker/.github/FUNDING.yml new file mode 100644 index 0000000..3d88b8d --- /dev/null +++ b/vendor/workerman/gateway-worker/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +open_collective: walkor +patreon: walkor diff --git a/vendor/workerman/gateway-worker/.gitignore b/vendor/workerman/gateway-worker/.gitignore new file mode 100644 index 0000000..f0753f3 --- /dev/null +++ b/vendor/workerman/gateway-worker/.gitignore @@ -0,0 +1,7 @@ +.buildpath +.project +.settings +.idea +vendor +.vscode +composer.lock \ No newline at end of file diff --git a/vendor/workerman/gateway-worker/MIT-LICENSE.txt b/vendor/workerman/gateway-worker/MIT-LICENSE.txt new file mode 100644 index 0000000..fd6b1c8 --- /dev/null +++ b/vendor/workerman/gateway-worker/MIT-LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009-2015 walkor and contributors (see https://github.com/walkor/workerman/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/workerman/gateway-worker/README.md b/vendor/workerman/gateway-worker/README.md new file mode 100644 index 0000000..1b69363 --- /dev/null +++ b/vendor/workerman/gateway-worker/README.md @@ -0,0 +1,36 @@ +GatewayWorker +================= + +GatewayWorker基于[Workerman](https://github.com/walkor/Workerman)开发的一个项目框架,用于快速开发长连接应用,例如app推送服务端、即时IM服务端、游戏服务端、物联网、智能家居等等。 + +GatewayWorker使用经典的Gateway和Worker进程模型。Gateway进程负责维持客户端连接,并转发客户端的数据给Worker进程处理;Worker进程负责处理实际的业务逻辑,并将结果推送给对应的客户端。Gateway服务和Worker服务可以分开部署在不同的服务器上,实现分布式集群。 + +GatewayWorker提供非常方便的API,可以全局广播数据、可以向某个群体广播数据、也可以向某个特定客户端推送数据。配合Workerman的定时器,也可以定时推送数据。 + +快速开始 +====== +开发者可以从一个简单的demo开始(demo中包含了GatewayWorker内核,以及start_gateway.php start_business.php等启动入口文件)
+[点击这里下载demo](http://www.workerman.net/download/GatewayWorker.zip)。
+demo说明见源码readme。 + +手册 +======= +http://www.workerman.net/gatewaydoc/ + +安装内核 +======= + +只安装GatewayWorker内核文件(不包含start_gateway.php start_businessworker.php等启动入口文件) +``` +composer require workerman/gateway-worker +``` + +使用GatewayWorker开发的项目 +======= +## [tadpole](http://kedou.workerman.net/) +[Source code](https://github.com/walkor/workerman-todpole) +![workerman todpole](http://www.workerman.net/img/workerman-todpole.png) + +## [chat room](http://chat.workerman.net/) +[Source code](https://github.com/walkor/workerman-chat) +![workerman-chat](http://www.workerman.net/img/workerman-chat.png) diff --git a/vendor/workerman/gateway-worker/composer.json b/vendor/workerman/gateway-worker/composer.json new file mode 100644 index 0000000..fed2200 --- /dev/null +++ b/vendor/workerman/gateway-worker/composer.json @@ -0,0 +1,18 @@ +{ + "name" : "workerman/gateway-worker", + "keywords": ["distributed","communication"], + "homepage": "http://www.workerman.net", + "license" : "MIT", + "require": { + "php": ">=7.0", + "workerman/workerman" : "^4.0.30" + }, + "autoload": { + "psr-4": {"GatewayWorker\\": "./src"} + }, + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + } +} diff --git a/vendor/workerman/gateway-worker/src/BusinessWorker.php b/vendor/workerman/gateway-worker/src/BusinessWorker.php new file mode 100644 index 0000000..f6fe324 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/BusinessWorker.php @@ -0,0 +1,515 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use Workerman\Connection\TcpConnection; + +use Workerman\Worker; +use Workerman\Timer; +use Workerman\Connection\AsyncTcpConnection; +use GatewayWorker\Protocols\GatewayProtocol; +use GatewayWorker\Lib\Context; + +/** + * + * BusinessWorker 用于处理Gateway转发来的数据 + * + * @author walkor + * + */ +class BusinessWorker extends Worker +{ + /** + * 保存与 gateway 的连接 connection 对象 + * + * @var array + */ + public $gatewayConnections = array(); + + /** + * 注册中心地址 + * + * @var string|array + */ + public $registerAddress = '127.0.0.1:1236'; + + /** + * 事件处理类,默认是 Event 类 + * + * @var string + */ + public $eventHandler = 'Events'; + + /** + * 秘钥 + * + * @var string + */ + public $secretKey = ''; + + /** + * businessWorker进程将消息转发给gateway进程的发送缓冲区大小 + * + * @var int + */ + public $sendToGatewayBufferSize = 10240000; + + /** + * 保存用户设置的 worker 启动回调 + * + * @var callable|null + */ + protected $_onWorkerStart = null; + + /** + * 保存用户设置的 workerReload 回调 + * + * @var callable|null + */ + protected $_onWorkerReload = null; + + /** + * 保存用户设置的 workerStop 回调 + * + * @var callable|null + */ + protected $_onWorkerStop= null; + + /** + * 到注册中心的连接 + * + * @var AsyncTcpConnection + */ + protected $_registerConnection = null; + + /** + * 处于连接状态的 gateway 通讯地址 + * + * @var array + */ + protected $_connectingGatewayAddresses = array(); + + /** + * 所有 geteway 内部通讯地址 + * + * @var array + */ + protected $_gatewayAddresses = array(); + + /** + * 等待连接个 gateway 地址 + * + * @var array + */ + protected $_waitingConnectGatewayAddresses = array(); + + /** + * Event::onConnect 回调 + * + * @var callable|null + */ + protected $_eventOnConnect = null; + + /** + * Event::onMessage 回调 + * + * @var callable|null + */ + protected $_eventOnMessage = null; + + /** + * Event::onClose 回调 + * + * @var callable|null + */ + protected $_eventOnClose = null; + + /** + * websocket回调 + * + * @var null + */ + protected $_eventOnWebSocketConnect = null; + + /** + * SESSION 版本缓存 + * + * @var array + */ + protected $_sessionVersion = array(); + + /** + * 用于保持长连接的心跳时间间隔 + * + * @var int + */ + const PERSISTENCE_CONNECTION_PING_INTERVAL = 25; + + /** + * 构造函数 + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name = '', $context_option = array()) + { + parent::__construct($socket_name, $context_option); + $backrace = debug_backtrace(); + $this->_autoloadRootPath = dirname($backrace[0]['file']); + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + $this->_onWorkerStart = $this->onWorkerStart; + $this->_onWorkerReload = $this->onWorkerReload; + $this->_onWorkerStop = $this->onWorkerStop; + $this->onWorkerStop = array($this, 'onWorkerStop'); + $this->onWorkerStart = array($this, 'onWorkerStart'); + $this->onWorkerReload = array($this, 'onWorkerReload'); + parent::run(); + } + + /** + * 当进程启动时一些初始化工作 + * + * @return void + */ + protected function onWorkerStart() + { + if (function_exists('opcache_reset')) { + opcache_reset(); + } + + if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); + } + + if (!is_array($this->registerAddress)) { + $this->registerAddress = array($this->registerAddress); + } + $this->connectToRegister(); + + \GatewayWorker\Lib\Gateway::setBusinessWorker($this); + \GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey; + if ($this->_onWorkerStart) { + call_user_func($this->_onWorkerStart, $this); + } + + if (is_callable($this->eventHandler . '::onWorkerStart')) { + call_user_func($this->eventHandler . '::onWorkerStart', $this); + } + + // 设置回调 + if (is_callable($this->eventHandler . '::onConnect')) { + $this->_eventOnConnect = $this->eventHandler . '::onConnect'; + } + + if (is_callable($this->eventHandler . '::onMessage')) { + $this->_eventOnMessage = $this->eventHandler . '::onMessage'; + } else { + echo "Waring: {$this->eventHandler}::onMessage is not callable\n"; + } + + if (is_callable($this->eventHandler . '::onClose')) { + $this->_eventOnClose = $this->eventHandler . '::onClose'; + } + + if (is_callable($this->eventHandler . '::onWebSocketConnect')) { + $this->_eventOnWebSocketConnect = $this->eventHandler . '::onWebSocketConnect'; + } + + } + + /** + * onWorkerReload 回调 + * + * @param Worker $worker + */ + protected function onWorkerReload($worker) + { + // 防止进程立刻退出 + $worker->reloadable = false; + // 延迟 0.05 秒退出,避免 BusinessWorker 瞬间全部退出导致没有可用的 BusinessWorker 进程 + Timer::add(0.05, array('Workerman\Worker', 'stopAll')); + // 执行用户定义的 onWorkerReload 回调 + if ($this->_onWorkerReload) { + call_user_func($this->_onWorkerReload, $this); + } + } + + /** + * 当进程关闭时一些清理工作 + * + * @return void + */ + protected function onWorkerStop() + { + if ($this->_onWorkerStop) { + call_user_func($this->_onWorkerStop, $this); + } + if (is_callable($this->eventHandler . '::onWorkerStop')) { + call_user_func($this->eventHandler . '::onWorkerStop', $this); + } + } + + /** + * 连接服务注册中心 + * + * @return void + */ + public function connectToRegister() + { + foreach ($this->registerAddress as $register_address) { + $register_connection = new AsyncTcpConnection("text://{$register_address}"); + $secret_key = $this->secretKey; + $register_connection->onConnect = function () use ($register_connection, $secret_key, $register_address) { + $register_connection->send('{"event":"worker_connect","secret_key":"' . $secret_key . '"}'); + // 如果Register服务器不在本地服务器,则需要保持心跳 + if (strpos($register_address, '127.0.0.1') !== 0) { + $register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) { + $register_connection->send('{"event":"ping"}'); + }); + } + }; + $register_connection->onClose = function ($register_connection) { + if(!empty($register_connection->ping_timer)) { + Timer::del($register_connection->ping_timer); + } + $register_connection->reconnect(1); + }; + $register_connection->onMessage = array($this, 'onRegisterConnectionMessage'); + $register_connection->connect(); + } + } + + + /** + * 当注册中心发来消息时 + * + * @return void + */ + public function onRegisterConnectionMessage($register_connection, $data) + { + $data = json_decode($data, true); + if (!isset($data['event'])) { + echo "Received bad data from Register\n"; + return; + } + $event = $data['event']; + switch ($event) { + case 'broadcast_addresses': + if (!is_array($data['addresses'])) { + echo "Received bad data from Register. Addresses empty\n"; + return; + } + $addresses = $data['addresses']; + $this->_gatewayAddresses = array(); + foreach ($addresses as $addr) { + $this->_gatewayAddresses[$addr] = $addr; + } + $this->checkGatewayConnections($addresses); + break; + default: + echo "Receive bad event:$event from Register.\n"; + } + } + + /** + * 当 gateway 转发来数据时 + * + * @param TcpConnection $connection + * @param mixed $data + */ + public function onGatewayMessage($connection, $data) + { + $cmd = $data['cmd']; + if ($cmd === GatewayProtocol::CMD_PING) { + return; + } + // 上下文数据 + Context::$client_ip = $data['client_ip']; + Context::$client_port = $data['client_port']; + Context::$local_ip = $data['local_ip']; + Context::$local_port = $data['local_port']; + Context::$connection_id = $data['connection_id']; + Context::$client_id = Context::addressToClientId($data['local_ip'], $data['local_port'], + $data['connection_id']); + // $_SERVER 变量 + $_SERVER = array( + 'REMOTE_ADDR' => long2ip($data['client_ip']), + 'REMOTE_PORT' => $data['client_port'], + 'GATEWAY_ADDR' => long2ip($data['local_ip']), + 'GATEWAY_PORT' => $data['gateway_port'], + 'GATEWAY_CLIENT_ID' => Context::$client_id, + ); + // 检查session版本,如果是过期的session数据则拉取最新的数据 + if ($cmd !== GatewayProtocol::CMD_ON_CLOSE && isset($this->_sessionVersion[Context::$client_id]) && $this->_sessionVersion[Context::$client_id] !== crc32($data['ext_data'])) { + $_SESSION = Context::$old_session = \GatewayWorker\Lib\Gateway::getSession(Context::$client_id); + $this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']); + } else { + if (!isset($this->_sessionVersion[Context::$client_id])) { + $this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']); + } + // 尝试解析 session + if ($data['ext_data'] != '') { + Context::$old_session = $_SESSION = Context::sessionDecode($data['ext_data']); + } else { + Context::$old_session = $_SESSION = null; + } + } + + // 尝试执行 Event::onConnection、Event::onMessage、Event::onClose + switch ($cmd) { + case GatewayProtocol::CMD_ON_CONNECT: + if ($this->_eventOnConnect) { + call_user_func($this->_eventOnConnect, Context::$client_id); + } + break; + case GatewayProtocol::CMD_ON_MESSAGE: + if ($this->_eventOnMessage) { + call_user_func($this->_eventOnMessage, Context::$client_id, $data['body']); + } + break; + case GatewayProtocol::CMD_ON_CLOSE: + unset($this->_sessionVersion[Context::$client_id]); + if ($this->_eventOnClose) { + call_user_func($this->_eventOnClose, Context::$client_id); + } + break; + case GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT: + if ($this->_eventOnWebSocketConnect) { + call_user_func($this->_eventOnWebSocketConnect, Context::$client_id, $data['body']); + } + break; + } + + // session 必须是数组 + if ($_SESSION !== null && !is_array($_SESSION)) { + throw new \Exception('$_SESSION must be an array. But $_SESSION=' . var_export($_SESSION, true) . ' is not array.'); + } + + // 判断 session 是否被更改 + if ($_SESSION !== Context::$old_session && $cmd !== GatewayProtocol::CMD_ON_CLOSE) { + $session_str_now = $_SESSION !== null ? Context::sessionEncode($_SESSION) : ''; + \GatewayWorker\Lib\Gateway::setSocketSession(Context::$client_id, $session_str_now); + $this->_sessionVersion[Context::$client_id] = crc32($session_str_now); + } + + Context::clear(); + } + + /** + * 当与 Gateway 的连接断开时触发 + * + * @param TcpConnection $connection + * @return void + */ + public function onGatewayClose($connection) + { + $addr = $connection->remoteAddr; + unset($this->gatewayConnections[$addr], $this->_connectingGatewayAddresses[$addr]); + if (isset($this->_gatewayAddresses[$addr]) && !isset($this->_waitingConnectGatewayAddresses[$addr])) { + Timer::add(1, array($this, 'tryToConnectGateway'), array($addr), false); + $this->_waitingConnectGatewayAddresses[$addr] = $addr; + } + } + + /** + * 尝试连接 Gateway 内部通讯地址 + * + * @param string $addr + */ + public function tryToConnectGateway($addr) + { + if (!isset($this->gatewayConnections[$addr]) && !isset($this->_connectingGatewayAddresses[$addr]) && isset($this->_gatewayAddresses[$addr])) { + $gateway_connection = new AsyncTcpConnection("GatewayProtocol://$addr"); + $gateway_connection->remoteAddr = $addr; + $gateway_connection->onConnect = array($this, 'onConnectGateway'); + $gateway_connection->onMessage = array($this, 'onGatewayMessage'); + $gateway_connection->onClose = array($this, 'onGatewayClose'); + $gateway_connection->onError = array($this, 'onGatewayError'); + $gateway_connection->maxSendBufferSize = $this->sendToGatewayBufferSize; + if (TcpConnection::$defaultMaxSendBufferSize == $gateway_connection->maxSendBufferSize) { + $gateway_connection->maxSendBufferSize = 50 * 1024 * 1024; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_WORKER_CONNECT; + $gateway_data['body'] = json_encode(array( + 'worker_key' =>"{$this->name}:{$this->id}", + 'secret_key' => $this->secretKey, + )); + $gateway_connection->send($gateway_data); + $gateway_connection->connect(); + $this->_connectingGatewayAddresses[$addr] = $addr; + } + unset($this->_waitingConnectGatewayAddresses[$addr]); + } + + /** + * 检查 gateway 的通信端口是否都已经连 + * 如果有未连接的端口,则尝试连接 + * + * @param array $addresses_list + */ + public function checkGatewayConnections($addresses_list) + { + if (empty($addresses_list)) { + return; + } + foreach ($addresses_list as $addr) { + if (!isset($this->_waitingConnectGatewayAddresses[$addr])) { + $this->tryToConnectGateway($addr); + } + } + } + + /** + * 当连接上 gateway 的通讯端口时触发 + * 将连接 connection 对象保存起来 + * + * @param TcpConnection $connection + * @return void + */ + public function onConnectGateway($connection) + { + $this->gatewayConnections[$connection->remoteAddr] = $connection; + unset($this->_connectingGatewayAddresses[$connection->remoteAddr], $this->_waitingConnectGatewayAddresses[$connection->remoteAddr]); + } + + /** + * 当与 gateway 的连接出现错误时触发 + * + * @param TcpConnection $connection + * @param int $error_no + * @param string $error_msg + */ + public function onGatewayError($connection, $error_no, $error_msg) + { + echo "GatewayConnection Error : $error_no ,$error_msg\n"; + } + + /** + * 获取所有 Gateway 内部通讯地址 + * + * @return array + */ + public function getAllGatewayAddresses() + { + return $this->_gatewayAddresses; + } +} diff --git a/vendor/workerman/gateway-worker/src/Gateway.php b/vendor/workerman/gateway-worker/src/Gateway.php new file mode 100644 index 0000000..ec307ea --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Gateway.php @@ -0,0 +1,1216 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use GatewayWorker\Lib\Context; + +use Workerman\Connection\TcpConnection; + +use Workerman\Worker; +use Workerman\Timer; +use Workerman\Autoloader; +use Workerman\Connection\AsyncTcpConnection; +use GatewayWorker\Protocols\GatewayProtocol; + +/** + * + * Gateway,基于Worker 开发 + * 用于转发客户端的数据给Worker处理,以及转发Worker的数据给客户端 + * + * @author walkor + * + */ +class Gateway extends Worker +{ + + /** + * 随机负载均衡 + * + *@var string + */ + const ROUTER_RANDOM = 'router_random'; + + /** + * 最小连接数负载均衡模式 + * + * @var string + */ + const ROUTER_LEAST_CONNECTIONS = 'router_least_connections'; + + /** + * Gateway 默认负载均衡模式 + * + * @var string $selectLoadBalancingMode + */ + public static $selectLoadBalancingMode = self::ROUTER_LEAST_CONNECTIONS; + + + /** + * 本机 IP + * 单机部署默认 127.0.0.1,如果是分布式部署,需要设置成本机 IP + * + * @var string + */ + public $lanIp = '127.0.0.1'; + + /** + * 如果宿主机为192.168.1.2 , gatewayworker in docker container (172.25.0.2) + * 此时 lanIp=192.68.1.2 GatewayClientSDK 能连上,但是$this->_innerTcpWorker stream_socket_server(): Unable to connect to tcp://192.168.1.2:2901 (Address not available) in + * 此时 lanIp=172.25.0.2 GatewayClientSDK stream_socket_server(): Unable to connect to tcp://172.25.0.2:2901 (Address not available) , $this->_innerTcpWorker 正常监听 + * + * solution: + * $gateway->lanIp=192.168.1.2 ; + * $gateway->innerTcpWorkerListen=172.25.0.2; // || 0.0.0.0 + * + * GatewayClientSDK connect 192.168.1.2:lanPort + * $this->_innerTcpWorker listen $gateway->innerTcpWorkerListen:lanPort + * + */ + public $innerTcpWorkerListen=''; + + /** + * 本机端口 + * + * @var string + */ + public $lanPort = 0; + + /** + * gateway 内部通讯起始端口,每个 gateway 实例应该都不同,步长1000 + * + * @var int + */ + public $startPort = 2000; + + /** + * 注册服务地址,用于注册 Gateway BusinessWorker,使之能够通讯 + * + * @var string|array + */ + public $registerAddress = '127.0.0.1:1236'; + + /** + * 心跳时间间隔 + * + * @var int + */ + public $pingInterval = 0; + + /** + * $pingNotResponseLimit * $pingInterval 时间内,客户端未发送任何数据,断开客户端连接 + * + * @var int + */ + public $pingNotResponseLimit = 0; + + /** + * 服务端向客户端发送的心跳数据 + * + * @var string + */ + public $pingData = ''; + + /** + * 秘钥 + * + * @var string + */ + public $secretKey = ''; + + /** + * 路由函数 + * + * @var callable|null + */ + public $router = null; + + + /** + * gateway进程转发给businessWorker进程的发送缓冲区大小 + * + * @var int + */ + public $sendToWorkerBufferSize = 10240000; + + /** + * gateway进程将数据发给客户端时每个客户端发送缓冲区大小 + * + * @var int + */ + public $sendToClientBufferSize = 1024000; + + /** + * 协议加速 + * + * @var bool + */ + public $protocolAccelerate = false; + + /** + * BusinessWorker 连接成功之后触发 + * + * @var callable|null + */ + public $onBusinessWorkerConnected = null; + + /** + * BusinessWorker 关闭时触发 + * + * @var callable|null + */ + public $onBusinessWorkerClose = null; + + /** + * 最小连接数负载均衡记录表,用于新上线业务服务器负载足够均衡 + * [ ip+businessworker key => 连接记录, ip+businessworker key => 连接记录, .... ] + * + * @var array + */ + protected static $leastConnectionsRecord = array(); + + /** + * 保存客户端的所有 connection 对象 + * + * @var array + */ + protected $_clientConnections = array(); + + /** + * uid 到 connection 的映射,一对多关系 + */ + protected $_uidConnections = array(); + + /** + * group 到 connection 的映射,一对多关系 + * + * @var array + */ + protected $_groupConnections = array(); + + /** + * 保存所有 worker 的内部连接的 connection 对象 + * + * @var array + */ + protected $_workerConnections = array(); + + /** + * gateway 内部监听 worker 内部连接的 worker + * + * @var Worker + */ + protected $_innerTcpWorker = null; + + /** + * 当 worker 启动时 + * + * @var callable|null + */ + protected $_onWorkerStart = null; + + /** + * 当有客户端连接时 + * + * @var callable|null + */ + protected $_onConnect = null; + + /** + * 当客户端发来消息时 + * + * @var callable|null + */ + protected $_onMessage = null; + + /** + * 当客户端连接关闭时 + * + * @var callable|null + */ + protected $_onClose = null; + + /** + * 当 worker 停止时 + * + * @var callable|null + */ + protected $_onWorkerStop = null; + + /** + * 进程启动时间 + * + * @var int + */ + protected $_startTime = 0; + + /** + * gateway 监听的端口 + * + * @var int + */ + protected $_gatewayPort = 0; + + /** + * connectionId 记录器 + * @var int + */ + protected static $_connectionIdRecorder = 0; + + /** + * 用于保持长连接的心跳时间间隔 + * + * @var int + */ + const PERSISTENCE_CONNECTION_PING_INTERVAL = 25; + + /** + * 构造函数 + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name, $context_option = array()) + { + parent::__construct($socket_name, $context_option); + $this->reloadable = false; + $this->_gatewayPort = substr(strrchr($socket_name,':'),1); + $this->router = array("\\GatewayWorker\\Gateway", 'routerBind'); + + $backtrace = debug_backtrace(); + $this->_autoloadRootPath = dirname($backtrace[0]['file']); + } + + /** + * {@inheritdoc} + */ + public function run() + { + // 保存用户的回调,当对应的事件发生时触发 + $this->_onWorkerStart = $this->onWorkerStart; + $this->onWorkerStart = array($this, 'onWorkerStart'); + // 保存用户的回调,当对应的事件发生时触发 + $this->_onConnect = $this->onConnect; + $this->onConnect = array($this, 'onClientConnect'); + + // onMessage禁止用户设置回调 + $this->onMessage = array($this, 'onClientMessage'); + + // 保存用户的回调,当对应的事件发生时触发 + $this->_onClose = $this->onClose; + $this->onClose = array($this, 'onClientClose'); + // 保存用户的回调,当对应的事件发生时触发 + $this->_onWorkerStop = $this->onWorkerStop; + $this->onWorkerStop = array($this, 'onWorkerStop'); + + if (!is_array($this->registerAddress)) { + $this->registerAddress = array($this->registerAddress); + } + + // 记录进程启动的时间 + $this->_startTime = time(); + // 运行父方法 + parent::run(); + } + + /** + * 当客户端发来数据时,转发给worker处理 + * + * @param TcpConnection $connection + * @param mixed $data + */ + public function onClientMessage($connection, $data) + { + $connection->pingNotResponseCount = -1; + $this->sendToWorker(GatewayProtocol::CMD_ON_MESSAGE, $connection, $data); + } + + /** + * 当客户端连接上来时,初始化一些客户端的数据 + * 包括全局唯一的client_id、初始化session等 + * + * @param TcpConnection $connection + */ + public function onClientConnect($connection) + { + $connection->id = self::generateConnectionId(); + // 保存该连接的内部通讯的数据包报头,避免每次重新初始化 + $connection->gatewayHeader = array( + 'local_ip' => ip2long(gethostbyname($this->lanIp)), + 'local_port' => $this->lanPort, + 'client_ip' => ip2long($connection->getRemoteIp()), + 'client_port' => $connection->getRemotePort(), + 'gateway_port' => $this->_gatewayPort, + 'connection_id' => $connection->id, + 'flag' => 0, + ); + // 连接的 session + $connection->session = ''; + // 该连接的心跳参数 + $connection->pingNotResponseCount = -1; + // 该链接发送缓冲区大小 + $connection->maxSendBufferSize = $this->sendToClientBufferSize; + // 保存客户端连接 connection 对象 + $this->_clientConnections[$connection->id] = $connection; + + // 如果用户有自定义 onConnect 回调,则执行 + if ($this->_onConnect) { + call_user_func($this->_onConnect, $connection); + if (isset($connection->onWebSocketConnect)) { + $connection->_onWebSocketConnect = $connection->onWebSocketConnect; + } + } + if ($connection->protocol === '\Workerman\Protocols\Websocket' || $connection->protocol === 'Workerman\Protocols\Websocket') { + $connection->onWebSocketConnect = array($this, 'onWebsocketConnect'); + } + + $this->sendToWorker(GatewayProtocol::CMD_ON_CONNECT, $connection); + } + + /** + * websocket握手时触发 + * + * @param $connection + * @param $request + */ + public function onWebsocketConnect($connection, $request) + { + if (isset($connection->_onWebSocketConnect)) { + call_user_func($connection->_onWebSocketConnect, $connection, $request); + unset($connection->_onWebSocketConnect); + } + if (is_object($request)) { + $server = [ + 'QUERY_STRING' => $request->queryString(), + 'REQUEST_METHOD' => $request->method(), + 'REQUEST_URI' => $request->uri(), + 'SERVER_PROTOCOL' => "HTTP/" . $request->protocolVersion(), + 'SERVER_NAME' => $request->host(false), + 'CONTENT_TYPE' => $request->header('content-type'), + 'REMOTE_ADDR' => $connection->getRemoteIp(), + 'REMOTE_PORT' => $connection->getRemotePort(), + 'SERVER_PORT' => $connection->getLocalPort(), + ]; + foreach ($request->header() as $key => $header) { + $key = str_replace('-', '_', strtoupper($key)); + $server["HTTP_$key"] = $header; + } + $data = array('get' => $request->get(), 'server' => $server, 'cookie' => $request->cookie()); + } else { + $data = array('get' => $_GET, 'server' => $_SERVER, 'cookie' => $_COOKIE); + } + $this->sendToWorker(GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT, $connection, $data); + } + + /** + * 生成connection id + * @return int + */ + protected function generateConnectionId() + { + $max_unsigned_int = 4294967295; + if (self::$_connectionIdRecorder >= $max_unsigned_int) { + self::$_connectionIdRecorder = 0; + } + while(++self::$_connectionIdRecorder <= $max_unsigned_int) { + if(!isset($this->_clientConnections[self::$_connectionIdRecorder])) { + break; + } + } + return self::$_connectionIdRecorder; + } + + /** + * 发送数据给 worker 进程 + * + * @param int $cmd + * @param TcpConnection $connection + * @param mixed $body + * @return bool + */ + protected function sendToWorker($cmd, $connection, $body = '') + { + $gateway_data = $connection->gatewayHeader; + $gateway_data['cmd'] = $cmd; + $gateway_data['body'] = $body; + $gateway_data['ext_data'] = $connection->session; + if ($this->_workerConnections) { + // 调用路由函数,选择一个worker把请求转发给它 + /** @var TcpConnection $worker_connection */ + $worker_connection = call_user_func($this->router, $this->_workerConnections, $connection, $cmd, $body); + if (false === $worker_connection->send($gateway_data)) { + $msg = "SendBufferToWorker fail. May be the send buffer are overflow. See http://doc2.workerman.net/send-buffer-overflow.html"; + static::error($msg); + return false; + } + } // 没有可用的 worker + else { + // gateway 启动后 1-2 秒内 SendBufferToWorker fail 是正常现象,因为与 worker 的连接还没建立起来, + // 所以不记录日志,只是关闭连接 + $time_diff = 2; + if (time() - $this->_startTime >= $time_diff) { + $msg = 'SendBufferToWorker fail. The connections between Gateway and BusinessWorker are not ready. See http://doc2.workerman.net/send-buffer-to-worker-fail.html'; + static::error($msg); + } + $connection->destroy(); + return false; + } + return true; + } + + /** + * 随机路由,返回 worker connection 标识 + * + * @param array $worker_connections + * @return string + */ + public static function routerRand(array $worker_connections) : string + { + return array_rand($worker_connections); + } + + /** + * 返回最少客户端连接数量的业务服务器标识 + * 新上线服务器由于客户端连接数过低,会先分配给新服务器 + * + * @throws \Exception + * @param array $leastConnections + * @return string + */ + protected static function routerLeastConnectionsRecord(array $leastConnections) : string + { + if (empty($leastConnections)) + { + throw new \Exception("The routing record is empty."); + } + + // 返回最少客户端连接数量的业务服务器地址 + return array_search(min($leastConnections), $leastConnections, true); + } + + /** + * @param array $worker_connections + * @param string $selectLoadBalancingMode + * @return string + * @throws \Exception + */ + protected static function businessWorkerAddress(array $worker_connections, string $selectLoadBalancingMode) + { + switch ($selectLoadBalancingMode) + { + case static::ROUTER_LEAST_CONNECTIONS: + // 选择连接最少的businessWorker 服务器 + $businessWorkerAddress = static::routerLeastConnectionsRecord(static::$leastConnectionsRecord); + // 更新轮询表连接数量 + static::$leastConnectionsRecord[$businessWorkerAddress]++; + return $businessWorkerAddress; + case static::ROUTER_RANDOM: + // 随机轮询 + return static::routerRand($worker_connections); + default: + throw new \Exception("The load balancing mode is not supported."); + } + } + + /** + * client_id 与 worker 绑定 + * + * @param array $worker_connections + * @param TcpConnection $client_connection + * @param int $cmd + * @param mixed $buffer + * @return TcpConnection + * @throws \Exception + */ + public static function routerBind($worker_connections, $client_connection, $cmd, $buffer) + { + if (!isset($client_connection->businessworker_address) || !isset($worker_connections[$client_connection->businessworker_address])) { + $client_connection->businessworker_address = static::businessWorkerAddress($worker_connections, static::$selectLoadBalancingMode); + } + return $worker_connections[$client_connection->businessworker_address]; + } + + /** + * 当客户端关闭时 + * + * @param TcpConnection $connection + */ + public function onClientClose($connection) + { + // 尝试通知 worker,触发 Event::onClose + $this->sendToWorker(GatewayProtocol::CMD_ON_CLOSE, $connection); + + // 客户端下线,更新路由表数据 + if(static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS && + isset($connection->businessworker_address)) + { + // 客户端连接数 >0,减少连接数 + if((static::$leastConnectionsRecord[$connection->businessworker_address])??0 > 0) + { + static::$leastConnectionsRecord[$connection->businessworker_address]--; + } + } + + unset($this->_clientConnections[$connection->id]); + // 清理 uid 数据 + if (!empty($connection->uid)) { + $uid = $connection->uid; + unset($this->_uidConnections[$uid][$connection->id]); + if (empty($this->_uidConnections[$uid])) { + unset($this->_uidConnections[$uid]); + } + } + // 清理 group 数据 + if (!empty($connection->groups)) { + foreach ($connection->groups as $group) { + unset($this->_groupConnections[$group][$connection->id]); + if (empty($this->_groupConnections[$group])) { + unset($this->_groupConnections[$group]); + } + } + } + // 触发 onClose + if ($this->_onClose) { + call_user_func($this->_onClose, $connection); + } + } + + /** + * 当 Gateway 启动的时候触发的回调函数 + * + * @return void + */ + public function onWorkerStart() + { + // 分配一个内部通讯端口 + $this->lanPort = $this->startPort + $this->id; + + // 如果有设置心跳,则定时执行 + if ($this->pingInterval > 0) { + $timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval / 2 : $this->pingInterval; + Timer::add($timer_interval, array($this, 'ping')); + } + + // 如果BusinessWorker ip不是127.0.0.1,则需要加gateway到BusinessWorker的心跳 + if ($this->lanIp !== '127.0.0.1') { + Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingBusinessWorker')); + } + + if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); + } + + //如为公网IP监听,直接换成0.0.0.0 ,否则用内网IP + $listen_ip=filter_var($this->lanIp,FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)?'0.0.0.0':$this->lanIp; + + //Use scenario to see line 64 + if($this->innerTcpWorkerListen != '') { + $listen_ip = $this->innerTcpWorkerListen; + } + + // 初始化 gateway 内部的监听,用于监听 worker 的连接已经连接上发来的数据 + $this->_innerTcpWorker = new Worker("GatewayProtocol://{$listen_ip}:{$this->lanPort}"); + $this->_innerTcpWorker->reusePort = false; + $this->_innerTcpWorker->listen(); + $this->_innerTcpWorker->name = 'GatewayInnerWorker'; + + if ($this->_autoloadRootPath && class_exists(Autoloader::class)) { + Autoloader::setRootPath($this->_autoloadRootPath); + } + + // 设置内部监听的相关回调 + $this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage'); + + $this->_innerTcpWorker->onConnect = array($this, 'onWorkerConnect'); + $this->_innerTcpWorker->onClose = array($this, 'onWorkerClose'); + + // 注册 gateway 的内部通讯地址,worker 去连这个地址,以便 gateway 与 worker 之间建立起 TCP 长连接 + $this->registerAddress(); + + if ($this->_onWorkerStart) { + call_user_func($this->_onWorkerStart, $this); + } + } + + + /** + * 当 worker 通过内部通讯端口连接到 gateway 时 + * + * @param TcpConnection $connection + */ + public function onWorkerConnect($connection) + { + $connection->maxSendBufferSize = $this->sendToWorkerBufferSize; + $connection->authorized = !$this->secretKey; + } + + /** + * 当 worker 发来数据时 + * + * @param TcpConnection $connection + * @param mixed $data + * @throws \Exception + * + * @return void + */ + public function onWorkerMessage($connection, $data) + { + $cmd = $data['cmd']; + if (empty($connection->authorized) && $cmd !== GatewayProtocol::CMD_WORKER_CONNECT && $cmd !== GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT) { + self::error("Unauthorized request from " . $connection->getRemoteIp() . ":" . $connection->getRemotePort()); + $connection->close(); + return; + } + switch ($cmd) { + // BusinessWorker连接Gateway + case GatewayProtocol::CMD_WORKER_CONNECT: + $worker_info = json_decode($data['body'], true); + if ($worker_info['secret_key'] !== $this->secretKey) { + self::error("Gateway: Worker key does not match ".var_export($this->secretKey, true)." !== ". var_export($this->secretKey)); + $connection->close(); + return; + } + $key = $connection->getRemoteIp() . ':' . $worker_info['worker_key']; + // 在一台服务器上businessWorker->name不能相同 + if (isset($this->_workerConnections[$key])) { + self::error("Gateway: Worker->name conflict. Key:{$key}"); + // 关闭老的,使用新的 + $this->_workerConnections[$key]->close(); + } + $connection->key = $key; + $this->_workerConnections[$key] = $connection; + $connection->authorized = true; + // 新上线业务服务器,初始路由表为0 + if(static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS) { + static::$leastConnectionsRecord[$key] = 0; + } + if ($this->onBusinessWorkerConnected) { + call_user_func($this->onBusinessWorkerConnected, $connection); + } + return; + // GatewayClient连接Gateway + case GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT: + $worker_info = json_decode($data['body'], true); + if ($worker_info['secret_key'] !== $this->secretKey) { + self::error("Gateway: GatewayClient key does not match ".var_export($this->secretKey, true)." !== ".var_export($this->secretKey, true)); + $connection->close(); + return; + } + $connection->authorized = true; + return; + // 向某客户端发送数据,Gateway::sendToClient($client_id, $message); + case GatewayProtocol::CMD_SEND_TO_ONE: + if (isset($this->_clientConnections[$data['connection_id']])) { + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $this->_clientConnections[$data['connection_id']]->send($body, $raw); + } + return; + // 踢出用户,Gateway::closeClient($client_id, $message); + case GatewayProtocol::CMD_KICK: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->close($data['body']); + } + return; + // 立即销毁用户连接, Gateway::destroyClient($client_id); + case GatewayProtocol::CMD_DESTROY: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->destroy(); + } + return; + // 广播, Gateway::sendToAll($message, $client_id_array) + case GatewayProtocol::CMD_SEND_TO_ALL: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $ext_data = $data['ext_data'] ? json_decode($data['ext_data'], true) : ''; + // $client_id_array 不为空时,只广播给 $client_id_array 指定的客户端 + if (isset($ext_data['connections'])) { + foreach ($ext_data['connections'] as $connection_id) { + if (isset($this->_clientConnections[$connection_id])) { + $this->_clientConnections[$connection_id]->send($body, $raw); + } + } + } // $client_id_array 为空时,广播给所有在线客户端 + else { + $exclude_connection_id = !empty($ext_data['exclude']) ? $ext_data['exclude'] : null; + foreach ($this->_clientConnections as $client_connection) { + if (!isset($exclude_connection_id[$client_connection->id])) { + $client_connection->send($body, $raw); + } + } + } + return; + case GatewayProtocol::CMD_SELECT: + $client_info_array = array(); + $ext_data = json_decode($data['ext_data'], true); + if (!$ext_data) { + echo 'CMD_SELECT ext_data=' . var_export($data['ext_data'], true) . '\r\n'; + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + } + $fields = $ext_data['fields']; + $where = $ext_data['where']; + if ($where) { + $connection_box_map = array( + 'groups' => $this->_groupConnections, + 'uid' => $this->_uidConnections + ); + // $where = ['groups'=>[x,x..], 'uid'=>[x,x..], 'connection_id'=>[x,x..]] + foreach ($where as $key => $items) { + if ($key !== 'connection_id') { + $connections_box = $connection_box_map[$key]; + foreach ($items as $item) { + if (isset($connections_box[$item])) { + foreach ($connections_box[$item] as $connection_id => $client_connection) { + if (!isset($client_info_array[$connection_id])) { + $client_info_array[$connection_id] = array(); + // $fields = ['groups', 'uid', 'session'] + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + + } + } + } else { + foreach ($items as $connection_id) { + if (isset($this->_clientConnections[$connection_id])) { + $client_connection = $this->_clientConnections[$connection_id]; + $client_info_array[$connection_id] = array(); + // $fields = ['groups', 'uid', 'session'] + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + } + } + } else { + foreach ($this->_clientConnections as $connection_id => $client_connection) { + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取在线群组列表 + case GatewayProtocol::CMD_GET_GROUP_ID_LIST: + $buffer = serialize(array_keys($this->_groupConnections)); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 重新赋值 session + case GatewayProtocol::CMD_SET_SESSION: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->session = $data['ext_data']; + } + return; + // session合并 + case GatewayProtocol::CMD_UPDATE_SESSION: + if (!isset($this->_clientConnections[$data['connection_id']])) { + return; + } else { + if (!$this->_clientConnections[$data['connection_id']]->session) { + $this->_clientConnections[$data['connection_id']]->session = $data['ext_data']; + return; + } + $session = Context::sessionDecode($this->_clientConnections[$data['connection_id']]->session); + $session_for_merge = Context::sessionDecode($data['ext_data']); + $session = array_replace_recursive($session, $session_for_merge); + $this->_clientConnections[$data['connection_id']]->session = Context::sessionEncode($session); + } + return; + case GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID: + if (!isset($this->_clientConnections[$data['connection_id']])) { + $session = serialize(null); + } else { + if (!$this->_clientConnections[$data['connection_id']]->session) { + $session = serialize(array()); + } else { + $session = $this->_clientConnections[$data['connection_id']]->session; + } + } + $connection->send(pack('N', strlen($session)) . $session, true); + return; + // 获得客户端sessions + case GatewayProtocol::CMD_GET_ALL_CLIENT_SESSIONS: + $client_info_array = array(); + foreach ($this->_clientConnections as $connection_id => $client_connection) { + $client_info_array[$connection_id] = $client_connection->session; + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 判断某个 client_id 是否在线 Gateway::isOnline($client_id) + case GatewayProtocol::CMD_IS_ONLINE: + $buffer = serialize((int)isset($this->_clientConnections[$data['connection_id']])); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 将 client_id 与 uid 绑定 + case GatewayProtocol::CMD_BIND_UID: + $uid = $data['ext_data']; + if (empty($uid)) { + echo "bindUid(client_id, uid) uid empty, uid=" . var_export($uid, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (isset($client_connection->uid)) { + $current_uid = $client_connection->uid; + unset($this->_uidConnections[$current_uid][$connection_id]); + if (empty($this->_uidConnections[$current_uid])) { + unset($this->_uidConnections[$current_uid]); + } + } + $client_connection->uid = $uid; + $this->_uidConnections[$uid][$connection_id] = $client_connection; + return; + // client_id 与 uid 解绑 Gateway::unbindUid($client_id, $uid); + case GatewayProtocol::CMD_UNBIND_UID: + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (isset($client_connection->uid)) { + $current_uid = $client_connection->uid; + unset($this->_uidConnections[$current_uid][$connection_id]); + if (empty($this->_uidConnections[$current_uid])) { + unset($this->_uidConnections[$current_uid]); + } + $client_connection->uid_info = ''; + $client_connection->uid = null; + } + return; + // 发送数据给 uid Gateway::sendToUid($uid, $msg); + case GatewayProtocol::CMD_SEND_TO_UID: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $uid_array = json_decode($data['ext_data'], true); + foreach ($uid_array as $uid) { + if (!empty($this->_uidConnections[$uid])) { + foreach ($this->_uidConnections[$uid] as $connection) { + /** @var TcpConnection $connection */ + $connection->send($body, $raw); + } + } + } + return; + // 将 $client_id 加入用户组 Gateway::joinGroup($client_id, $group); + case GatewayProtocol::CMD_JOIN_GROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "join(group) group empty, group=" . var_export($group, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (!isset($client_connection->groups)) { + $client_connection->groups = array(); + } + $client_connection->groups[$group] = $group; + $this->_groupConnections[$group][$connection_id] = $client_connection; + return; + // 将 $client_id 从某个用户组中移除 Gateway::leaveGroup($client_id, $group); + case GatewayProtocol::CMD_LEAVE_GROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "leave(group) group empty, group=" . var_export($group, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (!isset($client_connection->groups[$group])) { + return; + } + unset($client_connection->groups[$group], $this->_groupConnections[$group][$connection_id]); + if (empty($this->_groupConnections[$group])) { + unset($this->_groupConnections[$group]); + } + return; + // 解散分组 + case GatewayProtocol::CMD_UNGROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "leave(group) group empty, group=" . var_export($group, true); + return; + } + if (empty($this->_groupConnections[$group])) { + return; + } + foreach ($this->_groupConnections[$group] as $client_connection) { + unset($client_connection->groups[$group]); + } + unset($this->_groupConnections[$group]); + return; + // 向某个用户组发送消息 Gateway::sendToGroup($group, $msg); + case GatewayProtocol::CMD_SEND_TO_GROUP: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $ext_data = json_decode($data['ext_data'], true); + $group_array = $ext_data['group']; + $exclude_connection_id = $ext_data['exclude']; + + foreach ($group_array as $group) { + if (!empty($this->_groupConnections[$group])) { + foreach ($this->_groupConnections[$group] as $connection) { + if(!isset($exclude_connection_id[$connection->id])) + { + /** @var TcpConnection $connection */ + $connection->send($body, $raw); + } + } + } + } + return; + // 获取某用户组成员信息 Gateway::getClientSessionsByGroup($group); + case GatewayProtocol::CMD_GET_CLIENT_SESSIONS_BY_GROUP: + $group = $data['ext_data']; + if (!isset($this->_groupConnections[$group])) { + $buffer = serialize(array()); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + } + $client_info_array = array(); + foreach ($this->_groupConnections[$group] as $connection_id => $client_connection) { + $client_info_array[$connection_id] = $client_connection->session; + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取用户组成员数 Gateway::getClientCountByGroup($group); + case GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP: + $group = $data['ext_data']; + $count = 0; + if ($group !== '') { + if (isset($this->_groupConnections[$group])) { + $count = count($this->_groupConnections[$group]); + } + } else { + $count = count($this->_clientConnections); + } + $buffer = serialize($count); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取与某个 uid 绑定的所有 client_id Gateway::getClientIdByUid($uid); + case GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID: + $uid = $data['ext_data']; + if (empty($this->_uidConnections[$uid])) { + $buffer = serialize(array()); + } else { + $buffer = serialize(array_keys($this->_uidConnections[$uid])); + } + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 批量获取与 uid 绑定的所有 client_id Gateway::batchGetClientIdByUid($uid); + case GatewayProtocol::CMD_BATCH_GET_CLIENT_ID_BY_UID: + $uids = json_decode($data['ext_data']); + $return = []; + foreach ($uids as $uid) { + if (empty($this->_uidConnections[$uid])) { + $return[$uid] = []; + } else { + $return[$uid] = array_keys($this->_uidConnections[$uid]); + } + } + $buffer = serialize($return); + + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 批量获取群组ID内客户端个数 + case GatewayProtocol::CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP: + $groups = json_decode($data['ext_data'], true); + $return = []; + foreach ($groups as $group) { + if (isset($this->_groupConnections[$group])) { + $return[$group] = count($this->_groupConnections[$group]); + } else { + $return[$group] = 0; + } + } + + $buffer = serialize($return); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + default : + $err_msg = "gateway inner pack err cmd=$cmd"; + echo $err_msg; + } + } + + + /** + * 当worker连接关闭时 + * + * @param TcpConnection $connection + */ + public function onWorkerClose($connection) + { + if (isset($connection->key)) { + // 业务服务器下线, 清理路由表数据 + if (static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS) + { + unset(static::$leastConnectionsRecord[$connection->key]); + } + unset($this->_workerConnections[$connection->key]); + if ($this->onBusinessWorkerClose) { + call_user_func($this->onBusinessWorkerClose, $connection); + } + } + } + + /** + * 存储当前 Gateway 的内部通信地址 + * + * @return bool + */ + public function registerAddress() + { + $address = $this->lanIp . ':' . $this->lanPort; + foreach ($this->registerAddress as $register_address) { + $register_connection = new AsyncTcpConnection("text://{$register_address}"); + $secret_key = $this->secretKey; + $register_connection->onConnect = function($register_connection) use ($address, $secret_key, $register_address){ + $register_connection->send('{"event":"gateway_connect", "address":"' . $address . '", "secret_key":"' . $secret_key . '"}'); + // 如果Register服务器不在本地服务器,则需要保持心跳 + if (strpos($register_address, '127.0.0.1') !== 0) { + $register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) { + $register_connection->send('{"event":"ping"}'); + }); + } + }; + $register_connection->onClose = function ($register_connection) { + if(!empty($register_connection->ping_timer)) { + Timer::del($register_connection->ping_timer); + } + $register_connection->reconnect(1); + }; + $register_connection->connect(); + } + } + + + /** + * 心跳逻辑 + * + * @return void + */ + public function ping() + { + $ping_data = $this->pingData ? (string)$this->pingData : null; + $raw = false; + if ($this->protocolAccelerate && $ping_data && $this->protocol) { + $ping_data = $this->preEncodeForClient($ping_data); + $raw = true; + } + // 遍历所有客户端连接 + foreach ($this->_clientConnections as $connection) { + // 上次发送的心跳还没有回复次数大于限定值就断开 + if ($this->pingNotResponseLimit > 0 && + $connection->pingNotResponseCount >= $this->pingNotResponseLimit * 2 + ) { + $connection->destroy(); + continue; + } + // $connection->pingNotResponseCount 为 -1 说明最近客户端有发来消息,则不给客户端发送心跳 + $connection->pingNotResponseCount++; + if ($ping_data) { + if ($connection->pingNotResponseCount === 0 || + ($this->pingNotResponseLimit > 0 && $connection->pingNotResponseCount % 2 === 1) + ) { + continue; + } + $connection->send($ping_data, $raw); + } + } + } + + /** + * 向 BusinessWorker 发送心跳数据,用于保持长连接 + * + * @return void + */ + public function pingBusinessWorker() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_PING; + foreach ($this->_workerConnections as $connection) { + $connection->send($gateway_data); + } + } + + /** + * @param mixed $data + * + * @return string + */ + protected function preEncodeForClient($data) + { + foreach ($this->_clientConnections as $client_connection) { + return call_user_func(array($client_connection->protocol, 'encode'), $data, $client_connection); + } + } + + /** + * 当 gateway 关闭时触发,清理数据 + * + * @return void + */ + public function onWorkerStop() + { + // 尝试触发用户设置的回调 + if ($this->_onWorkerStop) { + call_user_func($this->_onWorkerStop, $this); + } + } + + /** + * error. + * @param string $msg + */ + public static function error($msg) + { + Timer::add(1, function() use ($msg) { + Worker::log($msg); + }, null, false); + } +} diff --git a/vendor/workerman/gateway-worker/src/Lib/Context.php b/vendor/workerman/gateway-worker/src/Lib/Context.php new file mode 100644 index 0000000..22ebccb --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Lib/Context.php @@ -0,0 +1,136 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Lib; + +use Exception; + +/** + * 上下文 包含当前用户 uid, 内部通信 local_ip local_port socket_id,以及客户端 client_ip client_port + */ +class Context +{ + /** + * 内部通讯 id + * + * @var string + */ + public static $local_ip; + + /** + * 内部通讯端口 + * + * @var int + */ + public static $local_port; + + /** + * 客户端 ip + * + * @var string + */ + public static $client_ip; + + /** + * 客户端端口 + * + * @var int + */ + public static $client_port; + + /** + * client_id + * + * @var string + */ + public static $client_id; + + /** + * 连接 connection->id + * + * @var int + */ + public static $connection_id; + + /** + * 旧的session + * + * @var string + */ + public static $old_session; + + /** + * 编码 session + * + * @param mixed $session_data + * @return string + */ + public static function sessionEncode($session_data = '') + { + if ($session_data !== '') { + return serialize($session_data); + } + return ''; + } + + /** + * 解码 session + * + * @param string $session_buffer + * @return mixed + */ + public static function sessionDecode($session_buffer) + { + return unserialize($session_buffer); + } + + /** + * 清除上下文 + * + * @return void + */ + public static function clear() + { + self::$local_ip = self::$local_port = self::$client_ip = self::$client_port = + self::$client_id = self::$connection_id = self::$old_session = null; + } + + /** + * 通讯地址到 client_id 的转换 + * + * @param int $local_ip + * @param int $local_port + * @param int $connection_id + * @return string + */ + public static function addressToClientId($local_ip, $local_port, $connection_id) + { + return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id)); + } + + /** + * client_id 到通讯地址的转换 + * + * @param string $client_id + * @return array + * @throws Exception + */ + public static function clientIdToAddress($client_id) + { + if (strlen($client_id) !== 20) { + echo new Exception("client_id $client_id is invalid"); + return false; + } + return unpack('Nlocal_ip/nlocal_port/Nconnection_id', pack('H*', $client_id)); + } +} diff --git a/vendor/workerman/gateway-worker/src/Lib/Db.php b/vendor/workerman/gateway-worker/src/Lib/Db.php new file mode 100644 index 0000000..9f0e4b6 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Lib/Db.php @@ -0,0 +1,76 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Lib; + +use Config\Db as DbConfig; +use Exception; + +/** + * 数据库类 + */ +class Db +{ + /** + * 实例数组 + * + * @var array + */ + protected static $instance = array(); + + /** + * 获取实例 + * + * @param string $config_name + * @return DbConnection + * @throws Exception + */ + public static function instance($config_name) + { + if (!isset(DbConfig::$$config_name)) { + echo "\\Config\\Db::$config_name not set\n"; + throw new Exception("\\Config\\Db::$config_name not set\n"); + } + + if (empty(self::$instance[$config_name])) { + $config = DbConfig::$$config_name; + self::$instance[$config_name] = new DbConnection($config['host'], $config['port'], + $config['user'], $config['password'], $config['dbname'],$config['charset']); + } + return self::$instance[$config_name]; + } + + /** + * 关闭数据库实例 + * + * @param string $config_name + */ + public static function close($config_name) + { + if (isset(self::$instance[$config_name])) { + self::$instance[$config_name]->closeConnection(); + self::$instance[$config_name] = null; + } + } + + /** + * 关闭所有数据库实例 + */ + public static function closeAll() + { + foreach (self::$instance as $connection) { + $connection->closeConnection(); + } + self::$instance = array(); + } +} diff --git a/vendor/workerman/gateway-worker/src/Lib/DbConnection.php b/vendor/workerman/gateway-worker/src/Lib/DbConnection.php new file mode 100644 index 0000000..eeebad6 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Lib/DbConnection.php @@ -0,0 +1,1979 @@ +type = 'SELECT'; + if (!is_array($cols)) { + $cols = array($cols); + } + $this->cols($cols); + return $this; + } + + /** + * 从哪个表删除 + * + * @param string $table + * @return self + */ + public function delete($table) + { + $this->type = 'DELETE'; + $this->table = $this->quoteName($table); + $this->fromRaw($this->quoteName($table)); + return $this; + } + + /** + * 更新哪个表 + * + * @param string $table + * @return self + */ + public function update($table) + { + $this->type = 'UPDATE'; + $this->table = $this->quoteName($table); + return $this; + } + + /** + * 向哪个表插入 + * + * @param string $table + * @return self + */ + public function insert($table) + { + $this->type = 'INSERT'; + $this->table = $this->quoteName($table); + return $this; + } + + /** + * + * 设置 SQL_CALC_FOUND_ROWS 标记. + * + * @param bool $enable + * @return self + */ + public function calcFoundRows($enable = true) + { + $this->setFlag('SQL_CALC_FOUND_ROWS', $enable); + return $this; + } + + /** + * 设置 SQL_CACHE 标记 + * + * @param bool $enable + * @return self + */ + public function cache($enable = true) + { + $this->setFlag('SQL_CACHE', $enable); + return $this; + } + + /** + * 设置 SQL_NO_CACHE 标记 + * + * @param bool $enable + * @return self + */ + public function noCache($enable = true) + { + $this->setFlag('SQL_NO_CACHE', $enable); + return $this; + } + + /** + * 设置 STRAIGHT_JOIN 标记. + * + * @param bool $enable + * @return self + */ + public function straightJoin($enable = true) + { + $this->setFlag('STRAIGHT_JOIN', $enable); + return $this; + } + + /** + * 设置 HIGH_PRIORITY 标记 + * + * @param bool $enable + * @return self + */ + public function highPriority($enable = true) + { + $this->setFlag('HIGH_PRIORITY', $enable); + return $this; + } + + /** + * 设置 SQL_SMALL_RESULT 标记 + * + * @param bool $enable + * @return self + */ + public function smallResult($enable = true) + { + $this->setFlag('SQL_SMALL_RESULT', $enable); + return $this; + } + + /** + * 设置 SQL_BIG_RESULT 标记 + * + * @param bool $enable + * @return self + */ + public function bigResult($enable = true) + { + $this->setFlag('SQL_BIG_RESULT', $enable); + return $this; + } + + /** + * 设置 SQL_BUFFER_RESULT 标记 + * + * @param bool $enable + * @return self + */ + public function bufferResult($enable = true) + { + $this->setFlag('SQL_BUFFER_RESULT', $enable); + return $this; + } + + /** + * 设置 FOR UPDATE 标记 + * + * @param bool $enable + * @return self + */ + public function forUpdate($enable = true) + { + $this->for_update = (bool)$enable; + return $this; + } + + /** + * 设置 DISTINCT 标记 + * + * @param bool $enable + * @return self + */ + public function distinct($enable = true) + { + $this->setFlag('DISTINCT', $enable); + return $this; + } + + /** + * 设置 LOW_PRIORITY 标记 + * + * @param bool $enable + * @return self + */ + public function lowPriority($enable = true) + { + $this->setFlag('LOW_PRIORITY', $enable); + return $this; + } + + /** + * 设置 IGNORE 标记 + * + * @param bool $enable + * @return self + */ + public function ignore($enable = true) + { + $this->setFlag('IGNORE', $enable); + return $this; + } + + /** + * 设置 QUICK 标记 + * + * @param bool $enable + * @return self + */ + public function quick($enable = true) + { + $this->setFlag('QUICK', $enable); + return $this; + } + + /** + * 设置 DELAYED 标记 + * + * @param bool $enable + * @return self + */ + public function delayed($enable = true) + { + $this->setFlag('DELAYED', $enable); + return $this; + } + + /** + * 序列化 + * + * @return string + */ + public function __toString() + { + $union = ''; + if ($this->union) { + $union = implode(' ', $this->union) . ' '; + } + return $union . $this->build(); + } + + /** + * 设置每页多少条记录 + * + * @param int $paging + * @return self + */ + public function setPaging($paging) + { + $this->paging = (int)$paging; + return $this; + } + + /** + * 获取每页多少条记录 + * + * @return int + */ + public function getPaging() + { + return $this->paging; + } + + /** + * 获取绑定在占位符上的值 + */ + public function getBindValues() + { + switch ($this->type) { + case 'SELECT': + return $this->getBindValuesSELECT(); + case 'DELETE': + case 'UPDATE': + case 'INSERT': + return $this->getBindValuesCOMMON(); + default : + throw new Exception("type err"); + } + } + + /** + * 获取绑定在占位符上的值 + * + * @return array + */ + public function getBindValuesSELECT() + { + $bind_values = $this->bind_values; + $i = 1; + foreach ($this->bind_where as $val) { + $bind_values[$i] = $val; + $i++; + } + foreach ($this->bind_having as $val) { + $bind_values[$i] = $val; + $i++; + } + return $bind_values; + } + + /** + * + * SELECT选择哪些列 + * + * @param mixed $key + * @param string $val + * @return void + */ + protected function addColSELECT($key, $val) + { + if (is_string($key)) { + $this->cols[$val] = $key; + } else { + $this->addColWithAlias($val); + } + } + + /** + * SELECT 增加选择的列 + * + * @param string $spec + */ + protected function addColWithAlias($spec) + { + $parts = explode(' ', $spec); + $count = count($parts); + if ($count == 2) { + $this->cols[$parts[1]] = $parts[0]; + } elseif ($count == 3 && strtoupper($parts[1]) == 'AS') { + $this->cols[$parts[2]] = $parts[0]; + } else { + $this->cols[] = $spec; + } + } + + /** + * from 哪个表 + * + * @param string $table + * @return self + */ + public function from($table) + { + return $this->fromRaw($this->quoteName($table)); + } + + /** + * from的表 + * + * @param string $table + * @return self + */ + public function fromRaw($table) + { + $this->from[] = array($table); + $this->from_key++; + return $this; + } + + /** + * + * 子查询 + * + * @param string $table + * @param string $name The alias name for the sub-select. + * @return self + */ + public function fromSubSelect($table, $name) + { + $this->from[] = array("($table) AS " . $this->quoteName($name)); + $this->from_key++; + return $this; + } + + + /** + * 增加 join 语句 + * + * @param string $table + * @param string $cond + * @param string $type + * @return self + * @throws Exception + */ + public function join($table, $cond = null, $type = '') + { + return $this->joinInternal($type, $table, $cond); + } + + /** + * 增加 join 语句 + * + * @param string $join inner, left, natural + * @param string $table + * @param string $cond + * @return self + * @throws Exception + */ + protected function joinInternal($join, $table, $cond = null) + { + if (!$this->from) { + throw new Exception('Cannot join() without from()'); + } + + $join = strtoupper(ltrim("$join JOIN")); + $table = $this->quoteName($table); + $cond = $this->fixJoinCondition($cond); + $this->from[$this->from_key][] = rtrim("$join $table $cond"); + return $this; + } + + /** + * quote + * + * @param string $cond + * @return string + * + */ + protected function fixJoinCondition($cond) + { + if (!$cond) { + return ''; + } + + $cond = $this->quoteNamesIn($cond); + + if (strtoupper(substr(ltrim($cond), 0, 3)) == 'ON ') { + return $cond; + } + + if (strtoupper(substr(ltrim($cond), 0, 6)) == 'USING ') { + return $cond; + } + + return 'ON ' . $cond; + } + + /** + * inner join + * + * @param string $table + * @param string $cond + * @return self + * @throws Exception + */ + public function innerJoin($table, $cond = null) + { + return $this->joinInternal('INNER', $table, $cond); + } + + /** + * left join + * + * @param string $table + * @param string $cond + * @return self + * @throws Exception + */ + public function leftJoin($table, $cond = null) + { + return $this->joinInternal('LEFT', $table, $cond); + } + + /** + * right join + * + * @param string $table + * @param string $cond + * @return self + * @throws Exception + */ + public function rightJoin($table, $cond = null) + { + return $this->joinInternal('RIGHT', $table, $cond); + } + + /** + * joinSubSelect + * + * @param string $join inner, left, natural + * @param string $spec + * @param string $name sub-select 的别名 + * @param string $cond + * @return self + * @throws Exception + */ + public function joinSubSelect($join, $spec, $name, $cond = null) + { + if (!$this->from) { + throw new \Exception('Cannot join() without from() first.'); + } + + $join = strtoupper(ltrim("$join JOIN")); + $name = $this->quoteName($name); + $cond = $this->fixJoinCondition($cond); + $this->from[$this->from_key][] = rtrim("$join ($spec) AS $name $cond"); + return $this; + } + + /** + * group by 语句 + * + * @param array $cols + * @return self + */ + public function groupBy(array $cols) + { + foreach ($cols as $col) { + $this->group_by[] = $this->quoteNamesIn($col); + } + return $this; + } + + /** + * having 语句 + * + * @param string $cond + * @return self + */ + public function having($cond) + { + $this->addClauseCondWithBind('having', 'AND', func_get_args()); + return $this; + } + + /** + * or having 语句 + * + * @param string $cond The HAVING condition. + * @return self + */ + public function orHaving($cond) + { + $this->addClauseCondWithBind('having', 'OR', func_get_args()); + return $this; + } + + /** + * 设置每页的记录数量 + * + * @param int $page + * @return self + */ + public function page($page) + { + $this->limit = 0; + $this->offset = 0; + + $page = (int)$page; + if ($page > 0) { + $this->limit = $this->paging; + $this->offset = $this->paging * ($page - 1); + } + return $this; + } + + /** + * union + * + * @return self + */ + public function union() + { + $this->union[] = $this->build() . ' UNION'; + $this->reset(); + return $this; + } + + /** + * unionAll + * + * @return self + */ + public function unionAll() + { + $this->union[] = $this->build() . ' UNION ALL'; + $this->reset(); + return $this; + } + + /** + * 重置 + */ + protected function reset() + { + $this->resetFlags(); + $this->cols = array(); + $this->from = array(); + $this->from_key = -1; + $this->where = array(); + $this->group_by = array(); + $this->having = array(); + $this->order_by = array(); + $this->limit = 0; + $this->offset = 0; + $this->for_update = false; + } + + /** + * 清除所有数据 + */ + protected function resetAll() + { + $this->union = array(); + $this->for_update = false; + $this->cols = array(); + $this->from = array(); + $this->from_key = -1; + $this->group_by = array(); + $this->having = array(); + $this->bind_having = array(); + $this->paging = 10; + $this->bind_values = array(); + $this->where = array(); + $this->bind_where = array(); + $this->order_by = array(); + $this->limit = 0; + $this->offset = 0; + $this->flags = array(); + $this->table = ''; + $this->last_insert_id_names = array(); + $this->col_values = array(); + $this->returning = array(); + $this->parameters = array(); + } + + /** + * 创建 SELECT SQL + * + * @return string + */ + protected function buildSELECT() + { + return 'SELECT' + . $this->buildFlags() + . $this->buildCols() + . $this->buildFrom() + . $this->buildWhere() + . $this->buildGroupBy() + . $this->buildHaving() + . $this->buildOrderBy() + . $this->buildLimit() + . $this->buildForUpdate(); + } + + /** + * 创建 DELETE SQL + */ + protected function buildDELETE() + { + return 'DELETE' + . $this->buildFlags() + . $this->buildFrom() + . $this->buildWhere() + . $this->buildOrderBy() + . $this->buildLimit() + . $this->buildReturning(); + } + + /** + * 生成 SELECT 列语句 + * + * @return string + * @throws Exception + */ + protected function buildCols() + { + if (!$this->cols) { + throw new Exception('No columns in the SELECT.'); + } + + $cols = array(); + foreach ($this->cols as $key => $val) { + if (is_int($key)) { + $cols[] = $this->quoteNamesIn($val); + } else { + $cols[] = $this->quoteNamesIn("$val AS $key"); + } + } + + return $this->indentCsv($cols); + } + + /** + * 生成 FROM 语句. + * + * @return string + */ + protected function buildFrom() + { + if (!$this->from) { + return ''; + } + + $refs = array(); + foreach ($this->from as $from) { + $refs[] = implode(' ', $from); + } + return ' FROM' . $this->indentCsv($refs); + } + + /** + * 生成 GROUP BY 语句. + * + * @return string + */ + protected function buildGroupBy() + { + if (!$this->group_by) { + return ''; + } + return ' GROUP BY' . $this->indentCsv($this->group_by); + } + + /** + * 生成 HAVING 语句. + * + * @return string + */ + protected function buildHaving() + { + if (!$this->having) { + return ''; + } + return ' HAVING' . $this->indent($this->having); + } + + /** + * 生成 FOR UPDATE 语句 + * + * @return string + */ + protected function buildForUpdate() + { + if (!$this->for_update) { + return ''; + } + return ' FOR UPDATE'; + } + + /** + * where + * + * @param string|array $cond + * @return self + */ + public function where($cond) + { + if (is_array($cond)) { + foreach ($cond as $key => $val) { + if (is_string($key)) { + $this->addWhere('AND', array($key, $val)); + } else { + $this->addWhere('AND', array($val)); + } + } + } else { + $this->addWhere('AND', func_get_args()); + } + return $this; + } + + /** + * or where + * + * @param string|array $cond + * @return self + */ + public function orWhere($cond) + { + if (is_array($cond)) { + foreach ($cond as $key => $val) { + if (is_string($key)) { + $this->addWhere('OR', array($key, $val)); + } else { + $this->addWhere('OR', array($val)); + } + } + } else { + $this->addWhere('OR', func_get_args()); + } + return $this; + } + + /** + * limit + * + * @param int $limit + * @return self + */ + public function limit($limit) + { + $this->limit = (int)$limit; + return $this; + } + + /** + * limit offset + * + * @param int $offset + * @return self + */ + public function offset($offset) + { + $this->offset = (int)$offset; + return $this; + } + + /** + * orderby. + * + * @param array $cols + * @return self + */ + public function orderBy(array $cols) + { + return $this->addOrderBy($cols); + } + + /** + * order by ASC OR DESC + * + * @param array $cols + * @param bool $order_asc + * @return self + */ + public function orderByASC(array $cols, $order_asc = true) + { + $this->order_asc = $order_asc; + return $this->addOrderBy($cols); + } + + /** + * order by DESC + * + * @param array $cols + * @return self + */ + public function orderByDESC(array $cols) + { + $this->order_asc = false; + return $this->addOrderBy($cols); + } + + // -------------abstractquery---------- + /** + * 返回逗号分隔的字符串 + * + * @param array $list + * @return string + */ + protected function indentCsv(array $list) + { + return ' ' . implode(',', $list); + } + + /** + * 返回空格分隔的字符串 + * + * @param array $list + * @return string + */ + protected function indent(array $list) + { + return ' ' . implode(' ', $list); + } + + /** + * 批量为占位符绑定值 + * + * @param array $bind_values + * @return self + * + */ + public function bindValues(array $bind_values) + { + foreach ($bind_values as $key => $val) { + $this->bindValue($key, $val); + } + return $this; + } + + /** + * 单个为占位符绑定值 + * + * @param string $name + * @param mixed $value + * @return self + */ + public function bindValue($name, $value) + { + $this->bind_values[$name] = $value; + return $this; + } + + /** + * 生成 flag + * + * @return string + */ + protected function buildFlags() + { + if (!$this->flags) { + return ''; + } + return ' ' . implode(' ', array_keys($this->flags)); + } + + /** + * 设置 flag. + * + * @param string $flag + * @param bool $enable + */ + protected function setFlag($flag, $enable = true) + { + if ($enable) { + $this->flags[$flag] = true; + } else { + unset($this->flags[$flag]); + } + } + + /** + * 重置 flag + */ + protected function resetFlags() + { + $this->flags = array(); + } + + /** + * + * 添加 where 语句 + * + * @param string $andor 'AND' or 'OR + * @param array $conditions + * @return self + * + */ + protected function addWhere($andor, $conditions) + { + $this->addClauseCondWithBind('where', $andor, $conditions); + return $this; + } + + /** + * 添加条件和绑定值 + * + * @param string $clause where 、having等 + * @param string $andor AND、OR等 + * @param array $conditions + */ + protected function addClauseCondWithBind($clause, $andor, $conditions) + { + $cond = array_shift($conditions); + $cond = $this->quoteNamesIn($cond); + + $bind =& $this->{"bind_{$clause}"}; + foreach ($conditions as $value) { + $bind[] = $value; + } + + $clause =& $this->$clause; + if ($clause) { + $clause[] = "$andor $cond"; + } else { + $clause[] = $cond; + } + } + + /** + * 生成 where 语句 + * + * @return string + */ + protected function buildWhere() + { + if (!$this->where) { + return ''; + } + return ' WHERE' . $this->indent($this->where); + } + + /** + * 增加 order by + * + * @param array $spec The columns and direction to order by. + * @return self + */ + protected function addOrderBy(array $spec) + { + foreach ($spec as $col) { + $this->order_by[] = $this->quoteNamesIn($col); + } + return $this; + } + + /** + * 生成 order by 语句 + * + * @return string + */ + protected function buildOrderBy() + { + if (!$this->order_by) { + return ''; + } + + if ($this->order_asc) { + return ' ORDER BY' . $this->indentCsv($this->order_by) . ' ASC'; + } else { + return ' ORDER BY' . $this->indentCsv($this->order_by) . ' DESC'; + } + } + + /** + * 生成 limit 语句 + * + * @return string + */ + protected function buildLimit() + { + $has_limit = $this->type == 'DELETE' || $this->type == 'UPDATE'; + $has_offset = $this->type == 'SELECT'; + + if ($has_offset && $this->limit) { + $clause = " LIMIT {$this->limit}"; + if ($this->offset) { + $clause .= " OFFSET {$this->offset}"; + } + return $clause; + } elseif ($has_limit && $this->limit) { + return " LIMIT {$this->limit}"; + } + return ''; + } + + /** + * Quotes + * + * @param string $spec + * @return string|array + */ + public function quoteName($spec) + { + $spec = trim($spec); + $seps = array(' AS ', ' ', '.'); + foreach ($seps as $sep) { + $pos = strripos($spec, $sep); + if ($pos) { + return $this->quoteNameWithSeparator($spec, $sep, $pos); + } + } + return $this->replaceName($spec); + } + + /** + * 指定分隔符的 Quotes + * + * @param string $spec + * @param string $sep + * @param int $pos + * @return string + */ + protected function quoteNameWithSeparator($spec, $sep, $pos) + { + $len = strlen($sep); + $part1 = $this->quoteName(substr($spec, 0, $pos)); + $part2 = $this->replaceName(substr($spec, $pos + $len)); + return "{$part1}{$sep}{$part2}"; + } + + /** + * Quotes "table.col" 格式的字符串 + * + * @param string $text + * @return string|array + */ + public function quoteNamesIn($text) + { + $list = $this->getListForQuoteNamesIn($text); + $last = count($list) - 1; + $text = null; + foreach ($list as $key => $val) { + if (($key + 1) % 3) { + $text .= $this->quoteNamesInLoop($val, $key == $last); + } + } + return $text; + } + + /** + * 返回 quote 元素列表 + * + * @param string $text + * @return array + */ + protected function getListForQuoteNamesIn($text) + { + $apos = "'"; + $quot = '"'; + return preg_split( + "/(($apos+|$quot+|\\$apos+|\\$quot+).*?\\2)/", + $text, + -1, + PREG_SPLIT_DELIM_CAPTURE + ); + } + + /** + * 循环 quote + * + * @param string $val + * @param bool $is_last + * @return string + */ + protected function quoteNamesInLoop($val, $is_last) + { + if ($is_last) { + return $this->replaceNamesAndAliasIn($val); + } + return $this->replaceNamesIn($val); + } + + /** + * 替换成别名 + * + * @param string $val + * @return string + */ + protected function replaceNamesAndAliasIn($val) + { + $quoted = $this->replaceNamesIn($val); + $pos = strripos($quoted, ' AS '); + if ($pos) { + $alias = $this->replaceName(substr($quoted, $pos + 4)); + $quoted = substr($quoted, 0, $pos) . " AS $alias"; + } + return $quoted; + } + + /** + * Quotes name + * + * @param string $name + * @return string + */ + protected function replaceName($name) + { + $name = trim($name); + if ($name == '*') { + return $name; + } + return '`' . $name . '`'; + } + + /** + * Quotes + * + * @param string $text + * @return string|array + */ + protected function replaceNamesIn($text) + { + $is_string_literal = strpos($text, "'") !== false + || strpos($text, '"') !== false; + if ($is_string_literal) { + return $text; + } + + $word = '[a-z_][a-z0-9_]+'; + + $find = "/(\\b)($word)\\.($word)(\\b)/i"; + + $repl = '$1`$2`.`$3`$4'; + + $text = preg_replace($find, $repl, $text); + + return $text; + } + + // ---------- insert -------------- + /** + * 设置 `table.column` 与 last-insert-id 的映射 + * + * @param array $last_insert_id_names + */ + public function setLastInsertIdNames(array $last_insert_id_names) + { + $this->last_insert_id_names = $last_insert_id_names; + } + + /** + * insert into. + * + * @param string $table + * @return self + */ + public function into($table) + { + $this->table = $this->quoteName($table); + return $this; + } + + /** + * 生成 INSERT 语句 + * + * @return string + */ + protected function buildINSERT() + { + return 'INSERT' + . $this->buildFlags() + . $this->buildInto() + . $this->buildValuesForInsert() + . $this->buildReturning(); + } + + /** + * 生成 INTO 语句 + * + * @return string + */ + protected function buildInto() + { + return " INTO " . $this->table; + } + + /** + * PDO::lastInsertId() + * + * @param string $col + * @return mixed + */ + public function getLastInsertIdName($col) + { + $key = str_replace('`', '', $this->table) . '.' . $col; + if (isset($this->last_insert_id_names[$key])) { + return $this->last_insert_id_names[$key]; + } + + return null; + } + + /** + * 设置一列,如果有第二各参数,则把第二个参数绑定在占位符上 + * + * @param string $col + * @return self + */ + public function col($col) + { + return call_user_func_array(array($this, 'addCol'), func_get_args()); + } + + /** + * 设置多列 + * + * @param array $cols + * @return self + */ + public function cols(array $cols) + { + if ($this->type == 'SELECT') { + foreach ($cols as $key => $val) { + $this->addColSELECT($key, $val); + } + return $this; + } + return $this->addCols($cols); + } + + /** + * 直接设置列的值 + * + * @param string $col + * @param string $value + * @return self + */ + public function set($col, $value) + { + return $this->setCol($col, $value); + } + + /** + * 为 INSERT 语句绑定值 + * + * @return string + */ + protected function buildValuesForInsert() + { + return ' (' . $this->indentCsv(array_keys($this->col_values)) . ') VALUES (' . + $this->indentCsv(array_values($this->col_values)) . ')'; + } + + // ------update------- + /** + * 更新哪个表 + * + * @param string $table + * @return self + */ + public function table($table) + { + $this->table = $this->quoteName($table); + return $this; + } + + /** + * 生成完整 SQL 语句 + * + * @return string + * @throws Exception + */ + protected function build() + { + switch ($this->type) { + case 'DELETE': + return $this->buildDELETE(); + case 'INSERT': + return $this->buildINSERT(); + case 'UPDATE': + return $this->buildUPDATE(); + case 'SELECT': + return $this->buildSELECT(); + } + throw new Exception("type empty"); + } + + /** + * 生成更新的 SQL 语句 + */ + protected function buildUPDATE() + { + return 'UPDATE' + . $this->buildFlags() + . $this->buildTable() + . $this->buildValuesForUpdate() + . $this->buildWhere() + . $this->buildOrderBy() + . $this->buildLimit() + . $this->buildReturning(); + } + + /** + * 哪个表 + * + * @return string + */ + protected function buildTable() + { + return " {$this->table}"; + } + + /** + * 为更新语句绑定值 + * + * @return string + */ + protected function buildValuesForUpdate() + { + $values = array(); + foreach ($this->col_values as $col => $value) { + $values[] = "{$col} = {$value}"; + } + return ' SET' . $this->indentCsv($values); + } + + // ----------Dml--------------- + /** + * 获取绑定的值 + * + * @return array + */ + public function getBindValuesCOMMON() + { + $bind_values = $this->bind_values; + $i = 1; + foreach ($this->bind_where as $val) { + $bind_values[$i] = $val; + $i++; + } + return $bind_values; + } + + /** + * 设置列 + * + * @param string $col + * @return self + */ + protected function addCol($col) + { + $key = $this->quoteName($col); + $this->col_values[$key] = ":$col"; + $args = func_get_args(); + if (count($args) > 1) { + $this->bindValue($col, $args[1]); + } + return $this; + } + + /** + * 设置多个列 + * + * @param array $cols + * @return self + */ + protected function addCols(array $cols) + { + foreach ($cols as $key => $val) { + if (is_int($key)) { + $this->addCol($val); + } else { + $this->addCol($key, $val); + } + } + return $this; + } + + /** + * 设置单列的值 + * + * @param string $col . + * @param string $value + * @return self + */ + protected function setCol($col, $value) + { + if ($value === null) { + $value = 'NULL'; + } + + $key = $this->quoteName($col); + $value = $this->quoteNamesIn($value); + $this->col_values[$key] = $value; + return $this; + } + + /** + * 增加返回的列 + * + * @param array $cols + * @return self + * + */ + protected function addReturning(array $cols) + { + foreach ($cols as $col) { + $this->returning[] = $this->quoteNamesIn($col); + } + return $this; + } + + /** + * 生成 RETURNING 语句 + * + * @return string + */ + protected function buildReturning() + { + if (!$this->returning) { + return ''; + } + return ' RETURNING' . $this->indentCsv($this->returning); + } + + /** + * 构造函数 + * + * @param string $host + * @param int $port + * @param string $user + * @param string $password + * @param string $db_name + * @param string $charset + */ + public function __construct($host, $port, $user, $password, $db_name, $charset = 'utf8') + { + $this->settings = array( + 'host' => $host, + 'port' => $port, + 'user' => $user, + 'password' => $password, + 'dbname' => $db_name, + 'charset' => $charset, + ); + $this->connect(); + } + + /** + * 创建 PDO 实例 + */ + protected function connect() + { + $dsn = 'mysql:dbname=' . $this->settings["dbname"] . ';host=' . + $this->settings["host"] . ';port=' . $this->settings['port']; + $this->pdo = new PDO($dsn, $this->settings["user"], $this->settings["password"], + array( + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . (!empty($this->settings['charset']) ? + $this->settings['charset'] : 'utf8') + )); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false); + $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + } + + /** + * 关闭连接 + */ + public function closeConnection() + { + $this->pdo = null; + } + + /** + * 执行 + * + * @param string $query + * @param string $parameters + * @throws PDOException + */ + protected function execute($query, $parameters = "") + { + try { + $this->sQuery = @$this->pdo->prepare($query); + $this->bindMore($parameters); + if (!empty($this->parameters)) { + foreach ($this->parameters as $param) { + $parameters = explode("\x7F", $param); + if ($parameters[0][0] !== ':') { + $parameters[0] = intval($parameters[0]); + } + $this->sQuery->bindParam($parameters[0], $parameters[1]); + } + } + $this->success = $this->sQuery->execute(); + } catch (PDOException $e) { + // 服务端断开时重连一次 + if (isset($e->errorInfo[1]) && ($e->errorInfo[1] == 2006 || $e->errorInfo[1] == 2013)) { + $this->closeConnection(); + $this->connect(); + + try { + $this->sQuery = $this->pdo->prepare($query); + $this->bindMore($parameters); + if (!empty($this->parameters)) { + foreach ($this->parameters as $param) { + $parameters = explode("\x7F", $param); + $this->sQuery->bindParam($parameters[0], $parameters[1]); + } + } + $this->success = $this->sQuery->execute(); + } catch (PDOException $ex) { + $this->rollBackTrans(); + throw $ex; + } + } else { + $this->rollBackTrans(); + $msg = $e->getMessage(); + $err_msg = "SQL:".$this->lastSQL()." ".$msg; + $exception = new \PDOException($err_msg, (int)$e->getCode()); + throw $exception; + } + } + $this->parameters = array(); + } + + /** + * 绑定 + * + * @param string $para + * @param string $value + */ + public function bind($para, $value) + { + if (is_string($para)) { + $this->parameters[sizeof($this->parameters)] = ":" . $para . "\x7F" . $value; + } else { + $this->parameters[sizeof($this->parameters)] = $para . "\x7F" . $value; + } + } + + /** + * 绑定多个 + * + * @param array $parray + */ + public function bindMore($parray) + { + if (empty($this->parameters) && is_array($parray)) { + $columns = array_keys($parray); + foreach ($columns as $i => &$column) { + $this->bind($column, $parray[$column]); + } + } + } + + /** + * 执行 SQL + * + * @param string $query + * @param array $params + * @param int $fetchmode + * @return mixed + */ + public function query($query = '', $params = null, $fetchmode = PDO::FETCH_ASSOC) + { + $query = trim($query); + if (empty($query)) { + $query = $this->build(); + if (!$params) { + $params = $this->getBindValues(); + } + } + + $this->resetAll(); + $this->lastSql = $query; + + $this->execute($query, $params); + + $rawStatement = explode(" ", $query); + + $statement = strtolower(trim($rawStatement[0])); + if ($statement === 'select' || $statement === 'show') { + return $this->sQuery->fetchAll($fetchmode); + } elseif ($statement === 'update' || $statement === 'delete') { + return $this->sQuery->rowCount(); + } elseif ($statement === 'insert') { + if ($this->sQuery->rowCount() > 0) { + return $this->lastInsertId(); + } + } else { + return null; + } + + return null; + } + + /** + * 返回一列 + * + * @param string $query + * @param array $params + * @return array + */ + public function column($query = '', $params = null) + { + $query = trim($query); + if (empty($query)) { + $query = $this->build(); + if (!$params) { + $params = $this->getBindValues(); + } + } + + $this->resetAll(); + $this->lastSql = $query; + + $this->execute($query, $params); + $columns = $this->sQuery->fetchAll(PDO::FETCH_NUM); + $column = null; + foreach ($columns as $cells) { + $column[] = $cells[0]; + } + return $column; + } + + /** + * 返回一行 + * + * @param string $query + * @param array $params + * @param int $fetchmode + * @return array + */ + public function row($query = '', $params = null, $fetchmode = PDO::FETCH_ASSOC) + { + $query = trim($query); + if (empty($query)) { + $query = $this->build(); + if (!$params) { + $params = $this->getBindValues(); + } + } + + $this->resetAll(); + $this->lastSql = $query; + + $this->execute($query, $params); + return $this->sQuery->fetch($fetchmode); + } + + /** + * 返回单个值 + * + * @param string $query + * @param array $params + * @return string + */ + public function single($query = '', $params = null) + { + $query = trim($query); + if (empty($query)) { + $query = $this->build(); + if (!$params) { + $params = $this->getBindValues(); + } + } + + $this->resetAll(); + $this->lastSql = $query; + + $this->execute($query, $params); + return $this->sQuery->fetchColumn(); + } + + /** + * 返回 lastInsertId + * + * @return string + */ + public function lastInsertId() + { + return $this->pdo->lastInsertId(); + } + + /** + * 返回最后一条执行的 sql + * + * @return string + */ + public function lastSQL() + { + return $this->lastSql; + } + + /** + * 开始事务 + */ + public function beginTrans() + { + try { + $this->pdo->beginTransaction(); + } catch (PDOException $e) { + // 服务端断开时重连一次 + if ($e->errorInfo[1] == 2006 || $e->errorInfo[1] == 2013) { + $this->pdo->beginTransaction(); + } else { + throw $e; + } + } + } + + /** + * 提交事务 + */ + public function commitTrans() + { + $this->pdo->commit(); + } + + /** + * 事务回滚 + */ + public function rollBackTrans() + { + if ($this->pdo->inTransaction()) { + $this->pdo->rollBack(); + } + } +} diff --git a/vendor/workerman/gateway-worker/src/Lib/Gateway.php b/vendor/workerman/gateway-worker/src/Lib/Gateway.php new file mode 100644 index 0000000..a027bc4 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Lib/Gateway.php @@ -0,0 +1,1428 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Lib; + +use Exception; +use GatewayWorker\Protocols\GatewayProtocol; +use Workerman\Connection\TcpConnection; + +/** + * 数据发送相关 + */ +class Gateway +{ + /** + * gateway 实例 + * + * @var object + */ + protected static $businessWorker = null; + + /** + * 注册中心地址 + * + * @var string|array + */ + public static $registerAddress = '127.0.0.1:1236'; + + /** + * 秘钥 + * @var string + */ + public static $secretKey = ''; + + /** + * 链接超时时间 + * @var int + */ + public static $connectTimeout = 3; + + /** + * 与Gateway是否是长链接 + * @var bool + */ + public static $persistentConnection = true; + + /** + * 是否清除注册地址缓存 + * @var bool + */ + public static $addressesCacheDisable = false; + + /** + * 与gateway建立的连接 + * @var array + */ + protected static $gatewayConnections = []; + + /** + * 向所有客户端连接(或者 client_id_array 指定的客户端连接)广播消息 + * + * @param string $message 向客户端发送的消息 + * @param array $client_id_array 客户端 id 数组 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 是否发送原始数据(即不调用gateway的协议的encode方法) + * @return void + * @throws Exception + */ + public static function sendToAll($message, $client_id_array = null, $exclude_client_id = null, $raw = false) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_ALL; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if ($exclude_client_id) { + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + if ($client_id_array) { + $exclude_client_id = array_flip($exclude_client_id); + } + } + + if ($client_id_array) { + if (!is_array($client_id_array)) { + echo new \Exception('bad $client_id_array:'.var_export($client_id_array, true)); + return; + } + $data_array = array(); + foreach ($client_id_array as $client_id) { + if (isset($exclude_client_id[$client_id])) { + continue; + } + $address = Context::clientIdToAddress($client_id); + if ($address) { + $key = long2ip($address['local_ip']) . ":{$address['local_port']}"; + $data_array[$key][$address['connection_id']] = $address['connection_id']; + } + } + foreach ($data_array as $addr => $connection_id_list) { + $the_gateway_data = $gateway_data; + $the_gateway_data['ext_data'] = json_encode(array('connections' => $connection_id_list)); + static::sendToGateway($addr, $the_gateway_data); + } + return; + } elseif (empty($client_id_array) && is_array($client_id_array)) { + return; + } + + if (!$exclude_client_id) { + return static::sendToAllGateway($gateway_data); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + static::sendToGateway($address, $gateway_data); + } + } + + } + + /** + * 向某个client_id对应的连接发消息 + * + * @param string $client_id + * @param string $message + * @param bool $raw + * @return bool + */ + public static function sendToClient($client_id, $message, $raw = false) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SEND_TO_ONE, $message, '', $raw); + } + + /** + * 向当前客户端连接发送消息 + * + * @param string $message + * @param bool $raw + * @return bool + */ + public static function sendToCurrentClient($message, $raw = false) + { + return static::sendCmdAndMessageToClient(null, GatewayProtocol::CMD_SEND_TO_ONE, $message, '', $raw); + } + + /** + * 判断某个uid是否在线 + * + * @param string $uid + * @return int 0|1 + */ + public static function isUidOnline($uid) + { + return (int)static::getClientIdByUid($uid); + } + + /** + * 判断client_id对应的连接是否在线 + * + * @param string $client_id + * @return int 0|1 + */ + public static function isOnline($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return 0; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return 0; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_IS_ONLINE; + $gateway_data['connection_id'] = $address_data['connection_id']; + return (int)static::sendAndRecv($address, $gateway_data); + } + + /** + * 获取所有在线用户的session,client_id为 key(弃用,请用getAllClientSessions代替) + * + * @param string $group + * @return array + */ + public static function getAllClientInfo($group = '') + { + echo "Warning: Gateway::getAllClientInfo is deprecated and will be removed in a future, please use Gateway::getAllClientSessions instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取所有在线client_id的session,client_id为 key + * + * @param string $group + * @return array + */ + public static function getAllClientSessions($group = '') + { + $gateway_data = GatewayProtocol::$empty; + if (!$group) { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_ALL_CLIENT_SESSIONS; + } else { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_SESSIONS_BY_GROUP; + $gateway_data['ext_data'] = $group; + } + $status_data = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $data) { + if ($data) { + foreach ($data as $connection_id => $session_buffer) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + if ($client_id === Context::$client_id) { + $status_data[$client_id] = (array)$_SESSION; + } else { + $status_data[$client_id] = $session_buffer ? Context::sessionDecode($session_buffer) : array(); + } + } + } + } + } + return $status_data; + } + + /** + * 获取某个组的连接信息(弃用,请用getClientSessionsByGroup代替) + * + * @param string $group + * @return array + */ + public static function getClientInfoByGroup($group) + { + echo "Warning: Gateway::getClientInfoByGroup is deprecated and will be removed in a future, please use Gateway::getClientSessionsByGroup instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取某个组的所有client_id的session信息 + * + * @param string $group + * + * @return array + */ + public static function getClientSessionsByGroup($group) + { + if (static::isValidGroupId($group)) { + return static::getAllClientSessions($group); + } + return array(); + } + + /** + * 获取所有在线client_id数 + * + * @return int + */ + public static function getAllClientIdCount() + { + return static::getClientCountByGroup(); + } + + /** + * 获取所有在线client_id数(getAllClientIdCount的别名) + * + * @return int + */ + public static function getAllClientCount() + { + return static::getAllClientIdCount(); + } + + /** + * 获取某个组的在线client_id数 + * + * @param string $group + * @return int + */ + public static function getClientIdCountByGroup($group = '') + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP; + $gateway_data['ext_data'] = $group; + $total_count = 0; + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $count) { + if ($count) { + $total_count += $count; + } + } + } + return $total_count; + } + + /** + * getClientIdCountByGroup 函数的别名 + * + * @param string $group + * @return int + */ + public static function getClientCountByGroup($group = '') + { + return static::getClientIdCountByGroup($group); + } + + /** + * 获取某个群组在线client_id列表 + * + * @param string $group + * @return array + */ + public static function getClientIdListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $data = static::select(array('uid'), array('groups' => is_array($group) ? $group : array($group))); + $client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_map[$client_id] = $client_id; + } + } + } + return $client_id_map; + } + + /** + * 获取集群所有在线client_id列表 + * + * @return array + */ + public static function getAllClientIdList() + { + return static::formatClientIdFromGatewayBuffer(static::select(array('uid'))); + } + + /** + * 格式化client_id + * + * @param $data + * @return array + */ + protected static function formatClientIdFromGatewayBuffer($data) + { + $client_id_list = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_list[$client_id] = $client_id; + } + } + } + return $client_id_list; + } + + + /** + * 获取与 uid 绑定的 client_id 列表 + * + * @param string $uid + * @return array + */ + public static function getClientIdByUid($uid) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID; + $gateway_data['ext_data'] = $uid; + $client_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $connection_id_array) { + if ($connection_id_array) { + foreach ($connection_id_array as $connection_id) { + $client_list[] = Context::addressToClientId($local_ip, $local_port, $connection_id); + } + } + } + } + return $client_list; + } + + /** + * 获取某个群组在线uid列表 + * + * @param string $group + * @return array + */ + public static function getUidListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $group = is_array($group) ? $group : array($group); + $data = static::select(array('uid'), array('groups' => $group)); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取某个群组在线uid数 + * + * @param string $group + * @return int + */ + public static function getUidCountByGroup($group) + { + if (static::isValidGroupId($group)) { + return count(static::getUidListByGroup($group)); + } + return 0; + } + + /** + * 获取全局在线uid列表 + * + * @return array + */ + public static function getAllUidList() + { + $data = static::select(array('uid')); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取全局在线uid数 + * @return int + */ + public static function getAllUidCount() + { + return count(static::getAllUidList()); + } + + /** + * 通过client_id获取uid + * + * @param $client_id + * @return mixed + */ + public static function getUidByClientId($client_id) + { + $data = static::select(array('uid'), array('client_id'=>array($client_id))); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $info) { + return $info['uid']; + } + } + } + } + + /** + * 获取所有在线的群组id + * + * @return array + */ + public static function getAllGroupIdList() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_GROUP_ID_LIST; + $group_id_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $group_id_array) { + if (is_array($group_id_array)) { + foreach ($group_id_array as $group_id) { + if (!isset($group_id_list[$group_id])) { + $group_id_list[$group_id] = $group_id; + } + } + } + } + } + return $group_id_list; + } + + + /** + * 获取所有在线分组的uid数量,也就是每个分组的在线用户数 + * + * @return array + */ + public static function getAllGroupUidCount() + { + $group_uid_map = static::getAllGroupUidList(); + $group_uid_count_map = array(); + foreach ($group_uid_map as $group_id => $uid_list) { + $group_uid_count_map[$group_id] = count($uid_list); + } + return $group_uid_count_map; + } + + + + /** + * 获取所有分组uid在线列表 + * + * @return array + */ + public static function getAllGroupUidList() + { + $data = static::select(array('uid','groups')); + $group_uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['uid']) || empty($info['groups'])) { + break; + } + $uid = $info['uid']; + foreach ($info['groups'] as $group_id) { + if(!isset($group_uid_map[$group_id])) { + $group_uid_map[$group_id] = array(); + } + $group_uid_map[$group_id][$uid] = $uid; + } + } + } + } + return $group_uid_map; + } + + /** + * 获取所有群组在线client_id列表 + * + * @return array + */ + public static function getAllGroupClientIdList() + { + $data = static::select(array('groups')); + $group_client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['groups'])) { + break; + } + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + foreach ($info['groups'] as $group_id) { + if(!isset($group_client_id_map[$group_id])) { + $group_client_id_map[$group_id] = array(); + } + $group_client_id_map[$group_id][$client_id] = $client_id; + } + } + } + } + return $group_client_id_map; + } + + /** + * 获取所有群组在线client_id数量,也就是获取每个群组在线连接数 + * + * @return array + */ + public static function getAllGroupClientIdCount() + { + $group_client_map = static::getAllGroupClientIdList(); + $group_client_count_map = array(); + foreach ($group_client_map as $group_id => $client_id_list) { + $group_client_count_map[$group_id] = count($client_id_list); + } + return $group_client_count_map; + } + + + /** + * 根据条件到gateway搜索数据 + * + * @param array $fields + * @param array $where + * @return array + */ + protected static function select($fields = array('session','uid','groups'), $where = array()) + { + $t = microtime(true); + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SELECT; + $gateway_data['ext_data'] = array('fields' => $fields, 'where' => $where); + $gateway_data_list = array(); + // 有client_id,能计算出需要和哪些gateway通讯,只和必要的gateway通讯能降低系统负载 + if (isset($where['client_id'])) { + $client_id_list = $where['client_id']; + unset($gateway_data['ext_data']['where']['client_id']); + $gateway_data['ext_data']['where']['connection_id'] = array(); + foreach ($client_id_list as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + continue; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + $gateway_data_list[$address]['ext_data']['where']['connection_id'][$address_data['connection_id']] = $address_data['connection_id']; + } + foreach ($gateway_data_list as $address => $item) { + $gateway_data_list[$address]['ext_data'] = json_encode($item['ext_data']); + } + // 有其它条件,则还是需要向所有gateway发送 + if (count($where) !== 1) { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + foreach (static::getAllGatewayAddress() as $address) { + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + } + } + $data = static::getBufferFromSomeGateway($gateway_data_list); + } else { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + $data = static::getBufferFromAllGateway($gateway_data); + } + + return $data; + } + + /** + * 生成验证包,用于验证此客户端的合法性 + * + * @return string + */ + protected static function generateAuthBuffer() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT; + $gateway_data['body'] = json_encode(array( + 'secret_key' => static::$secretKey, + )); + return GatewayProtocol::encode($gateway_data); + } + + /** + * 批量向某些gateway发包,并得到返回数组 + * + * @param array $gateway_data_array + * @return array + * @throws Exception + */ + protected static function getBufferFromSomeGateway($gateway_data_array) + { + $gateway_buffer_array = array(); + $auth_buffer = static::$secretKey ? static::generateAuthBuffer() : ''; + foreach ($gateway_data_array as $address => $gateway_data) { + if ($auth_buffer) { + $gateway_buffer_array[$address] = $auth_buffer.GatewayProtocol::encode($gateway_data); + } else { + $gateway_buffer_array[$address] = GatewayProtocol::encode($gateway_data); + } + } + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 批量向所有 gateway 发包,并得到返回数组 + * + * @param string $gateway_data + * @return array + * @throws Exception + */ + protected static function getBufferFromAllGateway($gateway_data) + { + $addresses = static::getAllGatewayAddress(); + $gateway_buffer_array = array(); + $gateway_buffer = GatewayProtocol::encode($gateway_data); + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + foreach ($addresses as $address) { + $gateway_buffer_array[$address] = $gateway_buffer; + } + + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 获取所有gateway内部通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddress() + { + if (isset(static::$businessWorker)) { + $addresses = static::$businessWorker->getAllGatewayAddresses(); + if (empty($addresses)) { + throw new Exception('businessWorker::getAllGatewayAddresses return empty'); + } + } else { + $addresses = static::getAllGatewayAddressesFromRegister(); + if (empty($addresses)) { + return array(); + } + } + return $addresses; + } + + /** + * 批量向gateway发送并获取数据 + * @param $gateway_buffer_array + * @return array + */ + protected static function getBufferFromGateway($gateway_buffer_array) + { + $client_array = $status_data = $client_address_map = $receive_buffer_array = $recv_length_array = array(); + // 批量向所有gateway进程发送请求数据 + foreach ($gateway_buffer_array as $address => $gateway_buffer) { + $client = static::getGatewayConnection("tcp://$address"); + if (strlen($gateway_buffer) === stream_socket_sendto($client, $gateway_buffer)) { + $socket_id = (int)$client; + $client_array[$socket_id] = $client; + $client_address_map[$socket_id] = explode(':', $address); + $receive_buffer_array[$socket_id] = ''; + } + } + // 超时5秒 + $timeout = 5; + $time_start = microtime(true); + // 批量接收请求 + while (count($client_array) > 0) { + $write = $except = array(); + $read = $client_array; + if (@stream_select($read, $write, $except, $timeout)) { + foreach ($read as $client) { + $socket_id = (int)$client; + $buffer = stream_socket_recvfrom($client, 65535); + if ($buffer !== '' && $buffer !== false) { + $receive_buffer_array[$socket_id] .= $buffer; + $receive_length = strlen($receive_buffer_array[$socket_id]); + if (empty($recv_length_array[$socket_id]) && $receive_length >= 4) { + $recv_length_array[$socket_id] = current(unpack('N', $receive_buffer_array[$socket_id])); + } + if (!empty($recv_length_array[$socket_id]) && $receive_length >= $recv_length_array[$socket_id] + 4) { + unset($client_array[$socket_id]); + } + } elseif (feof($client)) { + unset($client_array[$socket_id]); + } + } + } + if (microtime(true) - $time_start > $timeout) { + static::$gatewayConnections = []; + break; + } + } + $format_buffer_array = array(); + foreach ($receive_buffer_array as $socket_id => $buffer) { + $local_ip = ip2long($client_address_map[$socket_id][0]); + $local_port = $client_address_map[$socket_id][1]; + $format_buffer_array[$local_ip][$local_port] = unserialize(substr($buffer, 4)); + } + return $format_buffer_array; + } + + /** + * 踢掉某个客户端,并以$message通知被踢掉客户端 + * + * @param string $client_id + * @param string $message + * @return void + */ + public static function closeClient($client_id, $message = null) + { + if ($client_id === Context::$client_id) { + return static::closeCurrentClient($message); + } // 不是发给当前用户则使用存储中的地址 + else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::kickAddress($address, $address_data['connection_id'], $message); + } + } + + /** + * 踢掉当前客户端,并以$message通知被踢掉客户端 + * + * @param string $message + * @return bool + * @throws Exception + */ + public static function closeCurrentClient($message = null) + { + if (!Context::$connection_id) { + throw new Exception('closeCurrentClient can not be called in async context'); + } + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + return static::kickAddress($address, Context::$connection_id, $message); + } + + /** + * 踢掉某个客户端并直接立即销毁相关连接 + * + * @param string $client_id + * @return bool + */ + public static function destoryClient($client_id) + { + if ($client_id === Context::$client_id) { + return static::destoryCurrentClient(); + } // 不是发给当前用户则使用存储中的地址 + else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::destroyAddress($address, $address_data['connection_id']); + } + } + + /** + * 踢掉当前客户端并直接立即销毁相关连接 + * + * @return bool + * @throws Exception + */ + public static function destoryCurrentClient() + { + if (!Context::$connection_id) { + throw new Exception('destoryCurrentClient can not be called in async context'); + } + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + return static::destroyAddress($address, Context::$connection_id); + } + + /** + * 将 client_id 与 uid 绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function bindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_BIND_UID, '', $uid); + } + + /** + * 将 client_id 与 uid 解除绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function unbindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UNBIND_UID, '', $uid); + } + + /** + * 将 client_id 加入组 + * + * @param string $client_id + * @param int|string $group + * @return void + */ + public static function joinGroup($client_id, $group) + { + + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_JOIN_GROUP, '', $group); + } + + /** + * 将 client_id 离开组 + * + * @param string $client_id + * @param int|string $group + * + * @return void + */ + public static function leaveGroup($client_id, $group) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_LEAVE_GROUP, '', $group); + } + + /** + * 取消分组 + * + * @param int|string $group + * + * @return void + */ + public static function ungroup($group) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_UNGROUP; + $gateway_data['ext_data'] = $group; + return static::sendToAllGateway($gateway_data); + + } + + /** + * 向所有 uid 发送 + * + * @param int|string|array $uid + * @param string $message + * @param bool $raw + * + * @return void + */ + public static function sendToUid($uid, $message, $raw = false) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_UID; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if (!is_array($uid)) { + $uid = array($uid); + } + + $gateway_data['ext_data'] = json_encode($uid); + + static::sendToAllGateway($gateway_data); + } + + /** + * 向 group 发送 + * + * @param int|string|array $group 组(不允许是 0 '0' false null array()等为空的值) + * @param string $message 消息 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 发送原始数据(即不调用gateway的协议的encode方法) + * + * @return void + */ + public static function sendToGroup($group, $message, $exclude_client_id = null, $raw = false) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_GROUP; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if (!is_array($group)) { + $group = array($group); + } + + // 分组发送,没有排除的client_id,直接发送 + $default_ext_data_buffer = json_encode(array('group'=> $group, 'exclude'=> null)); + if (empty($exclude_client_id)) { + $gateway_data['ext_data'] = $default_ext_data_buffer; + return static::sendToAllGateway($gateway_data); + } + + // 分组发送,有排除的client_id,需要将client_id转换成对应gateway进程内的connectionId + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + static::sendToGateway($address, $gateway_data); + } + } + } + + /** + * 更新 session,框架自动调用,开发者不要调用 + * + * @param string $client_id + * @param string $session_str + * @return bool + */ + public static function setSocketSession($client_id, $session_str) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SET_SESSION, '', $session_str); + } + + /** + * 设置 session,原session值会被覆盖 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function setSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = $session; + Context::$old_session = $_SESSION; + } + static::setSocketSession($client_id, Context::sessionEncode($session)); + } + + /** + * 更新 session,实际上是与老的session合并 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function updateSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = array_replace_recursive((array)$_SESSION, $session); + Context::$old_session = $_SESSION; + } + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UPDATE_SESSION, '', Context::sessionEncode($session)); + } + + /** + * 获取某个client_id的session + * + * @param string $client_id + * @return mixed false表示出错、null表示用户不存在、array表示具体的session信息 + */ + public static function getSession($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return null; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID; + $gateway_data['connection_id'] = $address_data['connection_id']; + return static::sendAndRecv($address, $gateway_data); + } + + /** + * 向某个用户网关发送命令和消息 + * + * @param string $client_id + * @param int $cmd + * @param string $message + * @param string $ext_data + * @param bool $raw + * @return boolean + */ + protected static function sendCmdAndMessageToClient($client_id, $cmd, $message, $ext_data = '', $raw = false) + { + // 如果是发给当前用户则直接获取上下文中的地址 + if ($client_id === Context::$client_id || $client_id === null) { + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + $connection_id = Context::$connection_id; + } else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + $connection_id = $address_data['connection_id']; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = $cmd; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + if (!empty($ext_data)) { + $gateway_data['ext_data'] = $ext_data; + } + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + return static::sendToGateway($address, $gateway_data); + } + + /** + * 发送数据并返回 + * + * @param int $address + * @param mixed $data + * @return bool + * @throws Exception + */ + protected static function sendAndRecv($address, $data) + { + $buffer = GatewayProtocol::encode($data); + $buffer = static::$secretKey ? static::generateAuthBuffer() . $buffer : $buffer; + $address = "tcp://$address"; + $client = static::getGatewayConnection($address); + if (strlen($buffer) === stream_socket_sendto($client, $buffer)) { + $timeout = 5; + // 阻塞读 + stream_set_blocking($client, 1); + // 1秒超时 + stream_set_timeout($client, 1); + $all_buffer = ''; + $time_start = microtime(true); + $pack_len = 0; + while (1) { + $buf = stream_socket_recvfrom($client, 655350); + if ($buf !== '' && $buf !== false) { + $all_buffer .= $buf; + } else { + if (feof($client)) { + unset(static::$gatewayConnections[$address]); + throw new Exception("connection close $address"); + } elseif (microtime(true) - $time_start > $timeout) { + unset(static::$gatewayConnections[$address]); + break; + } + continue; + } + $recv_len = strlen($all_buffer); + if (!$pack_len && $recv_len >= 4) { + $pack_len= current(unpack('N', $all_buffer)); + } + if (microtime(true) - $time_start > $timeout) { + unset(static::$gatewayConnections[$address]); + break; + } + // 回复的数据都是以\n结尾 + if (($pack_len && $recv_len >= $pack_len + 4)) { + break; + } + } + // 返回结果 + return unserialize(substr($all_buffer, 4)); + } else { + throw new Exception("sendAndRecv($address, \$bufer) fail ! Can not send data!", 502); + } + } + + /** + * 发送数据到网关 + * + * @param string $address + * @param array $gateway_data + * @return bool + */ + protected static function sendToGateway($address, $gateway_data) + { + return static::sendBufferToGateway($address, GatewayProtocol::encode($gateway_data)); + } + + /** + * 发送buffer数据到网关 + * @param string $address + * @param string $gateway_buffer + * @return bool + */ + protected static function sendBufferToGateway($address, $gateway_buffer) + { + // 有$businessWorker说明是workerman环境,使用$businessWorker发送数据 + if (static::$businessWorker) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return false; + } + return static::$businessWorker->gatewayConnections[$address]->send($gateway_buffer, true); + } + // 非workerman环境 + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + $client = static::getGatewayConnection("tcp://$address"); + return strlen($gateway_buffer) == stream_socket_sendto($client, $gateway_buffer); + } + + /** + * 向所有 gateway 发送数据 + * + * @param string $gateway_data + * @throws Exception + * + * @return void + */ + protected static function sendToAllGateway($gateway_data) + { + $buffer = GatewayProtocol::encode($gateway_data); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $gateway_connection) { + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($buffer, true); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + static::sendBufferToGateway($address, $buffer); + } + } + } + + /** + * 踢掉某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function kickAddress($address, $connection_id, $message) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_KICK; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 销毁某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function destroyAddress($address, $connection_id) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_DESTROY; + $gateway_data['connection_id'] = $connection_id; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 将clientid数组转换成address数组 + * + * @param array $client_id_array + * @return array + */ + protected static function clientIdArrayToAddressArray(array $client_id_array) + { + $address_connection_array = array(); + foreach ($client_id_array as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if ($address_data) { + $address = long2ip($address_data['local_ip']) . + ":{$address_data['local_port']}"; + $address_connection_array[$address][$address_data['connection_id']] = $address_data['connection_id']; + } + } + return $address_connection_array; + } + + /** + * 设置 gateway 实例 + * + * @param \GatewayWorker\BusinessWorker $business_worker_instance + */ + public static function setBusinessWorker($business_worker_instance) + { + static::$businessWorker = $business_worker_instance; + } + + /** + * 获取通过注册中心获取所有 gateway 通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddressesFromRegister() + { + static $addresses_cache, $last_update; + if (static::$addressesCacheDisable) { + $addresses_cache = null; + } + $time_now = time(); + $expiration_time = 1; + $register_addresses = (array)static::$registerAddress; + if(empty($addresses_cache) || $time_now - $last_update > $expiration_time) { + foreach ($register_addresses as $register_address) { + $client = stream_socket_client('tcp://' . $register_address, $errno, $errmsg, static::$connectTimeout); + if ($client) { + break; + } + } + if (!$client) { + throw new Exception('Can not connect to tcp://' . $register_address . ' ' . $errmsg); + } + + fwrite($client, '{"event":"worker_connect","secret_key":"' . static::$secretKey . '"}' . "\n"); + stream_set_timeout($client, 5); + $ret = fgets($client, 655350); + if (!$ret || !$data = json_decode(trim($ret), true)) { + throw new Exception('getAllGatewayAddressesFromRegister fail. tcp://' . + $register_address . ' return ' . var_export($ret, true)); + } + $last_update = $time_now; + $addresses_cache = $data['addresses']; + } + if (!$addresses_cache) { + throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' . + json_encode(static::$registerAddress) . ' return ' . var_export($addresses_cache, true)); + } + return $addresses_cache; + } + + /** + * 检查群组id是否合法 + * + * @param $group + * @return bool + */ + protected static function isValidGroupId($group) + { + if (empty($group)) { + echo new \Exception('group('.var_export($group, true).') empty'); + return false; + } + return true; + } + + /** + * 获取与gateway的连接,用于数据返回 + * + * @param $address + * @return mixed + * @throws Exception + */ + protected static function getGatewayConnection($address) + { + $ttl = 50; + $time = time(); + if (isset(static::$gatewayConnections[$address])) { + $created_time = static::$gatewayConnections[$address]['created_time']; + $connection = static::$gatewayConnections[$address]['connection']; + if ($time - $created_time > $ttl || !is_resource($connection) || feof($connection)) { + \set_error_handler(function () {}); + fclose($connection); + \restore_error_handler(); + unset(static::$gatewayConnections[$address]); + } + } + if (!isset(static::$gatewayConnections[$address])) { + $client = stream_socket_client($address, $errno, $errmsg, static::$connectTimeout); + if (!$client) { + throw new Exception("can not connect to $address $errmsg"); + } + static::$gatewayConnections[$address] = [ + 'created_time' => $time, + 'connection' => $client + ]; + } + $client = static::$gatewayConnections[$address]['connection']; + if (!static::$persistentConnection) { + static::$gatewayConnections = []; + } + return $client; + } +} + +if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); +} diff --git a/vendor/workerman/gateway-worker/src/Protocols/GatewayProtocol.php b/vendor/workerman/gateway-worker/src/Protocols/GatewayProtocol.php new file mode 100644 index 0000000..6960ab5 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Protocols/GatewayProtocol.php @@ -0,0 +1,228 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Protocols; + +/** + * Gateway 与 Worker 间通讯的二进制协议 + * + * struct GatewayProtocol + * { + * unsigned int pack_len, + * unsigned char cmd,//命令字 + * unsigned int local_ip, + * unsigned short local_port, + * unsigned int client_ip, + * unsigned short client_port, + * unsigned int connection_id, + * unsigned char flag, + * unsigned short gateway_port, + * unsigned int ext_len, + * char[ext_len] ext_data, + * char[pack_length-HEAD_LEN] body//包体 + * } + * NCNnNnNCnN + */ +class GatewayProtocol +{ + // 发给worker,gateway有一个新的连接 + const CMD_ON_CONNECT = 1; + + // 发给worker的,客户端有消息 + const CMD_ON_MESSAGE = 3; + + // 发给worker上的关闭链接事件 + const CMD_ON_CLOSE = 4; + + // 发给gateway的向单个用户发送数据 + const CMD_SEND_TO_ONE = 5; + + // 发给gateway的向所有用户发送数据 + const CMD_SEND_TO_ALL = 6; + + // 发给gateway的踢出用户 + // 1、如果有待发消息,将在发送完后立即销毁用户连接 + // 2、如果无待发消息,将立即销毁用户连接 + const CMD_KICK = 7; + + // 发给gateway的立即销毁用户连接 + const CMD_DESTROY = 8; + + // 发给gateway,通知用户session更新 + const CMD_UPDATE_SESSION = 9; + + // 获取在线状态 + const CMD_GET_ALL_CLIENT_SESSIONS = 10; + + // 判断是否在线 + const CMD_IS_ONLINE = 11; + + // client_id绑定到uid + const CMD_BIND_UID = 12; + + // 解绑 + const CMD_UNBIND_UID = 13; + + // 向uid发送数据 + const CMD_SEND_TO_UID = 14; + + // 根据uid获取绑定的clientid + const CMD_GET_CLIENT_ID_BY_UID = 15; + + // 批量获取uid列表批量获取绑定的clientid + const CMD_BATCH_GET_CLIENT_ID_BY_UID = 16; + + // 加入组 + const CMD_JOIN_GROUP = 20; + + // 离开组 + const CMD_LEAVE_GROUP = 21; + + // 向组成员发消息 + const CMD_SEND_TO_GROUP = 22; + + // 获取组成员 + const CMD_GET_CLIENT_SESSIONS_BY_GROUP = 23; + + // 获取组在线连接数 + const CMD_GET_CLIENT_COUNT_BY_GROUP = 24; + + // 按照条件查找 + const CMD_SELECT = 25; + + // 获取在线的群组ID + const CMD_GET_GROUP_ID_LIST = 26; + + // 取消分组 + const CMD_UNGROUP = 27; + + // 批量获取群组ID内客户端个数 + const CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP = 28; + + // worker连接gateway事件 + const CMD_WORKER_CONNECT = 200; + + // 心跳 + const CMD_PING = 201; + + // GatewayClient连接gateway事件 + const CMD_GATEWAY_CLIENT_CONNECT = 202; + + // 根据client_id获取session + const CMD_GET_SESSION_BY_CLIENT_ID = 203; + + // 发给gateway,覆盖session + const CMD_SET_SESSION = 204; + + // 当websocket握手时触发,只有websocket协议支持此命令字 + const CMD_ON_WEBSOCKET_CONNECT = 205; + + // 包体是标量 + const FLAG_BODY_IS_SCALAR = 0x01; + + // 通知gateway在send时不调用协议encode方法,在广播组播时提升性能 + const FLAG_NOT_CALL_ENCODE = 0x02; + + /** + * 包头长度 + * + * @var int + */ + const HEAD_LEN = 28; + + public static $empty = array( + 'cmd' => 0, + 'local_ip' => 0, + 'local_port' => 0, + 'client_ip' => 0, + 'client_port' => 0, + 'connection_id' => 0, + 'flag' => 0, + 'gateway_port' => 0, + 'ext_data' => '', + 'body' => '', + ); + + /** + * 返回包长度 + * + * @param string $buffer + * @return int return current package length + */ + public static function input($buffer) + { + if (strlen($buffer) < self::HEAD_LEN) { + return 0; + } + + $data = unpack("Npack_len", $buffer); + return $data['pack_len']; + } + + /** + * 获取整个包的 buffer + * + * @param mixed $data + * @return string + */ + public static function encode($data) + { + $flag = (int)is_scalar($data['body']); + if (!$flag) { + $data['body'] = serialize($data['body']); + } + $data['flag'] |= $flag; + $ext_len = strlen($data['ext_data']??''); + $package_len = self::HEAD_LEN + $ext_len + strlen($data['body']); + return pack("NCNnNnNCnN", $package_len, + $data['cmd'], $data['local_ip'], + $data['local_port'], $data['client_ip'], + $data['client_port'], $data['connection_id'], + $data['flag'], $data['gateway_port'], + $ext_len) . $data['ext_data'] . $data['body']; + } + + /** + * 从二进制数据转换为数组 + * + * @param string $buffer + * @return array + */ + public static function decode($buffer) + { + $data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len", + $buffer); + if ($data['ext_len'] > 0) { + $data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']); + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']); + } else { + // 防止反序列化成类实例 + try { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len']), ['allowed_classes' => false]); + } catch (\Throwable $e) {} + } + } else { + $data['ext_data'] = ''; + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN); + } else { + // 防止反序列化成类实例 + try { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN), ['allowed_classes' => false]); + } catch (\Throwable $e) {} + } + } + return $data; + } +} diff --git a/vendor/workerman/gateway-worker/src/Register.php b/vendor/workerman/gateway-worker/src/Register.php new file mode 100644 index 0000000..65a8385 --- /dev/null +++ b/vendor/workerman/gateway-worker/src/Register.php @@ -0,0 +1,194 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use Workerman\Worker; +use Workerman\Timer; + +/** + * + * 注册中心,用于注册 Gateway 和 BusinessWorker + * + * @author walkor + * + */ +class Register extends Worker +{ + + /** + * 秘钥 + * @var string + */ + public $secretKey = ''; + + /** + * 所有 gateway 的连接 + * + * @var array + */ + protected $_gatewayConnections = array(); + + /** + * 所有 worker 的连接 + * + * @var array + */ + protected $_workerConnections = array(); + + /** + * 进程启动时间 + * + * @var int + */ + protected $_startTime = 0; + + + public function __construct(string $socketName = null, array $contextOption = []) + { + $this->name = 'Register'; + $this->reloadable = false; + parent::__construct($socketName, $contextOption); + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + // 设置 onMessage 连接回调 + $this->onConnect = array($this, 'onConnect'); + + // 设置 onMessage 回调 + $this->onMessage = array($this, 'onMessage'); + + // 设置 onClose 回调 + $this->onClose = array($this, 'onClose'); + + // 记录进程启动的时间 + $this->_startTime = time(); + + // 强制使用text协议 + $this->protocol = '\Workerman\Protocols\Text'; + + // reusePort + $this->reusePort = false; + + // 运行父方法 + parent::run(); + } + + /** + * 设置个定时器,将未及时发送验证的连接关闭 + * + * @param \Workerman\Connection\ConnectionInterface $connection + * @return void + */ + public function onConnect($connection) + { + $connection->timeout_timerid = Timer::add(10, function () use ($connection) { + Worker::log("Register auth timeout (".$connection->getRemoteIp()."). See http://doc2.workerman.net/register-auth-timeout.html"); + $connection->close(); + }, null, false); + } + + /** + * 设置消息回调 + * + * @param \Workerman\Connection\ConnectionInterface $connection + * @param string $buffer + * @return void + */ + public function onMessage($connection, $buffer) + { + // 删除定时器 + Timer::del($connection->timeout_timerid); + $data = @json_decode($buffer, true); + if (empty($data['event'])) { + $error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://doc2.workerman.net/register-auth-timeout.html"; + Worker::log($error); + return $connection->close($error); + } + $event = $data['event']; + $secret_key = isset($data['secret_key']) ? $data['secret_key'] : ''; + // 开始验证 + switch ($event) { + // 是 gateway 连接 + case 'gateway_connect': + if (empty($data['address'])) { + echo "address not found\n"; + return $connection->close(); + } + if ($secret_key !== $this->secretKey) { + Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true)); + return $connection->close(); + } + $this->_gatewayConnections[$connection->id] = $data['address']; + $this->broadcastAddresses(); + break; + // 是 worker 连接 + case 'worker_connect': + if ($secret_key !== $this->secretKey) { + Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true)); + return $connection->close(); + } + $this->_workerConnections[$connection->id] = $connection; + $this->broadcastAddresses($connection); + break; + case 'ping': + break; + default: + Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://doc2.workerman.net/register-auth-timeout.html"); + $connection->close(); + } + } + + /** + * 连接关闭时 + * + * @param \Workerman\Connection\ConnectionInterface $connection + */ + public function onClose($connection) + { + // 删除定时器 + Timer::del($connection->timeout_timerid); + if (isset($this->_gatewayConnections[$connection->id])) { + unset($this->_gatewayConnections[$connection->id]); + $this->broadcastAddresses(); + } + if (isset($this->_workerConnections[$connection->id])) { + unset($this->_workerConnections[$connection->id]); + } + } + + /** + * 向 BusinessWorker 广播 gateway 内部通讯地址 + * + * @param \Workerman\Connection\ConnectionInterface $connection + */ + public function broadcastAddresses($connection = null) + { + $data = array( + 'event' => 'broadcast_addresses', + 'addresses' => array_unique(array_values($this->_gatewayConnections)), + ); + $buffer = json_encode($data); + if ($connection) { + $connection->send($buffer); + return; + } + foreach ($this->_workerConnections as $con) { + $con->send($buffer); + } + } +} diff --git a/vendor/workerman/workerman/.github/FUNDING.yml b/vendor/workerman/workerman/.github/FUNDING.yml new file mode 100644 index 0000000..beae44f --- /dev/null +++ b/vendor/workerman/workerman/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +open_collective: workerman +patreon: walkor diff --git a/vendor/workerman/workerman/.gitignore b/vendor/workerman/workerman/.gitignore new file mode 100644 index 0000000..f3f9e18 --- /dev/null +++ b/vendor/workerman/workerman/.gitignore @@ -0,0 +1,6 @@ +logs +.buildpath +.project +.settings +.idea +.DS_Store diff --git a/vendor/workerman/workerman/Autoloader.php b/vendor/workerman/workerman/Autoloader.php new file mode 100644 index 0000000..7d760e9 --- /dev/null +++ b/vendor/workerman/workerman/Autoloader.php @@ -0,0 +1,69 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; + +/** + * Autoload. + */ +class Autoloader +{ + /** + * Autoload root path. + * + * @var string + */ + protected static $_autoloadRootPath = ''; + + /** + * Set autoload root path. + * + * @param string $root_path + * @return void + */ + public static function setRootPath($root_path) + { + self::$_autoloadRootPath = $root_path; + } + + /** + * Load files by namespace. + * + * @param string $name + * @return boolean + */ + public static function loadByNamespace($name) + { + $class_path = \str_replace('\\', \DIRECTORY_SEPARATOR, $name); + if (\strpos($name, 'Workerman\\') === 0) { + $class_file = __DIR__ . \substr($class_path, \strlen('Workerman')) . '.php'; + } else { + if (self::$_autoloadRootPath) { + $class_file = self::$_autoloadRootPath . \DIRECTORY_SEPARATOR . $class_path . '.php'; + } + if (empty($class_file) || !\is_file($class_file)) { + $class_file = __DIR__ . \DIRECTORY_SEPARATOR . '..' . \DIRECTORY_SEPARATOR . "$class_path.php"; + } + } + + if (\is_file($class_file)) { + require_once($class_file); + if (\class_exists($name, false)) { + return true; + } + } + return false; + } +} + +\spl_autoload_register('\Workerman\Autoloader::loadByNamespace'); \ No newline at end of file diff --git a/vendor/workerman/workerman/Connection/AsyncTcpConnection.php b/vendor/workerman/workerman/Connection/AsyncTcpConnection.php new file mode 100644 index 0000000..30b1638 --- /dev/null +++ b/vendor/workerman/workerman/Connection/AsyncTcpConnection.php @@ -0,0 +1,382 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use StdClass; +use Workerman\Events\EventInterface; +use Workerman\Lib\Timer; +use Workerman\Worker; +use Exception; + +/** + * AsyncTcpConnection. + */ +class AsyncTcpConnection extends TcpConnection +{ + /** + * Emitted when socket connection is successfully established. + * + * @var callable|null + */ + public $onConnect = null; + + /** + * Transport layer protocol. + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Status. + * + * @var int + */ + protected $_status = self::STATUS_INITIAL; + + /** + * Remote host. + * + * @var string + */ + protected $_remoteHost = ''; + + /** + * Remote port. + * + * @var int + */ + protected $_remotePort = 80; + + /** + * Connect start time. + * + * @var float + */ + protected $_connectStartTime = 0; + + /** + * Remote URI. + * + * @var string + */ + protected $_remoteURI = ''; + + /** + * Context option. + * + * @var array + */ + protected $_contextOption = null; + + /** + * Reconnect timer. + * + * @var int + */ + protected $_reconnectTimer = null; + + + /** + * PHP built-in protocols. + * + * @var array + */ + protected static $_builtinTransports = array( + 'tcp' => 'tcp', + 'udp' => 'udp', + 'unix' => 'unix', + 'ssl' => 'ssl', + 'sslv2' => 'sslv2', + 'sslv3' => 'sslv3', + 'tls' => 'tls' + ); + + /** + * Construct. + * + * @param string $remote_address + * @param array $context_option + * @throws Exception + */ + public function __construct($remote_address, array $context_option = array()) + { + $address_info = \parse_url($remote_address); + if (!$address_info) { + list($scheme, $this->_remoteAddress) = \explode(':', $remote_address, 2); + if('unix' === strtolower($scheme)) { + $this->_remoteAddress = substr($remote_address, strpos($remote_address, '/') + 2); + } + if (!$this->_remoteAddress) { + Worker::safeEcho(new \Exception('bad remote_address')); + } + } else { + if (!isset($address_info['port'])) { + $address_info['port'] = 0; + } + if (!isset($address_info['path'])) { + $address_info['path'] = '/'; + } + if (!isset($address_info['query'])) { + $address_info['query'] = ''; + } else { + $address_info['query'] = '?' . $address_info['query']; + } + $this->_remoteHost = $address_info['host']; + $this->_remotePort = $address_info['port']; + $this->_remoteURI = "{$address_info['path']}{$address_info['query']}"; + $scheme = isset($address_info['scheme']) ? $address_info['scheme'] : 'tcp'; + $this->_remoteAddress = 'unix' === strtolower($scheme) + ? substr($remote_address, strpos($remote_address, '/') + 2) + : $this->_remoteHost . ':' . $this->_remotePort; + } + + $this->id = $this->_id = self::$_idRecorder++; + if(\PHP_INT_MAX === self::$_idRecorder){ + self::$_idRecorder = 0; + } + // Check application layer protocol class. + if (!isset(self::$_builtinTransports[$scheme])) { + $scheme = \ucfirst($scheme); + $this->protocol = '\\Protocols\\' . $scheme; + if (!\class_exists($this->protocol)) { + $this->protocol = "\\Workerman\\Protocols\\$scheme"; + if (!\class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + } else { + $this->transport = self::$_builtinTransports[$scheme]; + } + + // For statistics. + ++self::$statistics['connection_count']; + $this->maxSendBufferSize = self::$defaultMaxSendBufferSize; + $this->maxPackageSize = self::$defaultMaxPackageSize; + $this->_contextOption = $context_option; + $this->context = new StdClass; + static::$connections[$this->_id] = $this; + } + + /** + * Do connect. + * + * @return void + */ + public function connect() + { + if ($this->_status !== self::STATUS_INITIAL && $this->_status !== self::STATUS_CLOSING && + $this->_status !== self::STATUS_CLOSED) { + return; + } + $this->_status = self::STATUS_CONNECTING; + $this->_connectStartTime = \microtime(true); + set_error_handler(function() { + return false; + }); + if ($this->transport !== 'unix') { + if (!$this->_remotePort) { + $this->_remotePort = $this->transport === 'ssl' ? 443 : 80; + $this->_remoteAddress = $this->_remoteHost.':'.$this->_remotePort; + } + // Open socket connection asynchronously. + if ($this->_contextOption) { + $context = \stream_context_create($this->_contextOption); + $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", + $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT, $context); + } else { + $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", + $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT); + } + } else { + $this->_socket = \stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0, + \STREAM_CLIENT_ASYNC_CONNECT); + } + restore_error_handler(); + // If failed attempt to emit onError callback. + if (!$this->_socket || !\is_resource($this->_socket)) { + $this->emitError(\WORKERMAN_CONNECT_FAIL, $errstr); + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + if ($this->_status === self::STATUS_CLOSED) { + $this->onConnect = null; + } + return; + } + // Add socket to global event loop waiting connection is successfully established or faild. + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'checkConnection')); + // For windows. + if(\DIRECTORY_SEPARATOR === '\\') { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_EXCEPT, array($this, 'checkConnection')); + } + } + + /** + * Reconnect. + * + * @param int $after + * @return void + */ + public function reconnect($after = 0) + { + $this->_status = self::STATUS_INITIAL; + static::$connections[$this->_id] = $this; + if ($this->_reconnectTimer) { + Timer::del($this->_reconnectTimer); + } + if ($after > 0) { + $this->_reconnectTimer = Timer::add($after, array($this, 'connect'), null, false); + return; + } + $this->connect(); + } + + /** + * CancelReconnect. + */ + public function cancelReconnect() + { + if ($this->_reconnectTimer) { + Timer::del($this->_reconnectTimer); + } + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteHost() + { + return $this->_remoteHost; + } + + /** + * Get remote URI. + * + * @return string + */ + public function getRemoteURI() + { + return $this->_remoteURI; + } + + /** + * Try to emit onError callback. + * + * @param int $code + * @param string $msg + * @return void + */ + protected function emitError($code, $msg) + { + $this->_status = self::STATUS_CLOSING; + if ($this->onError) { + try { + \call_user_func($this->onError, $this, $code, $msg); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } + + /** + * Check connection is successfully established or faild. + * + * @param resource $socket + * @return void + */ + public function checkConnection() + { + // Remove EV_EXPECT for windows. + if(\DIRECTORY_SEPARATOR === '\\') { + Worker::$globalEvent->del($this->_socket, EventInterface::EV_EXCEPT); + } + + // Remove write listener. + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + + if ($this->_status !== self::STATUS_CONNECTING) { + return; + } + + // Check socket state. + if ($address = \stream_socket_get_name($this->_socket, true)) { + // Nonblocking. + \stream_set_blocking($this->_socket, false); + // Compatible with hhvm + if (\function_exists('stream_set_read_buffer')) { + \stream_set_read_buffer($this->_socket, 0); + } + // Try to open keepalive for tcp and disable Nagle algorithm. + if (\function_exists('socket_import_stream') && $this->transport === 'tcp') { + $raw_socket = \socket_import_stream($this->_socket); + \socket_set_option($raw_socket, \SOL_SOCKET, \SO_KEEPALIVE, 1); + \socket_set_option($raw_socket, \SOL_TCP, \TCP_NODELAY, 1); + } + + // SSL handshake. + if ($this->transport === 'ssl') { + $this->_sslHandshakeCompleted = $this->doSslHandshake($this->_socket); + if ($this->_sslHandshakeCompleted === false) { + return; + } + } else { + // There are some data waiting to send. + if ($this->_sendBuffer) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + } + } + + // Register a listener waiting read event. + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + + $this->_status = self::STATUS_ESTABLISHED; + $this->_remoteAddress = $address; + + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + \call_user_func($this->onConnect, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + // Try to emit protocol::onConnect + if ($this->protocol && \method_exists($this->protocol, 'onConnect')) { + try { + \call_user_func(array($this->protocol, 'onConnect'), $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } else { + // Connection failed. + $this->emitError(\WORKERMAN_CONNECT_FAIL, 'connect ' . $this->_remoteAddress . ' fail after ' . round(\microtime(true) - $this->_connectStartTime, 4) . ' seconds'); + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + if ($this->_status === self::STATUS_CLOSED) { + $this->onConnect = null; + } + } + } +} diff --git a/vendor/workerman/workerman/Connection/AsyncUdpConnection.php b/vendor/workerman/workerman/Connection/AsyncUdpConnection.php new file mode 100644 index 0000000..745f060 --- /dev/null +++ b/vendor/workerman/workerman/Connection/AsyncUdpConnection.php @@ -0,0 +1,203 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use \Exception; + +/** + * AsyncUdpConnection. + */ +class AsyncUdpConnection extends UdpConnection +{ + /** + * Emitted when socket connection is successfully established. + * + * @var callable + */ + public $onConnect = null; + + /** + * Emitted when socket connection closed. + * + * @var callable + */ + public $onClose = null; + + /** + * Connected or not. + * + * @var bool + */ + protected $connected = false; + + /** + * Context option. + * + * @var array + */ + protected $_contextOption = null; + + /** + * Construct. + * + * @param string $remote_address + * @throws Exception + */ + public function __construct($remote_address, $context_option = null) + { + // Get the application layer communication protocol and listening address. + list($scheme, $address) = \explode(':', $remote_address, 2); + // Check application layer protocol class. + if ($scheme !== 'udp') { + $scheme = \ucfirst($scheme); + $this->protocol = '\\Protocols\\' . $scheme; + if (!\class_exists($this->protocol)) { + $this->protocol = "\\Workerman\\Protocols\\$scheme"; + if (!\class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + } + + $this->_remoteAddress = \substr($address, 2); + $this->_contextOption = $context_option; + } + + /** + * For udp package. + * + * @param resource $socket + * @return bool + */ + public function baseRead($socket) + { + $recv_buffer = \stream_socket_recvfrom($socket, Worker::MAX_UDP_PACKAGE_SIZE, 0, $remote_address); + if (false === $recv_buffer || empty($remote_address)) { + return false; + } + + if ($this->onMessage) { + if ($this->protocol) { + $parser = $this->protocol; + $recv_buffer = $parser::decode($recv_buffer, $this); + } + ++ConnectionInterface::$statistics['total_request']; + try { + \call_user_func($this->onMessage, $this, $recv_buffer); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + return true; + } + + /** + * Sends data on the connection. + * + * @param string $send_buffer + * @param bool $raw + * @return void|boolean + */ + public function send($send_buffer, $raw = false) + { + if (false === $raw && $this->protocol) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return; + } + } + if ($this->connected === false) { + $this->connect(); + } + return \strlen($send_buffer) === \stream_socket_sendto($this->_socket, $send_buffer, 0); + } + + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * + * @return bool + */ + public function close($data = null, $raw = false) + { + if ($data !== null) { + $this->send($data, $raw); + } + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + \fclose($this->_socket); + $this->connected = false; + // Try to emit onClose callback. + if ($this->onClose) { + try { + \call_user_func($this->onClose, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + $this->onConnect = $this->onMessage = $this->onClose = null; + return true; + } + + /** + * Connect. + * + * @return void + */ + public function connect() + { + if ($this->connected === true) { + return; + } + if ($this->_contextOption) { + $context = \stream_context_create($this->_contextOption); + $this->_socket = \stream_socket_client("udp://{$this->_remoteAddress}", $errno, $errmsg, + 30, \STREAM_CLIENT_CONNECT, $context); + } else { + $this->_socket = \stream_socket_client("udp://{$this->_remoteAddress}", $errno, $errmsg); + } + + if (!$this->_socket) { + Worker::safeEcho(new \Exception($errmsg)); + return; + } + + \stream_set_blocking($this->_socket, false); + + if ($this->onMessage) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + } + $this->connected = true; + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + \call_user_func($this->onConnect, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } + +} diff --git a/vendor/workerman/workerman/Connection/ConnectionInterface.php b/vendor/workerman/workerman/Connection/ConnectionInterface.php new file mode 100644 index 0000000..5d815d8 --- /dev/null +++ b/vendor/workerman/workerman/Connection/ConnectionInterface.php @@ -0,0 +1,126 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +/** + * ConnectionInterface. + */ +#[\AllowDynamicProperties] +abstract class ConnectionInterface +{ + /** + * Statistics for status command. + * + * @var array + */ + public static $statistics = array( + 'connection_count' => 0, + 'total_request' => 0, + 'throw_exception' => 0, + 'send_fail' => 0, + ); + + /** + * Emitted when data is received. + * + * @var callable + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callable + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callable + */ + public $onError = null; + + /** + * Sends data on the connection. + * + * @param mixed $send_buffer + * @return void|boolean + */ + abstract public function send($send_buffer); + + /** + * Get remote IP. + * + * @return string + */ + abstract public function getRemoteIp(); + + /** + * Get remote port. + * + * @return int + */ + abstract public function getRemotePort(); + + /** + * Get remote address. + * + * @return string + */ + abstract public function getRemoteAddress(); + + /** + * Get local IP. + * + * @return string + */ + abstract public function getLocalIp(); + + /** + * Get local port. + * + * @return int + */ + abstract public function getLocalPort(); + + /** + * Get local address. + * + * @return string + */ + abstract public function getLocalAddress(); + + /** + * Is ipv4. + * + * @return bool + */ + abstract public function isIPv4(); + + /** + * Is ipv6. + * + * @return bool + */ + abstract public function isIPv6(); + + /** + * Close connection. + * + * @param string|null $data + * @return void + */ + abstract public function close($data = null); +} diff --git a/vendor/workerman/workerman/Connection/TcpConnection.php b/vendor/workerman/workerman/Connection/TcpConnection.php new file mode 100644 index 0000000..a4995f9 --- /dev/null +++ b/vendor/workerman/workerman/Connection/TcpConnection.php @@ -0,0 +1,1004 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use \Exception; + +/** + * TcpConnection. + */ +class TcpConnection extends ConnectionInterface +{ + /** + * Read buffer size. + * + * @var int + */ + const READ_BUFFER_SIZE = 65535; + + /** + * Status initial. + * + * @var int + */ + const STATUS_INITIAL = 0; + + /** + * Status connecting. + * + * @var int + */ + const STATUS_CONNECTING = 1; + + /** + * Status connection established. + * + * @var int + */ + const STATUS_ESTABLISHED = 2; + + /** + * Status closing. + * + * @var int + */ + const STATUS_CLOSING = 4; + + /** + * Status closed. + * + * @var int + */ + const STATUS_CLOSED = 8; + + /** + * Emitted when data is received. + * + * @var callable + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callable + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callable + */ + public $onError = null; + + /** + * Emitted when the send buffer becomes full. + * + * @var callable + */ + public $onBufferFull = null; + + /** + * Emitted when the send buffer becomes empty. + * + * @var callable + */ + public $onBufferDrain = null; + + /** + * Application layer protocol. + * The format is like this Workerman\\Protocols\\Http. + * + * @var \Workerman\Protocols\ProtocolInterface + */ + public $protocol = null; + + /** + * Transport (tcp/udp/unix/ssl). + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Which worker belong to. + * + * @var Worker + */ + public $worker = null; + + /** + * Bytes read. + * + * @var int + */ + public $bytesRead = 0; + + /** + * Bytes written. + * + * @var int + */ + public $bytesWritten = 0; + + /** + * Connection->id. + * + * @var int + */ + public $id = 0; + + /** + * A copy of $worker->id which used to clean up the connection in worker->connections + * + * @var int + */ + protected $_id = 0; + + /** + * Sets the maximum send buffer size for the current connection. + * OnBufferFull callback will be emited When the send buffer is full. + * + * @var int + */ + public $maxSendBufferSize = 1048576; + + /** + * Context. + * + * @var object|null + */ + public $context = null; + + /** + * Default send buffer size. + * + * @var int + */ + public static $defaultMaxSendBufferSize = 1048576; + + /** + * Sets the maximum acceptable packet size for the current connection. + * + * @var int + */ + public $maxPackageSize = 1048576; + + /** + * Default maximum acceptable packet size. + * + * @var int + */ + public static $defaultMaxPackageSize = 10485760; + + /** + * Id recorder. + * + * @var int + */ + protected static $_idRecorder = 1; + + /** + * Socket + * + * @var resource + */ + protected $_socket = null; + + /** + * Send buffer. + * + * @var string + */ + protected $_sendBuffer = ''; + + /** + * Receive buffer. + * + * @var string + */ + protected $_recvBuffer = ''; + + /** + * Current package length. + * + * @var int + */ + protected $_currentPackageLength = 0; + + /** + * Connection status. + * + * @var int + */ + protected $_status = self::STATUS_ESTABLISHED; + + /** + * Remote address. + * + * @var string + */ + protected $_remoteAddress = ''; + + /** + * Is paused. + * + * @var bool + */ + protected $_isPaused = false; + + /** + * SSL handshake completed or not. + * + * @var bool + */ + protected $_sslHandshakeCompleted = false; + + /** + * All connection instances. + * + * @var array + */ + public static $connections = array(); + + /** + * Is safe. + * + * @var bool + */ + protected $_isSafe = true; + + /** + * Status to string. + * + * @var array + */ + public static $_statusToString = array( + self::STATUS_INITIAL => 'INITIAL', + self::STATUS_CONNECTING => 'CONNECTING', + self::STATUS_ESTABLISHED => 'ESTABLISHED', + self::STATUS_CLOSING => 'CLOSING', + self::STATUS_CLOSED => 'CLOSED', + ); + + /** + * Construct. + * + * @param resource $socket + * @param string $remote_address + */ + public function __construct($socket, $remote_address = '') + { + ++self::$statistics['connection_count']; + $this->id = $this->_id = self::$_idRecorder++; + if(self::$_idRecorder === \PHP_INT_MAX){ + self::$_idRecorder = 0; + } + $this->_socket = $socket; + \stream_set_blocking($this->_socket, 0); + // Compatible with hhvm + if (\function_exists('stream_set_read_buffer')) { + \stream_set_read_buffer($this->_socket, 0); + } + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + $this->maxSendBufferSize = self::$defaultMaxSendBufferSize; + $this->maxPackageSize = self::$defaultMaxPackageSize; + $this->_remoteAddress = $remote_address; + static::$connections[$this->id] = $this; + $this->context = new \stdClass; + } + + /** + * Get status. + * + * @param bool $raw_output + * + * @return int|string + */ + public function getStatus($raw_output = true) + { + if ($raw_output) { + return $this->_status; + } + return self::$_statusToString[$this->_status]; + } + + /** + * Sends data on the connection. + * + * @param mixed $send_buffer + * @param bool $raw + * @return bool|null + */ + public function send($send_buffer, $raw = false) + { + if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { + return false; + } + + // Try to call protocol::encode($send_buffer) before sending. + if (false === $raw && $this->protocol !== null) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return; + } + } + + if ($this->_status !== self::STATUS_ESTABLISHED || + ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) + ) { + if ($this->_sendBuffer && $this->bufferIsFull()) { + ++self::$statistics['send_fail']; + return false; + } + $this->_sendBuffer .= $send_buffer; + $this->checkBufferWillFull(); + return; + } + + // Attempt to send data directly. + if ($this->_sendBuffer === '') { + if ($this->transport === 'ssl') { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + $this->_sendBuffer = $send_buffer; + $this->checkBufferWillFull(); + return; + } + $len = 0; + try { + $len = @\fwrite($this->_socket, $send_buffer); + } catch (\Exception $e) { + Worker::log($e); + } catch (\Error $e) { + Worker::log($e); + } + // send successful. + if ($len === \strlen($send_buffer)) { + $this->bytesWritten += $len; + return true; + } + // Send only part of the data. + if ($len > 0) { + $this->_sendBuffer = \substr($send_buffer, $len); + $this->bytesWritten += $len; + } else { + // Connection closed? + if (!\is_resource($this->_socket) || \feof($this->_socket)) { + ++self::$statistics['send_fail']; + if ($this->onError) { + try { + \call_user_func($this->onError, $this, \WORKERMAN_SEND_FAIL, 'client closed'); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + $this->destroy(); + return false; + } + $this->_sendBuffer = $send_buffer; + } + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + // Check if the send buffer will be full. + $this->checkBufferWillFull(); + return; + } + + if ($this->bufferIsFull()) { + ++self::$statistics['send_fail']; + return false; + } + + $this->_sendBuffer .= $send_buffer; + // Check if the send buffer is full. + $this->checkBufferWillFull(); + } + + /** + * Get remote IP. + * + * @return string + */ + public function getRemoteIp() + { + $pos = \strrpos($this->_remoteAddress, ':'); + if ($pos) { + return (string) \substr($this->_remoteAddress, 0, $pos); + } + return ''; + } + + /** + * Get remote port. + * + * @return int + */ + public function getRemotePort() + { + if ($this->_remoteAddress) { + return (int) \substr(\strrchr($this->_remoteAddress, ':'), 1); + } + return 0; + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteAddress() + { + return $this->_remoteAddress; + } + + /** + * Get local IP. + * + * @return string + */ + public function getLocalIp() + { + $address = $this->getLocalAddress(); + $pos = \strrpos($address, ':'); + if (!$pos) { + return ''; + } + return \substr($address, 0, $pos); + } + + /** + * Get local port. + * + * @return int + */ + public function getLocalPort() + { + $address = $this->getLocalAddress(); + $pos = \strrpos($address, ':'); + if (!$pos) { + return 0; + } + return (int)\substr(\strrchr($address, ':'), 1); + } + + /** + * Get local address. + * + * @return string + */ + public function getLocalAddress() + { + if (!\is_resource($this->_socket)) { + return ''; + } + return (string)@\stream_socket_get_name($this->_socket, false); + } + + /** + * Get send buffer queue size. + * + * @return integer + */ + public function getSendBufferQueueSize() + { + return \strlen($this->_sendBuffer); + } + + /** + * Get recv buffer queue size. + * + * @return integer + */ + public function getRecvBufferQueueSize() + { + return \strlen($this->_recvBuffer); + } + + /** + * Is ipv4. + * + * return bool. + */ + public function isIpV4() + { + if ($this->transport === 'unix') { + return false; + } + return \strpos($this->getRemoteIp(), ':') === false; + } + + /** + * Is ipv6. + * + * return bool. + */ + public function isIpV6() + { + if ($this->transport === 'unix') { + return false; + } + return \strpos($this->getRemoteIp(), ':') !== false; + } + + /** + * Pauses the reading of data. That is onMessage will not be emitted. Useful to throttle back an upload. + * + * @return void + */ + public function pauseRecv() + { + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + $this->_isPaused = true; + } + + /** + * Resumes reading after a call to pauseRecv. + * + * @return void + */ + public function resumeRecv() + { + if ($this->_isPaused === true) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + $this->_isPaused = false; + $this->baseRead($this->_socket, false); + } + } + + + + /** + * Base read handler. + * + * @param resource $socket + * @param bool $check_eof + * @return void + */ + public function baseRead($socket, $check_eof = true) + { + // SSL handshake. + if ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) { + if ($this->doSslHandshake($socket)) { + $this->_sslHandshakeCompleted = true; + if ($this->_sendBuffer) { + Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + } + } else { + return; + } + } + + $buffer = ''; + try { + $buffer = @\fread($socket, self::READ_BUFFER_SIZE); + } catch (\Exception $e) {} catch (\Error $e) {} + + // Check connection closed. + if ($buffer === '' || $buffer === false) { + if ($check_eof && (\feof($socket) || !\is_resource($socket) || $buffer === false)) { + $this->destroy(); + return; + } + } else { + $this->bytesRead += \strlen($buffer); + $this->_recvBuffer .= $buffer; + } + + // If the application layer protocol has been set up. + if ($this->protocol !== null) { + $parser = $this->protocol; + while ($this->_recvBuffer !== '' && !$this->_isPaused) { + // The current packet length is known. + if ($this->_currentPackageLength) { + // Data is not enough for a package. + if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { + break; + } + } else { + // Get current package length. + try { + $this->_currentPackageLength = $parser::input($this->_recvBuffer, $this); + } catch (\Exception $e) {} catch (\Error $e) {} + // The packet length is unknown. + if ($this->_currentPackageLength === 0) { + break; + } elseif ($this->_currentPackageLength > 0 && $this->_currentPackageLength <= $this->maxPackageSize) { + // Data is not enough for a package. + if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { + break; + } + } // Wrong package. + else { + Worker::safeEcho('Error package. package_length=' . \var_export($this->_currentPackageLength, true)); + $this->destroy(); + return; + } + } + + // The data is enough for a packet. + ++self::$statistics['total_request']; + // The current packet length is equal to the length of the buffer. + if (\strlen($this->_recvBuffer) === $this->_currentPackageLength) { + $one_request_buffer = $this->_recvBuffer; + $this->_recvBuffer = ''; + } else { + // Get a full package from the buffer. + $one_request_buffer = \substr($this->_recvBuffer, 0, $this->_currentPackageLength); + // Remove the current package from the receive buffer. + $this->_recvBuffer = \substr($this->_recvBuffer, $this->_currentPackageLength); + } + // Reset the current packet length to 0. + $this->_currentPackageLength = 0; + if (!$this->onMessage) { + continue; + } + try { + // Decode request buffer before Emitting onMessage callback. + \call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this)); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + return; + } + + if ($this->_recvBuffer === '' || $this->_isPaused) { + return; + } + + // Applications protocol is not set. + ++self::$statistics['total_request']; + if (!$this->onMessage) { + $this->_recvBuffer = ''; + return; + } + try { + \call_user_func($this->onMessage, $this, $this->_recvBuffer); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + // Clean receive buffer. + $this->_recvBuffer = ''; + } + + /** + * Base write handler. + * + * @return void|bool + */ + public function baseWrite() + { + \set_error_handler(function(){}); + if ($this->transport === 'ssl') { + $len = @\fwrite($this->_socket, $this->_sendBuffer, 8192); + } else { + $len = @\fwrite($this->_socket, $this->_sendBuffer); + } + \restore_error_handler(); + if ($len === \strlen($this->_sendBuffer)) { + $this->bytesWritten += $len; + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + $this->_sendBuffer = ''; + // Try to emit onBufferDrain callback when the send buffer becomes empty. + if ($this->onBufferDrain) { + try { + \call_user_func($this->onBufferDrain, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + return true; + } + if ($len > 0) { + $this->bytesWritten += $len; + $this->_sendBuffer = \substr($this->_sendBuffer, $len); + } else { + ++self::$statistics['send_fail']; + $this->destroy(); + } + } + + /** + * SSL handshake. + * + * @param resource $socket + * @return bool + */ + public function doSslHandshake($socket){ + if (\feof($socket)) { + $this->destroy(); + return false; + } + $async = $this instanceof AsyncTcpConnection; + + /** + * We disabled ssl3 because https://blog.qualys.com/ssllabs/2014/10/15/ssl-3-is-dead-killed-by-the-poodle-attack. + * You can enable ssl3 by the codes below. + */ + /*if($async){ + $type = STREAM_CRYPTO_METHOD_SSLv2_CLIENT | STREAM_CRYPTO_METHOD_SSLv23_CLIENT | STREAM_CRYPTO_METHOD_SSLv3_CLIENT; + }else{ + $type = STREAM_CRYPTO_METHOD_SSLv2_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER | STREAM_CRYPTO_METHOD_SSLv3_SERVER; + }*/ + + if($async){ + $type = \STREAM_CRYPTO_METHOD_SSLv2_CLIENT | \STREAM_CRYPTO_METHOD_SSLv23_CLIENT; + }else{ + $type = \STREAM_CRYPTO_METHOD_SSLv2_SERVER | \STREAM_CRYPTO_METHOD_SSLv23_SERVER; + } + + // Hidden error. + \set_error_handler(function($errno, $errstr, $file){ + if (!Worker::$daemonize) { + Worker::safeEcho("SSL handshake error: $errstr \n"); + } + }); + $ret = \stream_socket_enable_crypto($socket, true, $type); + \restore_error_handler(); + // Negotiation has failed. + if (false === $ret) { + $this->destroy(); + return false; + } elseif (0 === $ret) { + // There isn't enough data and should try again. + return 0; + } + if (isset($this->onSslHandshake)) { + try { + \call_user_func($this->onSslHandshake, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + return true; + } + + /** + * This method pulls all the data out of a readable stream, and writes it to the supplied destination. + * + * @param self $dest + * @param bool $raw + * @return void + */ + public function pipe(self $dest, $raw = false) + { + $source = $this; + $this->onMessage = function ($source, $data) use ($dest, $raw) { + $dest->send($data, $raw); + }; + $this->onClose = function ($source) use ($dest) { + $dest->close(); + }; + $dest->onBufferFull = function ($dest) use ($source) { + $source->pauseRecv(); + }; + $dest->onBufferDrain = function ($dest) use ($source) { + $source->resumeRecv(); + }; + } + + /** + * Remove $length of data from receive buffer. + * + * @param int $length + * @return void + */ + public function consumeRecvBuffer($length) + { + $this->_recvBuffer = \substr($this->_recvBuffer, $length); + } + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * @return void + */ + public function close($data = null, $raw = false) + { + if($this->_status === self::STATUS_CONNECTING){ + $this->destroy(); + return; + } + + if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { + return; + } + + if ($data !== null) { + $this->send($data, $raw); + } + + $this->_status = self::STATUS_CLOSING; + + if ($this->_sendBuffer === '') { + $this->destroy(); + } else { + $this->pauseRecv(); + } + } + + /** + * Get the real socket. + * + * @return resource + */ + public function getSocket() + { + return $this->_socket; + } + + /** + * Check whether the send buffer will be full. + * + * @return void + */ + protected function checkBufferWillFull() + { + if ($this->maxSendBufferSize <= \strlen($this->_sendBuffer)) { + if ($this->onBufferFull) { + try { + \call_user_func($this->onBufferFull, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } + } + + /** + * Whether send buffer is full. + * + * @return bool + */ + protected function bufferIsFull() + { + // Buffer has been marked as full but still has data to send then the packet is discarded. + if ($this->maxSendBufferSize <= \strlen($this->_sendBuffer)) { + if ($this->onError) { + try { + \call_user_func($this->onError, $this, \WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + return true; + } + return false; + } + + /** + * Whether send buffer is Empty. + * + * @return bool + */ + public function bufferIsEmpty() + { + return empty($this->_sendBuffer); + } + + /** + * Destroy connection. + * + * @return void + */ + public function destroy() + { + // Avoid repeated calls. + if ($this->_status === self::STATUS_CLOSED) { + return; + } + // Remove event listener. + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + + // Close socket. + try { + @\fclose($this->_socket); + } catch (\Exception $e) {} catch (\Error $e) {} + + $this->_status = self::STATUS_CLOSED; + // Try to emit onClose callback. + if ($this->onClose) { + try { + \call_user_func($this->onClose, $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + // Try to emit protocol::onClose + if ($this->protocol && \method_exists($this->protocol, 'onClose')) { + try { + \call_user_func(array($this->protocol, 'onClose'), $this); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + $this->_sendBuffer = $this->_recvBuffer = ''; + $this->_currentPackageLength = 0; + $this->_isPaused = $this->_sslHandshakeCompleted = false; + if ($this->_status === self::STATUS_CLOSED) { + // Cleaning up the callback to avoid memory leaks. + $this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = null; + // Remove from worker->connections. + if ($this->worker) { + unset($this->worker->connections[$this->_id]); + } + unset(static::$connections[$this->_id]); + } + } + + + /** + * __wakeup. + * + * @return void + */ + public function __wakeup() + { + $this->_isSafe = false; + } + + /** + * Destruct. + * + * @return void + */ + public function __destruct() + { + static $mod; + if (!$this->_isSafe) { + return; + } + self::$statistics['connection_count']--; + if (Worker::getGracefulStop()) { + if (!isset($mod)) { + $mod = \ceil((self::$statistics['connection_count'] + 1) / 3); + } + + if (0 === self::$statistics['connection_count'] % $mod) { + Worker::log('worker[' . \posix_getpid() . '] remains ' . self::$statistics['connection_count'] . ' connection(s)'); + } + + if(0 === self::$statistics['connection_count']) { + Worker::stopAll(); + } + } + } +} diff --git a/vendor/workerman/workerman/Connection/UdpConnection.php b/vendor/workerman/workerman/Connection/UdpConnection.php new file mode 100644 index 0000000..9cd95ba --- /dev/null +++ b/vendor/workerman/workerman/Connection/UdpConnection.php @@ -0,0 +1,208 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +/** + * UdpConnection. + */ +class UdpConnection extends ConnectionInterface +{ + /** + * Application layer protocol. + * The format is like this Workerman\\Protocols\\Http. + * + * @var \Workerman\Protocols\ProtocolInterface + */ + public $protocol = null; + + /** + * Transport layer protocol. + * + * @var string + */ + public $transport = 'udp'; + + /** + * Udp socket. + * + * @var resource + */ + protected $_socket = null; + + /** + * Remote address. + * + * @var string + */ + protected $_remoteAddress = ''; + + /** + * Construct. + * + * @param resource $socket + * @param string $remote_address + */ + public function __construct($socket, $remote_address) + { + $this->_socket = $socket; + $this->_remoteAddress = $remote_address; + } + + /** + * Sends data on the connection. + * + * @param string $send_buffer + * @param bool $raw + * @return void|boolean + */ + public function send($send_buffer, $raw = false) + { + if (false === $raw && $this->protocol) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return; + } + } + return \strlen($send_buffer) === \stream_socket_sendto($this->_socket, $send_buffer, 0, $this->isIpV6() ? '[' . $this->getRemoteIp() . ']:' . $this->getRemotePort() : $this->_remoteAddress); + } + + /** + * Get remote IP. + * + * @return string + */ + public function getRemoteIp() + { + $pos = \strrpos($this->_remoteAddress, ':'); + if ($pos) { + return \trim(\substr($this->_remoteAddress, 0, $pos), '[]'); + } + return ''; + } + + /** + * Get remote port. + * + * @return int + */ + public function getRemotePort() + { + if ($this->_remoteAddress) { + return (int)\substr(\strrchr($this->_remoteAddress, ':'), 1); + } + return 0; + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteAddress() + { + return $this->_remoteAddress; + } + + /** + * Get local IP. + * + * @return string + */ + public function getLocalIp() + { + $address = $this->getLocalAddress(); + $pos = \strrpos($address, ':'); + if (!$pos) { + return ''; + } + return \substr($address, 0, $pos); + } + + /** + * Get local port. + * + * @return int + */ + public function getLocalPort() + { + $address = $this->getLocalAddress(); + $pos = \strrpos($address, ':'); + if (!$pos) { + return 0; + } + return (int)\substr(\strrchr($address, ':'), 1); + } + + /** + * Get local address. + * + * @return string + */ + public function getLocalAddress() + { + return (string)@\stream_socket_get_name($this->_socket, false); + } + + /** + * Is ipv4. + * + * @return bool. + */ + public function isIpV4() + { + if ($this->transport === 'unix') { + return false; + } + return \strpos($this->getRemoteIp(), ':') === false; + } + + /** + * Is ipv6. + * + * @return bool. + */ + public function isIpV6() + { + if ($this->transport === 'unix') { + return false; + } + return \strpos($this->getRemoteIp(), ':') !== false; + } + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * @return bool + */ + public function close($data = null, $raw = false) + { + if ($data !== null) { + $this->send($data, $raw); + } + return true; + } + + /** + * Get the real socket. + * + * @return resource + */ + public function getSocket() + { + return $this->_socket; + } +} diff --git a/vendor/workerman/workerman/Events/Ev.php b/vendor/workerman/workerman/Events/Ev.php new file mode 100644 index 0000000..8e21bc3 --- /dev/null +++ b/vendor/workerman/workerman/Events/Ev.php @@ -0,0 +1,189 @@ + + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; +use \EvWatcher; + +/** + * ev eventloop + */ +class Ev implements EventInterface +{ + /** + * All listeners for read/write event. + * + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * + * @var int + */ + protected static $_timerId = 1; + + /** + * Add a timer. + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = null) + { + $callback = function ($event, $socket) use ($fd, $func) { + try { + \call_user_func($func, $fd); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + }; + switch ($flag) { + case self::EV_SIGNAL: + $event = new \EvSignal($fd, $callback); + $this->_eventSignal[$fd] = $event; + return true; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $repeat = $flag === self::EV_TIMER_ONCE ? 0 : $fd; + $param = array($func, (array)$args, $flag, $fd, self::$_timerId); + $event = new \EvTimer($fd, $repeat, array($this, 'timerCallback'), $param); + $this->_eventTimer[self::$_timerId] = $event; + return self::$_timerId++; + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? \Ev::READ : \Ev::WRITE; + $event = new \EvIo($fd, $real_flag, $callback); + $this->_allEvents[$fd_key][$flag] = $event; + return true; + } + + } + + /** + * Remove a timer. + * {@inheritdoc} + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + $this->_allEvents[$fd_key][$flag]->stop(); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + $this->_eventSignal[$fd_key]->stop(); + unset($this->_eventSignal[$fd_key]); + } + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + if (isset($this->_eventTimer[$fd])) { + $this->_eventTimer[$fd]->stop(); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * + * @param EvWatcher $event + */ + public function timerCallback(EvWatcher $event) + { + $param = $event->data; + $timer_id = $param[4]; + if ($param[2] === self::EV_TIMER_ONCE) { + $this->_eventTimer[$timer_id]->stop(); + unset($this->_eventTimer[$timer_id]); + } + try { + \call_user_func_array($param[0], $param[1]); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + + /** + * Remove all timers. + * + * @return void + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $event) { + $event->stop(); + } + $this->_eventTimer = array(); + } + + /** + * Main loop. + * + * @see EventInterface::loop() + */ + public function loop() + { + \Ev::run(); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + \Ev::stop(\Ev::BREAK_ALL); + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_eventTimer); + } +} diff --git a/vendor/workerman/workerman/Events/Event.php b/vendor/workerman/workerman/Events/Event.php new file mode 100644 index 0000000..9e25521 --- /dev/null +++ b/vendor/workerman/workerman/Events/Event.php @@ -0,0 +1,215 @@ + + * @copyright 有个鬼<42765633@qq.com> + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * libevent eventloop + */ +class Event implements EventInterface +{ + /** + * Event base. + * @var object + */ + protected $_eventBase = null; + + /** + * All listeners for read/write event. + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * @var int + */ + protected static $_timerId = 1; + + /** + * construct + * @return void + */ + public function __construct() + { + if (\class_exists('\\\\EventBase', false)) { + $class_name = '\\\\EventBase'; + } else { + $class_name = '\EventBase'; + } + $this->_eventBase = new $class_name(); + } + + /** + * @see EventInterface::add() + */ + public function add($fd, $flag, $func, $args=array()) + { + if (\class_exists('\\\\Event', false)) { + $class_name = '\\\\Event'; + } else { + $class_name = '\Event'; + } + switch ($flag) { + case self::EV_SIGNAL: + + $fd_key = (int)$fd; + $event = $class_name::signal($this->_eventBase, $fd, $func); + if (!$event||!$event->add()) { + return false; + } + $this->_eventSignal[$fd_key] = $event; + return true; + + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + + $param = array($func, (array)$args, $flag, $fd, self::$_timerId); + $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT|$class_name::PERSIST, array($this, "timerCallback"), $param); + if (!$event||!$event->addTimer($fd)) { + return false; + } + $this->_eventTimer[self::$_timerId] = $event; + return self::$_timerId++; + + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? $class_name::READ | $class_name::PERSIST : $class_name::WRITE | $class_name::PERSIST; + $event = new $class_name($this->_eventBase, $fd, $real_flag, $func, $fd); + if (!$event||!$event->add()) { + return false; + } + $this->_allEvents[$fd_key][$flag] = $event; + return true; + } + } + + /** + * @see Events\EventInterface::del() + */ + public function del($fd, $flag) + { + switch ($flag) { + + case self::EV_READ: + case self::EV_WRITE: + + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + $this->_allEvents[$fd_key][$flag]->del(); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + $this->_eventSignal[$fd_key]->del(); + unset($this->_eventSignal[$fd_key]); + } + break; + + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + if (isset($this->_eventTimer[$fd])) { + $this->_eventTimer[$fd]->del(); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * @param int|null $fd + * @param int $what + * @param int $timer_id + */ + public function timerCallback($fd, $what, $param) + { + $timer_id = $param[4]; + + if ($param[2] === self::EV_TIMER_ONCE) { + $this->_eventTimer[$timer_id]->del(); + unset($this->_eventTimer[$timer_id]); + } + + try { + \call_user_func_array($param[0], $param[1]); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + + /** + * @see Events\EventInterface::clearAllTimer() + * @return void + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $event) { + $event->del(); + } + $this->_eventTimer = array(); + } + + + /** + * @see EventInterface::loop() + */ + public function loop() + { + $this->_eventBase->loop(); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + $this->_eventBase->exit(); + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_eventTimer); + } +} diff --git a/vendor/workerman/workerman/Events/EventInterface.php b/vendor/workerman/workerman/Events/EventInterface.php new file mode 100644 index 0000000..e6f59c6 --- /dev/null +++ b/vendor/workerman/workerman/Events/EventInterface.php @@ -0,0 +1,107 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +interface EventInterface +{ + /** + * Read event. + * + * @var int + */ + const EV_READ = 1; + + /** + * Write event. + * + * @var int + */ + const EV_WRITE = 2; + + /** + * Except event + * + * @var int + */ + const EV_EXCEPT = 3; + + /** + * Signal event. + * + * @var int + */ + const EV_SIGNAL = 4; + + /** + * Timer event. + * + * @var int + */ + const EV_TIMER = 8; + + /** + * Timer once event. + * + * @var int + */ + const EV_TIMER_ONCE = 16; + + /** + * Add event listener to event loop. + * + * @param mixed $fd + * @param int $flag + * @param callable $func + * @param array $args + * @return bool + */ + public function add($fd, $flag, $func, $args = array()); + + /** + * Remove event listener from event loop. + * + * @param mixed $fd + * @param int $flag + * @return bool + */ + public function del($fd, $flag); + + /** + * Remove all timers. + * + * @return void + */ + public function clearAllTimer(); + + /** + * Main loop. + * + * @return void + */ + public function loop(); + + /** + * Destroy loop. + * + * @return mixed + */ + public function destroy(); + + /** + * Get Timer count. + * + * @return mixed + */ + public function getTimerCount(); +} diff --git a/vendor/workerman/workerman/Events/Libevent.php b/vendor/workerman/workerman/Events/Libevent.php new file mode 100644 index 0000000..5f61e9c --- /dev/null +++ b/vendor/workerman/workerman/Events/Libevent.php @@ -0,0 +1,225 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * libevent eventloop + */ +class Libevent implements EventInterface +{ + /** + * Event base. + * + * @var resource + */ + protected $_eventBase = null; + + /** + * All listeners for read/write event. + * + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * construct + */ + public function __construct() + { + $this->_eventBase = \event_base_new(); + } + + /** + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = array()) + { + switch ($flag) { + case self::EV_SIGNAL: + $fd_key = (int)$fd; + $real_flag = \EV_SIGNAL | \EV_PERSIST; + $this->_eventSignal[$fd_key] = \event_new(); + if (!\event_set($this->_eventSignal[$fd_key], $fd, $real_flag, $func, null)) { + return false; + } + if (!\event_base_set($this->_eventSignal[$fd_key], $this->_eventBase)) { + return false; + } + if (!\event_add($this->_eventSignal[$fd_key])) { + return false; + } + return true; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $event = \event_new(); + $timer_id = (int)$event; + if (!\event_set($event, 0, \EV_TIMEOUT, array($this, 'timerCallback'), $timer_id)) { + return false; + } + + if (!\event_base_set($event, $this->_eventBase)) { + return false; + } + + $time_interval = $fd * 1000000; + if (!\event_add($event, $time_interval)) { + return false; + } + $this->_eventTimer[$timer_id] = array($func, (array)$args, $event, $flag, $time_interval); + return $timer_id; + + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? \EV_READ | \EV_PERSIST : \EV_WRITE | \EV_PERSIST; + + $event = \event_new(); + + if (!\event_set($event, $fd, $real_flag, $func, null)) { + return false; + } + + if (!\event_base_set($event, $this->_eventBase)) { + return false; + } + + if (!\event_add($event)) { + return false; + } + + $this->_allEvents[$fd_key][$flag] = $event; + + return true; + } + + } + + /** + * {@inheritdoc} + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + \event_del($this->_allEvents[$fd_key][$flag]); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + \event_del($this->_eventSignal[$fd_key]); + unset($this->_eventSignal[$fd_key]); + } + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + // 这里 fd 为timerid + if (isset($this->_eventTimer[$fd])) { + \event_del($this->_eventTimer[$fd][2]); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * + * @param mixed $_null1 + * @param int $_null2 + * @param mixed $timer_id + */ + protected function timerCallback($_null1, $_null2, $timer_id) + { + if ($this->_eventTimer[$timer_id][3] === self::EV_TIMER) { + \event_add($this->_eventTimer[$timer_id][2], $this->_eventTimer[$timer_id][4]); + } + try { + \call_user_func_array($this->_eventTimer[$timer_id][0], $this->_eventTimer[$timer_id][1]); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + if (isset($this->_eventTimer[$timer_id]) && $this->_eventTimer[$timer_id][3] === self::EV_TIMER_ONCE) { + $this->del($timer_id, self::EV_TIMER_ONCE); + } + } + + /** + * {@inheritdoc} + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $task_data) { + \event_del($task_data[2]); + } + $this->_eventTimer = array(); + } + + /** + * {@inheritdoc} + */ + public function loop() + { + \event_base_loop($this->_eventBase); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + foreach ($this->_eventSignal as $event) { + \event_del($event); + } + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_eventTimer); + } +} + diff --git a/vendor/workerman/workerman/Events/React/Base.php b/vendor/workerman/workerman/Events/React/Base.php new file mode 100644 index 0000000..bce4f73 --- /dev/null +++ b/vendor/workerman/workerman/Events/React/Base.php @@ -0,0 +1,264 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; + +use Workerman\Events\EventInterface; +use React\EventLoop\TimerInterface; +use React\EventLoop\LoopInterface; + +/** + * Class StreamSelectLoop + * @package Workerman\Events\React + */ +class Base implements LoopInterface +{ + /** + * @var array + */ + protected $_timerIdMap = array(); + + /** + * @var int + */ + protected $_timerIdIndex = 0; + + /** + * @var array + */ + protected $_signalHandlerMap = array(); + + /** + * @var LoopInterface + */ + protected $_eventLoop = null; + + /** + * Base constructor. + */ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\StreamSelectLoop(); + } + + /** + * Add event listener to event loop. + * + * @param int $fd + * @param int $flag + * @param callable $func + * @param array $args + * @return bool + */ + public function add($fd, $flag, $func, array $args = array()) + { + $args = (array)$args; + switch ($flag) { + case EventInterface::EV_READ: + return $this->addReadStream($fd, $func); + case EventInterface::EV_WRITE: + return $this->addWriteStream($fd, $func); + case EventInterface::EV_SIGNAL: + if (isset($this->_signalHandlerMap[$fd])) { + $this->removeSignal($fd, $this->_signalHandlerMap[$fd]); + } + $this->_signalHandlerMap[$fd] = $func; + return $this->addSignal($fd, $func); + case EventInterface::EV_TIMER: + $timer_obj = $this->addPeriodicTimer($fd, function() use ($func, $args) { + \call_user_func_array($func, $args); + }); + $this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj; + return $this->_timerIdIndex; + case EventInterface::EV_TIMER_ONCE: + $index = ++$this->_timerIdIndex; + $timer_obj = $this->addTimer($fd, function() use ($func, $args, $index) { + $this->del($index,EventInterface::EV_TIMER_ONCE); + \call_user_func_array($func, $args); + }); + $this->_timerIdMap[$index] = $timer_obj; + return $this->_timerIdIndex; + } + return false; + } + + /** + * Remove event listener from event loop. + * + * @param mixed $fd + * @param int $flag + * @return bool + */ + public function del($fd, $flag) + { + switch ($flag) { + case EventInterface::EV_READ: + return $this->removeReadStream($fd); + case EventInterface::EV_WRITE: + return $this->removeWriteStream($fd); + case EventInterface::EV_SIGNAL: + if (!isset($this->_eventLoop[$fd])) { + return false; + } + $func = $this->_eventLoop[$fd]; + unset($this->_eventLoop[$fd]); + return $this->removeSignal($fd, $func); + + case EventInterface::EV_TIMER: + case EventInterface::EV_TIMER_ONCE: + if (isset($this->_timerIdMap[$fd])){ + $timer_obj = $this->_timerIdMap[$fd]; + unset($this->_timerIdMap[$fd]); + $this->cancelTimer($timer_obj); + return true; + } + } + return false; + } + + + /** + * Main loop. + * + * @return void + */ + public function loop() + { + $this->run(); + } + + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_timerIdMap); + } + + /** + * @param resource $stream + * @param callable $listener + */ + public function addReadStream($stream, $listener) + { + return $this->_eventLoop->addReadStream($stream, $listener); + } + + /** + * @param resource $stream + * @param callable $listener + */ + public function addWriteStream($stream, $listener) + { + return $this->_eventLoop->addWriteStream($stream, $listener); + } + + /** + * @param resource $stream + */ + public function removeReadStream($stream) + { + return $this->_eventLoop->removeReadStream($stream); + } + + /** + * @param resource $stream + */ + public function removeWriteStream($stream) + { + return $this->_eventLoop->removeWriteStream($stream); + } + + /** + * @param float|int $interval + * @param callable $callback + * @return \React\EventLoop\Timer\Timer|TimerInterface + */ + public function addTimer($interval, $callback) + { + return $this->_eventLoop->addTimer($interval, $callback); + } + + /** + * @param float|int $interval + * @param callable $callback + * @return \React\EventLoop\Timer\Timer|TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + return $this->_eventLoop->addPeriodicTimer($interval, $callback); + } + + /** + * @param TimerInterface $timer + */ + public function cancelTimer(TimerInterface $timer) + { + return $this->_eventLoop->cancelTimer($timer); + } + + /** + * @param callable $listener + */ + public function futureTick($listener) + { + return $this->_eventLoop->futureTick($listener); + } + + /** + * @param int $signal + * @param callable $listener + */ + public function addSignal($signal, $listener) + { + return $this->_eventLoop->addSignal($signal, $listener); + } + + /** + * @param int $signal + * @param callable $listener + */ + public function removeSignal($signal, $listener) + { + return $this->_eventLoop->removeSignal($signal, $listener); + } + + /** + * Run. + */ + public function run() + { + return $this->_eventLoop->run(); + } + + /** + * Stop. + */ + public function stop() + { + return $this->_eventLoop->stop(); + } +} diff --git a/vendor/workerman/workerman/Events/React/ExtEventLoop.php b/vendor/workerman/workerman/Events/React/ExtEventLoop.php new file mode 100644 index 0000000..3dab25b --- /dev/null +++ b/vendor/workerman/workerman/Events/React/ExtEventLoop.php @@ -0,0 +1,27 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; + +/** + * Class ExtEventLoop + * @package Workerman\Events\React + */ +class ExtEventLoop extends Base +{ + + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\ExtEventLoop(); + } +} diff --git a/vendor/workerman/workerman/Events/React/ExtLibEventLoop.php b/vendor/workerman/workerman/Events/React/ExtLibEventLoop.php new file mode 100644 index 0000000..eb02b35 --- /dev/null +++ b/vendor/workerman/workerman/Events/React/ExtLibEventLoop.php @@ -0,0 +1,27 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; +use Workerman\Events\EventInterface; + +/** + * Class ExtLibEventLoop + * @package Workerman\Events\React + */ +class ExtLibEventLoop extends Base +{ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\ExtLibeventLoop(); + } +} diff --git a/vendor/workerman/workerman/Events/React/StreamSelectLoop.php b/vendor/workerman/workerman/Events/React/StreamSelectLoop.php new file mode 100644 index 0000000..7f5f94b --- /dev/null +++ b/vendor/workerman/workerman/Events/React/StreamSelectLoop.php @@ -0,0 +1,26 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; + +/** + * Class StreamSelectLoop + * @package Workerman\Events\React + */ +class StreamSelectLoop extends Base +{ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\StreamSelectLoop(); + } +} diff --git a/vendor/workerman/workerman/Events/Select.php b/vendor/workerman/workerman/Events/Select.php new file mode 100644 index 0000000..ebc263e --- /dev/null +++ b/vendor/workerman/workerman/Events/Select.php @@ -0,0 +1,357 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Throwable; +use Workerman\Worker; + +/** + * select eventloop + */ +class Select implements EventInterface +{ + /** + * All listeners for read/write event. + * + * @var array + */ + public $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + public $_signalEvents = array(); + + /** + * Fds waiting for read event. + * + * @var array + */ + protected $_readFds = array(); + + /** + * Fds waiting for write event. + * + * @var array + */ + protected $_writeFds = array(); + + /** + * Fds waiting for except event. + * + * @var array + */ + protected $_exceptFds = array(); + + /** + * Timer scheduler. + * {['data':timer_id, 'priority':run_timestamp], ..} + * + * @var \SplPriorityQueue + */ + protected $_scheduler = null; + + /** + * All timer event listeners. + * [[func, args, flag, timer_interval], ..] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * + * @var int + */ + protected $_timerId = 1; + + /** + * Select timeout. + * + * @var int + */ + protected $_selectTimeout = 100000000; + + /** + * Paired socket channels + * + * @var array + */ + protected $channel = array(); + + /** + * Construct. + */ + public function __construct() + { + // Init SplPriorityQueue. + $this->_scheduler = new \SplPriorityQueue(); + $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + } + + /** + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = array()) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $count = $flag === self::EV_READ ? \count($this->_readFds) : \count($this->_writeFds); + if ($count >= 1024) { + echo "Warning: system call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.\n"; + } else if (\DIRECTORY_SEPARATOR !== '/' && $count >= 256) { + echo "Warning: system call select exceeded the maximum number of connections 256.\n"; + } + $fd_key = (int)$fd; + $this->_allEvents[$fd_key][$flag] = array($func, $fd); + if ($flag === self::EV_READ) { + $this->_readFds[$fd_key] = $fd; + } else { + $this->_writeFds[$fd_key] = $fd; + } + break; + case self::EV_EXCEPT: + $fd_key = (int)$fd; + $this->_allEvents[$fd_key][$flag] = array($func, $fd); + $this->_exceptFds[$fd_key] = $fd; + break; + case self::EV_SIGNAL: + // Windows not support signal. + if(\DIRECTORY_SEPARATOR !== '/') { + return false; + } + $fd_key = (int)$fd; + $this->_signalEvents[$fd_key][$flag] = array($func, $fd); + \pcntl_signal($fd, array($this, 'signalHandler')); + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $timer_id = $this->_timerId++; + $run_time = \microtime(true) + $fd; + $this->_scheduler->insert($timer_id, -$run_time); + $this->_eventTimer[$timer_id] = array($func, (array)$args, $flag, $fd); + $select_timeout = ($run_time - \microtime(true)) * 1000000; + $select_timeout = $select_timeout <= 0 ? 1 : $select_timeout; + if( $this->_selectTimeout > $select_timeout ){ + $this->_selectTimeout = (int) $select_timeout; + } + return $timer_id; + } + + return true; + } + + /** + * Signal handler. + * + * @param int $signal + */ + public function signalHandler($signal) + { + \call_user_func_array($this->_signalEvents[$signal][self::EV_SIGNAL][0], array($signal)); + } + + /** + * {@inheritdoc} + */ + public function del($fd, $flag) + { + $fd_key = (int)$fd; + switch ($flag) { + case self::EV_READ: + unset($this->_allEvents[$fd_key][$flag], $this->_readFds[$fd_key]); + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_WRITE: + unset($this->_allEvents[$fd_key][$flag], $this->_writeFds[$fd_key]); + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_EXCEPT: + unset($this->_allEvents[$fd_key][$flag], $this->_exceptFds[$fd_key]); + if(empty($this->_allEvents[$fd_key])) + { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_SIGNAL: + if(\DIRECTORY_SEPARATOR !== '/') { + return false; + } + unset($this->_signalEvents[$fd_key]); + \pcntl_signal($fd, SIG_IGN); + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE; + unset($this->_eventTimer[$fd_key]); + return true; + } + return false; + } + + /** + * Tick for timer. + * + * @return void + */ + protected function tick() + { + $tasks_to_insert = []; + while (!$this->_scheduler->isEmpty()) { + $scheduler_data = $this->_scheduler->top(); + $timer_id = $scheduler_data['data']; + $next_run_time = -$scheduler_data['priority']; + $time_now = \microtime(true); + $this->_selectTimeout = (int) (($next_run_time - $time_now) * 1000000); + if ($this->_selectTimeout <= 0) { + $this->_scheduler->extract(); + + if (!isset($this->_eventTimer[$timer_id])) { + continue; + } + + // [func, args, flag, timer_interval] + $task_data = $this->_eventTimer[$timer_id]; + if ($task_data[2] === self::EV_TIMER) { + $next_run_time = $time_now + $task_data[3]; + $tasks_to_insert[] = [$timer_id, -$next_run_time]; + } + try { + \call_user_func_array($task_data[0], $task_data[1]); + } catch (Throwable $e) { + Worker::stopAll(250, $e); + } + if (isset($this->_eventTimer[$timer_id]) && $task_data[2] === self::EV_TIMER_ONCE) { + $this->del($timer_id, self::EV_TIMER_ONCE); + } + } else { + break; + } + } + foreach ($tasks_to_insert as $item) { + $this->_scheduler->insert($item[0], $item[1]); + } + if (!$this->_scheduler->isEmpty()) { + $scheduler_data = $this->_scheduler->top(); + $next_run_time = -$scheduler_data['priority']; + $time_now = \microtime(true); + $this->_selectTimeout = \max((int) (($next_run_time - $time_now) * 1000000), 0); + return; + } + $this->_selectTimeout = 100000000; + } + + /** + * {@inheritdoc} + */ + public function clearAllTimer() + { + $this->_scheduler = new \SplPriorityQueue(); + $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + $this->_eventTimer = array(); + } + + /** + * {@inheritdoc} + */ + public function loop() + { + while (1) { + if(\DIRECTORY_SEPARATOR === '/') { + // Calls signal handlers for pending signals + \pcntl_signal_dispatch(); + } + + $read = $this->_readFds; + $write = $this->_writeFds; + $except = $this->_exceptFds; + $ret = false; + + if ($read || $write || $except) { + // Waiting read/write/signal/timeout events. + try { + $ret = @stream_select($read, $write, $except, 0, $this->_selectTimeout); + } catch (\Exception $e) {} catch (\Error $e) {} + + } else { + $this->_selectTimeout >= 1 && usleep($this->_selectTimeout); + } + + if (!$this->_scheduler->isEmpty()) { + $this->tick(); + } + + if (!$ret) { + continue; + } + + if ($read) { + foreach ($read as $fd) { + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][self::EV_READ])) { + \call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0], + array($this->_allEvents[$fd_key][self::EV_READ][1])); + } + } + } + + if ($write) { + foreach ($write as $fd) { + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][self::EV_WRITE])) { + \call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0], + array($this->_allEvents[$fd_key][self::EV_WRITE][1])); + } + } + } + + if($except) { + foreach($except as $fd) { + $fd_key = (int) $fd; + if(isset($this->_allEvents[$fd_key][self::EV_EXCEPT])) { + \call_user_func_array($this->_allEvents[$fd_key][self::EV_EXCEPT][0], + array($this->_allEvents[$fd_key][self::EV_EXCEPT][1])); + } + } + } + } + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_eventTimer); + } +} diff --git a/vendor/workerman/workerman/Events/Swoole.php b/vendor/workerman/workerman/Events/Swoole.php new file mode 100644 index 0000000..dc11526 --- /dev/null +++ b/vendor/workerman/workerman/Events/Swoole.php @@ -0,0 +1,285 @@ + + * @link http://www.workerman.net/ + * @link https://github.com/ares333/Workerman + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; +use Swoole\Event; +use Swoole\Timer; +use Swoole\Coroutine; + +class Swoole implements EventInterface +{ + + protected $_timer = array(); + + protected $_timerOnceMap = array(); + + protected $mapId = 0; + + protected $_fd = array(); + + // milisecond + public static $signalDispatchInterval = 500; + + protected $_hasSignal = false; + + protected $_readEvents = array(); + + protected $_writeEvents = array(); + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::add() + */ + public function add($fd, $flag, $func, $args = array()) + { + switch ($flag) { + case self::EV_SIGNAL: + $res = \pcntl_signal($fd, $func, false); + if (! $this->_hasSignal && $res) { + Timer::tick(static::$signalDispatchInterval, + function () { + \pcntl_signal_dispatch(); + }); + $this->_hasSignal = true; + } + return $res; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $method = self::EV_TIMER === $flag ? 'tick' : 'after'; + if ($this->mapId > \PHP_INT_MAX) { + $this->mapId = 0; + } + $mapId = $this->mapId++; + $t = (int)($fd * 1000); + if ($t < 1) { + $t = 1; + } + $timer_id = Timer::$method($t, + function ($timer_id = null) use ($func, $args, $mapId) { + try { + \call_user_func_array($func, (array)$args); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + // EV_TIMER_ONCE + if (! isset($timer_id)) { + // may be deleted in $func + if (\array_key_exists($mapId, $this->_timerOnceMap)) { + $timer_id = $this->_timerOnceMap[$mapId]; + unset($this->_timer[$timer_id], + $this->_timerOnceMap[$mapId]); + } + } + }); + if ($flag === self::EV_TIMER_ONCE) { + $this->_timerOnceMap[$mapId] = $timer_id; + $this->_timer[$timer_id] = $mapId; + } else { + $this->_timer[$timer_id] = null; + } + return $timer_id; + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int) $fd; + if ($flag === self::EV_READ) { + $this->_readEvents[$fd_key] = $func; + } else { + $this->_writeEvents[$fd_key] = $func; + } + if (!isset($this->_fd[$fd_key])) { + if ($flag === self::EV_READ) { + $res = Event::add($fd, [$this, 'callRead'], null, SWOOLE_EVENT_READ); + $fd_type = SWOOLE_EVENT_READ; + } else { + $res = Event::add($fd, null, $func, SWOOLE_EVENT_WRITE); + $fd_type = SWOOLE_EVENT_WRITE; + } + if ($res) { + $this->_fd[$fd_key] = $fd_type; + } + } else { + $fd_val = $this->_fd[$fd_key]; + $res = true; + if ($flag === self::EV_READ) { + if (($fd_val & SWOOLE_EVENT_READ) !== SWOOLE_EVENT_READ) { + $res = Event::set($fd, $func, null, + SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); + $this->_fd[$fd_key] |= SWOOLE_EVENT_READ; + } + } else { + if (($fd_val & SWOOLE_EVENT_WRITE) !== SWOOLE_EVENT_WRITE) { + $res = Event::set($fd, null, $func, + SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); + $this->_fd[$fd_key] |= SWOOLE_EVENT_WRITE; + } + } + } + return $res; + } + } + + /** + * @param $fd + * @return void + */ + protected function callRead($stream) + { + $fd = (int) $stream; + if (isset($this->_readEvents[$fd])) { + try { + \call_user_func($this->_readEvents[$fd], $stream); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } + + /** + * @param $fd + * @return void + */ + protected function callWrite($stream) + { + $fd = (int) $stream; + if (isset($this->_writeEvents[$fd])) { + try { + \call_user_func($this->_writeEvents[$fd], $stream); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::del() + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_SIGNAL: + return \pcntl_signal($fd, SIG_IGN, false); + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + // already remove in EV_TIMER_ONCE callback. + if (! \array_key_exists($fd, $this->_timer)) { + return true; + } + $res = Timer::clear($fd); + if ($res) { + $mapId = $this->_timer[$fd]; + if (isset($mapId)) { + unset($this->_timerOnceMap[$mapId]); + } + unset($this->_timer[$fd]); + } + return $res; + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int) $fd; + if ($flag === self::EV_READ) { + unset($this->_readEvents[$fd_key]); + } elseif ($flag === self::EV_WRITE) { + unset($this->_writeEvents[$fd_key]); + } + if (isset($this->_fd[$fd_key])) { + $fd_val = $this->_fd[$fd_key]; + if ($flag === self::EV_READ) { + $flag_remove = ~ SWOOLE_EVENT_READ; + } else { + $flag_remove = ~ SWOOLE_EVENT_WRITE; + } + $fd_val &= $flag_remove; + if (0 === $fd_val) { + $res = Event::del($fd); + if ($res) { + unset($this->_fd[$fd_key]); + } + } else { + $res = Event::set($fd, null, null, $fd_val); + if ($res) { + $this->_fd[$fd_key] = $fd_val; + } + } + } else { + $res = true; + } + return $res; + } + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::clearAllTimer() + */ + public function clearAllTimer() + { + foreach (array_keys($this->_timer) as $v) { + Timer::clear($v); + } + $this->_timer = array(); + $this->_timerOnceMap = array(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::loop() + */ + public function loop() + { + Event::wait(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::destroy() + */ + public function destroy() + { + foreach (Coroutine::listCoroutines() as $coroutine) { + Coroutine::cancel($coroutine); + } + // Wait for coroutines to exit + usleep(100000); + Event::exit(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::getTimerCount() + */ + public function getTimerCount() + { + return \count($this->_timer); + } +} diff --git a/vendor/workerman/workerman/Events/Uv.php b/vendor/workerman/workerman/Events/Uv.php new file mode 100644 index 0000000..49f0ddd --- /dev/null +++ b/vendor/workerman/workerman/Events/Uv.php @@ -0,0 +1,260 @@ + + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * libuv eventloop + */ +class Uv implements EventInterface +{ + /** + * Event Loop. + * @var object + */ + protected $_eventLoop = null; + + /** + * All listeners for read/write event. + * + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * + * @var int + */ + protected static $_timerId = 1; + + /** + * @brief Constructor + * + * @param object $loop + * + * @return void + */ + public function __construct(\UVLoop $loop = null) + { + if(!extension_loaded('uv')) + { + throw new \Exception(__CLASS__ . ' requires the UV extension, but detected it has NOT been installed yet.'); + } + + if(empty($loop) || !$loop instanceof \UVLoop) + { + $this->_eventLoop = \uv_default_loop(); + return; + } + + $this->_eventLoop = $loop; + } + + /** + * @brief Add a timer + * + * @param resource $fd + * @param int $flag + * @param callback $func + * @param mixed $args + * + * @return mixed + */ + public function add($fd, $flag, $func, $args = null) + { + switch ($flag) + { + case self::EV_SIGNAL: + $signalCallback = function($watcher, $socket)use($func, $fd){ + try { + \call_user_func($func, $fd); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + }; + $signalWatcher = \uv_signal_init(); + \uv_signal_start($signalWatcher, $signalCallback, $fd); + $this->_eventSignal[$fd] = $signalWatcher; + return true; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $repeat = $flag === self::EV_TIMER_ONCE ? 0 : (int)($fd * 1000); + $param = array($func, (array)$args, $flag, $fd, self::$_timerId); + $timerWatcher = \uv_timer_init(); + \uv_timer_start($timerWatcher, ($flag === self::EV_TIMER_ONCE ? (int)($fd * 1000) :1), $repeat, function($watcher)use($param){ + call_user_func_array([$this, 'timerCallback'], [$param]); + }); + $this->_eventTimer[self::$_timerId] = $timerWatcher; + return self::$_timerId++; + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + $ioCallback = function($watcher, $status, $events, $fd)use($func){ + try { + \call_user_func($func, $fd); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + }; + $ioWatcher = \uv_poll_init($this->_eventLoop, $fd); + $real_flag = $flag === self::EV_READ ? \Uv::READABLE : \Uv::WRITABLE; + \uv_poll_start($ioWatcher, $real_flag, $ioCallback); + $this->_allEvents[$fd_key][$flag] = $ioWatcher; + return true; + default: + break; + } + } + + /** + * @brief Remove a timer + * + * @param resource $fd + * @param int $flag + * + * @return boolean + */ + public function del($fd, $flag) + { + switch ($flag) + { + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + $watcher = $this->_allEvents[$fd_key][$flag]; + \uv_is_active($watcher) && \uv_poll_stop($watcher); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + $watcher = $this->_eventSignal[$fd_key]; + \uv_is_active($watcher) && \uv_signal_stop($watcher); + unset($this->_eventSignal[$fd_key]); + } + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + if (isset($this->_eventTimer[$fd])) { + $watcher = $this->_eventTimer[$fd]; + \uv_is_active($watcher) && \uv_timer_stop($watcher); + unset($this->_eventTimer[$fd]); + } + break; + } + + return true; + } + + /** + * @brief Timer callback + * + * @param array $input + * + * @return void + */ + public function timerCallback($input) + { + if(!is_array($input)) return; + + $timer_id = $input[4]; + + if ($input[2] === self::EV_TIMER_ONCE) + { + $watcher = $this->_eventTimer[$timer_id]; + \uv_is_active($watcher) && \uv_timer_stop($watcher); + unset($this->_eventTimer[$timer_id]); + } + + try { + \call_user_func_array($input[0], $input[1]); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + } + + /** + * @brief Remove all timers + * + * @return void + */ + public function clearAllTimer() + { + if(!is_array($this->_eventTimer)) return; + + foreach($this->_eventTimer as $watcher) + { + \uv_is_active($watcher) && \uv_timer_stop($watcher); + } + + $this->_eventTimer = array(); + } + + /** + * @brief Start loop + * + * @return void + */ + public function loop() + { + \Uv_run(); + } + + /** + * @brief Destroy loop + * + * @return void + */ + public function destroy() + { + !empty($this->_eventLoop) && \uv_loop_delete($this->_eventLoop); + $this->_allEvents = []; + } + + /** + * @brief Get timer count + * + * @return integer + */ + public function getTimerCount() + { + return \count($this->_eventTimer); + } +} diff --git a/vendor/workerman/workerman/Lib/Constants.php b/vendor/workerman/workerman/Lib/Constants.php new file mode 100644 index 0000000..f5e2424 --- /dev/null +++ b/vendor/workerman/workerman/Lib/Constants.php @@ -0,0 +1,44 @@ + + * @copyright walkor + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * + * @link http://www.workerman.net/ + */ + +// Pcre.jit is not stable, temporarily disabled. +ini_set('pcre.jit', 0); + +// For onError callback. +const WORKERMAN_CONNECT_FAIL = 1; +// For onError callback. +const WORKERMAN_SEND_FAIL = 2; + +// Define OS Type +const OS_TYPE_LINUX = 'linux'; +const OS_TYPE_WINDOWS = 'windows'; + +// Compatible with php7 +if (!class_exists('Error')) { + class Error extends Exception + { + } +} + +if (!interface_exists('SessionHandlerInterface')) { + interface SessionHandlerInterface { + public function close(); + public function destroy($session_id); + public function gc($maxlifetime); + public function open($save_path ,$session_name); + public function read($session_id); + public function write($session_id , $session_data); + } +} diff --git a/vendor/workerman/workerman/Lib/Timer.php b/vendor/workerman/workerman/Lib/Timer.php new file mode 100644 index 0000000..b110051 --- /dev/null +++ b/vendor/workerman/workerman/Lib/Timer.php @@ -0,0 +1,22 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Lib; + +/** + * Do not use Workerman\Lib\Timer. + * Please use Workerman\Timer. + * This class is only used for compatibility with workerman 3.* + * @package Workerman\Lib + */ +class Timer extends \Workerman\Timer {} \ No newline at end of file diff --git a/vendor/workerman/workerman/MIT-LICENSE.txt b/vendor/workerman/workerman/MIT-LICENSE.txt new file mode 100644 index 0000000..fd6b1c8 --- /dev/null +++ b/vendor/workerman/workerman/MIT-LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009-2015 walkor and contributors (see https://github.com/walkor/workerman/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/workerman/workerman/Protocols/Frame.php b/vendor/workerman/workerman/Protocols/Frame.php new file mode 100644 index 0000000..26b04de --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Frame.php @@ -0,0 +1,61 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\TcpConnection; + +/** + * Frame Protocol. + */ +class Frame +{ + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param TcpConnection $connection + * @return int + */ + public static function input($buffer, TcpConnection $connection) + { + if (\strlen($buffer) < 4) { + return 0; + } + $unpack_data = \unpack('Ntotal_length', $buffer); + return $unpack_data['total_length']; + } + + /** + * Decode. + * + * @param string $buffer + * @return string + */ + public static function decode($buffer) + { + return \substr($buffer, 4); + } + + /** + * Encode. + * + * @param string $buffer + * @return string + */ + public static function encode($buffer) + { + $total_length = 4 + \strlen($buffer); + return \pack('N', $total_length) . $buffer; + } +} diff --git a/vendor/workerman/workerman/Protocols/Http.php b/vendor/workerman/workerman/Protocols/Http.php new file mode 100644 index 0000000..9e5d928 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http.php @@ -0,0 +1,323 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\TcpConnection; +use Workerman\Protocols\Http\Request; +use Workerman\Protocols\Http\Response; +use Workerman\Protocols\Http\Session; +use Workerman\Protocols\Websocket; +use Workerman\Worker; + +/** + * Class Http. + * @package Workerman\Protocols + */ +class Http +{ + /** + * Request class name. + * + * @var string + */ + protected static $_requestClass = 'Workerman\Protocols\Http\Request'; + + /** + * Upload tmp dir. + * + * @var string + */ + protected static $_uploadTmpDir = ''; + + /** + * Open cache. + * + * @var bool. + */ + protected static $_enableCache = true; + + /** + * Get or set session name. + * + * @param string|null $name + * @return string + */ + public static function sessionName($name = null) + { + if ($name !== null && $name !== '') { + Session::$name = (string)$name; + } + return Session::$name; + } + + /** + * Get or set the request class name. + * + * @param string|null $class_name + * @return string + */ + public static function requestClass($class_name = null) + { + if ($class_name) { + static::$_requestClass = $class_name; + } + return static::$_requestClass; + } + + /** + * Enable or disable Cache. + * + * @param mixed $value + */ + public static function enableCache($value) + { + static::$_enableCache = (bool)$value; + } + + /** + * Check the integrity of the package. + * + * @param string $recv_buffer + * @param TcpConnection $connection + * @return int + */ + public static function input($recv_buffer, TcpConnection $connection) + { + static $input = []; + if (!isset($recv_buffer[512]) && isset($input[$recv_buffer])) { + return $input[$recv_buffer]; + } + $crlf_pos = \strpos($recv_buffer, "\r\n\r\n"); + if (false === $crlf_pos) { + // Judge whether the package length exceeds the limit. + if (\strlen($recv_buffer) >= 16384) { + $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n", true); + return 0; + } + return 0; + } + + $length = $crlf_pos + 4; + $method = \strstr($recv_buffer, ' ', true); + + if (!\in_array($method, ['GET', 'POST', 'OPTIONS', 'HEAD', 'DELETE', 'PUT', 'PATCH'])) { + $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true); + return 0; + } + + $header = \substr($recv_buffer, 0, $crlf_pos); + if ($pos = \strpos($header, "\r\nContent-Length: ")) { + $length = $length + (int)\substr($header, $pos + 18, 10); + $has_content_length = true; + } else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) { + $length = $length + $match[1]; + $has_content_length = true; + } else { + $has_content_length = false; + if (false !== stripos($header, "\r\nTransfer-Encoding:")) { + $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true); + return 0; + } + } + + if ($has_content_length) { + if ($length > $connection->maxPackageSize) { + $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n", true); + return 0; + } + } + + if (!isset($recv_buffer[512])) { + $input[$recv_buffer] = $length; + if (\count($input) > 512) { + unset($input[key($input)]); + } + } + + return $length; + } + + /** + * Http decode. + * + * @param string $recv_buffer + * @param TcpConnection $connection + * @return \Workerman\Protocols\Http\Request + */ + public static function decode($recv_buffer, TcpConnection $connection) + { + static $requests = array(); + $cacheable = static::$_enableCache && !isset($recv_buffer[512]); + if (true === $cacheable && isset($requests[$recv_buffer])) { + $request = $requests[$recv_buffer]; + $request->connection = $connection; + $connection->__request = $request; + $request->properties = array(); + return $request; + } + $request = new static::$_requestClass($recv_buffer); + $request->connection = $connection; + $connection->__request = $request; + if (true === $cacheable) { + $requests[$recv_buffer] = $request; + if (\count($requests) > 512) { + unset($requests[key($requests)]); + } + } + return $request; + } + + /** + * Http encode. + * + * @param string|Response $response + * @param TcpConnection $connection + * @return string + */ + public static function encode($response, TcpConnection $connection) + { + if (isset($connection->__request)) { + $connection->__request->session = null; + $connection->__request->connection = null; + $connection->__request = null; + } + if (!\is_object($response)) { + $ext_header = ''; + if (isset($connection->__header)) { + foreach ($connection->__header as $name => $value) { + if (\is_array($value)) { + foreach ($value as $item) { + $ext_header = "$name: $item\r\n"; + } + } else { + $ext_header = "$name: $value\r\n"; + } + } + unset($connection->__header); + } + $body_len = \strlen((string)$response); + return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$ext_header}Connection: keep-alive\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $body_len\r\n\r\n$response"; + } + + if (isset($connection->__header)) { + $response->withHeaders($connection->__header); + unset($connection->__header); + } + + if (isset($response->file)) { + $file = $response->file['file']; + $offset = $response->file['offset']; + $length = $response->file['length']; + clearstatcache(); + $file_size = (int)\filesize($file); + $body_len = $length > 0 ? $length : $file_size - $offset; + $response->withHeaders(array( + 'Content-Length' => $body_len, + 'Accept-Ranges' => 'bytes', + )); + if ($offset || $length) { + $offset_end = $offset + $body_len - 1; + $response->header('Content-Range', "bytes $offset-$offset_end/$file_size"); + } + if ($body_len < 2 * 1024 * 1024) { + $connection->send((string)$response . file_get_contents($file, false, null, $offset, $body_len), true); + return ''; + } + $handler = \fopen($file, 'r'); + if (false === $handler) { + $connection->close(new Response(403, null, '403 Forbidden')); + return ''; + } + $connection->send((string)$response, true); + static::sendStream($connection, $handler, $offset, $length); + return ''; + } + + return (string)$response; + } + + /** + * Send remainder of a stream to client. + * + * @param TcpConnection $connection + * @param resource $handler + * @param int $offset + * @param int $length + */ + protected static function sendStream(TcpConnection $connection, $handler, $offset = 0, $length = 0) + { + $connection->bufferFull = false; + if ($offset !== 0) { + \fseek($handler, $offset); + } + $offset_end = $offset + $length; + // Read file content from disk piece by piece and send to client. + $do_write = function () use ($connection, $handler, $length, $offset_end) { + // Send buffer not full. + while ($connection->bufferFull === false) { + // Read from disk. + $size = 1024 * 1024; + if ($length !== 0) { + $tell = \ftell($handler); + $remain_size = $offset_end - $tell; + if ($remain_size <= 0) { + fclose($handler); + $connection->onBufferDrain = null; + return; + } + $size = $remain_size > $size ? $size : $remain_size; + } + + $buffer = \fread($handler, $size); + // Read eof. + if ($buffer === '' || $buffer === false) { + fclose($handler); + $connection->onBufferDrain = null; + return; + } + $connection->send($buffer, true); + } + }; + // Send buffer full. + $connection->onBufferFull = function ($connection) { + $connection->bufferFull = true; + }; + // Send buffer drain. + $connection->onBufferDrain = function ($connection) use ($do_write) { + $connection->bufferFull = false; + $do_write(); + }; + $do_write(); + } + + /** + * Set or get uploadTmpDir. + * + * @return bool|string + */ + public static function uploadTmpDir($dir = null) + { + if (null !== $dir) { + static::$_uploadTmpDir = $dir; + } + if (static::$_uploadTmpDir === '') { + if ($upload_tmp_dir = \ini_get('upload_tmp_dir')) { + static::$_uploadTmpDir = $upload_tmp_dir; + } else if ($upload_tmp_dir = \sys_get_temp_dir()) { + static::$_uploadTmpDir = $upload_tmp_dir; + } + } + return static::$_uploadTmpDir; + } +} diff --git a/vendor/workerman/workerman/Protocols/Http/Chunk.php b/vendor/workerman/workerman/Protocols/Http/Chunk.php new file mode 100644 index 0000000..ab06a9c --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Chunk.php @@ -0,0 +1,48 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http; + + +/** + * Class Chunk + * @package Workerman\Protocols\Http + */ +class Chunk +{ + /** + * Chunk buffer. + * + * @var string + */ + protected $_buffer = null; + + /** + * Chunk constructor. + * @param string $buffer + */ + public function __construct($buffer) + { + $this->_buffer = $buffer; + } + + /** + * __toString + * + * @return string + */ + public function __toString() + { + return \dechex(\strlen($this->_buffer))."\r\n$this->_buffer\r\n"; + } +} \ No newline at end of file diff --git a/vendor/workerman/workerman/Protocols/Http/Request.php b/vendor/workerman/workerman/Protocols/Http/Request.php new file mode 100644 index 0000000..6eca3e4 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Request.php @@ -0,0 +1,694 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http; + +use Workerman\Connection\TcpConnection; +use Workerman\Protocols\Http\Session; +use Workerman\Protocols\Http; +use Workerman\Worker; + +/** + * Class Request + * @package Workerman\Protocols\Http + */ +class Request +{ + /** + * Connection. + * + * @var TcpConnection + */ + public $connection = null; + + /** + * Session instance. + * + * @var Session + */ + public $session = null; + + /** + * Properties. + * + * @var array + */ + public $properties = array(); + + /** + * @var int + */ + public static $maxFileUploads = 1024; + + /** + * Http buffer. + * + * @var string + */ + protected $_buffer = null; + + /** + * Request data. + * + * @var array + */ + protected $_data = null; + + /** + * Enable cache. + * + * @var bool + */ + protected static $_enableCache = true; + + /** + * Is safe. + * + * @var bool + */ + protected $_isSafe = true; + + + /** + * Request constructor. + * + * @param string $buffer + */ + public function __construct($buffer) + { + $this->_buffer = $buffer; + } + + /** + * $_GET. + * + * @param string|null $name + * @param mixed|null $default + * @return mixed|null + */ + public function get($name = null, $default = null) + { + if (!isset($this->_data['get'])) { + $this->parseGet(); + } + if (null === $name) { + return $this->_data['get']; + } + return isset($this->_data['get'][$name]) ? $this->_data['get'][$name] : $default; + } + + /** + * $_POST. + * + * @param string|null $name + * @param mixed|null $default + * @return mixed|null + */ + public function post($name = null, $default = null) + { + if (!isset($this->_data['post'])) { + $this->parsePost(); + } + if (null === $name) { + return $this->_data['post']; + } + return isset($this->_data['post'][$name]) ? $this->_data['post'][$name] : $default; + } + + /** + * Get header item by name. + * + * @param string|null $name + * @param mixed|null $default + * @return array|string|null + */ + public function header($name = null, $default = null) + { + if (!isset($this->_data['headers'])) { + $this->parseHeaders(); + } + if (null === $name) { + return $this->_data['headers']; + } + $name = \strtolower($name); + return isset($this->_data['headers'][$name]) ? $this->_data['headers'][$name] : $default; + } + + /** + * Get cookie item by name. + * + * @param string|null $name + * @param mixed|null $default + * @return array|string|null + */ + public function cookie($name = null, $default = null) + { + if (!isset($this->_data['cookie'])) { + $this->_data['cookie'] = array(); + \parse_str(\preg_replace('/; ?/', '&', $this->header('cookie', '')), $this->_data['cookie']); + } + if ($name === null) { + return $this->_data['cookie']; + } + return isset($this->_data['cookie'][$name]) ? $this->_data['cookie'][$name] : $default; + } + + /** + * Get upload files. + * + * @param string|null $name + * @return array|null + */ + public function file(?string $name = null): mixed + { + if (!isset($this->_data['files'])) { + $this->parsePost(); + } + if (null === $name) { + return $this->_data['files']; + } + return isset($this->_data['files'][$name]) ? $this->_data['files'][$name] : null; + } + + /** + * Get method. + * + * @return string + */ + public function method() + { + if (!isset($this->_data['method'])) { + $this->parseHeadFirstLine(); + } + return $this->_data['method']; + } + + /** + * Get http protocol version. + * + * @return string + */ + public function protocolVersion() + { + if (!isset($this->_data['protocolVersion'])) { + $this->parseProtocolVersion(); + } + return $this->_data['protocolVersion']; + } + + /** + * Get host. + * + * @param bool $without_port + * @return string + */ + public function host($without_port = false) + { + $host = $this->header('host'); + if ($host && $without_port) { + return preg_replace('/:\d{1,5}$/', '', $host); + } + return $host; + } + + /** + * Get uri. + * + * @return mixed + */ + public function uri() + { + if (!isset($this->_data['uri'])) { + $this->parseHeadFirstLine(); + } + return $this->_data['uri']; + } + + /** + * Get path. + * + * @return mixed + */ + public function path() + { + if (!isset($this->_data['path'])) { + $this->_data['path'] = (string)\parse_url($this->uri(), PHP_URL_PATH); + } + return $this->_data['path']; + } + + /** + * Get query string. + * + * @return mixed + */ + public function queryString() + { + if (!isset($this->_data['query_string'])) { + $this->_data['query_string'] = (string)\parse_url($this->uri(), PHP_URL_QUERY); + } + return $this->_data['query_string']; + } + + /** + * Get session. + * + * @return bool|\Workerman\Protocols\Http\Session + */ + public function session() + { + if ($this->session === null) { + $session_id = $this->sessionId(); + if ($session_id === false) { + return false; + } + $this->session = new Session($session_id); + } + return $this->session; + } + + /** + * Get/Set session id. + * + * @param $session_id + * @return string + */ + public function sessionId($session_id = null) + { + if ($session_id) { + unset($this->sid); + } + if (!isset($this->sid)) { + $session_name = Session::$name; + $sid = $session_id ? '' : $this->cookie($session_name); + if ($sid === '' || $sid === null) { + if ($this->connection === null) { + Worker::safeEcho('Request->session() fail, header already send'); + return false; + } + $sid = $session_id ? $session_id : static::createSessionId(); + $cookie_params = Session::getCookieParams(); + $this->connection->__header['Set-Cookie'] = array($session_name . '=' . $sid + . (empty($cookie_params['domain']) ? '' : '; Domain=' . $cookie_params['domain']) + . (empty($cookie_params['lifetime']) ? '' : '; Max-Age=' . $cookie_params['lifetime']) + . (empty($cookie_params['path']) ? '' : '; Path=' . $cookie_params['path']) + . (empty($cookie_params['samesite']) ? '' : '; SameSite=' . $cookie_params['samesite']) + . (!$cookie_params['secure'] ? '' : '; Secure') + . (!$cookie_params['httponly'] ? '' : '; HttpOnly')); + } + $this->sid = $sid; + } + return $this->sid; + } + + /** + * Get http raw head. + * + * @return string + */ + public function rawHead() + { + if (!isset($this->_data['head'])) { + $this->_data['head'] = \strstr($this->_buffer, "\r\n\r\n", true); + } + return $this->_data['head']; + } + + /** + * Get http raw body. + * + * @return string + */ + public function rawBody() + { + return \substr($this->_buffer, \strpos($this->_buffer, "\r\n\r\n") + 4); + } + + /** + * Get raw buffer. + * + * @return string + */ + public function rawBuffer() + { + return $this->_buffer; + } + + /** + * Enable or disable cache. + * + * @param mixed $value + */ + public static function enableCache($value) + { + static::$_enableCache = (bool)$value; + } + + /** + * Parse first line of http header buffer. + * + * @return void + */ + protected function parseHeadFirstLine() + { + $first_line = \strstr($this->_buffer, "\r\n", true); + $tmp = \explode(' ', $first_line, 3); + $this->_data['method'] = $tmp[0]; + $this->_data['uri'] = isset($tmp[1]) ? $tmp[1] : '/'; + } + + /** + * Parse protocol version. + * + * @return void + */ + protected function parseProtocolVersion() + { + $first_line = \strstr($this->_buffer, "\r\n", true); + $protoco_version = substr(\strstr($first_line, 'HTTP/'), 5); + $this->_data['protocolVersion'] = $protoco_version ? $protoco_version : '1.0'; + } + + /** + * Parse headers. + * + * @return void + */ + protected function parseHeaders() + { + static $cache = []; + $this->_data['headers'] = array(); + $raw_head = $this->rawHead(); + $end_line_position = \strpos($raw_head, "\r\n"); + if ($end_line_position === false) { + return; + } + $head_buffer = \substr($raw_head, $end_line_position + 2); + $cacheable = static::$_enableCache && !isset($head_buffer[2048]); + if ($cacheable && isset($cache[$head_buffer])) { + $this->_data['headers'] = $cache[$head_buffer]; + return; + } + $head_data = \explode("\r\n", $head_buffer); + foreach ($head_data as $content) { + if (false !== \strpos($content, ':')) { + list($key, $value) = \explode(':', $content, 2); + $key = \strtolower($key); + $value = \ltrim($value); + } else { + $key = \strtolower($content); + $value = ''; + } + if (isset($this->_data['headers'][$key])) { + $this->_data['headers'][$key] = "{$this->_data['headers'][$key]},$value"; + } else { + $this->_data['headers'][$key] = $value; + } + } + if ($cacheable) { + $cache[$head_buffer] = $this->_data['headers']; + if (\count($cache) > 128) { + unset($cache[key($cache)]); + } + } + } + + /** + * Parse head. + * + * @return void + */ + protected function parseGet() + { + static $cache = []; + $query_string = $this->queryString(); + $this->_data['get'] = array(); + if ($query_string === '') { + return; + } + $cacheable = static::$_enableCache && !isset($query_string[1024]); + if ($cacheable && isset($cache[$query_string])) { + $this->_data['get'] = $cache[$query_string]; + return; + } + \parse_str($query_string, $this->_data['get']); + if ($cacheable) { + $cache[$query_string] = $this->_data['get']; + if (\count($cache) > 256) { + unset($cache[key($cache)]); + } + } + } + + /** + * Parse post. + * + * @return void + */ + protected function parsePost() + { + static $cache = []; + $this->_data['post'] = $this->_data['files'] = array(); + $content_type = $this->header('content-type', ''); + if (\preg_match('/boundary="?(\S+)"?/', $content_type, $match)) { + $http_post_boundary = '--' . $match[1]; + $this->parseUploadFiles($http_post_boundary); + return; + } + $body_buffer = $this->rawBody(); + if ($body_buffer === '') { + return; + } + $cacheable = static::$_enableCache && !isset($body_buffer[1024]); + if ($cacheable && isset($cache[$body_buffer])) { + $this->_data['post'] = $cache[$body_buffer]; + return; + } + if (\preg_match('/\bjson\b/i', $content_type)) { + $this->_data['post'] = (array) json_decode($body_buffer, true); + } else { + \parse_str($body_buffer, $this->_data['post']); + } + if ($cacheable) { + $cache[$body_buffer] = $this->_data['post']; + if (\count($cache) > 256) { + unset($cache[key($cache)]); + } + } + } + + /** + * Parse upload files. + * + * @param string $http_post_boundary + * @return void + */ + protected function parseUploadFiles($http_post_boundary) + { + $http_post_boundary = \trim($http_post_boundary, '"'); + $buffer = $this->_buffer; + $post_encode_string = ''; + $files_encode_string = ''; + $files = []; + $boday_position = strpos($buffer, "\r\n\r\n") + 4; + $offset = $boday_position + strlen($http_post_boundary) + 2; + $max_count = static::$maxFileUploads; + while ($max_count-- > 0 && $offset) { + $offset = $this->parseUploadFile($http_post_boundary, $offset, $post_encode_string, $files_encode_string, $files); + } + if ($post_encode_string) { + parse_str($post_encode_string, $this->_data['post']); + } + + if ($files_encode_string) { + parse_str($files_encode_string, $this->_data['files']); + \array_walk_recursive($this->_data['files'], function (&$value) use ($files) { + $value = $files[$value]; + }); + } + } + + /** + * @param $boundary + * @param $section_start_offset + * @return int + */ + protected function parseUploadFile($boundary, $section_start_offset, &$post_encode_string, &$files_encode_str, &$files) + { + $file = []; + $boundary = "\r\n$boundary"; + if (\strlen($this->_buffer) < $section_start_offset) { + return 0; + } + $section_end_offset = \strpos($this->_buffer, $boundary, $section_start_offset); + if (!$section_end_offset) { + return 0; + } + $content_lines_end_offset = \strpos($this->_buffer, "\r\n\r\n", $section_start_offset); + if (!$content_lines_end_offset || $content_lines_end_offset + 4 > $section_end_offset) { + return 0; + } + $content_lines_str = \substr($this->_buffer, $section_start_offset, $content_lines_end_offset - $section_start_offset); + $content_lines = \explode("\r\n", trim($content_lines_str . "\r\n")); + $boundary_value = \substr($this->_buffer, $content_lines_end_offset + 4, $section_end_offset - $content_lines_end_offset - 4); + $upload_key = false; + foreach ($content_lines as $content_line) { + if (!\strpos($content_line, ': ')) { + return 0; + } + list($key, $value) = \explode(': ', $content_line); + switch (strtolower($key)) { + case "content-disposition": + // Is file data. + if (\preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) { + $error = 0; + $tmp_file = ''; + $file_name = $match[2]; + $size = \strlen($boundary_value); + $tmp_upload_dir = HTTP::uploadTmpDir(); + if (!$tmp_upload_dir) { + $error = UPLOAD_ERR_NO_TMP_DIR; + } else if ($boundary_value === '' && $file_name === '') { + $error = UPLOAD_ERR_NO_FILE; + } else { + $tmp_file = \tempnam($tmp_upload_dir, 'workerman.upload.'); + if ($tmp_file === false || false === \file_put_contents($tmp_file, $boundary_value)) { + $error = UPLOAD_ERR_CANT_WRITE; + } + } + $upload_key = $match[1]; + // Parse upload files. + $file = [ + 'name' => $file_name, + 'tmp_name' => $tmp_file, + 'size' => $size, + 'error' => $error, + 'type' => '', + ]; + break; + } // Is post field. + else { + // Parse $_POST. + if (\preg_match('/name="(.*?)"$/', $value, $match)) { + $k = $match[1]; + $post_encode_string .= \urlencode($k) . "=" . \urlencode($boundary_value) . '&'; + } + return $section_end_offset + \strlen($boundary) + 2; + } + break; + case "content-type": + $file['type'] = \trim($value); + break; + } + } + if ($upload_key === false) { + return 0; + } + $files_encode_str .= \urlencode($upload_key) . '=' . \count($files) . '&'; + $files[] = $file; + + return $section_end_offset + \strlen($boundary) + 2; + } + + /** + * Create session id. + * + * @return string + */ + protected static function createSessionId() + { + return \bin2hex(\pack('d', \microtime(true)) . random_bytes(8)); + } + + /** + * Setter. + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + $this->properties[$name] = $value; + } + + /** + * Getter. + * + * @param string $name + * @return mixed|null + */ + public function __get($name) + { + return isset($this->properties[$name]) ? $this->properties[$name] : null; + } + + /** + * Isset. + * + * @param string $name + * @return bool + */ + public function __isset($name) + { + return isset($this->properties[$name]); + } + + /** + * Unset. + * + * @param string $name + * @return void + */ + public function __unset($name) + { + unset($this->properties[$name]); + } + + /** + * __toString. + */ + public function __toString() + { + return $this->_buffer; + } + + /** + * __wakeup. + * + * @return void + */ + public function __wakeup() + { + $this->_isSafe = false; + } + + /** + * __destruct. + * + * @return void + */ + public function __destruct() + { + if (isset($this->_data['files']) && $this->_isSafe) { + \clearstatcache(); + \array_walk_recursive($this->_data['files'], function($value, $key){ + if ($key === 'tmp_name') { + if (\is_file($value)) { + \unlink($value); + } + } + }); + } + } +} diff --git a/vendor/workerman/workerman/Protocols/Http/Response.php b/vendor/workerman/workerman/Protocols/Http/Response.php new file mode 100644 index 0000000..e423727 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Response.php @@ -0,0 +1,458 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http; + +/** + * Class Response + * @package Workerman\Protocols\Http + */ +class Response +{ + /** + * Header data. + * + * @var array + */ + protected $_header = null; + + /** + * Http status. + * + * @var int + */ + protected $_status = null; + + /** + * Http reason. + * + * @var string + */ + protected $_reason = null; + + /** + * Http version. + * + * @var string + */ + protected $_version = '1.1'; + + /** + * Http body. + * + * @var string + */ + protected $_body = null; + + /** + * Send file info + * + * @var array + */ + public $file = null; + + /** + * Mine type map. + * @var array + */ + protected static $_mimeTypeMap = null; + + /** + * Phrases. + * + * @var array + */ + protected static $_phrases = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary Redirect', + 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 Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ); + + /** + * Init. + * + * @return void + */ + public static function init() { + static::initMimeTypeMap(); + } + + /** + * Response constructor. + * + * @param int $status + * @param array $headers + * @param string $body + */ + public function __construct( + $status = 200, + $headers = array(), + $body = '' + ) { + $this->_status = $status; + $this->_header = $headers; + $this->_body = (string)$body; + } + + /** + * Set header. + * + * @param string $name + * @param string $value + * @return $this + */ + public function header($name, $value) { + $this->_header[$name] = $value; + return $this; + } + + /** + * Set header. + * + * @param string $name + * @param string $value + * @return Response + */ + public function withHeader($name, $value) { + return $this->header($name, $value); + } + + /** + * Set headers. + * + * @param array $headers + * @return $this + */ + public function withHeaders($headers) { + $this->_header = \array_merge_recursive($this->_header, $headers); + return $this; + } + + /** + * Remove header. + * + * @param string $name + * @return $this + */ + public function withoutHeader($name) { + unset($this->_header[$name]); + return $this; + } + + /** + * Get header. + * + * @param string $name + * @return null|array|string + */ + public function getHeader($name) { + if (!isset($this->_header[$name])) { + return null; + } + return $this->_header[$name]; + } + + /** + * Get headers. + * + * @return array + */ + public function getHeaders() { + return $this->_header; + } + + /** + * Set status. + * + * @param int $code + * @param string|null $reason_phrase + * @return $this + */ + public function withStatus($code, $reason_phrase = null) { + $this->_status = $code; + $this->_reason = $reason_phrase; + return $this; + } + + /** + * Get status code. + * + * @return int + */ + public function getStatusCode() { + return $this->_status; + } + + /** + * Get reason phrase. + * + * @return string + */ + public function getReasonPhrase() { + return $this->_reason; + } + + /** + * Set protocol version. + * + * @param int $version + * @return $this + */ + public function withProtocolVersion($version) { + $this->_version = $version; + return $this; + } + + /** + * Set http body. + * + * @param string $body + * @return $this + */ + public function withBody($body) { + $this->_body = $body; + return $this; + } + + /** + * Get http raw body. + * + * @return string + */ + public function rawBody() { + return $this->_body; + } + + /** + * Send file. + * + * @param string $file + * @param int $offset + * @param int $length + * @return $this + */ + public function withFile($file, $offset = 0, $length = 0) { + if (!\is_file($file)) { + return $this->withStatus(404)->withBody('

404 Not Found

'); + } + $this->file = array('file' => $file, 'offset' => $offset, 'length' => $length); + return $this; + } + + /** + * Set cookie. + * + * @param $name + * @param string $value + * @param int $max_age + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $http_only + * @param string $same_site + * @return $this + */ + public function cookie($name, $value = '', $max_age = null, $path = '', $domain = '', $secure = false, $http_only = false, $same_site = '') + { + $this->_header['Set-Cookie'][] = $name . '=' . \rawurlencode($value) + . (empty($domain) ? '' : '; Domain=' . $domain) + . ($max_age === null ? '' : '; Max-Age=' . $max_age) + . (empty($path) ? '' : '; Path=' . $path) + . (!$secure ? '' : '; Secure') + . (!$http_only ? '' : '; HttpOnly') + . (empty($same_site) ? '' : '; SameSite=' . $same_site); + return $this; + } + + /** + * Create header for file. + * + * @param array $file_info + * @return string + */ + protected function createHeadForFile($file_info) + { + $file = $file_info['file']; + $reason = $this->_reason ? $this->_reason : static::$_phrases[$this->_status]; + $head = "HTTP/{$this->_version} {$this->_status} $reason\r\n"; + $headers = $this->_header; + if (!isset($headers['Server'])) { + $head .= "Server: workerman\r\n"; + } + foreach ($headers as $name => $value) { + if (\is_array($value)) { + foreach ($value as $item) { + $head .= "$name: $item\r\n"; + } + continue; + } + $head .= "$name: $value\r\n"; + } + + if (!isset($headers['Connection'])) { + $head .= "Connection: keep-alive\r\n"; + } + + $file_info = \pathinfo($file); + $extension = isset($file_info['extension']) ? $file_info['extension'] : ''; + $base_name = isset($file_info['basename']) ? $file_info['basename'] : 'unknown'; + if (!isset($headers['Content-Type'])) { + if (isset(self::$_mimeTypeMap[$extension])) { + $head .= "Content-Type: " . self::$_mimeTypeMap[$extension] . "\r\n"; + } else { + $head .= "Content-Type: application/octet-stream\r\n"; + } + } + + if (!isset($headers['Content-Disposition']) && !isset(self::$_mimeTypeMap[$extension])) { + $head .= "Content-Disposition: attachment; filename=\"$base_name\"\r\n"; + } + + if (!isset($headers['Last-Modified'])) { + if ($mtime = \filemtime($file)) { + $head .= 'Last-Modified: '. \gmdate('D, d M Y H:i:s', $mtime) . ' GMT' . "\r\n"; + } + } + + return "{$head}\r\n"; + } + + /** + * __toString. + * + * @return string + */ + public function __toString() + { + if (isset($this->file)) { + return $this->createHeadForFile($this->file); + } + + $reason = $this->_reason ? $this->_reason : static::$_phrases[$this->_status]; + $body_len = \strlen($this->_body); + if (empty($this->_header)) { + return "HTTP/{$this->_version} {$this->_status} $reason\r\nServer: workerman\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $body_len\r\nConnection: keep-alive\r\n\r\n{$this->_body}"; + } + + $head = "HTTP/{$this->_version} {$this->_status} $reason\r\n"; + $headers = $this->_header; + if (!isset($headers['Server'])) { + $head .= "Server: workerman\r\n"; + } + foreach ($headers as $name => $value) { + if (\is_array($value)) { + foreach ($value as $item) { + $head .= "$name: $item\r\n"; + } + continue; + } + $head .= "$name: $value\r\n"; + } + + if (!isset($headers['Connection'])) { + $head .= "Connection: keep-alive\r\n"; + } + + if (!isset($headers['Content-Type'])) { + $head .= "Content-Type: text/html;charset=utf-8\r\n"; + } else if ($headers['Content-Type'] === 'text/event-stream') { + return $head . $this->_body; + } + + if (!isset($headers['Transfer-Encoding'])) { + $head .= "Content-Length: $body_len\r\n\r\n"; + } else { + return $body_len ? "$head\r\n" . dechex($body_len) . "\r\n{$this->_body}\r\n" : "$head\r\n"; + } + + // The whole http package + return $head . $this->_body; + } + + /** + * Init mime map. + * + * @return void + */ + public static function initMimeTypeMap() + { + $mime_file = __DIR__ . '/mime.types'; + $items = \file($mime_file, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES); + foreach ($items as $content) { + if (\preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) { + $mime_type = $match[1]; + $extension_var = $match[2]; + $extension_array = \explode(' ', \substr($extension_var, 0, -1)); + foreach ($extension_array as $file_extension) { + static::$_mimeTypeMap[$file_extension] = $mime_type; + } + } + } + } +} +Response::init(); diff --git a/vendor/workerman/workerman/Protocols/Http/ServerSentEvents.php b/vendor/workerman/workerman/Protocols/Http/ServerSentEvents.php new file mode 100644 index 0000000..a6e9e0d --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/ServerSentEvents.php @@ -0,0 +1,64 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http; + +/** + * Class ServerSentEvents + * @package Workerman\Protocols\Http + */ +class ServerSentEvents +{ + /** + * Data. + * @var array + */ + protected $_data = null; + + /** + * ServerSentEvents constructor. + * $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000] + * @param array $data + */ + public function __construct(array $data) + { + $this->_data = $data; + } + + /** + * __toString. + * + * @return string + */ + public function __toString() + { + $buffer = ''; + $data = $this->_data; + if (isset($data[''])) { + $buffer = ": {$data['']}\n"; + } + if (isset($data['event'])) { + $buffer .= "event: {$data['event']}\n"; + } + if (isset($data['id'])) { + $buffer .= "id: {$data['id']}\n"; + } + if (isset($data['retry'])) { + $buffer .= "retry: {$data['retry']}\n"; + } + if (isset($data['data'])) { + $buffer .= 'data: ' . str_replace("\n", "\ndata: ", $data['data']) . "\n"; + } + return $buffer . "\n"; + } +} \ No newline at end of file diff --git a/vendor/workerman/workerman/Protocols/Http/Session.php b/vendor/workerman/workerman/Protocols/Http/Session.php new file mode 100644 index 0000000..a0c2417 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Session.php @@ -0,0 +1,461 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace Workerman\Protocols\Http; + +use Workerman\Protocols\Http\Session\SessionHandlerInterface; + +/** + * Class Session + * @package Workerman\Protocols\Http + */ +class Session +{ + /** + * Session andler class which implements SessionHandlerInterface. + * + * @var string + */ + protected static $_handlerClass = 'Workerman\Protocols\Http\Session\FileSessionHandler'; + + /** + * Parameters of __constructor for session handler class. + * + * @var null + */ + protected static $_handlerConfig = null; + + /** + * Session name. + * + * @var string + */ + public static $name = 'PHPSID'; + + /** + * Auto update timestamp. + * + * @var bool + */ + public static $autoUpdateTimestamp = false; + + /** + * Session lifetime. + * + * @var int + */ + public static $lifetime = 1440; + + /** + * Cookie lifetime. + * + * @var int + */ + public static $cookieLifetime = 1440; + + /** + * Session cookie path. + * + * @var string + */ + public static $cookiePath = '/'; + + /** + * Session cookie domain. + * + * @var string + */ + public static $domain = ''; + + /** + * HTTPS only cookies. + * + * @var bool + */ + public static $secure = false; + + /** + * HTTP access only. + * + * @var bool + */ + public static $httpOnly = true; + + /** + * Same-site cookies. + * + * @var string + */ + public static $sameSite = ''; + + /** + * Gc probability. + * + * @var int[] + */ + public static $gcProbability = [1, 1000]; + + /** + * Session handler instance. + * + * @var SessionHandlerInterface + */ + protected static $_handler = null; + + /** + * Session data. + * + * @var array + */ + protected $_data = []; + + /** + * Session changed and need to save. + * + * @var bool + */ + protected $_needSave = false; + + /** + * Session id. + * + * @var null + */ + protected $_sessionId = null; + + /** + * Is safe. + * + * @var bool + */ + protected $_isSafe = true; + + /** + * Session constructor. + * + * @param string $session_id + */ + public function __construct($session_id) + { + static::checkSessionId($session_id); + if (static::$_handler === null) { + static::initHandler(); + } + $this->_sessionId = $session_id; + if ($data = static::$_handler->read($session_id)) { + $this->_data = \unserialize($data); + } + } + + /** + * Get session id. + * + * @return string + */ + public function getId() + { + return $this->_sessionId; + } + + /** + * Get session. + * + * @param string $name + * @param mixed|null $default + * @return mixed|null + */ + public function get($name, $default = null) + { + return isset($this->_data[$name]) ? $this->_data[$name] : $default; + } + + /** + * Store data in the session. + * + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->_data[$name] = $value; + $this->_needSave = true; + } + + /** + * Delete an item from the session. + * + * @param string $name + */ + public function delete($name) + { + unset($this->_data[$name]); + $this->_needSave = true; + } + + /** + * Retrieve and delete an item from the session. + * + * @param string $name + * @param mixed|null $default + * @return mixed|null + */ + public function pull($name, $default = null) + { + $value = $this->get($name, $default); + $this->delete($name); + return $value; + } + + /** + * Store data in the session. + * + * @param string|array $key + * @param mixed|null $value + */ + public function put($key, $value = null) + { + if (!\is_array($key)) { + $this->set($key, $value); + return; + } + + foreach ($key as $k => $v) { + $this->_data[$k] = $v; + } + $this->_needSave = true; + } + + /** + * Remove a piece of data from the session. + * + * @param string $name + */ + public function forget($name) + { + if (\is_scalar($name)) { + $this->delete($name); + return; + } + if (\is_array($name)) { + foreach ($name as $key) { + unset($this->_data[$key]); + } + } + $this->_needSave = true; + } + + /** + * Retrieve all the data in the session. + * + * @return array + */ + public function all() + { + return $this->_data; + } + + /** + * Remove all data from the session. + * + * @return void + */ + public function flush() + { + $this->_needSave = true; + $this->_data = []; + } + + /** + * Determining If An Item Exists In The Session. + * + * @param string $name + * @return bool + */ + public function has($name) + { + return isset($this->_data[$name]); + } + + /** + * To determine if an item is present in the session, even if its value is null. + * + * @param string $name + * @return bool + */ + public function exists($name) + { + return \array_key_exists($name, $this->_data); + } + + /** + * Save session to store. + * + * @return void + */ + public function save() + { + if ($this->_needSave) { + if (empty($this->_data)) { + static::$_handler->destroy($this->_sessionId); + } else { + static::$_handler->write($this->_sessionId, \serialize($this->_data)); + } + } elseif (static::$autoUpdateTimestamp) { + static::refresh(); + } + $this->_needSave = false; + } + + /** + * Refresh session expire time. + * + * @return bool + */ + public function refresh() + { + static::$_handler->updateTimestamp($this->getId()); + } + + /** + * Init. + * + * @return void + */ + public static function init() + { + if (($gc_probability = (int)\ini_get('session.gc_probability')) && ($gc_divisor = (int)\ini_get('session.gc_divisor'))) { + static::$gcProbability = [$gc_probability, $gc_divisor]; + } + + if ($gc_max_life_time = \ini_get('session.gc_maxlifetime')) { + self::$lifetime = (int)$gc_max_life_time; + } + + $session_cookie_params = \session_get_cookie_params(); + static::$cookieLifetime = $session_cookie_params['lifetime']; + static::$cookiePath = $session_cookie_params['path']; + static::$domain = $session_cookie_params['domain']; + static::$secure = $session_cookie_params['secure']; + static::$httpOnly = $session_cookie_params['httponly']; + } + + /** + * Set session handler class. + * + * @param mixed|null $class_name + * @param mixed|null $config + * @return string + */ + public static function handlerClass($class_name = null, $config = null) + { + if ($class_name) { + static::$_handlerClass = $class_name; + } + if ($config) { + static::$_handlerConfig = $config; + } + return static::$_handlerClass; + } + + /** + * Get cookie params. + * + * @return array + */ + public static function getCookieParams() + { + return [ + 'lifetime' => static::$cookieLifetime, + 'path' => static::$cookiePath, + 'domain' => static::$domain, + 'secure' => static::$secure, + 'httponly' => static::$httpOnly, + 'samesite' => static::$sameSite, + ]; + } + + /** + * Init handler. + * + * @return void + */ + protected static function initHandler() + { + if (static::$_handlerConfig === null) { + static::$_handler = new static::$_handlerClass(); + } else { + static::$_handler = new static::$_handlerClass(static::$_handlerConfig); + } + } + + /** + * GC sessions. + * + * @return void + */ + public function gc() + { + static::$_handler->gc(static::$lifetime); + } + + /** + * __wakeup. + * + * @return void + */ + public function __wakeup() + { + $this->_isSafe = false; + } + + /** + * __destruct. + * + * @return void + */ + public function __destruct() + { + if (!$this->_isSafe) { + return; + } + $this->save(); + if (\random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) { + $this->gc(); + } + } + + /** + * Check session id. + * + * @param string $session_id + */ + protected static function checkSessionId($session_id) + { + if (!\preg_match('/^[a-zA-Z0-9"]+$/', $session_id)) { + throw new SessionException("session_id $session_id is invalid"); + } + } +} + +/** + * Class SessionException + * @package Workerman\Protocols\Http + */ +class SessionException extends \RuntimeException +{ + +} + +// Init session. +Session::init(); diff --git a/vendor/workerman/workerman/Protocols/Http/Session/FileSessionHandler.php b/vendor/workerman/workerman/Protocols/Http/Session/FileSessionHandler.php new file mode 100644 index 0000000..a7cefbd --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Session/FileSessionHandler.php @@ -0,0 +1,183 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http\Session; + +use Workerman\Protocols\Http\Session; + +/** + * Class FileSessionHandler + * @package Workerman\Protocols\Http\Session + */ +class FileSessionHandler implements SessionHandlerInterface +{ + /** + * Session save path. + * + * @var string + */ + protected static $_sessionSavePath = null; + + /** + * Session file prefix. + * + * @var string + */ + protected static $_sessionFilePrefix = 'session_'; + + /** + * Init. + */ + public static function init() { + $save_path = @\session_save_path(); + if (!$save_path || \strpos($save_path, 'tcp://') === 0) { + $save_path = \sys_get_temp_dir(); + } + static::sessionSavePath($save_path); + } + + /** + * FileSessionHandler constructor. + * @param array $config + */ + public function __construct($config = array()) { + if (isset($config['save_path'])) { + static::sessionSavePath($config['save_path']); + } + } + + /** + * {@inheritdoc} + */ + public function open($save_path, $name) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function read($session_id) + { + $session_file = static::sessionFile($session_id); + \clearstatcache(); + if (\is_file($session_file)) { + if (\time() - \filemtime($session_file) > Session::$lifetime) { + \unlink($session_file); + return ''; + } + $data = \file_get_contents($session_file); + return $data ? $data : ''; + } + return ''; + } + + /** + * {@inheritdoc} + */ + public function write($session_id, $session_data) + { + $temp_file = static::$_sessionSavePath . uniqid(bin2hex(random_bytes(8)), true); + if (!\file_put_contents($temp_file, $session_data)) { + return false; + } + return \rename($temp_file, static::sessionFile($session_id)); + } + + /** + * Update sesstion modify time. + * + * @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php + * @see https://www.php.net/manual/zh/function.touch.php + * + * @param string $id Session id. + * @param string $data Session Data. + * + * @return bool + */ + public function updateTimestamp($id, $data = "") + { + $session_file = static::sessionFile($id); + if (!file_exists($session_file)) { + return false; + } + // set file modify time to current time + $set_modify_time = \touch($session_file); + // clear file stat cache + \clearstatcache(); + return $set_modify_time; + } + + /** + * {@inheritdoc} + */ + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function destroy($session_id) + { + $session_file = static::sessionFile($session_id); + if (\is_file($session_file)) { + \unlink($session_file); + } + return true; + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) { + $time_now = \time(); + foreach (\glob(static::$_sessionSavePath . static::$_sessionFilePrefix . '*') as $file) { + if(\is_file($file) && $time_now - \filemtime($file) > $maxlifetime) { + \unlink($file); + } + } + } + + /** + * Get session file path. + * + * @param string $session_id + * @return string + */ + protected static function sessionFile($session_id) { + return static::$_sessionSavePath.static::$_sessionFilePrefix.$session_id; + } + + /** + * Get or set session file path. + * + * @param string $path + * @return string + */ + public static function sessionSavePath($path) { + if ($path) { + if ($path[\strlen($path)-1] !== DIRECTORY_SEPARATOR) { + $path .= DIRECTORY_SEPARATOR; + } + static::$_sessionSavePath = $path; + if (!\is_dir($path)) { + \mkdir($path, 0777, true); + } + } + return $path; + } +} + +FileSessionHandler::init(); \ No newline at end of file diff --git a/vendor/workerman/workerman/Protocols/Http/Session/RedisClusterSessionHandler.php b/vendor/workerman/workerman/Protocols/Http/Session/RedisClusterSessionHandler.php new file mode 100644 index 0000000..281759a --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Session/RedisClusterSessionHandler.php @@ -0,0 +1,46 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace Workerman\Protocols\Http\Session; + +use Workerman\Protocols\Http\Session; + +class RedisClusterSessionHandler extends RedisSessionHandler +{ + public function __construct($config) + { + $timeout = isset($config['timeout']) ? $config['timeout'] : 2; + $read_timeout = isset($config['read_timeout']) ? $config['read_timeout'] : $timeout; + $persistent = isset($config['persistent']) ? $config['persistent'] : false; + $auth = isset($config['auth']) ? $config['auth'] : ''; + if ($auth) { + $this->_redis = new \RedisCluster(null, $config['host'], $timeout, $read_timeout, $persistent, $auth); + } else { + $this->_redis = new \RedisCluster(null, $config['host'], $timeout, $read_timeout, $persistent); + } + if (empty($config['prefix'])) { + $config['prefix'] = 'redis_session_'; + } + $this->_redis->setOption(\Redis::OPT_PREFIX, $config['prefix']); + } + + /** + * {@inheritdoc} + */ + public function read($session_id) + { + return $this->_redis->get($session_id); + } + +} diff --git a/vendor/workerman/workerman/Protocols/Http/Session/RedisSessionHandler.php b/vendor/workerman/workerman/Protocols/Http/Session/RedisSessionHandler.php new file mode 100644 index 0000000..e1b5bd5 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Session/RedisSessionHandler.php @@ -0,0 +1,154 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http\Session; + +use Workerman\Protocols\Http\Session; +use Workerman\Timer; +use RedisException; + +/** + * Class RedisSessionHandler + * @package Workerman\Protocols\Http\Session + */ +class RedisSessionHandler implements SessionHandlerInterface +{ + + /** + * @var \Redis + */ + protected $_redis; + + /** + * @var array + */ + protected $_config; + + /** + * RedisSessionHandler constructor. + * @param array $config = [ + * 'host' => '127.0.0.1', + * 'port' => 6379, + * 'timeout' => 2, + * 'auth' => '******', + * 'database' => 2, + * 'prefix' => 'redis_session_', + * 'ping' => 55, + * ] + */ + public function __construct($config) + { + if (false === extension_loaded('redis')) { + throw new \RuntimeException('Please install redis extension.'); + } + + if (!isset($config['timeout'])) { + $config['timeout'] = 2; + } + + $this->_config = $config; + + $this->connect(); + + Timer::add(!empty($config['ping']) ? $config['ping'] : 55, function () { + $this->_redis->get('ping'); + }); + } + + public function connect() + { + $config = $this->_config; + + $this->_redis = new \Redis(); + if (false === $this->_redis->connect($config['host'], $config['port'], $config['timeout'])) { + throw new \RuntimeException("Redis connect {$config['host']}:{$config['port']} fail."); + } + if (!empty($config['auth'])) { + $this->_redis->auth($config['auth']); + } + if (!empty($config['database'])) { + $this->_redis->select($config['database']); + } + if (empty($config['prefix'])) { + $config['prefix'] = 'redis_session_'; + } + $this->_redis->setOption(\Redis::OPT_PREFIX, $config['prefix']); + } + + /** + * {@inheritdoc} + */ + public function open($save_path, $name) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function read($session_id) + { + try { + return $this->_redis->get($session_id); + } catch (RedisException $e) { + $msg = strtolower($e->getMessage()); + if ($msg === 'connection lost' || strpos($msg, 'went away')) { + $this->connect(); + return $this->_redis->get($session_id); + } + throw $e; + } + + } + + /** + * {@inheritdoc} + */ + public function write($session_id, $session_data) + { + return true === $this->_redis->setex($session_id, Session::$lifetime, $session_data); + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($id, $data = "") + { + return true === $this->_redis->expire($id, Session::$lifetime); + } + + /** + * {@inheritdoc} + */ + public function destroy($session_id) + { + $this->_redis->del($session_id); + return true; + } + + /** + * {@inheritdoc} + */ + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + return true; + } +} diff --git a/vendor/workerman/workerman/Protocols/Http/Session/SessionHandlerInterface.php b/vendor/workerman/workerman/Protocols/Http/Session/SessionHandlerInterface.php new file mode 100644 index 0000000..23a47f2 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/Session/SessionHandlerInterface.php @@ -0,0 +1,114 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols\Http\Session; + +interface SessionHandlerInterface +{ + /** + * Close the session + * @link http://php.net/manual/en/sessionhandlerinterface.close.php + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function close(); + + /** + * Destroy a session + * @link http://php.net/manual/en/sessionhandlerinterface.destroy.php + * @param string $session_id The session ID being destroyed. + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function destroy($session_id); + + /** + * Cleanup old sessions + * @link http://php.net/manual/en/sessionhandlerinterface.gc.php + * @param int $maxlifetime

+ * Sessions that have not updated for + * the last maxlifetime seconds will be removed. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function gc($maxlifetime); + + /** + * Initialize session + * @link http://php.net/manual/en/sessionhandlerinterface.open.php + * @param string $save_path The path where to store/retrieve the session. + * @param string $name The session name. + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function open($save_path, $name); + + + /** + * Read session data + * @link http://php.net/manual/en/sessionhandlerinterface.read.php + * @param string $session_id The session id to read data for. + * @return string

+ * Returns an encoded string of the read data. + * If nothing was read, it must return an empty string. + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function read($session_id); + + /** + * Write session data + * @link http://php.net/manual/en/sessionhandlerinterface.write.php + * @param string $session_id The session id. + * @param string $session_data

+ * The encoded session data. This data is the + * result of the PHP internally encoding + * the $_SESSION superglobal to a serialized + * string and passing it as this parameter. + * Please note sessions use an alternative serialization method. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function write($session_id, $session_data); + + /** + * Update sesstion modify time. + * + * @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php + * + * @param string $id Session id. + * @param string $data Session Data. + * + * @return bool + */ + public function updateTimestamp($id, $data = ""); + +} diff --git a/vendor/workerman/workerman/Protocols/Http/mime.types b/vendor/workerman/workerman/Protocols/Http/mime.types new file mode 100644 index 0000000..e6ccf0a --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Http/mime.types @@ -0,0 +1,90 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + + application/font-woff woff; + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; + application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; + font/ttf ttf; +} diff --git a/vendor/workerman/workerman/Protocols/ProtocolInterface.php b/vendor/workerman/workerman/Protocols/ProtocolInterface.php new file mode 100644 index 0000000..4fea87d --- /dev/null +++ b/vendor/workerman/workerman/Protocols/ProtocolInterface.php @@ -0,0 +1,52 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\ConnectionInterface; + +/** + * Protocol interface + */ +interface ProtocolInterface +{ + /** + * Check the integrity of the package. + * Please return the length of package. + * If length is unknow please return 0 that mean wating more data. + * If the package has something wrong please return false the connection will be closed. + * + * @param string $recv_buffer + * @param ConnectionInterface $connection + * @return int|false + */ + public static function input($recv_buffer, ConnectionInterface $connection); + + /** + * Decode package and emit onMessage($message) callback, $message is the result that decode returned. + * + * @param string $recv_buffer + * @param ConnectionInterface $connection + * @return mixed + */ + public static function decode($recv_buffer, ConnectionInterface $connection); + + /** + * Encode package brefore sending to client. + * + * @param mixed $data + * @param ConnectionInterface $connection + * @return string + */ + public static function encode($data, ConnectionInterface $connection); +} diff --git a/vendor/workerman/workerman/Protocols/Text.php b/vendor/workerman/workerman/Protocols/Text.php new file mode 100644 index 0000000..407ea2d --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Text.php @@ -0,0 +1,70 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\ConnectionInterface; + +/** + * Text Protocol. + */ +class Text +{ + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return int + */ + public static function input($buffer, ConnectionInterface $connection) + { + // Judge whether the package length exceeds the limit. + if (isset($connection->maxPackageSize) && \strlen($buffer) >= $connection->maxPackageSize) { + $connection->close(); + return 0; + } + // Find the position of "\n". + $pos = \strpos($buffer, "\n"); + // No "\n", packet length is unknown, continue to wait for the data so return 0. + if ($pos === false) { + return 0; + } + // Return the current package length. + return $pos + 1; + } + + /** + * Encode. + * + * @param string $buffer + * @return string + */ + public static function encode($buffer) + { + // Add "\n" + return $buffer . "\n"; + } + + /** + * Decode. + * + * @param string $buffer + * @return string + */ + public static function decode($buffer) + { + // Remove "\n" + return \rtrim($buffer, "\r\n"); + } +} \ No newline at end of file diff --git a/vendor/workerman/workerman/Protocols/Websocket.php b/vendor/workerman/workerman/Protocols/Websocket.php new file mode 100644 index 0000000..e161228 --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Websocket.php @@ -0,0 +1,562 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace Workerman\Protocols; + +use Workerman\Connection\ConnectionInterface; +use Workerman\Connection\TcpConnection; +use Workerman\Protocols\Http\Request; +use Workerman\Worker; + +/** + * WebSocket protocol. + */ +class Websocket implements \Workerman\Protocols\ProtocolInterface +{ + /** + * Websocket blob type. + * + * @var string + */ + const BINARY_TYPE_BLOB = "\x81"; + + /** + * Websocket blob type. + * + * @var string + */ + const BINARY_TYPE_BLOB_DEFLATE = "\xc1"; + + /** + * Websocket arraybuffer type. + * + * @var string + */ + const BINARY_TYPE_ARRAYBUFFER = "\x82"; + + /** + * Websocket arraybuffer type. + * + * @var string + */ + const BINARY_TYPE_ARRAYBUFFER_DEFLATE = "\xc2"; + + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return int + */ + public static function input($buffer, ConnectionInterface $connection) + { + // Receive length. + $recv_len = \strlen($buffer); + // We need more data. + if ($recv_len < 6) { + return 0; + } + + // Has not yet completed the handshake. + if (empty($connection->context->websocketHandshake)) { + return static::dealHandshake($buffer, $connection); + } + + // Buffer websocket frame data. + if ($connection->context->websocketCurrentFrameLength) { + // We need more frame data. + if ($connection->context->websocketCurrentFrameLength > $recv_len) { + // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. + return 0; + } + } else { + $first_byte = \ord($buffer[0]); + $second_byte = \ord($buffer[1]); + $data_len = $second_byte & 127; + $is_fin_frame = $first_byte >> 7; + $masked = $second_byte >> 7; + + if (!$masked) { + Worker::safeEcho("frame not masked so close the connection\n"); + $connection->close(); + return 0; + } + + $opcode = $first_byte & 0xf; + switch ($opcode) { + case 0x0: + break; + // Blob type. + case 0x1: + break; + // Arraybuffer type. + case 0x2: + break; + // Close package. + case 0x8: + // Try to emit onWebSocketClose callback. + $close_cb = $connection->onWebSocketClose ?? $connection->worker->onWebSocketClose ?? false; + if ($close_cb) { + try { + $close_cb($connection); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } // Close connection. + else { + $connection->close("\x88\x02\x03\xe8", true); + } + return 0; + // Ping package. + case 0x9: + break; + // Pong package. + case 0xa: + break; + // Wrong opcode. + default : + Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n"); + $connection->close(); + return 0; + } + + // Calculate packet length. + $head_len = 6; + if ($data_len === 126) { + $head_len = 8; + if ($head_len > $recv_len) { + return 0; + } + $pack = \unpack('nn/ntotal_len', $buffer); + $data_len = $pack['total_len']; + } else { + if ($data_len === 127) { + $head_len = 14; + if ($head_len > $recv_len) { + return 0; + } + $arr = \unpack('n/N2c', $buffer); + $data_len = $arr['c1'] * 4294967296 + $arr['c2']; + } + } + $current_frame_length = $head_len + $data_len; + + $total_package_size = \strlen($connection->context->websocketDataBuffer) + $current_frame_length; + if ($total_package_size > $connection->maxPackageSize) { + Worker::safeEcho("error package. package_length=$total_package_size\n"); + $connection->close(); + return 0; + } + + if ($is_fin_frame) { + if ($opcode === 0x9) { + if ($recv_len >= $current_frame_length) { + $ping_data = static::decode(\substr($buffer, 0, $current_frame_length), $connection); + $connection->consumeRecvBuffer($current_frame_length); + $tmp_connection_type = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB; + $connection->websocketType = "\x8a"; + $ping_cb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false; + if ($ping_cb) { + try { + $ping_cb($connection, $ping_data); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } else { + $connection->send($ping_data); + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(\substr($buffer, $current_frame_length), $connection); + } + } + return 0; + } else if ($opcode === 0xa) { + if ($recv_len >= $current_frame_length) { + $pong_data = static::decode(\substr($buffer, 0, $current_frame_length), $connection); + $connection->consumeRecvBuffer($current_frame_length); + $tmp_connection_type = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB; + $connection->websocketType = "\x8a"; + // Try to emit onWebSocketPong callback. + $pong_cb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false; + if ($pong_cb) { + try { + $pong_cb($connection, $pong_data); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(\substr($buffer, $current_frame_length), $connection); + } + } + return 0; + } + return $current_frame_length; + } else { + $connection->context->websocketCurrentFrameLength = $current_frame_length; + } + } + + // Received just a frame length data. + if ($connection->context->websocketCurrentFrameLength === $recv_len) { + static::decode($buffer, $connection); + $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); + $connection->context->websocketCurrentFrameLength = 0; + return 0; + } // The length of the received data is greater than the length of a frame. + elseif ($connection->context->websocketCurrentFrameLength < $recv_len) { + static::decode(\substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); + $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); + $current_frame_length = $connection->context->websocketCurrentFrameLength; + $connection->context->websocketCurrentFrameLength = 0; + // Continue to read next frame. + return static::input(\substr($buffer, $current_frame_length), $connection); + } // The length of the received data is less than the length of a frame. + else { + return 0; + } + } + + /** + * Websocket encode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function encode($buffer, ConnectionInterface $connection) + { + if (!is_scalar($buffer)) { + throw new \Exception("You can't send(" . \gettype($buffer) . ") to client, you need to convert it to a string. "); + } + + if (empty($connection->websocketType)) { + $connection->websocketType = static::BINARY_TYPE_BLOB; + } + + // permessage-deflate + if (\ord($connection->websocketType) & 64) { + $buffer = static::deflate($connection, $buffer); + } + + $first_byte = $connection->websocketType; + $len = \strlen($buffer); + + if ($len <= 125) { + $encode_buffer = $first_byte . \chr($len) . $buffer; + } else { + if ($len <= 65535) { + $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer; + } else { + $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer; + } + } + + // Handshake not completed so temporary buffer websocket data waiting for send. + if (empty($connection->context->websocketHandshake)) { + if (empty($connection->context->tmpWebsocketData)) { + $connection->context->tmpWebsocketData = ''; + } + // If buffer has already full then discard the current package. + if (\strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { + if ($connection->onError) { + try { + ($connection->onError)($connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + return ''; + } + $connection->context->tmpWebsocketData .= $encode_buffer; + // Check buffer is full. + if ($connection->maxSendBufferSize <= \strlen($connection->context->tmpWebsocketData)) { + if ($connection->onBufferFull) { + try { + ($connection->onBufferFull)($connection); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + } + // Return empty string. + return ''; + } + + return $encode_buffer; + } + + /** + * Websocket decode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function decode($buffer, ConnectionInterface $connection) + { + $first_byte = \ord($buffer[0]); + $second_byte = \ord($buffer[1]); + $len = $second_byte & 127; + $is_fin_frame = $first_byte >> 7; + $rsv1 = 64 === ($first_byte & 64); + + if ($len === 126) { + $masks = \substr($buffer, 4, 4); + $data = \substr($buffer, 8); + } else { + if ($len === 127) { + $masks = \substr($buffer, 10, 4); + $data = \substr($buffer, 14); + } else { + $masks = \substr($buffer, 2, 4); + $data = \substr($buffer, 6); + } + } + $dataLength = \strlen($data); + $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4); + $decoded = $data ^ $masks; + if ($connection->context->websocketCurrentFrameLength) { + $connection->context->websocketDataBuffer .= $decoded; + if ($rsv1) { + return static::inflate($connection, $connection->context->websocketDataBuffer, $is_fin_frame); + } + return $connection->context->websocketDataBuffer; + } else { + if ($connection->context->websocketDataBuffer !== '') { + $decoded = $connection->context->websocketDataBuffer . $decoded; + $connection->context->websocketDataBuffer = ''; + } + if ($rsv1) { + return static::inflate($connection, $decoded, $is_fin_frame); + } + return $decoded; + } + } + + /** + * Inflate. + * + * @param $connection + * @param $buffer + * @param $is_fin_frame + * @return false|string + */ + protected static function inflate($connection, $buffer, $is_fin_frame) + { + if (!isset($connection->context->inflator)) { + $connection->context->inflator = \inflate_init( + \ZLIB_ENCODING_RAW, + [ + 'level' => -1, + 'memory' => 8, + 'window' => 15, + 'strategy' => \ZLIB_DEFAULT_STRATEGY + ] + ); + } + if ($is_fin_frame) { + $buffer .= "\x00\x00\xff\xff"; + } + return \inflate_add($connection->context->inflator, $buffer); + } + + /** + * Deflate. + * + * @param $connection + * @param $buffer + * @return false|string + */ + protected static function deflate($connection, $buffer) + { + if (!isset($connection->context->deflator)) { + $connection->context->deflator = \deflate_init( + \ZLIB_ENCODING_RAW, + [ + 'level' => -1, + 'memory' => 8, + 'window' => 15, + 'strategy' => \ZLIB_DEFAULT_STRATEGY + ] + ); + } + return \substr(\deflate_add($connection->context->deflator, $buffer), 0, -4); + } + + /** + * Websocket handshake. + * + * @param string $buffer + * @param TcpConnection $connection + * @return int + */ + public static function dealHandshake($buffer, $connection) + { + // HTTP protocol. + if (0 === \strpos($buffer, 'GET')) { + // Find \r\n\r\n. + $header_end_pos = \strpos($buffer, "\r\n\r\n"); + if (!$header_end_pos) { + return 0; + } + $header_length = $header_end_pos + 4; + + // Get Sec-WebSocket-Key. + $Sec_WebSocket_Key = ''; + if (\preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) { + $Sec_WebSocket_Key = $match[1]; + } else { + $connection->close("HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

WebSocket


workerman
", true); + return 0; + } + // Calculation websocket key. + $new_key = \base64_encode(\sha1($Sec_WebSocket_Key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); + // Handshake response data. + $handshake_message = "HTTP/1.1 101 Switching Protocols\r\n" + . "Upgrade: websocket\r\n" + . "Sec-WebSocket-Version: 13\r\n" + . "Connection: Upgrade\r\n" + . "Sec-WebSocket-Accept: " . $new_key . "\r\n"; + + // Websocket data buffer. + $connection->context->websocketDataBuffer = ''; + // Current websocket frame length. + $connection->context->websocketCurrentFrameLength = 0; + // Current websocket frame data. + $connection->context->websocketCurrentFrameBuffer = ''; + // Consume handshake data. + $connection->consumeRecvBuffer($header_length); + + // Try to emit onWebSocketConnect callback. + $on_websocket_connect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false; + if ($on_websocket_connect) { + static::parseHttpHeader($buffer); + try { + \call_user_func($on_websocket_connect, $connection, $buffer); + } catch (\Exception $e) { + Worker::stopAll(250, $e); + } catch (\Error $e) { + Worker::stopAll(250, $e); + } + if (!empty($_SESSION) && \class_exists('\GatewayWorker\Lib\Context')) { + $connection->session = \GatewayWorker\Lib\Context::sessionEncode($_SESSION); + } + $_GET = $_SERVER = $_SESSION = $_COOKIE = array(); + } + + // blob or arraybuffer + if (empty($connection->websocketType)) { + $connection->websocketType = static::BINARY_TYPE_BLOB; + } + + $has_server_header = false; + + if (isset($connection->headers)) { + if (\is_array($connection->headers)) { + foreach ($connection->headers as $header) { + if (\stripos($header, 'Server:') === 0) { + $has_server_header = true; + } + $handshake_message .= "$header\r\n"; + } + } else { + if (\stripos($connection->headers, 'Server:') !== false) { + $has_server_header = true; + } + $handshake_message .= "$connection->headers\r\n"; + } + } + if (!$has_server_header) { + $handshake_message .= "Server: workerman/" . Worker::VERSION . "\r\n"; + } + $handshake_message .= "\r\n"; + // Send handshake response. + $connection->send($handshake_message, true); + // Mark handshake complete.. + $connection->context->websocketHandshake = true; + + // There are data waiting to be sent. + if (!empty($connection->context->tmpWebsocketData)) { + $connection->send($connection->context->tmpWebsocketData, true); + $connection->context->tmpWebsocketData = ''; + } + if (\strlen($buffer) > $header_length) { + return static::input(\substr($buffer, $header_length), $connection); + } + return 0; + } + // Bad websocket handshake request. + $connection->close("HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

400 Bad Request


workerman
", true); + return 0; + } + + /** + * Parse http header. + * + * @param string $buffer + * @return void + */ + protected static function parseHttpHeader($buffer) + { + // Parse headers. + list($http_header, ) = \explode("\r\n\r\n", $buffer, 2); + $header_data = \explode("\r\n", $http_header); + + if ($_SERVER) { + $_SERVER = array(); + } + + list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = \explode(' ', + $header_data[0]); + + unset($header_data[0]); + foreach ($header_data as $content) { + // \r\n\r\n + if (empty($content)) { + continue; + } + list($key, $value) = \explode(':', $content, 2); + $key = \str_replace('-', '_', \strtoupper($key)); + $value = \trim($value); + $_SERVER['HTTP_' . $key] = $value; + switch ($key) { + // HTTP_HOST + case 'HOST': + $tmp = \explode(':', $value); + $_SERVER['SERVER_NAME'] = $tmp[0]; + if (isset($tmp[1])) { + $_SERVER['SERVER_PORT'] = $tmp[1]; + } + break; + // cookie + case 'COOKIE': + \parse_str(\str_replace('; ', '&', $_SERVER['HTTP_COOKIE']), $_COOKIE); + break; + } + } + + // QUERY_STRING + $_SERVER['QUERY_STRING'] = \parse_url($_SERVER['REQUEST_URI'], \PHP_URL_QUERY); + if ($_SERVER['QUERY_STRING']) { + // $GET + \parse_str($_SERVER['QUERY_STRING'], $_GET); + } else { + $_SERVER['QUERY_STRING'] = ''; + } + } + +} diff --git a/vendor/workerman/workerman/Protocols/Ws.php b/vendor/workerman/workerman/Protocols/Ws.php new file mode 100644 index 0000000..dd1bd1d --- /dev/null +++ b/vendor/workerman/workerman/Protocols/Ws.php @@ -0,0 +1,432 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace Workerman\Protocols; + +use Workerman\Worker; +use Workerman\Timer; +use Workerman\Connection\TcpConnection; +use Workerman\Connection\ConnectionInterface; + +/** + * Websocket protocol for client. + */ +class Ws +{ + /** + * Websocket blob type. + * + * @var string + */ + const BINARY_TYPE_BLOB = "\x81"; + + /** + * Websocket arraybuffer type. + * + * @var string + */ + const BINARY_TYPE_ARRAYBUFFER = "\x82"; + + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return int + */ + public static function input($buffer, ConnectionInterface $connection) + { + if (empty($connection->context->handshakeStep)) { + Worker::safeEcho("recv data before handshake. Buffer:" . \bin2hex($buffer) . "\n"); + return false; + } + // Recv handshake response + if ($connection->context->handshakeStep === 1) { + return self::dealHandshake($buffer, $connection); + } + $recvLen = \strlen($buffer); + if ($recvLen < 2) { + return 0; + } + // Buffer websocket frame data. + if ($connection->context->websocketCurrentFrameLength) { + // We need more frame data. + if ($connection->context->websocketCurrentFrameLength > $recvLen) { + // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. + return 0; + } + } else { + + $firstbyte = \ord($buffer[0]); + $secondbyte = \ord($buffer[1]); + $dataLen = $secondbyte & 127; + $isFinFrame = $firstbyte >> 7; + $masked = $secondbyte >> 7; + + if ($masked) { + Worker::safeEcho("frame masked so close the connection\n"); + $connection->close(); + return 0; + } + + $opcode = $firstbyte & 0xf; + + switch ($opcode) { + case 0x0: + // Blob type. + case 0x1: + // Arraybuffer type. + case 0x2: + // Ping package. + case 0x9: + // Pong package. + case 0xa: + break; + // Close package. + case 0x8: + // Try to emit onWebSocketClose callback. + if (isset($connection->onWebSocketClose)) { + try { + ($connection->onWebSocketClose)($connection); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } // Close connection. + else { + $connection->close(); + } + return 0; + // Wrong opcode. + default : + Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n"); + $connection->close(); + return 0; + } + // Calculate packet length. + if ($dataLen === 126) { + if (\strlen($buffer) < 4) { + return 0; + } + $pack = \unpack('nn/ntotal_len', $buffer); + $currentFrameLength = $pack['total_len'] + 4; + } else if ($dataLen === 127) { + if (\strlen($buffer) < 10) { + return 0; + } + $arr = \unpack('n/N2c', $buffer); + $currentFrameLength = $arr['c1'] * 4294967296 + $arr['c2'] + 10; + } else { + $currentFrameLength = $dataLen + 2; + } + + $totalPackageSize = \strlen($connection->context->websocketDataBuffer) + $currentFrameLength; + if ($totalPackageSize > $connection->maxPackageSize) { + Worker::safeEcho("error package. package_length=$totalPackageSize\n"); + $connection->close(); + return 0; + } + + if ($isFinFrame) { + if ($opcode === 0x9) { + if ($recvLen >= $currentFrameLength) { + $pingData = static::decode(\substr($buffer, 0, $currentFrameLength), $connection); + $connection->consumeRecvBuffer($currentFrameLength); + $tmpConnectionType = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB; + $connection->websocketType = "\x8a"; + if (isset($connection->onWebSocketPing)) { + try { + ($connection->onWebSocketPing)($connection, $pingData); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } else { + $connection->send($pingData); + } + $connection->websocketType = $tmpConnectionType; + if ($recvLen > $currentFrameLength) { + return static::input(\substr($buffer, $currentFrameLength), $connection); + } + } + return 0; + + } else if ($opcode === 0xa) { + if ($recvLen >= $currentFrameLength) { + $pongData = static::decode(\substr($buffer, 0, $currentFrameLength), $connection); + $connection->consumeRecvBuffer($currentFrameLength); + $tmpConnectionType = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB; + $connection->websocketType = "\x8a"; + // Try to emit onWebSocketPong callback. + if (isset($connection->onWebSocketPong)) { + try { + ($connection->onWebSocketPong)($connection, $pongData); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + $connection->websocketType = $tmpConnectionType; + if ($recvLen > $currentFrameLength) { + return static::input(\substr($buffer, $currentFrameLength), $connection); + } + } + return 0; + } + return $currentFrameLength; + } else { + $connection->context->websocketCurrentFrameLength = $currentFrameLength; + } + } + // Received just a frame length data. + if ($connection->context->websocketCurrentFrameLength === $recvLen) { + self::decode($buffer, $connection); + $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); + $connection->context->websocketCurrentFrameLength = 0; + return 0; + } // The length of the received data is greater than the length of a frame. + elseif ($connection->context->websocketCurrentFrameLength < $recvLen) { + self::decode(\substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); + $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); + $currentFrameLength = $connection->context->websocketCurrentFrameLength; + $connection->context->websocketCurrentFrameLength = 0; + // Continue to read next frame. + return self::input(\substr($buffer, $currentFrameLength), $connection); + } // The length of the received data is less than the length of a frame. + else { + return 0; + } + } + + /** + * Websocket encode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function encode($payload, ConnectionInterface $connection) + { + if (empty($connection->websocketType)) { + $connection->websocketType = self::BINARY_TYPE_BLOB; + } + $payload = (string)$payload; + if (empty($connection->context->handshakeStep)) { + static::sendHandshake($connection); + } + + $maskKey = "\x00\x00\x00\x00"; + $length = \strlen($payload); + + if (strlen($payload) < 126) { + $head = chr(0x80 | $length); + } elseif ($length < 0xFFFF) { + $head = chr(0x80 | 126) . pack("n", $length); + } else { + $head = chr(0x80 | 127) . pack("N", 0) . pack("N", $length); + } + + $frame = $connection->websocketType . $head . $maskKey; + // append payload to frame: + $maskKey = \str_repeat($maskKey, \floor($length / 4)) . \substr($maskKey, 0, $length % 4); + $frame .= $payload ^ $maskKey; + if ($connection->context->handshakeStep === 1) { + // If buffer has already full then discard the current package. + if (\strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { + if ($connection->onError) { + try { + ($connection->onError)($connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + return ''; + } + $connection->context->tmpWebsocketData = $connection->context->tmpWebsocketData . $frame; + // Check buffer is full. + if ($connection->maxSendBufferSize <= \strlen($connection->context->tmpWebsocketData)) { + if ($connection->onBufferFull) { + try { + ($connection->onBufferFull)($connection); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + } + return ''; + } + return $frame; + } + + /** + * Websocket decode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function decode($bytes, ConnectionInterface $connection) + { + $dataLength = \ord($bytes[1]); + + if ($dataLength === 126) { + $decodedData = \substr($bytes, 4); + } else if ($dataLength === 127) { + $decodedData = \substr($bytes, 10); + } else { + $decodedData = \substr($bytes, 2); + } + if ($connection->context->websocketCurrentFrameLength) { + $connection->context->websocketDataBuffer .= $decodedData; + return $connection->context->websocketDataBuffer; + } else { + if ($connection->context->websocketDataBuffer !== '') { + $decodedData = $connection->context->websocketDataBuffer . $decodedData; + $connection->context->websocketDataBuffer = ''; + } + return $decodedData; + } + } + + /** + * Send websocket handshake data. + * + * @return void + */ + public static function onConnect($connection) + { + static::sendHandshake($connection); + } + + /** + * Clean + * + * @param TcpConnection $connection + */ + public static function onClose($connection) + { + $connection->context->handshakeStep = null; + $connection->context->websocketCurrentFrameLength = 0; + $connection->context->tmpWebsocketData = ''; + $connection->context->websocketDataBuffer = ''; + if (!empty($connection->context->websocketPingTimer)) { + Timer::del($connection->context->websocketPingTimer); + $connection->context->websocketPingTimer = null; + } + } + + /** + * Send websocket handshake. + * + * @param TcpConnection $connection + * @return void + */ + public static function sendHandshake(ConnectionInterface $connection) + { + if (!empty($connection->context->handshakeStep)) { + return; + } + // Get Host. + $port = $connection->getRemotePort(); + $host = $port === 80 || $port === 443 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port; + // Handshake header. + $connection->context->websocketSecKey = \base64_encode(random_bytes(16)); + $userHeader = $connection->headers ?? null; + $userHeaderStr = ''; + if (!empty($userHeader)) { + if (\is_array($userHeader)) { + foreach ($userHeader as $k => $v) { + $userHeaderStr .= "$k: $v\r\n"; + } + } else { + $userHeaderStr .= $userHeader; + } + $userHeaderStr = "\r\n" . \trim($userHeaderStr); + } + $header = 'GET ' . $connection->getRemoteURI() . " HTTP/1.1\r\n" . + (!\preg_match("/\nHost:/i", $userHeaderStr) ? "Host: $host\r\n" : '') . + "Connection: Upgrade\r\n" . + "Upgrade: websocket\r\n" . + (isset($connection->websocketOrigin) ? "Origin: " . $connection->websocketOrigin . "\r\n" : '') . + (isset($connection->websocketClientProtocol) ? "Sec-WebSocket-Protocol: " . $connection->websocketClientProtocol . "\r\n" : '') . + "Sec-WebSocket-Version: 13\r\n" . + "Sec-WebSocket-Key: " . $connection->context->websocketSecKey . $userHeaderStr . "\r\n\r\n"; + $connection->send($header, true); + $connection->context->handshakeStep = 1; + $connection->context->websocketCurrentFrameLength = 0; + $connection->context->websocketDataBuffer = ''; + $connection->context->tmpWebsocketData = ''; + } + + /** + * Websocket handshake. + * + * @param string $buffer + * @param TcpConnection $connection + * @return int + */ + public static function dealHandshake($buffer, ConnectionInterface $connection) + { + $pos = \strpos($buffer, "\r\n\r\n"); + if ($pos) { + //checking Sec-WebSocket-Accept + if (\preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) { + if ($match[1] !== \base64_encode(\sha1($connection->context->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) { + Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . \substr($buffer, 0, $pos) . "\n"); + $connection->close(); + return 0; + } + } else { + Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . \substr($buffer, 0, $pos) . "\n"); + $connection->close(); + return 0; + } + + // handshake complete + + // Get WebSocket subprotocol (if specified by server) + if (\preg_match("/Sec-WebSocket-Protocol: *(.*?)\r\n/i", $buffer, $match)) { + $connection->websocketServerProtocol = \trim($match[1]); + } + + $connection->context->handshakeStep = 2; + $handshakeResponseLength = $pos + 4; + // Try to emit onWebSocketConnect callback. + if (isset($connection->onWebSocketConnect)) { + try { + ($connection->onWebSocketConnect)($connection, \substr($buffer, 0, $handshakeResponseLength)); + } catch (\Throwable $e) { + Worker::stopAll(250, $e); + } + } + // Headbeat. + if (!empty($connection->websocketPingInterval)) { + $connection->context->websocketPingTimer = Timer::add($connection->websocketPingInterval, function () use ($connection) { + if (false === $connection->send(\pack('H*', '898000000000'), true)) { + Timer::del($connection->context->websocketPingTimer); + $connection->context->websocketPingTimer = null; + } + }); + } + + $connection->consumeRecvBuffer($handshakeResponseLength); + if (!empty($connection->context->tmpWebsocketData)) { + $connection->send($connection->context->tmpWebsocketData, true); + $connection->context->tmpWebsocketData = ''; + } + if (\strlen($buffer) > $handshakeResponseLength) { + return self::input(\substr($buffer, $handshakeResponseLength), $connection); + } + } + return 0; + } + +} diff --git a/vendor/workerman/workerman/README.md b/vendor/workerman/workerman/README.md new file mode 100644 index 0000000..6038c02 --- /dev/null +++ b/vendor/workerman/workerman/README.md @@ -0,0 +1,342 @@ +# Workerman +[![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) +[![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman) +[![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman) +[![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman) +[![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman) +[![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman) + +## What is it +Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. +Workerman supports HTTP, Websocket, SSL and other custom protocols. +Workerman supports event extension. + +## Requires +PHP 7.0 or Higher +A POSIX compatible operating system (Linux, OSX, BSD) +POSIX and PCNTL extensions required +Event extension recommended for better performance + +## Installation + +``` +composer require workerman/workerman +``` + +## Basic Usage + +### A websocket server +```php +onConnect = function ($connection) { + echo "New connection\n"; +}; + +// Emitted when data received +$ws_worker->onMessage = function ($connection, $data) { + // Send hello $data + $connection->send('Hello ' . $data); +}; + +// Emitted when connection closed +$ws_worker->onClose = function ($connection) { + echo "Connection closed\n"; +}; + +// Run worker +Worker::runAll(); +``` + +### An http server +```php +count = 4; + +// Emitted when data received +$http_worker->onMessage = function ($connection, $request) { + //$request->get(); + //$request->post(); + //$request->header(); + //$request->cookie(); + //$request->session(); + //$request->uri(); + //$request->path(); + //$request->method(); + + // Send data to client + $connection->send("Hello World"); +}; + +// Run all workers +Worker::runAll(); +``` + +### A tcp server +```php +count = 4; + +// Emitted when new connection come +$tcp_worker->onConnect = function ($connection) { + echo "New Connection\n"; +}; + +// Emitted when data received +$tcp_worker->onMessage = function ($connection, $data) { + // Send data to client + $connection->send("Hello $data \n"); +}; + +// Emitted when connection is closed +$tcp_worker->onClose = function ($connection) { + echo "Connection closed\n"; +}; + +Worker::runAll(); +``` + +### A udp server + +```php +count = 4; + +// Emitted when data received +$worker->onMessage = function($connection, $data) +{ + $connection->send($data); +}; + +Worker::runAll(); +``` + +### Enable SSL +```php + array( + 'local_cert' => '/your/path/of/server.pem', + 'local_pk' => '/your/path/of/server.key', + 'verify_peer' => false, + ) +); + +// Create a Websocket server with ssl context. +$ws_worker = new Worker('websocket://0.0.0.0:2346', $context); + +// Enable SSL. WebSocket+SSL means that Secure WebSocket (wss://). +// The similar approaches for Https etc. +$ws_worker->transport = 'ssl'; + +$ws_worker->onMessage = function ($connection, $data) { + // Send hello $data + $connection->send('Hello ' . $data); +}; + +Worker::runAll(); +``` + +### Custom protocol +Protocols/MyTextProtocol.php +```php +onConnect = function ($connection) { + echo "New connection\n"; +}; + +$text_worker->onMessage = function ($connection, $data) { + // Send data to client + $connection->send("Hello world\n"); +}; + +$text_worker->onClose = function ($connection) { + echo "Connection closed\n"; +}; + +// Run all workers +Worker::runAll(); +``` + +### Timer +```php +onWorkerStart = function ($task) { + // 2.5 seconds + $time_interval = 2.5; + $timer_id = Timer::add($time_interval, function () { + echo "Timer run\n"; + }); +}; + +// Run all workers +Worker::runAll(); +``` + +### AsyncTcpConnection (tcp/ws/text/frame etc...) +```php +onWorkerStart = function () { + // Websocket protocol for client. + $ws_connection = new AsyncTcpConnection('ws://echo.websocket.org:80'); + $ws_connection->onConnect = function ($connection) { + $connection->send('Hello'); + }; + $ws_connection->onMessage = function ($connection, $data) { + echo "Recv: $data\n"; + }; + $ws_connection->onError = function ($connection, $code, $msg) { + echo "Error: $msg\n"; + }; + $ws_connection->onClose = function ($connection) { + echo "Connection closed\n"; + }; + $ws_connection->connect(); +}; + +Worker::runAll(); +``` + + + +## Available commands +```php start.php start ``` +```php start.php start -d ``` +![workerman start](http://www.workerman.net/img/workerman-start.png) +```php start.php status ``` +![workerman satus](http://www.workerman.net/img/workerman-status.png?a=123) +```php start.php connections``` +```php start.php stop ``` +```php start.php restart ``` +```php start.php reload ``` + +## Documentation + +中文主页:[http://www.workerman.net](https://www.workerman.net) + +中文文档: [https://www.workerman.net/doc/workerman](https://www.workerman.net/doc/workerman) + +Documentation:[https://github.com/walkor/workerman-manual](https://github.com/walkor/workerman-manual/blob/master/english/SUMMARY.md) + +# Benchmarks +https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=db&l=yyku7z-e7&a=2 +![image](https://user-images.githubusercontent.com/6073368/146704320-1559fe97-aa67-4ee3-95d6-61e341b3c93b.png) + +## Sponsors +[opencollective.com/walkor](https://opencollective.com/walkor) + +[patreon.com/walkor](https://patreon.com/walkor) + +## Donate + + + +## Other links with workerman + +[webman](https://github.com/walkor/webman) +[PHPSocket.IO](https://github.com/walkor/phpsocket.io) +[php-socks5](https://github.com/walkor/php-socks5) +[php-http-proxy](https://github.com/walkor/php-http-proxy) + +## LICENSE + +Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt). diff --git a/vendor/workerman/workerman/Timer.php b/vendor/workerman/workerman/Timer.php new file mode 100644 index 0000000..9f152f3 --- /dev/null +++ b/vendor/workerman/workerman/Timer.php @@ -0,0 +1,220 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use \Exception; + +/** + * Timer. + * + * example: + * Workerman\Timer::add($time_interval, callback, array($arg1, $arg2..)); + */ +class Timer +{ + /** + * Tasks that based on ALARM signal. + * [ + * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], + * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], + * .. + * ] + * + * @var array + */ + protected static $_tasks = array(); + + /** + * event + * + * @var EventInterface + */ + protected static $_event = null; + + /** + * timer id + * + * @var int + */ + protected static $_timerId = 0; + + /** + * timer status + * [ + * timer_id1 => bool, + * timer_id2 => bool, + * ...................., + * ] + * + * @var array + */ + protected static $_status = array(); + + /** + * Init. + * + * @param EventInterface $event + * @return void + */ + public static function init($event = null) + { + if ($event) { + self::$_event = $event; + return; + } + if (\function_exists('pcntl_signal')) { + \pcntl_signal(\SIGALRM, array('\Workerman\Lib\Timer', 'signalHandle'), false); + } + } + + /** + * ALARM signal handler. + * + * @return void + */ + public static function signalHandle() + { + if (!self::$_event) { + \pcntl_alarm(1); + self::tick(); + } + } + + /** + * Add a timer. + * + * @param float $time_interval + * @param callable $func + * @param mixed $args + * @param bool $persistent + * @return int|bool + */ + public static function add($time_interval, $func, $args = array(), $persistent = true) + { + if ($time_interval <= 0) { + Worker::safeEcho(new Exception("bad time_interval")); + return false; + } + + if ($args === null) { + $args = array(); + } + + if (self::$_event) { + return self::$_event->add($time_interval, + $persistent ? EventInterface::EV_TIMER : EventInterface::EV_TIMER_ONCE, $func, $args); + } + + // If not workerman runtime just return. + if (!Worker::getAllWorkers()) { + return; + } + + if (!\is_callable($func)) { + Worker::safeEcho(new Exception("not callable")); + return false; + } + + if (empty(self::$_tasks)) { + \pcntl_alarm(1); + } + + $run_time = \time() + $time_interval; + if (!isset(self::$_tasks[$run_time])) { + self::$_tasks[$run_time] = array(); + } + + self::$_timerId = self::$_timerId == \PHP_INT_MAX ? 1 : ++self::$_timerId; + self::$_status[self::$_timerId] = true; + self::$_tasks[$run_time][self::$_timerId] = array($func, (array)$args, $persistent, $time_interval); + + return self::$_timerId; + } + + + /** + * Tick. + * + * @return void + */ + public static function tick() + { + if (empty(self::$_tasks)) { + \pcntl_alarm(0); + return; + } + $time_now = \time(); + foreach (self::$_tasks as $run_time => $task_data) { + if ($time_now >= $run_time) { + foreach ($task_data as $index => $one_task) { + $task_func = $one_task[0]; + $task_args = $one_task[1]; + $persistent = $one_task[2]; + $time_interval = $one_task[3]; + try { + \call_user_func_array($task_func, $task_args); + } catch (\Exception $e) { + Worker::safeEcho($e); + } + if($persistent && !empty(self::$_status[$index])) { + $new_run_time = \time() + $time_interval; + if(!isset(self::$_tasks[$new_run_time])) self::$_tasks[$new_run_time] = array(); + self::$_tasks[$new_run_time][$index] = array($task_func, (array)$task_args, $persistent, $time_interval); + } + } + unset(self::$_tasks[$run_time]); + } + } + } + + /** + * Remove a timer. + * + * @param mixed $timer_id + * @return bool + */ + public static function del($timer_id) + { + if (self::$_event) { + return self::$_event->del($timer_id, EventInterface::EV_TIMER); + } + + foreach(self::$_tasks as $run_time => $task_data) + { + if(array_key_exists($timer_id, $task_data)) unset(self::$_tasks[$run_time][$timer_id]); + } + + if(array_key_exists($timer_id, self::$_status)) unset(self::$_status[$timer_id]); + + return true; + } + + /** + * Remove all timers. + * + * @return void + */ + public static function delAll() + { + self::$_tasks = self::$_status = array(); + if (\function_exists('pcntl_alarm')) { + \pcntl_alarm(0); + } + if (self::$_event) { + self::$_event->clearAllTimer(); + } + } +} diff --git a/vendor/workerman/workerman/Worker.php b/vendor/workerman/workerman/Worker.php new file mode 100644 index 0000000..e789322 --- /dev/null +++ b/vendor/workerman/workerman/Worker.php @@ -0,0 +1,2756 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; +require_once __DIR__ . '/Lib/Constants.php'; + +use Workerman\Events\EventInterface; +use Workerman\Connection\ConnectionInterface; +use Workerman\Connection\TcpConnection; +use Workerman\Connection\UdpConnection; +use Workerman\Lib\Timer; +use Workerman\Events\Select; +use \Exception; + +/** + * Worker class + * A container for listening ports + */ +#[\AllowDynamicProperties] +class Worker +{ + /** + * Version. + * + * @var string + */ + const VERSION = '4.2.1'; + + /** + * Status starting. + * + * @var int + */ + const STATUS_STARTING = 1; + + /** + * Status running. + * + * @var int + */ + const STATUS_RUNNING = 2; + + /** + * Status shutdown. + * + * @var int + */ + const STATUS_SHUTDOWN = 4; + + /** + * Status reloading. + * + * @var int + */ + const STATUS_RELOADING = 8; + + /** + * Default backlog. Backlog is the maximum length of the queue of pending connections. + * + * @var int + */ + const DEFAULT_BACKLOG = 102400; + + /** + * Max udp package size. + * + * @var int + */ + const MAX_UDP_PACKAGE_SIZE = 65535; + + /** + * The safe distance for columns adjacent + * + * @var int + */ + const UI_SAFE_LENGTH = 4; + + /** + * Worker id. + * + * @var int + */ + public $id = 0; + + /** + * Name of the worker processes. + * + * @var string + */ + public $name = 'none'; + + /** + * Number of worker processes. + * + * @var int + */ + public $count = 1; + + /** + * Unix user of processes, needs appropriate privileges (usually root). + * + * @var string + */ + public $user = ''; + + /** + * Unix group of processes, needs appropriate privileges (usually root). + * + * @var string + */ + public $group = ''; + + /** + * reloadable. + * + * @var bool + */ + public $reloadable = true; + + /** + * reuse port. + * + * @var bool + */ + public $reusePort = false; + + /** + * Emitted when worker processes start. + * + * @var callable + */ + public $onWorkerStart = null; + + /** + * Emitted when a socket connection is successfully established. + * + * @var callable + */ + public $onConnect = null; + + /** + * Emitted when data is received. + * + * @var callable + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callable + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callable + */ + public $onError = null; + + /** + * Emitted when the send buffer becomes full. + * + * @var callable + */ + public $onBufferFull = null; + + /** + * Emitted when the send buffer becomes empty. + * + * @var callable + */ + public $onBufferDrain = null; + + /** + * Emitted when worker processes stopped. + * + * @var callable + */ + public $onWorkerStop = null; + + /** + * Emitted when worker processes get reload signal. + * + * @var callable + */ + public $onWorkerReload = null; + + /** + * Emitted when worker processes exited. + * + * @var callable + */ + public $onWorkerExit = null; + + /** + * Transport layer protocol. + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Store all connections of clients. + * + * @var array + */ + public $connections = array(); + + /** + * Application layer protocol. + * + * @var string + */ + public $protocol = null; + + /** + * Root path for autoload. + * + * @var string + */ + protected $_autoloadRootPath = ''; + + /** + * Pause accept new connections or not. + * + * @var bool + */ + protected $_pauseAccept = true; + + /** + * Is worker stopping ? + * @var bool + */ + public $stopping = false; + + /** + * Daemonize. + * + * @var bool + */ + public static $daemonize = false; + + /** + * Stdout file. + * + * @var string + */ + public static $stdoutFile = '/dev/null'; + + /** + * The file to store master process PID. + * + * @var string + */ + public static $pidFile = ''; + + /** + * The file used to store the master process status file. + * + * @var string + */ + public static $statusFile = ''; + + /** + * Log file. + * + * @var mixed + */ + public static $logFile = ''; + + /** + * Global event loop. + * + * @var EventInterface + */ + public static $globalEvent = null; + + /** + * Emitted when the master process get reload signal. + * + * @var callable + */ + public static $onMasterReload = null; + + /** + * Emitted when the master process terminated. + * + * @var callable + */ + public static $onMasterStop = null; + + /** + * EventLoopClass + * + * @var string + */ + public static $eventLoopClass = ''; + + /** + * Process title + * + * @var string + */ + public static $processTitle = 'WorkerMan'; + + /** + * After sending the stop command to the child process stopTimeout seconds, + * if the process is still living then forced to kill. + * + * @var int + */ + public static $stopTimeout = 2; + + /** + * The PID of master process. + * + * @var int + */ + protected static $_masterPid = 0; + + /** + * Listening socket. + * + * @var resource + */ + protected $_mainSocket = null; + + /** + * Socket name. The format is like this http://0.0.0.0:80 . + * + * @var string + */ + protected $_socketName = ''; + + /** parse from _socketName avoid parse again in master or worker + * LocalSocket The format is like tcp://0.0.0.0:8080 + * @var string + */ + + protected $_localSocket=null; + + /** + * Context of socket. + * + * @var resource + */ + protected $_context = null; + + /** + * All worker instances. + * + * @var Worker[] + */ + protected static $_workers = array(); + + /** + * All worker processes pid. + * The format is like this [worker_id=>[pid=>pid, pid=>pid, ..], ..] + * + * @var array + */ + protected static $_pidMap = array(); + + /** + * All worker processes waiting for restart. + * The format is like this [pid=>pid, pid=>pid]. + * + * @var array + */ + protected static $_pidsToRestart = array(); + + /** + * Mapping from PID to worker process ID. + * The format is like this [worker_id=>[0=>$pid, 1=>$pid, ..], ..]. + * + * @var array + */ + protected static $_idMap = array(); + + /** + * Current status. + * + * @var int + */ + protected static $_status = self::STATUS_STARTING; + + /** + * Maximum length of the worker names. + * + * @var int + */ + protected static $_maxWorkerNameLength = 12; + + /** + * Maximum length of the socket names. + * + * @var int + */ + protected static $_maxSocketNameLength = 12; + + /** + * Maximum length of the process user names. + * + * @var int + */ + protected static $_maxUserNameLength = 12; + + /** + * Maximum length of the Proto names. + * + * @var int + */ + protected static $_maxProtoNameLength = 4; + + /** + * Maximum length of the Processes names. + * + * @var int + */ + protected static $_maxProcessesNameLength = 9; + + /** + * Maximum length of the Status names. + * + * @var int + */ + protected static $_maxStatusNameLength = 1; + + /** + * The file to store status info of current worker process. + * + * @var string + */ + protected static $_statisticsFile = ''; + + /** + * Start file. + * + * @var string + */ + protected static $_startFile = ''; + + /** + * OS. + * + * @var string + */ + protected static $_OS = \OS_TYPE_LINUX; + + /** + * Processes for windows. + * + * @var array + */ + protected static $_processForWindows = array(); + + /** + * Status info of current worker process. + * + * @var array + */ + protected static $_globalStatistics = array( + 'start_timestamp' => 0, + 'worker_exit_info' => array() + ); + + /** + * Available event loops. + * + * @var array + */ + protected static $_availableEventLoops = array( + 'event' => '\Workerman\Events\Event', + 'libevent' => '\Workerman\Events\Libevent' + ); + + /** + * PHP built-in protocols. + * + * @var array + */ + protected static $_builtinTransports = array( + 'tcp' => 'tcp', + 'udp' => 'udp', + 'unix' => 'unix', + 'ssl' => 'tcp' + ); + + /** + * PHP built-in error types. + * + * @var array + */ + protected static $_errorType = array( + \E_ERROR => 'E_ERROR', // 1 + \E_WARNING => 'E_WARNING', // 2 + \E_PARSE => 'E_PARSE', // 4 + \E_NOTICE => 'E_NOTICE', // 8 + \E_CORE_ERROR => 'E_CORE_ERROR', // 16 + \E_CORE_WARNING => 'E_CORE_WARNING', // 32 + \E_COMPILE_ERROR => 'E_COMPILE_ERROR', // 64 + \E_COMPILE_WARNING => 'E_COMPILE_WARNING', // 128 + \E_USER_ERROR => 'E_USER_ERROR', // 256 + \E_USER_WARNING => 'E_USER_WARNING', // 512 + \E_USER_NOTICE => 'E_USER_NOTICE', // 1024 + \E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', // 4096 + \E_DEPRECATED => 'E_DEPRECATED', // 8192 + \E_USER_DEPRECATED => 'E_USER_DEPRECATED' // 16384 + ); + + /** + * Graceful stop or not. + * + * @var bool + */ + protected static $_gracefulStop = false; + + /** + * Standard output stream + * @var resource + */ + protected static $_outputStream = null; + + /** + * If $outputStream support decorated + * @var bool + */ + protected static $_outputDecorated = null; + + protected static $liveVersionLength = null; + + /** + * Run all worker instances. + * + * @return void + */ + public static function runAll() + { + static::checkSapiEnv(); + static::init(); + static::parseCommand(); + static::lock(); + static::daemonize(); + static::initWorkers(); + static::installSignal(); + static::saveMasterPid(); + static::lock(\LOCK_UN); + static::displayUI(); + static::forkWorkers(); + static::resetStd(); + static::monitorWorkers(); + } + + /** + * Check sapi. + * + * @return void + */ + protected static function checkSapiEnv() + { + // Only for cli and micro. + if (!in_array(\PHP_SAPI, ['cli', 'micro'])) { + exit("Only run in command line mode \n"); + } + if (\DIRECTORY_SEPARATOR === '\\') { + self::$_OS = \OS_TYPE_WINDOWS; + } + } + + /** + * Init. + * + * @return void + */ + protected static function init() + { + \set_error_handler(function($code, $msg, $file, $line){ + Worker::safeEcho("$msg in file $file on line $line\n"); + }); + + // Start file. + $backtrace = \debug_backtrace(); + static::$_startFile = $backtrace[\count($backtrace) - 1]['file']; + + + $unique_prefix = \str_replace('/', '_', static::$_startFile); + + // Pid file. + if (empty(static::$pidFile)) { + static::$pidFile = __DIR__ . "/../$unique_prefix.pid"; + } + + // Log file. + if (empty(static::$logFile)) { + static::$logFile = __DIR__ . '/../workerman.log'; + } + $log_file = (string)static::$logFile; + if (!\is_file($log_file)) { + \touch($log_file); + \chmod($log_file, 0622); + } + + // State. + static::$_status = static::STATUS_STARTING; + + // For statistics. + static::$_globalStatistics['start_timestamp'] = \time(); + + // Process title. + static::setProcessTitle(static::$processTitle . ': master process start_file=' . static::$_startFile); + + // Init data for worker id. + static::initId(); + + // Timer init. + Timer::init(); + } + + /** + * Lock. + * + * @return void + */ + protected static function lock($flag = \LOCK_EX) + { + static $fd; + if (\DIRECTORY_SEPARATOR !== '/') { + return; + } + $lock_file = static::$pidFile . '.lock'; + $fd = $fd ?: \fopen($lock_file, 'a+'); + if ($fd) { + flock($fd, $flag); + if ($flag === \LOCK_UN) { + fclose($fd); + $fd = null; + clearstatcache(); + if (\is_file($lock_file)) { + unlink($lock_file); + } + } + } + } + + /** + * Init All worker instances. + * + * @return void + */ + protected static function initWorkers() + { + if (static::$_OS !== \OS_TYPE_LINUX) { + return; + } + + static::$_statisticsFile = static::$statusFile ? static::$statusFile : __DIR__ . '/../workerman-' .posix_getpid().'.status'; + + foreach (static::$_workers as $worker) { + // Worker name. + if (empty($worker->name)) { + $worker->name = 'none'; + } + + // Get unix user of the worker process. + if (empty($worker->user)) { + $worker->user = static::getCurrentUser(); + } else { + if (\posix_getuid() !== 0 && $worker->user !== static::getCurrentUser()) { + static::log('Warning: You must have the root privileges to change uid and gid.'); + } + } + + // Socket name. + $worker->socket = $worker->getSocketName(); + + // Status name. + $worker->status = ' [OK] '; + + // Get column mapping for UI + foreach(static::getUiColumns() as $column_name => $prop){ + $prop_length = \strlen((string) static::getWorkerProperty($worker, $prop)); + static::updateMaxNameLength($column_name, $prop_length); + } + + // Listen. + if (!$worker->reusePort) { + $worker->listen(); + } + } + } + + /** + * @param Worker $worker + * @param string $prop + * @return mixed + */ + protected static function getWorkerProperty($worker, $prop) + { + switch ($prop) { + case 'transport': + return $worker->transport; + case 'user': + return $worker->user; + case 'name': + return $worker->name; + case 'socket': + return $worker->socket; + case 'count': + return $worker->count; + case 'status': + return $worker->status; + } + return null; + } + + /** + * Update specified column name length + * + * @param string $column_name + * @param int $length + * @return void + */ + protected static function updateMaxNameLength($column_name, $length) + { + switch ($column_name) { + case 'processes': + static::$_maxProcessesNameLength = max(static::$_maxProcessesNameLength, $length); + break; + case 'proto': + static::$_maxProtoNameLength = max(static::$_maxProtoNameLength, $length); + break; + case 'listen': + case 'socket': + static::$_maxSocketNameLength = max(static::$_maxSocketNameLength, $length); + break; + case 'status': + static::$_maxStatusNameLength = max(static::$_maxStatusNameLength, $length); + break; + case 'user': + static::$_maxUserNameLength = max(static::$_maxUserNameLength, $length); + break; + case 'worker': + static::$_maxWorkerNameLength = max(static::$_maxWorkerNameLength, $length); + break; + } + } + + /** + * @param string $column_name + * @return int + */ + protected static function getMaxNameLength($column_name) + { + switch ($column_name) { + case 'processes': + return static::$_maxProcessesNameLength; + case 'proto': + return static::$_maxProtoNameLength; + case 'listen': + case 'socket': + return static::$_maxSocketNameLength; + case 'status': + return static::$_maxStatusNameLength; + case 'user': + return static::$_maxUserNameLength; + case 'worker': + return static::$_maxWorkerNameLength; + } + return 0; + } + + /** + * Reload all worker instances. + * + * @return void + */ + public static function reloadAllWorkers() + { + static::init(); + static::initWorkers(); + static::displayUI(); + static::$_status = static::STATUS_RELOADING; + } + + /** + * Get all worker instances. + * + * @return array + */ + public static function getAllWorkers() + { + return static::$_workers; + } + + /** + * Get global event-loop instance. + * + * @return EventInterface + */ + public static function getEventLoop() + { + return static::$globalEvent; + } + + /** + * Get main socket resource + * @return resource + */ + public function getMainSocket(){ + return $this->_mainSocket; + } + + /** + * Init idMap. + * return void + */ + protected static function initId() + { + foreach (static::$_workers as $worker_id => $worker) { + $new_id_map = array(); + $worker->count = $worker->count < 1 ? 1 : $worker->count; + for($key = 0; $key < $worker->count; $key++) { + $new_id_map[$key] = isset(static::$_idMap[$worker_id][$key]) ? static::$_idMap[$worker_id][$key] : 0; + } + static::$_idMap[$worker_id] = $new_id_map; + } + } + + /** + * Get unix user of current porcess. + * + * @return string + */ + protected static function getCurrentUser() + { + $user_info = \posix_getpwuid(\posix_getuid()); + return $user_info['name'] ?? 'unknown'; + } + + /** + * Display staring UI. + * + * @return void + */ + protected static function displayUI() + { + global $argv; + if (\in_array('-q', $argv)) { + return; + } + if (static::$_OS !== \OS_TYPE_LINUX) { + static::safeEcho("---------------------------------------------- WORKERMAN -----------------------------------------------\r\n"); + static::safeEcho('Workerman version:'. static::VERSION. ' PHP version:'. \PHP_VERSION. "\r\n"); + static::safeEcho("----------------------------------------------- WORKERS ------------------------------------------------\r\n"); + static::safeEcho("worker listen processes status\r\n"); + return; + } + + //show version + $line_version = 'Workerman version:' . static::VERSION . \str_pad('PHP version:', 22, ' ', \STR_PAD_LEFT) . \PHP_VERSION; + $line_version .= \str_pad('Event-Loop:', 22, ' ', \STR_PAD_LEFT) . static::getEventLoopName() . \PHP_EOL; + if (static::$liveVersionLength === null) { + static::$liveVersionLength = \strlen($line_version); + } + $total_length = static::getSingleLineTotalLength(); + $line_one = '' . \str_pad(' WORKERMAN ', $total_length + \strlen(''), '-', \STR_PAD_BOTH) . ''. \PHP_EOL; + $line_two = \str_pad(' WORKERS ' , $total_length + \strlen(''), '-', \STR_PAD_BOTH) . \PHP_EOL; + static::safeEcho($line_one . $line_version . $line_two); + + //Show title + $title = ''; + foreach(static::getUiColumns() as $column_name => $prop){ + $length = static::getMaxNameLength($column_name); + //just keep compatible with listen name + $column_name === 'socket' && $column_name = 'listen'; + $title.= "{$column_name}" . \str_pad('', $length + static::UI_SAFE_LENGTH - \strlen($column_name)); + } + $title && static::safeEcho($title . \PHP_EOL); + + //Show content + foreach (static::$_workers as $worker) { + $content = ''; + foreach(static::getUiColumns() as $column_name => $prop){ + \preg_match_all("/(|<\/n>||<\/w>||<\/g>)/is", (string) static::getWorkerProperty($worker, $prop), $matches); + $place_holder_length = !empty($matches) ? \strlen(\implode('', $matches[0])) : 0; + $content .= \str_pad((string) static::getWorkerProperty($worker, $prop), static::getMaxNameLength($column_name) + static::UI_SAFE_LENGTH + $place_holder_length); + } + $content && static::safeEcho($content . \PHP_EOL); + } + + //Show last line + $line_last = \str_pad('', static::getSingleLineTotalLength(), '-') . \PHP_EOL; + !empty($content) && static::safeEcho($line_last); + + if (static::$daemonize) { + $tmpArgv = $argv; + foreach ($tmpArgv as $index => $value) { + if ($value == '-d') { + unset($tmpArgv[$index]); + } elseif ($value == 'start' || $value == 'restart') { + $tmpArgv[$index] = 'stop'; + } + } + static::safeEcho("Input \"php ".implode(' ', $tmpArgv)."\" to stop. Start success.\n\n"); + } else { + static::safeEcho("Press Ctrl+C to stop. Start success.\n"); + } + } + + /** + * Get UI columns to be shown in terminal + * + * 1. $column_map: array('ui_column_name' => 'clas_property_name') + * 2. Consider move into configuration in future + * + * @return array + */ + public static function getUiColumns() + { + return array( + 'proto' => 'transport', + 'user' => 'user', + 'worker' => 'name', + 'socket' => 'socket', + 'processes' => 'count', + 'status' => 'status', + ); + } + + /** + * Get single line total length for ui + * + * @return int + */ + public static function getSingleLineTotalLength() + { + $total_length = 0; + + foreach(static::getUiColumns() as $column_name => $prop){ + $total_length += static::getMaxNameLength($column_name) + static::UI_SAFE_LENGTH; + } + + //keep beauty when show less colums + if (static::$liveVersionLength === null) { + static::$liveVersionLength = 0; + } + $total_length <= static::$liveVersionLength && $total_length = static::$liveVersionLength; + + return $total_length; + } + + /** + * Parse command. + * + * @return void + */ + protected static function parseCommand() + { + if (static::$_OS !== \OS_TYPE_LINUX) { + return; + } + global $argv; + // Check argv; + $start_file = $argv[0]; + $usage = "Usage: php yourfile [mode]\nCommands: \nstart\t\tStart worker in DEBUG mode.\n\t\tUse mode -d to start in DAEMON mode.\nstop\t\tStop worker.\n\t\tUse mode -g to stop gracefully.\nrestart\t\tRestart workers.\n\t\tUse mode -d to start in DAEMON mode.\n\t\tUse mode -g to stop gracefully.\nreload\t\tReload codes.\n\t\tUse mode -g to reload gracefully.\nstatus\t\tGet worker status.\n\t\tUse mode -d to show live status.\nconnections\tGet worker connections.\n"; + $available_commands = array( + 'start', + 'stop', + 'restart', + 'reload', + 'status', + 'connections', + ); + $available_mode = array( + '-d', + '-g' + ); + $command = $mode = ''; + foreach ($argv as $value) { + if (\in_array($value, $available_commands)) { + $command = $value; + } elseif (\in_array($value, $available_mode)) { + $mode = $value; + } + } + + if (!$command) { + exit($usage); + } + + // Start command. + $mode_str = ''; + if ($command === 'start') { + if ($mode === '-d' || static::$daemonize) { + $mode_str = 'in DAEMON mode'; + } else { + $mode_str = 'in DEBUG mode'; + } + } + static::log("Workerman[$start_file] $command $mode_str"); + + // Get master process PID. + $master_pid = \is_file(static::$pidFile) ? (int)\file_get_contents(static::$pidFile) : 0; + // Master is still alive? + if (static::checkMasterIsAlive($master_pid)) { + if ($command === 'start') { + static::log("Workerman[$start_file] already running"); + exit; + } + } elseif ($command !== 'start' && $command !== 'restart') { + static::log("Workerman[$start_file] not run"); + exit; + } + + $statistics_file = static::$statusFile ? static::$statusFile : __DIR__ . "/../workerman-$master_pid.status"; + + // execute command. + switch ($command) { + case 'start': + if ($mode === '-d') { + static::$daemonize = true; + } + break; + case 'status': + while (1) { + if (\is_file($statistics_file)) { + @\unlink($statistics_file); + } + // Master process will send SIGIOT signal to all child processes. + \posix_kill($master_pid, SIGIOT); + // Sleep 1 second. + \sleep(1); + // Clear terminal. + if ($mode === '-d') { + static::safeEcho("\33[H\33[2J\33(B\33[m", true); + } + // Echo status data. + static::safeEcho(static::formatStatusData($statistics_file)); + if ($mode !== '-d') { + exit(0); + } + static::safeEcho("\nPress Ctrl+C to quit.\n\n"); + } + exit(0); + case 'connections': + if (\is_file($statistics_file) && \is_writable($statistics_file)) { + \unlink($statistics_file); + } + // Master process will send SIGIO signal to all child processes. + \posix_kill($master_pid, SIGIO); + // Waiting amoment. + \usleep(500000); + // Display statisitcs data from a disk file. + if(\is_readable($statistics_file)) { + \readfile($statistics_file); + } + exit(0); + case 'restart': + case 'stop': + if ($mode === '-g') { + static::$_gracefulStop = true; + $sig = \SIGQUIT; + static::log("Workerman[$start_file] is gracefully stopping ..."); + } else { + static::$_gracefulStop = false; + $sig = \SIGINT; + static::log("Workerman[$start_file] is stopping ..."); + } + // Send stop signal to master process. + $master_pid && \posix_kill($master_pid, $sig); + // Timeout. + $timeout = static::$stopTimeout + 3; + $start_time = \time(); + // Check master process is still alive? + while (1) { + $master_is_alive = $master_pid && \posix_kill((int) $master_pid, 0); + if ($master_is_alive) { + // Timeout? + if (!static::$_gracefulStop && \time() - $start_time >= $timeout) { + static::log("Workerman[$start_file] stop fail"); + exit; + } + // Waiting amoment. + \usleep(10000); + continue; + } + // Stop success. + static::log("Workerman[$start_file] stop success"); + if ($command === 'stop') { + exit(0); + } + if ($mode === '-d') { + static::$daemonize = true; + } + break; + } + break; + case 'reload': + if($mode === '-g'){ + $sig = \SIGUSR2; + }else{ + $sig = \SIGUSR1; + } + \posix_kill($master_pid, $sig); + exit; + default : + if (isset($command)) { + static::safeEcho('Unknown command: ' . $command . "\n"); + } + exit($usage); + } + } + + /** + * Format status data. + * + * @param $statistics_file + * @return string + */ + protected static function formatStatusData($statistics_file) + { + static $total_request_cache = array(); + if (!\is_readable($statistics_file)) { + return ''; + } + $info = \file($statistics_file, \FILE_IGNORE_NEW_LINES); + if (!$info) { + return ''; + } + $status_str = ''; + $current_total_request = array(); + $workerInfo = []; + try { + $workerInfo = unserialize($info[0], ['allowed_classes' => false]); + } catch (Throwable $exception) {} + if (!is_array($workerInfo)) { + $workerInfo = []; + } + \ksort($workerInfo, SORT_NUMERIC); + unset($info[0]); + $data_waiting_sort = array(); + $read_process_status = false; + $total_requests = 0; + $total_qps = 0; + $total_connections = 0; + $total_fails = 0; + $total_memory = 0; + $total_timers = 0; + $maxLen1 = static::$_maxSocketNameLength; + $maxLen2 = static::$_maxWorkerNameLength; + foreach($info as $key => $value) { + if (!$read_process_status) { + $status_str .= $value . "\n"; + if (\preg_match('/^pid.*?memory.*?listening/', $value)) { + $read_process_status = true; + } + continue; + } + if(\preg_match('/^[0-9]+/', $value, $pid_math)) { + $pid = $pid_math[0]; + $data_waiting_sort[$pid] = $value; + if(\preg_match('/^\S+?\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?/', $value, $match)) { + $total_memory += \intval(\str_ireplace('M','',$match[1])); + $maxLen1 = \max($maxLen1,\strlen($match[2])); + $maxLen2 = \max($maxLen2,\strlen($match[3])); + $total_connections += \intval($match[4]); + $total_fails += \intval($match[5]); + $total_timers += \intval($match[6]); + $current_total_request[$pid] = $match[7]; + $total_requests += \intval($match[7]); + } + } + } + foreach($workerInfo as $pid => $info) { + if (!isset($data_waiting_sort[$pid])) { + $status_str .= "$pid\t" . \str_pad('N/A', 7) . " " + . \str_pad($info['listen'], static::$_maxSocketNameLength) . " " + . \str_pad($info['name'], static::$_maxWorkerNameLength) . " " + . \str_pad('N/A', 11) . " " . \str_pad('N/A', 9) . " " + . \str_pad('N/A', 7) . " " . \str_pad('N/A', 13) . " N/A [busy] \n"; + continue; + } + //$qps = isset($total_request_cache[$pid]) ? $current_total_request[$pid] + if (!isset($total_request_cache[$pid]) || !isset($current_total_request[$pid])) { + $qps = 0; + } else { + $qps = $current_total_request[$pid] - $total_request_cache[$pid]; + $total_qps += $qps; + } + $status_str .= $data_waiting_sort[$pid]. " " . \str_pad($qps, 6) ." [idle]\n"; + } + $total_request_cache = $current_total_request; + $status_str .= "----------------------------------------------PROCESS STATUS---------------------------------------------------\n"; + $status_str .= "Summary\t" . \str_pad($total_memory.'M', 7) . " " + . \str_pad('-', $maxLen1) . " " + . \str_pad('-', $maxLen2) . " " + . \str_pad($total_connections, 11) . " " . \str_pad($total_fails, 9) . " " + . \str_pad($total_timers, 7) . " " . \str_pad($total_requests, 13) . " " + . \str_pad($total_qps,6)." [Summary] \n"; + return $status_str; + } + + + /** + * Install signal handler. + * + * @return void + */ + protected static function installSignal() + { + if (static::$_OS !== \OS_TYPE_LINUX) { + return; + } + $signalHandler = '\Workerman\Worker::signalHandler'; + // stop + \pcntl_signal(\SIGINT, $signalHandler, false); + // stop + \pcntl_signal(\SIGTERM, $signalHandler, false); + // stop + \pcntl_signal(\SIGHUP, $signalHandler, false); + // stop + \pcntl_signal(\SIGTSTP, $signalHandler, false); + // graceful stop + \pcntl_signal(\SIGQUIT, $signalHandler, false); + // reload + \pcntl_signal(\SIGUSR1, $signalHandler, false); + // graceful reload + \pcntl_signal(\SIGUSR2, $signalHandler, false); + // status + \pcntl_signal(\SIGIOT, $signalHandler, false); + // connection status + \pcntl_signal(\SIGIO, $signalHandler, false); + // ignore + \pcntl_signal(\SIGPIPE, \SIG_IGN, false); + } + + /** + * Reinstall signal handler. + * + * @return void + */ + protected static function reinstallSignal() + { + if (static::$_OS !== \OS_TYPE_LINUX) { + return; + } + $signalHandler = '\Workerman\Worker::signalHandler'; + // uninstall stop signal handler + \pcntl_signal(\SIGINT, \SIG_IGN, false); + // uninstall stop signal handler + \pcntl_signal(\SIGTERM, \SIG_IGN, false); + // uninstall stop signal handler + \pcntl_signal(\SIGHUP, \SIG_IGN, false); + // uninstall stop signal handler + \pcntl_signal(\SIGTSTP, \SIG_IGN, false); + // uninstall graceful stop signal handler + \pcntl_signal(\SIGQUIT, \SIG_IGN, false); + // uninstall reload signal handler + \pcntl_signal(\SIGUSR1, \SIG_IGN, false); + // uninstall graceful reload signal handler + \pcntl_signal(\SIGUSR2, \SIG_IGN, false); + // uninstall status signal handler + \pcntl_signal(\SIGIOT, \SIG_IGN, false); + // uninstall connections status signal handler + \pcntl_signal(\SIGIO, \SIG_IGN, false); + // reinstall stop signal handler + static::$globalEvent->add(\SIGINT, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall graceful stop signal handler + static::$globalEvent->add(\SIGQUIT, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall graceful stop signal handler + static::$globalEvent->add(\SIGHUP, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall graceful stop signal handler + static::$globalEvent->add(\SIGTSTP, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall reload signal handler + static::$globalEvent->add(\SIGUSR1, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall graceful reload signal handler + static::$globalEvent->add(\SIGUSR2, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall status signal handler + static::$globalEvent->add(\SIGIOT, EventInterface::EV_SIGNAL, $signalHandler); + // reinstall connection status signal handler + static::$globalEvent->add(\SIGIO, EventInterface::EV_SIGNAL, $signalHandler); + } + + /** + * Signal handler. + * + * @param int $signal + */ + public static function signalHandler($signal) + { + switch ($signal) { + // Stop. + case \SIGINT: + case \SIGTERM: + case \SIGHUP: + case \SIGTSTP: + static::$_gracefulStop = false; + static::stopAll(); + break; + // Graceful stop. + case \SIGQUIT: + static::$_gracefulStop = true; + static::stopAll(); + break; + // Reload. + case \SIGUSR2: + case \SIGUSR1: + if (static::$_status === static::STATUS_SHUTDOWN || static::$_status === static::STATUS_RELOADING) { + return; + } + static::$_gracefulStop = $signal === \SIGUSR2; + static::$_pidsToRestart = static::getAllWorkerPids(); + static::reload(); + break; + // Show status. + case \SIGIOT: + static::writeStatisticsToStatusFile(); + break; + // Show connection status. + case \SIGIO: + static::writeConnectionsStatisticsToStatusFile(); + break; + } + } + + /** + * Run as daemon mode. + * + * @throws Exception + */ + protected static function daemonize() + { + if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) { + return; + } + \umask(0); + $pid = \pcntl_fork(); + if (-1 === $pid) { + throw new Exception('Fork fail'); + } elseif ($pid > 0) { + exit(0); + } + if (-1 === \posix_setsid()) { + throw new Exception("Setsid fail"); + } + // Fork again avoid SVR4 system regain the control of terminal. + $pid = \pcntl_fork(); + if (-1 === $pid) { + throw new Exception("Fork fail"); + } elseif (0 !== $pid) { + exit(0); + } + } + + /** + * Redirect standard input and output. + * + * @throws Exception + */ + public static function resetStd() + { + if (!static::$daemonize || \DIRECTORY_SEPARATOR !== '/') { + return; + } + global $STDOUT, $STDERR; + $handle = \fopen(static::$stdoutFile, "a"); + if ($handle) { + unset($handle); + \set_error_handler(function(){}); + if ($STDOUT) { + \fclose($STDOUT); + } + if ($STDERR) { + \fclose($STDERR); + } + if (\is_resource(\STDOUT)) { + \fclose(\STDOUT); + } + if (\is_resource(\STDERR)) { + \fclose(\STDERR); + } + $STDOUT = \fopen(static::$stdoutFile, "a"); + $STDERR = \fopen(static::$stdoutFile, "a"); + // Fix standard output cannot redirect of PHP 8.1.8's bug + if (\function_exists('posix_isatty') && \posix_isatty(2)) { + \ob_start(function ($string) { + \file_put_contents(static::$stdoutFile, $string, FILE_APPEND); + }, 1); + } + // change output stream + static::$_outputStream = null; + static::outputStream($STDOUT); + \restore_error_handler(); + return; + } + + throw new Exception('Can not open stdoutFile ' . static::$stdoutFile); + } + + /** + * Save pid. + * + * @throws Exception + */ + protected static function saveMasterPid() + { + if (static::$_OS !== \OS_TYPE_LINUX) { + return; + } + + static::$_masterPid = \posix_getpid(); + if (false === \file_put_contents(static::$pidFile, static::$_masterPid)) { + throw new Exception('can not save pid to ' . static::$pidFile); + } + } + + /** + * Get event loop name. + * + * @return string + */ + protected static function getEventLoopName() + { + if (static::$eventLoopClass) { + return static::$eventLoopClass; + } + + if (!\class_exists('\Swoole\Event', false)) { + unset(static::$_availableEventLoops['swoole']); + } + + $loop_name = ''; + foreach (static::$_availableEventLoops as $name=>$class) { + if (\extension_loaded($name)) { + $loop_name = $name; + break; + } + } + + if ($loop_name) { + static::$eventLoopClass = static::$_availableEventLoops[$loop_name]; + } else { + static::$eventLoopClass = '\Workerman\Events\Select'; + } + return static::$eventLoopClass; + } + + /** + * Get all pids of worker processes. + * + * @return array + */ + protected static function getAllWorkerPids() + { + $pid_array = array(); + foreach (static::$_pidMap as $worker_pid_array) { + foreach ($worker_pid_array as $worker_pid) { + $pid_array[$worker_pid] = $worker_pid; + } + } + return $pid_array; + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkers() + { + if (static::$_OS === \OS_TYPE_LINUX) { + static::forkWorkersForLinux(); + } else { + static::forkWorkersForWindows(); + } + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkersForLinux() + { + + foreach (static::$_workers as $worker) { + if (static::$_status === static::STATUS_STARTING) { + if (empty($worker->name)) { + $worker->name = $worker->getSocketName(); + } + $worker_name_length = \strlen($worker->name); + if (static::$_maxWorkerNameLength < $worker_name_length) { + static::$_maxWorkerNameLength = $worker_name_length; + } + } + + while (\count(static::$_pidMap[$worker->workerId]) < $worker->count) { + static::forkOneWorkerForLinux($worker); + } + } + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkersForWindows() + { + $files = static::getStartFilesForWindows(); + global $argv; + if(\in_array('-q', $argv) || \count($files) === 1) + { + if(\count(static::$_workers) > 1) + { + static::safeEcho("@@@ Error: multi workers init in one php file are not support @@@\r\n"); + static::safeEcho("@@@ See http://doc.workerman.net/faq/multi-woker-for-windows.html @@@\r\n"); + } + elseif(\count(static::$_workers) <= 0) + { + exit("@@@no worker inited@@@\r\n\r\n"); + } + + \reset(static::$_workers); + /** @var Worker $worker */ + $worker = current(static::$_workers); + + \Workerman\Timer::delAll(); + + //Update process state. + static::$_status = static::STATUS_RUNNING; + + // Register shutdown function for checking errors. + \register_shutdown_function([__CLASS__, 'checkErrors']); + + // Create a global event loop. + if (!static::$globalEvent) { + $eventLoopClass = static::getEventLoopName(); + static::$globalEvent = new $eventLoopClass; + } + + // Reinstall signal. + static::reinstallSignal(); + + // Init Timer. + Timer::init(static::$globalEvent); + + \restore_error_handler(); + + // Add an empty timer to prevent the event-loop from exiting. + Timer::add(1000000, function (){}); + + // Display UI. + static::safeEcho(\str_pad($worker->name, 48) . \str_pad($worker->getSocketName(), 36) . \str_pad('1', 10) . " [ok]\n"); + $worker->listen(); + $worker->run(); + static::$globalEvent->loop(); + if (static::$_status !== self::STATUS_SHUTDOWN) { + $err = new Exception('event-loop exited'); + static::log($err); + exit(250); + } + exit(0); + } + else + { + static::$globalEvent = new \Workerman\Events\Select(); + Timer::init(static::$globalEvent); + foreach($files as $start_file) + { + static::forkOneWorkerForWindows($start_file); + } + } + } + + /** + * Get start files for windows. + * + * @return array + */ + public static function getStartFilesForWindows() { + global $argv; + $files = array(); + foreach($argv as $file) + { + if(\is_file($file)) + { + $files[$file] = $file; + } + } + return $files; + } + + /** + * Fork one worker process. + * + * @param string $start_file + */ + public static function forkOneWorkerForWindows($start_file) + { + $start_file = \realpath($start_file); + + $descriptorspec = array( + STDIN, STDOUT, STDOUT + ); + + $pipes = array(); + $process = \proc_open("php \"$start_file\" -q", $descriptorspec, $pipes); + + if (empty(static::$globalEvent)) { + static::$globalEvent = new Select(); + Timer::init(static::$globalEvent); + } + + // 保存子进程句柄 + static::$_processForWindows[$start_file] = array($process, $start_file); + } + + /** + * check worker status for windows. + * @return void + */ + public static function checkWorkerStatusForWindows() + { + foreach(static::$_processForWindows as $process_data) + { + $process = $process_data[0]; + $start_file = $process_data[1]; + $status = \proc_get_status($process); + if(isset($status['running'])) + { + if(!$status['running']) + { + static::safeEcho("process $start_file terminated and try to restart\n"); + \proc_close($process); + static::forkOneWorkerForWindows($start_file); + } + } + else + { + static::safeEcho("proc_get_status fail\n"); + } + } + } + + + /** + * Fork one worker process. + * + * @param self $worker + * @throws Exception + */ + protected static function forkOneWorkerForLinux(self $worker) + { + // Get available worker id. + $id = static::getId($worker->workerId, 0); + if ($id === false) { + return; + } + $pid = \pcntl_fork(); + // For master process. + if ($pid > 0) { + static::$_pidMap[$worker->workerId][$pid] = $pid; + static::$_idMap[$worker->workerId][$id] = $pid; + } // For child processes. + elseif (0 === $pid) { + \srand(); + \mt_srand(); + static::$_gracefulStop = false; + if (static::$_status === static::STATUS_STARTING) { + static::resetStd(); + } + static::$_pidMap = array(); + // Remove other listener. + foreach(static::$_workers as $key => $one_worker) { + if ($one_worker->workerId !== $worker->workerId) { + $one_worker->unlisten(); + unset(static::$_workers[$key]); + } + } + Timer::delAll(); + //Update process state. + static::$_status = static::STATUS_RUNNING; + + // Register shutdown function for checking errors. + \register_shutdown_function(array("\\Workerman\\Worker", 'checkErrors')); + + // Create a global event loop. + if (!static::$globalEvent) { + $event_loop_class = static::getEventLoopName(); + static::$globalEvent = new $event_loop_class; + } + + // Reinstall signal. + static::reinstallSignal(); + + // Init Timer. + Timer::init(static::$globalEvent); + + \restore_error_handler(); + + static::setProcessTitle(self::$processTitle . ': worker process ' . $worker->name . ' ' . $worker->getSocketName()); + $worker->setUserAndGroup(); + $worker->id = $id; + $worker->run(); + // Main loop. + static::$globalEvent->loop(); + if (strpos(static::$eventLoopClass, 'Workerman\Events\Swoole') !== false) { + exit(0); + } + $err = new Exception('event-loop exited'); + static::log($err); + exit(250); + } else { + throw new Exception("forkOneWorker fail"); + } + } + + /** + * Get worker id. + * + * @param string $worker_id + * @param int $pid + * + * @return integer + */ + protected static function getId($worker_id, $pid) + { + return \array_search($pid, static::$_idMap[$worker_id]); + } + + /** + * Set unix user and group for current process. + * + * @return void + */ + public function setUserAndGroup() + { + // Get uid. + $user_info = \posix_getpwnam($this->user); + if (!$user_info) { + static::log("Warning: User {$this->user} not exists"); + return; + } + $uid = $user_info['uid']; + // Get gid. + if ($this->group) { + $group_info = \posix_getgrnam($this->group); + if (!$group_info) { + static::log("Warning: Group {$this->group} not exists"); + return; + } + $gid = $group_info['gid']; + } else { + $gid = $user_info['gid']; + } + + // Set uid and gid. + if ($uid !== \posix_getuid() || $gid !== \posix_getgid()) { + if (!\posix_setgid($gid) || !\posix_initgroups($user_info['name'], $gid) || !\posix_setuid($uid)) { + static::log("Warning: change gid or uid fail."); + } + } + } + + /** + * Set process name. + * + * @param string $title + * @return void + */ + protected static function setProcessTitle($title) + { + \set_error_handler(function(){}); + // >=php 5.5 + if (\function_exists('cli_set_process_title')) { + \cli_set_process_title($title); + } // Need proctitle when php<=5.5 . + elseif (\extension_loaded('proctitle') && \function_exists('setproctitle')) { + \setproctitle($title); + } + \restore_error_handler(); + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkers() + { + if (static::$_OS === \OS_TYPE_LINUX) { + static::monitorWorkersForLinux(); + } else { + static::monitorWorkersForWindows(); + } + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkersForLinux() + { + static::$_status = static::STATUS_RUNNING; + while (1) { + // Calls signal handlers for pending signals. + \pcntl_signal_dispatch(); + // Suspends execution of the current process until a child has exited, or until a signal is delivered + $status = 0; + $pid = \pcntl_wait($status, \WUNTRACED); + // Calls signal handlers for pending signals again. + \pcntl_signal_dispatch(); + // If a child has already exited. + if ($pid > 0) { + // Find out which worker process exited. + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + if (isset($worker_pid_array[$pid])) { + $worker = static::$_workers[$worker_id]; + // Fix exit with status 2 for php8.2 + if ($status === \SIGINT && static::$_status === static::STATUS_SHUTDOWN) { + $status = 0; + } + // Exit status. + if ($status !== 0) { + static::log("worker[{$worker->name}:$pid] exit with status $status"); + } + + // onWorkerExit + if ($worker->onWorkerExit) { + try { + ($worker->onWorkerExit)($worker, $status, $pid); + } catch (\Throwable $exception) { + static::log("worker[{$worker->name}] onWorkerExit $exception"); + } + } + + // For Statistics. + if (!isset(static::$_globalStatistics['worker_exit_info'][$worker_id][$status])) { + static::$_globalStatistics['worker_exit_info'][$worker_id][$status] = 0; + } + ++static::$_globalStatistics['worker_exit_info'][$worker_id][$status]; + + // Clear process data. + unset(static::$_pidMap[$worker_id][$pid]); + + // Mark id is available. + $id = static::getId($worker_id, $pid); + static::$_idMap[$worker_id][$id] = 0; + + break; + } + } + // Is still running state then fork a new worker process. + if (static::$_status !== static::STATUS_SHUTDOWN) { + static::forkWorkers(); + // If reloading continue. + if (isset(static::$_pidsToRestart[$pid])) { + unset(static::$_pidsToRestart[$pid]); + static::reload(); + } + } + } + + // If shutdown state and all child processes exited then master process exit. + if (static::$_status === static::STATUS_SHUTDOWN && !static::getAllWorkerPids()) { + static::exitAndClearAll(); + } + } + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkersForWindows() + { + Timer::add(1, "\\Workerman\\Worker::checkWorkerStatusForWindows"); + + static::$globalEvent->loop(); + } + + /** + * Exit current process. + * + * @return void + */ + protected static function exitAndClearAll() + { + foreach (static::$_workers as $worker) { + $socket_name = $worker->getSocketName(); + if ($worker->transport === 'unix' && $socket_name) { + list(, $address) = \explode(':', $socket_name, 2); + $address = substr($address, strpos($address, '/') + 2); + @\unlink($address); + } + } + @\unlink(static::$pidFile); + static::log("Workerman[" . \basename(static::$_startFile) . "] has been stopped"); + if (static::$onMasterStop) { + \call_user_func(static::$onMasterStop); + } + exit(0); + } + + /** + * Execute reload. + * + * @return void + */ + protected static function reload() + { + // For master process. + if (static::$_masterPid === \posix_getpid()) { + if (static::$_gracefulStop) { + $sig = \SIGUSR2; + } else { + $sig = \SIGUSR1; + } + // Set reloading state. + if (static::$_status !== static::STATUS_RELOADING && static::$_status !== static::STATUS_SHUTDOWN) { + static::log("Workerman[" . \basename(static::$_startFile) . "] reloading"); + static::$_status = static::STATUS_RELOADING; + // Try to emit onMasterReload callback. + if (static::$onMasterReload) { + try { + \call_user_func(static::$onMasterReload); + } catch (\Exception $e) { + static::stopAll(250, $e); + } catch (\Error $e) { + static::stopAll(250, $e); + } + static::initId(); + } + + // Send reload signal to all child processes. + $reloadable_pid_array = array(); + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + $worker = static::$_workers[$worker_id]; + if ($worker->reloadable) { + foreach ($worker_pid_array as $pid) { + $reloadable_pid_array[$pid] = $pid; + } + } else { + foreach ($worker_pid_array as $pid) { + // Send reload signal to a worker process which reloadable is false. + \posix_kill($pid, $sig); + } + } + } + + // Get all pids that are waiting reload. + static::$_pidsToRestart = \array_intersect(static::$_pidsToRestart, $reloadable_pid_array); + + } + + // Reload complete. + if (empty(static::$_pidsToRestart)) { + if (static::$_status !== static::STATUS_SHUTDOWN) { + static::$_status = static::STATUS_RUNNING; + } + return; + } + // Continue reload. + $one_worker_pid = \current(static::$_pidsToRestart); + // Send reload signal to a worker process. + \posix_kill($one_worker_pid, $sig); + // If the process does not exit after static::$stopTimeout seconds try to kill it. + if(!static::$_gracefulStop){ + Timer::add(static::$stopTimeout, '\posix_kill', array($one_worker_pid, \SIGKILL), false); + } + } // For child processes. + else { + \reset(static::$_workers); + $worker = \current(static::$_workers); + // Try to emit onWorkerReload callback. + if ($worker->onWorkerReload) { + try { + \call_user_func($worker->onWorkerReload, $worker); + } catch (\Exception $e) { + static::stopAll(250, $e); + } catch (\Error $e) { + static::stopAll(250, $e); + } + } + + if ($worker->reloadable) { + static::stopAll(); + } + } + } + + /** + * Stop all. + * + * @param int $code + * @param string $log + */ + public static function stopAll($code = 0, $log = '') + { + if ($log) { + static::log($log); + } + + static::$_status = static::STATUS_SHUTDOWN; + // For master process. + if (\DIRECTORY_SEPARATOR === '/' && static::$_masterPid === \posix_getpid()) { + static::log("Workerman[" . \basename(static::$_startFile) . "] stopping ..."); + $worker_pid_array = static::getAllWorkerPids(); + // Send stop signal to all child processes. + if (static::$_gracefulStop) { + $sig = \SIGQUIT; + } else { + $sig = \SIGINT; + } + foreach ($worker_pid_array as $worker_pid) { + if (static::$daemonize) { + \posix_kill($worker_pid, $sig); + } else { + Timer::add(1, '\posix_kill', array($worker_pid, $sig), false); + } + if(!static::$_gracefulStop){ + Timer::add(static::$stopTimeout, '\posix_kill', array($worker_pid, \SIGKILL), false); + } + } + Timer::add(1, "\\Workerman\\Worker::checkIfChildRunning"); + // Remove statistics file. + if (\is_file(static::$_statisticsFile)) { + @\unlink(static::$_statisticsFile); + } + } // For child processes. + else { + // Execute exit. + $workers = array_reverse(static::$_workers); + foreach ($workers as $worker) { + if(!$worker->stopping){ + $worker->stop(); + $worker->stopping = true; + } + } + if (!static::$_gracefulStop || ConnectionInterface::$statistics['connection_count'] <= 0) { + static::$_workers = array(); + if (static::$globalEvent) { + static::$globalEvent->destroy(); + } + + try { + exit($code); + } catch (Exception $e) { + + } + } + } + } + + /** + * check if child processes is really running + */ + public static function checkIfChildRunning() + { + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + foreach ($worker_pid_array as $pid => $worker_pid) { + if (!\posix_kill($pid, 0)) { + unset(static::$_pidMap[$worker_id][$pid]); + } + } + } + } + + /** + * Get process status. + * + * @return number + */ + public static function getStatus() + { + return static::$_status; + } + + /** + * If stop gracefully. + * + * @return bool + */ + public static function getGracefulStop() + { + return static::$_gracefulStop; + } + + /** + * Write statistics data to disk. + * + * @return void + */ + protected static function writeStatisticsToStatusFile() + { + // For master process. + if (static::$_masterPid === \posix_getpid()) { + $all_worker_info = array(); + foreach(static::$_pidMap as $worker_id => $pid_array) { + /** @var Worker $worker */ + $worker = static::$_workers[$worker_id]; + foreach($pid_array as $pid) { + $all_worker_info[$pid] = array('name' => $worker->name, 'listen' => $worker->getSocketName()); + } + } + + \file_put_contents(static::$_statisticsFile, \serialize($all_worker_info)."\n", \FILE_APPEND); + $loadavg = \function_exists('sys_getloadavg') ? \array_map('round', \sys_getloadavg(), array(2,2,2)) : array('-', '-', '-'); + \file_put_contents(static::$_statisticsFile, + "----------------------------------------------GLOBAL STATUS----------------------------------------------------\n", \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, + 'Workerman version:' . static::VERSION . " PHP version:" . \PHP_VERSION . "\n", \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, 'start time:' . \date('Y-m-d H:i:s', + static::$_globalStatistics['start_timestamp']) . ' run ' . \floor((\time() - static::$_globalStatistics['start_timestamp']) / (24 * 60 * 60)) . ' days ' . \floor(((\time() - static::$_globalStatistics['start_timestamp']) % (24 * 60 * 60)) / (60 * 60)) . " hours \n", + FILE_APPEND); + $load_str = 'load average: ' . \implode(", ", $loadavg); + \file_put_contents(static::$_statisticsFile, + \str_pad($load_str, 33) . 'event-loop:' . static::getEventLoopName() . "\n", \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, + \count(static::$_pidMap) . ' workers ' . \count(static::getAllWorkerPids()) . " processes\n", + \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, + \str_pad('worker_name', static::$_maxWorkerNameLength) . " exit_status exit_count\n", \FILE_APPEND); + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + $worker = static::$_workers[$worker_id]; + if (isset(static::$_globalStatistics['worker_exit_info'][$worker_id])) { + foreach (static::$_globalStatistics['worker_exit_info'][$worker_id] as $worker_exit_status => $worker_exit_count) { + \file_put_contents(static::$_statisticsFile, + \str_pad($worker->name, static::$_maxWorkerNameLength) . " " . \str_pad($worker_exit_status, + 16) . " $worker_exit_count\n", \FILE_APPEND); + } + } else { + \file_put_contents(static::$_statisticsFile, + \str_pad($worker->name, static::$_maxWorkerNameLength) . " " . \str_pad(0, 16) . " 0\n", + \FILE_APPEND); + } + } + \file_put_contents(static::$_statisticsFile, + "----------------------------------------------PROCESS STATUS---------------------------------------------------\n", + \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, + "pid\tmemory " . \str_pad('listening', static::$_maxSocketNameLength) . " " . \str_pad('worker_name', + static::$_maxWorkerNameLength) . " connections " . \str_pad('send_fail', 9) . " " + . \str_pad('timers', 8) . \str_pad('total_request', 13) ." qps status\n", \FILE_APPEND); + + \chmod(static::$_statisticsFile, 0722); + + foreach (static::getAllWorkerPids() as $worker_pid) { + \posix_kill($worker_pid, \SIGIOT); + } + return; + } + + // For child processes. + \gc_collect_cycles(); + if (\function_exists('gc_mem_caches')) { + \gc_mem_caches(); + } + \reset(static::$_workers); + /** @var \Workerman\Worker $worker */ + $worker = current(static::$_workers); + $worker_status_str = \posix_getpid() . "\t" . \str_pad(round(memory_get_usage(false) / (1024 * 1024), 2) . "M", 7) + . " " . \str_pad($worker->getSocketName(), static::$_maxSocketNameLength) . " " + . \str_pad(($worker->name === $worker->getSocketName() ? 'none' : $worker->name), static::$_maxWorkerNameLength) + . " "; + $worker_status_str .= \str_pad(ConnectionInterface::$statistics['connection_count'], 11) + . " " . \str_pad(ConnectionInterface::$statistics['send_fail'], 9) + . " " . \str_pad(static::$globalEvent->getTimerCount(), 7) + . " " . \str_pad(ConnectionInterface::$statistics['total_request'], 13) . "\n"; + \file_put_contents(static::$_statisticsFile, $worker_status_str, \FILE_APPEND); + } + + /** + * Write statistics data to disk. + * + * @return void + */ + protected static function writeConnectionsStatisticsToStatusFile() + { + // For master process. + if (static::$_masterPid === \posix_getpid()) { + \file_put_contents(static::$_statisticsFile, "--------------------------------------------------------------------- WORKERMAN CONNECTION STATUS --------------------------------------------------------------------------------\n", \FILE_APPEND); + \file_put_contents(static::$_statisticsFile, "PID Worker CID Trans Protocol ipv4 ipv6 Recv-Q Send-Q Bytes-R Bytes-W Status Local Address Foreign Address\n", \FILE_APPEND); + \chmod(static::$_statisticsFile, 0722); + foreach (static::getAllWorkerPids() as $worker_pid) { + \posix_kill($worker_pid, \SIGIO); + } + return; + } + + // For child processes. + $bytes_format = function($bytes) + { + if($bytes > 1024*1024*1024*1024) { + return round($bytes/(1024*1024*1024*1024), 1)."TB"; + } + if($bytes > 1024*1024*1024) { + return round($bytes/(1024*1024*1024), 1)."GB"; + } + if($bytes > 1024*1024) { + return round($bytes/(1024*1024), 1)."MB"; + } + if($bytes > 1024) { + return round($bytes/(1024), 1)."KB"; + } + return $bytes."B"; + }; + + $pid = \posix_getpid(); + $str = ''; + \reset(static::$_workers); + $current_worker = current(static::$_workers); + $default_worker_name = $current_worker->name; + + /** @var \Workerman\Worker $worker */ + foreach(TcpConnection::$connections as $connection) { + /** @var \Workerman\Connection\TcpConnection $connection */ + $transport = $connection->transport; + $ipv4 = $connection->isIpV4() ? ' 1' : ' 0'; + $ipv6 = $connection->isIpV6() ? ' 1' : ' 0'; + $recv_q = $bytes_format($connection->getRecvBufferQueueSize()); + $send_q = $bytes_format($connection->getSendBufferQueueSize()); + $local_address = \trim($connection->getLocalAddress()); + $remote_address = \trim($connection->getRemoteAddress()); + $state = $connection->getStatus(false); + $bytes_read = $bytes_format($connection->bytesRead); + $bytes_written = $bytes_format($connection->bytesWritten); + $id = $connection->id; + $protocol = $connection->protocol ? $connection->protocol : $connection->transport; + $pos = \strrpos($protocol, '\\'); + if ($pos) { + $protocol = \substr($protocol, $pos+1); + } + if (\strlen($protocol) > 15) { + $protocol = \substr($protocol, 0, 13) . '..'; + } + $worker_name = isset($connection->worker) ? $connection->worker->name : $default_worker_name; + if (\strlen($worker_name) > 14) { + $worker_name = \substr($worker_name, 0, 12) . '..'; + } + $str .= \str_pad($pid, 9) . \str_pad($worker_name, 16) . \str_pad($id, 10) . \str_pad($transport, 8) + . \str_pad($protocol, 16) . \str_pad($ipv4, 7) . \str_pad($ipv6, 7) . \str_pad($recv_q, 13) + . \str_pad($send_q, 13) . \str_pad($bytes_read, 13) . \str_pad($bytes_written, 13) . ' ' + . \str_pad($state, 14) . ' ' . \str_pad($local_address, 22) . ' ' . \str_pad($remote_address, 22) ."\n"; + } + if ($str) { + \file_put_contents(static::$_statisticsFile, $str, \FILE_APPEND); + } + } + + /** + * Check errors when current process exited. + * + * @return void + */ + public static function checkErrors() + { + if (static::STATUS_SHUTDOWN !== static::$_status) { + $error_msg = static::$_OS === \OS_TYPE_LINUX ? 'Worker['. \posix_getpid() .'] process terminated' : 'Worker process terminated'; + $errors = error_get_last(); + if ($errors && ($errors['type'] === \E_ERROR || + $errors['type'] === \E_PARSE || + $errors['type'] === \E_CORE_ERROR || + $errors['type'] === \E_COMPILE_ERROR || + $errors['type'] === \E_RECOVERABLE_ERROR) + ) { + $error_msg .= ' with ERROR: ' . static::getErrorType($errors['type']) . " \"{$errors['message']} in {$errors['file']} on line {$errors['line']}\""; + } + static::log($error_msg); + } + } + + /** + * Get error message by error code. + * + * @param integer $type + * @return string + */ + protected static function getErrorType($type) + { + if(isset(self::$_errorType[$type])) { + return self::$_errorType[$type]; + } + + return ''; + } + + /** + * Log. + * + * @param string $msg + * @return void + */ + public static function log($msg) + { + $msg = $msg . "\n"; + if (!static::$daemonize) { + static::safeEcho($msg); + } + \file_put_contents((string)static::$logFile, \date('Y-m-d H:i:s') . ' ' . 'pid:' + . (static::$_OS === \OS_TYPE_LINUX ? \posix_getpid() : 1) . ' ' . $msg, \FILE_APPEND | \LOCK_EX); + } + + /** + * Safe Echo. + * @param string $msg + * @param bool $decorated + * @return bool + */ + public static function safeEcho($msg, $decorated = false) + { + $stream = static::outputStream(); + if (!$stream) { + return false; + } + if (!$decorated) { + $line = $white = $green = $end = ''; + if (static::$_outputDecorated) { + $line = "\033[1A\n\033[K"; + $white = "\033[47;30m"; + $green = "\033[32;40m"; + $end = "\033[0m"; + } + $msg = \str_replace(array('', '', ''), array($line, $white, $green), $msg); + $msg = \str_replace(array('', '', ''), $end, $msg); + } elseif (!static::$_outputDecorated) { + return false; + } + set_error_handler(function(){}); + if (!feof($stream)) { + fwrite($stream, $msg); + fflush($stream); + } + restore_error_handler(); + return true; + } + + /** + * @param resource|null $stream + * @return bool|resource + */ + private static function outputStream($stream = null) + { + if (!$stream) { + $stream = static::$_outputStream ? static::$_outputStream : \STDOUT; + } + if (!$stream || !\is_resource($stream) || 'stream' !== \get_resource_type($stream)) { + return false; + } + $stat = \fstat($stream); + if (!$stat) { + return false; + } + if (($stat['mode'] & 0170000) === 0100000) { + // file + static::$_outputDecorated = false; + } else { + static::$_outputDecorated = + static::$_OS === \OS_TYPE_LINUX && + \function_exists('posix_isatty') && + \posix_isatty($stream); + } + return static::$_outputStream = $stream; + } + + /** + * Construct. + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name = '', array $context_option = array()) + { + // Save all worker instances. + $this->workerId = \spl_object_hash($this); + static::$_workers[$this->workerId] = $this; + static::$_pidMap[$this->workerId] = array(); + + // Get autoload root path. + $backtrace = \debug_backtrace(); + $this->_autoloadRootPath = \dirname($backtrace[0]['file']); + Autoloader::setRootPath($this->_autoloadRootPath); + + // Context for socket. + if ($socket_name) { + $this->_socketName = $socket_name; + if (!isset($context_option['socket']['backlog'])) { + $context_option['socket']['backlog'] = static::DEFAULT_BACKLOG; + } + $this->_context = \stream_context_create($context_option); + } + + // Turn reusePort on. + /*if (static::$_OS === \OS_TYPE_LINUX // if linux + && \version_compare(\PHP_VERSION,'7.0.0', 'ge') // if php >= 7.0.0 + && \version_compare(php_uname('r'), '3.9', 'ge') // if kernel >=3.9 + && \strtolower(\php_uname('s')) !== 'darwin' // if not Mac OS + && strpos($socket_name,'unix') !== 0) { // if not unix socket + + $this->reusePort = true; + }*/ + } + + + /** + * Listen. + * + * @throws Exception + */ + public function listen() + { + if (!$this->_socketName) { + return; + } + + // Autoload. + Autoloader::setRootPath($this->_autoloadRootPath); + + if (!$this->_mainSocket) { + + $local_socket = $this->parseSocketAddress(); + + // Flag. + $flags = $this->transport === 'udp' ? \STREAM_SERVER_BIND : \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN; + $errno = 0; + $errmsg = ''; + // SO_REUSEPORT. + if ($this->reusePort) { + \stream_context_set_option($this->_context, 'socket', 'so_reuseport', 1); + } + + // Create an Internet or Unix domain server socket. + $this->_mainSocket = \stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context); + if (!$this->_mainSocket) { + throw new Exception($errmsg); + } + + if ($this->transport === 'ssl') { + \stream_socket_enable_crypto($this->_mainSocket, false); + } elseif ($this->transport === 'unix') { + $socket_file = \substr($local_socket, 7); + if ($this->user) { + \chown($socket_file, $this->user); + } + if ($this->group) { + \chgrp($socket_file, $this->group); + } + } + + // Try to open keepalive for tcp and disable Nagle algorithm. + if (\function_exists('socket_import_stream') && static::$_builtinTransports[$this->transport] === 'tcp') { + \set_error_handler(function(){}); + $socket = \socket_import_stream($this->_mainSocket); + \socket_set_option($socket, \SOL_SOCKET, \SO_KEEPALIVE, 1); + \socket_set_option($socket, \SOL_TCP, \TCP_NODELAY, 1); + \restore_error_handler(); + } + + // Non blocking. + \stream_set_blocking($this->_mainSocket, false); + } + + $this->resumeAccept(); + } + + /** + * Unlisten. + * + * @return void + */ + public function unlisten() { + $this->pauseAccept(); + if ($this->_mainSocket) { + \set_error_handler(function(){}); + \fclose($this->_mainSocket); + \restore_error_handler(); + $this->_mainSocket = null; + } + } + + /** + * Parse local socket address. + * + * @throws Exception + */ + protected function parseSocketAddress() { + if (!$this->_socketName) { + return; + } + // Get the application layer communication protocol and listening address. + list($scheme, $address) = \explode(':', $this->_socketName, 2); + // Check application layer protocol class. + if (!isset(static::$_builtinTransports[$scheme])) { + $scheme = \ucfirst($scheme); + $this->protocol = \substr($scheme,0,1)==='\\' ? $scheme : 'Protocols\\' . $scheme; + if (!\class_exists($this->protocol)) { + $this->protocol = "Workerman\\Protocols\\$scheme"; + if (!\class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + + if (!isset(static::$_builtinTransports[$this->transport])) { + throw new Exception('Bad worker->transport ' . \var_export($this->transport, true)); + } + } else { + $this->transport = $scheme; + } + //local socket + return static::$_builtinTransports[$this->transport] . ":" . $address; + } + + /** + * Pause accept new connections. + * + * @return void + */ + public function pauseAccept() + { + if (static::$globalEvent && false === $this->_pauseAccept && $this->_mainSocket) { + static::$globalEvent->del($this->_mainSocket, EventInterface::EV_READ); + $this->_pauseAccept = true; + } + } + + /** + * Resume accept new connections. + * + * @return void + */ + public function resumeAccept() + { + // Register a listener to be notified when server socket is ready to read. + if (static::$globalEvent && true === $this->_pauseAccept && $this->_mainSocket) { + if ($this->transport !== 'udp') { + static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection')); + } else { + static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptUdpConnection')); + } + $this->_pauseAccept = false; + } + } + + /** + * Get socket name. + * + * @return string + */ + public function getSocketName() + { + return $this->_socketName ? \lcfirst($this->_socketName) : 'none'; + } + + /** + * Run worker instance. + * + * @return void + * @throws Exception + */ + public function run() + { + $this->listen(); + + // Try to emit onWorkerStart callback. + if ($this->onWorkerStart) { + try { + \call_user_func($this->onWorkerStart, $this); + } catch (\Exception $e) { + // Avoid rapid infinite loop exit. + sleep(1); + static::stopAll(250, $e); + } catch (\Error $e) { + // Avoid rapid infinite loop exit. + sleep(1); + static::stopAll(250, $e); + } + } + } + + /** + * Stop current worker instance. + * + * @return void + */ + public function stop() + { + // Try to emit onWorkerStop callback. + if ($this->onWorkerStop) { + try { + \call_user_func($this->onWorkerStop, $this); + } catch (\Exception $e) { + static::stopAll(250, $e); + } catch (\Error $e) { + static::stopAll(250, $e); + } + } + // Remove listener for server socket. + $this->unlisten(); + // Close all connections for the worker. + if (!static::$_gracefulStop) { + foreach ($this->connections as $connection) { + $connection->close(); + } + } + // Remove worker. + foreach(static::$_workers as $key => $one_worker) { + if ($one_worker->workerId === $this->workerId) { + unset(static::$_workers[$key]); + } + } + + // Clear callback. + $this->onMessage = $this->onClose = $this->onError = $this->onBufferDrain = $this->onBufferFull = null; + } + + /** + * Accept a connection. + * + * @param resource $socket + * @return void + */ + public function acceptConnection($socket) + { + // Accept a connection on server socket. + \set_error_handler(function(){}); + $new_socket = \stream_socket_accept($socket, 0, $remote_address); + \restore_error_handler(); + + // Thundering herd. + if (!$new_socket) { + return; + } + + // TcpConnection. + $connection = new TcpConnection($new_socket, $remote_address); + $this->connections[$connection->id] = $connection; + $connection->worker = $this; + $connection->protocol = $this->protocol; + $connection->transport = $this->transport; + $connection->onMessage = $this->onMessage; + $connection->onClose = $this->onClose; + $connection->onError = $this->onError; + $connection->onBufferDrain = $this->onBufferDrain; + $connection->onBufferFull = $this->onBufferFull; + + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + \call_user_func($this->onConnect, $connection); + } catch (\Exception $e) { + static::stopAll(250, $e); + } catch (\Error $e) { + static::stopAll(250, $e); + } + } + } + + /** + * For udp package. + * + * @param resource $socket + * @return bool + */ + public function acceptUdpConnection($socket) + { + \set_error_handler(function(){}); + $recv_buffer = \stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remote_address); + \restore_error_handler(); + if (false === $recv_buffer || empty($remote_address)) { + return false; + } + // UdpConnection. + $connection = new UdpConnection($socket, $remote_address); + $connection->protocol = $this->protocol; + if ($this->onMessage) { + try { + if ($this->protocol !== null) { + /** @var \Workerman\Protocols\ProtocolInterface $parser */ + $parser = $this->protocol; + if ($parser && \method_exists($parser, 'input')) { + while ($recv_buffer !== '') { + $len = $parser::input($recv_buffer, $connection); + if ($len === 0) + return true; + $package = \substr($recv_buffer, 0, $len); + $recv_buffer = \substr($recv_buffer, $len); + $data = $parser::decode($package, $connection); + if ($data === false) + continue; + \call_user_func($this->onMessage, $connection, $data); + } + } else { + $data = $parser::decode($recv_buffer, $connection); + // Discard bad packets. + if ($data === false) + return true; + \call_user_func($this->onMessage, $connection, $data); + } + } else { + \call_user_func($this->onMessage, $connection, $recv_buffer); + } + ++ConnectionInterface::$statistics['total_request']; + } catch (\Exception $e) { + static::stopAll(250, $e); + } catch (\Error $e) { + static::stopAll(250, $e); + } + } + return true; + } + + /** + * Check master process is alive + * + * @param int $master_pid + * @return bool + */ + protected static function checkMasterIsAlive($master_pid) + { + if (empty($master_pid)) { + return false; + } + + $master_is_alive = $master_pid && \posix_kill((int) $master_pid, 0) && \posix_getpid() !== $master_pid; + if (!$master_is_alive) { + return false; + } + + $cmdline = "/proc/{$master_pid}/cmdline"; + if (!is_readable($cmdline) || empty(static::$processTitle)) { + return true; + } + + $content = file_get_contents($cmdline); + if (empty($content)) { + return true; + } + + return stripos($content, static::$processTitle) !== false || stripos($content, 'php') !== false; + } +} diff --git a/vendor/workerman/workerman/composer.json b/vendor/workerman/workerman/composer.json new file mode 100644 index 0000000..b9897b1 --- /dev/null +++ b/vendor/workerman/workerman/composer.json @@ -0,0 +1,38 @@ +{ + "name": "workerman/workerman", + "type": "library", + "keywords": [ + "event-loop", + "asynchronous" + ], + "homepage": "http://www.workerman.net", + "license": "MIT", + "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "support": { + "email": "walkor@workerman.net", + "issues": "https://github.com/walkor/workerman/issues", + "forum": "http://wenda.workerman.net/", + "wiki": "http://doc.workerman.net/", + "source": "https://github.com/walkor/workerman" + }, + "require": { + "php": ">=8.0" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "autoload": { + "psr-4": { + "Workerman\\": "./" + } + }, + "minimum-stability": "dev" +}