76 changed files with 18847 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
/.idea |
|||
/.vscode |
|||
*.log |
|||
.env |
@ -0,0 +1,84 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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"); |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
|
@ -0,0 +1,78 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
|
@ -0,0 +1,28 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
|
@ -0,0 +1,6 @@ |
|||
{ |
|||
"require": { |
|||
"workerman/gateway-worker": "^3.1", |
|||
"ext-json": "*" |
|||
} |
|||
} |
@ -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" |
|||
} |
@ -0,0 +1,37 @@ |
|||
<?php |
|||
/** |
|||
* run with command |
|||
* php start.php start |
|||
*/ |
|||
|
|||
ini_set('display_errors', 'on'); |
|||
use Workerman\Worker; |
|||
|
|||
if(strpos(strtolower(PHP_OS), 'win') === 0) |
|||
{ |
|||
exit("start.php not support windows, please use start_for_win.bat\n"); |
|||
} |
|||
|
|||
// 检查扩展 |
|||
if(!extension_loaded('pcntl')) |
|||
{ |
|||
exit("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n"); |
|||
} |
|||
|
|||
if(!extension_loaded('posix')) |
|||
{ |
|||
exit("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n"); |
|||
} |
|||
|
|||
// 标记是全局启动 |
|||
define('GLOBAL_START', 1); |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// 加载所有Applications/*/start.php,以便启动所有服务 |
|||
foreach(glob(__DIR__.'/app/start*.php') as $start_file) |
|||
{ |
|||
require_once $start_file; |
|||
} |
|||
// 运行所有服务 |
|||
Worker::runAll(); |
@ -0,0 +1,2 @@ |
|||
php Applications\YourApp\start_register.php Applications\YourApp\start_gateway.php Applications\YourApp\start_businessworker.php |
|||
pause |
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
// autoload.php @generated by Composer |
|||
|
|||
if (PHP_VERSION_ID < 50600) { |
|||
if (!headers_sent()) { |
|||
header('HTTP/1.1 500 Internal Server Error'); |
|||
} |
|||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; |
|||
if (!ini_get('display_errors')) { |
|||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { |
|||
fwrite(STDERR, $err); |
|||
} elseif (!headers_sent()) { |
|||
echo $err; |
|||
} |
|||
} |
|||
trigger_error( |
|||
$err, |
|||
E_USER_ERROR |
|||
); |
|||
} |
|||
|
|||
require_once __DIR__ . '/composer/autoload_real.php'; |
|||
|
|||
return ComposerAutoloaderInit9c794a7c09bf62eba0cfbd5ecda2bf9b::getLoader(); |
@ -0,0 +1,579 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of Composer. |
|||
* |
|||
* (c) Nils Adermann <naderman@naderman.de> |
|||
* Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* 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 <fabien@symfony.com> |
|||
* @author Jordi Boggiano <j.boggiano@seld.be> |
|||
* @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<string, array<string, int>> |
|||
*/ |
|||
private $prefixLengthsPsr4 = array(); |
|||
/** |
|||
* @var array<string, list<string>> |
|||
*/ |
|||
private $prefixDirsPsr4 = array(); |
|||
/** |
|||
* @var list<string> |
|||
*/ |
|||
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<string, array<string, list<string>>> |
|||
*/ |
|||
private $prefixesPsr0 = array(); |
|||
/** |
|||
* @var list<string> |
|||
*/ |
|||
private $fallbackDirsPsr0 = array(); |
|||
|
|||
/** @var bool */ |
|||
private $useIncludePath = false; |
|||
|
|||
/** |
|||
* @var array<string, string> |
|||
*/ |
|||
private $classMap = array(); |
|||
|
|||
/** @var bool */ |
|||
private $classMapAuthoritative = false; |
|||
|
|||
/** |
|||
* @var array<string, bool> |
|||
*/ |
|||
private $missingClasses = array(); |
|||
|
|||
/** @var string|null */ |
|||
private $apcuPrefix; |
|||
|
|||
/** |
|||
* @var array<string, self> |
|||
*/ |
|||
private static $registeredLoaders = array(); |
|||
|
|||
/** |
|||
* @param string|null $vendorDir |
|||
*/ |
|||
public function __construct($vendorDir = null) |
|||
{ |
|||
$this->vendorDir = $vendorDir; |
|||
self::initializeIncludeClosure(); |
|||
} |
|||
|
|||
/** |
|||
* @return array<string, list<string>> |
|||
*/ |
|||
public function getPrefixes() |
|||
{ |
|||
if (!empty($this->prefixesPsr0)) { |
|||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); |
|||
} |
|||
|
|||
return array(); |
|||
} |
|||
|
|||
/** |
|||
* @return array<string, list<string>> |
|||
*/ |
|||
public function getPrefixesPsr4() |
|||
{ |
|||
return $this->prefixDirsPsr4; |
|||
} |
|||
|
|||
/** |
|||
* @return list<string> |
|||
*/ |
|||
public function getFallbackDirs() |
|||
{ |
|||
return $this->fallbackDirsPsr0; |
|||
} |
|||
|
|||
/** |
|||
* @return list<string> |
|||
*/ |
|||
public function getFallbackDirsPsr4() |
|||
{ |
|||
return $this->fallbackDirsPsr4; |
|||
} |
|||
|
|||
/** |
|||
* @return array<string, string> Array of classname => path |
|||
*/ |
|||
public function getClassMap() |
|||
{ |
|||
return $this->classMap; |
|||
} |
|||
|
|||
/** |
|||
* @param array<string, string> $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>|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>|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>|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>|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<string, self> |
|||
*/ |
|||
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); |
|||
} |
|||
} |
@ -0,0 +1,378 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of Composer. |
|||
* |
|||
* (c) Nils Adermann <naderman@naderman.de> |
|||
* Jordi Boggiano <j.boggiano@seld.be> |
|||
* |
|||
* 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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null |
|||
*/ |
|||
private static $installed; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
private static $installedIsLocalDir; |
|||
|
|||
/** |
|||
* @var bool|null |
|||
*/ |
|||
private static $canGetVendors; |
|||
|
|||
/** |
|||
* @var array[] |
|||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> |
|||
*/ |
|||
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<string> |
|||
*/ |
|||
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<string> |
|||
*/ |
|||
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} |
|||
*/ |
|||
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<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> |
|||
*/ |
|||
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $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<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> |
|||
*/ |
|||
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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $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<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */ |
|||
$required = require __DIR__ . '/installed.php'; |
|||
self::$installed = $required; |
|||
} else { |
|||
self::$installed = array(); |
|||
} |
|||
} |
|||
|
|||
if (self::$installed !== array() && !$copiedLocalDir) { |
|||
$installed[] = self::$installed; |
|||
} |
|||
|
|||
return $installed; |
|||
} |
|||
} |
@ -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. |
|||
|
@ -0,0 +1,10 @@ |
|||
<?php |
|||
|
|||
// autoload_classmap.php @generated by Composer |
|||
|
|||
$vendorDir = dirname(__DIR__); |
|||
$baseDir = dirname($vendorDir); |
|||
|
|||
return array( |
|||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', |
|||
); |
@ -0,0 +1,9 @@ |
|||
<?php |
|||
|
|||
// autoload_namespaces.php @generated by Composer |
|||
|
|||
$vendorDir = dirname(__DIR__); |
|||
$baseDir = dirname($vendorDir); |
|||
|
|||
return array( |
|||
); |
@ -0,0 +1,11 @@ |
|||
<?php |
|||
|
|||
// autoload_psr4.php @generated by Composer |
|||
|
|||
$vendorDir = dirname(__DIR__); |
|||
$baseDir = dirname($vendorDir); |
|||
|
|||
return array( |
|||
'Workerman\\' => array($vendorDir . '/workerman/workerman'), |
|||
'GatewayWorker\\' => array($vendorDir . '/workerman/gateway-worker/src'), |
|||
); |
@ -0,0 +1,38 @@ |
|||
<?php |
|||
|
|||
// autoload_real.php @generated by Composer |
|||
|
|||
class ComposerAutoloaderInit9c794a7c09bf62eba0cfbd5ecda2bf9b |
|||
{ |
|||
private static $loader; |
|||
|
|||
public static function loadClassLoader($class) |
|||
{ |
|||
if ('Composer\Autoload\ClassLoader' === $class) { |
|||
require __DIR__ . '/ClassLoader.php'; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return \Composer\Autoload\ClassLoader |
|||
*/ |
|||
public static function getLoader() |
|||
{ |
|||
if (null !== self::$loader) { |
|||
return self::$loader; |
|||
} |
|||
|
|||
require __DIR__ . '/platform_check.php'; |
|||
|
|||
spl_autoload_register(array('ComposerAutoloaderInit9c794a7c09bf62eba0cfbd5ecda2bf9b', 'loadClassLoader'), true, true); |
|||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); |
|||
spl_autoload_unregister(array('ComposerAutoloaderInit9c794a7c09bf62eba0cfbd5ecda2bf9b', 'loadClassLoader')); |
|||
|
|||
require __DIR__ . '/autoload_static.php'; |
|||
call_user_func(\Composer\Autoload\ComposerStaticInit9c794a7c09bf62eba0cfbd5ecda2bf9b::getInitializer($loader)); |
|||
|
|||
$loader->register(true); |
|||
|
|||
return $loader; |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?php |
|||
|
|||
// autoload_static.php @generated by Composer |
|||
|
|||
namespace Composer\Autoload; |
|||
|
|||
class ComposerStaticInit9c794a7c09bf62eba0cfbd5ecda2bf9b |
|||
{ |
|||
public static $prefixLengthsPsr4 = array ( |
|||
'W' => |
|||
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); |
|||
} |
|||
} |
@ -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": [] |
|||
} |
@ -0,0 +1,41 @@ |
|||
<?php return array( |
|||
'root' => 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, |
|||
), |
|||
), |
|||
); |
@ -0,0 +1,26 @@ |
|||
<?php |
|||
|
|||
// platform_check.php @generated by Composer |
|||
|
|||
$issues = array(); |
|||
|
|||
if (!(PHP_VERSION_ID >= 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 |
|||
); |
|||
} |
@ -0,0 +1,4 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
open_collective: walkor |
|||
patreon: walkor |
@ -0,0 +1,7 @@ |
|||
.buildpath |
|||
.project |
|||
.settings |
|||
.idea |
|||
vendor |
|||
.vscode |
|||
composer.lock |
@ -0,0 +1,21 @@ |
|||
The MIT License |
|||
|
|||
Copyright (c) 2009-2015 walkor<walkor@workerman.net> 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. |
@ -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等启动入口文件)<br> |
|||
[点击这里下载demo](http://www.workerman.net/download/GatewayWorker.zip)。<br> |
|||
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) |
|||
 |
|||
|
|||
## [chat room](http://chat.workerman.net/) |
|||
[Source code](https://github.com/walkor/workerman-chat) |
|||
 |
@ -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" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,515 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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<walkor@workerman.net> |
|||
* |
|||
*/ |
|||
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; |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,136 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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)); |
|||
} |
|||
} |
@ -0,0 +1,76 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,228 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,194 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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<walkor@workerman.net> |
|||
* |
|||
*/ |
|||
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); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,4 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
open_collective: workerman |
|||
patreon: walkor |
@ -0,0 +1,6 @@ |
|||
logs |
|||
.buildpath |
|||
.project |
|||
.settings |
|||
.idea |
|||
.DS_Store |
@ -0,0 +1,69 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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'); |
@ -0,0 +1,382 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,203 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,126 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,208 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,189 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author 有个鬼<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; |
|||
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); |
|||
} |
|||
} |
@ -0,0 +1,215 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author 有个鬼<42765633@qq.com> |
|||
* @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); |
|||
} |
|||
} |
@ -0,0 +1,107 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
@ -0,0 +1,225 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
} |
|||
|
@ -0,0 +1,264 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
@ -0,0 +1,27 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
@ -0,0 +1,27 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
@ -0,0 +1,357 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
} |
@ -0,0 +1,285 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author Ares<aresrr#qq.com> |
|||
* @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); |
|||
} |
|||
} |
@ -0,0 +1,260 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author 爬山虎<blogdaren@163.com> |
|||
* @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); |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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 {} |
@ -0,0 +1,21 @@ |
|||
The MIT License |
|||
|
|||
Copyright (c) 2009-2015 walkor<walkor@workerman.net> 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. |
@ -0,0 +1,61 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,323 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,48 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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"; |
|||
} |
|||
} |
@ -0,0 +1,694 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,458 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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('<h3>404 Not Found</h3>'); |
|||
} |
|||
$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(); |
@ -0,0 +1,64 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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"; |
|||
} |
|||
} |
@ -0,0 +1,461 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
@ -0,0 +1,183 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
@ -0,0 +1,46 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,154 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
} |
@ -0,0 +1,114 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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 <p> |
|||
* The return value (usually TRUE on success, FALSE on failure). |
|||
* Note this value is returned internally to PHP for processing. |
|||
* </p> |
|||
* @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 <p> |
|||
* The return value (usually TRUE on success, FALSE on failure). |
|||
* Note this value is returned internally to PHP for processing. |
|||
* </p> |
|||
* @since 5.4.0 |
|||
*/ |
|||
public function destroy($session_id); |
|||
|
|||
/** |
|||
* Cleanup old sessions |
|||
* @link http://php.net/manual/en/sessionhandlerinterface.gc.php |
|||
* @param int $maxlifetime <p> |
|||
* Sessions that have not updated for |
|||
* the last maxlifetime seconds will be removed. |
|||
* </p> |
|||
* @return bool <p> |
|||
* The return value (usually TRUE on success, FALSE on failure). |
|||
* Note this value is returned internally to PHP for processing. |
|||
* </p> |
|||
* @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 <p> |
|||
* The return value (usually TRUE on success, FALSE on failure). |
|||
* Note this value is returned internally to PHP for processing. |
|||
* </p> |
|||
* @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 <p> |
|||
* 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. |
|||
* </p> |
|||
* @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 <p> |
|||
* 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. |
|||
* </p> |
|||
* @return bool <p> |
|||
* The return value (usually TRUE on success, FALSE on failure). |
|||
* Note this value is returned internally to PHP for processing. |
|||
* </p> |
|||
* @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 = ""); |
|||
|
|||
} |
@ -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; |
|||
} |
@ -0,0 +1,52 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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); |
|||
} |
@ -0,0 +1,70 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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"); |
|||
} |
|||
} |
@ -0,0 +1,562 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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<div style=\"text-align:center\"><h1>WebSocket</h1><hr>workerman</div>", 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<div style=\"text-align:center\"><h1>400 Bad Request</h1><hr>workerman</div>", 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'] = ''; |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,432 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,342 @@ |
|||
# Workerman |
|||
[](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) |
|||
[](https://packagist.org/packages/workerman/workerman) |
|||
[](https://packagist.org/packages/workerman/workerman) |
|||
[](https://packagist.org/packages/workerman/workerman) |
|||
[](https://packagist.org/packages/workerman/workerman) |
|||
[](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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// Create a Websocket server |
|||
$ws_worker = new Worker('websocket://0.0.0.0:2346'); |
|||
|
|||
// Emitted when new connection come |
|||
$ws_worker->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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// #### http worker #### |
|||
$http_worker = new Worker('http://0.0.0.0:2345'); |
|||
|
|||
// 4 processes |
|||
$http_worker->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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// #### create socket and listen 1234 port #### |
|||
$tcp_worker = new Worker('tcp://0.0.0.0:1234'); |
|||
|
|||
// 4 processes |
|||
$tcp_worker->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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
$worker = new Worker('udp://0.0.0.0:1234'); |
|||
|
|||
// 4 processes |
|||
$tcp_worker->count = 4; |
|||
|
|||
// Emitted when data received |
|||
$worker->onMessage = function($connection, $data) |
|||
{ |
|||
$connection->send($data); |
|||
}; |
|||
|
|||
Worker::runAll(); |
|||
``` |
|||
|
|||
### Enable SSL |
|||
```php |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// SSL context. |
|||
$context = array( |
|||
'ssl' => 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 |
|||
<?php |
|||
|
|||
namespace Protocols; |
|||
|
|||
/** |
|||
* User defined protocol |
|||
* Format Text+"\n" |
|||
*/ |
|||
class MyTextProtocol |
|||
{ |
|||
public static function input($recv_buffer) |
|||
{ |
|||
// Find the position of the first occurrence of "\n" |
|||
$pos = strpos($recv_buffer, "\n"); |
|||
|
|||
// Not a complete package. Return 0 because the length of package can not be calculated |
|||
if ($pos === false) { |
|||
return 0; |
|||
} |
|||
|
|||
// Return length of the package |
|||
return $pos+1; |
|||
} |
|||
|
|||
public static function decode($recv_buffer) |
|||
{ |
|||
return trim($recv_buffer); |
|||
} |
|||
|
|||
public static function encode($data) |
|||
{ |
|||
return $data . "\n"; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
// #### MyTextProtocol worker #### |
|||
$text_worker = new Worker('MyTextProtocol://0.0.0.0:5678'); |
|||
|
|||
$text_worker->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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
use Workerman\Timer; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
$task = new Worker(); |
|||
$task->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 |
|||
<?php |
|||
|
|||
use Workerman\Worker; |
|||
use Workerman\Connection\AsyncTcpConnection; |
|||
|
|||
require_once __DIR__ . '/vendor/autoload.php'; |
|||
|
|||
$worker = new Worker(); |
|||
$worker->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 ``` |
|||
 |
|||
```php start.php status ``` |
|||
 |
|||
```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 |
|||
 |
|||
|
|||
## Sponsors |
|||
[opencollective.com/walkor](https://opencollective.com/walkor) |
|||
|
|||
[patreon.com/walkor](https://patreon.com/walkor) |
|||
|
|||
## Donate |
|||
|
|||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UQGGS9UB35WWG"><img src="http://donate.workerman.net/img/donate.png"></a> |
|||
|
|||
## 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). |
@ -0,0 +1,220 @@ |
|||
<?php |
|||
/** |
|||
* This file is part of workerman. |
|||
* |
|||
* Licensed under The MIT License |
|||
* For full copyright and license information, please see the MIT-LICENSE.txt |
|||
* Redistributions of files must retain the above copyright notice. |
|||
* |
|||
* @author walkor<walkor@workerman.net> |
|||
* @copyright walkor<walkor@workerman.net> |
|||
* @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(); |
|||
} |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -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" |
|||
} |
Loading…
Reference in new issue