优化忽略文件

This commit is contained in:
吴昊天 2026-09-24 11:41:54 +08:00
parent b837b2f8de
commit 78507197c2
60 changed files with 12630 additions and 101 deletions

119
.gitignore vendored
View File

@ -1,109 +1,26 @@
# 本文件只影响未跟踪文件;已有跟踪项不会因新增规则自动退出仓库。
# backend/vendor 保持现有随仓库分发方式。
# 系统文件与临时产物
.DS_Store
._*
Thumbs.db
Desktop.ini
*.log
*.tmp.md
*.swp
*.swo
# 以 "." 开头的目录全部忽略,保留仓库根目录 .crmeb/
.*/
!/.crmeb/
!/.crmeb/**
# 编辑器本地设置(适用于各层目录)
.idea/
.vscode/
.hbuilderx/
*.code-workspace
# backend 本地文件与目录
/backend/public/uploads/
/backend/public/install.lock
/backend/public/install/install.log
/backend/runtime/
/backend/.constant
/backend/.env
# 可选本地开发容器配置:Dev Containers 只识别仓库根的 /.devcontainer/ 或 /.devcontainer.json
# 入库版本已移除,需要容器化开发者本地自建,不入库
/.devcontainer/
/.devcontainer.json
# 根目录本地 AI 工具入口;规则、技能、角色与各端约定维护在 .crmeb/
/.agents/
/.claude/
/.codebuddy/
/.codex/
/.codegraph/
/.cursor/
/.gemini/
# `.github/` 整体忽略:Copilot 入口已移除且不再入库。若将来要添加 GitHub Actions
# 工作流等 GitHub 配置,需把本行收窄为只忽略具体文件,否则会被静默忽略
/.github/
/.qoder/
/.reasonix/
/.shared/
/.trae/
/.workbuddy/
/.zcode/
/frontend/.codebuddy/
/frontend/.trae/
/.mcp.json
/reasonix.toml
# 本地环境与凭据;示例文件和随源码分发的默认配置保留
.env
.env.*
!.env.example
!.env.sample
!.env.template
!/backend/public/install/.env
!/frontend/admin/.env.production
!/frontend/uni-app/.env.production
/backend/pem/
# 前端依赖与构建产物
# frontend/admin 本地文件与构建产物
/frontend/admin/dist/
/frontend/admin/node_modules/
/frontend/admin/.env
/frontend/admin/.env.dev
# frontend/uni-app 本地文件与构建产物
/frontend/uni-app/dist/
/frontend/uni-app/node_modules/
# Node 依赖(包括插件或扩展的独立构建目录)
node_modules/
# 首版不再维护的后端历史升级目录
/backend/app/install/
# 历史手工补丁目录已移除,结构真源为 backend/public/install/crmeb_table.sql;本地再生成不入库
/backend/database/
# 后端运行数据与导出文件
/backend/runtime/
/backend/public/install.lock
/backend/public/uploads/
/backend/public/phpExcel/
/backend/backup/*
!/backend/backup/README.md
/backend/.phpunit.cache/
/backend/.phpunit.result.cache
/backend/vendor/dh2y/think-qrcode/src/phpqrcode/*-errors.txt
# 发布到后端的前端构建资源
/backend/public/theme/
# 升级程序包二进制产物:目录内脚本/清单/增量文本入库,zip 与前端构建产物出包时现生成(见 .crmeb/rules/升级包规范.md)
/backend/upgrade/packages/*.zip
/backend/upgrade/packages/*/frontend/
# 插件包发布产物:真源在插件目录(含 upgrades/ 迁移脚本),zip 出包时现生成不入库(见 .crmeb/rules/插件开发规范.md 第七节)
/tests/plugin-packages/
# Docker 数据卷与日志;不忽略同目录下的源码配置
/docker/mysql/data/
/docker/mysql/log/
/docker/nginx/log/
/crmeb/runtime/
/backend/tests/
/crmeb/public/install.lock
# 帮助文档
/docv8/
backend/vendor/workerman/
backend/.constant
backend/.env
/frontend/uni-app/.env
/frontend/uni-app/.env.development

View File

@ -0,0 +1 @@
2018-08-08 15:16:58: imagepng(/data/wwwroot/web/www.shiyongxiu.com/runtime/QRcode_cache/1533712618.png): failed to open stream: No such file or directory

View File

@ -0,0 +1 @@
2018-08-08 15:17:01: imagepng(/data/wwwroot/web/www.shiyongxiu.com/runtime/QRcode_cache/1533712621.png): failed to open stream: No such file or directory

View File

@ -0,0 +1 @@
2026-07-13 17:46:26: Implicit conversion from float 0.25 to int loses precision

View File

@ -0,0 +1,101 @@
# Channel
基于订阅的多进程通讯组件,用于workerman进程间通讯或者服务器集群通讯,类似redis订阅发布机制。基于workerman开发。
Channel 提供两种通讯形式,分别是发布订阅的事件机制和消息队列机制。
它们的主要区别是:
- 事件机制是消息发出后,所有订阅该事件的客户端都能收到消息。
- 消息队列机制是消息发出后,所有订阅该消息的客户端只有一个会收到消息,如果客户端忙消息会进行排队直到有客户端闲置后重新取到消息。
- 需要注意的是 Channel 只是提供一种通讯方式,本身并不提供消息确认、重试、延迟、持久化等功能,请根据实际情况合理使用。
# 手册地址
[Channel手册](http://doc.workerman.net/components/channel.html)
# 服务端
```php
use Workerman\Worker;
//Tcp 通讯方式
$channel_server = new Channel\Server('0.0.0.0', 2206);
//Unix Domain Socket 通讯方式
//$channel_server = new Channel\Server('unix:///tmp/workerman-channel.sock');
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}
```
# 客户端
```php
use Workerman\Worker;
$worker = new Worker();
$worker->onWorkerStart = function()
{
// Channel客户端连接到Channel服务端
Channel\Client::connect('<Channel服务端ip>', 2206);
// 使用 Unix Domain Socket 通讯
//Channel\Client::connect('unix:///tmp/workerman-channel.sock');
// 要订阅的事件名称(名称可以为任意的数字和字符串组合)
$event_name = 'event_xxxx';
// 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调
Channel\Client::on($event_name, function($event_data){
var_dump($event_data);
});
};
$worker->onMessage = function($connection, $data)
{
// 要发布的事件名称
$event_name = 'event_xxxx';
// 事件数据(数据格式可以为数字、字符串、数组),会传递给客户端回调函数作为参数
$event_data = array('some data.', 'some data..');
// 发布某个自定义事件,订阅这个事件的客户端会收到事件数据,并触发客户端对应的事件回调
Channel\Client::publish($event_name, $event_data);
};
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}
````
## 消息队列示例
```php
use Workerman\Worker;
use Workerman\Timer;
$worker = new Worker();
$worker->name = 'Producer';
$worker->onWorkerStart = function()
{
Client::connect();
$count = 0;
Timer::add(1, function() {
Client::enqueue('queue', 'Hello World '.time());
});
};
$mq = new Worker();
$mq->name = 'Consumer';
$mq->count = 4;
$mq->onWorkerStart = function($worker) {
Client::connect();
//订阅消息 queue
Client::watch('queue', function($data) use ($worker) {
echo "Worker {$worker->id} get queue: $data\n";
});
//10 秒后取消订阅该消息
Timer::add(10, function() {
Client::unwatch('queue');
}, [], false);
};
Worker::runAll();
```

View File

@ -0,0 +1,12 @@
{
"name" : "workerman/channel",
"type" : "library",
"homepage": "http://www.workerman.net",
"license" : "MIT",
"require": {
"workerman/workerman" : ">=4.0.12"
},
"autoload": {
"psr-4": {"Channel\\": "./src"}
}
}

View File

@ -0,0 +1,393 @@
<?php
namespace Channel;
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Timer;
use Workerman\Protocols\Frame;
/**
* Channel/Client
*/
class Client
{
/**
* onMessage.
* @var callback
*/
public static $onMessage = null;
/**
* onConnect
* @var callback
*/
public static $onConnect = null;
/**
* onClose
* @var callback
*/
public static $onClose = null;
/**
* Connction to channel server.
* @var \Workerman\Connection\TcpConnection
*/
protected static $_remoteConnection = null;
/**
* Channel server ip.
* @var string
*/
protected static $_remoteIp = null;
/**
* Channel server port.
* @var int
*/
protected static $_remotePort = null;
/**
* Reconnect timer.
* @var Timer
*/
protected static $_reconnectTimer = null;
/**
* Ping timer.
* @var Timer
*/
protected static $_pingTimer = null;
/**
* All event callback.
* @var array
*/
protected static $_events = array();
/**
* All queue callback.
* @var callable
*/
protected static $_queues = array();
/**
* @var bool
*/
protected static $_isWorkermanEnv = true;
/**
* Ping interval.
* @var int
*/
public static $pingInterval = 55;
/**
* Connect to channel server
* @param string $ip Channel server ip address or unix domain socket address
* Ip like (TCP): 192.168.1.100
* Unix domain socket like: unix:///tmp/workerman-channel.sock
* @param int $port Port to connect when use tcp
*/
public static function connect($ip = '127.0.0.1', $port = 2206)
{
if (self::$_remoteConnection) {
return;
}
self::$_remoteIp = $ip;
self::$_remotePort = $port;
if (!in_array(PHP_SAPI, ['cli', 'micro']) || !class_exists('Workerman\Worker', false)) {
self::$_isWorkermanEnv = false;
}
// For workerman environment.
if (self::$_isWorkermanEnv) {
if (strpos($ip, 'unix://') === false) {
$conn = new AsyncTcpConnection('frame://' . self::$_remoteIp . ':' . self::$_remotePort);
} else {
$conn = new AsyncTcpConnection($ip);
$conn->protocol = Frame::class;
}
$conn->onClose = [self::class, 'onRemoteClose'];
$conn->onConnect = [self::class, 'onRemoteConnect'];
$conn->onMessage = [self::class , 'onRemoteMessage'];
$conn->connect();
if (empty(self::$_pingTimer)) {
self::$_pingTimer = Timer::add(self::$pingInterval, 'Channel\Client::ping');
}
// Not workerman environment.
} else {
$remote = strpos($ip, 'unix://') === false ? 'tcp://'.self::$_remoteIp.':'.self::$_remotePort : $ip;
$conn = stream_socket_client($remote, $code, $message, 5);
if (!$conn) {
throw new \Exception($message);
}
}
self::$_remoteConnection = $conn;
}
/**
* onRemoteMessage.
* @param \Workerman\Connection\TcpConnection $connection
* @param string $data
* @throws \Exception
*/
public static function onRemoteMessage($connection, $data)
{
$data = unserialize($data);
$type = $data['type'];
$event = $data['channel'];
$event_data = $data['data'];
$callback = null;
if ($type == 'event') {
if (!empty(self::$_events[$event])) {
call_user_func(self::$_events[$event], $event_data);
} elseif (!empty(Client::$onMessage)) {
call_user_func(Client::$onMessage, $event, $event_data);
} else {
throw new \Exception("event:$event have not callback");
}
} else {
if (isset(self::$_queues[$event])) {
call_user_func(self::$_queues[$event], $event_data);
} else {
throw new \Exception("queue:$event have not callback");
}
}
}
/**
* Ping.
* @return void
*/
public static function ping()
{
if(self::$_remoteConnection)
{
self::$_remoteConnection->send('');
}
}
/**
* onRemoteClose.
* @return void
*/
public static function onRemoteClose()
{
Timer::add(0.5, function() {
echo "Waring channel connection closed and try to reconnect\n";
}, array(), false);
self::$_remoteConnection = null;
self::clearTimer();
self::$_reconnectTimer = Timer::add(1, 'Channel\Client::connect', array(self::$_remoteIp, self::$_remotePort));
if (self::$onClose) {
call_user_func(Client::$onClose);
}
}
/**
* onRemoteConnect.
* @return void
*/
public static function onRemoteConnect()
{
$all_event_names = array_keys(self::$_events);
if($all_event_names)
{
self::subscribe($all_event_names);
}
self::clearTimer();
if (self::$onConnect) {
call_user_func(Client::$onConnect);
}
}
/**
* clearTimer.
* @return void
*/
public static function clearTimer()
{
if (!self::$_isWorkermanEnv) {
throw new \Exception('Channel\\Client not support clearTimer method when it is not in the workerman environment.');
}
if(self::$_reconnectTimer)
{
Timer::del(self::$_reconnectTimer);
self::$_reconnectTimer = null;
}
}
/**
* On.
* @param string $event
* @param callback $callback
* @throws \Exception
*/
public static function on($event, $callback)
{
if (!is_callable($callback)) {
throw new \Exception('callback is not callable for event.');
}
self::$_events[$event] = $callback;
self::subscribe($event);
}
/**
* Subscribe.
* @param string $events
* @return void
*/
public static function subscribe($events)
{
$events = (array)$events;
self::send(array('type' => 'subscribe', 'channels'=>$events));
foreach ($events as $event) {
if(!isset(self::$_events[$event])) {
self::$_events[$event] = null;
}
}
}
/**
* Unsubscribe.
* @param string $events
* @return void
*/
public static function unsubscribe($events)
{
$events = (array)$events;
self::send(array('type' => 'unsubscribe', 'channels'=>$events));
foreach($events as $event) {
unset(self::$_events[$event]);
}
}
/**
* Publish.
* @param string $events
* @param mixed $data
*/
public static function publish($events, $data , $is_loop = false)
{
$type = $is_loop == true ? 'publishLoop' : 'publish';
self::sendAnyway(array('type' => $type, 'channels' => (array)$events, 'data' => $data));
}
/**
* Watch a channel of queue
* @param string|array $channels
* @param callable $callback
* @param boolean $autoReserve Auto reserve after callback finished.
* But sometime you may don't want reserve immediately, or in some asynchronous job,
* you want reserve in finished callback, so you should set $autoReserve to false
* and call Client::reserve() after watch() and in finish callback manually.
* @throws \Exception
*/
public static function watch($channels, $callback, $autoReserve=true)
{
if (!is_callable($callback)) {
throw new \Exception('callback is not callable for watch.');
}
if ($autoReserve) {
$callback = static function($data) use ($callback) {
try {
call_user_func($callback, $data);
} catch (\Exception $e) {
throw $e;
} catch (\Error $e) {
throw $e;
} finally {
self::reserve();
}
};
}
$channels = (array)$channels;
self::send(array('type' => 'watch', 'channels'=>$channels));
foreach ($channels as $channel) {
self::$_queues[$channel] = $callback;
}
if ($autoReserve) {
self::reserve();
}
}
/**
* Unwatch a channel of queue
* @param string $channel
* @throws \Exception
*/
public static function unwatch($channels)
{
$channels = (array)$channels;
self::send(array('type' => 'unwatch', 'channels'=>$channels));
foreach ($channels as $channel) {
if (isset(self::$_queues[$channel])) {
unset(self::$_queues[$channel]);
}
}
}
/**
* Put data to queue
* @param string|array $channels
* @param mixed $data
* @throws \Exception
*/
public static function enqueue($channels, $data)
{
self::sendAnyway(array('type' => 'enqueue', 'channels' => (array)$channels, 'data' => $data));
}
/**
* Start reserve queue manual
* @throws \Exception
*/
public static function reserve()
{
self::send(array('type' => 'reserve'));
}
/**
* Send through workerman environment
* @param $data
* @throws \Exception
*/
protected static function send($data)
{
if (!self::$_isWorkermanEnv) {
throw new \Exception("Channel\\Client not support {$data['type']} method when it is not in the workerman environment.");
}
self::connect(self::$_remoteIp, self::$_remotePort);
self::$_remoteConnection->send(serialize($data));
}
/**
* Send from any environment
* @param $data
* @throws \Exception
*/
protected static function sendAnyway($data)
{
self::connect(self::$_remoteIp, self::$_remotePort);
$body = serialize($data);
if (self::$_isWorkermanEnv) {
self::$_remoteConnection->send($body);
} else {
$buffer = pack('N', 4+strlen($body)) . $body;
fwrite(self::$_remoteConnection, $buffer);
}
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Channel;
use Workerman\Connection\TcpConnection;
class Queue
{
public $name = 'default';
public $watcher = array();
public $consumer = array();
protected $queue = null;
public function __construct($name)
{
$this->name = $name;
$this->queue = new \SplQueue();
}
/**
* @param TcpConnection $connection
*/
public function addWatch($connection)
{
if (!isset($this->watcher[$connection->id])) {
$this->watcher[$connection->id] = $connection;
$connection->watchs[] = $this->name;
}
}
/**
* @param TcpConnection $connection
*/
public function removeWatch($connection)
{
if (isset($connection->watchs) && in_array($this->name, $connection->watchs)) {
$idx = array_search($this->name, $connection->watchs);
unset($connection->watchs[$idx]);
}
if (isset($this->watcher[$connection->id])) {
unset($this->watcher[$connection->id]);
}
if (isset($this->consumer[$connection->id])) {
unset($this->consumer[$connection->id]);
}
}
/**
* @param TcpConnection $connection
*/
public function addConsumer($connection)
{
if (isset($this->watcher[$connection->id]) && !isset($this->consumer[$connection->id])) {
$this->consumer[$connection->id] = $connection;
}
$this->dispatch();
}
public function enqueue($data)
{
$this->queue->enqueue($data);
$this->dispatch();
}
private function dispatch()
{
if ($this->queue->isEmpty() || count($this->consumer) == 0) {
return;
}
while (!$this->queue->isEmpty()) {
$data = $this->queue->dequeue();
$idx = key($this->consumer);
$connection = $this->consumer[$idx];
unset($this->consumer[$idx]);
$connection->send(serialize(array('type'=>'queue', 'channel'=>$this->name, 'data' => $data)));
if (count($this->consumer) == 0) {
break;
}
}
}
public function isEmpty()
{
return empty($this->watcher) && $this->queue->isEmpty();
}
}

View File

@ -0,0 +1,179 @@
<?php
namespace Channel;
use Workerman\Protocols\Frame;
use Workerman\Worker;
/**
* Channel server.
*/
class Server
{
/**
* Worker instance.
* @var Worker
*/
protected $_worker = null;
/**
* Queues
* @var Queue[]
*/
protected $_queues = array();
private $ip;
/**
* Construct.
* @param string $ip Bind ip address or unix domain socket.
* Bind unix domain socket use 'unix:///tmp/channel.sock'
* @param int $port Tcp port to bind, only used when listen on tcp.
*/
public function __construct($ip = '0.0.0.0', $port = 2206)
{
if (strpos($ip, 'unix:') === false) {
$worker = new Worker("frame://$ip:$port");
} else {
$worker = new Worker($ip);
$worker->protocol = Frame::class;
}
$this->ip = $ip;
$worker->count = 1;
$worker->name = 'ChannelServer';
$worker->channels = array();
$worker->onMessage = array($this, 'onMessage') ;
$worker->onClose = array($this, 'onClose');
$this->_worker = $worker;
}
/**
* onClose
* @return void
*/
public function onClose($connection)
{
if (!empty($connection->channels)) {
foreach ($connection->channels as $channel) {
unset($this->_worker->channels[$channel][$connection->id]);
if (empty($this->_worker->channels[$channel])) {
unset($this->_worker->channels[$channel]);
}
}
}
if (!empty($connection->watchs)) {
foreach ($connection->watchs as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->removeWatch($connection);
if ($this->_queues[$channel]->isEmpty()) {
unset($this->_queues[$channel]);
}
}
}
}
}
/**
* onMessage.
* @param \Workerman\Connection\TcpConnection $connection
* @param string $data
*/
public function onMessage($connection, $data)
{
if(!$data)
{
return;
}
$worker = $this->_worker;
$data = unserialize($data);
$type = $data['type'];
switch($type)
{
case 'subscribe':
foreach($data['channels'] as $channel)
{
$connection->channels[$channel] = $channel;
$worker->channels[$channel][$connection->id] = $connection;
}
break;
case 'unsubscribe':
foreach($data['channels'] as $channel) {
if (isset($connection->channels[$channel])) {
unset($connection->channels[$channel]);
}
if (isset($worker->channels[$channel][$connection->id])) {
unset($worker->channels[$channel][$connection->id]);
if (empty($worker->channels[$channel])) {
unset($worker->channels[$channel]);
}
}
}
break;
case 'publish':
foreach ($data['channels'] as $channel) {
if (empty($worker->channels[$channel])) {
continue;
}
$buffer = serialize(array('type' => 'event', 'channel' => $channel, 'data' => $data['data']));
foreach ($worker->channels[$channel] as $connection) {
$connection->send($buffer);
}
}
break;
case 'publishLoop':
//choose one subscriber from the list
foreach ($data['channels'] as $channel) {
if (empty($worker->channels[$channel])) {
continue;
}
$buffer = serialize(array('type' => 'event', 'channel' => $channel, 'data' => $data['data']));
//这是要点,每次取出一个元素,如果取不到,说明已经到最后,重置到第一个
$connection = next($worker->channels[$channel]);
if( $connection == false ){
$connection = reset($worker->channels[$channel]);
}
$connection->send($buffer);
}
break;
case 'watch':
foreach ($data['channels'] as $channel) {
$this->getQueue($channel)->addWatch($connection);
}
break;
case 'unwatch':
foreach ($data['channels'] as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->removeWatch($connection);
if ($this->_queues[$channel]->isEmpty()) {
unset($this->_queues[$channel]);
}
}
}
break;
case 'enqueue':
foreach ($data['channels'] as $channel) {
$this->getQueue($channel)->enqueue($data['data']);
}
break;
case 'reserve':
if (isset($connection->watchs)) {
foreach ($connection->watchs as $channel) {
if (isset($this->_queues[$channel])) {
$this->_queues[$channel]->addConsumer($connection);
}
}
}
break;
}
}
private function getQueue($channel)
{
if (isset($this->_queues[$channel])) {
return $this->_queues[$channel];
}
return ($this->_queues[$channel] = new Queue($channel));
}
}

View File

@ -0,0 +1,53 @@
<?php
use Channel\Client;
use Channel\Server;
use Workerman\Worker;
use Workerman\Timer;
// composer autoload
include __DIR__ . '/../vendor/autoload.php';
$channel_server = new Server();
$worker = new Worker();
$worker->name = 'Event';
$worker->onWorkerStart = function()
{
Client::connect();
$count = 0;
$timerId = Timer::add(0.01, function() use (&$timerId, &$count) {
Client::publish('test event', 'some data');
$count++;
Client::enqueue('task-queue', time());
if ($count == 1000) {
Timer::del($timerId);
}
});
Timer::add(10, function() {
Client::enqueue('task-queue', 'hello every 10 seconds');
});
};
$mq = new Worker();
$mq->name = 'Queue';
$mq->count = 4;
$mq->onWorkerStart = function($worker) {
Client::connect();
$countDown = 20;
$id = 1;
Client::watch('task-queue', function($data) use ($worker, &$countDown, &$id) {
echo "[$id] Worker {$worker->id} get queue: $data\n";
sleep(0.2);
$countDown--;
$id++;
if ($worker->id > 1 && $countDown == 0) {
Client::unwatch('task-queue');
}
Timer::add(1, [Client::class, 'reserve'], [], false);
});
};
Worker::runAll();

View File

@ -0,0 +1,28 @@
<?php
use Channel\Client;
use Channel\Server;
use Workerman\Worker;
use Workerman\Timer;
// composer autoload
include __DIR__ . '/../vendor/autoload.php';
$channel_server = new Server();
$worker = new Worker();
$worker->onWorkerStart = function()
{
Client::connect();
Client::on('test event', function($event_data){
echo 'test event triggered event_data :';
var_dump($event_data);
});
Timer::add(2, function(){
Client::publish('test event', 'some data');
});
};
Worker::runAll();

View File

@ -0,0 +1,23 @@
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2022/2/20
* Time: 12:00
*/
include_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
$processName = "ChannelServerTest";
Worker::$pidFile = "var/{$processName}.pid";
Worker::$logFile = "var/{$processName}_logFile.log";
Worker::$stdoutFile = "var/{$processName}_stdout.log";
$channel_server = new Channel\Server('0.0.0.0', 2206);
if(!defined('GLOBAL_START'))
{
Worker::runAll();
}

View File

@ -0,0 +1,34 @@
<?php
include_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\TcpConnection;
use Workerman\Connection\AsyncUdpConnection;
use Workerman\Connection\AsyncTcpConnection;
//监听端口
$worker = new Worker("");
//开启进程数量
$worker->count = 8;
$processName = "client";
$worker->name = $processName;
$worker->reusePort = true; //开启均衡负载模式
Worker::$pidFile = "var/{$processName}.pid";
Worker::$logFile = "var/{$processName}_logFile.log";
Worker::$stdoutFile = "var/{$processName}_stdout.log";
$worker->onWorkerStart = function() use($worker){
usleep(10);
Channel\Client::connect('127.0.0.1' , 2206);
$event_name = "test_channel";
Channel\Client::on($event_name, function($event_data)use($worker ,$event_name ){
$log_str = "{$worker->id} on {$event_name}:".json_encode($event_data,320)."\n";
echo $log_str;
});
};
Worker::runAll();

View File

@ -0,0 +1,35 @@
<?php
include_once __DIR__ . '/vendor/autoload.php';
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\TcpConnection;
use Workerman\Connection\AsyncUdpConnection;
use Workerman\Connection\AsyncTcpConnection;
//监听端口
$worker = new Worker("");
//开启进程数量
$worker->count = 1;
$processName = "send";
$worker->name = $processName;
$worker->reusePort = true; //开启均衡负载模式
Worker::$pidFile = "var/{$processName}.pid";
Worker::$logFile = "var/{$processName}_logFile.log";
Worker::$stdoutFile = "var/{$processName}_stdout.log";
$worker->onWorkerStart = function() use($worker){
Channel\Client::connect('127.0.0.1' , 2206);
Timer::add( 1 , function ()use($worker){
$data_arr = [
'time' => microtime(true),
'date' => date("Y-m-d H:i:s"),
];
$event_name = "test_channel";
Channel\Client::publish($event_name, $data_arr , true);
});
};
Worker::runAll();

View File

@ -0,0 +1,29 @@
# Crontab
A crontab with precision in seconds written in PHP based on [workerman](https://github.com/walkor/workerman).
# Install
```
composer require workerman/crontab
```
# Usage
start.php
```php
<?php
use Workerman\Worker;
require __DIR__ . '/../vendor/autoload.php';
use Workerman\Crontab\Crontab;
$worker = new Worker();
$worker->onWorkerStart = function () {
// Execute the function in the first second of every minute.
new Crontab('1 * * * * *', function(){
echo date('Y-m-d H:i:s')."\n";
});
};
Worker::runAll();
```
Run with commands `php start.php start` or php `start.php start -d`

View File

@ -0,0 +1,34 @@
{
"name": "workerman/crontab",
"type": "library",
"keywords": [
"crontab"
],
"homepage": "http://www.workerman.net",
"license": "MIT",
"description": "A crontab written in PHP based on workerman",
"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/crontab"
},
"require": {
"php": ">=7.0",
"workerman/workerman": ">=4.0.20"
},
"autoload": {
"psr-4": {
"Workerman\\Crontab\\": "./src"
}
}
}

View File

@ -0,0 +1,16 @@
<?php
use Workerman\Worker;
require __DIR__ . '/../vendor/autoload.php';
use Workerman\Crontab\Crontab;
$worker = new Worker();
$worker->onWorkerStart = function () {
// Execute the function in the first second of every minute.
new Crontab('1 * * * * *', function(){
echo date('Y-m-d H:i:s')."\n";
});
};
Worker::runAll();

View File

@ -0,0 +1,177 @@
<?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\Crontab;
use Workerman\Timer;
/**
* Class Crontab
* @package Workerman\Crontab
*/
class Crontab
{
/**
* @var string
*/
protected $_rule;
/**
* @var callable
*/
protected $_callback;
/**
* @var string
*/
protected $_name;
/**
* @var int
*/
protected $_id;
/**
* @var array
*/
protected static $_instances = [];
/**
* Crontab constructor.
* @param string $rule
* @param callable $callback
* @param string $name
*/
public function __construct($rule, $callback, $name = '')
{
$this->_rule = $rule;
$this->_callback = $callback;
$this->_name = $name;
$this->_id = static::createId();
static::$_instances[$this->_id] = $this;
static::tryInit();
}
/**
* @return string
*/
public function getRule()
{
return $this->_rule;
}
/**
* @return callable
*/
public function getCallback()
{
return $this->_callback;
}
/**
* @return string
*/
public function getName()
{
return $this->_name;
}
/**
* @return int
*/
public function getId()
{
return $this->_id;
}
/**
* @return bool
*/
public function destroy()
{
return static::remove($this->_id);
}
/**
* @return array
*/
public static function getAll()
{
return static::$_instances;
}
/**
* @param $id
* @return bool
*/
public static function remove($id)
{
if ($id instanceof Crontab) {
$id = $id->getId();
}
if (!isset(static::$_instances[$id])) {
return false;
}
unset(static::$_instances[$id]);
return true;
}
/**
* @return int
*/
protected static function createId()
{
static $id = 0;
return ++$id;
}
/**
* tryInit
*/
protected static function tryInit()
{
static $inited = false;
if ($inited) {
return;
}
$inited = true;
$parser = new Parser();
$callback = function () use ($parser, &$callback) {
foreach (static::$_instances as $crontab) {
$rule = $crontab->getRule();
$cb = $crontab->getCallback();
if (!$cb || !$rule) {
continue;
}
$times = $parser->parse($rule);
$now = time();
foreach ($times as $time) {
$t = $time-$now;
if ($t <= 0) {
$t = 0.000001;
}
Timer::add($t, $cb, null, false);
}
}
Timer::add(60 - time()%60, $callback, null, false);
};
$next_time = time()%60;
if ($next_time == 0) {
$next_time = 0.00001;
} else {
$next_time = 60 - $next_time;
}
Timer::add($next_time, $callback, null, false);
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* @author: Jan Konieczny <jkonieczny@gmail.com>, group@hyperf.io
* @license: http://www.gnu.org/licenses/
* @license: https://github.com/hyperf/hyperf/blob/master/LICENSE
*
* This is a simple script to parse crontab syntax to get the execution time
*
* Eg.: $timestamp = Crontab::parse('12 * * * 1-5');
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides basic cron syntax parsing functionality
*
* @author: Jan Konieczny <jkonieczny@gmail.com>, group@hyperf.io
* @license: http://www.gnu.org/licenses/
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Workerman\Crontab;
/**
* Class Parser
* @package Workerman\Crontab
*/
class Parser
{
/**
* Finds next execution time(stamp) parsin crontab syntax.
*
* @param string $crontab_string :
* 0 1 2 3 4 5
* * * * * * *
* - - - - - -
* | | | | | |
* | | | | | +----- day of week (0 - 6) (Sunday=0)
* | | | | +----- month (1 - 12)
* | | | +------- day of month (1 - 31)
* | | +--------- hour (0 - 23)
* | +----------- min (0 - 59)
* +------------- sec (0-59)
*
* @param null|int $start_time
* @throws \InvalidArgumentException
* @return int[]
*/
public function parse($crontab_string, $start_time = null)
{
if (! $this->isValid($crontab_string)) {
throw new \InvalidArgumentException('Invalid cron string: ' . $crontab_string);
}
$start_time = $start_time ? $start_time : time();
$date = $this->parseDate($crontab_string);
if (in_array((int) date('i', $start_time), $date['minutes'])
&& in_array((int) date('G', $start_time), $date['hours'])
&& in_array((int) date('j', $start_time), $date['day'])
&& in_array((int) date('w', $start_time), $date['week'])
&& in_array((int) date('n', $start_time), $date['month'])
) {
$result = [];
foreach ($date['second'] as $second) {
$result[] = $start_time + $second;
}
return $result;
}
return [];
}
public function isValid(string $crontab_string): bool
{
if (! preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', trim($crontab_string))) {
if (! preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', trim($crontab_string))) {
return false;
}
}
return true;
}
/**
* Parse each segment of crontab string.
*/
protected function parseSegment(string $string, int $min, int $max, ?int $start = null)
{
if ($start === null || $start < $min) {
$start = $min;
}
$result = [];
if ($string === '*') {
for ($i = $start; $i <= $max; ++$i) {
$result[] = $i;
}
} elseif (strpos($string, ',') !== false) {
$exploded = explode(',', $string);
foreach ($exploded as $value) {
if (strpos($value, '/') !== false || strpos($string, '-') !== false) {
$result = array_merge($result, $this->parseSegment($value, $min, $max, $start));
continue;
}
if (trim($value) === '' || ! $this->between((int) $value, (int) ($min > $start ? $min : $start), (int) $max)) {
continue;
}
$result[] = (int) $value;
}
} elseif (strpos($string, '/') !== false) {
$exploded = explode('/', $string);
if (strpos($exploded[0], '-') !== false) {
[$nMin, $nMax] = explode('-', $exploded[0]);
$nMin > $min && $min = (int) $nMin;
$nMax < $max && $max = (int) $nMax;
}
$start < $min && $start = $min;
for ($i = $start; $i <= $max;) {
$result[] = $i;
$i += $exploded[1];
}
} elseif (strpos($string, '-') !== false) {
$result = array_merge($result, $this->parseSegment($string . '/1', $min, $max, $start));
} elseif ($this->between((int) $string, $min > $start ? $min : $start, $max)) {
$result[] = (int) $string;
}
return $result;
}
/**
* Determire if the $value is between in $min and $max ?
*/
private function between(int $value, int $min, int $max): bool
{
return $value >= $min && $value <= $max;
}
private function parseDate(string $crontab_string): array
{
$cron = preg_split('/[\\s]+/i', trim($crontab_string));
if (count($cron) == 6) {
$date = [
'second' => $this->parseSegment($cron[0], 0, 59),
'minutes' => $this->parseSegment($cron[1], 0, 59),
'hours' => $this->parseSegment($cron[2], 0, 23),
'day' => $this->parseSegment($cron[3], 1, 31),
'month' => $this->parseSegment($cron[4], 1, 12),
'week' => $this->parseSegment($cron[5], 0, 6),
];
} else {
$date = [
'second' => [1 => 0],
'minutes' => $this->parseSegment($cron[0], 0, 59),
'hours' => $this->parseSegment($cron[1], 0, 23),
'day' => $this->parseSegment($cron[2], 1, 31),
'month' => $this->parseSegment($cron[3], 1, 12),
'week' => $this->parseSegment($cron[4], 0, 6),
];
}
return $date;
}
}

View File

@ -0,0 +1,6 @@
logs
.buildpath
.project
.settings
.idea
.DS_Store

View File

@ -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');

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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 Load Diff

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}
});
}
}
}

View File

@ -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();

View File

@ -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";
}
}

View File

@ -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();

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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 = "");
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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");
}
}

View File

@ -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'] = '';
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,342 @@
# Workerman
[![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge)
[![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman)
[![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman)
[![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman)
[![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman)
[![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman)
## What is it
Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications.
Workerman supports HTTP, Websocket, SSL and other custom protocols.
Workerman supports event extension.
## Requires
PHP 7.0 or Higher
A POSIX compatible operating system (Linux, OSX, BSD)
POSIX and PCNTL extensions required
Event extension recommended for better performance
## Installation
```
composer require workerman/workerman
```
## Basic Usage
### A websocket server
```php
<?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 ```
![workerman start](http://www.workerman.net/img/workerman-start.png)
```php start.php status ```
![workerman satus](http://www.workerman.net/img/workerman-status.png?a=123)
```php start.php connections```
```php start.php stop ```
```php start.php restart ```
```php start.php reload ```
## Documentation
中文主页:[http://www.workerman.net](https://www.workerman.net)
中文文档: [https://www.workerman.net/doc/workerman](https://www.workerman.net/doc/workerman)
Documentation:[https://github.com/walkor/workerman-manual](https://github.com/walkor/workerman-manual/blob/master/english/SUMMARY.md)
# Benchmarks
https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=db&l=yyku7z-e7&a=2
![image](https://user-images.githubusercontent.com/6073368/146704320-1559fe97-aa67-4ee3-95d6-61e341b3c93b.png)
## Sponsors
[opencollective.com/walkor](https://opencollective.com/walkor)
[patreon.com/walkor](https://patreon.com/walkor)
## Donate
<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).

View File

@ -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 Load Diff

View File

@ -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"
}