- 框架初始化
 - 安装插件
 - 修复PHP8.4报错
This commit is contained in:
2025-04-19 17:21:20 +08:00
commit c6a4e1f5f6
5306 changed files with 967782 additions and 0 deletions

101
vendor/workerman/channel/README.md vendored Normal file
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();
```

12
vendor/workerman/channel/composer.json vendored Normal file
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"}
}
}

394
vendor/workerman/channel/src/Client.php vendored Normal file
View File

@@ -0,0 +1,394 @@
<?php
namespace Channel;
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Timer;
use Workerman\Protocols\Frame;
/**
* Channel/Client
* @version 1.0.7
*/
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 (PHP_SAPI !== 'cli' || !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);
}
}
}

89
vendor/workerman/channel/src/Queue.php vendored Normal file
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();
}
}

179
vendor/workerman/channel/src/Server.php vendored Normal file
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));
}
}

53
vendor/workerman/channel/test/queue.php vendored Normal file
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();

184
vendor/workerman/phpsocket.io/README.md vendored Normal file
View File

@@ -0,0 +1,184 @@
# phpsocket.io
A server side alternative implementation of [socket.io](https://github.com/socketio/socket.io) in PHP based on [Workerman](https://github.com/walkor/Workerman).<br>
# Notice
Only support socket.io >= v1.3.0 and <= v2.x <br>
This project is just translate socket.io by [workerman](https://github.com/walkor/Workerman).<br>
More api just see [https://socket.io/docs/v2/server-api/](https://socket.io/docs/v2/server-api/)
# Install
composer require workerman/phpsocket.io
# Examples
## Simple chat
start.php
```php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// Listen port 2021 for socket.io client
$io = new SocketIO(2021);
$io->on('connection', function ($socket) use ($io) {
$socket->on('chat message', function ($msg) use ($io) {
$io->emit('chat message', $msg);
});
});
Worker::runAll();
```
## Another chat demo
https://github.com/walkor/phpsocket.io/blob/master/examples/chat/start_io.php
```php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
// Listen port 2020 for socket.io client
$io = new SocketIO(2020);
$io->on('connection', function ($socket) {
$socket->addedUser = false;
// When the client emits 'new message', this listens and executes
$socket->on('new message', function ($data) use ($socket) {
// We tell the client to execute 'new message'
$socket->broadcast->emit('new message', array(
'username' => $socket->username,
'message' => $data
));
});
// When the client emits 'add user', this listens and executes
$socket->on('add user', function ($username) use ($socket) {
global $usernames, $numUsers;
// We store the username in the socket session for this client
$socket->username = $username;
// Add the client's username to the global list
$usernames[$username] = $username;
++$numUsers;
$socket->addedUser = true;
$socket->emit('login', array(
'numUsers' => $numUsers
));
// echo globally (all clients) that a person has connected
$socket->broadcast->emit('user joined', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
});
// When the client emits 'typing', we broadcast it to others
$socket->on('typing', function () use ($socket) {
$socket->broadcast->emit('typing', array(
'username' => $socket->username
));
});
// When the client emits 'stop typing', we broadcast it to others
$socket->on('stop typing', function () use ($socket) {
$socket->broadcast->emit('stop typing', array(
'username' => $socket->username
));
});
// When the user disconnects, perform this
$socket->on('disconnect', function () use ($socket) {
global $usernames, $numUsers;
// Remove the username from global usernames list
if ($socket->addedUser) {
unset($usernames[$socket->username]);
--$numUsers;
// echo globally that this client has left
$socket->broadcast->emit('user left', array(
'username' => $socket->username,
'numUsers' => $numUsers
));
}
});
});
Worker::runAll();
```
## Enable SSL for https
**```(phpsocket.io>=1.1.1 && workerman>=3.3.7 required)```**
start.php
```php
<?php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
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
)
);
$io = new SocketIO(2021, $context);
$io->on('connection', function ($connection) use ($io) {
echo "New connection coming\n";
});
Worker::runAll();
```
## Acknowledgement callback
```php
use Workerman\Worker;
use PHPSocketIO\SocketIO;
require_once __DIR__ . '/vendor/autoload.php';
$io = new SocketIO(2021);
$io->on('connection', function ($connection) use ($io) {
$socket->on('message with ack', function ($data, $callback) use ($socket, $io) {
// acknowledgement callback
if ($callback && is_callable($callback)) {
$callback(0);
}
});
});
Worker::runAll();
```
# 手册
[中文手册](https://github.com/walkor/phpsocket.io/tree/master/docs/zh)
# Livedemo
[chat demo](http://demos.workerman.net/phpsocketio-chat/)
# Run chat example
cd examples/chat
## Start
```php start.php start``` for debug mode
```php start.php start -d ``` for daemon mode
## Stop
```php start.php stop```
## Status
```php start.php status```
# License
MIT

View File

@@ -0,0 +1,30 @@
{
"name": "workerman/phpsocket.io",
"description": "A server side alternative implementation of socket.io in PHP based on Workerman",
"type": "library",
"keywords": [
"socket.io",
"phpsocket.io",
"workerman",
"sockets",
"async",
"stream",
"server",
"non-blocking"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"require": {
"workerman/workerman": "^4.0.0",
"workerman/channel": ">=1.0.0",
"ext-json": "*"
},
"autoload": {
"psr-4": {
"PHPSocketIO\\": "./src"
}
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7"
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace PHPSocketIO;
use Exception;
class ChannelAdapter extends DefaultAdapter
{
protected $_channelId = null;
public static $ip = '127.0.0.1';
public static $port = 2206;
/**
* @throws Exception
*/
public function __construct($nsp)
{
parent::__construct($nsp);
$this->_channelId = (function_exists('random_int') ? random_int(1, 10000000) : rand(1, 10000000)) . "-" . (function_exists('posix_getpid') ? posix_getpid() : 1);
\Channel\Client::connect(self::$ip, self::$port);
\Channel\Client::$onMessage = [$this, 'onChannelMessage'];
\Channel\Client::subscribe("socket.io#/#");
Debug::debug('ChannelAdapter __construct');
}
public function __destruct()
{
Debug::debug('ChannelAdapter __destruct');
}
public function add($id, $room)
{
$this->sids[$id][$room] = true;
$this->rooms[$room][$id] = true;
$channel = "socket.io#/#$room#";
\Channel\Client::subscribe($channel);
}
public function del($id, $room)
{
unset($this->sids[$id][$room]);
unset($this->rooms[$room][$id]);
if (empty($this->rooms[$room])) {
unset($this->rooms[$room]);
$channel = "socket.io#/#$room#";
\Channel\Client::unsubscribe($channel);
}
}
public function delAll($id)
{
$rooms = isset($this->sids[$id]) ? array_keys($this->sids[$id]) : [];
if ($rooms) {
foreach ($rooms as $room) {
if (isset($this->rooms[$room][$id])) {
unset($this->rooms[$room][$id]);
$channel = "socket.io#/#$room#";
\Channel\Client::unsubscribe($channel);
}
if (isset($this->rooms[$room]) && empty($this->rooms[$room])) {
unset($this->rooms[$room]);
}
}
}
unset($this->sids[$id]);
}
public function onChannelMessage($channel, $msg)
{
if ($this->_channelId === array_shift($msg)) {
return;
}
$packet = $msg[0];
$opts = $msg[1];
if (! $packet) {
echo "invalid channel:$channel packet \n";
return;
}
if (empty($packet['nsp'])) {
$packet['nsp'] = '/';
}
if ($packet['nsp'] != $this->nsp->name) {
echo "ignore different namespace {$packet['nsp']} != {$this->nsp->name}\n";
return;
}
$this->broadcast($packet, $opts, true);
}
public function broadcast($packet, $opts, $remote = false)
{
parent::broadcast($packet, $opts);
if (! $remote) {
$packet['nsp'] = '/';
if (! empty($opts['rooms'])) {
foreach ($opts['rooms'] as $room) {
$chn = "socket.io#/#$room#";
$msg = [$this->_channelId, $packet, $opts];
\Channel\Client::publish($chn, $msg);
}
} else {
$chn = "socket.io#/#";
$msg = [$this->_channelId, $packet, $opts];
\Channel\Client::publish($chn, $msg);
}
}
}
}

View File

@@ -0,0 +1,250 @@
<?php
namespace PHPSocketIO;
use Exception;
use PHPSocketIO\Parser\Decoder;
use PHPSocketIO\Parser\Encoder;
use PHPSocketIO\Parser\Parser;
class Client
{
public $server = null;
public $conn = null;
public $encoder = null;
public $decoder = null;
public $id = null;
public $request = null;
public $nsps = [];
public $connectBuffer = [];
/**
* @var array|mixed|null
*/
public $sockets;
public function __construct($server, $conn)
{
$this->server = $server;
$this->conn = $conn;
$this->encoder = new Encoder();
$this->decoder = new Decoder();
$this->id = $conn->id;
$this->request = $conn->request;
$this->setup();
Debug::debug('Client __construct');
}
public function __destruct()
{
Debug::debug('Client __destruct');
}
/**
* Sets up event listeners.
*
* @api private
*/
public function setup()
{
$this->decoder->on('decoded', [$this, 'ondecoded']);
$this->conn->on('data', [$this, 'ondata']);
$this->conn->on('error', [$this, 'onerror']);
$this->conn->on('close', [$this, 'onclose']);
}
/**
* Connects a client to a namespace.
*
* @param {String} namespace name
* @api private
*/
public function connect($name)
{
if (! isset($this->server->nsps[$name])) {
$this->packet(['type' => Parser::ERROR, 'nsp' => $name, 'data' => 'Invalid namespace']);
return;
}
$nsp = $this->server->of($name);
if ('/' !== $name && ! isset($this->nsps['/'])) {
$this->connectBuffer[$name] = $name;
return;
}
$nsp->add($this, $nsp, [$this, 'nspAdd']);
}
public function nspAdd($socket, $nsp)
{
$this->sockets[$socket->id] = $socket;
$this->nsps[$nsp->name] = $socket;
if ('/' === $nsp->name && $this->connectBuffer) {
foreach ($this->connectBuffer as $name) {
$this->connect($name);
}
$this->connectBuffer = [];
}
}
/**
* Disconnects from all namespaces and closes transport.
*
* @api private
*/
public function disconnect()
{
foreach ($this->sockets as $socket) {
$socket->disconnect();
}
$this->sockets = [];
$this->close();
}
/**
* Removes a socket. Called by each `Socket`.
*
* @api private
*/
public function remove($socket)
{
if (isset($this->sockets[$socket->id])) {
$nsp = $this->sockets[$socket->id]->nsp->name;
unset($this->sockets[$socket->id]);
unset($this->nsps[$nsp]);
}
}
/**
* Closes the underlying connection.
*
* @api private
*/
public function close()
{
if (empty($this->conn)) {
return;
}
if ('open' === $this->conn->readyState) {
$this->conn->close();
$this->onclose('forced server close');
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} options
* @api private
*/
public function packet($packet, $preEncoded = false, $volatile = false)
{
if (! empty($this->conn) && 'open' === $this->conn->readyState) {
if (! $preEncoded) {
// not broadcasting, need to encode
$encodedPackets = $this->encoder->encode($packet);
$this->writeToEngine($encodedPackets, $volatile);
} else { // a broadcast pre-encodes a packet
$this->writeToEngine($packet);
}
}
}
public function writeToEngine($encodedPackets, $volatile = false)
{
if ($volatile) {
echo new Exception('volatile');
}
if ($volatile && ! $this->conn->transport->writable) {
return;
}
if (isset($encodedPackets['nsp'])) {
unset($encodedPackets['nsp']);
}
foreach ($encodedPackets as $packet) {
$this->conn->write($packet);
}
}
/**
* Called with incoming transport data.
*
* @api private
*/
public function ondata($data)
{
try {
// todo chek '2["chat message","2"]' . "\0" . ''
$this->decoder->add(trim($data));
} catch (Exception $e) {
$this->onerror($e);
}
}
/**
* Called when parser fully decodes a packet.
*
* @api private
*/
public function ondecoded($packet)
{
if (Parser::CONNECT == $packet['type']) {
$this->connect($packet['nsp']);
} else {
if (isset($this->nsps[$packet['nsp']])) {
$this->nsps[$packet['nsp']]->onpacket($packet);
}
}
}
/**
* Handles an error.
*
* @param {Objcet} error object
* @api private
*/
public function onerror($err)
{
foreach ($this->sockets as $socket) {
$socket->onerror($err);
}
$this->onclose('client error');
}
/**
* Called upon transport close.
*
* @param {String} reason
* @api private
*/
public function onclose($reason)
{
if (empty($this->conn)) {
return;
}
// ignore a potential subsequent `close` event
$this->destroy();
// `nsps` and `sockets` are cleaned up seamlessly
foreach ($this->sockets as $socket) {
$socket->onclose($reason);
}
$this->sockets = null;
}
/**
* Cleans up event listeners.
*
* @api private
*/
public function destroy()
{
if (! $this->conn) {
return;
}
$this->conn->removeAllListeners();
$this->decoder->removeAllListeners();
$this->encoder->removeAllListeners();
$this->server = $this->conn = $this->encoder = $this->decoder = $this->request = $this->nsps = null;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace PHPSocketIO;
class Debug
{
public static function debug($var)
{
global $debug;
if ($debug) {
echo var_export($var, true) . "\n";
}
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace PHPSocketIO;
class DefaultAdapter
{
public $nsp = null;
public $rooms = [];
public $sids = [];
public $encoder = null;
public function __construct($nsp)
{
$this->nsp = $nsp;
$this->encoder = new Parser\Encoder();
Debug::debug('DefaultAdapter __construct');
}
public function __destruct()
{
Debug::debug('DefaultAdapter __destruct');
}
public function add($id, $room)
{
$this->sids[$id][$room] = true;
$this->rooms[$room][$id] = true;
}
public function del($id, $room)
{
unset($this->sids[$id][$room]);
unset($this->rooms[$room][$id]);
if (empty($this->rooms[$room])) {
unset($this->rooms[$room]);
}
}
public function delAll($id)
{
$rooms = array_keys($this->sids[$id] ?? []);
foreach ($rooms as $room) {
$this->del($id, $room);
}
unset($this->sids[$id]);
}
public function broadcast($packet, $opts, $remote = false)
{
$rooms = $opts['rooms'] ?? [];
$except = $opts['except'] ?? [];
$flags = $opts['flags'] ?? [];
$packetOpts = [
'preEncoded' => true,
'volatile' => $flags['volatile'] ?? null,
'compress' => $flags['compress'] ?? null
];
$packet['nsp'] = $this->nsp->name;
$encodedPackets = $this->encoder->encode($packet);
if ($rooms) {
$ids = [];
foreach ($rooms as $i => $room) {
if (! isset($this->rooms[$room])) {
continue;
}
$room = $this->rooms[$room];
foreach ($room as $id => $item) {
if (isset($ids[$id]) || isset($except[$id])) {
continue;
}
if (isset($this->nsp->connected[$id])) {
$ids[$id] = true;
$this->nsp->connected[$id]->packet($encodedPackets, $packetOpts);
}
}
}
} else {
foreach ($this->sids as $id => $sid) {
if (isset($except[$id])) {
continue;
}
if (isset($this->nsp->connected[$id])) {
$socket = $this->nsp->connected[$id];
$volatile = $flags['volatile'] ?? null;
$socket->packet($encodedPackets, true, $volatile);
}
}
}
}
public function clients($rooms, $fn)
{
$sids = [];
foreach ($rooms as $room) {
$sids = array_merge($sids, $this->rooms[$room]);
}
$fn();
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace PHPSocketIO\Engine;
use Exception;
use PHPSocketIO\Engine\Transports\WebSocket;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Debug;
class Engine extends Emitter
{
public $server;
public $pingTimeout = 60;
public $pingInterval = 25;
public $upgradeTimeout = 5;
public $transports = [];
public $allowUpgrades = [];
public $allowRequest = [];
public $clients = [];
public $origins = '*:*';
public static $allowTransports = [
'polling' => 'polling',
'websocket' => 'websocket'
];
public static $errorMessages = [
'Transport unknown',
'Session ID unknown',
'Bad handshake method',
'Bad request'
];
private const ERROR_UNKNOWN_TRANSPORT = 0;
private const ERROR_UNKNOWN_SID = 1;
private const ERROR_BAD_HANDSHAKE_METHOD = 2;
private const ERROR_BAD_REQUEST = 3;
public function __construct($opts = [])
{
$ops_map = [
'pingTimeout',
'pingInterval',
'upgradeTimeout',
'transports',
'allowUpgrades',
'allowRequest'
];
foreach ($ops_map as $key) {
if (isset($opts[$key])) {
$this->$key = $opts[$key];
}
}
Debug::debug('Engine __construct');
}
public function __destruct()
{
Debug::debug('Engine __destruct');
}
public function handleRequest(object $req, object $res)
{
$this->prepare($req);
$req->res = $res;
$this->verify($req, $res, false, [$this, 'dealRequest']);
}
/**
* @throws Exception
*/
public function dealRequest($err, bool $success, object $req)
{
if (! $success) {
self::sendErrorMessage($req, $req->res, $err);
return;
}
if (isset($req->_query['sid'])) {
$this->clients[$req->_query['sid']]->transport->onRequest($req);
} else {
$this->handshake($req->_query['transport'], $req);
}
}
protected function sendErrorMessage(object $req, object $res, ?string $code): void
{
$headers = ['Content-Type' => 'application/json'];
if (isset($req->headers['origin'])) {
$headers['Access-Control-Allow-Credentials'] = 'true';
$headers['Access-Control-Allow-Origin'] = $req->headers['origin'];
} else {
$headers['Access-Control-Allow-Origin'] = '*';
}
$res->writeHead(403, '', $headers);
$res->end(
json_encode(
[
'code' => $code,
'message' => self::$errorMessages[$code] ?? $code
]
)
);
}
protected function verify(object $req, object $res, bool $upgrade, callable $fn)
{
if (! isset($req->_query['transport']) || ! isset(self::$allowTransports[$req->_query['transport']])) {
return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res);
}
$transport = $req->_query['transport'];
$sid = $req->_query['sid'] ?? '';
if ($sid) {
if (! isset($this->clients[$sid])) {
return call_user_func($fn, self::ERROR_UNKNOWN_SID, false, $req, $res);
}
if (! $upgrade && $this->clients[$sid]->transport->name !== $transport) {
return call_user_func($fn, self::ERROR_BAD_REQUEST, false, $req, $res);
}
} else {
if ('GET' !== $req->method) {
return call_user_func($fn, self::ERROR_BAD_HANDSHAKE_METHOD, false, $req, $res);
}
return $this->checkRequest($req, $res, $fn);
}
call_user_func($fn, null, true, $req, $res);
}
public function checkRequest(object $req, object $res, callable $fn)
{
if ($this->origins === "*:*" || empty($this->origins)) {
return call_user_func($fn, null, true, $req, $res);
}
$origin = null;
if (isset($req->headers['origin'])) {
$origin = $req->headers['origin'];
} elseif (isset($req->headers['referer'])) {
$origin = $req->headers['referer'];
}
// file:// URLs produce a null Origin which can't be authorized via echo-back
if ('null' === $origin || null === $origin) {
return call_user_func($fn, null, true, $req, $res);
}
if ($origin) {
$parts = parse_url($origin);
$defaultPort = 'https:' === $parts['scheme'] ? 443 : 80;
$parts['port'] = $parts['port'] ?? $defaultPort;
$allowed_origins = explode(' ', $this->origins);
foreach ($allowed_origins as $allow_origin) {
$ok =
$allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'] ||
$allow_origin === $parts['scheme'] . '://' . $parts['host'] ||
$allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':*' ||
$allow_origin === '*:' . $parts['port'];
if ($ok) {
return call_user_func($fn, null, true, $req, $res);
}
}
}
call_user_func($fn, null, false, $req, $res);
}
protected function prepare(object $req)
{
if (! isset($req->_query)) {
$info = parse_url($req->url);
if (isset($info['query'])) {
parse_str($info['query'], $req->_query);
}
}
}
/**
* @throws Exception
*/
public function handshake(string $transport, object $req)
{
$id = bin2hex(pack('d', microtime(true)) . pack('N', function_exists('random_int') ? random_int(1, 100000000) : rand(1, 100000000)));
if ($transport == 'websocket') {
$transport = '\\PHPSocketIO\\Engine\\Transports\\WebSocket';
} elseif (isset($req->_query['j'])) {
$transport = '\\PHPSocketIO\\Engine\\Transports\\PollingJsonp';
} else {
$transport = '\\PHPSocketIO\\Engine\\Transports\\PollingXHR';
}
$transport = new $transport($req);
$transport->supportsBinary = ! isset($req->_query['b64']);
$socket = new Socket($id, $this, $transport, $req);
$transport->onRequest($req);
$this->clients[$id] = $socket;
$socket->once('close', [$this, 'onSocketClose']);
$this->emit('connection', $socket);
}
public function onSocketClose($id): void
{
unset($this->clients[$id]);
}
public function attach($worker): void
{
$this->server = $worker;
$worker->onConnect = [$this, 'onConnect'];
}
public function onConnect(object $connection): void
{
$connection->onRequest = [$this, 'handleRequest'];
$connection->onWebSocketConnect = [$this, 'onWebSocketConnect'];
// clean
$connection->onClose = function ($connection) {
if (! empty($connection->httpRequest)) {
$connection->httpRequest->destroy();
$connection->httpRequest = null;
}
if (! empty($connection->httpResponse)) {
$connection->httpResponse->destroy();
$connection->httpResponse = null;
}
if (! empty($connection->onRequest)) {
$connection->onRequest = null;
}
if (! empty($connection->onWebSocketConnect)) {
$connection->onWebSocketConnect = null;
}
};
}
public function onWebSocketConnect($connection, object $req, object $res): void
{
$this->prepare($req);
$this->verify($req, $res, true, [$this, 'dealWebSocketConnect']);
}
/**
* @throws Exception
*/
public function dealWebSocketConnect($err, bool $success, object $req, object $res): void
{
if (! $success) {
self::sendErrorMessage($req, $res, $err);
return;
}
if (isset($req->_query['sid'])) {
if (! isset($this->clients[$req->_query['sid']])) {
self::sendErrorMessage($req, $res, 'upgrade attempt for closed client');
return;
}
$client = $this->clients[$req->_query['sid']];
if ($client->upgrading) {
self::sendErrorMessage($req, $res, 'transport has already been trying to upgrade');
return;
}
if ($client->upgraded) {
self::sendErrorMessage($req, $res, 'transport had already been upgraded');
return;
}
$transport = new WebSocket($req);
$client->maybeUpgrade($transport);
} else {
$this->handshake($req->_query['transport'], $req);
}
}
}

View File

@@ -0,0 +1,253 @@
<?php
namespace PHPSocketIO\Engine;
use Exception;
use PHPSocketIO\Debug;
class Parser
{
public function __construct()
{
Debug::debug('Engine/Parser __construct');
}
public static $packets = [
'open' => 0, // non-ws
'close' => 1, // non-ws
'ping' => 2,
'pong' => 3,
'message' => 4,
'upgrade' => 5,
'noop' => 6,
];
public static $packetsList = [
'open',
'close',
'ping',
'pong',
'message',
'upgrade',
'noop'
];
public static $err = [
'type' => 'error',
'data' => 'parser error'
];
public static function encodePacket($packet): string
{
$data = ! isset($packet['data']) ? '' : $packet['data'];
return self::$packets[$packet['type']] . $data;
}
/**
* Decodes a packet. Data also available as an ArrayBuffer if requested.
*
* @return array|string[] {Object} with `type` and `data` (if any)
*/
public static function decodePacket(string $data): array
{
if ($data[0] === 'b') {
return self::decodeBase64Packet(substr($data, 1));
}
$type = $data[0];
if (! isset(self::$packetsList[$type])) {
return self::$err;
}
if (isset($data[1])) {
return ['type' => self::$packetsList[$type], 'data' => substr($data, 1)];
} else {
return ['type' => self::$packetsList[$type]];
}
}
/**
* Decodes a packet encoded in a base64 string.
*
* @param $msg
* @return array {Object} with `type` and `data` (if any)
*/
public static function decodeBase64Packet($msg): array
{
$type = self::$packetsList[$msg[0]];
$data = base64_decode(substr($msg, 1));
return ['type' => $type, 'data' => $data];
}
/**
* Encodes multiple messages (payload).
*
* <length>:data
*
* Example:
*
* 11:hello world2:hi
*
* If any contents are binary, they will be encoded as base64 strings. Base64
* encoded strings are marked with a b before the length specifier
*
* @param {Array} packets
* @api private
*/
public static function encodePayload($packets, $supportsBinary = null): string
{
if ($supportsBinary) {
return self::encodePayloadAsBinary($packets);
}
if (! $packets) {
return '0:';
}
$results = '';
foreach ($packets as $msg) {
$results .= self::encodeOne($msg);
}
return $results;
}
public static function encodeOne($packet): string
{
$message = self::encodePacket($packet);
return strlen($message) . ':' . $message;
}
/*
* Decodes data when a payload is maybe expected. Possible binary contents are
* decoded from their base64 representation
*
* @api public
*/
public static function decodePayload($data, $binaryType = null)
{
if (! preg_match('/^\d+:\d/', $data)) {
return self::decodePayloadAsBinary($data, $binaryType);
}
if ($data === '') {
// parser error - ignoring payload
return self::$err;
}
$length = '';//, n, msg;
for ($i = 0, $l = strlen($data); $i < $l; $i++) {
$chr = $data[$i];
if (':' != $chr) {
$length .= $chr;
} else {
if ('' == $length || ($length != ($n = intval($length)))) {
// parser error - ignoring payload
return self::$err;
}
$msg = substr($data, $i + 1);
if (isset($msg[0])) {
$packet = self::decodePacket($msg);
if (self::$err['type'] == $packet['type'] && self::$err['data'] == $packet['data']) {
// parser error in individual packet - ignoring payload
return self::$err;
}
return $packet;
}
// advance cursor
$i += $n;
$length = '';
}
}
if ($length !== '') {
// parser error - ignoring payload
echo new Exception('parser error');
return self::$err;
}
}
/**
* Encodes multiple messages (payload) as binary.
*
* <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
* 255><data>
*
* Example:
* 1 3 255 1 2 3, if the binary contents are interpreted as 8-bit integers
*
* @param {Array} packets
* @return string {Buffer} encoded payload
* @api private
*/
public static function encodePayloadAsBinary($packets): string
{
$results = '';
foreach ($packets as $msg) {
$results .= self::encodeOneAsBinary($msg);
}
return $results;
}
public static function encodeOneAsBinary($p): string
{
$packet = self::encodePacket($p);
$encodingLength = '' . strlen($packet);
$sizeBuffer = chr(0);
for ($i = 0; $i < strlen($encodingLength); $i++) {
$sizeBuffer .= chr($encodingLength[$i]);
}
$sizeBuffer .= chr(255);
return $sizeBuffer . $packet;
}
/*
* Decodes data when a payload is maybe expected. Strings are decoded by
* interpreting each byte as a key code for entries marked to start with 0. See
* description of encodePayloadAsBinary
* @api public
*/
public static function decodePayloadAsBinary($data, $binaryType = null): array
{
$bufferTail = $data;
$buffers = [];
while (strlen($bufferTail) > 0) {
$strLen = '';
$numberTooLong = false;
for ($i = 1;; $i++) {
$tail = ord($bufferTail[$i]);
if ($tail === 255) {
break;
}
// 310 = char length of Number.MAX_VALUE
if (strlen($strLen) > 310) {
$numberTooLong = true;
break;
}
$strLen .= $tail;
}
if ($numberTooLong) {
return self::$err;
}
$bufferTail = substr($bufferTail, strlen($strLen) + 1);
$msgLength = intval($strLen);
$msg = substr($bufferTail, 1, $msgLength + 1);
$buffers[] = $msg;
$bufferTail = substr($bufferTail, $msgLength + 1);
}
$packets = [];
foreach ($buffers as $i => $buffer) {
$packets[] = self::decodePacket($buffer);
}
return $packets;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace PHPSocketIO\Engine\Protocols\Http;
class Request
{
public $onData = null;
public $onEnd = null;
public $onClose = null;
public $httpVersion = null;
public $headers = [];
public $rawHeaders = null;
public $method = null;
public $url = null;
public $connection = null;
public $_query = null;
public $res = null;
public $cleanup = null;
public function __construct($connection, $raw_head)
{
$this->connection = $connection;
$this->parseHead($raw_head);
}
public function parseHead($raw_head)
{
$header_data = explode("\r\n", $raw_head);
list($this->method, $this->url, $protocol) = explode(' ', $header_data[0]);
list($null, $this->httpVersion) = explode('/', $protocol);
unset($header_data[0]);
foreach ($header_data as $content) {
if (empty($content)) {
continue;
}
$this->rawHeaders[] = $content;
list($key, $value) = explode(':', $content, 2);
$this->headers[strtolower($key)] = trim($value);
}
}
public function destroy()
{
$this->onData = $this->onEnd = $this->onClose = null;
$this->connection = null;
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace PHPSocketIO\Engine\Protocols\Http;
use Exception;
class Response
{
public $statusCode = 200;
protected $_statusPhrase = null;
protected $_connection = null;
protected $_headers = [];
public $headersSent = false;
public $writable = true;
protected $_buffer = '';
public function __construct($connection)
{
$this->_connection = $connection;
}
protected function initHeader()
{
$this->_headers['Connection'] = 'keep-alive';
$this->_headers['Content-Type'] = 'Content-Type: text/html;charset=utf-8';
}
public function writeHead($status_code, $reason_phrase = '', $headers = null)
{
if ($this->headersSent) {
echo "header has already send\n";
return false;
}
$this->statusCode = $status_code;
if ($reason_phrase) {
$this->_statusPhrase = $reason_phrase;
}
if ($headers) {
foreach ($headers as $key => $val) {
$this->_headers[$key] = $val;
}
}
$this->_buffer = $this->getHeadBuffer();
$this->headersSent = true;
}
public function getHeadBuffer(): string
{
if (! $this->_statusPhrase) {
$this->_statusPhrase = self::$codes[$this->statusCode] ?? '';
}
$head_buffer = "HTTP/1.1 $this->statusCode $this->_statusPhrase\r\n";
if (! isset($this->_headers['Content-Length']) && ! isset($this->_headers['Transfer-Encoding'])) {
$head_buffer .= "Transfer-Encoding: chunked\r\n";
}
if (! isset($this->_headers['Connection'])) {
$head_buffer .= "Connection: keep-alive\r\n";
}
foreach ($this->_headers as $key => $val) {
if ($key === 'Set-Cookie' && is_array($val)) {
foreach ($val as $v) {
$head_buffer .= "Set-Cookie: $v\r\n";
}
continue;
}
$head_buffer .= "$key: $val\r\n";
}
return $head_buffer . "\r\n";
}
public function setHeader($key, $val)
{
$this->_headers[$key] = $val;
}
public function getHeader($name)
{
return $this->_headers[$name] ?? '';
}
public function removeHeader($name)
{
unset($this->_headers[$name]);
}
public function write($chunk)
{
if (! isset($this->_headers['Content-Length'])) {
$chunk = dechex(strlen($chunk)) . "\r\n" . $chunk . "\r\n";
}
if (! $this->headersSent) {
$head_buffer = $this->getHeadBuffer();
$this->_buffer = $head_buffer . $chunk;
$this->headersSent = true;
} else {
$this->_buffer .= $chunk;
}
}
public function end($data = null)
{
if (! $this->writable) {
echo new Exception('unwirtable');
return false;
}
if ($data !== null) {
$this->write($data);
}
if (! $this->headersSent) {
$head_buffer = $this->getHeadBuffer();
$this->_buffer = $head_buffer;
$this->headersSent = true;
}
if (! isset($this->_headers['Content-Length'])) {
$ret = $this->_connection->send($this->_buffer . "0\r\n\r\n", true);
$this->destroy();
return $ret;
}
$ret = $this->_connection->send($this->_buffer, true);
$this->destroy();
return $ret;
}
public function destroy()
{
if (! empty($this->_connection->httpRequest)) {
$this->_connection->httpRequest->destroy();
}
if (! empty($this->_connection)) {
$this->_connection->httpResponse = $this->_connection->httpRequest = null;
}
$this->_connection = null;
$this->writable = false;
}
public static $codes = [
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
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 Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
422 => 'Unprocessable Entity',
423 => 'Locked',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
];
}

View File

@@ -0,0 +1,170 @@
<?php
namespace PHPSocketIO\Engine\Protocols;
use Exception;
use PHPSocketIO\Engine\Protocols\Http\Request;
use PHPSocketIO\Engine\Protocols\Http\Response;
use Workerman\Connection\TcpConnection;
class SocketIO
{
public static function input($http_buffer, $connection)
{
if (! empty($connection->hasReadedHead)) {
return strlen($http_buffer);
}
$pos = strpos($http_buffer, "\r\n\r\n");
if (! $pos) {
if (strlen($http_buffer) >= $connection->maxPackageSize) {
$connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long");
return 0;
}
return 0;
}
$head_len = $pos + 4;
$raw_head = substr($http_buffer, 0, $head_len);
$raw_body = substr($http_buffer, $head_len);
$req = new Request($connection, $raw_head);
$res = new Response($connection);
$connection->httpRequest = $req;
$connection->httpResponse = $res;
$connection->hasReadedHead = true;
TcpConnection::$statistics['total_request']++;
$connection->onClose = '\PHPSocketIO\Engine\Protocols\SocketIO::emitClose';
if (isset($req->headers['upgrade']) && strtolower($req->headers['upgrade']) === 'websocket') {
$connection->consumeRecvBuffer(strlen($http_buffer));
WebSocket::dealHandshake($connection, $req, $res);
self::cleanup($connection);
return 0;
}
if (! empty($connection->onRequest)) {
$connection->consumeRecvBuffer(strlen($http_buffer));
self::emitRequest($connection, $req, $res);
if ($req->method === 'GET' || $req->method === 'OPTIONS') {
self::emitEnd($connection, $req);
return 0;
}
// POST
if ('\PHPSocketIO\Engine\Protocols\SocketIO::onData' !== $connection->onMessage) {
$connection->onMessage = '\PHPSocketIO\Engine\Protocols\SocketIO::onData';
}
if (! $raw_body) {
return 0;
}
self::onData($connection, $raw_body);
return 0;
} else {
if ($req->method === 'GET') {
return $pos + 4;
} elseif (isset($req->headers['content-length'])) {
return $req->headers['content-length'];
} else {
$connection->close("HTTP/1.1 400 bad request\r\n\r\ntrunk not support");
return 0;
}
}
}
public static function onData($connection, $data)
{
$req = $connection->httpRequest;
self::emitData($connection, $req, $data);
if ((isset($req->headers['content-length']) && $req->headers['content-length'] <= strlen($data))
|| substr($data, -5) === "0\r\n\r\n"
) {
self::emitEnd($connection, $req);
}
}
protected static function emitRequest($connection, $req, $res)
{
try {
call_user_func($connection->onRequest, $req, $res);
} catch (Exception $e) {
echo $e;
}
}
public static function emitClose($connection)
{
$req = $connection->httpRequest;
if (isset($req->onClose)) {
try {
call_user_func($req->onClose, $req);
} catch (Exception $e) {
echo $e;
}
}
$res = $connection->httpResponse;
if (isset($res->onClose)) {
try {
call_user_func($res->onClose, $res);
} catch (Exception $e) {
echo $e;
}
}
self::cleanup($connection);
}
public static function cleanup($connection)
{
if (! empty($connection->onRequest)) {
$connection->onRequest = null;
}
if (! empty($connection->onWebSocketConnect)) {
$connection->onWebSocketConnect = null;
}
if (! empty($connection->httpRequest)) {
$connection->httpRequest->destroy();
$connection->httpRequest = null;
}
if (! empty($connection->httpResponse)) {
$connection->httpResponse->destroy();
$connection->httpResponse = null;
}
}
public static function emitData($connection, $req, $data)
{
if (isset($req->onData)) {
try {
call_user_func($req->onData, $req, $data);
} catch (Exception $e) {
echo $e;
}
}
}
public static function emitEnd($connection, $req)
{
if (isset($req->onEnd)) {
try {
call_user_func($req->onEnd, $req);
} catch (Exception $e) {
echo $e;
}
}
$connection->hasReadedHead = false;
}
public static function encode($buffer, $connection)
{
if (! isset($connection->onRequest)) {
$connection->httpResponse->setHeader('Content-Length', strlen($buffer));
return $connection->httpResponse->getHeadBuffer() . $buffer;
}
return $buffer;
}
public static function decode($http_buffer, $connection)
{
if (isset($connection->onRequest)) {
return $http_buffer;
} else {
list($head, $body) = explode("\r\n\r\n", $http_buffer, 2);
return $body;
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace PHPSocketIO\Engine\Protocols;
use PHPSocketIO\Engine\Protocols\Http\Request;
use PHPSocketIO\Engine\Protocols\Http\Response;
use PHPSocketIO\Engine\Protocols\WebSocket\RFC6455;
use Workerman\Connection\TcpConnection;
/**
* WebSocket 协议服务端解包和打包
*/
class WebSocket
{
/**
* 最小包头
*
* @var int
*/
const MIN_HEAD_LEN = 7;
/**
* 检查包的完整性
*
* @param string $buffer
*/
public static function input($buffer, $connection)
{
if (strlen($buffer) < self::MIN_HEAD_LEN) {
return 0;
}
// flash policy file
if (0 === strpos($buffer, '<policy')) {
$policy_xml = '<?xml version="1.0"?><cross-domain-policy><site-control permitted-cross-domain-policies="all"/><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>' . "\0";
$connection->send($policy_xml, true);
$connection->consumeRecvBuffer(strlen($buffer));
return 0;
}
// http head
$pos = strpos($buffer, "\r\n\r\n");
if (! $pos) {
if (strlen($buffer) >= TcpConnection::$maxPackageSize) {
$connection->close("HTTP/1.1 400 bad request\r\n\r\nheader too long");
return 0;
}
return 0;
}
$req = new Request($connection, $buffer);
$res = new Response($connection);
$connection->consumeRecvBuffer(strlen($buffer));
return self::dealHandshake($connection, $req, $res);
}
/**
* 处理websocket握手
*
* @param TcpConnection $connection
* @param $req
* @param $res
* @return int
*/
public static function dealHandshake($connection, $req, $res)
{
if (isset($req->headers['sec-websocket-key1'])) {
$res->writeHead(400);
$res->end("Not support");
return 0;
}
$connection->protocol = 'PHPSocketIO\Engine\Protocols\WebSocket\RFC6455';
return RFC6455::dealHandshake($connection, $req, $res);
}
}

View File

@@ -0,0 +1,300 @@
<?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 PHPSocketIO\Engine\Protocols\WebSocket;
use Workerman\Connection\ConnectionInterface;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\ProtocolInterface;
/**
* WebSocket 协议服务端解包和打包
*/
class RFC6455 implements ProtocolInterface
{
/**
* websocket头部最小长度
*
* @var int
*/
const MIN_HEAD_LEN = 6;
/**
* websocket blob类型
*
* @var string
*/
const BINARY_TYPE_BLOB = "\x81";
/**
* websocket arraybuffer类型
*
* @var string
*/
const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* 检查包的完整性
*
* @param string $buffer
*/
public static function input($buffer, ConnectionInterface $connection)
{
// 数据长度
$recv_len = strlen($buffer);
// 长度不够
if ($recv_len < self::MIN_HEAD_LEN) {
return 0;
}
// $connection->websocketCurrentFrameLength有值说明当前fin为0则缓冲websocket帧数据
if ($connection->websocketCurrentFrameLength) {
// 如果当前帧数据未收全,则继续收
if ($connection->websocketCurrentFrameLength > $recv_len) {
// 返回0因为不清楚完整的数据包长度需要等待fin=1的帧
return 0;
}
} else {
$data_len = ord($buffer[1]) & 127;
$firstbyte = ord($buffer[0]);
$is_fin_frame = $firstbyte >> 7;
$opcode = $firstbyte & 0xf;
switch ($opcode) {
// 附加数据帧 @todo 实现附加数据帧
case 0x1:
case 0x2:
case 0x0:
break;
// 文本数据帧
// 二进制数据帧
// 关闭的包
case 0x8:
// 如果有设置onWebSocketClose回调尝试执行
if (isset($connection->onWebSocketClose)) {
call_user_func($connection->onWebSocketClose, $connection);
} // 默认行为是关闭连接
else {
$connection->close();
}
return 0;
// ping的包
case 0x9:
// 如果有设置onWebSocketPing回调尝试执行
if (isset($connection->onWebSocketPing)) {
call_user_func($connection->onWebSocketPing, $connection);
} // 默认发送pong
else {
$connection->send(pack('H*', '8a00'), true);
}
// 从接受缓冲区中消费掉该数据包
if (! $data_len) {
$connection->consumeRecvBuffer(self::MIN_HEAD_LEN);
return 0;
}
break;
// pong的包
case 0xa:
// 如果有设置onWebSocketPong回调尝试执行
if (isset($connection->onWebSocketPong)) {
call_user_func($connection->onWebSocketPong, $connection);
}
// 从接受缓冲区中消费掉该数据包
if (! $data_len) {
$connection->consumeRecvBuffer(self::MIN_HEAD_LEN);
return 0;
}
break;
// 错误的opcode
default:
echo "error opcode $opcode and close websocket connection\n";
$connection->close();
return 0;
}
// websocket二进制数据
$head_len = self::MIN_HEAD_LEN;
if ($data_len === 126) {
$head_len = 8;
if ($head_len > $recv_len) {
return 0;
}
$pack = unpack('ntotal_len', substr($buffer, 2, 2));
$data_len = $pack['total_len'];
} elseif ($data_len === 127) {
$head_len = 14;
if ($head_len > $recv_len) {
return 0;
}
$arr = unpack('N2', substr($buffer, 2, 8));
$data_len = $arr[1] * 4294967296 + $arr[2];
}
$current_frame_length = $head_len + $data_len;
if ($is_fin_frame) {
return $current_frame_length;
} else {
$connection->websocketCurrentFrameLength = $current_frame_length;
}
}
// 收到的数据刚好是一个frame
if ($connection->websocketCurrentFrameLength == $recv_len) {
self::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$connection->websocketCurrentFrameLength = 0;
return 0;
} // 收到的数据大于一个frame
elseif ($connection->websocketCurrentFrameLength < $recv_len) {
self::decode(substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection);
$connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
$current_frame_length = $connection->websocketCurrentFrameLength;
$connection->websocketCurrentFrameLength = 0;
// 继续读取下一个frame
return self::input(substr($buffer, $current_frame_length), $connection);
} // 收到的数据不足一个frame
else {
return 0;
}
}
/**
* 打包
*
* @param string $buffer
* @return string
*/
public static function encode($buffer, ConnectionInterface $connection)
{
$len = strlen($buffer);
if (empty($connection->websocketHandshake)) {
// 默认是utf8文本格式
$connection->websocketType = self::BINARY_TYPE_BLOB;
}
$first_byte = $connection->websocketType;
if ($len <= 125) {
$encode_buffer = $first_byte . chr($len) . $buffer;
} elseif ($len <= 65535) {
$encode_buffer = $first_byte . chr(126) . pack("n", $len) . $buffer;
} else {
$encode_buffer = $first_byte . chr(127) . pack("xxxxN", $len) . $buffer;
}
// 还没握手不能发数据,先将数据缓冲起来,等握手完毕后发送
if (empty($connection->websocketHandshake)) {
if (empty($connection->websocketTmpData)) {
// 临时数据缓冲
$connection->websocketTmpData = '';
}
$connection->websocketTmpData .= $encode_buffer;
// 返回空,阻止发送
return '';
}
return $encode_buffer;
}
/**
* 解包
*
* @param string $buffer
* @return string
*/
public static function decode($buffer, ConnectionInterface $connection)
{
$masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} elseif ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
if ($connection->websocketCurrentFrameLength) {
$connection->websocketDataBuffer .= $decoded;
return $connection->websocketDataBuffer;
} else {
$decoded = $connection->websocketDataBuffer . $decoded;
$connection->websocketDataBuffer = '';
return $decoded;
}
}
/**
* 处理websocket握手
*
* @param TcpConnection $connection
* @param $req
* @param $res
* @return int
*/
public static function dealHandshake($connection, $req, $res)
{
$headers = [];
if (isset($connection->onWebSocketConnect)) {
try {
call_user_func_array($connection->onWebSocketConnect, [$connection, $req, $res]);
} catch (\Exception $e) {
echo $e;
}
if (! $res->writable) {
return false;
}
}
if (isset($req->headers['sec-websocket-key'])) {
$sec_websocket_key = $req->headers['sec-websocket-key'];
} else {
$res->writeHead(400);
$res->end('<b>400 Bad Request</b><br>Upgrade to websocket but Sec-WebSocket-Key not found.');
return 0;
}
// 标记已经握手
$connection->websocketHandshake = true;
// 缓冲fin为0的包直到fin为1
$connection->websocketDataBuffer = '';
// 当前数据帧的长度可能是fin为0的帧也可能是fin为1的帧
$connection->websocketCurrentFrameLength = 0;
// 当前帧的数据缓冲
$connection->websocketCurrentFrameBuffer = '';
// blob or arraybuffer
$connection->websocketType = self::BINARY_TYPE_BLOB;
$sec_websocket_accept = base64_encode(sha1($sec_websocket_key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$headers['Content-Length'] = 0;
$headers['Upgrade'] = 'websocket';
$headers['Sec-WebSocket-Version'] = 13;
$headers['Connection'] = 'Upgrade';
$headers['Sec-WebSocket-Accept'] = $sec_websocket_accept;
$res->writeHead(101, '', $headers);
$res->end();
// 握手后有数据要发送
if (! empty($connection->websocketTmpData)) {
$connection->send($connection->websocketTmpData, true);
$connection->websocketTmpData = '';
}
return 0;
}
}

View File

@@ -0,0 +1,360 @@
<?php
namespace PHPSocketIO\Engine;
use PHPSocketIO\Event\Emitter;
use Workerman\Timer;
use PHPSocketIO\Debug;
class Socket extends Emitter
{
public $id = 0;
public $server = null;
public $upgrading = false;
public $upgraded = false;
public $readyState = 'opening';
public $writeBuffer = [];
public $packetsFn = [];
public $sentCallbackFn = [];
public $request = null;
public $remoteAddress = '';
public $checkIntervalTimer;
public $upgradeTimeoutTimer = null;
public $pingTimeoutTimer = null;
public $upgradeTransport = null;
public $transport = null;
public function __construct($id, $server, $transport, $req)
{
$this->id = $id;
$this->server = $server;
$this->request = $req;
$this->remoteAddress = $req->connection->getRemoteIp() . ':' . $req->connection->getRemotePort();
$this->setTransport($transport);
$this->onOpen();
Debug::debug('Engine/Socket __construct');
}
public function __destruct()
{
Debug::debug('Engine/Socket __destruct');
}
public function maybeUpgrade(object $transport): void
{
$this->upgrading = true;
$this->upgradeTimeoutTimer = Timer::add(
$this->server->upgradeTimeout,
[$this, 'upgradeTimeoutCallback'],
[$transport],
false
);
$this->upgradeTransport = $transport;
$transport->on('packet', [$this, 'onUpgradePacket']);
$transport->once('close', [$this, 'onUpgradeTransportClose']);
$transport->once('error', [$this, 'onUpgradeTransportError']);
$this->once('close', [$this, 'onUpgradeTransportClose']);
}
public function onUpgradePacket(array $packet): void
{
if (empty($this->upgradeTransport)) {
$this->onError('upgradeTransport empty');
return;
}
if ('ping' === $packet['type'] && (isset($packet['data']) && 'probe' === $packet['data'])) {
$this->upgradeTransport->send([['type' => 'pong', 'data' => 'probe']]);
if ($this->checkIntervalTimer) {
Timer::del($this->checkIntervalTimer);
}
$this->checkIntervalTimer = Timer::add(0.5, [$this, 'check']);
} elseif ('upgrade' === $packet['type'] && $this->readyState !== 'closed') {
$this->upgradeCleanup();
$this->upgraded = true;
$this->clearTransport();
$this->transport->destroy();
$this->setTransport($this->upgradeTransport);
$this->emit('upgrade', $this->upgradeTransport);
$this->upgradeTransport = null;
$this->setPingTimeout();
$this->flush();
if ($this->readyState === 'closing') {
$this->transport->close([$this, 'onClose']);
}
} else {
$this->upgradeCleanup();
$this->upgradeTransport->close();
$this->upgradeTransport = null;
}
}
public function upgradeCleanup(): void
{
$this->upgrading = false;
Timer::del($this->checkIntervalTimer);
Timer::del($this->upgradeTimeoutTimer);
if (! empty($this->upgradeTransport)) {
$this->upgradeTransport->removeListener('packet', [$this, 'onUpgradePacket']);
$this->upgradeTransport->removeListener('close', [$this, 'onUpgradeTransportClose']);
$this->upgradeTransport->removeListener('error', [$this, 'onUpgradeTransportError']);
}
$this->removeListener('close', [$this, 'onUpgradeTransportClose']);
}
public function onUpgradeTransportClose(): void
{
$this->onUpgradeTransportError('transport closed');
}
public function onUpgradeTransportError($err): void
{
$this->upgradeCleanup();
if ($this->upgradeTransport) {
$this->upgradeTransport->close();
$this->upgradeTransport = null;
}
}
public function upgradeTimeoutCallback(object $transport): void
{
$this->upgradeCleanup();
if ('open' === $transport->readyState) {
$transport->close();
}
}
public function setTransport(object $transport)
{
$this->transport = $transport;
$this->transport->once('error', [$this, 'onError']);
$this->transport->on('packet', [$this, 'onPacket']);
$this->transport->on('drain', [$this, 'flush']);
$this->transport->once('close', [$this, 'onClose']);
//this function will manage packet events (also message callbacks)
$this->setupSendCallback();
}
public function onOpen(): void
{
$this->readyState = 'open';
$this->transport->sid = $this->id;
$this->sendPacket(
'open',
json_encode(
[
'sid' => $this->id,
'upgrades' => $this->getAvailableUpgrades(),
'pingInterval' => $this->server->pingInterval * 1000,
'pingTimeout' => $this->server->pingTimeout * 1000
]
)
);
$this->emit('open');
$this->setPingTimeout();
}
public function onPacket(array $packet)
{
if ('open' === $this->readyState) {
// export packet event
$this->emit('packet', $packet);
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
$this->setPingTimeout();
switch ($packet['type']) {
case 'ping':
$this->sendPacket('pong');
$this->emit('heartbeat');
break;
case 'error':
$this->onClose('parse error');
break;
case 'message':
$this->emit('data', $packet['data']);
$this->emit('message', $packet['data']);
break;
}
} else {
echo('packet received with closed socket');
}
}
public function check(): void
{
if ('polling' == $this->transport->name && $this->transport->writable) {
$this->transport->send([['type' => 'noop']]);
}
}
public function onError($err): void
{
$this->onClose('transport error', $err);
}
public function setPingTimeout(): void
{
if ($this->pingTimeoutTimer) {
Timer::del($this->pingTimeoutTimer);
}
$this->pingTimeoutTimer = Timer::add(
$this->server->pingInterval + $this->server->pingTimeout,
[$this, 'pingTimeoutCallback'],
null,
false
);
}
public function pingTimeoutCallback(): void
{
$this->transport->close();
$this->onClose('ping timeout');
}
public function clearTransport(): void
{
$this->transport->close();
Timer::del($this->pingTimeoutTimer);
}
public function onClose(string $reason = '', ?string $description = null): void
{
if ('closed' !== $this->readyState) {
Timer::del($this->pingTimeoutTimer);
if (! empty($this->checkIntervalTimer)) {
Timer::del($this->checkIntervalTimer);
}
$this->checkIntervalTimer = null;
if (! empty($this->checkIntervalTimer)) {
Timer::del($this->upgradeTimeoutTimer);
}
// clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event
$this->writeBuffer = [];
$this->packetsFn = [];
$this->sentCallbackFn = [];
$this->clearTransport();
$this->readyState = 'closed';
$this->emit('close', $this->id, $reason, $description);
$this->server = null;
$this->request = null;
$this->upgradeTransport = null;
$this->removeAllListeners();
if (! empty($this->transport)) {
$this->transport->removeAllListeners();
$this->transport = null;
}
}
}
public function send($data, $options, ?callable $callback): Socket
{
$this->sendPacket('message', $data, $callback);
return $this;
}
public function write($data, ?array $options = [], ?callable $callback = null): Socket
{
return $this->send($data, $options, $callback);
}
public function sendPacket(string $type, $data = null, $callback = null): void
{
if ('closing' !== $this->readyState) {
$packet = [
'type' => $type
];
if ($data !== null) {
$packet['data'] = $data;
}
// exports packetCreate event
$this->emit('packetCreate', $packet);
$this->writeBuffer[] = $packet;
//add send callback to object
if ($callback) {
$this->packetsFn[] = $callback;
}
$this->flush();
}
}
public function flush(): void
{
if ('closed' !== $this->readyState && $this->transport->writable
&& $this->writeBuffer
) {
$this->emit('flush', $this->writeBuffer);
$this->server->emit('flush', $this, $this->writeBuffer);
$wbuf = $this->writeBuffer;
$this->writeBuffer = [];
if ($this->packetsFn) {
if (! empty($this->transport->supportsFraming)) {
$this->sentCallbackFn[] = $this->packetsFn;
} else {
// @todo check
$this->sentCallbackFn[] = $this->packetsFn;
}
}
$this->packetsFn = [];
$this->transport->send($wbuf);
$this->emit('drain');
if ($this->server) {
$this->server->emit('drain', $this);
}
}
}
public function getAvailableUpgrades(): array
{
return ['websocket'];
}
public function close(): void
{
if ('open' !== $this->readyState) {
return;
}
$this->readyState = 'closing';
if ($this->writeBuffer) {
$this->once('drain', [$this, 'closeTransport']);
return;
}
$this->closeTransport();
}
public function closeTransport(): void
{
$this->transport->close([$this, 'onClose']);
}
public function setupSendCallback(): void
{
//the message was sent successfully, execute the callback
$this->transport->on('drain', [$this, 'onDrainCallback']);
}
public function onDrainCallback(): void
{
if ($this->sentCallbackFn) {
$seqFn = array_shift($this->sentCallbackFn);
if (is_callable($seqFn)) {
echo('executing send callback');
call_user_func($seqFn, $this->transport);
} elseif (is_array($seqFn)) {
echo('executing batch send callback');
foreach ($seqFn as $fn) {
call_user_func($fn, $this->transport);
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace PHPSocketIO\Engine;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Debug;
class Transport extends Emitter
{
public $readyState = 'opening';
public $req = null;
public $res = null;
public $shouldClose = null;
public function __construct()
{
Debug::debug('Transport __construct no access !!!!');
}
public function __destruct()
{
Debug::debug('Transport __destruct');
}
public function noop()
{
}
public function onRequest($req)
{
$this->req = $req;
}
public function close(?callable $fn = null): void
{
$this->readyState = 'closing';
$fn = $fn ?: [$this, 'noop'];
$this->doClose($fn);
}
public function onError(string $msg, ?string $desc = '')
{
if ($this->listeners('error')) {
$this->emit('error', "TransportError: {$desc}");
} else {
echo("ignored transport error $msg $desc\n");
}
}
public function onPacket($packet): void
{
$this->emit('packet', $packet);
}
public function onData($data)
{
$this->onPacket(Parser::decodePacket($data));
}
public function onClose()
{
$this->req = $this->res = null;
$this->readyState = 'closed';
$this->emit('close');
$this->removeAllListeners();
}
public function destroy(): void
{
$this->req = null;
$this->res = null;
$this->readyState = 'closed';
$this->removeAllListeners();
$this->shouldClose = null;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use PHPSocketIO\Engine\Transport;
use PHPSocketIO\Engine\Parser;
class Polling extends Transport
{
public $name = 'polling';
public $chunks = '';
public $shouldClose = null;
public $writable = false;
public $supportsBinary = null;
public $dataRes = null;
public $dataReq = null;
public function onRequest($req)
{
$res = $req->res;
if ('GET' === $req->method) {
$this->onPollRequest($req, $res);
} elseif ('POST' === $req->method) {
$this->onDataRequest($req, $res);
} else {
$res->writeHead(500);
$res->end();
}
}
public function onPollRequest(object $req, object $res): void
{
if ($this->req) {
$this->onError('overlap from client');
$res->writeHead(500);
return;
}
$this->req = $req;
$this->res = $res;
$req->onClose = [$this, 'pollRequestOnClose'];
$req->cleanup = [$this, 'pollRequestClean'];
$this->writable = true;
$this->emit('drain');
if ($this->writable && $this->shouldClose) {
echo('triggering empty send to append close packet');
$this->send([['type' => 'noop']]);
}
}
public function pollRequestOnClose(): void
{
$this->onError('poll connection closed prematurely');
$this->pollRequestClean();
}
public function pollRequestClean(): void
{
if (isset($this->req)) {
$this->req = null;
$this->res = null;
}
}
public function onDataRequest($req, $res): void
{
if (isset($this->dataReq)) {
$this->onError('data request overlap from client');
$res->writeHead(500);
return;
}
$this->dataReq = $req;
$this->dataRes = $res;
$req->onClose = [$this, 'dataRequestOnClose'];
$req->onData = [$this, 'dataRequestOnData'];
$req->onEnd = [$this, 'dataRequestOnEnd'];
}
public function dataRequestCleanup(): void
{
$this->chunks = '';
$this->dataReq = null;
$this->dataRes = null;
}
public function dataRequestOnClose(): void
{
$this->dataRequestCleanup();
$this->onError('data request connection closed prematurely');
}
public function dataRequestOnData($req, $data): void
{
$this->chunks .= $data;
}
public function dataRequestOnEnd(): void
{
$this->onData($this->chunks);
$headers = [
'Content-Type' => 'text/html',
'Content-Length' => 2,
'X-XSS-Protection' => '0',
];
$this->dataRes->writeHead(200, '', $this->headers($this->dataReq, $headers));
$this->dataRes->end('ok');
$this->dataRequestCleanup();
}
public function onData($data)
{
$packets = Parser::decodePayload($data);
if (isset($packets['type'])) {
if ('close' === $packets['type']) {
$this->onClose();
return false;
} else {
$packets = [$packets];
}
}
foreach ($packets as $packet) {
$this->onPacket($packet);
}
}
public function onClose()
{
if ($this->writable) {
$this->send([['type' => 'noop']]);
}
parent::onClose();
}
public function send($packets): void
{
$this->writable = false;
if ($this->shouldClose) {
echo('appending close packet to payload');
$packets[] = ['type' => 'close'];
call_user_func($this->shouldClose);
$this->shouldClose = null;
}
$data = Parser::encodePayload($packets, $this->supportsBinary);
$this->write($data);
}
public function write($data): void
{
$this->doWrite($data);
if (! empty($this->req->cleanup)) {
call_user_func($this->req->cleanup);
}
}
public function doClose(callable $fn): void
{
if (! empty($this->dataReq)) {
$this->dataReq->destroy();
}
if ($this->writable) {
$this->send([['type' => 'close']]);
call_user_func($fn);
} else {
$this->shouldClose = $fn;
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use Exception;
use PHPSocketIO\Debug;
class PollingJsonp extends Polling
{
public $head = null;
public $foot = ');';
public function __construct($req)
{
$this->head = '___eio[' . (isset($req['_query']['j']) ? preg_replace('/[^0-9]/', '', $req['_query']['j']) : '') . '](';
Debug::debug('PollingJsonp __construct');
}
public function __destruct()
{
Debug::debug('PollingJsonp __destruct');
}
public function onData($data)
{
$parsed_data = null;
parse_str($data, $parsed_data);
$data = $parsed_data['d'];
call_user_func(array(get_parent_class($this), 'onData'), preg_replace('/\\\\n/', '\\n', $data));
}
public function doWrite($data): void
{
$js = json_encode($data);
$data = $this->head . $js . $this->foot;
// explicit UTF-8 is required for pages not served under utf
$headers = [
'Content-Type' => 'text/javascript; charset=UTF-8',
'Content-Length' => strlen($data),
'X-XSS-Protection' => '0'
];
if (empty($this->res)) {
echo new Exception('empty $this->res');
return;
}
$this->res->writeHead(200, '', $this->headers($headers));
$this->res->end($data);
}
public function headers(array $headers = []): array
{
$listeners = $this->listeners('headers');
foreach ($listeners as $listener) {
$listener($headers);
}
return $headers;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use PHPSocketIO\Debug;
class PollingXHR extends Polling
{
public $sid = null;
public function __construct()
{
Debug::debug('PollingXHR __construct');
}
public function __destruct()
{
Debug::debug('PollingXHR __destruct');
}
public function onRequest($req)
{
if ('OPTIONS' === $req->method) {
$res = $req->res;
$headers = $this->headers($req);
$headers['Access-Control-Allow-Headers'] = 'Content-Type';
$res->writeHead(200, '', $headers);
$res->end();
} else {
parent::onRequest($req);
}
}
public function doWrite($data)
{
// explicit UTF-8 is required for pages not served under utf todo
$content_type = preg_match('/^\d+:/', $data) ? 'text/plain; charset=UTF-8' : 'application/octet-stream';
$content_length = strlen($data);
$headers = [
'Content-Type' => $content_type,
'Content-Length' => $content_length,
'X-XSS-Protection' => '0',
];
if (empty($this->res)) {
echo new \Exception('empty this->res');
return;
}
$this->res->writeHead(200, '', $this->headers($this->req, $headers));
$this->res->end($data);
}
public function headers(object $req, ?array $headers = []): array
{
if (isset($req->headers['origin'])) {
$headers['Access-Control-Allow-Credentials'] = 'true';
$headers['Access-Control-Allow-Origin'] = $req->headers['origin'];
} else {
$headers['Access-Control-Allow-Origin'] = '*';
}
$listeners = $this->listeners('headers');
foreach ($listeners as $listener) {
$listener($headers);
}
return $headers;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace PHPSocketIO\Engine\Transports;
use PHPSocketIO\Engine\Transport;
use PHPSocketIO\Engine\Parser;
use PHPSocketIO\Debug;
class WebSocket extends Transport
{
public $sid = null;
public $writable = true;
public $supportsFraming = true;
public $supportsBinary = true;
public $name = 'websocket';
public $socket = null;
public function __construct($req)
{
$this->socket = $req->connection;
$this->socket->onMessage = [$this, 'onData2'];
$this->socket->onClose = [$this, 'onClose'];
$this->socket->onError = [$this, 'onError2'];
Debug::debug('WebSocket __construct');
}
public function __destruct()
{
Debug::debug('WebSocket __destruct');
}
public function onData2($connection, $data): void
{
call_user_func(array(get_parent_class($this), 'onData'), $data);
}
public function onError2($conection, $code, $msg): void
{
call_user_func(array(get_parent_class($this), 'onData'), $code, $msg);
}
public function send(array $packets): void
{
foreach ($packets as $packet) {
$data = Parser::encodePacket($packet);
if ($this->socket) {
$this->socket->send($data);
$this->emit('drain');
}
}
}
public function doClose(callable $fn = null): void
{
if ($this->socket) {
$this->socket->close();
$this->socket = null;
if (! empty($fn)) {
call_user_func($fn);
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace PHPSocketIO\Event;
use PHPSocketIO\Debug;
class Emitter
{
public function __construct()
{
Debug::debug('Emitter __construct');
}
public function __destruct()
{
Debug::debug('Emitter __destruct');
}
/**
* [event=>[[listener1, once?], [listener2,once?], ..], ..]
*/
protected $_eventListenerMap = [];
public function on($event_name, $listener): Emitter
{
$this->emit('newListener', $event_name, $listener);
$this->_eventListenerMap[$event_name][] = [$listener, 0];
return $this;
}
public function once($event_name, $listener): Emitter
{
$this->_eventListenerMap[$event_name][] = [$listener, 1];
return $this;
}
public function removeListener($event_name, $listener): Emitter
{
if (! isset($this->_eventListenerMap[$event_name])) {
return $this;
}
foreach ($this->_eventListenerMap[$event_name] as $key => $item) {
if ($item[0] === $listener) {
$this->emit('removeListener', $event_name, $listener);
unset($this->_eventListenerMap[$event_name][$key]);
}
}
if (empty($this->_eventListenerMap[$event_name])) {
unset($this->_eventListenerMap[$event_name]);
}
return $this;
}
public function removeAllListeners($event_name = null): Emitter
{
$this->emit('removeListener', $event_name);
if (null === $event_name) {
$this->_eventListenerMap = [];
return $this;
}
unset($this->_eventListenerMap[$event_name]);
return $this;
}
public function listeners($event_name): array
{
if (empty($this->_eventListenerMap[$event_name])) {
return [];
}
$listeners = [];
foreach ($this->_eventListenerMap[$event_name] as $item) {
$listeners[] = $item[0];
}
return $listeners;
}
public function emit($event_name = null)
{
if (empty($event_name) || empty($this->_eventListenerMap[$event_name])) {
return false;
}
foreach ($this->_eventListenerMap[$event_name] as $key => $item) {
$args = func_get_args();
unset($args[0]);
call_user_func_array($item[0], $args);
// once ?
if ($item[1]) {
unset($this->_eventListenerMap[$event_name][$key]);
if (empty($this->_eventListenerMap[$event_name])) {
unset($this->_eventListenerMap[$event_name]);
}
}
}
return true;
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace PHPSocketIO;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Parser\Parser;
class Nsp extends Emitter
{
public $adapter;
public $name = null;
public $server = null;
public $rooms = [];
public $flags = [];
public $sockets = [];
public $connected = [];
public $fns = [];
public $ids = 0;
public $acks = [];
public static $events = [
'connect' => 'connect', // for symmetry with client
'connection' => 'connection',
'newListener' => 'newListener'
];
public function __construct($server, $name)
{
$this->name = $name;
$this->server = $server;
$this->initAdapter();
Debug::debug('Nsp __construct');
}
public function __destruct()
{
Debug::debug('Nsp __destruct');
}
public function initAdapter()
{
$adapter_name = $this->server->adapter();
$this->adapter = new $adapter_name($this);
}
public function to($name): Nsp
{
if (! isset($this->rooms[$name])) {
$this->rooms[$name] = $name;
}
return $this;
}
public function in($name): Nsp
{
return $this->to($name);
}
public function add($client, $nsp, $fn)
{
$socket_name = $this->server->socket();
$socket = new $socket_name($this, $client);
if ('open' === $client->conn->readyState) {
$this->sockets[$socket->id] = $socket;
$socket->onconnect();
if (! empty($fn)) {
call_user_func($fn, $socket, $nsp);
}
$this->emit('connect', $socket);
$this->emit('connection', $socket);
} else {
echo('next called after client was closed - ignoring socket');
}
}
/**
* Removes a client. Called by each `Socket`.
*
* @api private
*/
public function remove($socket)
{
// todo $socket->id
unset($this->sockets[$socket->id]);
}
/**
* Emits to all clients.
*
* @param null $ev
* @return Nsp|void {Namespace} self
* @api public
*/
public function emit($ev = null)
{
$args = func_get_args();
if (isset(self::$events[$ev])) {
call_user_func_array([get_parent_class(__CLASS__), 'emit'], $args);
} else {
// set up packet object
$parserType = Parser::EVENT; // default
//if (self::hasBin($args)) { $parserType = Parser::BINARY_EVENT; } // binary
$packet = ['type' => $parserType, 'data' => $args];
if (is_callable(end($args))) {
echo('Callbacks are not supported when broadcasting');
return;
}
$this->adapter->broadcast(
$packet,
[
'rooms' => $this->rooms,
'flags' => $this->flags
]
);
$this->rooms = [];
$this->flags = [];
}
return $this;
}
public function send(): Nsp
{
$args = func_get_args();
array_unshift($args, 'message');
$this->emit($args);
return $this;
}
public function write()
{
$args = func_get_args();
return call_user_func_array([$this, 'send'], $args);
}
public function clients($fn): Nsp
{
$this->adapter->clients($this->rooms, $fn);
return $this;
}
/**
* Sets the compress flag.
*
* @param {Boolean} if `true`, compresses the sending data
* @return Nsp {Socket} self
* @api public
*/
public function compress($compress): Nsp
{
$this->flags['compress'] = $compress;
return $this;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace PHPSocketIO\Parser;
use Exception;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Debug;
class Decoder extends Emitter
{
public function __construct()
{
Debug::debug('Decoder __construct');
}
public function __destruct()
{
Debug::debug('Decoder __destruct');
}
/**
* @throws Exception
*/
public function add($obj): void
{
if (is_string($obj)) {
$packet = self::decodeString($obj);
$this->emit('decoded', $packet);
}
}
/**
* @throws Exception
*/
public function decodeString($str): array
{
$p = [];
$i = 0;
// look up type
$p['type'] = $str[0];
if (! isset(Parser::$types[$p['type']])) {
return self::error();
}
// look up attachments if type binary
if (Parser::BINARY_EVENT == $p['type'] || Parser::BINARY_ACK == $p['type']) {
$buf = '';
while ($str[++$i] != '-') {
$buf .= $str[$i];
if ($i == strlen($str)) {
break;
}
}
if ($buf != intval($buf) || $str[$i] != '-') {
throw new Exception('Illegal attachments');
}
$p['attachments'] = intval($buf);
}
// look up namespace (if any)
if (isset($str[$i + 1]) && '/' === $str[$i + 1]) {
$p['nsp'] = '';
while (++$i) {
if ($i === strlen($str)) {
break;
}
$c = $str[$i];
if (',' === $c) {
break;
}
$p['nsp'] .= $c;
}
} else {
$p['nsp'] = '/';
}
// look up id
if (isset($str[$i + 1])) {
$next = $str[$i + 1];
if ('' !== $next && strval((int)$next) === strval($next)) {
$p['id'] = '';
while (++$i) {
$c = $str[$i];
if (null == $c || strval((int)$c) != strval($c)) {
--$i;
break;
}
$p['id'] .= $str[$i];
if ($i == strlen($str)) {
break;
}
}
$p['id'] = (int)$p['id'];
}
}
// look up json data
if (isset($str[++$i])) {
$p['data'] = json_decode(substr($str, $i), true);
}
return $p;
}
public static function error(): array
{
return [
'type' => Parser::ERROR,
'data' => 'parser error'
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace PHPSocketIO\Parser;
use Exception;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Debug;
class Encoder extends Emitter
{
public function __construct()
{
Debug::debug('Encoder __construct');
}
public function __destruct()
{
Debug::debug('Encoder __destruct');
}
public function encode($obj): array
{
if (Parser::BINARY_EVENT == $obj['type'] || Parser::BINARY_ACK == $obj['type']) {
echo new Exception("not support BINARY_EVENT BINARY_ACK");
return [];
} else {
$encoding = self::encodeAsString($obj);
return [$encoding];
}
}
public static function encodeAsString($obj): string
{
$str = '';
$nsp = false;
// first is type
$str .= $obj['type'];
// attachments if we have them
if (Parser::BINARY_EVENT == $obj['type'] || Parser::BINARY_ACK == $obj['type']) {
$str .= $obj['attachments'];
$str .= '-';
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (! empty($obj['nsp']) && '/' !== $obj['nsp']) {
$nsp = true;
$str .= $obj['nsp'];
}
// immediately followed by the id
if (isset($obj['id'])) {
if ($nsp) {
$str .= ',';
$nsp = false;
}
$str .= $obj['id'];
}
// json data
if (isset($obj['data'])) {
if ($nsp) {
$str .= ',';
}
$str .= json_encode($obj['data']);
}
return $str;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace PHPSocketIO\Parser;
class Parser
{
/**
* Packet type `connect`.
*
* @api public
*/
const CONNECT = 0;
/**
* Packet type `disconnect`.
*
* @api public
*/
const DISCONNECT = 1;
/**
* Packet type `event`.
*
* @api public
*/
const EVENT = 2;
/**
* Packet type `ack`.
*
* @api public
*/
const ACK = 3;
/**
* Packet type `error`.
*
* @api public
*/
const ERROR = 4;
/**
* Packet type 'binary event'
*
* @api public
*/
const BINARY_EVENT = 5;
/**
* Packet type `binary ack`. For acks with binary arguments.
*
* @api public
*/
const BINARY_ACK = 6;
public static $types = [
'CONNECT',
'DISCONNECT',
'EVENT',
'BINARY_EVENT',
'ACK',
'BINARY_ACK',
'ERROR'
];
}

View File

@@ -0,0 +1,468 @@
<?php
namespace PHPSocketIO;
use Closure;
use Exception;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Parser\Parser;
class Socket extends Emitter
{
public $nsp = null;
public $server = null;
public $adapter = null;
public $id = null;
public $path = '/';
public $request = null;
public $client = null;
public $conn = null;
public $rooms = [];
public $_rooms = [];
public $flags = [];
public $acks = [];
public $connected = true;
public $disconnected = false;
public $handshake = [];
public $userId = null;
public $isGuest = false;
public $addedUser = null;
public $username = null;
public static $events = [
'error' => 'error',
'connect' => 'connect',
'disconnect' => 'disconnect',
'newListener' => 'newListener',
'removeListener' => 'removeListener'
];
public static $flagsMap = [
'json' => 'json',
'volatile' => 'volatile',
'broadcast' => 'broadcast'
];
public function __construct($nsp, $client)
{
$this->nsp = $nsp;
$this->server = $nsp->server;
$this->adapter = $this->nsp->adapter;
$this->id = ($nsp->name !== '/') ? $nsp->name . '#' . $client->id : $client->id;
$this->request = $client->request;
$this->client = $client;
$this->conn = $client->conn;
$this->handshake = $this->buildHandshake();
Debug::debug('IO Socket __construct');
}
public function __destruct()
{
Debug::debug('IO Socket __destruct');
}
public function buildHandshake(): array
{
//todo check this->request->_query
$info = ! empty($this->request->url) ? parse_url($this->request->url) : [];
$query = [];
if (isset($info['query'])) {
parse_str($info['query'], $query);
}
return [
'headers' => $this->request->headers ?? [],
'time' => date('D M d Y H:i:s') . ' GMT',
'address' => $this->conn->remoteAddress,
'xdomain' => isset($this->request->headers['origin']),
'secure' => ! empty($this->request->connection->encrypted),
'issued' => time(),
'url' => $this->request->url ?? '',
'query' => $query,
];
}
public function __get($name)
{
if ($name === 'broadcast') {
$this->flags['broadcast'] = true;
return $this;
}
return null;
}
/**
* @throws Exception
*/
public function emit($ev = null)
{
$args = func_get_args();
if (isset(self::$events[$ev])) {
call_user_func_array(array(get_parent_class(__CLASS__), 'emit'), $args);
} else {
$packet = [];
$packet['type'] = Parser::EVENT;
$packet['data'] = $args;
$flags = $this->flags;
// access last argument to see if it's an ACK callback
if (is_callable(end($args))) {
if ($this->_rooms || isset($flags['broadcast'])) {
throw new Exception('Callbacks are not supported when broadcasting');
}
echo('emitting packet with ack id ' . $this->nsp->ids);
$this->acks[$this->nsp->ids] = array_pop($args);
$packet['id'] = $this->nsp->ids++;
}
if ($this->_rooms || ! empty($flags['broadcast'])) {
$this->adapter->broadcast(
$packet,
[
'except' => [$this->id => $this->id],
'rooms' => $this->_rooms,
'flags' => $flags
]
);
} else {
// dispatch packet
$this->packet($packet);
}
// reset flags
$this->_rooms = [];
$this->flags = [];
}
return $this;
}
/**
* Targets a room when broadcasting.
*
* @param {String} name
* @return Socket {Socket} self
* @api public
*/
public function to($name): Socket
{
if (! isset($this->_rooms[$name])) {
$this->_rooms[$name] = $name;
}
return $this;
}
public function in($name): Socket
{
return $this->to($name);
}
/**
* Sends a `message` event.
*
* @return Socket {Socket} self
* @api public
*/
public function send(): Socket
{
$args = func_get_args();
array_unshift($args, 'message');
call_user_func_array([$this, 'emit'], $args);
return $this;
}
public function write(): Socket
{
$args = func_get_args();
array_unshift($args, 'message');
call_user_func_array([$this, 'emit'], $args);
return $this;
}
/**
* Writes a packet.
*
* @param {Object} packet object
* @param {Object} options
* @api private
*/
public function packet($packet, $preEncoded = false)
{
if (! $this->nsp || ! $this->client) {
return;
}
$packet['nsp'] = $this->nsp->name;
$this->client->packet($packet, $preEncoded, false);
}
/**
* Joins a room.
*
* @param {String} room
* @return Socket {Socket} self
* @api private
*/
public function join($room): Socket
{
if (! $this->connected) {
return $this;
}
if (isset($this->rooms[$room])) {
return $this;
}
$this->adapter->add($this->id, $room);
$this->rooms[$room] = $room;
return $this;
}
/**
* Leaves a room.
*
* @param {String} room
* @return Socket {Socket} self
* @api private
*/
public function leave($room): Socket
{
$this->adapter->del($this->id, $room);
unset($this->rooms[$room]);
return $this;
}
/**
* Leave all rooms.
*
* @api private
*/
public function leaveAll()
{
$this->adapter->delAll($this->id);
$this->rooms = [];
}
/**
* Called by `Namespace` upon succesful
* middleware execution (ie: authorization).
*
* @api private
*/
public function onconnect()
{
$this->nsp->connected[$this->id] = $this;
$this->join($this->id);
$this->packet(
[
'type' => Parser::CONNECT
]
);
}
/**
* Called with each packet. Called by `Client`.
*
* @param {Object} packet
* @throws Exception
* @api private
*/
public function onpacket($packet)
{
switch ($packet['type']) {
case Parser::BINARY_EVENT:
case Parser::EVENT:
$this->onevent($packet);
break;
case Parser::BINARY_ACK:
case Parser::ACK:
$this->onack($packet);
break;
case Parser::DISCONNECT:
$this->ondisconnect();
break;
case Parser::ERROR:
$this->emit('error', $packet['data']);
}
}
/**
* Called upon event packet.
*
* @param {Object} packet object
* @api private
*/
public function onevent($packet)
{
$args = $packet['data'] ?? [];
if (! empty($packet['id']) || (isset($packet['id']) && $packet['id'] === 0)) {
$args[] = $this->ack($packet['id']);
}
call_user_func_array(array(get_parent_class(__CLASS__), 'emit'), $args);
}
/**
* Produces an ack callback to emit with an event.
*
* @param {Number} packet id
* @api private
*/
public function ack($id): Closure
{
$sent = false;
return function () use (&$sent, $id) {
$self = $this;
// prevent double callbacks
if ($sent) {
return;
}
$args = func_get_args();
$type = $this->hasBin($args) ? Parser::BINARY_ACK : Parser::ACK;
$self->packet(
[
'id' => $id,
'type' => $type,
'data' => $args
]
);
};
}
/**
* Called upon ack packet.
*
* @api private
*/
public function onack($packet)
{
$ack = $this->acks[$packet['id']];
if (is_callable($ack)) {
call_user_func($ack, $packet['data']);
unset($this->acks[$packet['id']]);
} else {
echo('bad ack ' . $packet['id']);
}
}
/**
* Called upon client disconnect packet.
*
* @throws Exception
* @api private
*/
public function ondisconnect()
{
$this->onclose('client namespace disconnect');
}
/**
* Handles a client error.
*
* @throws Exception
* @api private
*/
public function onerror($err)
{
if ($this->listeners('error')) {
$this->emit('error', $err);
}
}
/**
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @param {Error} optional error object
* @throws Exception
* @api private
*/
public function onclose($reason)
{
if (! $this->connected) {
return $this;
}
$this->emit('disconnect', $reason);
$this->leaveAll();
$this->nsp->remove($this);
$this->client->remove($this);
$this->connected = false;
$this->disconnected = true;
unset($this->nsp->connected[$this->id]);
// ....
$this->nsp = null;
$this->server = null;
$this->adapter = null;
$this->request = null;
$this->client = null;
$this->conn = null;
$this->removeAllListeners();
}
/**
* Produces an `error` packet.
*
* @param {Object} error object
* @api private
*/
public function error($err)
{
$this->packet(
[
'type' => Parser::ERROR, 'data' => $err
]
);
}
/**
* Disconnects this client.
*
* @param bool $close
* @return Socket {Socket} self
* @throws Exception
* @api public
*/
public function disconnect(bool $close = false): Socket
{
if (! $this->connected) {
return $this;
}
if ($close) {
$this->client->disconnect();
} else {
$this->packet(
[
'type' => Parser::DISCONNECT
]
);
$this->onclose('server namespace disconnect');
}
return $this;
}
/**
* Sets the compress flag.
*
* @param {Boolean} if `true`, compresses the sending data
* @return Socket {Socket} self
* @api public
*/
public function compress($compress): Socket
{
$this->flags['compress'] = $compress;
return $this;
}
protected function hasBin($args): bool
{
$hasBin = false;
array_walk_recursive(
$args,
function ($item, $key) use ($hasBin) {
if (! ctype_print($item)) {
$hasBin = true;
}
}
);
return $hasBin;
}
}

View File

@@ -0,0 +1,183 @@
<?php
namespace PHPSocketIO;
use Workerman\Worker;
use PHPSocketIO\Engine\Engine;
class SocketIO
{
public $worker;
public $sockets;
public $nsps = [];
protected $_nsp = null;
protected $_socket = null;
protected $_adapter = null;
public $engine = null;
protected $_origins = '*:*';
protected $_path = null;
public function __construct($port = null, $opts = [])
{
$nsp = $opts['nsp'] ?? '\PHPSocketIO\Nsp';
$this->nsp($nsp);
$socket = $opts['socket'] ?? '\PHPSocketIO\Socket';
$this->socket($socket);
$adapter = $opts['adapter'] ?? '\PHPSocketIO\DefaultAdapter';
$this->adapter($adapter);
if (isset($opts['origins'])) {
$this->origins($opts['origins']);
}
unset($opts['nsp'], $opts['socket'], $opts['adapter'], $opts['origins']);
$this->sockets = $this->of('/');
if (! class_exists('Protocols\SocketIO')) {
class_alias('PHPSocketIO\Engine\Protocols\SocketIO', 'Protocols\SocketIO');
}
if ($port) {
$host = '0.0.0.0';
if (isset($opts['host'])) {
$ip = trim($opts['host'], '[]');
if (filter_var($ip, FILTER_VALIDATE_IP)) {
$host = (strpos($ip, ':') !== false) ? "[$ip]" : $ip;
}
}
$worker = new Worker('SocketIO://' . $host . ':' . $port, $opts);
$worker->name = 'PHPSocketIO';
if (isset($opts['ssl'])) {
$worker->transport = 'ssl';
}
$this->attach($worker);
}
}
public function nsp($v = null)
{
if (empty($v)) {
return $this->_nsp;
}
$this->_nsp = $v;
return $this;
}
public function socket($v = null)
{
if (empty($v)) {
return $this->_socket;
}
$this->_socket = $v;
return $this;
}
public function adapter($v = null)
{
if (empty($v)) {
return $this->_adapter;
}
$this->_adapter = $v;
foreach ($this->nsps as $nsp) {
$nsp->initAdapter();
}
return $this;
}
public function origins($v = null)
{
if ($v === null) {
return $this->_origins;
}
$this->_origins = $v;
if (isset($this->engine)) {
$this->engine->origins = $this->_origins;
}
return $this;
}
public function attach($srv, $opts = []): SocketIO
{
$engine = new Engine();
$engine->attach($srv, $opts);
// Export http server
$this->worker = $srv;
// bind to engine events
$this->bind($engine);
return $this;
}
public function bind($engine): SocketIO
{
$this->engine = $engine;
$this->engine->on('connection', [$this, 'onConnection']);
$this->engine->origins = $this->_origins;
return $this;
}
public function of($name, $fn = null)
{
if ($name[0] !== '/') {
$name = "/$name";
}
if (empty($this->nsps[$name])) {
$nsp_name = $this->nsp();
$this->nsps[$name] = new $nsp_name($this, $name);
}
if ($fn) {
$this->nsps[$name]->on('connect', $fn);
}
return $this->nsps[$name];
}
public function onConnection($engine_socket): SocketIO
{
$client = new Client($this, $engine_socket);
$client->connect('/');
return $this;
}
public function on()
{
$args = array_pad(func_get_args(), 2, null);
if ($args[0] === 'workerStart') {
$this->worker->onWorkerStart = $args[1];
} elseif ($args[0] === 'workerStop') {
$this->worker->onWorkerStop = $args[1];
} elseif ($args[0] !== null) {
return call_user_func_array([$this->sockets, 'on'], $args);
}
}
public function in()
{
return call_user_func_array([$this->sockets, 'in'], func_get_args());
}
public function to()
{
return call_user_func_array([$this->sockets, 'to'], func_get_args());
}
public function emit()
{
return call_user_func_array([$this->sockets, 'emit'], func_get_args());
}
public function send()
{
return call_user_func_array([$this->sockets, 'send'], func_get_args());
}
public function write()
{
return call_user_func_array([$this->sockets, 'write'], func_get_args());
}
}

View File

@@ -0,0 +1,4 @@
# These are supported funding model platforms
open_collective: workerman
patreon: walkor

6
vendor/workerman/workerman/.gitignore vendored Normal file
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;
}
}

189
vendor/workerman/workerman/Events/Ev.php vendored Normal file
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);
}
}

260
vendor/workerman/workerman/Events/Uv.php vendored Normal file
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;
}
}

342
vendor/workerman/workerman/README.md vendored Normal file
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).

220
vendor/workerman/workerman/Timer.php vendored Normal file
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();
}
}
}

2756
vendor/workerman/workerman/Worker.php vendored Normal file

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