Showing preview only (411K chars total). Download the full file or copy to clipboard to get everything.
Repository: walkor/workerman
Branch: master
Commit: 6ecda94609c4
Files: 60
Total size: 391.0 KB
Directory structure:
gitextract_71rp8_fe/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── MIT-LICENSE.txt
├── README.md
├── SECURITY.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│ ├── Connection/
│ │ ├── AsyncTcpConnection.php
│ │ ├── AsyncUdpConnection.php
│ │ ├── ConnectionInterface.php
│ │ ├── TcpConnection.php
│ │ └── UdpConnection.php
│ ├── Events/
│ │ ├── Ev.php
│ │ ├── Event.php
│ │ ├── EventInterface.php
│ │ ├── Fiber.php
│ │ ├── Select.php
│ │ ├── Swoole.php
│ │ └── Swow.php
│ ├── Protocols/
│ │ ├── Frame.php
│ │ ├── Http/
│ │ │ ├── Chunk.php
│ │ │ ├── Request.php
│ │ │ ├── Response.php
│ │ │ ├── ServerSentEvents.php
│ │ │ ├── Session/
│ │ │ │ ├── FileSessionHandler.php
│ │ │ │ ├── RedisClusterSessionHandler.php
│ │ │ │ ├── RedisSessionHandler.php
│ │ │ │ └── SessionHandlerInterface.php
│ │ │ └── Session.php
│ │ ├── Http.php
│ │ ├── ProtocolInterface.php
│ │ ├── Text.php
│ │ ├── Websocket.php
│ │ └── Ws.php
│ ├── Timer.php
│ └── Worker.php
└── tests/
├── Feature/
│ ├── ExampleTest.php
│ ├── HttpConnectionTest.php
│ ├── Stub/
│ │ ├── HttpServer.php
│ │ ├── UdpServer.php
│ │ ├── WebsocketClient.php
│ │ └── WebsocketServer.php
│ ├── UdpConnectionTest.php
│ └── WebsocketServiceTest.php
├── Pest.php
├── TestCase.php
└── Unit/
├── Connection/
│ ├── TcpConnectionEndOnMessageTest.php
│ ├── TcpConnectionEndTest.php
│ └── UdpConnectionTest.php
└── Protocols/
├── FrameTest.php
├── Http/
│ ├── RequestSessionTest.php
│ ├── ResponseTest.php
│ └── ServerSentEventsTest.php
├── HttpTest.php
└── TextTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/.gitattributes export-ignore
/.github/ export-ignore
/.gitignore export-ignore
/phpunit.xml.dist export-ignore
/tests/ export-ignore
/phpstan.neon.dist export-ignore
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
open_collective: workerman
patreon: walkor
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: 报告一个 bug
title: "[BUG]"
labels: bug
---
Please answer these questions before submitting your issue.
1. What did you do? If possible, provide a simple script for reproducing the error.
2. What did you expect to see?
3. What did you see instead?
4. What version of Workerman are you using (show your `composer info`)?
5. What is your machine environment used (show your `uname -a` & `php -v` & `php -m`) ?
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
default: bug_report.md
================================================
FILE: .github/workflows/test.yml
================================================
name: tests
on:
push:
branches:
- master
- feature/tests
- feature/feature-tests
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
linux_tests:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: ["8.2", "8.3", "8.4", "8.5"]
stability: [prefer-lowest, prefer-stable]
name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: json, posix, pcntl
ini-values: error_reporting=E_ALL
tools: composer:v2
coverage: xdebug
- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 5
command: composer update --${{ matrix.stability }} --prefer-lowest --prefer-dist --no-interaction --no-progress --ansi
- name: Static analysis
#continue-on-error: true
run: composer analyze
- name: Execute tests
run: composer test
================================================
FILE: .gitignore
================================================
logs
.buildpath
.project
.settings
.idea
.DS_Store
vendor/
/.vscode
composer.lock
phpunit.xml
/phpstan.neon
/*.pid
/*.pid.lock
================================================
FILE: MIT-LICENSE.txt
================================================
The MIT License
Copyright (c) 2009-2025 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.
================================================
FILE: README.md
================================================
# Workerman
[](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge)
[](https://packagist.org/packages/workerman/workerman)
[](https://packagist.org/packages/workerman/workerman)
[](https://packagist.org/packages/workerman/workerman)
[](https://packagist.org/packages/workerman/workerman)
[](https://packagist.org/packages/workerman/workerman)
## What is it
Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. It supports HTTP, WebSocket, custom protocols, coroutines, and connection pools, making it ideal for handling high-concurrency scenarios efficiently.
## Requires
A POSIX compatible operating system (Linux, OSX, BSD)
POSIX and PCNTL extensions required
Event/Swoole/Swow extension recommended for better performance
## Installation
```
composer require workerman/workerman
```
## Documentation
[https://manual.workerman.net](https://manual.workerman.net)
## 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
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
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();
```
### Enable SSL
```php
<?php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// SSL context.
$context = [
'ssl' => [
'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();
```
### AsyncTcpConnection (tcp/ws/text/frame etc...)
```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();
```
### Coroutine
Coroutine is used to create coroutines, enabling the execution of asynchronous tasks to improve concurrency performance.
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
Coroutine::create(function () {
echo file_get_contents("http://www.example.com/event/notify");
});
$connection->send('ok');
};
Worker::runAll();
```
> Note: Coroutine require Swoole extension or Swow extension or [Fiber revolt/event-loop](https://github.com/revoltphp/event-loop), and the same applies below
### Barrier
Barrier is used to manage concurrency and synchronization in coroutines. It allows tasks to run concurrently and waits until all tasks are completed, ensuring process synchronization.
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Barrier;
use Workerman\Coroutine;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
$barrier = Barrier::create();
for ($i=1; $i<5; $i++) {
Coroutine::create(function () use ($barrier, $i) {
file_get_contents("http://127.0.0.1:8002?task_id=$i");
});
}
// Wait all coroutine done
Barrier::wait($barrier);
$connection->send('All Task Done');
};
// Task Server
$task = new Worker('http://0.0.0.0:8002');
$task->onMessage = function (TcpConnection $connection, Request $request) {
$task_id = $request->get('task_id');
$message = "Task $task_id Done";
echo $message . PHP_EOL;
$connection->close($message);
};
Worker::runAll();
```
### Parallel
Parallel executes multiple tasks concurrently and collects results. Use add to add tasks and wait to wait for completion and get results. Unlike Barrier, Parallel directly returns the results of each task.
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Parallel;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
$parallel = new Parallel();
for ($i=1; $i<5; $i++) {
$parallel->add(function () use ($i) {
return file_get_contents("http://127.0.0.1:8002?task_id=$i");
});
}
$results = $parallel->wait();
$connection->send(json_encode($results)); // Response: ["Task 1 Done","Task 2 Done","Task 3 Done","Task 4 Done"]
};
// Task Server
$task = new Worker('http://0.0.0.0:8002');
$task->onMessage = function (TcpConnection $connection, Request $request) {
$task_id = $request->get('task_id');
$message = "Task $task_id Done";
$connection->close($message);
};
Worker::runAll();
```
### Channel
Channel is a mechanism for communication between coroutines. One coroutine can push data into the channel, while another can pop data from it, enabling synchronization and data sharing between coroutines.
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Channel;
use Workerman\Coroutine;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
$channel = new Channel(2);
Coroutine::create(function () use ($channel) {
$channel->push('Task 1 Done');
});
Coroutine::create(function () use ($channel) {
$channel->push('Task 2 Done');
});
$result = [];
for ($i = 0; $i < 2; $i++) {
$result[] = $channel->pop();
}
$connection->send(json_encode($result)); // Response: ["Task 1 Done","Task 2 Done"]
};
Worker::runAll();
```
### Pool
Pool is used to manage connection or resource pools, improving performance by reusing resources (e.g., database connections). It supports acquiring, returning, creating, and destroying resources.
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Pool;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
class RedisPool
{
private Pool $pool;
public function __construct($host, $port, $max_connections = 10)
{
$pool = new Pool($max_connections);
$pool->setConnectionCreator(function () use ($host, $port) {
$redis = new \Redis();
$redis->connect($host, $port);
return $redis;
});
$pool->setConnectionCloser(function ($redis) {
$redis->close();
});
$pool->setHeartbeatChecker(function ($redis) {
$redis->ping();
});
$this->pool = $pool;
}
public function get(): \Redis
{
return $this->pool->get();
}
public function put($redis): void
{
$this->pool->put($redis);
}
}
// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
static $pool;
if (!$pool) {
$pool = new RedisPool('127.0.0.1', 6379, 10);
}
$redis = $pool->get();
$redis->set('key', 'hello');
$value = $redis->get('key');
$pool->put($redis);
$connection->send($value);
};
Worker::runAll();
```
### Pool for automatic acquisition and release
```php
<?php
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
class Db
{
private static ?Pool $pool = null;
public static function __callStatic($name, $arguments)
{
if (self::$pool === null) {
self::initializePool();
}
// Get the connection from the coroutine context
// to ensure the same connection is used within the same coroutine
$pdo = Context::get('pdo');
if (!$pdo) {
// If no connection is retrieved, get one from the connection pool
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// When the coroutine is destroyed, return the connection to the pool
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
self::$pool = new Pool(10);
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
// Http Server
$worker = new Worker('http://0.0.0.0:8001');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onMessage = function (TcpConnection $connection, Request $request) {
$value = Db::query('SELECT NOW() as now')->fetchAll();
$connection->send(json_encode($value));
};
Worker::runAll();
```
## Available commands
```php start.php start ```
```php start.php start -d ```
```php start.php status ```
```php start.php status -d ```
```php start.php connections```
```php start.php stop ```
```php start.php stop -g ```
```php start.php restart ```
```php start.php reload ```
```php start.php reload -g ```
# Benchmarks
https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext&l=zik073-1r
### Supported by
[](https://jb.gg/OpenSourceSupport)
## Other links with workerman
[webman](https://github.com/walkor/webman)
[AdapterMan](https://github.com/joanhey/AdapterMan)
## Donate
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UQGGS9UB35WWG">PayPal</a>
## LICENSE
Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt).
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
Please contact by email walkor@workerman.net
================================================
FILE: composer.json
================================================
{
"name": "workerman/workerman",
"type": "library",
"keywords": [
"event-loop",
"asynchronous",
"http",
"framework"
],
"homepage": "https://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": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/workerman/issues",
"forum": "https://www.workerman.net/questions",
"wiki": "https://www.workerman.net/doc/workerman/",
"source": "https://github.com/walkor/workerman"
},
"require": {
"php": ">=8.1",
"ext-json": "*",
"workerman/coroutine": "^1.1 || dev-main"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"Workerman\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"conflict": {
"ext-swow": "<v1.0.0"
},
"require-dev": {
"pestphp/pest": "^2.36 || ^3 || ^4",
"mockery/mockery": "^1.6",
"guzzlehttp/guzzle": "^7.10",
"phpstan/phpstan": "^2.1"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"analyze": "php -d memory_limit=1G vendor/phpstan/phpstan/phpstan.phar",
"test": "pest --colors=always"
}
}
================================================
FILE: phpstan.neon.dist
================================================
parameters:
level: 5
paths:
- src
- tests
excludePaths:
- src/Events/Swow.php
ignoreErrors:
- path: src/Events/Fiber.php
messages:
- '#Property Workerman\\Events\\Fiber::\$driver has unknown class Revolt\\EventLoop\\Driver as its type.#'
- '#Call to static method getDriver\(\) on an unknown class Revolt\\EventLoop.#'
- '#Method Workerman\\Events\\Fiber::driver\(\) has invalid return type Revolt\\EventLoop\\Driver.#'
- '#Call to method .* on an unknown class Revolt\\EventLoop\\Driver.#'
- path: src/Events/Event.php
reportUnmatched: false
messages:
- '#Call to an undefined method EventBase::+.#'
- path: src/Timer.php
message: '#Call to static method getSuspension\(\) on an unknown class Revolt\\EventLoop.#'
- path: src/Worker.php
reportUnmatched: false
messages:
- '#Constant LINE_VERSION_LENGTH not found.#'
- '#Call to static method create\(\) on an unknown class Workerman\\Coroutine\\Coroutine.#'
- identifier: throws.unusedType
message: "#RedisClusterException in PHPDoc @throws tag but it's not thrown.#"
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage/>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
================================================
FILE: src/Connection/AsyncTcpConnection.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Exception;
use RuntimeException;
use stdClass;
use Throwable;
use Workerman\Timer;
use Workerman\Worker;
use function class_exists;
use function explode;
use function function_exists;
use function is_resource;
use function method_exists;
use function microtime;
use function parse_url;
use function socket_import_stream;
use function socket_set_option;
use function stream_context_create;
use function stream_set_blocking;
use function stream_set_read_buffer;
use function stream_socket_client;
use function stream_socket_get_name;
use function ucfirst;
use const DIRECTORY_SEPARATOR;
use const PHP_INT_MAX;
use const SO_KEEPALIVE;
use const SOL_SOCKET;
use const SOL_TCP;
use const STREAM_CLIENT_ASYNC_CONNECT;
use const TCP_NODELAY;
/**
* AsyncTcpConnection.
*/
class AsyncTcpConnection extends TcpConnection
{
/**
* PHP built-in protocols.
*
* @var array<string, string>
*/
public const BUILD_IN_TRANSPORTS = [
'tcp' => 'tcp',
'udp' => 'udp',
'unix' => 'unix',
'ssl' => 'ssl',
'sslv2' => 'sslv2',
'sslv3' => 'sslv3',
'tls' => 'tls'
];
/**
* Emitted when socket connection is successfully established.
*
* @var ?callable
*/
public $onConnect = null;
/**
* Emitted when websocket handshake completed (Only work when protocol is ws).
*
* @var ?callable
*/
public $onWebSocketConnect = null;
/**
* Transport layer protocol.
*
* @var string
*/
public string $transport = 'tcp';
/**
* Socks5 proxy.
*
* @var string
*/
public string $proxySocks5 = '';
/**
* Http proxy.
*
* @var string
*/
public string $proxyHttp = '';
/**
* Status.
*
* @var int
*/
protected int $status = self::STATUS_INITIAL;
/**
* Remote host.
*
* @var string
*/
protected string $remoteHost = '';
/**
* Remote port.
*
* @var int
*/
protected int $remotePort = 80;
/**
* Connect start time.
*
* @var float
*/
protected float $connectStartTime = 0;
/**
* Remote URI.
*
* @var string
*/
protected string $remoteURI = '';
/**
* Context option.
*
* @var array
*/
protected array $socketContext = [];
/**
* Reconnect timer.
*
* @var int
*/
protected int $reconnectTimer = 0;
/**
* Construct.
*
* @param string $remoteAddress
* @param array $socketContext
*/
public function __construct(string $remoteAddress, array $socketContext = [])
{
$addressInfo = parse_url($remoteAddress);
if (!$addressInfo) {
[$scheme, $this->remoteAddress] = explode(':', $remoteAddress, 2);
if ('unix' === strtolower($scheme)) {
$this->remoteAddress = substr($remoteAddress, strpos($remoteAddress, '/') + 2);
}
if (!$this->remoteAddress) {
throw new RuntimeException('Bad remoteAddress');
}
} else {
$addressInfo['port'] ??= 0;
$addressInfo['path'] ??= '/';
if (!isset($addressInfo['query'])) {
$addressInfo['query'] = '';
} else {
$addressInfo['query'] = '?' . $addressInfo['query'];
}
$this->remoteHost = $addressInfo['host'];
$this->remotePort = $addressInfo['port'];
$this->remoteURI = "{$addressInfo['path']}{$addressInfo['query']}";
$scheme = $addressInfo['scheme'] ?? 'tcp';
$this->remoteAddress = 'unix' === strtolower($scheme)
? substr($remoteAddress, strpos($remoteAddress, '/') + 2)
: $this->remoteHost . ':' . $this->remotePort;
}
$this->id = $this->realId = self::$idRecorder++;
if (PHP_INT_MAX === self::$idRecorder) {
self::$idRecorder = 0;
}
// Check application layer protocol class.
if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) {
// Validate scheme contains only safe characters for class name resolution.
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $scheme)) {
throw new RuntimeException("Invalid protocol scheme '$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 RuntimeException("class \\Protocols\\$scheme not exist");
}
}
} else {
$this->transport = self::BUILD_IN_TRANSPORTS[$scheme];
}
// For statistics.
++self::$statistics['connection_count'];
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
$this->maxPackageSize = self::$defaultMaxPackageSize;
$this->socketContext = $socketContext;
static::$connections[$this->realId] = $this;
$this->context = new stdClass;
}
/**
* Reconnect.
*
* @param int $after
* @return void
*/
public function reconnect(int $after = 0): void
{
$this->status = self::STATUS_INITIAL;
static::$connections[$this->realId] = $this;
if ($this->reconnectTimer) {
Timer::del($this->reconnectTimer);
}
if ($after > 0) {
$this->reconnectTimer = Timer::add($after, $this->connect(...), null, false);
return;
}
$this->connect();
}
/**
* Do connect.
*
* @return void
*/
public function connect(): void
{
if ($this->status !== self::STATUS_INITIAL && $this->status !== self::STATUS_CLOSING &&
$this->status !== self::STATUS_CLOSED) {
return;
}
$this->eventLoop ??= Worker::getEventLoop();
$this->status = self::STATUS_CONNECTING;
$this->connectStartTime = microtime(true);
set_error_handler(fn() => 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->proxySocks5) {
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->proxySocks5", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else if ($this->proxyHttp) {
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->proxyHttp", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else if ($this->socketContext) {
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else {
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT);
}
} else {
$this->socket = stream_socket_client("$this->transport://$this->remoteAddress", $errno, $err_str, 0,
STREAM_CLIENT_ASYNC_CONNECT);
}
restore_error_handler();
// If failed attempt to emit onError callback.
if (!$this->socket || !is_resource($this->socket)) {
$this->emitError(static::CONNECT_FAIL, $err_str);
if ($this->status === self::STATUS_CLOSING) {
$this->destroy();
}
if ($this->status === self::STATUS_CLOSED) {
$this->onConnect = null;
}
return;
}
$this->eventLoop ??= Worker::getEventLoop();
// Add socket to global event loop waiting connection is successfully established or failed.
$this->eventLoop->onWritable($this->socket, $this->checkConnection(...));
// For windows.
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'onExcept')) {
$this->eventLoop->onExcept($this->socket, $this->checkConnection(...));
}
}
/**
* Try to emit onError callback.
*
* @param int $code
* @param mixed $msg
* @return void
*/
protected function emitError(int $code, mixed $msg): void
{
$this->status = self::STATUS_CLOSING;
if ($this->onError) {
try {
($this->onError)($this, $code, $msg);
} catch (Throwable $e) {
$this->error($e);
}
}
}
/**
* CancelReconnect.
*/
public function cancelReconnect(): void
{
if ($this->reconnectTimer) {
Timer::del($this->reconnectTimer);
$this->reconnectTimer = 0;
}
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteHost(): string
{
return $this->remoteHost;
}
/**
* Get remote URI.
*
* @return string
*/
public function getRemoteURI(): string
{
return $this->remoteURI;
}
/**
* Check connection is successfully established or failed.
*
* @return void
*/
public function checkConnection(): void
{
// Remove EV_EXPECT for windows.
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'offExcept')) {
$this->eventLoop->offExcept($this->socket);
}
// Remove write listener.
$this->eventLoop->offWritable($this->socket);
if ($this->status !== self::STATUS_CONNECTING) {
return;
}
// Check socket state.
if ($address = stream_socket_get_name($this->socket, true)) {
// Proxy
if ($this->proxySocks5 && $address === $this->proxySocks5) {
fwrite($this->socket, chr(5) . chr(1) . chr(0));
fread($this->socket, 512);
fwrite($this->socket, chr(5) . chr(1) . chr(0) . chr(3) . chr(strlen($this->remoteHost)) . $this->remoteHost . pack("n", $this->remotePort));
fread($this->socket, 512);
}
if ($this->proxyHttp && $address === $this->proxyHttp) {
$str = "CONNECT $this->remoteHost:$this->remotePort HTTP/1.1\r\n";
$str .= "Host: $this->remoteHost:$this->remotePort\r\n";
$str .= "Proxy-Connection: keep-alive\r\n\r\n";
fwrite($this->socket, $str);
fread($this->socket, 512);
}
if (!is_resource($this->socket)) {
$this->emitError(static::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;
}
return;
}
// Nonblocking.
stream_set_blocking($this->socket, false);
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') {
$socket = socket_import_stream($this->socket);
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
if (defined('TCP_KEEPIDLE') && defined('TCP_KEEPINTVL') && defined('TCP_KEEPCNT')) {
socket_set_option($socket, SOL_TCP, TCP_KEEPIDLE, static::TCP_KEEPALIVE_INTERVAL);
socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, static::TCP_KEEPALIVE_INTERVAL);
socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 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) {
$this->eventLoop->onWritable($this->socket, $this->baseWrite(...));
}
}
// Register a listener waiting read event.
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
$this->status = self::STATUS_ESTABLISHED;
$this->remoteAddress = $address;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
($this->onConnect)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
// Try to emit protocol::onConnect
if ($this->protocol && method_exists($this->protocol, 'onConnect')) {
try {
$this->protocol::onConnect($this);
} catch (Throwable $e) {
$this->error($e);
}
}
} else {
// Connection failed.
$this->emitError(static::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;
}
}
}
}
================================================
FILE: src/Connection/AsyncUdpConnection.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Exception;
use RuntimeException;
use Throwable;
use Workerman\Protocols\ProtocolInterface;
use Workerman\Worker;
use function class_exists;
use function is_resource;
use function explode;
use function fclose;
use function stream_context_create;
use function stream_set_blocking;
use function stream_socket_client;
use function stream_socket_recvfrom;
use function stream_socket_sendto;
use function strlen;
use function substr;
use function ucfirst;
use const STREAM_CLIENT_CONNECT;
/**
* 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 bool $connected = false;
/**
* Context option.
*
* @var array
*/
protected array $contextOption = [];
/**
* Construct.
*
* @param string $remoteAddress
* @throws Throwable
*/
public function __construct($remoteAddress, $contextOption = [])
{
// Get the application layer communication protocol and listening address.
[$scheme, $address] = explode(':', $remoteAddress, 2);
// Check application layer protocol class.
if ($scheme !== 'udp') {
// Validate scheme contains only safe characters for class name resolution.
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]*$/', $scheme)) {
throw new RuntimeException("Invalid protocol scheme '$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 RuntimeException("class \\Protocols\\$scheme not exist");
}
}
}
$this->remoteAddress = substr($address, 2);
$this->contextOption = $contextOption;
}
/**
* For udp package.
*
* @param resource $socket
* @return void
*/
public function baseRead($socket): void
{
$recvBuffer = stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress);
if (false === $recvBuffer || empty($remoteAddress)) {
return;
}
if ($this->onMessage) {
if ($this->protocol) {
$recvBuffer = $this->protocol::decode($recvBuffer, $this);
}
++ConnectionInterface::$statistics['total_request'];
try {
($this->onMessage)($this, $recvBuffer);
} catch (Throwable $e) {
$this->error($e);
}
}
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close(mixed $data = null, bool $raw = false): void
{
if ($data !== null) {
$this->send($data, $raw);
}
if ($this->eventLoop) {
$this->eventLoop->offReadable($this->socket);
}
if (is_resource($this->socket)) {
fclose($this->socket);
}
$this->socket = null; // intentionally nullable to mark closed state
$this->connected = false;
// Try to emit onClose callback.
if ($this->onClose) {
try {
($this->onClose)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
$this->onConnect = $this->onMessage = $this->onClose = $this->eventLoop = $this->errorHandler = null;
}
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
public function send(mixed $sendBuffer, bool $raw = false): bool|null
{
if (false === $raw && $this->protocol) {
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
if ($sendBuffer === '') {
return null;
}
}
if ($this->connected === false) {
$this->connect();
}
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer);
}
/**
* Connect.
*
* @return void
*/
public function connect(): void
{
if ($this->connected === true) {
return;
}
$this->eventLoop ??= Worker::getEventLoop();
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((string)(new Exception($errmsg)));
$this->eventLoop = null;
return;
}
$this->eventLoop ??= Worker::getEventLoop();
stream_set_blocking($this->socket, false);
if ($this->onMessage) {
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
}
$this->connected = true;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
($this->onConnect)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
}
}
================================================
FILE: src/Connection/ConnectionInterface.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Throwable;
use Workerman\Events\Event;
use Workerman\Events\EventInterface;
use Workerman\Worker;
use AllowDynamicProperties;
/**
* ConnectionInterface.
*/
#[AllowDynamicProperties]
abstract class ConnectionInterface
{
/**
* Connect failed.
*
* @var int
*/
public const CONNECT_FAIL = 1;
/**
* Send failed.
*
* @var int
*/
public const SEND_FAIL = 2;
/**
* Statistics for status command.
*
* @var array
*/
public static array $statistics = [
'connection_count' => 0,
'total_request' => 0,
'throw_exception' => 0,
'send_fail' => 0,
];
/**
* Application layer protocol.
* The format is like this Workerman\\Protocols\\Http.
*
* @var ?class-string
*/
public ?string $protocol = null;
/**
* Emitted when data is received.
*
* @var ?callable
*/
public $onMessage = null;
/**
* Emitted when the other end of the socket sends a FIN packet.
*
* @var ?callable
*/
public $onClose = null;
/**
* Emitted when an error occurs with connection.
*
* @var ?callable
*/
public $onError = null;
/**
* @var ?EventInterface
*/
public ?EventInterface $eventLoop = null;
/**
* @var ?callable
*/
public $errorHandler = null;
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
abstract public function send(mixed $sendBuffer, bool $raw = false): bool|null;
/**
* Get remote IP.
*
* @return string
*/
abstract public function getRemoteIp(): string;
/**
* Get remote port.
*
* @return int
*/
abstract public function getRemotePort(): int;
/**
* Get remote address.
*
* @return string
*/
abstract public function getRemoteAddress(): string;
/**
* Get local IP.
*
* @return string
*/
abstract public function getLocalIp(): string;
/**
* Get local port.
*
* @return int
*/
abstract public function getLocalPort(): int;
/**
* Get local address.
*
* @return string
*/
abstract public function getLocalAddress(): string;
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
abstract public function close(mixed $data = null, bool $raw = false): void;
/**
* Is ipv4.
*
* return bool.
*/
abstract public function isIpV4(): bool;
/**
* Is ipv6.
*
* return bool.
*/
abstract public function isIpV6(): bool;
/**
* @param Throwable $exception
* @return void
*/
public function error(Throwable $exception): void
{
if (!$this->errorHandler) {
Worker::stopAll(250, $exception);
return;
}
try {
($this->errorHandler)($exception);
} catch (Throwable $exception) {
Worker::stopAll(250, $exception);
return;
}
}
}
================================================
FILE: src/Connection/TcpConnection.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Connection;
use JsonSerializable;
use RuntimeException;
use stdClass;
use Throwable;
use Workerman\Events\EventInterface;
use Workerman\Protocols\Http;
use Workerman\Protocols\Http\Request;
use Workerman\Timer;
use Workerman\Worker;
use function ceil;
use function count;
use function fclose;
use function feof;
use function fread;
use function function_exists;
use function fwrite;
use function is_object;
use function is_resource;
use function key;
use function method_exists;
use function posix_getpid;
use function restore_error_handler;
use function set_error_handler;
use function stream_set_blocking;
use function stream_set_read_buffer;
use function stream_socket_shutdown;
use function stream_socket_enable_crypto;
use function stream_socket_get_name;
use function strlen;
use function strrchr;
use function strrpos;
use function substr;
use function var_export;
use const PHP_INT_MAX;
use const STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
use const STREAM_CRYPTO_METHOD_SSLv23_SERVER;
use const STREAM_CRYPTO_METHOD_SSLv2_CLIENT;
use const STREAM_CRYPTO_METHOD_SSLv2_SERVER;
use const STREAM_SHUT_WR;
/**
* TcpConnection.
* @property string $websocketType
* @property string|null $websocketClientProtocol
* @property string|null $websocketOrigin
*/
class TcpConnection extends ConnectionInterface implements JsonSerializable
{
/**
* Read buffer size.
*
* @var int
*/
public const READ_BUFFER_SIZE = 87380;
/**
* Status initial.
*
* @var int
*/
public const STATUS_INITIAL = 0;
/**
* Status connecting.
*
* @var int
*/
public const STATUS_CONNECTING = 1;
/**
* Status connection established.
*
* @var int
*/
public const STATUS_ESTABLISHED = 2;
/**
* Status ending (graceful close: write -> FIN -> linger/drain -> close).
*
* @var int
*/
public const STATUS_ENDING = 4;
/**
* Status closing.
*
* @var int
*/
public const STATUS_CLOSING = 8;
/**
* Status closed.
*
* @var int
*/
public const STATUS_CLOSED = 16;
/**
* Maximum string length for cache
*
* @var int
*/
public const MAX_CACHE_STRING_LENGTH = 2048;
/**
* Maximum cache size.
*
* @var int
*/
public const MAX_CACHE_SIZE = 512;
/**
* Tcp keepalive interval.
*/
public const TCP_KEEPALIVE_INTERVAL = 55;
/**
* Emitted when socket connection is successfully established.
*
* @var ?callable
*/
public $onConnect = null;
/**
* Emitted before websocket handshake (Only called when protocol is ws).
*
* @var ?callable
*/
public $onWebSocketConnect = null;
/**
* Emitted after websocket handshake (Only called when protocol is ws).
*
* @var ?callable
*/
public $onWebSocketConnected = null;
/**
* Emitted when websocket connection is closed (Only called when protocol is ws).
*
* @var ?callable
*/
public $onWebSocketClose = null;
/**
* Emitted when data is received.
*
* @var ?callable
*/
public $onMessage = null;
/**
* Emitted when the other end of the socket sends a FIN packet.
*
* @var ?callable
*/
public $onClose = null;
/**
* Emitted when an error occurs with connection.
*
* @var ?callable
*/
public $onError = null;
/**
* Emitted when the send buffer becomes full.
*
* @var ?callable
*/
public $onBufferFull = null;
/**
* Emitted when send buffer becomes empty.
*
* @var ?callable
*/
public $onBufferDrain = null;
/**
* Transport (tcp/udp/unix/ssl).
*
* @var string
*/
public string $transport = 'tcp';
/**
* Which worker belong to.
*
* @var ?Worker
*/
public ?Worker $worker = null;
/**
* Bytes read.
*
* @var int
*/
public int $bytesRead = 0;
/**
* Bytes written.
*
* @var int
*/
public int $bytesWritten = 0;
/**
* Connection->id.
*
* @var int
*/
public int $id = 0;
/**
* A copy of $worker->id which used to clean up the connection in worker->connections
*
* @var int
*/
protected int $realId = 0;
/**
* Sets the maximum send buffer size for the current connection.
* OnBufferFull callback will be emitted When send buffer is full.
*
* @var int
*/
public int $maxSendBufferSize = 1048576;
/**
* Context.
*
* @var ?stdClass
*/
public ?stdClass $context = null;
/**
* Internal use only. Do not access or modify from application code.
*
* @internal Framework internal API
* @deprecated Do not set this property, use $response->header() or $response->widthHeaders() instead
* @var array
*/
public array $headers = [];
/**
* Is safe.
*
* @var bool
*/
protected bool $isSafe = true;
/**
* Default send buffer size.
*
* @var int
*/
public static int $defaultMaxSendBufferSize = 1048576;
/**
* Sets the maximum acceptable packet size for the current connection.
*
* @var int
*/
public int $maxPackageSize = 1048576;
/**
* Default maximum acceptable packet size.
*
* @var int
*/
public static int $defaultMaxPackageSize = 10485760;
/**
* Default linger timeout for graceful end (seconds).
*
* @var float
*/
public static float $defaultLingerTimeout = 1.0;
/**
* Linger timeout for graceful end (seconds).
*
* @var float
*/
public float $lingerTimeout = 1.0;
/**
* Id recorder.
*
* @var int
*/
protected static int $idRecorder = 1;
/**
* Socket
*
* @var resource
*/
protected $socket = null;
/**
* Send buffer.
*
* @var string
*/
protected string $sendBuffer = '';
/**
* Receive buffer.
*
* @var string
*/
protected string $recvBuffer = '';
/**
* Current package length.
*
* @var int
*/
protected int $currentPackageLength = 0;
/**
* Connection status.
*
* @var int
*/
protected int $status = self::STATUS_ESTABLISHED;
/**
* Linger timer id for end().
*
* @var int
*/
protected int $endLingerTimerId = 0;
/**
* Whether write side has been shutdown (FIN sent) during end().
*
* @var bool
*/
protected bool $endWriteShutdown = false;
/**
* Remote address.
*
* @var string
*/
protected string $remoteAddress = '';
/**
* Is paused.
*
* @var bool
*/
protected bool $isPaused = false;
/**
* SSL handshake completed or not.
*
* @var bool
*/
protected bool|int $sslHandshakeCompleted = false;
/**
* All connection instances.
*
* @var array
*/
public static array $connections = [];
/**
* Status to string.
*
* @var array
*/
public const STATUS_TO_STRING = [
self::STATUS_INITIAL => 'INITIAL',
self::STATUS_CONNECTING => 'CONNECTING',
self::STATUS_ESTABLISHED => 'ESTABLISHED',
self::STATUS_CLOSING => 'CLOSING',
self::STATUS_ENDING => 'ENDING',
self::STATUS_CLOSED => 'CLOSED',
];
/**
* Construct.
*
* @param EventInterface $eventLoop
* @param resource $socket
* @param string $remoteAddress
*/
public function __construct(EventInterface $eventLoop, $socket, string $remoteAddress = '')
{
++self::$statistics['connection_count'];
$this->id = $this->realId = self::$idRecorder++;
if (self::$idRecorder === PHP_INT_MAX) {
self::$idRecorder = 0;
}
$this->socket = $socket;
stream_set_blocking($this->socket, false);
stream_set_read_buffer($this->socket, 0);
$this->eventLoop = $eventLoop;
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
$this->maxPackageSize = self::$defaultMaxPackageSize;
$this->lingerTimeout = self::$defaultLingerTimeout;
$this->remoteAddress = $remoteAddress;
static::$connections[$this->id] = $this;
$this->context = new stdClass();
}
/**
* Get status.
*
* @param bool $rawOutput
*
* @return int|string
*/
public function getStatus(bool $rawOutput = true): int|string
{
if ($rawOutput) {
return $this->status;
}
return self::STATUS_TO_STRING[$this->status];
}
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
public function send(mixed $sendBuffer, bool $raw = false): bool|null
{
if ($this->status === self::STATUS_ENDING || $this->status === self::STATUS_CLOSING || $this->status === self::STATUS_CLOSED) {
return false;
}
// Try to call protocol::encode($sendBuffer) before sending.
if (false === $raw && $this->protocol !== null) {
try {
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
} catch(Throwable $e) {
$this->error($e);
}
if ($sendBuffer === '') {
return null;
}
}
if ($this->status !== self::STATUS_ESTABLISHED ||
($this->transport === 'ssl' && $this->sslHandshakeCompleted !== true)
) {
if ($this->sendBuffer && $this->bufferIsFull()) {
++self::$statistics['send_fail'];
return false;
}
$this->sendBuffer .= $sendBuffer;
$this->checkBufferWillFull();
return null;
}
// Attempt to send data directly.
if ($this->sendBuffer === '') {
if ($this->transport === 'ssl') {
$this->eventLoop->onWritable($this->socket, $this->baseWrite(...));
$this->sendBuffer = $sendBuffer;
$this->checkBufferWillFull();
return null;
}
$len = 0;
try {
$len = @fwrite($this->socket, $sendBuffer);
} catch (Throwable $e) {
Worker::log($e);
}
// send successful.
if ($len === strlen($sendBuffer)) {
$this->bytesWritten += $len;
return true;
}
// Send only part of the data.
if ($len > 0) {
$this->sendBuffer = substr($sendBuffer, $len);
$this->bytesWritten += $len;
} else {
// Connection closed?
if (!is_resource($this->socket) || feof($this->socket)) {
++self::$statistics['send_fail'];
if ($this->onError) {
try {
($this->onError)($this, static::SEND_FAIL, 'client closed');
} catch (Throwable $e) {
$this->error($e);
}
}
$this->destroy();
return false;
}
$this->sendBuffer = $sendBuffer;
}
$this->eventLoop->onWritable($this->socket, $this->baseWrite(...));
// Check if send buffer will be full.
$this->checkBufferWillFull();
return null;
}
if ($this->bufferIsFull()) {
++self::$statistics['send_fail'];
return false;
}
$this->sendBuffer .= $sendBuffer;
// Check if send buffer is full.
$this->checkBufferWillFull();
return null;
}
/**
* Get remote IP.
*
* @return string
*/
public function getRemoteIp(): string
{
$pos = strrpos($this->remoteAddress, ':');
if ($pos) {
return substr($this->remoteAddress, 0, $pos);
}
return '';
}
/**
* Get remote port.
*
* @return int
*/
public function getRemotePort(): int
{
if ($this->remoteAddress) {
return (int)substr(strrchr($this->remoteAddress, ':'), 1);
}
return 0;
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteAddress(): string
{
return $this->remoteAddress;
}
/**
* Get local IP.
*
* @return string
*/
public function getLocalIp(): string
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return '';
}
return substr($address, 0, $pos);
}
/**
* Get local port.
*
* @return int
*/
public function getLocalPort(): int
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return 0;
}
return (int)substr(strrchr($address, ':'), 1);
}
/**
* Get local address.
*
* @return string
*/
public function getLocalAddress(): string
{
if (!is_resource($this->socket)) {
return '';
}
return (string)@stream_socket_get_name($this->socket, false);
}
/**
* Get send buffer queue size.
*
* @return integer
*/
public function getSendBufferQueueSize(): int
{
return strlen($this->sendBuffer);
}
/**
* Get receive buffer queue size.
*
* @return integer
*/
public function getRecvBufferQueueSize(): int
{
return strlen($this->recvBuffer);
}
/**
* Pauses the reading of data. That is onMessage will not be emitted. Useful to throttle back an upload.
*
* @return void
*/
public function pauseRecv(): void
{
if($this->eventLoop !== null){
$this->eventLoop->offReadable($this->socket);
}
$this->isPaused = true;
}
/**
* Resumes reading after a call to pauseRecv.
*
* @return void
*/
public function resumeRecv(): void
{
if ($this->isPaused === true) {
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
$this->isPaused = false;
$this->baseRead($this->socket, false);
}
}
/**
* Base read handler.
*
* @param resource $socket
* @param bool $checkEof
* @return void
*/
public function baseRead($socket, bool $checkEof = true): void
{
static $requests = [];
// SSL handshake.
if ($this->transport === 'ssl' && $this->sslHandshakeCompleted !== true) {
if ($this->doSslHandshake($socket)) {
$this->sslHandshakeCompleted = true;
if ($this->sendBuffer) {
$this->eventLoop->onWritable($socket, $this->baseWrite(...));
}
} else {
return;
}
}
$buffer = '';
try {
$buffer = @fread($socket, self::READ_BUFFER_SIZE);
} catch (Throwable) {
// do nothing
}
// Check connection closed.
if ($buffer === '' || $buffer === false) {
if ($checkEof && (!is_resource($socket) || feof($socket) || $buffer === false)) {
$this->destroy();
return;
}
} else {
$this->bytesRead += strlen($buffer);
if ($this->status === self::STATUS_ENDING) {
return;
}
if ($this->recvBuffer === '') {
if (!isset($buffer[static::MAX_CACHE_STRING_LENGTH]) && isset($requests[$buffer])) {
++self::$statistics['total_request'];
if ($this->protocol === Http::class) {
$request = $requests[$buffer];
$request->connection = $this;
try {
($this->onMessage)($this, $request);
} catch (Throwable $e) {
$this->error($e);
}
$request = clone $request;
$request->destroy();
$requests[$buffer] = $request;
return;
}
$request = $requests[$buffer];
try {
($this->onMessage)($this, $request);
} catch (Throwable $e) {
$this->error($e);
}
return;
}
$this->recvBuffer = $buffer;
} else {
$this->recvBuffer .= $buffer;
}
}
// If the application layer protocol has been set up.
if ($this->protocol !== null) {
while ($this->recvBuffer !== '' && !$this->isPaused) {
// The current packet length is known.
if ($this->currentPackageLength) {
// Data is not enough for a package.
$recvBufferLength = strlen($this->recvBuffer);
if ($this->currentPackageLength > $recvBufferLength) {
break;
}
} else {
// Get current package length.
try {
$this->currentPackageLength = $this->protocol::input($this->recvBuffer, $this);
} catch (Throwable $e) {
$this->currentPackageLength = -1;
Worker::safeEcho((string)$e);
}
// The packet length is unknown.
if ($this->currentPackageLength === 0) {
break;
} elseif ($this->currentPackageLength > 0 && $this->currentPackageLength <= $this->maxPackageSize) {
// Data is not enough for a package.
// Note: recalculate length here since protocol::input() may call consumeRecvBuffer().
$recvBufferLength = strlen($this->recvBuffer);
if ($this->currentPackageLength > $recvBufferLength) {
break;
}
} // Wrong package.
else {
Worker::safeEcho((string)(new RuntimeException("Protocol $this->protocol Error package. package_length=" . var_export($this->currentPackageLength, true))));
$this->destroy();
return;
}
}
// The data is enough for a packet.
++self::$statistics['total_request'];
// The current packet length is equal to the length of the buffer.
if ($one = ($recvBufferLength === $this->currentPackageLength)) {
$oneRequestBuffer = $this->recvBuffer;
$this->recvBuffer = '';
} else {
// Get a full package from the buffer.
$oneRequestBuffer = substr($this->recvBuffer, 0, $this->currentPackageLength);
// Remove the current package from receive buffer.
$this->recvBuffer = substr($this->recvBuffer, $this->currentPackageLength);
}
// Reset the current packet length to 0.
$this->currentPackageLength = 0;
try {
// Decode request buffer before Emitting onMessage callback.
$request = $this->protocol::decode($oneRequestBuffer, $this);
if ((!is_object($request) || $request instanceof Request) && $one && !isset($oneRequestBuffer[static::MAX_CACHE_STRING_LENGTH])) {
($this->onMessage)($this, $request);
if ($request instanceof Request) {
$request = clone $request;
$request->destroy();
}
$requests[$oneRequestBuffer] = $request;
if (count($requests) > static::MAX_CACHE_SIZE) {
unset($requests[key($requests)]);
}
return;
}
($this->onMessage)($this, $request);
} catch (Throwable $e) {
$this->error($e);
}
}
return;
}
if ($this->recvBuffer === '' || $this->isPaused) {
return;
}
// Application protocol is not set.
++self::$statistics['total_request'];
try {
($this->onMessage)($this, $this->recvBuffer);
} catch (Throwable $e) {
$this->error($e);
}
// Clean receive buffer.
$this->recvBuffer = '';
}
/**
* Base write handler.
*
* @return void
*/
public function baseWrite(): void
{
$len = 0;
try {
if ($this->transport === 'ssl') {
$len = @fwrite($this->socket, $this->sendBuffer, 8192);
} else {
$len = @fwrite($this->socket, $this->sendBuffer);
}
} catch (Throwable) {
}
if ($len === strlen($this->sendBuffer)) {
$this->bytesWritten += $len;
$this->eventLoop->offWritable($this->socket);
$this->sendBuffer = '';
// Try to emit onBufferDrain callback when send buffer becomes empty.
if ($this->onBufferDrain) {
try {
($this->onBufferDrain)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
if ($this->status === self::STATUS_ENDING) {
$this->endMaybeShutdownWrite();
}
if ($this->status === self::STATUS_CLOSING) {
if (!empty($this->context->streamSending)) {
return;
}
$this->destroy();
}
return;
}
if ($len > 0) {
$this->bytesWritten += $len;
$this->sendBuffer = substr($this->sendBuffer, $len);
} else {
++self::$statistics['send_fail'];
$this->destroy();
}
}
/**
* SSL handshake.
*
* @param resource $socket
* @return bool|int
*/
public function doSslHandshake($socket): bool|int
{
if (!is_resource($socket) || feof($socket)) {
$this->destroy();
return false;
}
$async = $this instanceof AsyncTcpConnection;
/**
* We disabled ssl3 because https://blog.qualys.com/ssllabs/2014/10/15/ssl-3-is-dead-killed-by-the-poodle-attack.
* You can enable ssl3 by the codes below.
*/
/*if($async){
$type = STREAM_CRYPTO_METHOD_SSLv2_CLIENT | STREAM_CRYPTO_METHOD_SSLv23_CLIENT | STREAM_CRYPTO_METHOD_SSLv3_CLIENT;
}else{
$type = STREAM_CRYPTO_METHOD_SSLv2_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER | STREAM_CRYPTO_METHOD_SSLv3_SERVER;
}*/
if ($async) {
$type = STREAM_CRYPTO_METHOD_SSLv2_CLIENT | STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
} else {
$type = STREAM_CRYPTO_METHOD_SSLv2_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER;
}
// Hidden error.
set_error_handler(static function (int $code, string $msg): bool {
if (!Worker::$daemonize) {
Worker::safeEcho(sprintf("SSL handshake error: %s\n", $msg));
}
return true;
});
$ret = stream_socket_enable_crypto($socket, true, $type);
restore_error_handler();
// Negotiation has failed.
if (false === $ret) {
$this->destroy();
return false;
}
if (0 === $ret) {
// There isn't enough data and should try again.
return 0;
}
return true;
}
/**
* This method pulls all the data out of a readable stream, and writes it to the supplied destination.
*
* @param self $dest
* @return void
*/
public function pipe(self $dest): void
{
$this->onMessage = fn ($source, $data) => $dest->send($data);
$this->onClose = fn () => $dest->close();
$dest->onBufferFull = fn () => $this->pauseRecv();
$dest->onBufferDrain = fn() => $this->resumeRecv();
}
/**
* Remove $length of data from receive buffer.
*
* @param int $length
* @return void
*/
public function consumeRecvBuffer(int $length): void
{
$this->recvBuffer = substr($this->recvBuffer, $length);
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close(mixed $data = null, bool $raw = false): void
{
if ($this->status === self::STATUS_INITIAL || $this->status === self::STATUS_CONNECTING) {
$this->destroy();
return;
}
if ($this->status === self::STATUS_CLOSING || $this->status === self::STATUS_CLOSED) {
return;
}
if ($data !== null) {
$this->send($data, $raw);
}
$this->status = self::STATUS_CLOSING;
if ($this->sendBuffer === '') {
$this->destroy();
} else {
$this->pauseRecv();
}
}
/**
* Graceful end connection.
* It tries to: send response -> wait sendBuffer empty -> shutdown write(FIN) -> linger/drain reads -> close().
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function end(mixed $data = null, bool $raw = false): void
{
if ($this->status === self::STATUS_INITIAL || $this->status === self::STATUS_CONNECTING) {
$this->destroy();
return;
}
if ($this->status === self::STATUS_ENDING || $this->status === self::STATUS_CLOSING || $this->status === self::STATUS_CLOSED) {
return;
}
if ($data !== null) {
$this->send($data, $raw);
}
// Enter ending mode: stop protocol parsing and only drain incoming data.
$this->status = self::STATUS_ENDING;
// Disable business callback after end().
$this->onMessage = static function (self $connection, mixed $data = null): void {};
$this->recvBuffer = '';
$this->currentPackageLength = 0;
// If already flushed to kernel, shutdown write now. Otherwise, baseWrite() will call endMaybeShutdownWrite()
// when sendBuffer becomes empty.
if ($this->sendBuffer === '') {
$this->endMaybeShutdownWrite();
return;
}
}
/**
* If in ENDING and sendBuffer is empty, shutdown write side and start linger timer.
*
* @return void
*/
protected function endMaybeShutdownWrite(): void
{
if ($this->status !== self::STATUS_ENDING || $this->endWriteShutdown || $this->sendBuffer !== '') {
return;
}
if (is_resource($this->socket)) {
try {
@stream_socket_shutdown($this->socket, STREAM_SHUT_WR);
} catch (Throwable) {
// ignore
}
}
$this->endWriteShutdown = true;
$timeout = $this->lingerTimeout;
if ($timeout <= 0) {
$this->close();
return;
}
$this->endLingerTimerId = Timer::delay($timeout, function (): void {
$this->endLingerTimerId = 0;
if ($this->status === self::STATUS_CLOSED) {
return;
}
$this->close();
});
}
/**
* Is ipv4.
*
* return bool.
*/
public function isIpV4(): bool
{
if ($this->transport === 'unix') {
return false;
}
return !str_contains($this->getRemoteIp(), ':');
}
/**
* Is ipv6.
*
* return bool.
*/
public function isIpV6(): bool
{
if ($this->transport === 'unix') {
return false;
}
return str_contains($this->getRemoteIp(), ':');
}
/**
* Get the real socket.
*
* @return resource
*/
public function getSocket()
{
return $this->socket;
}
/**
* Check whether send buffer will be full.
*
* @return void
*/
protected function checkBufferWillFull(): void
{
if ($this->onBufferFull && $this->maxSendBufferSize <= strlen($this->sendBuffer)) {
try {
($this->onBufferFull)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
}
/**
* Whether send buffer is full.
*
* @return bool
*/
protected function bufferIsFull(): bool
{
// Buffer has been marked as full but still has data to send then the packet is discarded.
if ($this->maxSendBufferSize <= strlen($this->sendBuffer)) {
if ($this->onError) {
try {
($this->onError)($this, static::SEND_FAIL, 'send buffer full and drop package');
} catch (Throwable $e) {
$this->error($e);
}
}
return true;
}
return false;
}
/**
* Whether send buffer is Empty.
*
* @return bool
*/
public function bufferIsEmpty(): bool
{
return empty($this->sendBuffer);
}
/**
* Destroy connection.
*
* @return void
*/
public function destroy(): void
{
// Avoid repeated calls.
if ($this->status === self::STATUS_CLOSED) {
return;
}
// Remove event listener.
if($this->eventLoop !== null){
$this->eventLoop->offReadable($this->socket);
$this->eventLoop->offWritable($this->socket);
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'offExcept')) {
$this->eventLoop->offExcept($this->socket);
}
}
// Close socket.
try {
@fclose($this->socket);
} catch (Throwable) {
}
$this->status = self::STATUS_CLOSED;
// Try to emit onClose callback.
if ($this->onClose) {
try {
($this->onClose)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
// Try to emit protocol::onClose
if ($this->protocol && method_exists($this->protocol, 'onClose')) {
try {
$this->protocol::onClose($this);
} catch (Throwable $e) {
$this->error($e);
}
}
$this->sendBuffer = $this->recvBuffer = '';
$this->currentPackageLength = 0;
$this->isPaused = $this->sslHandshakeCompleted = false;
$this->endWriteShutdown = false;
if ($this->status === self::STATUS_CLOSED) {
// Cleaning up the callback to avoid memory leaks.
$this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = $this->eventLoop = $this->errorHandler = null;
// Remove from worker->connections.
if ($this->worker) {
unset($this->worker->connections[$this->realId]);
}
$this->worker = null;
unset(static::$connections[$this->realId]);
}
}
/**
* Get the json_encode information.
*
* @return array
*/
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'status' => $this->getStatus(),
'transport' => $this->transport,
'getRemoteIp' => $this->getRemoteIp(),
'remotePort' => $this->getRemotePort(),
'getRemoteAddress' => $this->getRemoteAddress(),
'getLocalIp' => $this->getLocalIp(),
'getLocalPort' => $this->getLocalPort(),
'getLocalAddress' => $this->getLocalAddress(),
'isIpV4' => $this->isIpV4(),
'isIpV6' => $this->isIpV6(),
];
}
/**
* __unserialize.
*
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->isSafe = false;
}
/**
* Destruct.
*
* @return void
*/
public function __destruct()
{
static $mod;
if (!$this->isSafe) {
return;
}
self::$statistics['connection_count']--;
if (Worker::getGracefulStop()) {
$mod ??= ceil((self::$statistics['connection_count'] + 1) / 3);
if (0 === self::$statistics['connection_count'] % $mod) {
$pid = function_exists('posix_getpid') ? posix_getpid() : 0;
Worker::log('worker[' . $pid . '] remains ' . self::$statistics['connection_count'] . ' connection(s)');
}
if (0 === self::$statistics['connection_count']) {
Worker::stopAll();
}
}
}
}
================================================
FILE: src/Connection/UdpConnection.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Connection;
use JsonSerializable;
use Workerman\Protocols\ProtocolInterface;
use function stream_socket_get_name;
use function stream_socket_sendto;
use function strlen;
use function strrchr;
use function strrpos;
use function substr;
use function trim;
/**
* UdpConnection.
*/
class UdpConnection extends ConnectionInterface implements JsonSerializable
{
/**
* Max udp package size.
*
* @var int
*/
public const MAX_UDP_PACKAGE_SIZE = 65535;
/**
* Transport layer protocol.
*
* @var string
*/
public string $transport = 'udp';
/**
* Construct.
*
* @param resource $socket
* @param string $remoteAddress
*/
/**
* @param resource|null $socket
*/
public function __construct(
/** @var resource|null */ protected $socket,
protected string $remoteAddress) {}
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
public function send(mixed $sendBuffer, bool $raw = false): bool|null
{
if (false === $raw && $this->protocol) {
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
if ($sendBuffer === '') {
return null;
}
}
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer, 0, $this->isIpV6() ? '[' . $this->getRemoteIp() . ']:' . $this->getRemotePort() : $this->remoteAddress);
}
/**
* Get remote IP.
*
* @return string
*/
public function getRemoteIp(): string
{
$pos = strrpos($this->remoteAddress, ':');
if ($pos) {
return trim(substr($this->remoteAddress, 0, $pos), '[]');
}
return '';
}
/**
* Get remote port.
*
* @return int
*/
public function getRemotePort(): int
{
if ($this->remoteAddress) {
return (int)substr(strrchr($this->remoteAddress, ':'), 1);
}
return 0;
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteAddress(): string
{
return $this->remoteAddress;
}
/**
* Get local IP.
*
* @return string
*/
public function getLocalIp(): string
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return '';
}
return substr($address, 0, $pos);
}
/**
* Get local port.
*
* @return int
*/
public function getLocalPort(): int
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return 0;
}
return (int)substr(strrchr($address, ':'), 1);
}
/**
* Get local address.
*
* @return string
*/
public function getLocalAddress(): string
{
return is_resource($this->socket) ? (string)@stream_socket_get_name($this->socket, false) : '';
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close(mixed $data = null, bool $raw = false): void
{
if ($data !== null) {
$this->send($data, $raw);
}
if ($this->eventLoop) {
$this->eventLoop->offReadable($this->socket);
}
if (is_resource($this->socket)) {
@fclose($this->socket);
}
$this->socket = null;
$this->eventLoop = $this->errorHandler = null;
}
/**
* Is ipv4.
*
* return bool.
*/
public function isIpV4(): bool
{
if ($this->transport === 'unix') {
return false;
}
return !str_contains($this->getRemoteIp(), ':');
}
/**
* Is ipv6.
*
* return bool.
*/
public function isIpV6(): bool
{
if ($this->transport === 'unix') {
return false;
}
return str_contains($this->getRemoteIp(), ':');
}
/**
* Get the real socket.
*
* @return resource
*/
/**
* @return resource|null
*/
public function getSocket()
{
return $this->socket;
}
/**
* Get the json_encode information.
*
* @return array
*/
public function jsonSerialize(): array
{
return [
'transport' => $this->transport,
'getRemoteIp' => $this->getRemoteIp(),
'remotePort' => $this->getRemotePort(),
'getRemoteAddress' => $this->getRemoteAddress(),
'getLocalIp' => $this->getLocalIp(),
'getLocalPort' => $this->getLocalPort(),
'getLocalAddress' => $this->getLocalAddress(),
'isIpV4' => $this->isIpV4(),
'isIpV6' => $this->isIpV6(),
];
}
}
================================================
FILE: src/Events/Ev.php
================================================
<?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>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
/**
* Ev eventloop
*/
final class Ev implements EventInterface
{
/**
* All listeners for read event.
*
* @var array<int, \EvIo>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, \EvIo>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, \EvSignal>
*/
private array $eventSignal = [];
/**
* All timer event listeners.
*
* @var array<int, \EvTimer>
*/
private array $eventTimer = [];
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Timer id.
*
* @var int
*/
private static int $timerId = 1;
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = self::$timerId;
$event = new \EvTimer($delay, 0, function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
$this->eventTimer[self::$timerId] = $event;
return self::$timerId++;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->eventTimer[$timerId]->stop();
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$event = new \EvTimer($interval, $interval, fn () => $this->safeCall($func, $args));
$this->eventTimer[self::$timerId] = $event;
return self::$timerId++;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fdKey = (int)$stream;
$event = new \EvIo($stream, \Ev::READ, fn () => $this->safeCall($func, [$stream]));
$this->readEvents[$fdKey] = $event;
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]->stop();
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fdKey = (int)$stream;
$event = new \EvIo($stream, \Ev::WRITE, fn () => $this->safeCall($func, [$stream]));
$this->writeEvents[$fdKey] = $event;
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]->stop();
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$event = new \EvSignal($signal, fn () => $this->safeCall($func, [$signal]));
$this->eventSignal[$signal] = $event;
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (isset($this->eventSignal[$signal])) {
$this->eventSignal[$signal]->stop();
unset($this->eventSignal[$signal]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $event) {
$event->stop();
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
\Ev::run();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
\Ev::stop();
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
================================================
FILE: src/Events/Event.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
/**
* libevent eventloop
*/
final class Event implements EventInterface
{
/**
* Event base.
*
* @var \EventBase
*/
private \EventBase $eventBase;
/**
* All listeners for read event.
*
* @var array<int, \Event>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, \Event>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, \Event>
*/
private array $eventSignal = [];
/**
* All timer event listeners.
*
* @var array<int, \Event>
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 0;
/**
* Event class name.
*
* @var string
*/
private string $eventClassName = '';
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Construct.
*/
public function __construct()
{
if (\class_exists('\\\\Event', false)) {
$className = '\\\\Event';
} else {
$className = '\Event';
}
$this->eventClassName = $className;
if (\class_exists('\\\\EventBase', false)) {
$className = '\\\\EventBase';
} else {
$className = '\EventBase';
}
$this->eventBase = new $className();
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$className = $this->eventClassName;
$timerId = $this->timerId++;
$event = new $className($this->eventBase, -1, $className::TIMEOUT, function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
if (!$event->addTimer($delay)) {
throw new \RuntimeException("Event::addTimer($delay) failed");
}
$this->eventTimer[$timerId] = $event;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->eventTimer[$timerId]->del();
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$className = $this->eventClassName;
$timerId = $this->timerId++;
$event = new $className($this->eventBase, -1, $className::TIMEOUT | $className::PERSIST, function () use ($func, $args) {
$this->safeCall($func, $args);
});
if (!$event->addTimer($interval)) {
throw new \RuntimeException("Event::addTimer($interval) failed");
}
$this->eventTimer[$timerId] = $event;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$className = $this->eventClassName;
$fdKey = (int)$stream;
$event = new $className($this->eventBase, $stream, $className::READ | $className::PERSIST, $func);
if ($event->add()) {
$this->readEvents[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]->del();
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$className = $this->eventClassName;
$fdKey = (int)$stream;
$event = new $className($this->eventBase, $stream, $className::WRITE | $className::PERSIST, $func);
if ($event->add()) {
$this->writeEvents[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]->del();
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$className = $this->eventClassName;
$fdKey = $signal;
$event = $className::signal($this->eventBase, $signal, fn () => $this->safeCall($func, [$signal]));
if ($event->add()) {
$this->eventSignal[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->eventSignal[$fdKey]->del();
unset($this->eventSignal[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $event) {
$event->del();
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
$this->eventBase->loop();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
$this->eventBase->exit();
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return \count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
================================================
FILE: src/Events/EventInterface.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
interface EventInterface
{
/**
* Delay the execution of a callback.
*
* @param float $delay
* @param callable(mixed...): void $func
* @param array $args
* @return int
*/
public function delay(float $delay, callable $func, array $args = []): int;
/**
* Delete a delay timer.
*
* @param int $timerId
* @return bool
*/
public function offDelay(int $timerId): bool;
/**
* Repeatedly execute a callback.
*
* @param float $interval
* @param callable(mixed...): void $func
* @param array $args
* @return int
*/
public function repeat(float $interval, callable $func, array $args = []): int;
/**
* Delete a repeat timer.
*
* @param int $timerId
* @return bool
*/
public function offRepeat(int $timerId): bool;
/**
* Execute a callback when a stream resource becomes readable or is closed for reading.
*
* @param resource $stream
* @param callable(resource): void $func
* @return void
*/
public function onReadable($stream, callable $func): void;
/**
* Cancel a callback of stream readable.
*
* @param resource $stream
* @return bool
*/
public function offReadable($stream): bool;
/**
* Execute a callback when a stream resource becomes writable or is closed for writing.
*
* @param resource $stream
* @param callable(resource): void $func
* @return void
*/
public function onWritable($stream, callable $func): void;
/**
* Cancel a callback of stream writable.
*
* @param resource $stream
* @return bool
*/
public function offWritable($stream): bool;
/**
* Execute a callback when a signal is received.
*
* @param int $signal
* @param callable(int): void $func
* @return void
*/
public function onSignal(int $signal, callable $func): void;
/**
* Cancel a callback of signal.
*
* @param int $signal
* @return bool
*/
public function offSignal(int $signal): bool;
/**
* Delete all timer.
*
* @return void
*/
public function deleteAllTimer(): void;
/**
* Run the event loop.
*
* @return void
*/
public function run(): void;
/**
* Stop event loop.
*
* @return void
*/
public function stop(): void;
/**
* Get Timer count.
*
* @return int
*/
public function getTimerCount(): int;
/**
* Set error handler.
*
* @param callable(\Throwable): void $errorHandler
* @return void
*/
public function setErrorHandler(callable $errorHandler): void;
}
================================================
FILE: src/Events/Fiber.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
use Fiber as BaseFiber;
use Revolt\EventLoop;
use Revolt\EventLoop\Driver;
use function count;
use function function_exists;
use function pcntl_signal;
/**
* Revolt eventloop
*/
final class Fiber implements EventInterface
{
/**
* @var Driver
*/
private Driver $driver;
/**
* All listeners for read event.
*
* @var array<int, string>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, string>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, string>
*/
private array $eventSignal = [];
/**
* Event listeners of timer.
*
* @var array<int, string>
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 1;
/**
* Construct.
*/
public function __construct()
{
$this->driver = EventLoop::getDriver();
}
/**
* Get driver.
*
* @return Driver
*/
public function driver(): Driver
{
return $this->driver;
}
/**
* {@inheritdoc}
*/
public function run(): void
{
$this->driver->run();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
foreach ($this->eventSignal as $cbId) {
$this->driver->cancel($cbId);
}
$this->driver->stop();
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, SIG_IGN);
}
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$closure = function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, ...$args);
};
$cbId = $this->driver->delay($delay, $closure);
$this->eventTimer[$timerId] = $cbId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$cbId = $this->driver->repeat($interval, fn() => $this->safeCall($func, ...$args));
$this->eventTimer[$timerId] = $cbId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->driver->cancel($this->readEvents[$fdKey]);
}
$this->readEvents[$fdKey] = $this->driver->onReadable($stream, fn() => $this->safeCall($func, $stream));
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->driver->cancel($this->readEvents[$fdKey]);
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->driver->cancel($this->writeEvents[$fdKey]);
unset($this->writeEvents[$fdKey]);
}
$this->writeEvents[$fdKey] = $this->driver->onWritable($stream, fn() => $this->safeCall($func, $stream));
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->driver->cancel($this->writeEvents[$fdKey]);
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->driver->cancel($this->eventSignal[$fdKey]);
unset($this->eventSignal[$fdKey]);
}
$this->eventSignal[$fdKey] = $this->driver->onSignal($signal, fn() => $this->safeCall($func, $signal));
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->driver->cancel($this->eventSignal[$fdKey]);
unset($this->eventSignal[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->driver->cancel($this->eventTimer[$timerId]);
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $cbId) {
$this->driver->cancel($cbId);
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->driver->setErrorHandler($errorHandler);
}
/**
* @param callable $func
* @param ...$args
* @return void
* @throws \Throwable
*/
protected function safeCall(callable $func, ...$args): void
{
(new BaseFiber(fn() => $func(...$args)))->start();
}
}
================================================
FILE: src/Events/Select.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
use SplPriorityQueue;
use Throwable;
use function count;
use function max;
use function microtime;
use function pcntl_signal;
use function pcntl_signal_dispatch;
use const DIRECTORY_SEPARATOR;
/**
* select eventloop
*/
final class Select implements EventInterface
{
/**
* Running.
*
* @var bool
*/
private bool $running = true;
/**
* All listeners for read/write event.
*
* @var array<int, callable>
*/
private array $readEvents = [];
/**
* All listeners for read/write event.
*
* @var array<int, callable>
*/
private array $writeEvents = [];
/**
* @var array<int, callable>
*/
private array $exceptEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, callable>
*/
private array $signalEvents = [];
/**
* Fds waiting for read event.
*
* @var array<int, resource>
*/
private array $readFds = [];
/**
* Fds waiting for write event.
*
* @var array<int, resource>
*/
private array $writeFds = [];
/**
* Fds waiting for except event.
*
* @var array<int, resource>
*/
private array $exceptFds = [];
/**
* Timer scheduler.
* {['data':timer_id, 'priority':run_timestamp], ..}
*
* @var SplPriorityQueue
*/
private SplPriorityQueue $scheduler;
/**
* All timer event listeners.
* [[func, args, flag, timer_interval], ..]
*
* @var array
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 1;
/**
* Select timeout.
*
* @var int
*/
private int $selectTimeout = self::MAX_SELECT_TIMOUT_US;
/**
* Next run time of the timer.
*
* @var float
*/
private float $nextTickTime = 0;
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Select timeout.
*
* @var int
*/
const MAX_SELECT_TIMOUT_US = 800000;
/**
* Construct.
*/
public function __construct()
{
$this->scheduler = new SplPriorityQueue();
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$runTime = microtime(true) + $delay;
$this->scheduler->insert($timerId, -$runTime);
$this->eventTimer[$timerId] = [$func, $args];
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
$this->setNextTickTime($runTime);
}
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$runTime = microtime(true) + $interval;
$this->scheduler->insert($timerId, -$runTime);
$this->eventTimer[$timerId] = [$func, $args, $interval];
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
$this->setNextTickTime($runTime);
}
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$count = count($this->readFds);
if ($count >= 1024) {
trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING);
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
}
$fdKey = (int)$stream;
$this->readEvents[$fdKey] = $func;
$this->readFds[$fdKey] = $stream;
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
unset($this->readEvents[$fdKey], $this->readFds[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$count = count($this->writeFds);
if ($count >= 1024) {
trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING);
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
}
$fdKey = (int)$stream;
$this->writeEvents[$fdKey] = $func;
$this->writeFds[$fdKey] = $stream;
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]);
return true;
}
return false;
}
/**
* On except.
*
* @param resource $stream
* @param callable $func
*/
public function onExcept($stream, callable $func): void
{
$fdKey = (int)$stream;
$this->exceptEvents[$fdKey] = $func;
$this->exceptFds[$fdKey] = $stream;
}
/**
* Off except.
*
* @param resource $stream
* @return bool
*/
public function offExcept($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->exceptEvents[$fdKey])) {
unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
if (!function_exists('pcntl_signal')) {
return;
}
$this->signalEvents[$signal] = $func;
pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal]));
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (!function_exists('pcntl_signal')) {
return false;
}
pcntl_signal($signal, SIG_IGN);
if (isset($this->signalEvents[$signal])) {
unset($this->signalEvents[$signal]);
return true;
}
return false;
}
/**
* Tick for timer.
*
* @return void
*/
protected function tick(): void
{
$tasksToInsert = [];
while (!$this->scheduler->isEmpty()) {
$schedulerData = $this->scheduler->top();
$timerId = $schedulerData['data'];
$nextRunTime = -$schedulerData['priority'];
$timeNow = microtime(true);
$this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000);
if ($this->selectTimeout <= 0) {
$this->scheduler->extract();
if (!isset($this->eventTimer[$timerId])) {
continue;
}
// [func, args, timer_interval]
$taskData = $this->eventTimer[$timerId];
if (isset($taskData[2])) {
$nextRunTime = $timeNow + $taskData[2];
$tasksToInsert[] = [$timerId, -$nextRunTime];
} else {
unset($this->eventTimer[$timerId]);
}
$this->safeCall($taskData[0], $taskData[1]);
} else {
break;
}
}
foreach ($tasksToInsert as $item) {
$this->scheduler->insert($item[0], $item[1]);
}
if (!$this->scheduler->isEmpty()) {
$schedulerData = $this->scheduler->top();
$nextRunTime = -$schedulerData['priority'];
$this->setNextTickTime($nextRunTime);
return;
}
$this->setNextTickTime(0);
}
/**
* Set next tick time.
*
* @param float $nextTickTime
* @return void
*/
protected function setNextTickTime(float $nextTickTime): void
{
$this->nextTickTime = $nextTickTime;
if ($nextTickTime == 0) {
$this->selectTimeout = self::MAX_SELECT_TIMOUT_US;
return;
}
$this->selectTimeout = min(max((int)(($nextTickTime - microtime(true)) * 1000000), 0), self::MAX_SELECT_TIMOUT_US);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
$this->scheduler = new SplPriorityQueue();
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
while ($this->running) {
$read = $this->readFds;
$write = $this->writeFds;
$except = $this->exceptFds;
if ($read || $write || $except) {
// Waiting read/write/signal/timeout events.
try {
@stream_select($read, $write, $except, 0, $this->selectTimeout);
} catch (Throwable) {
// do nothing
}
} else {
$this->selectTimeout >= 1 && usleep($this->selectTimeout);
}
foreach ($read as $fd) {
$fdKey = (int)$fd;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]($fd);
}
}
foreach ($write as $fd) {
$fdKey = (int)$fd;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]($fd);
}
}
foreach ($except as $fd) {
$fdKey = (int)$fd;
if (isset($this->exceptEvents[$fdKey])) {
$this->exceptEvents[$fdKey]($fd);
}
}
if ($this->nextTickTime > 0) {
if (microtime(true) >= $this->nextTickTime) {
$this->tick();
} else {
$this->selectTimeout = (int)(($this->nextTickTime - microtime(true)) * 1000000);
}
}
// The $this->signalEvents are empty under Windows, make sure not to call pcntl_signal_dispatch.
if ($this->signalEvents) {
// Calls signal handlers for pending signals
pcntl_signal_dispatch();
}
}
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
$this->running = false;
$this->deleteAllTimer();
foreach ($this->signalEvents as $signal => $item) {
$this->offsignal($signal);
}
$this->readFds = [];
$this->writeFds = [];
$this->exceptFds = [];
$this->readEvents = [];
$this->writeEvents = [];
$this->exceptEvents = [];
$this->signalEvents = [];
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
================================================
FILE: src/Events/Swoole.php
================================================
<?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>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
use Swoole\Coroutine;
use Swoole\Event;
use Swoole\Process;
use Swoole\Timer;
use Throwable;
final class Swoole implements EventInterface
{
/**
* All listeners for read timer
*
* @var array<int, int>
*/
private array $eventTimer = [];
/**
* All listeners for read event.
*
* @var array<int, array>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, array>
*/
private array $writeEvents = [];
/**
* @var ?callable
*/
private $errorHandler = null;
private bool $stopping = false;
/**
* Constructor.
*/
public function __construct()
{
Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$t = (int)($delay * 1000);
$t = max($t, 1);
$timerId = Timer::after($t, function () use ($func, $args, &$timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
Timer::clear($timerId);
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$t = (int)($interval * 1000);
$t = max($t, 1);
$timerId = Timer::tick($t, function () use ($func, $args) {
$this->safeCall($func, $args);
});
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
Event::add($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
} elseif (isset($this->writeEvents[$fd])) {
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE);
} else {
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
}
$this->readEvents[$fd] = [$func, [$stream]];
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd])) {
return false;
}
unset($this->readEvents[$fd]);
if (!isset($this->writeEvents[$fd])) {
Event::del($stream);
return true;
}
Event::set($stream, null, null, SWOOLE_EVENT_WRITE);
return true;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
Event::add($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE);
} elseif (isset($this->readEvents[$fd])) {
Event::set($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE | SWOOLE_EVENT_READ);
} else {
Event::set($stream, null, fn () =>$this->callWrite($fd), SWOOLE_EVENT_WRITE);
}
$this->writeEvents[$fd] = [$func, [$stream]];
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fd = (int)$stream;
if (!isset($this->writeEvents[$fd])) {
return false;
}
unset($this->writeEvents[$fd]);
if (!isset($this->readEvents[$fd])) {
Event::del($stream);
return true;
}
Event::set($stream, null, null, SWOOLE_EVENT_READ);
return true;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
Process::signal($signal, fn () => $this->safeCall($func, [$signal]));
}
/**
* Please see https://wiki.swoole.com/#/process/process?id=signal
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
return Process::signal($signal, null);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $timerId) {
Timer::clear($timerId);
}
}
/**
* {@inheritdoc}
*/
public function run(): void
{
// Avoid process exit due to no listening
Timer::tick(100000000, static fn() => null);
Event::wait();
}
/**
* Destroy loop.
*
* @return void
*/
public function stop(): void
{
if ($this->stopping) {
return;
}
$this->stopping = true;
// Cancel all coroutines before Event::exit
foreach (Coroutine::listCoroutines() as $coroutine) {
Coroutine::cancel($coroutine);
}
// Wait for coroutines to exit
usleep(200000);
Event::exit();
}
/**
* Get timer count.
*
* @return integer
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param $fd
* @return void
*/
private function callRead($fd)
{
if (isset($this->readEvents[$fd])) {
$this->safeCall($this->readEvents[$fd][0], $this->readEvents[$fd][1]);
}
}
/**
* @param $fd
* @return void
*/
private function callWrite($fd)
{
if (isset($this->writeEvents[$fd])) {
$this->safeCall($this->writeEvents[$fd][0], $this->writeEvents[$fd][1]);
}
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
Coroutine::create(function() use ($func, $args) {
try {
$func(...$args);
} catch (Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
});
}
}
================================================
FILE: src/Events/Swow.php
================================================
<?php
declare(strict_types=1);
namespace Workerman\Events;
use RuntimeException;
use Workerman\Coroutine\Coroutine\Swow as Coroutine;
use Swow\Signal;
use Swow\SignalException;
use function Swow\Sync\waitAll;
final class Swow implements EventInterface
{
/**
* All listeners for read timer.
*
* @var array<int, int>
*/
private array $eventTimer = [];
/**
* All listeners for read event.
*
* @var array<int, Coroutine>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, Coroutine>
*/
private array $writeEvents = [];
/**
* All listeners for signal.
*
* @var array<int, Coroutine>
*/
private array $signalListener = [];
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Get timer count.
*
* @return integer
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$t = (int)($delay * 1000);
$t = max($t, 1);
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
msleep($t);
unset($this->eventTimer[Coroutine::getCurrent()->getId()]);
$this->safeCall($func, $args);
});
$timerId = $coroutine->getId();
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$t = (int)($interval * 1000);
$t = max($t, 1);
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
// @phpstan-ignore-next-line While loop condition is always true.
while (true) {
msleep($t);
$this->safeCall($func, $args);
}
});
$timerId = $coroutine->getId();
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
try {
(Coroutine::getAll()[$timerId])->kill();
return true;
} finally {
unset($this->eventTimer[$timerId]);
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $timerId) {
$this->offDelay($timerId);
}
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fd = (int)$stream;
if (isset($this->readEvents[$fd])) {
$this->offReadable($stream);
}
Coroutine::run(function () use ($stream, $func, $fd): void {
try {
$this->readEvents[$fd] = Coroutine::getCurrent();
while (true) {
if (!is_resource($stream)) {
$this->offReadable($stream);
break;
}
// Under Windows, setting a timeout is necessary; otherwise, the accept cannot be listened to.
// Setting it to 1000ms will result in a 1-second delay for the first accept under Windows.
if (!isset($this->readEvents[$fd]) || $this->readEvents[$fd] !== Coroutine::getCurrent()) {
break;
}
$rEvent = stream_poll_one($stream, STREAM_POLLIN | STREAM_POLLHUP, 1000);
if ($rEvent !== STREAM_POLLNONE) {
$this->safeCall($func, [$stream]);
}
if ($rEvent !== STREAM_POLLIN && $rEvent !== STREAM_POLLNONE) {
$this->offReadable($stream);
break;
}
}
} catch (RuntimeException) {
$this->offReadable($stream);
}
});
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
// 在当前协程执行 $coroutine->kill() 会导致不可预知问题,所以没有使用$coroutine->kill()
$fd = (int)$stream;
if (isset($this->readEvents[$fd])) {
unset($this->readEvents[$fd]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fd = (int)$stream;
if (isset($this->writeEvents[$fd])) {
$this->offWritable($stream);
}
Coroutine::run(function () use ($stream, $func, $fd): void {
try {
$this->writeEvents[$fd] = Coroutine::getCurrent();
while (true) {
if (!is_resource($stream)) {
$this->offWritable($stream);
break;
}
if (!isset($this->writeEvents[$fd]) || $this->writeEvents[$fd] !== Coroutine::getCurrent()) {
break;
}
$rEvent = stream_poll_one($stream, STREAM_POLLOUT | STREAM_POLLHUP, 1000);
if ($rEvent !== STREAM_POLLNONE) {
$this->safeCall($func, [$stream]);
}
if ($rEvent !== STREAM_POLLOUT && $rEvent !== STREAM_POLLNONE) {
$this->offWritable($stream);
break;
}
}
} catch (RuntimeException) {
$this->offWritable($stream);
}
});
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fd = (int)$stream;
if (isset($this->writeEvents[$fd])) {
unset($this->writeEvents[$fd]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
Coroutine::run(function () use ($signal, $func): void {
$this->signalListener[$signal] = Coroutine::getCurrent();
while (1) {
try {
Signal::wait($signal);
if (!isset($this->signalListener[$signal]) ||
$this->signalListener[$signal] !== Coroutine::getCurrent()) {
break;
}
$this->safeCall($func, [$signal]);
} catch (SignalException) {
// do nothing
}
}
});
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (!isset($this->signalListener[$signal])) {
return false;
}
unset($this->signalListener[$signal]);
return true;
}
/**
* {@inheritdoc}
*/
public function run(): void
{
waitAll();
}
/**
* Destroy loop.
*
* @return void
*/
public function stop(): void
{
Coroutine::killAll();
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
Coroutine::run(function () use ($func, $args): void {
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
});
}
}
================================================
FILE: src/Protocols/Frame.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use function pack;
use function strlen;
use function substr;
use function unpack;
/**
* Frame Protocol.
*/
class Frame
{
/**
* Check the integrity of the package.
*
* @param string $buffer
* @return int
*/
public static function input(string $buffer): int
{
if (strlen($buffer) < 4) {
return 0;
}
$unpackData = unpack('Ntotal_length', $buffer);
return $unpackData['total_length'];
}
/**
* Decode.
*
* @param string $buffer
* @return string
*/
public static function decode(string $buffer): string
{
return substr($buffer, 4);
}
/**
* Encode.
*
* @param string $data
* @return string
*/
public static function encode(string $data): string
{
$totalLength = 4 + strlen($data);
return pack('N', $totalLength) . $data;
}
}
================================================
FILE: src/Protocols/Http/Chunk.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function dechex;
use function strlen;
/**
* Class Chunk
* @package Workerman\Protocols\Http
*/
class Chunk implements Stringable
{
public function __construct(protected string $buffer) {}
public function __toString(): string
{
return dechex(strlen($this->buffer)) . "\r\n$this->buffer\r\n";
}
}
================================================
FILE: src/Protocols/Http/Request.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Exception;
use RuntimeException;
use Stringable;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http;
use function array_walk_recursive;
use function bin2hex;
use function clearstatcache;
use function count;
use function explode;
use function file_put_contents;
use function is_file;
use function json_decode;
use function ltrim;
use function microtime;
use function pack;
use function parse_str;
use function parse_url;
use function preg_match;
use function preg_replace;
use function strlen;
use function strpos;
use function strstr;
use function strtolower;
use function substr;
use function tempnam;
use function trim;
use function unlink;
use function urlencode;
/**
* Class Request
* @package Workerman\Protocols\Http
*/
class Request implements Stringable
{
/**
* Connection.
*
* @var ?TcpConnection
*/
public ?TcpConnection $connection = null;
/**
* @var int
*/
public static int $maxFileUploads = 1024;
/**
* Maximum string length for cache
*
* @var int
*/
public const MAX_CACHE_STRING_LENGTH = 4096;
/**
* Maximum cache size.
*
* @var int
*/
public const MAX_CACHE_SIZE = 256;
/**
* Properties.
*
* @var array
*/
public array $properties = [];
/**
* Request data.
*
* @var array
*/
protected array $data = [];
/**
* Is safe.
*
* @var bool
*/
protected bool $isSafe = true;
/**
* Context.
*
* @var array
*/
public array $context = [];
/**
* Request constructor.
*
*/
public function __construct(protected string $buffer) {}
/**
* Get query.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function get(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['get'])) {
$this->parseGet();
}
if (null === $name) {
return $this->data['get'];
}
return $this->data['get'][$name] ?? $default;
}
/**
* Get post.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function post(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['post'])) {
$this->parsePost();
}
if (null === $name) {
return $this->data['post'];
}
return $this->data['post'][$name] ?? $default;
}
/**
* Get header item by name.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function header(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['headers'])) {
$this->parseHeaders();
}
if (null === $name) {
return $this->data['headers'];
}
$name = strtolower($name);
return $this->data['headers'][$name] ?? $default;
}
/**
* Get cookie item by name.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function cookie(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['cookie'])) {
$cookies = explode(';', $this->header('cookie', ''));
$mapped = array();
foreach ($cookies as $cookie) {
$cookie = explode('=', $cookie, 2);
if (count($cookie) !== 2) {
continue;
}
$mapped[trim($cookie[0])] = $cookie[1];
}
$this->data['cookie'] = $mapped;
}
if ($name === null) {
return $this->data['cookie'];
}
return $this->data['cookie'][$name] ?? $default;
}
/**
* Get upload files.
*
* @param string|null $name
* @return array|null
*/
public function file(?string $name = null): mixed
{
clearstatcache();
if (!empty($this->data['files'])) {
array_walk_recursive($this->data['files'], function ($value, $key) {
if ($key === 'tmp_name' && !is_file($value)) {
$this->data['files'] = [];
}
});
}
if (empty($this->data['files'])) {
$this->parsePost();
}
if (null === $name) {
return $this->data['files'];
}
return $this->data['files'][$name] ?? null;
}
/**
* Get method.
*
* @return string
*/
public function method(): string
{
if (!isset($this->data['method'])) {
$this->parseHeadFirstLine();
}
return $this->data['method'];
}
/**
* Get http protocol version.
*
* @return string
*/
public function protocolVersion(): string
{
if (!isset($this->data['protocolVersion'])) {
$this->parseProtocolVersion();
}
return $this->data['protocolVersion'];
}
/**
* Get host.
*
* @param bool $withoutPort
* @return string|null
*/
public function host(bool $withoutPort = false): ?string
{
$host = $this->header('host');
if ($host && $withoutPort) {
return preg_replace('/:\d{1,5}$/', '', $host);
}
return $host;
}
/**
* Get uri.
*
* @return string
*/
public function uri(): string
{
if (!isset($this->data['uri'])) {
$this->parseHeadFirstLine();
}
return $this->data['uri'];
}
/**
* Get path.
*
* @return string
*/
public function path(): string
{
if (!isset($this->data['path'])) {
$this->parseUriComponents();
}
return $this->data['path'];
}
/**
* Get query string.
*
* @return string
*/
public function queryString(): string
{
if (!isset($this->data['query_string'])) {
$this->parseUriComponents();
}
return $this->data['query_string'];
}
/**
* Parse URI into path and query string components (single parse_url call).
*
* @return void
*/
protected function parseUriComponents(): void
{
$uri = $this->uri();
$parsed = parse_url($uri);
$this->data['path'] = $parsed['path'] ?? '/';
$this->data['query_string'] = $parsed['query'] ?? '';
}
/**
* Get session.
*
* @return Session
* @throws Exception
*/
public function session(): Session
{
return $this->context['session'] ??= new Session($this->sessionId());
}
/**
* Get/Set session id.
*
* @param string|null $sessionId
* @return string
* @throws Exception
*/
public function sessionId(?string $sessionId = null): string
{
if ($sessionId) {
unset($this->context['sid'], $this->context['session']);
}
if (!isset($this->context['sid'])) {
$sessionName = Session::$name;
$sid = $sessionId ? '' : $this->cookie($sessionName);
// Strip surrounding double quotes (RFC 6265 allows DQUOTE-wrapped cookie values).
if (is_string($sid) && isset($sid[1]) && $sid[0] === '"' && $sid[-1] === '"') {
$sid = substr($sid, 1, -1);
}
$sid = $this->isValidSessionId($sid) ? $sid : '';
if ($sid === '') {
if (!$this->connection) {
throw new RuntimeException('Request->session() fail, header already send');
}
$sid = $sessionId ?: static::createSessionId();
$cookieParams = Session::getCookieParams();
$this->setSidCookie($sessionName, $sid, $cookieParams);
}
$this->context['sid'] = $sid;
}
return $this->context['sid'];
}
/**
* Check if session id is valid.
*
* @param mixed $sessionId
* @return bool
*/
public function isValidSessionId(mixed $sessionId): bool
{
return is_string($sessionId) && preg_match('/^[a-zA-Z0-9,-]{16,256}$/', $sessionId);
}
/**
* Session regenerate id.
*
* @param bool $deleteOldSession
* @return string
* @throws Exception
*/
public function sessionRegenerateId(bool $deleteOldSession = false): string
{
$session = $this->session();
$sessionData = $session->all();
if ($deleteOldSession) {
$session->flush();
}
$newSid = static::createSessionId();
$session = new Session($newSid);
$session->put($sessionData);
$cookieParams = Session::getCookieParams();
$sessionName = Session::$name;
$this->setSidCookie($sessionName, $newSid, $cookieParams);
$this->context['sid'] = $newSid;
$this->context['session'] = $session;
return $newSid;
}
/**
* Get http raw head.
*
* @return string
*/
public function rawHead(): string
{
return $this->data['head'] ??= strstr($this->buffer, "\r\n\r\n", true);
}
/**
* Get http raw body.
*
* @return string
*/
public function rawBody(): string
{
return substr($this->buffer, strpos($this->buffer, "\r\n\r\n") + 4);
}
/**
* Get raw buffer.
*
* @return string
*/
public function rawBuffer(): string
{
return $this->buffer;
}
/**
* Parse first line of http header buffer.
*
* @return void
*/
protected function parseHeadFirstLine(): void
{
$firstLine = strstr($this->buffer, "\r\n", true);
$tmp = explode(' ', $firstLine, 3);
$this->data['method'] = $tmp[0];
$this->data['uri'] = $tmp[1] ?? '/';
}
/**
* Parse protocol version.
*
* @return void
*/
protected function parseProtocolVersion(): void
{
$firstLine = strstr($this->buffer, "\r\n", true);
$httpStr = strstr($firstLine, 'HTTP/');
$protocolVersion = $httpStr ? substr($httpStr, 5) : '1.0';
$this->data['protocolVersion'] = $protocolVersion;
}
/**
* Parse headers.
*
* @return void
*/
protected function parseHeaders(): void
{
static $cache = [];
$this->data['headers'] = [];
$rawHead = $this->rawHead();
$endLinePosition = strpos($rawHead, "\r\n");
if ($endLinePosition === false) {
return;
}
$headBuffer = substr($rawHead, $endLinePosition + 2);
$cacheable = !isset($headBuffer[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$headBuffer])) {
$this->data['headers'] = $cache[$headBuffer];
return;
}
$headData = explode("\r\n", $headBuffer);
foreach ($headData as $content) {
if (str_contains($content, ':')) {
[$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[$headBuffer] = $this->data['headers'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse head.
*
* @return void
*/
protected function parseGet(): void
{
static $cache = [];
$queryString = $this->queryString();
$this->data['get'] = [];
if ($queryString === '') {
return;
}
$cacheable = !isset($queryString[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$queryString])) {
$this->data['get'] = $cache[$queryString];
return;
}
parse_str($queryString, $this->data['get']);
if ($cacheable) {
$cache[$queryString] = $this->data['get'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse post.
*
* @return void
*/
protected function parsePost(): void
{
static $cache = [];
$this->data['post'] = $this->data['files'] = [];
$contentType = $this->header('content-type', '');
if (preg_match('/boundary="?(\S+)"?/', $contentType, $match)) {
$httpPostBoundary = '--' . $match[1];
$this->parseUploadFiles($httpPostBoundary);
return;
}
$bodyBuffer = $this->rawBody();
if ($bodyBuffer === '') {
return;
}
$cacheable = !isset($bodyBuffer[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$bodyBuffer])) {
$this->data['post'] = $cache[$bodyBuffer];
return;
}
if (preg_match('/\bjson\b/i', $contentType)) {
$this->data['post'] = (array)json_decode($bodyBuffer, true);
} else {
parse_str($bodyBuffer, $this->data['post']);
}
if ($cacheable) {
$cache[$bodyBuffer] = $this->data['post'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse upload files.
*
* @param string $httpPostBoundary
* @return void
*/
protected function parseUploadFiles(string $httpPostBoundary): void
{
$httpPostBoundary = trim($httpPostBoundary, '"');
$buffer = $this->buffer;
$postEncodeString = '';
$filesEncodeString = '';
$files = [];
$bodyPosition = strpos($buffer, "\r\n\r\n") + 4;
$offset = $bodyPosition + strlen($httpPostBoundary) + 2;
$maxCount = static::$maxFileUploads;
while ($maxCount-- > 0 && $offset) {
$offset = $this->parseUploadFile($httpPostBoundary, $offset, $postEncodeString, $filesEncodeString, $files);
}
if ($postEncodeString) {
parse_str($postEncodeString, $this->data['post']);
}
if ($filesEncodeString) {
parse_str($filesEncodeString, $this->data['files']);
array_walk_recursive($this->data['files'], function (&$value) use ($files) {
$value = $files[$value];
});
}
}
/**
* Parse upload file.
*
* @param string $boundary
* @param int $sectionStartOffset
* @param string $postEncodeString
* @param string $filesEncodeStr
* @param array $files
* @return int
*/
protected function parseUploadFile(string $boundary, int $sectionStartOffset, string &$postEncodeString, string &$filesEncodeStr, array &$files): int
{
$file = [];
$boundary = "\r\n$boundary";
if (strlen($this->buffer) < $sectionStartOffset) {
return 0;
}
$sectionEndOffset = strpos($this->buffer, $boundary, $sectionStartOffset);
if (!$sectionEndOffset) {
return 0;
}
$contentLinesEndOffset = strpos($this->buffer, "\r\n\r\n", $sectionStartOffset);
if (!$contentLinesEndOffset || $contentLinesEndOffset + 4 > $sectionEndOffset) {
return 0;
}
$contentLinesStr = substr($this->buffer, $sectionStartOffset, $contentLinesEndOffset - $sectionStartOffset);
$contentLines = explode("\r\n", trim($contentLinesStr . "\r\n"));
$boundaryValue = substr($this->buffer, $contentLinesEndOffset + 4, $sectionEndOffset - $contentLinesEndOffset - 4);
$uploadKey = false;
foreach ($contentLines as $contentLine) {
if (!strpos($contentLine, ': ')) {
return 0;
}
[$key, $value] = explode(': ', $contentLine);
switch (strtolower($key)) {
case "content-disposition":
// Is file data.
if (preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
$error = 0;
$tmpFile = '';
$fileName = $match[1];
$size = strlen($boundaryValue);
$tmpUploadDir = HTTP::uploadTmpDir();
if (!$tmpUploadDir) {
$error = UPLOAD_ERR_NO_TMP_DIR;
} else if ($boundaryValue === '' && $fileName === '') {
$error = UPLOAD_ERR_NO_FILE;
} else {
$tmpFile = tempnam($tmpUploadDir, 'workerman.upload.');
if ($tmpFile === false || false === file_put_contents($tmpFile, $boundaryValue)) {
$error = UPLOAD_ERR_CANT_WRITE;
}
}
$uploadKey = $fileName;
// Parse upload files.
$file = [...$file, 'name' => $match[2], 'tmp_name' => $tmpFile, 'size' => $size, 'error' => $error, 'full_path' => $match[2]];
$file['type'] ??= '';
break;
}
// Is post field.
// Parse $POST.
if (preg_match('/name="(.*?)"$/', $value, $match)) {
$k = $match[1];
$postEncodeString .= urlencode($k) . "=" . urlencode($boundaryValue) . '&';
}
return $sectionEndOffset + strlen($boundary) + 2;
case "content-type":
$file['type'] = trim($value);
break;
case "webkitrelativepath":
$file['full_path'] = trim($value);
break;
}
}
if ($uploadKey === false) {
return 0;
}
$filesEncodeStr .= urlencode($uploadKey) . '=' . count($files) . '&';
$files[] = $file;
return $sectionEndOffset + strlen($boundary) + 2;
}
/**
* Create session id.
*
* @return string
* @throws RuntimeException
*/
public static function createSessionId(): string
{
$sid = session_create_id();
if ($sid === false) {
throw new RuntimeException('session_create_id() failed');
}
return $sid;
}
/**
* @param string $sessionName
* @param string $sid
* @param array $cookieParams
* @return void
*/
protected function setSidCookie(string $sessionName, string $sid, array $cookieParams): void
{
if (!$this->connection) {
throw new RuntimeException('Request->setSidCookie() fail, header already send');
}
$this->connection->headers['Set-Cookie'] = [$sessionName . '=' . $sid
. (empty($cookieParams['domain']) ? '' : '; Domain=' . $cookieParams['domain'])
. (empty($cookieParams['lifetime']) ? '' : '; Max-Age=' . $cookieParams['lifetime'])
. (empty($cookieParams['path']) ? '' : '; Path=' . $cookieParams['path'])
. (empty($cookieParams['samesite']) ? '' : '; SameSite=' . $cookieParams['samesite'])
. (!$cookieParams['secure'] ? '' : '; Secure')
. (!$cookieParams['httponly'] ? '' : '; HttpOnly')];
}
/**
* __toString.
*/
public function __toString(): string
{
return $this->buffer;
}
/**
* Setter.
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}
/**
* Getter.
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
return $this->properties[$name] ?? null;
}
/**
* Isset.
*
* @param string $name
* @return bool
*/
public function __isset(string $name): bool
{
return isset($this->properties[$name]);
}
/**
* Unset.
*
* @param string $name
* @return void
*/
public function __unset(string $name): void
{
unset($this->properties[$name]);
}
/**
* __unserialize.
*
* @param array $data
* @return void
*/
public function __unserialize(array $data): void
{
$this->isSafe = false;
}
/**
* Destroy.
*
* @return void
*/
public function destroy(): void
{
if ($this->context) {
$this->context = [];
}
if ($this->properties) {
$this->properties = [];
}
$this->connection = null;
}
/**
* Destructor.
*
* @return void
*/
public function __destruct()
{
if (!empty($this->data['files']) && $this->isSafe) {
clearstatcache();
array_walk_recursive($this->data['files'], function ($value, $key) {
if ($key === 'tmp_name' && is_file($value)) {
unlink($value);
}
});
}
}
}
================================================
FILE: src/Protocols/Http/Response.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function array_merge_recursive;
use function filemtime;
use function gmdate;
use function is_array;
use function is_file;
use function pathinfo;
use function rawurlencode;
use function strlen;
/**
* Class Response
* @package Workerman\Protocols\Http
*/
class Response implements Stringable
{
/**
* Http reason.
*
* @var ?string
*/
protected ?string $reason = null;
/**
* Http version.
*
* @var string
*/
protected string $version = '1.1';
/**
* Send file info
*
* @var ?array
*/
public ?array $file = null;
/**
* Mine type map.
* @var array
*/
protected static array $mimeTypeMap = [
// text
'html' => 'text/html',
'htm' => 'text/html',
'shtml' => 'text/html',
'css' => 'text/css',
'xml' => 'text/xml',
'mml' => 'text/mathml',
'txt' => 'text/plain',
'jad' => 'text/vnd.sun.j2me.app-descriptor',
'wml' => 'text/vnd.wap.wml',
'htc' => 'text/x-component',
// image
'gif' => 'image/gif',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'png' => 'image/png',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'wbmp' => 'image/vnd.wap.wbmp',
'ico' => 'image/x-icon',
'jng' => 'image/x-jng',
'bmp' => 'image/x-ms-bmp',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'webp' => 'image/webp',
'avif' => 'image/avif',
// application
'js' => 'application/javascript',
'atom' => 'application/atom+xml',
'rss' => 'application/rss+xml',
'wasm' => 'application/wasm',
'jar' => 'application/java-archive',
'war' => 'application/java-archive',
'ear' => 'application/java-archive',
'json' => 'application/json',
'hqx' => 'application/mac-binhex40',
'doc' => 'application/msword',
'pdf' => 'application/pdf',
'ps' => 'application/postscript',
'eps' => 'application/postscript',
'ai' => 'application/postscript',
'rtf' => 'application/rtf',
'm3u8' => 'application/vnd.apple.mpegurl',
'xls' => 'application/vnd.ms-excel',
'eot' => 'application/vnd.ms-fontobject',
'ppt' => 'application/vnd.ms-powerpoint',
'wmlc' => 'application/vnd.wap.wmlc',
'kml' => 'application/vnd.google-earth.kml+xml',
'kmz' => 'application/vnd.google-earth.kmz',
'7z' => 'application/x-7z-compressed',
'cco' => 'application/x-cocoa',
'jardiff' => 'application/x-java-archive-diff',
'jnlp' => 'application/x-java-jnlp-file',
'run' => 'application/x-makeself',
'pl' => 'application/x-perl',
'pm' => 'application/x-perl',
'prc' => 'application/x-pilot',
'pdb' => 'application/x-pilot',
'rar' => 'application/x-rar-compressed',
'rpm' => 'application/x-redhat-package-manager',
'sea' => 'application/x-sea',
'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit',
'tcl' => 'application/x-tcl',
'tk' => 'application/x-tcl',
'der' => 'application/x-x509-ca-cert',
'pem' => 'application/x-x509-ca-cert',
'crt' => 'application/x-x509-ca-cert',
'xpi' => 'application/x-xpinstall',
'xhtml' => 'application/xhtml+xml',
'xspf' => 'application/xspf+xml',
'zip' => 'application/zip',
'bin' => 'application/octet-stream',
'exe' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'deb' => 'application/octet-stream',
'dmg' => 'application/octet-stream',
'iso' => 'application/octet-stream',
'img' => 'application/octet-stream',
'msi' => 'application/octet-stream',
'msp' => 'application/octet-stream',
'msm' => 'application/octet-stream',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
// audio
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'kar' => 'audio/midi',
'mp3' => 'audio/mpeg',
'ogg' => 'audio/ogg',
'm4a' => 'audio/x-m4a',
'ra' => 'audio/x-realaudio',
// video
'3gpp' => 'video/3gpp',
'3gp' => 'video/3gpp',
'ts' => 'video/mp2t',
'mp4' => 'video/mp4',
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mov' => 'video/quicktime',
'webm' => 'video/webm',
'flv' => 'video/x-flv',
'm4v' => 'video/x-m4v',
'mng' => 'video/x-mng',
'asx' => 'video/x-ms-asf',
'asf' => 'video/x-ms-asf',
'wmv' => 'video/x-ms-wmv',
'avi' => 'video/x-msvideo',
// font
'ttf' => 'font/ttf',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
];
/**
* Phrases.
*
* @var array<int, string>
*
* @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
*/
public const PHRASES = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', // WebDAV; RFC 2518
103 => 'Early Hints', // RFC 8297
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information', // since HTTP/1.1
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content', // RFC 7233
207 => 'Multi-Status', // WebDAV; RFC 4918
208 => 'Already Reported', // WebDAV; RFC 5842
226 => 'IM Used', // RFC 3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found', // Previously "Moved temporarily"
303 => 'See Other', // since HTTP/1.1
304 => 'Not Modified', // RFC 7232
305 => 'Use Proxy', // since HTTP/1.1
306 => 'Switch Proxy',
307 => 'Temporary Redirect', // since HTTP/1.1
308 => 'Permanent Redirect', // RFC 7538
400 => 'Bad Request',
401 => 'Unauthorized', // RFC 7235
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required', // RFC 7235
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed', // RFC 7232
413 => 'Payload Too Large', // RFC 7231
414 => 'URI Too Long', // RFC 7231
415 => 'Unsupported Media Type', // RFC 7231
416 => 'Range Not Satisfiable', // RFC 7233
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC 2324, RFC 7168
421 => 'Misdirected Request', // RFC 7540
422 => 'Unprocessable Entity', // WebDAV; RFC 4918
423 => 'Locked', // WebDAV; RFC 4918
424 => 'Failed Dependency', // WebDAV; RFC 4918
425 => 'Too Early', // RFC 8470
426 => 'Upgrade Required',
428 => 'Precondition Required', // RFC 6585
429 => 'Too Many Requests', // RFC 6585
431 => 'Request Header Fields Too Large', // RFC 6585
451 => 'Unavailable For Legal Reasons', // RFC 7725
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates', // RFC 2295
507 => 'Insufficient Storage', // WebDAV; RFC 4918
508 => 'Loop Detected', // WebDAV; RFC 5842
510 => 'Not Extended', // RFC 2774
511 => 'Network Authentication Required', // RFC 6585
];
/**
* Response constructor.
*
* @param int $status
* @param array $headers
* @param string $body
*/
public function __construct(
protected int $status = 200,
protected array $headers = [],
protected string $body = ''
) {}
/**
* Set header.
*
* @param string $name
* @param string $value
* @return $this
*/
public function header(string $name, string $value): static
{
$this->headers[$name] = $value;
return $this;
}
/**
* Set header.
*
* @param string $name
* @param string $value
* @return $this
*/
public function withHeader(string $name, string $value): static
{
return $this->header($name, $value);
}
/**
* Set headers.
*
* @param array $headers
* @return $this
*/
public function withHeaders(array $headers): static
{
$this->headers = array_merge_recursive($this->headers, $headers);
return $this;
}
/**
* Remove header.
*
* @param string $name
* @return $this
*/
public function withoutHeader(string $name): static
{
unset($this->headers[$name]);
return $this;
}
/**
* Get header.
*
* @param string $name
* @return null|array|string
*/
public function getHeader(string $name): array|string|null
{
return $this->headers[$name] ?? null;
}
/**
* Get headers.
*
* @return array
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Set status.
*
* @param int $code
* @param string|null $reasonPhrase
* @return $this
*/
public function withStatus(int $code, ?string $reasonPhrase = null): static
{
$this->status = $code;
$this->reason = $reasonPhrase !== null ? str_replace(["\r", "\n"], '', $reasonPhrase) : null;
return $this;
}
/**
* Get status code.
*
* @return int
*/
public function getStatusCode(): int
{
return $this->status;
}
/**
* Get reason phrase.
*
* @return ?string
*/
public function getReasonPhrase(): ?string
{
return $this->reason;
}
/**
* Set protocol version.
*
* @param string $version
* @return $this
*/
public function withProtocolVersion(string $version): static
{
$this->version = str_replace(["\r", "\n"], '', $version);
return $this;
}
/**
* Set http body.
*
* @param string $body
* @return $this
*/
public function withBody(string $body): static
{
$this->body = $body;
return $this;
}
/**
* Get http raw body.
*
* @return string
*/
public function rawBody(): string
{
return $this->body;
}
/**
* Send file.
*
* @param string $file
* @param int $offset
* @param int $length
* @return $this
*/
public function withFile(string $file, int $offset = 0, int $length = 0): static
{
if (!is_file($file)) {
return $this->withStatus(404)->withBody('<h3>404 Not Found</h3>');
}
$this->file = ['file' => $file, 'offset' => $offset, 'length' => $length];
return $this;
}
/**
* Set cookie.
*
* @param string $name
* @param string $value
* @param int|null $maxAge
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
* @param string $sameSite
* @return $this
*/
public function cookie(string $name, string $value = '', ?int $maxAge = null, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, string $sameSite = ''): static
{
$this->headers['Set-Cookie'][] = $name . '=' . rawurlencode($value)
. (empty($domain) ? '' : '; Domain=' . $domain)
. ($maxAge === null ? '' : '; Max-Age=' . $maxAge)
. (empty($path) ? '' : '; Path=' . $path)
. (!$secure ? '' : '; Secure')
. (!$httpOnly ? '' : '; HttpOnly')
. (empty($sameSite) ? '' : '; SameSite=' . $sameSite);
return $this;
}
/**
* Create header for file.
*
* @param array $fileInfo
* @return string
*/
protected function createHeadForFile(array $fileInfo): string
{
$file = $fileInfo['file'];
$reason = $this->reason ?: self::PHRASES[$this->status];
$head = "HTTP/$this->version $this->status $reason\r\n";
$headers = $this->headers;
if (!isset($headers['Server'])) {
$head .= "Server: workerman\r\n";
}
foreach ($headers as $name => $value) {
// Skip unsafe header names
if (strpbrk((string)$name, ":\r\n") !== false) {
continue;
}
if (is_array($value)) {
foreach ($value as $item) {
// Skip unsafe header values
if (strpbrk((string)$item, "\r\n") !== false) {
continue;
}
$head .= "$name: $item\r\n";
}
continue;
}
// Skip unsafe header values
if (strpbrk((string)$value, "\r\n") !== false) {
continue;
}
$head .= "$name: $value\r\n";
}
if (!isset($headers['Connection'])) {
$head .= "Connection: keep-alive\r\n";
}
$fileInfo = pathinfo($file);
$extension = $fileInfo['extension'] ?? '';
$baseName = $fileInfo['basename'] ?: 'unknown';
// Remove ASCII control characters (0x00-0x1F, 0x7F) and unsafe quotes/backslashes to avoid breaking header formatting
$baseName = preg_replace('/["\\\\\x00-\x1F\x7F]/', '', $baseName);
if ($baseName === '') {
$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=\"$baseName\"\r\n";
}
if (!isset($headers['Last-Modified']) && $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(): string
{
if ($this->file) {
return $this->createHeadForFile($this->file);
}
$reason = $this->reason ?: self::PHRASES[$this->status] ?? '';
$bodyLen = strlen($this->body);
if (empty($this->headers)) {
return "HTTP/$this->version $this->status $reason\r\nServer: workerman\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $bodyLen\r\nConnection: keep-alive\r\n\r\n$this->body";
}
$head = "HTTP/$this->version $this->status $reason\r\n";
$headers = $this->headers;
if (!isset($headers['Server'])) {
$head .= "Server: workerman\r\n";
}
foreach ($headers as $name => $value) {
// Skip unsafe header names
if (strpbrk((string)$name, ":\r\n") !== false) {
continue;
}
if (is_array($value)) {
foreach ($value as $item) {
// Skip unsafe header values
if (strpbrk((string)$item, "\r\n") !== false) {
continue;
}
$head .= "$name: $item\r\n";
}
continue;
}
// Skip unsafe header values
if (strpbrk((string)$value, "\r\n") !== false) {
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') {
// For Server-Sent Events, send headers once and keep the connection open.
// Headers must be terminated by an empty line; ignore any preset body to avoid
// polluting the event stream with extra bytes or OS-specific newlines.
return $head . "\r\n";
}
if (!isset($headers['Transfer-Encoding'])) {
$head .= "Content-Length: $bodyLen\r\n\r\n";
} else {
return $bodyLen ? "$head\r\n" . dechex($bodyLen) . "\r\n$this->body\r\n" : "$head\r\n";
}
// The whole http package
return $head . $this->body;
}
}
================================================
FILE: src/Protocols/Http/ServerSentEvents.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function str_replace;
/**
* Class ServerSentEvents
* @package Workerman\Protocols\Http
*/
class ServerSentEvents implements Stringable
{
/**
* ServerSentEvents constructor.
* $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000]
*/
public function __construct(protected array $data) {}
public function __toString(): string
{
$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";
}
}
================================================
FILE: src/Protocols/Http/Session/FileSessionHandler.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Exception;
use Workerman\Protocols\Http\Session;
use function clearstatcache;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function glob;
use function is_dir;
use function is_file;
use function mkdir;
use function rename;
use function session_save_path;
use function strlen;
use function sys_get_temp_dir;
use function time;
use function touch;
use function unlink;
/**
* Class FileSessionHandler
* @package Workerman\Protocols\Http\Session
*/
class FileSessionHandler implements SessionHandlerInterface
{
/**
* Session save path.
*
* @var string
*/
protected static string $sessionSavePath;
/**
* Session file prefix.
*
* @var string
*/
protected static string $sessionFilePrefix = 'session_';
/**
* Init.
*/
public static function init()
{
$savePath = @session_save_path();
if (!$savePath || str_starts_with($savePath, 'tcp://')) {
$savePath = sys_get_temp_dir();
}
static::sessionSavePath($savePath);
}
/**
* FileSessionHandler constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
if (isset($config['save_path'])) {
static::sessionSavePath($config['save_path']);
}
}
/**
* {@inheritdoc}
*/
public function open(string $savePath, string $name): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function read(string $sessionId): string|false
{
$sessionFile = static::sessionFile($sessionId);
clearstatcache();
if (is_file($sessionFile)) {
if (time() - filemtime($sessionFile) > Session::$lifetime) {
unlink($sessionFile);
return false;
}
$data = file_get_contents($sessionFile);
return $data ?: false;
}
return false;
}
/**
* {@inheritdoc}
* @throws Exception
*/
public function write(string $sessionId, string $sessionData): bool
{
$tempFile = static::$sessionSavePath . uniqid(bin2hex(random_bytes(8)), true);
if (!file_put_contents($tempFile, $sessionData)) {
return false;
}
return rename($tempFile, static::sessionFile($sessionId));
}
/**
* Update session modify time.
*
* @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php
* @see https://www.php.net/manual/zh/function.touch.php
*
* @param string $sessionId Session id.
* @param string $data Session Data.
*
* @return bool
*/
public function updateTimestamp(string $sessionId, string $data = ""): bool
{
$sessionFile = static::sessionFile($sessionId);
if (!file_exists($sessionFile)) {
return false;
}
// set file modify time to current time
$setModifyTime = touch($sessionFile);
// clear file stat cache
clearstatcache();
return $setModifyTime;
}
/**
* {@inheritdoc}
*/
public function close(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function destroy(string $sessionId): bool
{
$sessionFile = static::sessionFile($sessionId);
if (is_file($sessionFile)) {
unlink($sessionFile);
}
return true;
}
/**
* {@inheritdoc}
*/
public function gc(int $maxLifetime): bool
{
$timeNow = time();
foreach (glob(static::$sessionSavePath . static::$sessionFilePrefix . '*') as $file) {
if (is_file($file) && $timeNow - filemtime($file) > $maxLifetime) {
unlink($file);
}
}
return true;
}
/**
* Get session file path.
*
* @param string $sessionId
* @return string
*/
protected static function sessionFile(string $sessionId): string
{
return static::$sessionSavePath . static::$sessionFilePrefix . $sessionId;
}
/**
* Get or set session file path.
*
* @param string $path
* @return string
*/
public static function sessionSavePath(string $path): string
{
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();
================================================
FILE: src/Protocols/Http/Session/RedisClusterSessionHandler.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Redis;
use RedisCluster;
use RedisClusterException;
use RedisException;
class RedisClusterSessionHandler extends RedisSessionHandler
{
/**
* @param array $config
* @throws RedisClusterException
* @throws RedisException
*/
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* Create redis connection.
* @param array $config
* @return Redis|RedisCluster
* @throws RedisClusterException
*/
protected function createRedisConnection(array $config): Redis|RedisCluster
{
$timeout = $config['timeout'] ?? 2;
$readTimeout = $config['read_timeout'] ?? $timeout;
$persistent = $config['persistent'] ?? false;
$auth = $config['auth'] ?? '';
$args = [null, $config['host'], $timeout, $readTimeout, $persistent];
if ($auth) {
$args[] = $auth;
}
$redis = new RedisCluster(...$args);
if (empty($config['prefix'])) {
$config['prefix'] = 'redis_session_';
}
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
return $redis;
}
}
================================================
FILE: src/Protocols/Http/Session/RedisSessionHandler.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Redis;
use RedisCluster;
use RedisException;
use RuntimeException;
use Throwable;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Workerman\Events\Fiber;
use Workerman\Protocols\Http\Session;
use Workerman\Timer;
use Workerman\Coroutine\Pool;
use Workerman\Coroutine\Context;
use Workerman\Worker;
/**
* Class RedisSessionHandler
* @package Workerman\Protocols\Http\Session
*/
class RedisSessionHandler implements SessionHandlerInterface
{
/**
* @var Redis|RedisCluster
*/
protected Redis|RedisCluster|null $connection = null;
/**
* @var array
*/
protected array $config;
/**
* @var Pool|null
*/
protected static ?Pool $pool = null;
/**
* RedisSessionHandler constructor.
* @param array $config = [
* 'host' => '127.0.0.1',
* 'port' => 6379,
* 'timeout' => 2,
* 'auth' => '******',
* 'database' => 2,
* 'prefix' => 'redis_session_',
* 'ping' => 55,
* ]
* @throws RedisException
*/
public function __construct(array $config)
{
if (false === extension_loaded('redis')) {
throw new RuntimeException('Please install redis extension.');
}
$config['timeout'] ??= 2;
$this->config = $config;
}
/**
* Get connection.
* @return Redis
* @throws Throwable
*/
protected function connection(): Redis|RedisCluster
{
// Cannot switch fibers in current execution context when PHP < 8.4
if (Worker::$eventLoopClass === Fiber::class && PHP_VERSION_ID < 80400) {
if (!$this->connection) {
$this->connection = $this->createRedisConnection($this->config);
Timer::delay($this->config['pool']['heartbeat_interval'] ?? 55, function () {
$this->connection->ping();
});
}
return $this->connection;
}
$key = 'session.redis.connection';
/** @var Redis|null $connection */
$connection = Context::get($key);
if (!$connection) {
if (!static::$pool) {
$poolConfig = $this->config['pool'] ?? [];
static::$pool = new Pool($poolConfig['max_connections'] ?? 10, $poolConfig);
static::$pool->setConnectionCreator(function () {
return $this->createRedisConnection($this->config);
});
static::$pool->setConnectionCloser(function (Redis|RedisCluster $connection) {
$connection->close();
});
static::$pool->setHeartbeatChecker(function (Redis|RedisCluster $connection) {
$connection->ping();
});
}
try {
$connection = static::$pool->get();
Context::set($key, $connection);
} finally {
$closure = function () use ($connection) {
try {
$connection && static::$pool && static::$pool->put($connection);
} catch (Throwable) {
// ignore
}
};
$obj = Context::get('context.onDestroy');
if (!$obj) {
$obj = new \stdClass();
Context::set('context.onDestroy', $obj);
}
DestructionWatcher::watch($obj, $closure);
}
}
return $connection;
}
/**
* Create redis connection.
* @param array $config
* @return Redis
*/
protected function createRedisConnection(array $config): Redis|RedisCluster
{
$redis = new Redis();
if (false === $redis->connect($config['host'], $config['port'], $config['timeout'])) {
throw new RuntimeException("Redis connect {$config['host']}:{$config['port']} fail.");
}
if (!empty($config['auth'])) {
$redis->auth($config['auth']);
}
if (!empty($config['database'])) {
$redis->select((int)$config['database']);
}
if (empty($config['prefix'])) {
$config['prefix'] = 'redis_session_';
}
$redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
return $redis;
}
/**
* {@inheritdoc}
*/
public function open(string $savePath, string $name): bool
{
return true;
}
/**
* {@inheritdoc}
* @param string $sessionId
* @return string|false
* @throws RedisException
* @throws Throwable
*/
public function read(string $sessionId): string|false
{
return $this->connection()->get($sessionId);
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function write(string $sessionId, string $sessionData): bool
{
return true === $this->connection()->setex($sessionId, Session::$lifetime, $sessionData);
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function updateTimestamp(string $sessionId, string $data = ""): bool
{
return true === $this->connection()->expire($sessionId, Session::$lifetime);
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function destroy(string $sessionId): bool
{
$this->connection()->del($sessionId);
return true;
}
/**
* {@inheritdoc}
*/
public function close(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function gc(int $maxLifetime): bool
{
return true;
}
}
================================================
FILE: src/Protocols/Http/Session/SessionHandlerInterface.php
================================================
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
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(): bool;
/**
* Destroy a session
* @link http://php.net/manual/en/sessionhandlerinterface.destroy.php
* @param string $sessionId 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(string $sessionId): bool;
/**
* 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(int $maxLifetime): bool;
/**
* Initialize session
* @link http://php.net/manual/en/sessionhandlerinterface.open.php
* @param string $savePath 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(string $savePath, string $name): bool;
/**
* Read session data
* @link http://php.net/manual/en/sessionhandlerinterface.read.php
* @param string $sessionId The session id to read data for.
* @return string|false <p>
* Returns an encoded string of the read data.
* If nothing was read, it must return false.
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function read(string $sessionId): string|false;
/**
* Write session data
* @link http://php.net/manual/en/sessionhandlerinterface.write
gitextract_71rp8_fe/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── MIT-LICENSE.txt
├── README.md
├── SECURITY.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│ ├── Connection/
│ │ ├── AsyncTcpConnection.php
│ │ ├── AsyncUdpConnection.php
│ │ ├── ConnectionInterface.php
│ │ ├── TcpConnection.php
│ │ └── UdpConnection.php
│ ├── Events/
│ │ ├── Ev.php
│ │ ├── Event.php
│ │ ├── EventInterface.php
│ │ ├── Fiber.php
│ │ ├── Select.php
│ │ ├── Swoole.php
│ │ └── Swow.php
│ ├── Protocols/
│ │ ├── Frame.php
│ │ ├── Http/
│ │ │ ├── Chunk.php
│ │ │ ├── Request.php
│ │ │ ├── Response.php
│ │ │ ├── ServerSentEvents.php
│ │ │ ├── Session/
│ │ │ │ ├── FileSessionHandler.php
│ │ │ │ ├── RedisClusterSessionHandler.php
│ │ │ │ ├── RedisSessionHandler.php
│ │ │ │ └── SessionHandlerInterface.php
│ │ │ └── Session.php
│ │ ├── Http.php
│ │ ├── ProtocolInterface.php
│ │ ├── Text.php
│ │ ├── Websocket.php
│ │ └── Ws.php
│ ├── Timer.php
│ └── Worker.php
└── tests/
├── Feature/
│ ├── ExampleTest.php
│ ├── HttpConnectionTest.php
│ ├── Stub/
│ │ ├── HttpServer.php
│ │ ├── UdpServer.php
│ │ ├── WebsocketClient.php
│ │ └── WebsocketServer.php
│ ├── UdpConnectionTest.php
│ └── WebsocketServiceTest.php
├── Pest.php
├── TestCase.php
└── Unit/
├── Connection/
│ ├── TcpConnectionEndOnMessageTest.php
│ ├── TcpConnectionEndTest.php
│ └── UdpConnectionTest.php
└── Protocols/
├── FrameTest.php
├── Http/
│ ├── RequestSessionTest.php
│ ├── ResponseTest.php
│ └── ServerSentEventsTest.php
├── HttpTest.php
└── TextTest.php
SYMBOL INDEX (442 symbols across 32 files)
FILE: src/Connection/AsyncTcpConnection.php
class AsyncTcpConnection (line 51) | class AsyncTcpConnection extends TcpConnection
method __construct (line 158) | public function __construct(string $remoteAddress, array $socketContex...
method reconnect (line 223) | public function reconnect(int $after = 0): void
method connect (line 242) | public function connect(): void
method emitError (line 310) | protected function emitError(int $code, mixed $msg): void
method cancelReconnect (line 325) | public function cancelReconnect(): void
method getRemoteHost (line 338) | public function getRemoteHost(): string
method getRemoteURI (line 348) | public function getRemoteURI(): string
method checkConnection (line 358) | public function checkConnection(): void
FILE: src/Connection/AsyncUdpConnection.php
class AsyncUdpConnection (line 41) | class AsyncUdpConnection extends UdpConnection
method __construct (line 77) | public function __construct($remoteAddress, $contextOption = [])
method baseRead (line 107) | public function baseRead($socket): void
method close (line 134) | public function close(mixed $data = null, bool $raw = false): void
method send (line 165) | public function send(mixed $sendBuffer, bool $raw = false): bool|null
method connect (line 184) | public function connect(): void
FILE: src/Connection/ConnectionInterface.php
class ConnectionInterface (line 28) | #[AllowDynamicProperties]
method send (line 103) | abstract public function send(mixed $sendBuffer, bool $raw = false): b...
method getRemoteIp (line 110) | abstract public function getRemoteIp(): string;
method getRemotePort (line 117) | abstract public function getRemotePort(): int;
method getRemoteAddress (line 124) | abstract public function getRemoteAddress(): string;
method getLocalIp (line 131) | abstract public function getLocalIp(): string;
method getLocalPort (line 138) | abstract public function getLocalPort(): int;
method getLocalAddress (line 145) | abstract public function getLocalAddress(): string;
method close (line 154) | abstract public function close(mixed $data = null, bool $raw = false):...
method isIpV4 (line 161) | abstract public function isIpV4(): bool;
method isIpV6 (line 168) | abstract public function isIpV6(): bool;
method error (line 174) | public function error(Throwable $exception): void
FILE: src/Connection/TcpConnection.php
class TcpConnection (line 66) | class TcpConnection extends ConnectionInterface implements JsonSerializable
method __construct (line 412) | public function __construct(EventInterface $eventLoop, $socket, string...
method getStatus (line 439) | public function getStatus(bool $rawOutput = true): int|string
method send (line 454) | public function send(mixed $sendBuffer, bool $raw = false): bool|null
method getRemoteIp (line 545) | public function getRemoteIp(): string
method getRemotePort (line 559) | public function getRemotePort(): int
method getRemoteAddress (line 572) | public function getRemoteAddress(): string
method getLocalIp (line 582) | public function getLocalIp(): string
method getLocalPort (line 597) | public function getLocalPort(): int
method getLocalAddress (line 612) | public function getLocalAddress(): string
method getSendBufferQueueSize (line 625) | public function getSendBufferQueueSize(): int
method getRecvBufferQueueSize (line 635) | public function getRecvBufferQueueSize(): int
method pauseRecv (line 645) | public function pauseRecv(): void
method resumeRecv (line 658) | public function resumeRecv(): void
method baseRead (line 675) | public function baseRead($socket, bool $checkEof = true): void
method baseWrite (line 831) | public function baseWrite(): void
method doSslHandshake (line 880) | public function doSslHandshake($socket): bool|int
method pipe (line 931) | public function pipe(self $dest): void
method consumeRecvBuffer (line 945) | public function consumeRecvBuffer(int $length): void
method close (line 957) | public function close(mixed $data = null, bool $raw = false): void
method end (line 989) | public function end(mixed $data = null, bool $raw = false): void
method endMaybeShutdownWrite (line 1024) | protected function endMaybeShutdownWrite(): void
method isIpV4 (line 1059) | public function isIpV4(): bool
method isIpV6 (line 1072) | public function isIpV6(): bool
method getSocket (line 1085) | public function getSocket()
method checkBufferWillFull (line 1095) | protected function checkBufferWillFull(): void
method bufferIsFull (line 1111) | protected function bufferIsFull(): bool
method bufferIsEmpty (line 1132) | public function bufferIsEmpty(): bool
method destroy (line 1142) | public function destroy(): void
method jsonSerialize (line 1201) | public function jsonSerialize(): array
method __unserialize (line 1224) | public function __unserialize(array $data): void
method __destruct (line 1234) | public function __destruct()
FILE: src/Connection/UdpConnection.php
class UdpConnection (line 32) | class UdpConnection extends ConnectionInterface implements JsonSerializable
method __construct (line 57) | public function __construct(
method send (line 68) | public function send(mixed $sendBuffer, bool $raw = false): bool|null
method getRemoteIp (line 84) | public function getRemoteIp(): string
method getRemotePort (line 98) | public function getRemotePort(): int
method getRemoteAddress (line 111) | public function getRemoteAddress(): string
method getLocalIp (line 121) | public function getLocalIp(): string
method getLocalPort (line 136) | public function getLocalPort(): int
method getLocalAddress (line 151) | public function getLocalAddress(): string
method close (line 164) | public function close(mixed $data = null, bool $raw = false): void
method isIpV4 (line 184) | public function isIpV4(): bool
method isIpV6 (line 197) | public function isIpV6(): bool
method getSocket (line 213) | public function getSocket()
method jsonSerialize (line 223) | public function jsonSerialize(): array
FILE: src/Events/Ev.php
class Ev (line 21) | final class Ev implements EventInterface
method delay (line 66) | public function delay(float $delay, callable $func, array $args = []):...
method offDelay (line 80) | public function offDelay(int $timerId): bool
method offRepeat (line 93) | public function offRepeat(int $timerId): bool
method repeat (line 101) | public function repeat(float $interval, callable $func, array $args = ...
method onReadable (line 111) | public function onReadable($stream, callable $func): void
method offReadable (line 121) | public function offReadable($stream): bool
method onWritable (line 135) | public function onWritable($stream, callable $func): void
method offWritable (line 145) | public function offWritable($stream): bool
method onSignal (line 159) | public function onSignal(int $signal, callable $func): void
method offSignal (line 168) | public function offSignal(int $signal): bool
method deleteAllTimer (line 181) | public function deleteAllTimer(): void
method run (line 192) | public function run(): void
method stop (line 200) | public function stop(): void
method getTimerCount (line 208) | public function getTimerCount(): int
method setErrorHandler (line 216) | public function setErrorHandler(callable $errorHandler): void
method safeCall (line 226) | private function safeCall(callable $func, array $args = []): void
FILE: src/Events/Event.php
class Event (line 22) | final class Event implements EventInterface
method __construct (line 81) | public function __construct()
method delay (line 100) | public function delay(float $delay, callable $func, array $args = []):...
method offDelay (line 118) | public function offDelay(int $timerId): bool
method offRepeat (line 131) | public function offRepeat(int $timerId): bool
method repeat (line 139) | public function repeat(float $interval, callable $func, array $args = ...
method onReadable (line 156) | public function onReadable($stream, callable $func): void
method offReadable (line 169) | public function offReadable($stream): bool
method onWritable (line 183) | public function onWritable($stream, callable $func): void
method offWritable (line 196) | public function offWritable($stream): bool
method onSignal (line 210) | public function onSignal(int $signal, callable $func): void
method offSignal (line 223) | public function offSignal(int $signal): bool
method deleteAllTimer (line 237) | public function deleteAllTimer(): void
method run (line 248) | public function run(): void
method stop (line 256) | public function stop(): void
method getTimerCount (line 264) | public function getTimerCount(): int
method setErrorHandler (line 272) | public function setErrorHandler(callable $errorHandler): void
method safeCall (line 282) | private function safeCall(callable $func, array $args = []): void
FILE: src/Events/EventInterface.php
type EventInterface (line 19) | interface EventInterface
method delay (line 29) | public function delay(float $delay, callable $func, array $args = []):...
method offDelay (line 37) | public function offDelay(int $timerId): bool;
method repeat (line 47) | public function repeat(float $interval, callable $func, array $args = ...
method offRepeat (line 55) | public function offRepeat(int $timerId): bool;
method onReadable (line 64) | public function onReadable($stream, callable $func): void;
method offReadable (line 72) | public function offReadable($stream): bool;
method onWritable (line 81) | public function onWritable($stream, callable $func): void;
method offWritable (line 89) | public function offWritable($stream): bool;
method onSignal (line 98) | public function onSignal(int $signal, callable $func): void;
method offSignal (line 106) | public function offSignal(int $signal): bool;
method deleteAllTimer (line 113) | public function deleteAllTimer(): void;
method run (line 120) | public function run(): void;
method stop (line 127) | public function stop(): void;
method getTimerCount (line 134) | public function getTimerCount(): int;
method setErrorHandler (line 142) | public function setErrorHandler(callable $errorHandler): void;
FILE: src/Events/Fiber.php
class Fiber (line 29) | final class Fiber implements EventInterface
method __construct (line 74) | public function __construct()
method driver (line 84) | public function driver(): Driver
method run (line 92) | public function run(): void
method stop (line 100) | public function stop(): void
method delay (line 114) | public function delay(float $delay, callable $func, array $args = []):...
method repeat (line 129) | public function repeat(float $interval, callable $func, array $args = ...
method onReadable (line 140) | public function onReadable($stream, callable $func): void
method offReadable (line 153) | public function offReadable($stream): bool
method onWritable (line 167) | public function onWritable($stream, callable $func): void
method offWritable (line 180) | public function offWritable($stream): bool
method onSignal (line 194) | public function onSignal(int $signal, callable $func): void
method offSignal (line 207) | public function offSignal(int $signal): bool
method offDelay (line 221) | public function offDelay(int $timerId): bool
method offRepeat (line 234) | public function offRepeat(int $timerId): bool
method deleteAllTimer (line 242) | public function deleteAllTimer(): void
method getTimerCount (line 253) | public function getTimerCount(): int
method setErrorHandler (line 261) | public function setErrorHandler(callable $errorHandler): void
method safeCall (line 272) | protected function safeCall(callable $func, ...$args): void
FILE: src/Events/Select.php
class Select (line 31) | final class Select implements EventInterface
method __construct (line 139) | public function __construct()
method delay (line 148) | public function delay(float $delay, callable $func, array $args = []):...
method repeat (line 163) | public function repeat(float $interval, callable $func, array $args = ...
method offDelay (line 178) | public function offDelay(int $timerId): bool
method offRepeat (line 190) | public function offRepeat(int $timerId): bool
method onReadable (line 198) | public function onReadable($stream, callable $func): void
method offReadable (line 214) | public function offReadable($stream): bool
method onWritable (line 227) | public function onWritable($stream, callable $func): void
method offWritable (line 243) | public function offWritable($stream): bool
method onExcept (line 259) | public function onExcept($stream, callable $func): void
method offExcept (line 272) | public function offExcept($stream): bool
method onSignal (line 285) | public function onSignal(int $signal, callable $func): void
method offSignal (line 297) | public function offSignal(int $signal): bool
method tick (line 315) | protected function tick(): void
method setNextTickTime (line 363) | protected function setNextTickTime(float $nextTickTime): void
method deleteAllTimer (line 376) | public function deleteAllTimer(): void
method run (line 386) | public function run(): void
method stop (line 443) | public function stop(): void
method getTimerCount (line 462) | public function getTimerCount(): int
method setErrorHandler (line 470) | public function setErrorHandler(callable $errorHandler): void
method safeCall (line 480) | private function safeCall(callable $func, array $args = []): void
FILE: src/Events/Swoole.php
class Swoole (line 24) | final class Swoole implements EventInterface
method __construct (line 57) | public function __construct()
method delay (line 65) | public function delay(float $delay, callable $func, array $args = []):...
method offDelay (line 80) | public function offDelay(int $timerId): bool
method offRepeat (line 93) | public function offRepeat(int $timerId): bool
method repeat (line 101) | public function repeat(float $interval, callable $func, array $args = ...
method onReadable (line 115) | public function onReadable($stream, callable $func): void
method offReadable (line 132) | public function offReadable($stream): bool
method onWritable (line 150) | public function onWritable($stream, callable $func): void
method offWritable (line 167) | public function offWritable($stream): bool
method onSignal (line 185) | public function onSignal(int $signal, callable $func): void
method offSignal (line 194) | public function offSignal(int $signal): bool
method deleteAllTimer (line 202) | public function deleteAllTimer(): void
method run (line 212) | public function run(): void
method stop (line 224) | public function stop(): void
method getTimerCount (line 244) | public function getTimerCount(): int
method setErrorHandler (line 252) | public function setErrorHandler(callable $errorHandler): void
method callRead (line 261) | private function callRead($fd)
method callWrite (line 272) | private function callWrite($fd)
method safeCall (line 284) | private function safeCall(callable $func, array $args = []): void
FILE: src/Events/Swow.php
class Swow (line 13) | final class Swow implements EventInterface
method getTimerCount (line 53) | public function getTimerCount(): int
method delay (line 61) | public function delay(float $delay, callable $func, array $args = []):...
method repeat (line 78) | public function repeat(float $interval, callable $func, array $args = ...
method offDelay (line 97) | public function offDelay(int $timerId): bool
method offRepeat (line 113) | public function offRepeat(int $timerId): bool
method deleteAllTimer (line 121) | public function deleteAllTimer(): void
method onReadable (line 131) | public function onReadable($stream, callable $func): void
method offReadable (line 168) | public function offReadable($stream): bool
method onWritable (line 182) | public function onWritable($stream, callable $func): void
method offWritable (line 217) | public function offWritable($stream): bool
method onSignal (line 230) | public function onSignal(int $signal, callable $func): void
method offSignal (line 252) | public function offSignal(int $signal): bool
method run (line 264) | public function run(): void
method stop (line 274) | public function stop(): void
method setErrorHandler (line 282) | public function setErrorHandler(callable $errorHandler): void
method safeCall (line 292) | private function safeCall(callable $func, array $args = []): void
FILE: src/Protocols/Frame.php
class Frame (line 27) | class Frame
method input (line 35) | public static function input(string $buffer): int
method decode (line 50) | public static function decode(string $buffer): string
method encode (line 61) | public static function encode(string $data): string
FILE: src/Protocols/Http.php
class Http (line 42) | class Http
method requestClass (line 64) | public static function requestClass(?string $className = null): string
method input (line 79) | public static function input(string $buffer, TcpConnection $connection...
method decode (line 145) | public static function decode(string $buffer, TcpConnection $connectio...
method encode (line 159) | public static function encode(mixed $response, TcpConnection $connecti...
method sendStream (line 230) | protected static function sendStream(TcpConnection $connection, $handl...
method uploadTmpDir (line 285) | public static function uploadTmpDir(string|null $dir = null): string
FILE: src/Protocols/Http/Chunk.php
class Chunk (line 28) | class Chunk implements Stringable
method __construct (line 31) | public function __construct(protected string $buffer) {}
method __toString (line 33) | public function __toString(): string
FILE: src/Protocols/Http/Request.php
class Request (line 53) | class Request implements Stringable
method __construct (line 113) | public function __construct(protected string $buffer) {}
method get (line 122) | public function get(?string $name = null, mixed $default = null): mixed
method post (line 140) | public function post(?string $name = null, mixed $default = null): mixed
method header (line 158) | public function header(?string $name = null, mixed $default = null): m...
method cookie (line 177) | public function cookie(?string $name = null, mixed $default = null): m...
method file (line 204) | public function file(?string $name = null): mixed
method method (line 228) | public function method(): string
method protocolVersion (line 241) | public function protocolVersion(): string
method host (line 255) | public function host(bool $withoutPort = false): ?string
method uri (line 269) | public function uri(): string
method path (line 282) | public function path(): string
method queryString (line 295) | public function queryString(): string
method parseUriComponents (line 308) | protected function parseUriComponents(): void
method session (line 322) | public function session(): Session
method sessionId (line 334) | public function sessionId(?string $sessionId = null): string
method isValidSessionId (line 366) | public function isValidSessionId(mixed $sessionId): bool
method sessionRegenerateId (line 378) | public function sessionRegenerateId(bool $deleteOldSession = false): s...
method rawHead (line 401) | public function rawHead(): string
method rawBody (line 411) | public function rawBody(): string
method rawBuffer (line 421) | public function rawBuffer(): string
method parseHeadFirstLine (line 431) | protected function parseHeadFirstLine(): void
method parseProtocolVersion (line 444) | protected function parseProtocolVersion(): void
method parseHeaders (line 457) | protected function parseHeaders(): void
method parseGet (line 501) | protected function parseGet(): void
method parsePost (line 528) | protected function parsePost(): void
method parseUploadFiles (line 566) | protected function parseUploadFiles(string $httpPostBoundary): void
method parseUploadFile (line 601) | protected function parseUploadFile(string $boundary, int $sectionStart...
method createSessionId (line 683) | public static function createSessionId(): string
method setSidCookie (line 698) | protected function setSidCookie(string $sessionName, string $sid, arra...
method __toString (line 715) | public function __toString(): string
method __set (line 727) | public function __set(string $name, mixed $value): void
method __get (line 738) | public function __get(string $name): mixed
method __isset (line 749) | public function __isset(string $name): bool
method __unset (line 760) | public function __unset(string $name): void
method __unserialize (line 771) | public function __unserialize(array $data): void
method destroy (line 781) | public function destroy(): void
method __destruct (line 797) | public function __destruct()
FILE: src/Protocols/Http/Response.php
class Response (line 34) | class Response implements Stringable
method __construct (line 267) | public function __construct(
method header (line 280) | public function header(string $name, string $value): static
method withHeader (line 293) | public function withHeader(string $name, string $value): static
method withHeaders (line 304) | public function withHeaders(array $headers): static
method withoutHeader (line 316) | public function withoutHeader(string $name): static
method getHeader (line 328) | public function getHeader(string $name): array|string|null
method getHeaders (line 338) | public function getHeaders(): array
method withStatus (line 350) | public function withStatus(int $code, ?string $reasonPhrase = null): s...
method getStatusCode (line 362) | public function getStatusCode(): int
method getReasonPhrase (line 372) | public function getReasonPhrase(): ?string
method withProtocolVersion (line 383) | public function withProtocolVersion(string $version): static
method withBody (line 395) | public function withBody(string $body): static
method rawBody (line 406) | public function rawBody(): string
method withFile (line 419) | public function withFile(string $file, int $offset = 0, int $length = ...
method cookie (line 441) | public function cookie(string $name, string $value = '', ?int $maxAge ...
method createHeadForFile (line 459) | protected function createHeadForFile(array $fileInfo): string
method __toString (line 526) | public function __toString(): string
FILE: src/Protocols/Http/ServerSentEvents.php
class ServerSentEvents (line 27) | class ServerSentEvents implements Stringable
method __construct (line 33) | public function __construct(protected array $data) {}
method __toString (line 35) | public function __toString(): string
FILE: src/Protocols/Http/Session.php
class Session (line 35) | class Session
method __construct (line 167) | public function __construct(string $sessionId)
method getId (line 186) | public function getId(): string
method get (line 198) | public function get(string $name, mixed $default = null): mixed
method set (line 209) | public function set(string $name, mixed $value): void
method delete (line 220) | public function delete(string $name): void
method pull (line 233) | public function pull(string $name, mixed $default = null): mixed
method put (line 246) | public function put(array|string $key, mixed $value = null): void
method forget (line 264) | public function forget(array|string $name): void
method all (line 281) | public function all(): array
method flush (line 291) | public function flush(): void
method has (line 303) | public function has(string $name): bool
method exists (line 314) | public function exists(string $name): bool
method save (line 324) | public function save(): void
method refresh (line 343) | public function refresh(): bool
method init (line 353) | public static function init(): void
method handlerClass (line 378) | public static function handlerClass(mixed $className = null, mixed $co...
method getCookieParams (line 394) | public static function getCookieParams(): array
method initHandler (line 411) | protected static function initHandler(): void
method safeDeserialize (line 426) | protected function safeDeserialize(string $data): array
method gc (line 441) | public function gc(): void
method __unserialize (line 452) | public function __unserialize(array $data): void
method __destruct (line 463) | public function __destruct()
FILE: src/Protocols/Http/Session/FileSessionHandler.php
class FileSessionHandler (line 41) | class FileSessionHandler implements SessionHandlerInterface
method init (line 60) | public static function init()
method __construct (line 73) | public function __construct(array $config = [])
method open (line 83) | public function open(string $savePath, string $name): bool
method read (line 91) | public function read(string $sessionId): string|false
method write (line 110) | public function write(string $sessionId, string $sessionData): bool
method updateTimestamp (line 130) | public function updateTimestamp(string $sessionId, string $data = ""):...
method close (line 146) | public function close(): bool
method destroy (line 154) | public function destroy(string $sessionId): bool
method gc (line 166) | public function gc(int $maxLifetime): bool
method sessionFile (line 183) | protected static function sessionFile(string $sessionId): string
method sessionSavePath (line 194) | public static function sessionSavePath(string $path): string
FILE: src/Protocols/Http/Session/RedisClusterSessionHandler.php
class RedisClusterSessionHandler (line 24) | class RedisClusterSessionHandler extends RedisSessionHandler
method __construct (line 31) | public function __construct(array $config)
method createRedisConnection (line 42) | protected function createRedisConnection(array $config): Redis|RedisCl...
FILE: src/Protocols/Http/Session/RedisSessionHandler.php
class RedisSessionHandler (line 36) | class RedisSessionHandler implements SessionHandlerInterface
method __construct (line 66) | public function __construct(array $config)
method connection (line 81) | protected function connection(): Redis|RedisCluster
method createRedisConnection (line 138) | protected function createRedisConnection(array $config): Redis|RedisCl...
method open (line 160) | public function open(string $savePath, string $name): bool
method read (line 172) | public function read(string $sessionId): string|false
method write (line 181) | public function write(string $sessionId, string $sessionData): bool
method updateTimestamp (line 190) | public function updateTimestamp(string $sessionId, string $data = ""):...
method destroy (line 199) | public function destroy(string $sessionId): bool
method close (line 208) | public function close(): bool
method gc (line 216) | public function gc(int $maxLifetime): bool
FILE: src/Protocols/Http/Session/SessionHandlerInterface.php
type SessionHandlerInterface (line 19) | interface SessionHandlerInterface
method close (line 30) | public function close(): bool;
method destroy (line 42) | public function destroy(string $sessionId): bool;
method gc (line 57) | public function gc(int $maxLifetime): bool;
method open (line 70) | public function open(string $savePath, string $name): bool;
method read (line 84) | public function read(string $sessionId): string|false;
method write (line 103) | public function write(string $sessionId, string $sessionData): bool;
method updateTimestamp (line 115) | public function updateTimestamp(string $sessionId, string $data = ""):...
FILE: src/Protocols/ProtocolInterface.php
type ProtocolInterface (line 24) | interface ProtocolInterface
method input (line 36) | public static function input(string $buffer, ConnectionInterface $conn...
method decode (line 45) | public static function decode(string $buffer, ConnectionInterface $con...
method encode (line 54) | public static function encode(mixed $data, ConnectionInterface $connec...
FILE: src/Protocols/Text.php
class Text (line 27) | class Text
method input (line 36) | public static function input(string $buffer, ConnectionInterface $conn...
method encode (line 59) | public static function encode(string $buffer): string
method decode (line 71) | public static function decode(string $buffer): string
FILE: src/Protocols/Websocket.php
class Websocket (line 48) | class Websocket
method input (line 85) | public static function input(string $buffer, TcpConnection $connection...
method encode (line 265) | public static function encode(mixed $buffer, TcpConnection $connection...
method decode (line 331) | public static function decode(string $buffer, TcpConnection $connectio...
method inflate (line 379) | protected static function inflate(TcpConnection $connection, string $b...
method deflate (line 412) | protected static function deflate(TcpConnection $connection, string $b...
method dealHandshake (line 435) | public static function dealHandshake(string $buffer, TcpConnection $co...
FILE: src/Protocols/Ws.php
class Ws (line 43) | class Ws
method input (line 66) | public static function input(string $buffer, AsyncTcpConnection $conne...
method encode (line 237) | public static function encode(string $payload, AsyncTcpConnection $con...
method decode (line 294) | public static function decode(string $bytes, AsyncTcpConnection $conne...
method onConnect (line 320) | public static function onConnect(AsyncTcpConnection $connection): void
method onClose (line 332) | public static function onClose(AsyncTcpConnection $connection): void
method sendHandshake (line 351) | public static function sendHandshake(AsyncTcpConnection $connection): ...
method dealHandshake (line 404) | public static function dealHandshake(string $buffer, AsyncTcpConnectio...
method parseResponse (line 462) | protected static function parseResponse(string $buffer): Response
FILE: src/Timer.php
class Timer (line 36) | class Timer
method init (line 82) | public static function init(?EventInterface $event = null): void
method repeat (line 101) | public static function repeat(float $timeInterval, callable $func, arr...
method delay (line 114) | public static function delay(float $timeInterval, callable $func, arra...
method signalHandle (line 124) | public static function signalHandle(): void
method add (line 141) | public static function add(float $timeInterval, callable $func, ?array...
method sleep (line 182) | public static function sleep(float $delay): void
method tick (line 206) | protected static function tick(): void
method del (line 244) | public static function del(int $timerId): bool
method delAll (line 265) | public static function delAll(): void
FILE: src/Worker.php
class Worker (line 56) | #[AllowDynamicProperties]
method __construct (line 561) | public function __construct(?string $socketName = null, array $socketC...
method runAll (line 588) | public static function runAll(): void
method checkSapiEnv (line 616) | protected static function checkSapiEnv(): void
method initStdOut (line 675) | protected static function initStdOut(): void
method hasColorSupport (line 692) | private static function hasColorSupport(): bool
method init (line 718) | protected static function init(): void
method initGlobalEvent (line 790) | protected static function initGlobalEvent(): void
method lock (line 817) | protected static function lock(int $flag = LOCK_EX): void
method initWorkers (line 843) | protected static function initWorkers(): void
method getAllWorkers (line 894) | public static function getAllWorkers(): array
method getEventLoop (line 904) | public static function getEventLoop(): EventInterface
method getMainSocket (line 914) | public function getMainSocket(): mixed
method initId (line 924) | protected static function initId(): void
method getCurrentUser (line 941) | protected static function getCurrentUser(): string
method displayUI (line 952) | protected static function displayUI(): void
method getVersionLine (line 1016) | protected static function getVersionLine(): string
method getUiColumns (line 1034) | public static function getUiColumns(): array
method getSingleLineTotalLength (line 1052) | public static function getSingleLineTotalLength(): int
method parseCommand (line 1073) | protected static function parseCommand(): void
method getArgv (line 1227) | public static function getArgv(): array
method formatProcessStatusData (line 1238) | protected static function formatProcessStatusData(): string
method formatConnectionStatusData (line 1323) | protected static function formatConnectionStatusData(): string
method installSignal (line 1333) | protected static function installSignal(): void
method reinstallSignal (line 1351) | protected static function reinstallSignal(): void
method signalHandler (line 1368) | protected static function signalHandler(int $signal): void
method getSignalName (line 1411) | protected static function getSignalName(int $signal): string
method daemonize (line 1430) | protected static function daemonize(): void
method resetStd (line 1459) | public static function resetStd(): void
method saveMasterPid (line 1498) | protected static function saveMasterPid(): void
method getAllWorkerPids (line 1515) | protected static function getAllWorkerPids(): array
method forkWorkers (line 1531) | protected static function forkWorkers(): void
method forkWorkersForLinux (line 1545) | protected static function forkWorkersForLinux(): void
method forkWorkersForWindows (line 1564) | protected static function forkWorkersForWindows(): void
method getStartFilesForWindows (line 1644) | public static function getStartFilesForWindows(): array
method forkOneWorkerForWindows (line 1660) | public static function forkOneWorkerForWindows(string $startFile): void
method checkWorkerStatusForWindows (line 1684) | protected static function checkWorkerStatusForWindows(): void
method forkOneWorkerForLinux (line 1703) | protected static function forkOneWorkerForLinux(self $worker): void
method getId (line 1777) | protected static function getId(string $workerId, int $pid): false|int...
method setUserAndGroup (line 1787) | public function setUserAndGroup(): void
method setProcessTitle (line 1822) | protected static function setProcessTitle(string $title): void
method monitorWorkers (line 1835) | protected static function monitorWorkers(): void
method monitorWorkersForLinux (line 1849) | protected static function monitorWorkersForLinux(): void
method monitorWorkersForWindows (line 1924) | protected static function monitorWorkersForWindows(): void
method exitAndClearAll (line 1934) | protected static function exitAndClearAll(): void
method reload (line 1962) | protected static function reload(): void
method stopAll (line 2040) | public static function stopAll(int $code = 0, mixed $log = ''): void
method checkIfChildRunning (line 2104) | protected static function checkIfChildRunning(): void
method getStatus (line 2120) | public static function getStatus(): int
method getGracefulStop (line 2130) | public static function getGracefulStop(): bool
method writeStatisticsToStatusFile (line 2141) | protected static function writeStatisticsToStatusFile(): void
method getUiColumnLength (line 2221) | protected static function getUiColumnLength($name): int
method writeConnectionsStatisticsToStatusFile (line 2231) | protected static function writeConnectionsStatisticsToStatusFile(): void
method checkErrors (line 2308) | protected static function checkErrors(): void
method getErrorType (line 2331) | protected static function getErrorType(int $type): string
method log (line 2343) | public static function log(Stringable|string $msg, bool $decorated = f...
method safeEcho (line 2410) | public static function safeEcho(string $msg, bool $decorated = false):...
method listen (line 2440) | public function listen(bool $autoAccept = true): void
method unlisten (line 2505) | public function unlisten(): void
method checkPortAvailable (line 2521) | protected static function checkPortAvailable(): void
method parseSocketAddress (line 2551) | protected function parseSocketAddress(): ?string
method pauseAccept (line 2588) | public function pauseAccept(): void
method resumeAccept (line 2601) | public function resumeAccept(): void
method getSocketName (line 2619) | public function getSocketName(): string
method run (line 2630) | public function run(): void
method stop (line 2669) | public function stop(bool $force = true): void
method acceptTcpConnection (line 2703) | protected function acceptTcpConnection(mixed $socket): void
method acceptUdpConnection (line 2743) | protected function acceptUdpConnection(mixed $socket): void
method checkMasterIsAlive (line 2797) | protected static function checkMasterIsAlive(int $masterPid): bool
method isRunning (line 2827) | public static function isRunning(): bool
FILE: tests/Pest.php
function something (line 41) | function something()
function testWithConnectionClose (line 46) | function testWithConnectionClose(Closure $closure, ?string $dataContains...
function testWithConnectionEnd (line 59) | function testWithConnectionEnd(Closure $closure, ?string $dataContains =...
function getNonFrameOutput (line 72) | function getNonFrameOutput(string $output): string
FILE: tests/TestCase.php
class TestCase (line 7) | abstract class TestCase extends BaseTestCase
FILE: tests/Unit/Protocols/Http/RequestSessionTest.php
function withSessionRequest (line 11) | function withSessionRequest(Closure $callback, bool $customCookieParams ...
Condensed preview — 60 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (420K chars).
[
{
"path": ".gitattributes",
"chars": 204,
"preview": "/.gitattributes export-ignore\n/.github/ export-ignore\n/.gitignore export-ignore\n/phpunit.xml.dist "
},
{
"path": ".github/FUNDING.yml",
"chars": 90,
"preview": "# These are supported funding model platforms\n\nopen_collective: workerman\npatreon: walkor\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 447,
"preview": "---\nname: Bug Report\nabout: 报告一个 bug\ntitle: \"[BUG]\"\nlabels: bug\n---\n\nPlease answer these questions before submitting you"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 23,
"preview": "default: bug_report.md\n"
},
{
"path": ".github/workflows/test.yml",
"chars": 1307,
"preview": "name: tests\r\n\r\non:\r\n push:\r\n branches:\r\n - master\r\n - feature/tests\r\n - feature/feature-tests\r\n pull"
},
{
"path": ".gitignore",
"chars": 126,
"preview": "logs\n.buildpath\n.project\n.settings\n.idea\n.DS_Store\nvendor/\n/.vscode\ncomposer.lock\nphpunit.xml\n/phpstan.neon\n/*.pid\n/*.pi"
},
{
"path": "MIT-LICENSE.txt",
"chars": 1166,
"preview": "The MIT License\n\nCopyright (c) 2009-2025 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/wo"
},
{
"path": "README.md",
"chars": 13950,
"preview": "# Workerman\n[](https://gitter.im/walkor/Workerman?utm_source=bad"
},
{
"path": "SECURITY.md",
"chars": 95,
"preview": "# Security Policy\n\n\n## Reporting a Vulnerability\n\nPlease contact by email walkor@workerman.net\n"
},
{
"path": "composer.json",
"chars": 1745,
"preview": "{\n \"name\": \"workerman/workerman\",\n \"type\": \"library\",\n \"keywords\": [\n \"event-loop\",\n \"asynchronou"
},
{
"path": "phpstan.neon.dist",
"chars": 1193,
"preview": "parameters:\n\tlevel: 5\n\tpaths:\n\t\t- src\n\t\t- tests\n\texcludePaths:\n\t - src/Events/Swow.php\n\tignoreErrors:\n\t - path: sr"
},
{
"path": "phpunit.xml.dist",
"chars": 515,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n xsi:noNam"
},
{
"path": "src/Connection/AsyncTcpConnection.php",
"chars": 14924,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Connection/AsyncUdpConnection.php",
"chars": 6236,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Connection/ConnectionInterface.php",
"chars": 3684,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Connection/TcpConnection.php",
"chars": 34466,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Connection/UdpConnection.php",
"chars": 5413,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Ev.php",
"chars": 5269,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Event.php",
"chars": 6875,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/EventInterface.php",
"chars": 3243,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Fiber.php",
"chars": 6358,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Select.php",
"chars": 12761,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Swoole.php",
"chars": 7256,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Events/Swow.php",
"chars": 8488,
"preview": "<?php\r\n\r\ndeclare(strict_types=1);\r\n\r\nnamespace Workerman\\Events;\r\n\r\nuse RuntimeException;\r\nuse Workerman\\Coroutine\\Corou"
},
{
"path": "src/Protocols/Frame.php",
"chars": 1411,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Chunk.php",
"chars": 848,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Request.php",
"chars": 22243,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Response.php",
"chars": 17791,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/ServerSentEvents.php",
"chars": 1512,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Session/FileSessionHandler.php",
"chars": 5178,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Session/RedisClusterSessionHandler.php",
"chars": 1668,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Session/RedisSessionHandler.php",
"chars": 6201,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Session/SessionHandlerInterface.php",
"chars": 3861,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http/Session.php",
"chars": 10400,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Http.php",
"chars": 10822,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/ProtocolInterface.php",
"chars": 1638,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Text.php",
"chars": 1867,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Websocket.php",
"chars": 19913,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Protocols/Ws.php",
"chars": 18984,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Timer.php",
"chars": 7119,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "src/Worker.php",
"chars": 91554,
"preview": "<?php\n/**\n * This file is part of workerman.\n *\n * Licensed under The MIT License\n * For full copyright and license info"
},
{
"path": "tests/Feature/ExampleTest.php",
"chars": 71,
"preview": "<?php\n\ntest('example', function () {\n expect(true)->toBeTrue();\n});\n"
},
{
"path": "tests/Feature/HttpConnectionTest.php",
"chars": 2937,
"preview": "<?php\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\Cookie\\CookieJar;\nuse GuzzleHttp\\Psr7\\Utils;\nuse Symfony\\Component\\Process\\"
},
{
"path": "tests/Feature/Stub/HttpServer.php",
"chars": 2239,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Protocols\\Http\\Request;\nuse Worke"
},
{
"path": "tests/Feature/Stub/UdpServer.php",
"chars": 638,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Workerman\\Worker;\n\nrequire './vendor/autoload.php';\n\nif(!defined('STDIN')) define('"
},
{
"path": "tests/Feature/Stub/WebsocketClient.php",
"chars": 730,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Workerman\\Connection\\AsyncTcpConnection;\nuse Workerman\\Worker;\n\nrequire_once __DIR_"
},
{
"path": "tests/Feature/Stub/WebsocketServer.php",
"chars": 660,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Protocols\\Http\\Request;\nuse Worke"
},
{
"path": "tests/Feature/UdpConnectionTest.php",
"chars": 1082,
"preview": "<?php\r\n\r\nuse Symfony\\Component\\Process\\PhpProcess;\r\n\r\n$process = null;\r\nbeforeAll(function () use (&$process) {\r\n $pr"
},
{
"path": "tests/Feature/WebsocketServiceTest.php",
"chars": 4305,
"preview": "<?php\n\nuse Symfony\\Component\\Process\\PhpProcess;\n\n$serverCode = file_get_contents(__DIR__ . '/Stub/WebsocketServer.php')"
},
{
"path": "tests/Pest.php",
"chars": 2675,
"preview": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Test Case\n|---------------------"
},
{
"path": "tests/TestCase.php",
"chars": 130,
"preview": "<?php\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{"
},
{
"path": "tests/Unit/Connection/TcpConnectionEndOnMessageTest.php",
"chars": 1405,
"preview": "<?php\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Events\\Select;\nuse Workerman\\Protocols\\Text;\nuse Workerman\\"
},
{
"path": "tests/Unit/Connection/TcpConnectionEndTest.php",
"chars": 1079,
"preview": "<?php\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Events\\Select;\nuse Workerman\\Timer;\n\nit('closes connection "
},
{
"path": "tests/Unit/Connection/UdpConnectionTest.php",
"chars": 1729,
"preview": "<?php\n\nuse Workerman\\Connection\\UdpConnection;\nuse Symfony\\Component\\Process\\PhpProcess;\nuse Workerman\\Protocols\\Text;\n\n"
},
{
"path": "tests/Unit/Protocols/FrameTest.php",
"chars": 454,
"preview": "<?php\r\n\r\nuse Workerman\\Protocols\\Frame;\r\n\r\nit('tests ::input', function () {\r\n expect(Frame::input('foo'))->toBe(0)\r\n"
},
{
"path": "tests/Unit/Protocols/Http/RequestSessionTest.php",
"chars": 9842,
"preview": "<?php\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Protocols\\Http\\Request;\nuse Workerman\\Protocols\\Http\\Sessio"
},
{
"path": "tests/Unit/Protocols/Http/ResponseTest.php",
"chars": 2028,
"preview": "<?php\r\n\r\nuse Workerman\\Protocols\\Http\\Response;\r\n\r\nit('test some simple case', function () {\r\n $response = new Respon"
},
{
"path": "tests/Unit/Protocols/Http/ServerSentEventsTest.php",
"chars": 451,
"preview": "<?php\r\n\r\nuse Workerman\\Protocols\\Http\\ServerSentEvents;\r\n\r\nit('tests ' . ServerSentEvents::class, function () {\r\n $da"
},
{
"path": "tests/Unit/Protocols/HttpTest.php",
"chars": 8294,
"preview": "<?php\n\nuse Workerman\\Connection\\TcpConnection;\nuse Workerman\\Protocols\\Http;\nuse Workerman\\Protocols\\Http\\Request;\nuse W"
},
{
"path": "tests/Unit/Protocols/TextTest.php",
"chars": 791,
"preview": "<?php\n\nuse Workerman\\Connection\\ConnectionInterface;\nuse Workerman\\Protocols\\Text;\n\ntest(Text::class, function () {\n "
}
]
About this extraction
This page contains the full source code of the walkor/workerman GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 60 files (391.0 KB), approximately 96.9k tokens, and a symbol index with 442 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.