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 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 [![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) [![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman) [![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman) [![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman) [![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman) [![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman) ## What is it Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. 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 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 [ '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 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 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 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 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 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 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 [![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport) ## Other links with workerman [webman](https://github.com/walkor/webman) [AdapterMan](https://github.com/joanhey/AdapterMan) ## Donate PayPal ## 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": " ./tests ./src ================================================ FILE: src/Connection/AsyncTcpConnection.php ================================================ * @copyright walkor * @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 */ 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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @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 */ private array $readEvents = []; /** * All listeners for write event. * * @var array */ private array $writeEvents = []; /** * Event listeners of signal. * * @var array */ private array $eventSignal = []; /** * All timer event listeners. * * @var array */ 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 ================================================ * @copyright walkor * @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 */ private array $readEvents = []; /** * All listeners for write event. * * @var array */ private array $writeEvents = []; /** * Event listeners of signal. * * @var array */ private array $eventSignal = []; /** * All timer event listeners. * * @var array */ 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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 */ private array $readEvents = []; /** * All listeners for write event. * * @var array */ private array $writeEvents = []; /** * Event listeners of signal. * * @var array */ private array $eventSignal = []; /** * Event listeners of timer. * * @var array */ 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 ================================================ * @copyright walkor * @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 */ private array $readEvents = []; /** * All listeners for read/write event. * * @var array */ private array $writeEvents = []; /** * @var array */ private array $exceptEvents = []; /** * Event listeners of signal. * * @var array */ private array $signalEvents = []; /** * Fds waiting for read event. * * @var array */ private array $readFds = []; /** * Fds waiting for write event. * * @var array */ private array $writeFds = []; /** * Fds waiting for except event. * * @var array */ 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 ================================================ * @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 */ private array $eventTimer = []; /** * All listeners for read event. * * @var array */ private array $readEvents = []; /** * All listeners for write event. * * @var 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 ================================================ */ private array $eventTimer = []; /** * All listeners for read event. * * @var array */ private array $readEvents = []; /** * All listeners for write event. * * @var array */ private array $writeEvents = []; /** * All listeners for signal. * * @var array */ 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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 * * @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('

404 Not Found

'); } $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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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 ================================================ * @copyright walkor * @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

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

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

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

* @since 5.4.0 */ public function destroy(string $sessionId): bool; /** * Cleanup old sessions * @link http://php.net/manual/en/sessionhandlerinterface.gc.php * @param int $maxLifetime

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

* @return bool

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

* @since 5.4.0 */ public function gc(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

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

* @since 5.4.0 */ public function open(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

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

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

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

* @return bool

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

* @since 5.4.0 */ public function write(string $sessionId, string $sessionData): bool; /** * Update session modify time. * * @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php * * @param string $sessionId * @param string $data Session Data. * * @return bool */ public function updateTimestamp(string $sessionId, string $data = ""): bool; } ================================================ FILE: src/Protocols/Http/Session.php ================================================ * @copyright walkor * @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 Throwable; use Workerman\Protocols\Http\Session\FileSessionHandler; use Workerman\Protocols\Http\Session\SessionHandlerInterface; use function array_key_exists; use function ini_get; use function is_array; use function is_scalar; use function random_int; use function session_get_cookie_params; /** * Class Session * @package Workerman\Protocols\Http */ class Session { /** * Session andler class which implements SessionHandlerInterface. * * @var string */ protected static string $handlerClass = FileSessionHandler::class; /** * Parameters of __constructor for session handler class. * * @var mixed */ protected static mixed $handlerConfig = null; /** * Session name. * * @var string */ public static string $name = 'PHPSID'; /** * Auto update timestamp. * * @var bool */ public static bool $autoUpdateTimestamp = false; /** * Session lifetime. * * @var int */ public static int $lifetime = 1440; /** * Cookie lifetime. * * @var int */ public static int $cookieLifetime = 1440; /** * Session cookie path. * * @var string */ public static string $cookiePath = '/'; /** * Session cookie domain. * * @var string */ public static string $domain = ''; /** * HTTPS only cookies. * * @var bool */ public static bool $secure = false; /** * HTTP access only. * * @var bool */ public static bool $httpOnly = true; /** * Same-site cookies. * * @var string */ public static string $sameSite = ''; /** * Gc probability. * * @var int[] */ public static array $gcProbability = [1, 20000]; /** * Session handler instance. * * @var ?SessionHandlerInterface */ protected static ?SessionHandlerInterface $handler = null; /** * Session data. * * @var array */ protected mixed $data = []; /** * Session changed and need to save. * * @var bool */ protected bool $needSave = false; /** * Session id. * * @var string */ protected string $sessionId; /** * Is safe. * * @var bool */ protected bool $isSafe = true; /** * Session serialize_handler * @var array|string[] */ protected array $serializer = ['serialize', 'unserialize']; /** * Session constructor. * * @param string $sessionId */ public function __construct(string $sessionId) { if (extension_loaded('igbinary') && ini_get('session.serialize_handler') == 'igbinary') { $this->serializer = ['igbinary_serialize', 'igbinary_unserialize']; } if (static::$handler === null) { static::initHandler(); } $this->sessionId = $sessionId; if ($data = static::$handler->read($sessionId)) { $this->data = $this->safeDeserialize($data); } } /** * Get session id. * * @return string */ public function getId(): string { return $this->sessionId; } /** * Get session. * * @param string $name * @param mixed $default * @return mixed */ public function get(string $name, mixed $default = null): mixed { return $this->data[$name] ?? $default; } /** * Store data in the session. * * @param string $name * @param mixed $value */ public function set(string $name, mixed $value): void { $this->data[$name] = $value; $this->needSave = true; } /** * Delete an item from the session. * * @param string $name */ public function delete(string $name): void { unset($this->data[$name]); $this->needSave = true; } /** * Retrieve and delete an item from the session. * * @param string $name * @param mixed $default * @return mixed */ public function pull(string $name, mixed $default = null): mixed { $value = $this->get($name, $default); $this->delete($name); return $value; } /** * Store data in the session. * * @param array|string $key * @param mixed $value */ public function put(array|string $key, mixed $value = null): void { if (!is_array($key)) { $this->set($key, $value); return; } foreach ($key as $k => $v) { $this->data[$k] = $v; } $this->needSave = true; } /** * Remove a piece of data from the session. * * @param array|string $name */ public function forget(array|string $name): void { if (is_scalar($name)) { $this->delete($name); return; } foreach ($name as $key) { unset($this->data[$key]); } $this->needSave = true; } /** * Retrieve all the data in the session. * * @return array */ public function all(): array { return $this->data; } /** * Remove all data from the session. * * @return void */ public function flush(): void { $this->needSave = true; $this->data = []; } /** * Determining If An Item Exists In The Session. * * @param string $name * @return bool */ public function has(string $name): bool { return isset($this->data[$name]); } /** * To determine if an item is present in the session, even if its value is null. * * @param string $name * @return bool */ public function exists(string $name): bool { return array_key_exists($name, $this->data); } /** * Save session to store. * * @return void */ public function save(): void { if ($this->needSave) { if (empty($this->data)) { static::$handler->destroy($this->sessionId); } else { static::$handler->write($this->sessionId, $this->serializer[0]($this->data)); } } elseif (static::$autoUpdateTimestamp) { $this->refresh(); } $this->needSave = false; } /** * Refresh session expire time. * * @return bool */ public function refresh(): bool { return static::$handler->updateTimestamp($this->getId()); } /** * Init. * * @return void */ public static function init(): void { if (($gcProbability = (int)ini_get('session.gc_probability')) && ($gcDivisor = (int)ini_get('session.gc_divisor'))) { static::$gcProbability = [$gcProbability, $gcDivisor]; } if ($gcMaxLifeTime = ini_get('session.gc_maxlifetime')) { self::$lifetime = (int)$gcMaxLifeTime; } $sessionCookieParams = session_get_cookie_params(); static::$cookieLifetime = $sessionCookieParams['lifetime']; static::$cookiePath = $sessionCookieParams['path']; static::$domain = $sessionCookieParams['domain']; static::$secure = $sessionCookieParams['secure']; static::$httpOnly = $sessionCookieParams['httponly']; } /** * Set session handler class. * * @param mixed $className * @param mixed $config * @return string */ public static function handlerClass(mixed $className = null, mixed $config = null): string { if ($className) { static::$handlerClass = $className; } if ($config) { static::$handlerConfig = $config; } return static::$handlerClass; } /** * Get cookie params. * * @return array */ public static function getCookieParams(): array { return [ 'lifetime' => static::$cookieLifetime, 'path' => static::$cookiePath, 'domain' => static::$domain, 'secure' => static::$secure, 'httponly' => static::$httpOnly, 'samesite' => static::$sameSite, ]; } /** * Init handler. * * @return void */ protected static function initHandler(): void { if (static::$handlerConfig === null) { static::$handler = new static::$handlerClass(); } else { static::$handler = new static::$handlerClass(static::$handlerConfig); } } /** * Safely deserialize session data, preventing object instantiation. * * @param string $data * @return array */ protected function safeDeserialize(string $data): array { if ($this->serializer[1] === 'unserialize') { $result = unserialize($data, ['allowed_classes' => false]); } else { $result = ($this->serializer[1])($data); } return is_array($result) ? $result : []; } /** * GC sessions. * * @return void */ public function gc(): void { static::$handler->gc(static::$lifetime); } /** * __unserialize. * * @param array $data * @return void */ public function __unserialize(array $data): void { $this->isSafe = false; } /** * __destruct. * * @return void * @throws Throwable */ public function __destruct() { if (!$this->isSafe) { return; } $this->save(); if (random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) { $this->gc(); } } } // Init session. Session::init(); ================================================ FILE: src/Protocols/Http.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman\Protocols; use Workerman\Connection\TcpConnection; use Workerman\Protocols\Http\Request; use Workerman\Protocols\Http\Response; use function clearstatcache; use function filesize; use function fopen; use function fread; use function fseek; use function ftell; use function ini_get; use function is_array; use function is_object; use function preg_match; use function str_starts_with; use function strlen; use function strpos; use function substr; use function sys_get_temp_dir; /** * Class Http. * @package Workerman\Protocols */ class Http { /** * Request class name. * * @var string */ protected static string $requestClass = Request::class; /** * Upload tmp dir. * * @var string */ protected static string $uploadTmpDir = ''; /** * Get or set the request class name. * * @param class-string|null $className * @return string */ public static function requestClass(?string $className = null): string { if ($className !== null) { static::$requestClass = $className; } return static::$requestClass; } /** * Check the integrity of the package. * * @param string $buffer * @param TcpConnection $connection * @return int */ public static function input(string $buffer, TcpConnection $connection): int { $crlfPos = strpos($buffer, "\r\n\r\n"); if (false === $crlfPos) { // Judge whether the package length exceeds the limit. if (strlen($buffer) >= 16384) { $connection->end("HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", true); } return 0; } $length = $crlfPos + 4; // Only slice when necessary (avoid extra string copy). // Keep the trailing "\r\n\r\n" in $header for simpler/faster validation patterns. $header = isset($buffer[$length]) ? substr($buffer, 0, $length) : $buffer; // Validate request line: METHOD SP request-target SP HTTP/1.0|1.1 CRLF // Request-target validation: // - Allow origin-form only (must start with "/") for all methods below. // - Do NOT support asterisk-form ("*") for OPTIONS. // - For compatibility, allow any bytes except ASCII control characters, spaces and DEL in request-target. // (Strictly speaking, URI should be ASCII and non-ASCII should be percent-encoded; but many clients send UTF-8.) // - Disallow "Transfer-Encoding" header (case-insensitive; line-start must be "\r\n" to avoid matching "x-Transfer-Encoding"). // - Optionally capture Content-Length (case-insensitive; line-start must be "\r\n" to avoid matching "x-Content-Length"). // - If Content-Length exists, it must be a valid decimal number and the whole field-value must be digits + optional OWS. // - Disallow duplicate Content-Length headers. // Note: All lookaheads are placed at \A so they can scan the entire header including the request line. // Use [ \t]* instead of \s* to avoid matching across lines. // The pattern uses case-insensitive modifier (~i) for header name matching. $headerValidatePattern = '~\A' // Optional: capture Content-Length value (must be at \A to scan entire header) . '(?:(?=[\s\S]*\r\nContent-Length[ \t]*:[ \t]*(\d+)[ \t]*\r\n))?' // Disallow Transfer-Encoding header . '(?![\s\S]*\r\nTransfer-Encoding[ \t]*:)' // If Content-Length header exists, its value must be pure digits + optional OWS . '(?![\s\S]*\r\nContent-Length[ \t]*:(?![ \t]*\d+[ \t]*\r\n)[^\r]*\r\n)' // Disallow duplicate Content-Length headers (adjacent or separated) . '(?![\s\S]*\r\nContent-Length[ \t]*:[^\r\n]*\r\n(?:[\s\S]*?\r\n)?Content-Length[ \t]*:)' // Match request line: METHOD SP request-target SP HTTP-version CRLF . '(?:GET|POST|OPTIONS|HEAD|DELETE|PUT|PATCH) +\/[^\x00-\x20\x7f]* +HTTP\/1\.[01]\r\n~i'; if (!preg_match($headerValidatePattern, $header, $matches)) { $connection->end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", true); return 0; } if (isset($matches[1])) { $length += (int)$matches[1]; } if ($length > $connection->maxPackageSize) { $connection->end("HTTP/1.1 413 Payload Too Large\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", true); return 0; } return $length; } /** * Http decode. * * @param string $buffer * @param TcpConnection $connection * @return mixed */ public static function decode(string $buffer, TcpConnection $connection): mixed { $request = new static::$requestClass($buffer); $request->connection = $connection; return $request; } /** * Http encode. * * @param string|Response $response * @param TcpConnection $connection * @return string */ public static function encode(mixed $response, TcpConnection $connection): string { if (!is_object($response)) { $extHeader = ''; $contentType = 'text/html;charset=utf-8'; foreach ($connection->headers as $name => $value) { if ($name === 'Content-Type') { $contentType = $value; continue; } if (is_array($value)) { foreach ($value as $item) { $extHeader .= "$name: $item\r\n"; } } else { $extHeader .= "$name: $value\r\n"; } } $connection->headers = []; $response = (string)$response; $bodyLen = strlen($response); return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$extHeader}Connection: keep-alive\r\nContent-Type: $contentType\r\nContent-Length: $bodyLen\r\n\r\n$response"; } if ($connection->headers) { $response->withHeaders($connection->headers); $connection->headers = []; } if (isset($response->file)) { $file = $response->file['file']; $offset = $response->file['offset'] ?: 0; $length = $response->file['length'] ?: 0; clearstatcache(); $fileSize = (int)filesize($file); $bodyLen = $length > 0 ? $length : $fileSize - $offset; $response->withHeaders([ 'Content-Length' => $bodyLen, 'Accept-Ranges' => 'bytes', ]); if ($offset || $length) { $offsetEnd = $offset + $bodyLen - 1; $response->header('Content-Range', "bytes $offset-$offsetEnd/$fileSize"); $response->withStatus(206); } if ($bodyLen < 2 * 1024 * 1024) { $connection->send($response . file_get_contents($file, false, null, $offset, $bodyLen), true); return ''; } $handler = fopen($file, 'r'); if (false === $handler) { $connection->close(new Response(403, [], '403 Forbidden')); return ''; } $connection->send((string)$response, true); static::sendStream($connection, $handler, $offset, $length); return ''; } return (string)$response; } /** * Send remainder of a stream to client. * * @param TcpConnection $connection * @param resource $handler * @param int $offset * @param int $length */ protected static function sendStream(TcpConnection $connection, $handler, int $offset = 0, int $length = 0): void { $connection->context->bufferFull = false; $connection->context->streamSending = true; if ($offset !== 0) { fseek($handler, $offset); } $offsetEnd = $offset + $length; // Read file content from disk piece by piece and send to client. $doWrite = function () use ($connection, $handler, $length, $offsetEnd) { // Send buffer not full. /** @phpstan-ignore-next-line */ while ($connection->context->bufferFull === false) { // Read from disk. $size = 1024 * 1024; if ($length !== 0) { $tell = ftell($handler); $remainSize = $offsetEnd - $tell; if ($remainSize <= 0) { fclose($handler); $connection->onBufferDrain = null; return; } $size = min($remainSize, $size); } $buffer = fread($handler, $size); // Read eof. if ($buffer === '' || $buffer === false) { fclose($handler); $connection->onBufferDrain = null; $connection->context->streamSending = false; return; } $connection->send($buffer, true); } }; // Send buffer full. $connection->onBufferFull = function ($connection) { $connection->context->bufferFull = true; }; // Send buffer drain. $connection->onBufferDrain = function ($connection) use ($doWrite) { $connection->context->bufferFull = false; $doWrite(); }; $doWrite(); } /** * Set or get uploadTmpDir. * * @param string|null $dir * @return string */ public static function uploadTmpDir(string|null $dir = null): string { if (null !== $dir) { static::$uploadTmpDir = $dir; } if (static::$uploadTmpDir === '') { if ($uploadTmpDir = ini_get('upload_tmp_dir')) { static::$uploadTmpDir = $uploadTmpDir; } else if ($uploadTmpDir = sys_get_temp_dir()) { static::$uploadTmpDir = $uploadTmpDir; } } return static::$uploadTmpDir; } } ================================================ FILE: src/Protocols/ProtocolInterface.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman\Protocols; use Workerman\Connection\ConnectionInterface; /** * Protocol interface */ interface ProtocolInterface { /** * Check the integrity of the package. * Please return the length of package. * If length is unknown please return 0 that means waiting for more data. * If the package has something wrong please return -1 the connection will be closed. * * @param string $buffer * @param ConnectionInterface $connection * @return int */ public static function input(string $buffer, ConnectionInterface $connection): int; /** * Decode package and emit onMessage($message) callback, $message is the result that decode returned. * * @param string $buffer * @param ConnectionInterface $connection * @return mixed */ public static function decode(string $buffer, ConnectionInterface $connection): mixed; /** * Encode package before sending to client. * * @param mixed $data * @param ConnectionInterface $connection * @return string */ public static function encode(mixed $data, ConnectionInterface $connection): string; } ================================================ FILE: src/Protocols/Text.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman\Protocols; use Workerman\Connection\ConnectionInterface; use function rtrim; use function strlen; use function strpos; /** * Text Protocol. */ class Text { /** * Check the integrity of the package. * * @param string $buffer * @param ConnectionInterface $connection * @return int */ public static function input(string $buffer, ConnectionInterface $connection): int { // Judge whether the package length exceeds the limit. if (isset($connection->maxPackageSize) && strlen($buffer) >= $connection->maxPackageSize) { $connection->close(); return 0; } // Find the position of "\n". $pos = strpos($buffer, "\n"); // No "\n", packet length is unknown, continue to wait for the data so return 0. if ($pos === false) { return 0; } // Return the current package length. return $pos + 1; } /** * Encode. * * @param string $buffer * @return string */ public static function encode(string $buffer): string { // Add "\n" return $buffer . "\n"; } /** * Decode. * * @param string $buffer * @return string */ public static function decode(string $buffer): string { // Remove "\n" return rtrim($buffer, "\r\n"); } } ================================================ FILE: src/Protocols/Websocket.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman\Protocols; use Throwable; use Workerman\Connection\ConnectionInterface; use Workerman\Connection\TcpConnection; use Workerman\Protocols\Http\Request; use Workerman\Worker; use function base64_encode; use function chr; use function deflate_add; use function deflate_init; use function floor; use function inflate_add; use function inflate_init; use function is_scalar; use function ord; use function pack; use function preg_match; use function sha1; use function str_repeat; use function stripos; use function strlen; use function strpos; use function substr; use function unpack; use const ZLIB_DEFAULT_STRATEGY; use const ZLIB_ENCODING_RAW; /** * WebSocket protocol. */ class Websocket { /** * Websocket blob type. * * @var string */ public const BINARY_TYPE_BLOB = "\x81"; /** * Websocket blob type. * * @var string */ const BINARY_TYPE_BLOB_DEFLATE = "\xc1"; /** * Websocket arraybuffer type. * * @var string */ public const BINARY_TYPE_ARRAYBUFFER = "\x82"; /** * Websocket arraybuffer type. * * @var string */ const BINARY_TYPE_ARRAYBUFFER_DEFLATE = "\xc2"; /** * Check the integrity of the package. * * @param string $buffer * @param TcpConnection $connection * @return int */ public static function input(string $buffer, TcpConnection $connection): int { $connection->websocketOrigin = $connection->websocketOrigin ?? null; $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; // Receive length. $recvLen = strlen($buffer); // We need more data. if ($recvLen < 6) { return 0; } // Has not yet completed the handshake. if (empty($connection->context->websocketHandshake)) { return static::dealHandshake($buffer, $connection); } // Buffer websocket frame data. if ($connection->context->websocketCurrentFrameLength) { // We need more frame data. if ($connection->context->websocketCurrentFrameLength > $recvLen) { // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. return 0; } } else { $firstByte = ord($buffer[0]); $secondByte = ord($buffer[1]); $dataLen = $secondByte & 127; $isFinFrame = $firstByte >> 7; $masked = $secondByte >> 7; if (!$masked) { Worker::safeEcho("frame not masked so close the connection\n"); $connection->close(); return 0; } $opcode = $firstByte & 0xf; switch ($opcode) { case 0x0: // Blob type. case 0x1: // Arraybuffer type. case 0x2: // Ping package. case 0x9: // Pong package. case 0xa: break; // Close package. case 0x8: // Try to emit onWebSocketClose callback. $closeCb = $connection->onWebSocketClose ?? $connection->worker->onWebSocketClose ?? false; if ($closeCb) { try { $closeCb($connection); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // Close connection. else { $connection->close("\x88\x02\x03\xe8", true); } return 0; // Wrong opcode. default : Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n"); $connection->close(); return 0; } // Calculate packet length. $headLen = 6; if ($dataLen === 126) { $headLen = 8; if ($headLen > $recvLen) { return 0; } $pack = unpack('nn/ntotal_len', $buffer); $dataLen = $pack['total_len']; } else { if ($dataLen === 127) { $headLen = 14; if ($headLen > $recvLen) { return 0; } $arr = unpack('n/N2c', $buffer); $dataLen = $arr['c1'] * 4294967296 + $arr['c2']; } } $currentFrameLength = $headLen + $dataLen; $totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength; if ($totalPackageSize > $connection->maxPackageSize) { Worker::safeEcho("error package. package_length=$totalPackageSize\n"); $connection->close(); return 0; } if ($isFinFrame) { if ($opcode === 0x9) { if ($recvLen >= $currentFrameLength) { $pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); $connection->consumeRecvBuffer($currentFrameLength); $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; $connection->websocketType = "\x8a"; $pingCb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false; if ($pingCb) { try { $pingCb($connection, $pingData); } catch (Throwable $e) { Worker::stopAll(250, $e); } } else { $connection->send($pingData); } $connection->websocketType = $tmpConnectionType; if ($recvLen > $currentFrameLength) { return static::input(substr($buffer, $currentFrameLength), $connection); } } return 0; } if ($opcode === 0xa) { if ($recvLen >= $currentFrameLength) { $pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); $connection->consumeRecvBuffer($currentFrameLength); $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; $connection->websocketType = "\x8a"; // Try to emit onWebSocketPong callback. $pongCb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false; if ($pongCb) { try { $pongCb($connection, $pongData); } catch (Throwable $e) { Worker::stopAll(250, $e); } } $connection->websocketType = $tmpConnectionType; if ($recvLen > $currentFrameLength) { return static::input(substr($buffer, $currentFrameLength), $connection); } } return 0; } return $currentFrameLength; } $connection->context->websocketCurrentFrameLength = $currentFrameLength; } // Received just a frame length data. if ($connection->context->websocketCurrentFrameLength === $recvLen) { static::decode($buffer, $connection); $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); $connection->context->websocketCurrentFrameLength = 0; return 0; } // The length of the received data is greater than the length of a frame. if ($connection->context->websocketCurrentFrameLength < $recvLen) { static::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); $currentFrameLength = $connection->context->websocketCurrentFrameLength; $connection->context->websocketCurrentFrameLength = 0; // Continue to read next frame. return static::input(substr($buffer, $currentFrameLength), $connection); } // The length of the received data is less than the length of a frame. return 0; } /** * Websocket encode. * * @param mixed $buffer * @param TcpConnection $connection * @return string */ public static function encode(mixed $buffer, TcpConnection $connection): string { if (!is_scalar($buffer)) { $buffer = json_encode($buffer, JSON_UNESCAPED_UNICODE); } if (empty($connection->websocketType)) { $connection->websocketType = static::BINARY_TYPE_BLOB; } if (ord($connection->websocketType) & 64) { $buffer = static::deflate($connection, $buffer); } $firstByte = $connection->websocketType; $len = strlen($buffer); if ($len <= 125) { $encodeBuffer = $firstByte . chr($len) . $buffer; } else { if ($len <= 65535) { $encodeBuffer = $firstByte . chr(126) . pack("n", $len) . $buffer; } else { $encodeBuffer = $firstByte . chr(127) . pack("xxxxN", $len) . $buffer; } } // Handshake not completed so temporary buffer websocket data waiting for send. if (empty($connection->context->websocketHandshake)) { if (empty($connection->context->tmpWebsocketData)) { $connection->context->tmpWebsocketData = ''; } // If buffer has already full then discard the current package. if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { if ($connection->onError) { try { ($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package'); } catch (Throwable $e) { Worker::stopAll(250, $e); } } return ''; } $connection->context->tmpWebsocketData .= $encodeBuffer; // Check buffer is full. if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) { try { ($connection->onBufferFull)($connection); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // Return empty string. return ''; } return $encodeBuffer; } /** * Websocket decode. * * @param string $buffer * @param TcpConnection $connection * @return string */ public static function decode(string $buffer, TcpConnection $connection): string { $firstByte = ord($buffer[0]); $secondByte = ord($buffer[1]); $len = $secondByte & 127; $isFinFrame = (bool)($firstByte >> 7); $rsv1 = 64 === ($firstByte & 64); if ($len === 126) { $masks = substr($buffer, 4, 4); $data = substr($buffer, 8); } else { if ($len === 127) { $masks = substr($buffer, 10, 4); $data = substr($buffer, 14); } else { $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } } $dataLength = strlen($data); $masks = str_repeat($masks, (int)floor($dataLength / 4)) . substr($masks, 0, $dataLength % 4); $decoded = $data ^ $masks; if ($connection->context->websocketCurrentFrameLength) { $connection->context->websocketDataBuffer .= $decoded; if ($rsv1) { return static::inflate($connection, $connection->context->websocketDataBuffer, $isFinFrame); } return $connection->context->websocketDataBuffer; } if ($connection->context->websocketDataBuffer !== '') { $decoded = $connection->context->websocketDataBuffer . $decoded; $connection->context->websocketDataBuffer = ''; } if ($rsv1) { return static::inflate($connection, $decoded, $isFinFrame); } return $decoded; } /** * Inflate. * * @param TcpConnection $connection * @param string $buffer * @param bool $isFinFrame * @return false|string */ protected static function inflate(TcpConnection $connection, string $buffer, bool $isFinFrame): bool|string { if (!isset($connection->context->inflator)) { $connection->context->inflator = inflate_init( ZLIB_ENCODING_RAW, [ 'level' => -1, 'memory' => 8, 'window' => 15, 'strategy' => ZLIB_DEFAULT_STRATEGY ] ); } if ($isFinFrame) { $buffer .= "\x00\x00\xff\xff"; } $result = inflate_add($connection->context->inflator, $buffer); // Guard against decompression bomb: check inflated size against maxPackageSize. if ($result !== false && strlen($result) > $connection->maxPackageSize) { Worker::safeEcho("WebSocket inflate data exceeds maxPackageSize limit\n"); $connection->close(); return false; } return $result; } /** * Deflate. * * @param TcpConnection $connection * @param string $buffer * @return false|string */ protected static function deflate(TcpConnection $connection, string $buffer): bool|string { if (!isset($connection->context->deflator)) { $connection->context->deflator = deflate_init( ZLIB_ENCODING_RAW, [ 'level' => -1, 'memory' => 8, 'window' => 15, 'strategy' => ZLIB_DEFAULT_STRATEGY ] ); } return substr(deflate_add($connection->context->deflator, $buffer), 0, -4); } /** * Websocket handshake. * * @param string $buffer * @param TcpConnection $connection * @return int */ public static function dealHandshake(string $buffer, TcpConnection $connection): int { // HTTP protocol. if (str_starts_with($buffer, 'GET')) { // Find \r\n\r\n. $headerEndPos = strpos($buffer, "\r\n\r\n"); if (!$headerEndPos) { return 0; } $headerLength = $headerEndPos + 4; // Get Sec-WebSocket-Key. if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) { $SecWebSocketKey = $match[1]; } else { $connection->close( "HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

WebSocket


workerman
", true); return 0; } // Calculation websocket key. $newKey = base64_encode(sha1($SecWebSocketKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); // Handshake response data. $handshakeMessage = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Sec-WebSocket-Version: 13\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $newKey . "\r\n"; // Websocket data buffer. $connection->context->websocketDataBuffer = ''; // Current websocket frame length. $connection->context->websocketCurrentFrameLength = 0; // Current websocket frame data. $connection->context->websocketCurrentFrameBuffer = ''; // Consume handshake data. $connection->consumeRecvBuffer($headerLength); // Request from buffer $request = new Request($buffer); // Try to emit onWebSocketConnect callback. $onWebsocketConnect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false; if ($onWebsocketConnect) { try { $onWebsocketConnect($connection, $request); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // blob or arraybuffer if (empty($connection->websocketType)) { $connection->websocketType = static::BINARY_TYPE_BLOB; } $hasServerHeader = false; if ($connection->headers) { foreach ($connection->headers as $header) { if (strpbrk($header, "\r\n") !== false) { continue; } if (stripos($header, 'Server:') === 0) { $hasServerHeader = true; } $handshakeMessage .= "$header\r\n"; } } if (!$hasServerHeader) { $handshakeMessage .= "Server: workerman\r\n"; } $handshakeMessage .= "\r\n"; // Send handshake response. $connection->send($handshakeMessage, true); // Mark handshake complete. $connection->context->websocketHandshake = true; // Try to emit onWebSocketConnected callback. $onWebsocketConnected = $connection->onWebSocketConnected ?? $connection->worker->onWebSocketConnected ?? false; if ($onWebsocketConnected) { try { $onWebsocketConnected($connection, $request); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // There are data waiting to be sent. if (!empty($connection->context->tmpWebsocketData)) { $connection->send($connection->context->tmpWebsocketData, true); $connection->context->tmpWebsocketData = ''; } if (strlen($buffer) > $headerLength) { return static::input(substr($buffer, $headerLength), $connection); } return 0; } // Bad websocket handshake request. $connection->close( "HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

400 Bad Request


workerman
", true); return 0; } } ================================================ FILE: src/Protocols/Ws.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman\Protocols; use Throwable; use Workerman\Connection\AsyncTcpConnection; use Workerman\Connection\ConnectionInterface; use Workerman\Protocols\Http\Response; use Workerman\Timer; use Workerman\Worker; use function base64_encode; use function bin2hex; use function explode; use function floor; use function ord; use function pack; use function preg_match; use function sha1; use function str_repeat; use function strlen; use function strpos; use function substr; use function trim; use function unpack; /** * Websocket protocol for client. */ class Ws { /** * Websocket blob type. * * @var string */ public const BINARY_TYPE_BLOB = "\x81"; /** * Websocket arraybuffer type. * * @var string */ public const BINARY_TYPE_ARRAYBUFFER = "\x82"; /** * Check the integrity of the package. * * @param string $buffer * @param AsyncTcpConnection $connection * @return int */ public static function input(string $buffer, AsyncTcpConnection $connection): int { if (empty($connection->context->handshakeStep)) { Worker::safeEcho("recv data before handshake. Buffer:" . bin2hex($buffer) . "\n"); return -1; } // Recv handshake response if ($connection->context->handshakeStep === 1) { return self::dealHandshake($buffer, $connection); } $recvLen = strlen($buffer); if ($recvLen < 2) { return 0; } // Buffer websocket frame data. if ($connection->context->websocketCurrentFrameLength) { // We need more frame data. if ($connection->context->websocketCurrentFrameLength > $recvLen) { // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. return 0; } } else { $firstByte = ord($buffer[0]); $secondByte = ord($buffer[1]); $dataLen = $secondByte & 127; $isFinFrame = $firstByte >> 7; $masked = $secondByte >> 7; if ($masked) { Worker::safeEcho("frame masked so close the connection\n"); $connection->close(); return 0; } $opcode = $firstByte & 0xf; switch ($opcode) { case 0x0: // Blob type. case 0x1: // Arraybuffer type. case 0x2: // Ping package. case 0x9: // Pong package. case 0xa: break; // Close package. case 0x8: // Try to emit onWebSocketClose callback. if (isset($connection->onWebSocketClose)) { try { ($connection->onWebSocketClose)($connection, self::decode($buffer, $connection)); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // Close connection. else { $connection->close(); } return 0; // Wrong opcode. default : Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n"); $connection->close(); return 0; } // Calculate packet length. if ($dataLen === 126) { if (strlen($buffer) < 4) { return 0; } $pack = unpack('nn/ntotal_len', $buffer); $currentFrameLength = $pack['total_len'] + 4; } else if ($dataLen === 127) { if (strlen($buffer) < 10) { return 0; } $arr = unpack('n/N2c', $buffer); $currentFrameLength = $arr['c1'] * 4294967296 + $arr['c2'] + 10; } else { $currentFrameLength = $dataLen + 2; } $totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength; if ($totalPackageSize > $connection->maxPackageSize) { Worker::safeEcho("error package. package_length=$totalPackageSize\n"); $connection->close(); return 0; } if ($isFinFrame) { if ($opcode === 0x9) { if ($recvLen >= $currentFrameLength) { $pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); $connection->consumeRecvBuffer($currentFrameLength); $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; $connection->websocketType = "\x8a"; if (isset($connection->onWebSocketPing)) { try { ($connection->onWebSocketPing)($connection, $pingData); } catch (Throwable $e) { Worker::stopAll(250, $e); } } else { $connection->send($pingData); } $connection->websocketType = $tmpConnectionType; if ($recvLen > $currentFrameLength) { return static::input(substr($buffer, $currentFrameLength), $connection); } } return 0; } if ($opcode === 0xa) { if ($recvLen >= $currentFrameLength) { $pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); $connection->consumeRecvBuffer($currentFrameLength); $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; $connection->websocketType = "\x8a"; // Try to emit onWebSocketPong callback. if (isset($connection->onWebSocketPong)) { try { ($connection->onWebSocketPong)($connection, $pongData); } catch (Throwable $e) { Worker::stopAll(250, $e); } } $connection->websocketType = $tmpConnectionType; if ($recvLen > $currentFrameLength) { return static::input(substr($buffer, $currentFrameLength), $connection); } } return 0; } return $currentFrameLength; } $connection->context->websocketCurrentFrameLength = $currentFrameLength; } // Received just a frame length data. if ($connection->context->websocketCurrentFrameLength === $recvLen) { self::decode($buffer, $connection); $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); $connection->context->websocketCurrentFrameLength = 0; return 0; } // The length of the received data is greater than the length of a frame. elseif ($connection->context->websocketCurrentFrameLength < $recvLen) { self::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); $currentFrameLength = $connection->context->websocketCurrentFrameLength; $connection->context->websocketCurrentFrameLength = 0; // Continue to read next frame. return self::input(substr($buffer, $currentFrameLength), $connection); } // The length of the received data is less than the length of a frame. else { return 0; } } /** * Websocket encode. * * @param string $payload * @param AsyncTcpConnection $connection * @return string * @throws Throwable */ public static function encode(string $payload, AsyncTcpConnection $connection): string { if (empty($connection->websocketType)) { $connection->websocketType = self::BINARY_TYPE_BLOB; } $connection->websocketOrigin = $connection->websocketOrigin ?? null; $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; if (empty($connection->context->handshakeStep)) { static::sendHandshake($connection); } $maskKey = "\x00\x00\x00\x00"; $length = strlen($payload); $head = match(true) { $length < 126 => chr(0x80 | $length), $length < 0xFFFF => chr(0x80 | 126) . pack("n", $length), default => chr(0x80 | 127) . pack("N", 0) . pack("N", $length), }; $frame = $connection->websocketType . $head . $maskKey; // append payload to frame: $maskKey = str_repeat($maskKey, (int)floor($length / 4)) . substr($maskKey, 0, $length % 4); $frame .= $payload ^ $maskKey; if ($connection->context->handshakeStep === 1) { // If buffer has already full then discard the current package. if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { if ($connection->onError) { try { ($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package'); } catch (Throwable $e) { Worker::stopAll(250, $e); } } return ''; } $connection->context->tmpWebsocketData .= $frame; // Check buffer is full. if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) { try { ($connection->onBufferFull)($connection); } catch (Throwable $e) { Worker::stopAll(250, $e); } } return ''; } return $frame; } /** * Websocket decode. * * @param string $bytes * @param AsyncTcpConnection $connection * @return string */ public static function decode(string $bytes, AsyncTcpConnection $connection): string { $decodedData = match(ord($bytes[1])) { // data length 126 => substr($bytes, 4), 127 => substr($bytes, 10), default => substr($bytes, 2), }; if ($connection->context->websocketCurrentFrameLength) { return $connection->context->websocketDataBuffer .= $decodedData; } if ($connection->context->websocketDataBuffer !== '') { $decodedData = $connection->context->websocketDataBuffer . $decodedData; $connection->context->websocketDataBuffer = ''; } return $decodedData; } /** * Send websocket handshake data. * * @param AsyncTcpConnection $connection * @return void * @throws Throwable */ public static function onConnect(AsyncTcpConnection $connection): void { $connection->websocketOrigin = $connection->websocketOrigin ?? null; $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; static::sendHandshake($connection); } /** * Clean * * @param AsyncTcpConnection $connection */ public static function onClose(AsyncTcpConnection $connection): void { $connection->context->handshakeStep = null; $connection->context->websocketCurrentFrameLength = 0; $connection->context->tmpWebsocketData = ''; $connection->context->websocketDataBuffer = ''; if (!empty($connection->context->websocketPingTimer)) { Timer::del($connection->context->websocketPingTimer); $connection->context->websocketPingTimer = null; } } /** * Send websocket handshake. * * @param AsyncTcpConnection $connection * @return void * @throws Throwable */ public static function sendHandshake(AsyncTcpConnection $connection): void { if (!empty($connection->context->handshakeStep)) { return; } // Get Host. $port = $connection->getRemotePort(); $host = $port === 80 || $port === 443 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port; // Handshake header. $connection->context->websocketSecKey = base64_encode(random_bytes(16)); $userHeader = $connection->headers ?? null; $userHeaderStr = ''; if (!empty($userHeader)) { foreach ($userHeader as $k => $v) { // Skip unsafe header names or values containing CR/LF if (strpbrk((string)$k, ":\r\n") !== false) { continue; } if (strpbrk((string)$v, "\r\n") !== false) { continue; } $userHeaderStr .= "$k: $v\r\n"; } $userHeaderStr = $userHeaderStr !== '' ? "\r\n" . trim($userHeaderStr) : ''; } $requestUri = str_replace(["\r", "\n"], '', $connection->getRemoteURI()); // Sanitize Origin and Sec-WebSocket-Protocol $origin = $connection->websocketOrigin ?? null; $origin = $origin !== null ? str_replace(["\r", "\n"], '', $origin) : null; $clientProtocol = $connection->websocketClientProtocol ?? null; $clientProtocol = $clientProtocol !== null ? str_replace(["\r", "\n"], '', $clientProtocol) : null; $header = 'GET ' . $requestUri . " HTTP/1.1\r\n" . (!preg_match("/\nHost:/i", $userHeaderStr) ? "Host: $host\r\n" : '') . "Connection: Upgrade\r\n" . "Upgrade: websocket\r\n" . ($origin ? "Origin: " . $origin . "\r\n" : '') . ($clientProtocol ? "Sec-WebSocket-Protocol: " . $clientProtocol . "\r\n" : '') . "Sec-WebSocket-Version: 13\r\n" . "Sec-WebSocket-Key: " . $connection->context->websocketSecKey . $userHeaderStr . "\r\n\r\n"; $connection->send($header, true); $connection->context->handshakeStep = 1; $connection->context->websocketCurrentFrameLength = 0; $connection->context->websocketDataBuffer = ''; $connection->context->tmpWebsocketData = ''; } /** * Websocket handshake. * * @param string $buffer * @param AsyncTcpConnection $connection * @return bool|int */ public static function dealHandshake(string $buffer, AsyncTcpConnection $connection): bool|int { $pos = strpos($buffer, "\r\n\r\n"); if ($pos) { //checking Sec-WebSocket-Accept if (preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) { if ($match[1] !== base64_encode(sha1($connection->context->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) { Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . substr($buffer, 0, $pos) . "\n"); $connection->close(); return 0; } } else { Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . substr($buffer, 0, $pos) . "\n"); $connection->close(); return 0; } // handshake complete $connection->context->handshakeStep = 2; $handshakeResponseLength = $pos + 4; $buffer = substr($buffer, 0, $handshakeResponseLength); $response = static::parseResponse($buffer); // Try to emit onWebSocketConnect callback. if (isset($connection->onWebSocketConnect)) { try { ($connection->onWebSocketConnect)($connection, $response); } catch (Throwable $e) { Worker::stopAll(250, $e); } } // Headbeat. if (!empty($connection->websocketPingInterval)) { $connection->context->websocketPingTimer = Timer::add($connection->websocketPingInterval, function () use ($connection) { if (false === $connection->send(pack('H*', '898000000000'), true)) { Timer::del($connection->context->websocketPingTimer); $connection->context->websocketPingTimer = null; } }); } $connection->consumeRecvBuffer($handshakeResponseLength); if (!empty($connection->context->tmpWebsocketData)) { $connection->send($connection->context->tmpWebsocketData, true); $connection->context->tmpWebsocketData = ''; } if (strlen($buffer) > $handshakeResponseLength) { return self::input(substr($buffer, $handshakeResponseLength), $connection); } } return 0; } /** * Parse response. * * @param string $buffer * @return Response */ protected static function parseResponse(string $buffer): Response { [$http_header, ] = explode("\r\n\r\n", $buffer, 2); $header_data = explode("\r\n", $http_header); [$protocol, $status, $phrase] = explode(' ', $header_data[0], 3); $protocolVersion = substr($protocol, 5); unset($header_data[0]); $headers = []; foreach ($header_data as $content) { // \r\n\r\n if (empty($content)) { continue; } list($key, $value) = explode(':', $content, 2); $value = trim($value); $headers[$key] = $value; } return (new Response())->withStatus((int)$status, $phrase)->withHeaders($headers)->withProtocolVersion($protocolVersion); } } ================================================ FILE: src/Timer.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman; use RuntimeException; use Throwable; use Workerman\Events\EventInterface; use Workerman\Events\Fiber; use Workerman\Events\Swoole; use Revolt\EventLoop; use Swoole\Coroutine\System; use function function_exists; use function pcntl_alarm; use function pcntl_signal; use function time; use const PHP_INT_MAX; use const SIGALRM; /** * Timer. */ class Timer { /** * Tasks that based on ALARM signal. * [ * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], * .. * ] * * @var array */ protected static array $tasks = []; /** * Event * * @var ?EventInterface */ protected static ?EventInterface $event = null; /** * Timer id * * @var int */ protected static int $timerId = 0; /** * Timer status * [ * timer_id1 => bool, * timer_id2 => bool, * ...................., * ] * * @var array */ protected static array $status = []; /** * Init. * * @param EventInterface|null $event * @return void */ public static function init(?EventInterface $event = null): void { if ($event) { self::$event = $event; return; } if (function_exists('pcntl_signal')) { pcntl_signal(SIGALRM, self::signalHandle(...), false); } } /** * Repeat. * * @param float $timeInterval * @param callable $func * @param array $args * @return int */ public static function repeat(float $timeInterval, callable $func, array $args = []): int { return self::$event->repeat($timeInterval, $func, $args); } /** * Delay. * * @param float $timeInterval * @param callable $func * @param array $args * @return int */ public static function delay(float $timeInterval, callable $func, array $args = []): int { return self::$event->delay($timeInterval, $func, $args); } /** * ALARM signal handler. * * @return void */ public static function signalHandle(): void { if (!self::$event) { pcntl_alarm(1); self::tick(); } } /** * Add a timer. * * @param float $timeInterval * @param callable $func * @param null|array $args * @param bool $persistent * @return int */ public static function add(float $timeInterval, callable $func, ?array $args = [], bool $persistent = true): int { if ($timeInterval < 0) { throw new RuntimeException('$timeInterval can not less than 0'); } if ($args === null) { $args = []; } if (self::$event) { return $persistent ? self::$event->repeat($timeInterval, $func, $args) : self::$event->delay($timeInterval, $func, $args); } // If not workerman runtime just return. if (!Worker::getAllWorkers()) { throw new RuntimeException('Timer can only be used in workerman running environment'); } if (empty(self::$tasks)) { pcntl_alarm(1); } $runTime = (int)floor(time() + $timeInterval); if (!isset(self::$tasks[$runTime])) { self::$tasks[$runTime] = []; } self::$timerId = self::$timerId == PHP_INT_MAX ? 1 : ++self::$timerId; self::$status[self::$timerId] = true; self::$tasks[$runTime][self::$timerId] = [$func, (array)$args, $persistent, $timeInterval]; return self::$timerId; } /** * Coroutine sleep. * * @param float $delay * @return void */ public static function sleep(float $delay): void { switch (Worker::$eventLoopClass) { // Fiber case Fiber::class: $suspension = EventLoop::getSuspension(); static::add($delay, function () use ($suspension) { $suspension->resume(); }, null, false); $suspension->suspend(); return; // Swoole case Swoole::class: System::sleep($delay); return; } usleep((int)($delay * 1000 * 1000)); } /** * Tick. * * @return void */ protected static function tick(): void { if (empty(self::$tasks)) { pcntl_alarm(0); return; } $timeNow = time(); foreach (self::$tasks as $runTime => $taskData) { if ($timeNow >= $runTime) { foreach ($taskData as $index => $oneTask) { $taskFunc = $oneTask[0]; $taskArgs = $oneTask[1]; $persistent = $oneTask[2]; $timeInterval = $oneTask[3]; try { $taskFunc(...$taskArgs); } catch (Throwable $e) { Worker::safeEcho((string)$e); } if ($persistent && !empty(self::$status[$index])) { $newRunTime = (int)floor(time() + $timeInterval); if (!isset(self::$tasks[$newRunTime])) { self::$tasks[$newRunTime] = []; } self::$tasks[$newRunTime][$index] = [$taskFunc, (array)$taskArgs, $persistent, $timeInterval]; } } unset(self::$tasks[$runTime]); } } } /** * Remove a timer. * * @param int $timerId * @return bool */ public static function del(int $timerId): bool { if (self::$event) { return self::$event->offDelay($timerId); } foreach (self::$tasks as $runTime => $taskData) { if (array_key_exists($timerId, $taskData)) { unset(self::$tasks[$runTime][$timerId]); } } if (array_key_exists($timerId, self::$status)) { unset(self::$status[$timerId]); } return true; } /** * Remove all timers. * * @return void */ public static function delAll(): void { self::$tasks = self::$status = []; if (function_exists('pcntl_alarm')) { pcntl_alarm(0); } self::$event?->deleteAllTimer(); } } ================================================ FILE: src/Worker.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ declare(strict_types=1); namespace Workerman; use AllowDynamicProperties; use Exception; use RuntimeException; use stdClass; use Stringable; use Throwable; use Workerman\Connection\ConnectionInterface; use Workerman\Connection\TcpConnection; use Workerman\Connection\UdpConnection; use Workerman\Coroutine; use Workerman\Coroutine\Context; use Workerman\Events\Event; use Workerman\Events\EventInterface; use Workerman\Events\Fiber; use Workerman\Events\Select; use Workerman\Events\Swoole; use Workerman\Events\Swow; use function defined; use function function_exists; use function is_resource; use function method_exists; use function restore_error_handler; use function set_error_handler; use function stream_socket_accept; use function stream_socket_recvfrom; use function substr; use function array_walk; use function get_class; use const DIRECTORY_SEPARATOR; use const PHP_SAPI; use const PHP_VERSION; use const STDOUT; /** * Worker class * A container for listening ports */ #[AllowDynamicProperties] class Worker { /** * Version. * * @var string */ final public const VERSION = '5.1.10'; /** * Status initial. * * @var int */ public const STATUS_INITIAL = 0; /** * Status starting. * * @var int */ public const STATUS_STARTING = 1; /** * Status running. * * @var int */ public const STATUS_RUNNING = 2; /** * Status shutdown. * * @var int */ public const STATUS_SHUTDOWN = 4; /** * Status reloading. * * @var int */ public const STATUS_RELOADING = 8; /** * Default backlog. Backlog is the maximum length of the queue of pending connections. * * @var int */ public const DEFAULT_BACKLOG = 102400; /** * The safe distance for columns adjacent * * @var int */ public const UI_SAFE_LENGTH = 4; /** * Worker id. * * @var int */ public int $id = 0; /** * Name of the worker processes. * * @var string */ public string $name = 'none'; /** * Number of worker processes. * * @var int */ public int $count = 1; /** * Unix user of processes, needs appropriate privileges (usually root). * * @var string */ public string $user = ''; /** * Unix group of processes, needs appropriate privileges (usually root). * * @var string */ public string $group = ''; /** * reloadable. * * @var bool */ public bool $reloadable = true; /** * reuse port. * * @var bool */ public bool $reusePort = false; /** * Emitted when worker processes is starting. * * @var ?callable */ public $onWorkerStart = null; /** * Emitted when a socket connection is successfully established. * * @var ?callable */ public $onConnect = null; /** * Emitted before websocket handshake (Only works when protocol is ws). * * @var ?callable */ public $onWebSocketConnect = null; /** * Emitted after websocket handshake (Only works when protocol is ws). * * @var ?callable */ public $onWebSocketConnected = null; /** * Emitted when data is received. * * @var ?callable */ public $onMessage = null; /** * Emitted when the other end of the socket sends a FIN packet. * * @var ?callable */ public $onClose = null; /** * Emitted when an error occurs with connection. * * @var ?callable */ public $onError = null; /** * Emitted when the send buffer becomes full. * * @var ?callable */ public $onBufferFull = null; /** * Emitted when the send buffer becomes empty. * * @var ?callable */ public $onBufferDrain = null; /** * Emitted when worker processes has stopped. * * @var ?callable */ public $onWorkerStop = null; /** * Emitted when worker processes receives reload signal. * * @var ?callable */ public $onWorkerReload = null; /** * Transport layer protocol. * * @var string */ public string $transport = 'tcp'; /** * Store all connections of clients. * * @internal Framework internal API * * @var TcpConnection[] */ public array $connections = []; /** * Application layer protocol. * * @var ?string */ public ?string $protocol = null; /** * Pause accept new connections or not. * * @var bool */ protected ?bool $pauseAccept = null; /** * Is worker stopping ? * * @var bool */ public bool $stopping = false; /** * EventLoop class. * * @var ?string */ public ?string $eventLoop = null; /** * Daemonize. * * @var bool */ public static bool $daemonize = false; /** * Standard output stream * * @var resource */ public static $outputStream; /** * Stdout file. * * @var string */ public static string $stdoutFile = '/dev/null'; /** * The file to store master process PID. * * @var string */ public static string $pidFile = ''; /** * The file used to store the master process status. * * @var string */ public static string $statusFile = ''; /** * Log file. * * @var string */ public static string $logFile = ''; /** * Log file maximum size in bytes, default 10M. * * @var int */ public static int $logFileMaxSize = 10_485_760; /** * Global event loop. * * @var ?EventInterface */ public static ?EventInterface $globalEvent = null; /** * Emitted when the master process gets a reload signal. * * @var ?callable */ public static $onMasterReload = null; /** * Emitted when the master process terminated. * * @var ?callable */ public static $onMasterStop = null; /** * Emitted when worker processes exited. * * @var ?callable */ public static $onWorkerExit = null; /** * EventLoopClass * * @var ?class-string */ public static ?string $eventLoopClass = null; /** * After sending the stop command to the child process stopTimeout seconds, * if the process is still living then forced to kill. * * @var int */ public static int $stopTimeout = 2; /** * Command * * @var string */ public static string $command = ''; /** * The PID of master process. * * @var int */ protected static int $masterPid = 0; /** * Listening socket. * * @var ?resource */ protected $mainSocket = null; /** * Socket name. The format is like this http://0.0.0.0:80 . * * @var string */ protected string $socketName = ''; /** * Context of socket. * * @var resource */ protected $socketContext = null; /** * @var stdClass */ protected stdClass $context; /** * All worker instances. * * @var Worker[] */ protected static array $workers = []; /** * All worker processes pid. * The format is like this [worker_id=>[pid=>pid, pid=>pid, ..], ..] * * @var array */ protected static array $pidMap = []; /** * All worker processes waiting for restart. * The format is like this [pid=>pid, pid=>pid]. * * @var array */ protected static array $pidsToRestart = []; /** * Mapping from PID to worker process ID. * The format is like this [worker_id=>[0=>$pid, 1=>$pid, ..], ..]. * * @var array */ protected static array $idMap = []; /** * Current status. * * @var int */ protected static int $status = self::STATUS_INITIAL; /** * UI data. * * @var array|int[] */ protected static array $uiLengthData = []; /** * The file to store status info of current worker process. * * @var string */ protected static string $statisticsFile = ''; /** * The file to store status info of connections. * * @var string */ protected static string $connectionsFile = ''; /** * Start file. * * @var string */ protected static string $startFile = ''; /** * Processes for windows. * * @var array */ protected static array $processForWindows = []; /** * Status info of current worker process. * * @var array */ protected static array $globalStatistics = [ 'start_timestamp' => 0, 'worker_exit_info' => [] ]; /** * PHP built-in protocols. * * @var array */ public const BUILD_IN_TRANSPORTS = [ 'tcp' => 'tcp', 'udp' => 'udp', 'unix' => 'unix', 'ssl' => 'tcp' ]; /** * PHP built-in error types. * * @var array */ public const ERROR_TYPE = [ E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING', E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE', E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING', E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING', E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING', E_USER_NOTICE => 'E_USER_NOTICE', E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', E_DEPRECATED => 'E_DEPRECATED', E_USER_DEPRECATED => 'E_USER_DEPRECATED' ]; /** * Graceful stop or not. * * @var bool */ protected static bool $gracefulStop = false; /** * If $outputStream support decorated * * @var bool */ protected static bool $outputDecorated; /** * Worker object's hash id(unique identifier). * * @var ?string */ protected ?string $workerId = null; /** * Constructor. * * @param string|null $socketName * @param array $socketContext */ public function __construct(?string $socketName = null, array $socketContext = []) { // Save all worker instances. $this->workerId = spl_object_hash($this); $this->context = new stdClass(); static::$workers[$this->workerId] = $this; static::$pidMap[$this->workerId] = []; // Context for socket. if ($socketName) { $this->socketName = $socketName; $socketContext['socket']['backlog'] ??= static::DEFAULT_BACKLOG; $this->socketContext = stream_context_create($socketContext); } // Set an empty onMessage callback. $this->onMessage = function () { // Empty. }; } /** * Run all worker instances. * * @return void */ public static function runAll(): void { try { static::checkSapiEnv(); static::initStdOut(); static::init(); static::parseCommand(); static::checkPortAvailable(); static::lock(); static::daemonize(); static::initWorkers(); static::installSignal(); static::saveMasterPid(); static::lock(LOCK_UN); static::displayUI(); static::forkWorkers(); static::resetStd(); static::monitorWorkers(); } catch (Throwable $e) { static::log($e); } } /** * Check sapi. * * @return void */ protected static function checkSapiEnv(): void { // Only for cli and micro. if (!in_array(PHP_SAPI, ['cli', 'micro'])) { exit("Only run in command line mode" . PHP_EOL); } // Check pcntl and posix extension for unix. if (DIRECTORY_SEPARATOR === '/') { foreach (['pcntl', 'posix'] as $name) { if (!extension_loaded($name)) { exit("Please install $name extension" . PHP_EOL); } } } // Check disable functions. $disabledFunctions = explode(',', ini_get('disable_functions')); $disabledFunctions = array_map('trim', $disabledFunctions); $functionsToCheck = [ 'stream_socket_server', 'stream_socket_accept', 'stream_socket_client', 'pcntl_signal_dispatch', 'pcntl_signal', 'pcntl_alarm', 'pcntl_fork', 'pcntl_wait', 'posix_getuid', 'posix_getpwuid', 'posix_kill', 'posix_setsid', 'posix_getpid', 'posix_getpwnam', 'posix_getgrnam', 'posix_getgid', 'posix_setgid', 'posix_initgroups', 'posix_setuid', 'posix_isatty', 'proc_open', 'proc_get_status', 'proc_close', 'shell_exec', 'exec', 'putenv', 'getenv', ]; $disabled = array_intersect($functionsToCheck, $disabledFunctions); if (!empty($disabled)) { $iniFilePath = (string)php_ini_loaded_file(); exit('Notice: '. implode(',', $disabled) . " are disabled by disable_functions. " . PHP_EOL . "Please remove them from disable_functions in $iniFilePath" . PHP_EOL); } } /** * Init stdout. * * @return void */ protected static function initStdOut(): void { $defaultStream = fn () => defined('STDOUT') ? STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w')); static::$outputStream ??= $defaultStream(); //@phpstan-ignore-line if (!is_resource(self::$outputStream) || get_resource_type(self::$outputStream) !== 'stream') { $type = get_debug_type(self::$outputStream); static::$outputStream = $defaultStream(); throw new RuntimeException(sprintf('The $outputStream must to be a stream, %s given', $type)); } static::$outputDecorated ??= self::hasColorSupport(); } /** * Borrowed from the symfony console * @link https://github.com/symfony/console/blob/0d14a9f6d04d4ac38a8cea1171f4554e325dae92/Output/StreamOutput.php#L92 */ private static function hasColorSupport(): bool { // Follow https://no-color.org/ if (getenv('NO_COLOR') !== false) { return false; } if (getenv('TERM_PROGRAM') === 'Hyper') { return true; } if (DIRECTORY_SEPARATOR === '\\') { return (function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(self::$outputStream)) || getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON' || getenv('TERM') === 'xterm'; } return stream_isatty(self::$outputStream); } /** * Init. * * @return void */ protected static function init(): void { set_error_handler(static function (int $code, string $msg, string $file, int $line): bool { static::safeEcho(sprintf("%s \"%s\" in file %s on line %d\n", static::getErrorType($code), $msg, $file, $line)); return true; }); // $_SERVER. $_SERVER['SERVER_SOFTWARE'] = 'Workerman/' . static::VERSION; $_SERVER['SERVER_START_TIME'] = time(); // Start file. $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); static::$startFile = static::$startFile ?: end($backtrace)['file']; $startFilePrefix = basename(static::$startFile); $startFileDir = dirname(static::$startFile); // Compatible with older workerman versions for pid file. if (empty(static::$pidFile)) { $unique_prefix = \str_replace('/', '_', static::$startFile); $file = __DIR__ . "/../../$unique_prefix.pid"; if (is_file($file)) { static::$pidFile = $file; } } // Pid file. static::$pidFile = static::$pidFile ?: sprintf('%s/workerman.%s.pid', $startFileDir, $startFilePrefix); // Status file. static::$statusFile = static::$statusFile ?: sprintf('%s/workerman.%s.status', $startFileDir, $startFilePrefix); static::$statisticsFile = static::$statisticsFile ?: static::$statusFile; static::$connectionsFile = static::$connectionsFile ?: static::$statusFile . '.connection'; // Log file. static::$logFile = static::$logFile ?: sprintf('%s/workerman.log', $startFileDir); if (static::$logFile !== '/dev/null' && !is_file(static::$logFile) && !str_contains(static::$logFile, '://')) { // if /runtime/logs default folder not exists if (!is_dir(dirname(static::$logFile))) { mkdir(dirname(static::$logFile), 0777, true); } touch(static::$logFile); chmod(static::$logFile, 0644); } // State. static::$status = static::STATUS_STARTING; // Init global event. static::initGlobalEvent(); // For statistics. static::$globalStatistics['start_timestamp'] = time(); // Process title. static::setProcessTitle('WorkerMan: master process start_file=' . static::$startFile); // Init data for worker id. static::initId(); // Timer init. Timer::init(); restore_error_handler(); } /** * Init global event. * * @return void */ protected static function initGlobalEvent(): void { if (static::$globalEvent !== null) { static::$eventLoopClass = get_class(static::$globalEvent); static::$globalEvent = null; return; } if (!empty(static::$eventLoopClass)) { if (!is_subclass_of(static::$eventLoopClass, EventInterface::class)) { throw new RuntimeException(sprintf('%s::$eventLoopClass must implement %s', static::class, EventInterface::class)); } return; } static::$eventLoopClass = match (true) { extension_loaded('event') => Event::class, default => Select::class, }; } /** * Lock. * * @param int $flag * @return void */ protected static function lock(int $flag = LOCK_EX): void { static $fd; if (DIRECTORY_SEPARATOR !== '/') { return; } $lockFile = static::$pidFile . '.lock'; $fd = $fd ?: fopen($lockFile, 'a+'); if ($fd) { flock($fd, $flag); if ($flag === LOCK_UN) { fclose($fd); $fd = null; clearstatcache(); if (is_file($lockFile)) { unlink($lockFile); } } } } /** * Init All worker instances. * * @return void */ protected static function initWorkers(): void { if (DIRECTORY_SEPARATOR !== '/') { return; } foreach (static::$workers as $worker) { // Worker name. if (empty($worker->name)) { $worker->name = 'none'; } // Get unix user of the worker process. if (empty($worker->user)) { $worker->user = static::getCurrentUser(); } else { if (posix_getuid() !== 0 && $worker->user !== static::getCurrentUser()) { static::log('Warning: You must have the root privileges to change uid and gid.'); } } // Socket name. $worker->context->statusSocket = $worker->getSocketName(); // Event-loop name. $eventLoopName = $worker->eventLoop ?: static::$eventLoopClass; $worker->context->eventLoopName = strtolower(substr($eventLoopName, strrpos($eventLoopName, '\\') + 1)); // Status name. $worker->context->statusState = ' [OK] '; // Get column mapping for UI foreach (static::getUiColumns() as $columnName => $prop) { !isset($worker->$prop) && !isset($worker->context->$prop) && $worker->context->$prop = 'NNNN'; $propLength = strlen((string)($worker->$prop ?? $worker->context->$prop)); $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; static::$uiLengthData[$key] = max(static::$uiLengthData[$key] ?? 2 * static::UI_SAFE_LENGTH, $propLength); } // Listen. if (!$worker->reusePort) { $worker->listen(false); } } } /** * Get all worker instances. * * @return Worker[] */ public static function getAllWorkers(): array { return static::$workers; } /** * Get global event-loop instance. * * @return EventInterface */ public static function getEventLoop(): EventInterface { return static::$globalEvent; } /** * Get main socket resource * * @return resource */ public function getMainSocket(): mixed { return $this->mainSocket; } /** * Init idMap. * * @return void */ protected static function initId(): void { foreach (static::$workers as $workerId => $worker) { $newIdMap = []; $worker->count = max($worker->count, 1); for ($key = 0; $key < $worker->count; $key++) { $newIdMap[$key] = static::$idMap[$workerId][$key] ?? 0; } static::$idMap[$workerId] = $newIdMap; } } /** * Get unix user of current process. * * @return string */ protected static function getCurrentUser(): string { $userInfo = posix_getpwuid(posix_getuid()); return $userInfo['name'] ?? 'unknown'; } /** * Display staring UI. * * @return void */ protected static function displayUI(): void { $tmpArgv = static::getArgv(); if (in_array('-q', $tmpArgv)) { return; } $lineVersion = static::getVersionLine(); // For windows if (DIRECTORY_SEPARATOR !== '/') { static::safeEcho("---------------------------------------------- WORKERMAN -----------------------------------------------\r\n"); static::safeEcho($lineVersion); static::safeEcho("----------------------------------------------- WORKERS ------------------------------------------------\r\n"); static::safeEcho("worker listen processes status\r\n"); return; } // For unix !defined('LINE_VERSION_LENGTH') && define('LINE_VERSION_LENGTH', strlen($lineVersion)); $totalLength = static::getSingleLineTotalLength(); $lineOne = '' . str_pad(' WORKERMAN ', $totalLength + strlen(''), '-', STR_PAD_BOTH) . '' . PHP_EOL; $lineTwo = str_pad(' WORKERS ', $totalLength + strlen(''), '-', STR_PAD_BOTH) . PHP_EOL; static::safeEcho($lineOne . $lineVersion . $lineTwo); //Show title $title = ''; foreach (static::getUiColumns() as $columnName => $prop) { $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; //just keep compatible with listen name $columnName === 'socket' && $columnName = 'listen'; $title .= "$columnName" . str_pad('', static::getUiColumnLength($key) + static::UI_SAFE_LENGTH - strlen($columnName)); } $title && static::safeEcho($title . PHP_EOL); //Show content foreach (static::$workers as $worker) { $content = ''; foreach (static::getUiColumns() as $columnName => $prop) { $propValue = (string)($worker->$prop ?? $worker->context->$prop); $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; preg_match_all("/(|<\/n>||<\/w>||<\/g>)/i", $propValue, $matches); $placeHolderLength = !empty($matches[0]) ? strlen(implode('', $matches[0])) : 0; $content .= str_pad($propValue, static::getUiColumnLength($key) + static::UI_SAFE_LENGTH + $placeHolderLength); } $content && static::safeEcho($content . PHP_EOL); } //Show last line $lineLast = str_pad('', static::getSingleLineTotalLength(), '-') . PHP_EOL; !empty($content) && static::safeEcho($lineLast); if (static::$daemonize) { static::safeEcho('Input "php ' . basename(static::$startFile) . ' stop" to stop. Start success.' . "\n\n"); } else if (!empty(static::$command)) { static::safeEcho("Start success.\n"); // Workerman used as library } else { static::safeEcho("Press Ctrl+C to stop. Start success.\n"); } } /** * @return string */ protected static function getVersionLine(): string { //Show version $jitStatus = function_exists('opcache_get_status') && (opcache_get_status()['jit']['on'] ?? false) === true ? 'on' : 'off'; $version = str_pad('Workerman/' . static::VERSION, 24); $version .= str_pad('PHP/' . PHP_VERSION . ' (JIT ' . $jitStatus . ')', 30); $version .= php_uname('s') . '/' . php_uname('r') . PHP_EOL; return $version; } /** * Get UI columns to be shown in terminal * * 1. $columnMap: ['ui_column_name' => 'clas_property_name'] * 2. Consider move into configuration in future * * @return array */ public static function getUiColumns(): array { return [ 'event-loop' => 'eventLoopName', 'proto' => 'transport', 'user' => 'user', 'worker' => 'name', 'socket' => 'statusSocket', 'count' => 'count', 'state' => 'statusState', ]; } /** * Get single line total length for ui * * @return int */ public static function getSingleLineTotalLength(): int { $totalLength = 0; foreach (static::getUiColumns() as $columnName => $prop) { $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; $totalLength += static::getUiColumnLength($key) + static::UI_SAFE_LENGTH; } //Keep beauty when show less columns !defined('LINE_VERSION_LENGTH') && define('LINE_VERSION_LENGTH', 0); $totalLength <= LINE_VERSION_LENGTH && $totalLength = LINE_VERSION_LENGTH; return $totalLength; } /** * Parse command. * * @return void */ protected static function parseCommand(): void { if (DIRECTORY_SEPARATOR !== '/') { return; } // Check argv; $startFile = basename(static::$startFile); $usage = "Usage: php yourfile [mode]\nCommands: \nstart\t\tStart worker in DEBUG mode.\n\t\tUse mode -d to start in DAEMON mode.\nstop\t\tStop worker.\n\t\tUse mode -g to stop gracefully.\nrestart\t\tRestart workers.\n\t\tUse mode -d to start in DAEMON mode.\n\t\tUse mode -g to stop gracefully.\nreload\t\tReload codes.\n\t\tUse mode -g to reload gracefully.\nstatus\t\tGet worker status.\n\t\tUse mode -d to show live status.\nconnections\tGet worker connections.\n"; $availableCommands = [ 'start', 'stop', 'restart', 'reload', 'status', 'connections', ]; $availableMode = [ '-d', '-g' ]; $command = $mode = ''; foreach (static::getArgv() as $value) { if (!$command && in_array($value, $availableCommands)) { $command = $value; } if (!$mode && in_array($value, $availableMode)) { $mode = $value; } } if (!$command) { exit($usage); } // Start command. $modeStr = ''; if ($command === 'start') { if ($mode === '-d' || static::$daemonize) { $modeStr = 'in DAEMON mode'; } else { $modeStr = 'in DEBUG mode'; } } static::log("Workerman[$startFile] $command $modeStr"); // Get master process PID. $masterPid = is_file(static::$pidFile) ? (int)file_get_contents(static::$pidFile) : 0; // Master is still alive? if (static::checkMasterIsAlive($masterPid)) { if ($command === 'start') { static::log("Workerman[$startFile] already running"); exit; } } elseif ($command !== 'start' && $command !== 'restart') { static::log("Workerman[$startFile] not run"); exit; } // execute command. switch ($command) { case 'start': if ($mode === '-d') { static::$daemonize = true; } break; case 'status': // Delete status file on shutdown register_shutdown_function(unlink(...), static::$statisticsFile); while (1) { // Master process will send SIGIOT signal to all child processes. posix_kill($masterPid, SIGIOT); // Waiting a moment. sleep(1); // Clear terminal. if ($mode === '-d') { static::safeEcho("\33[H\33[2J\33(B\33[m", true); } // Echo status data. static::safeEcho(static::formatProcessStatusData()); if ($mode !== '-d') { exit(0); } static::safeEcho("\nPress Ctrl+C to quit.\n\n"); } case 'connections': // Delete status file on shutdown register_shutdown_function(unlink(...), static::$connectionsFile); // Master process will send SIGIO signal to all child processes. posix_kill($masterPid, SIGIO); // Waiting a moment. usleep(500000); // Display statistics data from a disk file. static::safeEcho(static::formatConnectionStatusData()); exit(0); case 'restart': case 'stop': if ($mode === '-g') { static::$gracefulStop = true; $sig = SIGQUIT; static::log("Workerman[$startFile] is gracefully stopping ..."); } else { static::$gracefulStop = false; $sig = SIGINT; static::log("Workerman[$startFile] is stopping ..."); } // Send stop signal to master process. $masterPid && posix_kill($masterPid, $sig); // Timeout. $timeout = static::$stopTimeout + 3; $startTime = time(); // Check master process is still alive? while (1) { $masterIsAlive = $masterPid && posix_kill($masterPid, 0); if ($masterIsAlive) { // Timeout? if (!static::getGracefulStop() && time() - $startTime >= $timeout) { static::log("Workerman[$startFile] stop fail"); exit; } // Waiting a moment. usleep(10000); continue; } // Stop success. static::log("Workerman[$startFile] stop success"); if ($command === 'stop') { exit(0); } if ($mode === '-d') { static::$daemonize = true; } break; } break; case 'reload': if ($mode === '-g') { $sig = SIGUSR2; } else { $sig = SIGUSR1; } posix_kill($masterPid, $sig); exit; default : static::safeEcho('Unknown command: ' . $command . "\n"); exit($usage); } } /** * Get argv. * * @return array */ public static function getArgv(): array { global $argv; return static::$command ? [...$argv, ...explode(' ', static::$command)] : $argv; } /** * Format status data. * * @return string */ protected static function formatProcessStatusData(): string { static $totalRequestCache = []; if (!is_readable(static::$statisticsFile)) { return ''; } $info = file(static::$statisticsFile, FILE_IGNORE_NEW_LINES); if (!$info) { return ''; } $statusStr = ''; $currentTotalRequest = []; $workerInfo = []; try { $workerInfo = unserialize($info[0], ['allowed_classes' => false]); } catch (Throwable) { // do nothing } if (!is_array($workerInfo)) { $workerInfo = []; } ksort($workerInfo, SORT_NUMERIC); unset($info[0]); $dataWaitingSort = []; $readProcessStatus = false; $totalRequests = 0; $totalQps = 0; $totalConnections = 0; $totalFails = 0; $totalMemory = 0; $totalTimers = 0; $maxLen1 = max(static::getUiColumnLength('maxSocketNameLength'), 2 * static::UI_SAFE_LENGTH); $maxLen2 = max(static::getUiColumnLength('maxWorkerNameLength'), 2 * static::UI_SAFE_LENGTH); foreach ($info as $value) { if (!$readProcessStatus) { $statusStr .= $value . "\n"; if (preg_match('/^pid.*?memory.*?listening/', $value)) { $readProcessStatus = true; } continue; } if (preg_match('/^[0-9]+/', $value, $pidMath)) { $pid = $pidMath[0]; $dataWaitingSort[$pid] = $value; if (preg_match('/^\S+?\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?/', $value, $match)) { $totalMemory += (float)str_ireplace('M', '', $match[1]); $maxLen1 = max($maxLen1, strlen($match[2])); $maxLen2 = max($maxLen2, strlen($match[3])); $totalConnections += (int)$match[4]; $totalFails += (int)$match[5]; $totalTimers += (int)$match[6]; $currentTotalRequest[$pid] = $match[7]; $totalRequests += (int)$match[7]; } } } foreach ($workerInfo as $pid => $info) { if (!isset($dataWaitingSort[$pid])) { $statusStr .= "$pid\t" . str_pad('N/A', 7) . " " . str_pad($info['listen'], $maxLen1) . " " . str_pad((string)$info['name'], $maxLen2) . " " . str_pad('N/A', 11) . " " . str_pad('N/A', 9) . " " . str_pad('N/A', 7) . " " . str_pad('N/A', 13) . " N/A [busy] \n"; continue; } //$qps = isset($totalRequestCache[$pid]) ? $currentTotalRequest[$pid] if (!isset($totalRequestCache[$pid], $currentTotalRequest[$pid])) { $qps = 0; } else { $qps = $currentTotalRequest[$pid] - $totalRequestCache[$pid]; $totalQps += $qps; } $statusStr .= $dataWaitingSort[$pid] . " " . str_pad((string)$qps, 6) . " [idle]\n"; } $totalRequestCache = $currentTotalRequest; $statusStr .= "---------------------------------------------------PROCESS STATUS--------------------------------------------------------\n"; $statusStr .= "Summary\t" . str_pad($totalMemory . 'M', 7) . " " . str_pad('-', $maxLen1) . " " . str_pad('-', $maxLen2) . " " . str_pad((string)$totalConnections, 11) . " " . str_pad((string)$totalFails, 9) . " " . str_pad((string)$totalTimers, 7) . " " . str_pad((string)$totalRequests, 13) . " " . str_pad((string)$totalQps, 6) . " [Summary] \n"; return $statusStr; } protected static function formatConnectionStatusData(): string { return file_get_contents(static::$connectionsFile); } /** * Install signal handler. * * @return void */ protected static function installSignal(): void { if (DIRECTORY_SEPARATOR !== '/') { return; } $signals = [SIGINT, SIGTERM, SIGHUP, SIGTSTP, SIGQUIT, SIGUSR1, SIGUSR2, SIGIOT, SIGIO]; foreach ($signals as $signal) { pcntl_signal($signal, static::signalHandler(...), false); } // ignore pcntl_signal(SIGPIPE, SIG_IGN, false); } /** * Reinstall signal handler. * * @return void */ protected static function reinstallSignal(): void { if (DIRECTORY_SEPARATOR !== '/') { return; } $signals = [SIGINT, SIGTERM, SIGHUP, SIGTSTP, SIGQUIT, SIGUSR1, SIGUSR2, SIGIOT, SIGIO]; foreach ($signals as $signal) { // Rewrite master process signal. static::$globalEvent->onSignal($signal, static::signalHandler(...)); } } /** * Signal handler. * * @param int $signal */ protected static function signalHandler(int $signal): void { switch ($signal) { // Stop. case SIGINT: case SIGTERM: case SIGHUP: case SIGTSTP: static::$gracefulStop = false; static::stopAll(0, 'received signal ' . static::getSignalName($signal)); break; // Graceful stop. case SIGQUIT: static::$gracefulStop = true; static::stopAll(0, 'received signal ' . static::getSignalName($signal)); break; // Reload. case SIGUSR2: case SIGUSR1: if (static::$status === static::STATUS_RELOADING || static::$status === static::STATUS_SHUTDOWN) { return; } static::$gracefulStop = $signal === SIGUSR2; static::$pidsToRestart = static::getAllWorkerPids(); static::reload(); break; // Show status. case SIGIOT: static::writeStatisticsToStatusFile(); break; // Show connection status. case SIGIO: static::writeConnectionsStatisticsToStatusFile(); break; } } /** * Get signal name. * * @param int $signal * @return string */ protected static function getSignalName(int $signal): string { return match ($signal) { SIGINT => 'SIGINT', SIGTERM => 'SIGTERM', SIGHUP => 'SIGHUP', SIGTSTP => 'SIGTSTP', SIGQUIT => 'SIGQUIT', SIGUSR1 => 'SIGUSR1', SIGUSR2 => 'SIGUSR2', SIGIOT => 'SIGIOT', SIGIO => 'SIGIO', default => $signal, }; } /** * Run as daemon mode. */ protected static function daemonize(): void { if (!static::$daemonize || DIRECTORY_SEPARATOR !== '/') { return; } umask(0); $pid = pcntl_fork(); if (-1 === $pid) { throw new RuntimeException('Fork fail'); } elseif ($pid > 0) { exit(0); } if (-1 === posix_setsid()) { throw new RuntimeException("Setsid fail"); } // Fork again avoid SVR4 system regain the control of terminal. $pid = pcntl_fork(); if (-1 === $pid) { throw new RuntimeException("Fork fail"); } elseif (0 !== $pid) { exit(0); } } /** * Redirect standard output to stdoutFile. * * @return void */ public static function resetStd(): void { if (!static::$daemonize || DIRECTORY_SEPARATOR !== '/') { return; } if (is_resource(STDOUT)) { fclose(STDOUT); } if (is_resource(STDERR)) { fclose(STDERR); } if (is_resource(static::$outputStream)) { fclose(static::$outputStream); } set_error_handler(static fn (): bool => true); $stdOutStream = fopen(static::$stdoutFile, 'a'); restore_error_handler(); if ($stdOutStream === false) { return; } static::$outputStream = $stdOutStream; // Fix standard output cannot redirect of PHP 8.1.8's bug if (function_exists('posix_isatty') && posix_isatty(2)) { ob_start(function (string $string) { file_put_contents(static::$stdoutFile, $string, FILE_APPEND); }, 1); } } /** * Save pid. */ protected static function saveMasterPid(): void { if (DIRECTORY_SEPARATOR !== '/') { return; } static::$masterPid = posix_getpid(); if (false === file_put_contents(static::$pidFile, static::$masterPid)) { throw new RuntimeException('can not save pid to ' . static::$pidFile); } } /** * Get all pids of worker processes. * * @return array */ protected static function getAllWorkerPids(): array { $pidArray = []; foreach (static::$pidMap as $workerPidArray) { foreach ($workerPidArray as $workerPid) { $pidArray[$workerPid] = $workerPid; } } return $pidArray; } /** * Fork some worker processes. * * @return void */ protected static function forkWorkers(): void { if (DIRECTORY_SEPARATOR === '/') { static::forkWorkersForLinux(); } else { static::forkWorkersForWindows(); } } /** * Fork some worker processes. * * @return void */ protected static function forkWorkersForLinux(): void { foreach (static::$workers as $worker) { if (static::$status === static::STATUS_STARTING) { if (empty($worker->name)) { $worker->name = $worker->getSocketName(); } } while (count(static::$pidMap[$worker->workerId]) < $worker->count) { static::forkOneWorkerForLinux($worker); } } } /** * Fork some worker processes. * * @return void */ protected static function forkWorkersForWindows(): void { $files = static::getStartFilesForWindows(); if (count($files) === 1 || in_array('-q', static::getArgv())) { if (count(static::$workers) > 1) { static::safeEcho("@@@ Error: multi workers init in one php file are not support @@@\r\n"); static::safeEcho("@@@ See https://www.workerman.net/doc/workerman/faq/multi-woker-for-windows.html @@@\r\n"); } elseif (count(static::$workers) <= 0) { exit("@@@no worker inited@@@\r\n\r\n"); } reset(static::$workers); /** @var Worker $worker */ $worker = current(static::$workers); Timer::delAll(); //Update process state. static::$status = static::STATUS_RUNNING; // Register shutdown function for checking errors. register_shutdown_function(static::checkErrors(...)); // Create a global event loop. if (static::$globalEvent === null) { static::$eventLoopClass = $worker->eventLoop ?: static::$eventLoopClass; static::$globalEvent = new static::$eventLoopClass(); static::$globalEvent->setErrorHandler(function ($exception) { static::stopAll(250, $exception); }); } // Reinstall signal. static::reinstallSignal(); // Init Timer. Timer::init(static::$globalEvent); restore_error_handler(); // Add an empty timer to prevent the event-loop from exiting. Timer::add(0.8, function (){}); // Compatibility with the bug in Swow where the first request on Windows fails to trigger stream_select. if (extension_loaded('swow')) { Timer::delay(0.1 , function(){ $stream = tmpfile(); static::$globalEvent->onReadable($stream, function($stream) { static::$globalEvent->offReadable($stream); }); }); } // Display UI. static::safeEcho(str_pad($worker->name, 48) . str_pad($worker->getSocketName(), 36) . str_pad('1', 10) . " [ok]\n"); $worker->run(); static::$globalEvent->run(); if (static::$status !== self::STATUS_SHUTDOWN) { $err = new RuntimeException('event-loop exited'); static::log($err); exit(250); } exit(0); } static::$globalEvent = new Select(); static::$globalEvent->setErrorHandler(function ($exception) { static::stopAll(250, $exception); }); Timer::init(static::$globalEvent); foreach ($files as $startFile) { static::forkOneWorkerForWindows($startFile); } } /** * Get start files for windows. * * @return array */ public static function getStartFilesForWindows(): array { $files = []; foreach (static::getArgv() as $file) { if (is_file($file)) { $files[$file] = $file; } } return $files; } /** * Fork one worker process. * * @param string $startFile */ public static function forkOneWorkerForWindows(string $startFile): void { $startFile = realpath($startFile); $descriptorSpec = [STDIN, STDOUT, STDOUT]; $pipes = []; $process = proc_open('"' . PHP_BINARY . '" ' . " \"$startFile\" -q", $descriptorSpec, $pipes, null, null, ['bypass_shell' => true]); if (static::$globalEvent === null) { static::$globalEvent = new Select(); static::$globalEvent->setErrorHandler(function ($exception) { static::stopAll(250, $exception); }); Timer::init(static::$globalEvent); } // 保存子进程句柄 static::$processForWindows[$startFile] = [$process, $startFile]; } /** * check worker status for windows. * * @return void */ protected static function checkWorkerStatusForWindows(): void { foreach (static::$processForWindows as $processData) { $process = $processData[0]; $startFile = $processData[1]; $status = proc_get_status($process); if (!$status['running']) { static::safeEcho("process $startFile terminated and try to restart\n"); proc_close($process); static::forkOneWorkerForWindows($startFile); } } } /** * Fork one worker process. * * @param self $worker */ protected static function forkOneWorkerForLinux(self $worker): void { // Get available worker id. $id = static::getId($worker->workerId, 0); $pid = pcntl_fork(); // For master process. if ($pid > 0) { static::$pidMap[$worker->workerId][$pid] = $pid; static::$idMap[$worker->workerId][$id] = $pid; } // For child processes. elseif (0 === $pid) { srand(); mt_srand(); static::$gracefulStop = false; if (static::$status === static::STATUS_STARTING) { static::resetStd(); } static::$pidsToRestart = static::$pidMap = []; // Remove other listener. foreach (static::$workers as $key => $oneWorker) { if ($oneWorker->workerId !== $worker->workerId) { $oneWorker->unlisten(); unset(static::$workers[$key]); } } Timer::delAll(); //Update process state. static::$status = static::STATUS_RUNNING; // Register shutdown function for checking errors. register_shutdown_function(static::checkErrors(...)); // Create a global event loop. if (static::$globalEvent === null) { static::$eventLoopClass = $worker->eventLoop ?: static::$eventLoopClass; static::$globalEvent = new static::$eventLoopClass(); static::$globalEvent->setErrorHandler(function ($exception) { static::stopAll(250, $exception); }); } // Reinstall signal. static::reinstallSignal(); // Init Timer. Timer::init(static::$globalEvent); restore_error_handler(); static::setProcessTitle('WorkerMan: worker process ' . $worker->name . ' ' . $worker->getSocketName()); $worker->setUserAndGroup(); $worker->id = $id; $worker->run(); // Main loop. static::$globalEvent->run(); if (static::$status !== self::STATUS_SHUTDOWN) { $err = new Exception('event-loop exited'); static::log($err); exit(250); } exit(0); } else { throw new RuntimeException("forkOneWorker fail"); } } /** * Get worker id. * * @param string $workerId * @param int $pid * @return false|int|string */ protected static function getId(string $workerId, int $pid): false|int|string { return array_search($pid, static::$idMap[$workerId]); } /** * Set unix user and group for current process. * * @return void */ public function setUserAndGroup(): void { // Get uid. $userInfo = posix_getpwnam($this->user); if (!$userInfo) { static::log("Warning: User $this->user not exists"); return; } $uid = $userInfo['uid']; // Get gid. if ($this->group) { $groupInfo = posix_getgrnam($this->group); if (!$groupInfo) { static::log("Warning: Group $this->group not exists"); return; } $gid = $groupInfo['gid']; } else { $gid = $userInfo['gid']; } // Set uid and gid. if ($uid !== posix_getuid() || $gid !== posix_getgid()) { if (!posix_setgid($gid) || !posix_initgroups($userInfo['name'], $gid) || !posix_setuid($uid)) { static::log("Warning: change gid or uid fail."); } } } /** * Set process name. * * @param string $title * @return void */ protected static function setProcessTitle(string $title): void { set_error_handler(static fn (): bool => true); cli_set_process_title($title); restore_error_handler(); } /** * Monitor all child processes. * * @return void * @throws Throwable */ protected static function monitorWorkers(): void { if (DIRECTORY_SEPARATOR === '/') { static::monitorWorkersForLinux(); } else { static::monitorWorkersForWindows(); } } /** * Monitor all child processes. * * @return void */ protected static function monitorWorkersForLinux(): void { static::$status = static::STATUS_RUNNING; // @phpstan-ignore-next-line While loop condition is always true. while (1) { // Calls signal handlers for pending signals. pcntl_signal_dispatch(); // Suspends execution of the current process until a child has exited, or until a signal is delivered $status = 0; $pid = pcntl_wait($status, WUNTRACED); // Calls signal handlers for pending signals again. pcntl_signal_dispatch(); // If a child has already exited. if ($pid > 0) { // Find out which worker process exited. foreach (static::$pidMap as $workerId => $workerPidArray) { if (isset($workerPidArray[$pid])) { $worker = static::$workers[$workerId]; // Fix exit with status 2 for php8.2 if ($status === SIGINT && static::$status === static::STATUS_SHUTDOWN) { $status = 0; } // Exit status. if ($status !== 0) { static::log("worker[$worker->name:$pid] exit with status $status"); } // onWorkerExit if (static::$onWorkerExit) { try { (static::$onWorkerExit)($worker, $status, $pid); } catch (Throwable $exception) { static::log("worker[$worker->name] onWorkerExit $exception"); } } // For Statistics. static::$globalStatistics['worker_exit_info'][$workerId][$status] ??= 0; static::$globalStatistics['worker_exit_info'][$workerId][$status]++; // Clear process data. unset(static::$pidMap[$workerId][$pid]); // Mark id is available. $id = static::getId($workerId, $pid); if ($id !== false) { static::$idMap[$workerId][$id] = 0; } break; } } // Is still running state then fork a new worker process. if (static::$status !== static::STATUS_SHUTDOWN) { static::forkWorkers(); // If reloading continue. if (isset(static::$pidsToRestart[$pid])) { unset(static::$pidsToRestart[$pid]); static::reload(); } } } // If shutdown state and all child processes exited, then master process exit. if (static::$status === static::STATUS_SHUTDOWN && empty(static::getAllWorkerPids())) { static::exitAndClearAll(); } } } /** * Monitor all child processes. * * @return void */ protected static function monitorWorkersForWindows(): void { Timer::add(1, static::checkWorkerStatusForWindows(...)); static::$globalEvent->run(); } /** * Exit current process. */ protected static function exitAndClearAll(): void { clearstatcache(); foreach (static::$workers as $worker) { $socketName = $worker->getSocketName(); if ($worker->transport === 'unix' && $socketName) { [, $address] = explode(':', $socketName, 2); $address = substr($address, strpos($address, '/') + 2); if (file_exists($address)) { @unlink($address); } } } if (file_exists(static::$pidFile)) { @unlink(static::$pidFile); } static::log("Workerman[" . basename(static::$startFile) . "] has been stopped"); if (static::$onMasterStop) { (static::$onMasterStop)(); } exit(0); } /** * Execute reload. * * @return void */ protected static function reload(): void { // For master process. if (static::$masterPid === posix_getpid()) { $sig = static::getGracefulStop() ? SIGUSR2 : SIGUSR1; // Set reloading state. if (static::$status === static::STATUS_RUNNING) { static::log("Workerman[" . basename(static::$startFile) . "] reloading"); static::$status = static::STATUS_RELOADING; static::resetStd(); // Try to emit onMasterReload callback. if (static::$onMasterReload) { try { (static::$onMasterReload)(); } catch (Throwable $e) { static::stopAll(250, $e); } static::initId(); } // Send reload signal to all child processes. $reloadablePidArray = []; foreach (static::$pidMap as $workerId => $workerPidArray) { $worker = static::$workers[$workerId]; if ($worker->reloadable) { $reloadablePidArray += $workerPidArray; continue; } // Send reload signal to a worker process which reloadable is false. array_walk($workerPidArray, static fn ($pid) => posix_kill($pid, $sig)); } // Get all pids that are waiting reload. static::$pidsToRestart = array_intersect(static::$pidsToRestart, $reloadablePidArray); } // Reload complete. if (empty(static::$pidsToRestart)) { if (static::$status !== static::STATUS_SHUTDOWN) { static::$status = static::STATUS_RUNNING; } return; } // Continue reload. $oneWorkerPid = current(static::$pidsToRestart); // Send reload signal to a worker process. posix_kill($oneWorkerPid, $sig); // If the process does not exit after stopTimeout seconds try to kill it. if (!static::getGracefulStop()) { Timer::add(static::$stopTimeout, posix_kill(...), [$oneWorkerPid, SIGKILL], false); } } // For child processes. else { reset(static::$workers); $worker = current(static::$workers); // Try to emit onWorkerReload callback. if ($worker->onWorkerReload) { try { ($worker->onWorkerReload)($worker); } catch (Throwable $e) { static::stopAll(250, $e); } } if ($worker->reloadable) { static::stopAll(); } else { static::resetStd(); } } } /** * Stop all. * * @param int $code * @param mixed $log */ public static function stopAll(int $code = 0, mixed $log = ''): void { static::$status = static::STATUS_SHUTDOWN; // For master process. if (DIRECTORY_SEPARATOR === '/' && static::$masterPid === posix_getpid()) { if ($log) { static::log("Workerman[" . basename(static::$startFile) . "] $log"); } static::log("Workerman[" . basename(static::$startFile) . "] stopping" . ($code ? ", code [$code]" : '')); $workerPidArray = static::getAllWorkerPids(); // Send stop signal to all child processes. $sig = static::getGracefulStop() ? SIGQUIT : SIGINT; foreach ($workerPidArray as $workerPid) { // Fix exit with status 2 for php8.2 if ($sig === SIGINT && !static::$daemonize) { Timer::add(1, posix_kill(...), [$workerPid, SIGINT], false); } else { posix_kill($workerPid, $sig); } if (!static::getGracefulStop()) { Timer::add(ceil(static::$stopTimeout), posix_kill(...), [$workerPid, SIGKILL], false); } } Timer::add(1, static::checkIfChildRunning(...)); } // For child processes. else { if ($code && $log) { static::log($log); } // Execute exit. $workers = array_reverse(static::$workers); array_walk($workers, static fn (Worker $worker) => $worker->stop(false)); $callback = function () use ($code, $workers) { $allWorkerConnectionClosed = true; if (!static::getGracefulStop()) { foreach ($workers as $worker) { foreach ($worker->connections as $connection) { // Delay closing, waiting for data to be sent. if (!$connection->getRecvBufferQueueSize() && !isset($connection->context->closeTimer)) { $connection->context->closeTimer = Timer::delay(0.01, static fn () => $connection->close()); } $allWorkerConnectionClosed = false; } } } if ((!static::getGracefulStop() && $allWorkerConnectionClosed) || ConnectionInterface::$statistics['connection_count'] <= 0) { static::$globalEvent?->stop(); try { // Ignore Swoole ExitException: Swoole exit. exit($code); /** @phpstan-ignore-next-line */ } catch (Throwable) { // do nothing } } }; Timer::repeat(0.01, $callback); } } /** * check if child processes is really running */ protected static function checkIfChildRunning(): void { foreach (static::$pidMap as $workerId => $workerPidArray) { foreach ($workerPidArray as $pid => $workerPid) { if (!posix_kill($pid, 0)) { unset(static::$pidMap[$workerId][$pid]); } } } } /** * Get process status. * * @return int */ public static function getStatus(): int { return static::$status; } /** * If stop gracefully. * * @return bool */ public static function getGracefulStop(): bool { return static::$gracefulStop; } /** * * Write statistics data to disk. * * @return void */ protected static function writeStatisticsToStatusFile(): void { // For master process. if (static::$masterPid === posix_getpid()) { $allWorkerInfo = []; foreach (static::$pidMap as $workerId => $pidArray) { $worker = static::$workers[$workerId]; foreach ($pidArray as $pid) { $allWorkerInfo[$pid] = ['name' => $worker->name, 'listen' => $worker->getSocketName()]; } } file_put_contents(static::$statisticsFile, ''); chmod(static::$statisticsFile, 0722); file_put_contents(static::$statisticsFile, serialize($allWorkerInfo) . "\n", FILE_APPEND); $loadavg = function_exists('sys_getloadavg') ? array_map(round(...), sys_getloadavg(), [2, 2, 2]) : ['-', '-', '-']; file_put_contents(static::$statisticsFile, (static::$daemonize ? "Start worker in DAEMON mode." : "Start worker in DEBUG mode.") . "\n", FILE_APPEND); file_put_contents(static::$statisticsFile, "---------------------------------------------------GLOBAL STATUS---------------------------------------------------------\n", FILE_APPEND); file_put_contents(static::$statisticsFile, static::getVersionLine(), FILE_APPEND); file_put_contents(static::$statisticsFile, 'start time:' . date('Y-m-d H:i:s', static::$globalStatistics['start_timestamp']) . ' run ' . floor((time() - static::$globalStatistics['start_timestamp']) / (24 * 60 * 60)) . ' days ' . floor(((time() - static::$globalStatistics['start_timestamp']) % (24 * 60 * 60)) / (60 * 60)) . " hours " . 'load average: ' . implode(", ", $loadavg) . "\n", FILE_APPEND); file_put_contents(static::$statisticsFile, count(static::$pidMap) . ' workers ' . count(static::getAllWorkerPids()) . " processes\n", FILE_APPEND); file_put_contents(static::$statisticsFile, str_pad('name', static::getUiColumnLength('maxWorkerNameLength')) . " event-loop exit_status exit_count\n", FILE_APPEND); foreach (static::$pidMap as $workerId => $workerPidArray) { $worker = static::$workers[$workerId]; if (isset(static::$globalStatistics['worker_exit_info'][$workerId])) { foreach (static::$globalStatistics['worker_exit_info'][$workerId] as $workerExitStatus => $workerExitCount) { file_put_contents(static::$statisticsFile, str_pad($worker->name, static::getUiColumnLength('maxWorkerNameLength')) . " " . str_pad($worker->context->eventLoopName, 14) . " " . str_pad((string)$workerExitStatus, 16) . str_pad((string)$workerExitCount, 16) . "\n", FILE_APPEND); } } else { file_put_contents(static::$statisticsFile, str_pad($worker->name, static::getUiColumnLength('maxWorkerNameLength')) . " " . str_pad($worker->context->eventLoopName, 14) . " " . str_pad('0', 16) . str_pad('0', 16) . "\n", FILE_APPEND); } } file_put_contents(static::$statisticsFile, "---------------------------------------------------PROCESS STATUS--------------------------------------------------------\n", FILE_APPEND); file_put_contents(static::$statisticsFile, "pid\tmemory " . str_pad('listening', static::getUiColumnLength('maxSocketNameLength')) . " " . str_pad('name', static::getUiColumnLength('maxWorkerNameLength')) . " connections " . str_pad('send_fail', 9) . " " . str_pad('timers', 8) . str_pad('total_request', 13) . " qps status\n", FILE_APPEND); foreach (static::getAllWorkerPids() as $workerPid) { posix_kill($workerPid, SIGIOT); } return; } reset(static::$workers); /** @var static $worker */ $worker = current(static::$workers); $workerStatusStr = posix_getpid() . "\t" . str_pad(round(memory_get_usage() / (1024 * 1024), 2) . "M", 7) . " " . str_pad($worker->getSocketName(), static::getUiColumnLength('maxSocketNameLength')) . " " . str_pad(($worker->name === $worker->getSocketName() ? 'none' : $worker->name), static::getUiColumnLength('maxWorkerNameLength')) . " "; $workerStatusStr .= str_pad((string)ConnectionInterface::$statistics['connection_count'], 11) . " " . str_pad((string)ConnectionInterface::$statistics['send_fail'], 9) . " " . str_pad((string)static::$globalEvent->getTimerCount(), 7) . " " . str_pad((string)ConnectionInterface::$statistics['total_request'], 13) . "\n"; file_put_contents(static::$statisticsFile, $workerStatusStr, FILE_APPEND); } /** * Get UI column length * * @param $name * @return int */ protected static function getUiColumnLength($name): int { return static::$uiLengthData[$name] ?? 0; } /** * Write statistics data to disk. * * @return void */ protected static function writeConnectionsStatisticsToStatusFile(): void { // For master process. if (static::$masterPid === posix_getpid()) { file_put_contents(static::$connectionsFile, ''); chmod(static::$connectionsFile, 0722); file_put_contents(static::$connectionsFile, "--------------------------------------------------------------------- WORKERMAN CONNECTION STATUS --------------------------------------------------------------------------------\n", FILE_APPEND); file_put_contents(static::$connectionsFile, "PID Worker CID Trans Protocol ipv4 ipv6 Recv-Q Send-Q Bytes-R Bytes-W Status Local Address Foreign Address\n", FILE_APPEND); foreach (static::getAllWorkerPids() as $workerPid) { posix_kill($workerPid, SIGIO); } return; } // For child processes. $bytesFormat = function ($bytes) { if ($bytes > 1024 * 1024 * 1024 * 1024) { return round($bytes / (1024 * 1024 * 1024 * 1024), 1) . "TB"; } if ($bytes > 1024 * 1024 * 1024) { return round($bytes / (1024 * 1024 * 1024), 1) . "GB"; } if ($bytes > 1024 * 1024) { return round($bytes / (1024 * 1024), 1) . "MB"; } if ($bytes > 1024) { return round($bytes / (1024), 1) . "KB"; } return $bytes . "B"; }; $pid = posix_getpid(); $str = ''; reset(static::$workers); $currentWorker = current(static::$workers); $defaultWorkerName = $currentWorker->name; foreach (TcpConnection::$connections as $connection) { /** @var TcpConnection $connection */ $transport = $connection->transport; $ipv4 = $connection->isIpV4() ? ' 1' : ' 0'; $ipv6 = $connection->isIpV6() ? ' 1' : ' 0'; $recvQ = $bytesFormat($connection->getRecvBufferQueueSize()); $sendQ = $bytesFormat($connection->getSendBufferQueueSize()); $localAddress = trim($connection->getLocalAddress()); $remoteAddress = trim($connection->getRemoteAddress()); $state = $connection->getStatus(false); $bytesRead = $bytesFormat($connection->bytesRead); $bytesWritten = $bytesFormat($connection->bytesWritten); $id = $connection->id; $protocol = $connection->protocol ?: $connection->transport; $pos = strrpos($protocol, '\\'); if ($pos) { $protocol = substr($protocol, $pos + 1); } if (strlen($protocol) > 15) { $protocol = substr($protocol, 0, 13) . '..'; } $workerName = isset($connection->worker) ? $connection->worker->name : $defaultWorkerName; if (strlen($workerName) > 14) { $workerName = substr($workerName, 0, 12) . '..'; } $str .= str_pad((string)$pid, 9) . str_pad($workerName, 16) . str_pad((string)$id, 10) . str_pad($transport, 8) . str_pad($protocol, 16) . str_pad($ipv4, 7) . str_pad($ipv6, 7) . str_pad($recvQ, 13) . str_pad($sendQ, 13) . str_pad($bytesRead, 13) . str_pad($bytesWritten, 13) . ' ' . str_pad($state, 14) . ' ' . str_pad($localAddress, 22) . ' ' . str_pad($remoteAddress, 22) . "\n"; } if ($str) { file_put_contents(static::$connectionsFile, $str, FILE_APPEND); } } /** * Check errors when current process exited. * * @return void */ protected static function checkErrors(): void { if (static::STATUS_SHUTDOWN !== static::$status) { $errorMsg = DIRECTORY_SEPARATOR === '/' ? 'Worker[' . posix_getpid() . '] process terminated' : 'Worker process terminated'; $errors = error_get_last(); if ($errors && ($errors['type'] === E_ERROR || $errors['type'] === E_PARSE || $errors['type'] === E_CORE_ERROR || $errors['type'] === E_COMPILE_ERROR || $errors['type'] === E_RECOVERABLE_ERROR) ) { $errorMsg .= ' with ERROR: ' . static::getErrorType($errors['type']) . " \"{$errors['message']} in {$errors['file']} on line {$errors['line']}\""; } static::log($errorMsg); } } /** * Get error message by error code. * * @param int $type * @return string */ protected static function getErrorType(int $type): string { return self::ERROR_TYPE[$type] ?? ''; } /** * Log. * * @param Stringable|string $msg * @param bool $decorated * @return void */ public static function log(Stringable|string $msg, bool $decorated = false): void { $msg = trim((string)$msg); if (!static::$daemonize) { static::safeEcho("$msg\n", $decorated); } if (isset(static::$logFile)) { $pid = DIRECTORY_SEPARATOR === '/' ? posix_getpid() : 1; file_put_contents(static::$logFile, sprintf("%s pid:%d %s\n", date('Y-m-d H:i:s'), $pid, $msg), FILE_APPEND | LOCK_EX); // Check the file size and truncate if it exceeds max size if (!empty(static::$logFileMaxSize) && ($fileSize = filesize(static::$logFile)) > static::$logFileMaxSize) { // Open files $source = fopen(static::$logFile, 'r'); if (!$source) { return; } else if (!flock($source, LOCK_EX)) { fclose($source); return; } $newFile = static::$logFile . '.tmp'; $destination = fopen($newFile, 'w'); if (!$destination) { flock($source, LOCK_UN); fclose($source); return; } // Move to the halfway point in the source file $halfwayPoint = (int)($fileSize / 2); fseek($source, $halfwayPoint); // Find the next newline character to ensure we don't cut in the middle of a line while (($char = fgetc($source)) !== false) { if ($char === "\n") { break; } } // Copy the second half into the new file while (!feof($source)) { fwrite($destination, fread($source, 8192)); // Read and write 8KB chunks } // Replace the old file with the new truncated file rename($newFile, static::$logFile); // Close both files flock($source, LOCK_UN); fclose($source); fclose($destination); } } } /** * Safe Echo. * * @param string $msg * @param bool $decorated * @return void */ public static function safeEcho(string $msg, bool $decorated = false): void { if ((static::$outputDecorated ?? false) && $decorated) { $line = "\033[1A\n\033[K"; $white = "\033[47;30m"; $green = "\033[32;40m"; $end = "\033[0m"; } else { $line = ''; $white = ''; $green = ''; $end = ''; } $msg = str_replace(['', '', ''], [$line, $white, $green], $msg); $msg = str_replace(['', '', ''], $end, $msg); set_error_handler(static fn (): bool => true); if (!feof(self::$outputStream)) { fwrite(self::$outputStream, $msg); fflush(self::$outputStream); } restore_error_handler(); } /** * Listen. * * @param bool $autoAccept * @return void */ public function listen(bool $autoAccept = true): void { if (!$this->socketName) { return; } if (!$this->mainSocket) { $localSocket = $this->parseSocketAddress(); // Flag. $flags = $this->transport === 'udp' ? STREAM_SERVER_BIND : STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $errNo = 0; $errMsg = ''; // SO_REUSEPORT. if ($this->reusePort && DIRECTORY_SEPARATOR !== '\\') { stream_context_set_option($this->socketContext, 'socket', 'so_reuseport', 1); } // Create an Internet or Unix domain server socket. $this->mainSocket = stream_socket_server($localSocket, $errNo, $errMsg, $flags, $this->socketContext); if (!$this->mainSocket) { throw new RuntimeException($errMsg); } if ($this->transport === 'ssl') { stream_socket_enable_crypto($this->mainSocket, false); } elseif ($this->transport === 'unix') { $socketFile = substr($localSocket, 7); if ($this->user) { chown($socketFile, $this->user); } if ($this->group) { chgrp($socketFile, $this->group); } } // Try to open keepalive for tcp and disable Nagle algorithm. if (function_exists('socket_import_stream') && self::BUILD_IN_TRANSPORTS[$this->transport] === 'tcp') { set_error_handler(static fn (): bool => true); $socket = socket_import_stream($this->mainSocket); 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, TcpConnection::TCP_KEEPALIVE_INTERVAL); socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, TcpConnection::TCP_KEEPALIVE_INTERVAL); socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 1); } restore_error_handler(); } // Non blocking. stream_set_blocking($this->mainSocket, false); } if ($autoAccept) { $this->resumeAccept(); } } /** * Unlisten. * * @return void */ public function unlisten(): void { $this->pauseAccept(); if ($this->mainSocket) { set_error_handler(static fn (): bool => true); fclose($this->mainSocket); restore_error_handler(); $this->mainSocket = null; } } /** * Check port available. * * @return void */ protected static function checkPortAvailable(): void { foreach (static::$workers as $worker) { $socketName = $worker->getSocketName(); if (DIRECTORY_SEPARATOR === '/' // if linux && static::$status === static::STATUS_STARTING // only for starting status && $worker->transport === 'tcp' // if tcp socket && !str_starts_with($socketName, 'unix') // if not unix socket && !str_starts_with($socketName, 'udp')) { // if not udp socket $address = parse_url($socketName); if (isset($address['host']) && isset($address['port'])) { $address = "tcp://{$address['host']}:{$address['port']}"; $server = null; set_error_handler(function ($code, $msg) { throw new RuntimeException($msg); }); $server = stream_socket_server($address, $code, $msg); if ($server) { fclose($server); } restore_error_handler(); } } } } /** * Parse local socket address. */ protected function parseSocketAddress(): ?string { if (!$this->socketName) { return null; } // Get the application layer communication protocol and listening address. [$scheme, $address] = explode(':', $this->socketName, 2); // 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"); } } if (!isset(self::BUILD_IN_TRANSPORTS[$this->transport])) { throw new RuntimeException('Bad worker->transport ' . var_export($this->transport, true)); } } else if ($this->transport === 'tcp') { $this->transport = $scheme; } //local socket return self::BUILD_IN_TRANSPORTS[$this->transport] . ":" . $address; } /** * Pause accept new connections. * * @return void */ public function pauseAccept(): void { if (static::$globalEvent !== null && !$this->pauseAccept && $this->mainSocket !== null) { static::$globalEvent->offReadable($this->mainSocket); $this->pauseAccept = true; } } /** * Resume accept new connections. * * @return void */ public function resumeAccept(): void { // Register a listener to be notified when server socket is ready to read. if (static::$globalEvent !== null && ($this->pauseAccept === null || $this->pauseAccept === true) && $this->mainSocket !== null) { if ($this->transport !== 'udp') { static::$globalEvent->onReadable($this->mainSocket, $this->acceptTcpConnection(...)); } else { static::$globalEvent->onReadable($this->mainSocket, $this->acceptUdpConnection(...)); } $this->pauseAccept = false; } } /** * Get socket name. * * @return string */ public function getSocketName(): string { return $this->socketName ? lcfirst($this->socketName) : 'none'; } /** * Run worker instance. * * @return void * @throws Throwable */ public function run(): void { $this->listen(!$this->onWorkerStart); if (!$this->onWorkerStart) { return; } // Try to emit onWorkerStart callback. $callback = function() { try { ($this->onWorkerStart)($this); } catch (Throwable $e) { // Avoid rapid infinite loop exit. sleep(1); static::stopAll(250, $e); } finally { if ($this->pauseAccept === null) { $this->resumeAccept(); } Context::destroy(); } }; match (Worker::$eventLoopClass) { Swoole::class, Swow::class, Fiber::class => Coroutine::create($callback), default => (new \Fiber($callback))->start(), }; } /** * Stop current worker instance. * * @param bool $force * @return void */ public function stop(bool $force = true): void { if ($this->stopping === true) { return; } // Try to emit onWorkerStop callback. if ($this->onWorkerStop) { try { ($this->onWorkerStop)($this); } catch (Throwable $e) { static::log($e); } } // Remove listener for server socket. $this->unlisten(); // Close all connections for the worker. if (!static::getGracefulStop()) { foreach ($this->connections as $connection) { if ($force || !$connection->getRecvBufferQueueSize()) { $connection->close(); } } } // Clear callback. $this->onMessage = $this->onClose = $this->onError = $this->onBufferDrain = $this->onBufferFull = null; $this->stopping = true; } /** * Accept a connection. * * @param resource $socket * @return void */ protected function acceptTcpConnection(mixed $socket): void { // Accept a connection on server socket. set_error_handler(static fn (): bool => true); $newSocket = stream_socket_accept($socket, 0, $remoteAddress); restore_error_handler(); // Thundering herd. if (!$newSocket) { return; } // TcpConnection. $connection = new TcpConnection(static::$globalEvent, $newSocket, $remoteAddress); $this->connections[$connection->id] = $connection; $connection->worker = $this; $connection->protocol = $this->protocol; $connection->transport = $this->transport; $connection->onMessage = $this->onMessage; $connection->onClose = $this->onClose; $connection->onError = $this->onError; $connection->onBufferDrain = $this->onBufferDrain; $connection->onBufferFull = $this->onBufferFull; // Try to emit onConnect callback. if ($this->onConnect) { try { ($this->onConnect)($connection); } catch (Throwable $e) { static::stopAll(250, $e); } } } /** * For udp package. * * @param resource $socket * @return void */ protected function acceptUdpConnection(mixed $socket): void { set_error_handler(static fn (): bool => true); $recvBuffer = stream_socket_recvfrom($socket, UdpConnection::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress); restore_error_handler(); if (false === $recvBuffer || empty($remoteAddress)) { return; } // UdpConnection. $connection = new UdpConnection($socket, $remoteAddress); $connection->protocol = $this->protocol; $messageCallback = $this->onMessage; if ($messageCallback) { try { if ($this->protocol !== null) { $parser = $this->protocol; if ($parser && method_exists($parser, 'input')) { while ($recvBuffer !== '') { $len = $parser::input($recvBuffer, $connection); if ($len === 0) { return; } $package = substr($recvBuffer, 0, $len); $recvBuffer = substr($recvBuffer, $len); $data = $parser::decode($package, $connection); if ($data === false) { continue; } $messageCallback($connection, $data); } } else { $data = $parser::decode($recvBuffer, $connection); // Discard bad packets. if ($data === false) { return; } $messageCallback($connection, $data); } } else { $messageCallback($connection, $recvBuffer); } ConnectionInterface::$statistics['total_request']++; } catch (Throwable $e) { static::stopAll(250, $e); } } } /** * Check master process is alive * * @param int $masterPid * @return bool */ protected static function checkMasterIsAlive(int $masterPid): bool { if (empty($masterPid)) { return false; } $masterIsAlive = posix_kill($masterPid, 0) && posix_getpid() !== $masterPid; if (!$masterIsAlive) { static::log("Master pid:$masterPid is not alive"); return false; } $cmdline = "/proc/$masterPid/cmdline"; if (!is_readable($cmdline)) { return true; } $content = file_get_contents($cmdline); if (empty($content)) { return true; } return str_contains($content, 'WorkerMan') || str_contains($content, 'php'); } /** * If worker is running. * * @return bool */ public static function isRunning(): bool { return Worker::$status !== Worker::STATUS_INITIAL; } } ================================================ FILE: tests/Feature/ExampleTest.php ================================================ toBeTrue(); }); ================================================ FILE: tests/Feature/HttpConnectionTest.php ================================================ start(); usleep(250000); }); afterAll(function () use (&$process) { echo $process->getOutput(); $process->stop(); }); it('tests http connection', function () { $client = new Client([ 'base_uri' => 'http://127.0.0.1:8080', 'cookies' => true, 'http_errors' => false, ]); $response = $client->get('/'); expect($response->getStatusCode()) ->toBe(200) ->and($response->getHeaderLine('Server')) ->tobe('workerman') ->and($response->getHeaderLine('Content-Length')) ->tobe('12') ->and($response->getBody()->getContents()) ->toBe('Hello Chance'); $data = [ 'foo' => 'bar', 'key' => ['hello', 'chance'] ]; $response = $client->get('/get', [ 'query' => $data ]); expect($response->getBody()->getContents()) ->toBeJson() ->json() ->toBe($data); $response = $client->post('/post', [ 'json' => $data ]); expect($response->getBody()->getContents()) ->toBeJson() ->json() ->toBe($data); $response = $client->post('/header', [ 'headers' => [ 'foo' => 'bar' ] ]); expect($response->getBody()->getContents()) ->toBe('bar'); $cookie = new CookieJar(); $client->get('/setSession', [ 'cookies' => $cookie ]); $response = $client->get('/session', [ 'cookies' => $cookie ]); expect($response->getBody()->getContents()) ->toBe('bar'); $response = $client->get('/session', [ 'cookies' => $cookie ]); expect($response->getBody()->getContents()) ->toBe(''); $response = $client->get('/sse', [ 'stream' => true, ]); $stream = $response->getBody(); $i = 0; while (!$stream->eof()) { if ($i >= 5) { expect($stream->read(1024))->toBeEmpty(); break; } $i++; expect($stream->read(1024))->toBe("data: hello$i\n\n"); } $file = Utils::tryFopen(__DIR__ . '/Stub/HttpServer.php', 'r'); $response = $client->post('/file', [ 'multipart' => [ [ 'name' => 'file', 'contents' => $file ] ] ]); expect($response->getBody()->getContents()) ->toBeJson() ->json() ->toMatchArray([ 'name' => 'HttpServer.php', 'error' => 0, ]); $response = $client->get('/404'); expect($response->getStatusCode()) ->toBe(404) ->and($response->getBody()->getContents()) ->toBe('404 not found'); }); ================================================ FILE: tests/Feature/Stub/HttpServer.php ================================================ onMessage = function (TcpConnection $connection, Request $request) { match ($request->path()) { '/' => $connection->send('Hello Chance'), '/get' => $connection->send(json_encode($request->get())), '/post' => $connection->send(json_encode($request->post())), '/header' => $connection->send($request->header('foo')), '/setSession' => (function () use ($connection, $request) { $request->session()->set('foo', 'bar'); $connection->send(''); })(), '/session' => $connection->send($request->session()->pull('foo')), '/sse' => (function () use ($connection) { $connection->send(new Response(200, ['Content-Type' => 'text/event-stream'], "\r\n")); $i = 0; $timer_id = Timer::add(0.001, function () use ($connection, &$timer_id, &$i) { if ($connection->getStatus() !== TcpConnection::STATUS_ESTABLISHED) { Timer::del($timer_id); return; } if ($i >= 5) { Timer::del($timer_id); $connection->close(); return; } $i++; $connection->send(new ServerSentEvents(['data' => "hello$i"])); }); })(), '/file' => $connection->send(json_encode($request->file('file'))), default => $connection->send(new Response(404, [], '404 not found')) }; }; Worker::$command = 'start'; Worker::$pidFile = sprintf('%s/test-http-server.pid', sys_get_temp_dir()); Worker::$logFile = sprintf('%s/test-http-server.log', sys_get_temp_dir()); Worker::runAll(); ================================================ FILE: tests/Feature/Stub/UdpServer.php ================================================ onMessage = function ($connection, $data) { $connection->send('received: ' . $data); }; Worker::$pidFile = sprintf('%s/test-udp-server.pid', sys_get_temp_dir()); Worker::$logFile = sprintf('%s/test-udp-server.log', sys_get_temp_dir()); Worker::$command = 'start'; Worker::runAll(); ================================================ FILE: tests/Feature/Stub/WebsocketClient.php ================================================ onWorkerStart = function($worker) { $con = new AsyncTcpConnection('ws://127.0.0.1:8081'); //%action% $con->connect(); }; Worker::$pidFile = sprintf('%s/test-websocket-client.pid', sys_get_temp_dir()); Worker::$logFile = sprintf('%s/test-websocket-client.log', sys_get_temp_dir()); Worker::$command = 'start'; Worker::runAll(); ================================================ FILE: tests/Feature/Stub/WebsocketServer.php ================================================ start(); usleep(600000); }); afterAll(function () use (&$process) { echo "\nUDP Test:\n", $process->getOutput(); $process->stop(); }); it('tests udp connection', function () { $socket = stream_socket_client('udp://127.0.0.1:8083', $errno, $errstr, 1); expect($errno)->toBeInt()->toBe(0); stream_set_timeout($socket, 1); fwrite($socket, 'xiami'); // 使用 recvfrom 读取,循环等待最多 ~1s $data = ''; $start = microtime(true); do { $peer = null; $chunk = @stream_socket_recvfrom($socket, 1024, 0, $peer); if ($chunk !== false && $chunk !== '') { $data = $chunk; break; } usleep(50000); } while ((microtime(true) - $start) < 1.0); expect($data)->toBe('received: xiami'); fclose($socket); }) ->skipOnWindows(); //require posix ================================================ FILE: tests/Feature/WebsocketServiceTest.php ================================================ onWebSocketConnect = function () { echo "connected"; }; \$worker->onMessage = function () {}; PHP)); $serverProcess->start(); usleep(600000); $clientProcess = new PhpProcess(str_replace(subject: $clientCode, search: '//%action%', replace: <<onWebSocketConnect = function(AsyncTcpConnection \$con) { \$con->send('connect'); }; PHP)); $clientProcess->start(); usleep(600000); try { expect(getNonFrameOutput($serverProcess->getOutput()))->toBe('connected') ->and(getNonFrameOutput($clientProcess->getOutput()))->toBe(''); } finally { $serverProcess->stop(); $clientProcess->stop(); } }); it('tests server and client sending and receiving messages', function () use ($serverCode, $clientCode) { $serverProcess = new PhpProcess(str_replace(subject: $serverCode, search: '//%action%', replace: <<onMessage = function (TcpConnection \$connection, \$data) { echo \$data; \$connection->send('Hi'); }; PHP)); $serverProcess->start(); usleep(600000); $clientProcess = new PhpProcess(str_replace(subject: $clientCode, search: '//%action%', replace: <<onWebSocketConnect = function(AsyncTcpConnection \$con) { \$con->send('Hello Chance'); }; \$con->onMessage = function(\$con, \$data) { echo \$data; }; PHP)); $clientProcess->start(); usleep(600000); try { expect(getNonFrameOutput($serverProcess->getOutput()))->toBe('Hello Chance') ->and(getNonFrameOutput($clientProcess->getOutput()))->toBe('Hi'); } finally { $serverProcess->stop(); $clientProcess->stop(); } }); it('tests server close connection', function () use ($serverCode, $clientCode) { $serverProcess = new PhpProcess(str_replace(subject: $serverCode, search: '//%action%', replace: <<onWebSocketConnect = function (TcpConnection \$connection) { echo 'close connection'; \$connection->close(); }; \$worker->onMessage = function () {}; PHP)); $serverProcess->start(); usleep(600000); $clientProcess = new PhpProcess(str_replace(subject: $clientCode, search: '//%action%', replace: <<onWebSocketConnect = function(AsyncTcpConnection \$con) { \$con->send('connect'); }; \$con->onClose = function () { echo 'closed'; }; PHP)); $clientProcess->start(); usleep(600000); try { expect(getNonFrameOutput($serverProcess->getOutput()))->toBe('close connection') ->and(getNonFrameOutput($clientProcess->getOutput()))->toBe('closed'); } finally { $serverProcess->stop(); $clientProcess->stop(); } }); it('tests client close connection', function () use ($serverCode, $clientCode) { $serverProcess = new PhpProcess(str_replace(subject: $serverCode, search: '//%action%', replace: <<onMessage = function () {}; \$worker->onClose = function () { echo 'closed'; }; PHP)); $serverProcess->start(); usleep(600000); $clientProcess = new PhpProcess(str_replace(subject: $clientCode, search: '//%action%', replace: <<onWebSocketConnect = function(AsyncTcpConnection \$con) { \$con->send('connect'); echo 'close connection'; \$con->close(); }; PHP)); $clientProcess->start(); usleep(600000); try { expect(getNonFrameOutput($serverProcess->getOutput()))->toBe('closed') ->and(getNonFrameOutput($clientProcess->getOutput()))->toBe('close connection'); } finally { $serverProcess->stop(); $clientProcess->stop(); } }); ================================================ FILE: tests/Pest.php ================================================ in('Feature'); /* |-------------------------------------------------------------------------- | Expectations |-------------------------------------------------------------------------- | | When you're writing tests, you often need to check that values meet certain conditions. The | "expect()" function gives you access to a set of "expectations" methods that you can use | to assert different things. Of course, you may extend the Expectation API at any time. | */ use Workerman\Connection\TcpConnection; /* |-------------------------------------------------------------------------- | Functions |-------------------------------------------------------------------------- | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your | project that you don't want to repeat in every file. Here you can also expose helpers as | global functions to help you to reduce the number of lines of code in your test files. | */ function something() { // .. } function testWithConnectionClose(Closure $closure, ?string $dataContains = null, $connectionClass = TcpConnection::class): void { $tcpConnection = Mockery::spy($connectionClass); $closure($tcpConnection); if ($dataContains) { $tcpConnection->shouldHaveReceived('close', function ($actual) use ($dataContains) { return str_contains($actual, $dataContains); }); } else { $tcpConnection->shouldHaveReceived('close'); } } function testWithConnectionEnd(Closure $closure, ?string $dataContains = null, $connectionClass = TcpConnection::class): void { $tcpConnection = Mockery::spy($connectionClass); $closure($tcpConnection); if ($dataContains) { $tcpConnection->shouldHaveReceived('end', function ($actual) use ($dataContains) { return str_contains($actual, $dataContains); }); } else { $tcpConnection->shouldHaveReceived('end'); } } function getNonFrameOutput(string $output): string { $end = "Start success.\n"; $pos = strpos($output, $end); if ($pos !== false) { return substr($output, $pos + strlen($end)); } return $output; } ================================================ FILE: tests/TestCase.php ================================================ not->toBeFalse(); $serverName = stream_socket_get_name($server, false); expect($serverName)->not->toBeFalse(); $client = stream_socket_client('tcp://' . $serverName, $errno, $errstr, 1); expect($client)->not->toBeFalse(); $accepted = stream_socket_accept($server, 1); expect($accepted)->not->toBeFalse(); $remoteAddress = (string)stream_socket_get_name($accepted, true); $connection = new TcpConnection($event, $accepted, $remoteAddress); $connection->protocol = Text::class; $connection->lingerTimeout = 0.01; $calls = 0; $connection->onMessage = function (TcpConnection $c, string $msg) use (&$calls): void { $calls++; if ($msg === 'a') { // Simulate app deciding to end() while there is another pipelined message in buffer. $c->end(); } }; fwrite($client, "a\nb\n"); $event->delay(0.03, static fn () => $event->stop()); $event->run(); expect($calls)->toBe(1); fclose($client); fclose($server); }); ================================================ FILE: tests/Unit/Connection/TcpConnectionEndTest.php ================================================ not->toBeFalse(); $serverName = stream_socket_get_name($server, false); expect($serverName)->not->toBeFalse(); $client = stream_socket_client('tcp://' . $serverName, $errno, $errstr, 1); expect($client)->not->toBeFalse(); $accepted = stream_socket_accept($server, 1); expect($accepted)->not->toBeFalse(); $remoteAddress = (string)stream_socket_get_name($accepted, true); $connection = new TcpConnection($event, $accepted, $remoteAddress); $connection->lingerTimeout = 0.01; $connection->end(); $event->delay(0.03, static fn () => $event->stop()); $event->run(); expect($connection->getStatus())->toBe(TcpConnection::STATUS_CLOSED); fclose($client); fclose($server); }); ================================================ FILE: tests/Unit/Connection/UdpConnectionTest.php ================================================ start(); usleep(250000); }); afterAll(function () use (&$process) { $process->stop(); }); it('tests ' . UdpConnection::class, function () use ($remoteAddress) { $socketClient = stream_socket_client("udp://$remoteAddress"); $udpConnection = new UdpConnection($socketClient, $remoteAddress); $udpConnection->protocol = Text::class; expect($udpConnection->send('foo'))->toBeTrue() ->and($udpConnection->getRemoteIp())->toBe('127.0.0.1') ->and($udpConnection->getRemotePort())->toBe(8082) ->and($udpConnection->getRemoteAddress())->toBe($remoteAddress) ->and($udpConnection->getLocalIp())->toBeIn(['::1', '[::1]', '127.0.0.1']) ->and($udpConnection->getLocalPort())->toBeInt() ->and(json_encode($udpConnection))->toBeJson() ->toContain('transport') ->toContain('getRemoteIp') ->toContain('remotePort') ->toContain('getRemoteAddress') ->toContain('getLocalIp') ->toContain('getLocalPort') ->toContain('isIpV4') ->toContain('isIpV6'); $udpConnection->close('bye'); if (is_resource($socketClient)) { fclose($socketClient); } }); ================================================ FILE: tests/Unit/Protocols/FrameTest.php ================================================ toBe(0) ->and(Frame::input("\0\0\0*foobar")) ->toBe(42); }); it('tests ::decode', function () { $buffer = pack('N', 5) . 'jhdxr'; expect(Frame::decode($buffer)) ->toBe('jhdxr'); }); it('tests ::encode', function () { expect(Frame::encode('jhdxr')) ->toBe(pack('N', 9) . 'jhdxr'); }); ================================================ FILE: tests/Unit/Protocols/Http/RequestSessionTest.php ================================================ headers = []; $request->connection = $connection; try { $callback($request); } finally { $request->destroy(); Session::$cookieLifetime = $origLifetime; Session::$cookiePath = $origPath; Session::$domain = $origDomain; Session::$secure = $origSecure; Session::$httpOnly = $origHttpOnly; Session::$sameSite = $origSameSite; $files = glob($sessionSavePath . '*'); if ($files) { array_map('unlink', array_filter($files, 'is_file')); } if (is_dir($sessionSavePath)) { @rmdir($sessionSavePath); } } } // ─── sessionRegenerateId ──────────────────────────────────────────────────── describe('sessionRegenerateId', function () { it('returns a new session id different from the original', function () { withSessionRequest(function (Request $request) { $oldSid = $request->sessionId(); $newSid = $request->sessionRegenerateId(); expect($newSid)->not->toBe($oldSid) ->and($newSid)->toMatch('/^[a-zA-Z0-9,-]{16,256}$/'); }); }); it('updates sessionId() to return the regenerated id', function () { withSessionRequest(function (Request $request) { $request->sessionId(); $newSid = $request->sessionRegenerateId(); expect($request->sessionId())->toBe($newSid); }); }); it('updates session() to return a new Session instance with the regenerated id', function () { withSessionRequest(function (Request $request) { $oldSession = $request->session(); $newSid = $request->sessionRegenerateId(); $newSession = $request->session(); expect($newSession)->not->toBe($oldSession) ->and($newSession->getId())->toBe($newSid); }); }); it('migrates existing session data to the new session', function () { withSessionRequest(function (Request $request) { $request->session()->set('user_id', 42); $request->session()->set('role', 'admin'); $request->sessionRegenerateId(); expect($request->session()->get('user_id'))->toBe(42) ->and($request->session()->get('role'))->toBe('admin'); }); }); it('preserves old session in storage when deleteOldSession is false', function () { withSessionRequest(function (Request $request) { $oldSid = $request->sessionId(); $request->session()->set('key', 'value'); $request->session()->save(); $request->sessionRegenerateId(false); $reloaded = new Session($oldSid); expect($reloaded->get('key'))->toBe('value'); }); }); it('destroys old session in storage when deleteOldSession is true', function () { withSessionRequest(function (Request $request) { $oldSid = $request->sessionId(); $request->session()->set('key', 'value'); $request->session()->save(); $request->sessionRegenerateId(true); $reloaded = new Session($oldSid); expect($reloaded->all())->toBe([]); }); }); it('writes Set-Cookie header with correct cookie attributes', function () { withSessionRequest(function (Request $request) { $newSid = $request->sessionRegenerateId(); $connection = $request->connection; assert($connection !== null); $cookies = $connection->headers['Set-Cookie'] ?? []; expect($cookies)->toBeArray()->toHaveCount(1); $cookie = $cookies[0]; expect($cookie) ->toStartWith(Session::$name . '=' . $newSid) ->toContain('Domain=example.com') ->toContain('Max-Age=7200') ->toContain('Path=/app') ->toContain('SameSite=Lax') ->toContain('Secure') ->toContain('HttpOnly'); }, customCookieParams: true); }); it('stays consistent after multiple regenerations', function () { withSessionRequest(function (Request $request) { $request->session()->set('counter', 1); $sid1 = $request->sessionRegenerateId(); expect($request->sessionId())->toBe($sid1) ->and($request->session()->getId())->toBe($sid1) ->and($request->session()->get('counter'))->toBe(1); $request->session()->set('counter', 2); $sid2 = $request->sessionRegenerateId(); expect($request->sessionId())->toBe($sid2) ->and($request->session()->getId())->toBe($sid2) ->and($request->session()->get('counter'))->toBe(2) ->and($sid2)->not->toBe($sid1); }); }); }); // ─── sessionId setter ─────────────────────────────────────────────────────── describe('sessionId setter', function () { it('switches sessionId() and session() to the new id', function () { withSessionRequest(function (Request $request) { $oldSession = $request->session(); $newSid = str_repeat('A', 32); $request->sessionId($newSid); expect($request->sessionId())->toBe($newSid) ->and($request->session()->getId())->toBe($newSid) ->and($request->session())->not->toBe($oldSession); }); }); it('does not migrate old session data to the new session', function () { withSessionRequest(function (Request $request) { $request->session()->set('user_id', 42); $request->session()->set('role', 'admin'); $newSid = str_repeat('B', 32); $request->sessionId($newSid); expect($request->session()->get('user_id'))->toBeNull() ->and($request->session()->get('role'))->toBeNull() ->and($request->session()->all())->toBe([]); }); }); it('persists old session data to storage when switching', function () { withSessionRequest(function (Request $request) { $oldSid = $request->sessionId(); $request->session()->set('key', 'value'); $request->session()->save(); $newSid = str_repeat('C', 32); $request->sessionId($newSid); $reloaded = new Session($oldSid); expect($reloaded->get('key'))->toBe('value'); }); }); it('saves unsaved in-memory modifications of old session before switching', function () { withSessionRequest(function (Request $request) { $oldSid = $request->sessionId(); $request->session()->set('saved_key', 'saved_val'); $request->session()->save(); $request->session()->set('unsaved_key', 'unsaved_val'); $newSid = str_repeat('D', 32); $request->sessionId($newSid); $reloaded = new Session($oldSid); expect($reloaded->get('saved_key'))->toBe('saved_val') ->and($reloaded->get('unsaved_key'))->toBe('unsaved_val'); }); }); it('writes Set-Cookie header with correct cookie attributes', function () { withSessionRequest(function (Request $request) { $newSid = str_repeat('E', 32); $request->sessionId($newSid); $connection = $request->connection; assert($connection !== null); $cookies = $connection->headers['Set-Cookie'] ?? []; expect($cookies)->toBeArray()->toHaveCount(1); $cookie = $cookies[0]; expect($cookie) ->toStartWith(Session::$name . '=' . $newSid) ->toContain('Domain=example.com') ->toContain('Max-Age=7200') ->toContain('Path=/app') ->toContain('SameSite=Lax') ->toContain('Secure') ->toContain('HttpOnly'); }, customCookieParams: true); }); it('loads pre-existing data when switching to an existing session id', function () { withSessionRequest(function (Request $request) { $existingSid = str_repeat('F', 32); $pre = new Session($existingSid); $pre->set('pre_key', 'pre_val'); $pre->save(); $request->session()->set('current', 'data'); $request->sessionId($existingSid); expect($request->session()->get('pre_key'))->toBe('pre_val') ->and($request->session()->get('current'))->toBeNull(); }); }); }); ================================================ FILE: tests/Unit/Protocols/Http/ResponseTest.php ================================================ 'bar'], 'hello, xiami'); expect($response->getStatusCode())->toBe(201) ->and($response->getHeaders())->toBe(['X-foo' => 'bar']) ->and($response->rawBody())->toBe('hello, xiami'); //headers $response->header('abc', '123'); $response->withHeader('X-foo', 'baz'); $response->withHeaders(['def' => '456']); expect((string)$response) ->toContain('X-foo: baz') ->toContain('abc: 123') ->toContain('def: 456'); $response->withoutHeader('def'); expect((string)$response)->not->toContain('def: 456') ->and($response->getHeader('abc')) ->toBe('123'); $response->withStatus(202, 'some reason'); expect($response->getReasonPhrase())->toBe('some reason'); $response->withProtocolVersion('1.0'); $response->withBody('hello, world'); expect((string)$response) ->toContain('HTTP/1.0') ->toContain('hello, world') ->toContain('Content-Type: ') ->toContain('Content-Length: 12') ->not()->toContain('Transfer-Encoding: '); //cookie $response->cookie('foo', 'bar', domain: 'xia.moe', httpOnly: true); expect((string)$response) ->toContain('Set-Cookie: foo=bar; Domain=xia.moe; HttpOnly'); }); it('tests file', function (){ //todo may have to redo the simple test, // as the implementation of headers is a different function for files. // or actually maybe the Response is the one should be rewritten to reuse? $response = new Response(); $tmpFile = tempnam(sys_get_temp_dir(), 'test'); rename($tmpFile, $tmpFile .'.jpg'); $tmpFile .= '.jpg'; file_put_contents($tmpFile, 'hello, xiami'); $response->withFile($tmpFile, 0, 12); expect((string)$response) ->toContain('Content-Type: image/jpeg') ->toContain('Last-Modified: '); }); ================================================ FILE: tests/Unit/Protocols/Http/ServerSentEventsTest.php ================================================ 'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000, ]; $sse = new ServerSentEvents($data); $expected = "event: {$data['event']}\nid: {$data['id']}\nretry: {$data['retry']}\ndata: {$data['data']}\n\n"; expect((string)$sse)->toBe($expected); }); ================================================ FILE: tests/Unit/Protocols/HttpTest.php ================================================ toBe($class::class); //restore old request class Http::requestClass($oldRequestClass); }); it('tests ::input', function () { //test 413 payload too large testWithConnectionEnd(function (TcpConnection $tcpConnection) { expect(Http::input(str_repeat('jhdxr', 3333), $tcpConnection)) ->toBe(0); }, '413 Payload Too Large'); //example request from ChatGPT :) $buffer = "POST /path/to/resource HTTP/1.1\r\n" . "Host: example.com\r\n" . "Content-Type: application/json\r\n" . "Content-Length: 27\r\n" . "\r\n" . '{"key": "value", "foo": "bar"}'; //unrecognized method testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { expect(Http::input(str_replace('POST', 'MIAOWU', $buffer), $tcpConnection)) ->toBe(0); }, '400 Bad Request'); //content-length exceeds connection max package size testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { $tcpConnection->maxPackageSize = 10; expect(Http::input($buffer, $tcpConnection)) ->toBe(0); }, '413 Payload Too Large'); }); it('sends 413 with Connection: close header', function () { /** @var TcpConnection&\Mockery\MockInterface $tcpConnection */ $tcpConnection = Mockery::spy(TcpConnection::class); Http::input(str_repeat('a', 16384), $tcpConnection); $tcpConnection->shouldHaveReceived('end', function ($actual) { return str_contains($actual, '413 Payload Too Large') && str_contains($actual, "Connection: close\r\n"); }); }); it('tests ::input request-line and header validation matrix', function (string $buffer, int $expectedLength) { /** @var TcpConnection&\Mockery\MockInterface $tcpConnection */ $tcpConnection = Mockery::spy(TcpConnection::class); $tcpConnection->maxPackageSize = 1024 * 1024; expect(Http::input($buffer, $tcpConnection))->toBe($expectedLength); $tcpConnection->shouldNotHaveReceived('close'); })->with([ 'minimal GET / HTTP/1.1' => [ "GET / HTTP/1.1\r\n\r\n", 18, // strlen("GET / HTTP/1.1\r\n\r\n") ], 'lowercase method and version is allowed' => [ "get / http/1.1\r\n\r\n", 18, ], 'all supported methods' => [ "PATCH /a HTTP/1.0\r\n\r\n", 21, // PATCH(5) + space + /a(2) + space + HTTP/1.0(8) + \r\n\r\n(4) = 21 ], 'GET with Content-Length is allowed and affects package length' => [ "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\nhello", strlen("GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n") + 5, // header length + body length ], 'request-target allows UTF-8 bytes (compatibility)' => [ "GET /中文 HTTP/1.1\r\n\r\n", strlen("GET /中文 HTTP/1.1\r\n\r\n"), ], 'pipeline: first request length is returned' => [ "GET / HTTP/1.1\r\n\r\nGET /b HTTP/1.1\r\n\r\n", 18, ], 'X-Transfer-Encoding does not trigger Transfer-Encoding ban' => [ "GET / HTTP/1.1\r\nX-Transfer-Encoding: chunked\r\n\r\n", 18 + strlen("X-Transfer-Encoding: chunked\r\n"), ], ]); it('rejects invalid request-line cases in ::input', function (string $buffer) { testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { expect(Http::input($buffer, $tcpConnection))->toBe(0); }, '400 Bad Request'); })->with([ 'unknown method similar to valid one' => [ "POSTS / HTTP/1.1\r\n\r\n", ], 'tab delimiter between method and path is not allowed' => [ "GET\t/ HTTP/1.1\r\n\r\n", ], 'leading whitespace before method is not allowed' => [ " GET / HTTP/1.1\r\n\r\n", ], 'absolute-form request-target is not supported' => [ "GET http://example.com/ HTTP/1.1\r\n\r\n", ], 'asterisk-form request-target is not supported (including OPTIONS *)' => [ "OPTIONS * HTTP/1.1\r\n\r\n", ], 'invalid http version' => [ "GET / HTTP/2.0\r\n\r\n", ], 'invalid path contains space' => [ "GET /a b HTTP/1.1\r\n\r\n", ], 'invalid path contains DEL' => [ "GET /\x7f HTTP/1.1\r\n\r\n", ], 'CRLF injection attempt in request-target' => [ "GET /foo\r\nX: y HTTP/1.1\r\n\r\n", ], ]); it('rejects Transfer-Encoding and bad/duplicate Content-Length in ::input', function (string $buffer, ?string $expectedCloseContains = '400 Bad Request') { testWithConnectionEnd(function (TcpConnection $tcpConnection) use ($buffer) { expect(Http::input($buffer, $tcpConnection))->toBe(0); }, $expectedCloseContains); })->with([ 'Transfer-Encoding is forbidden (case-insensitive)' => [ "GET / HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n", '400 Bad Request', ], 'Content-Length must be digits (not a number)' => [ "GET / HTTP/1.1\r\nContent-Length: abc\r\n\r\n", '400 Bad Request', ], 'Content-Length must be digits (digits + letters)' => [ "GET / HTTP/1.1\r\nContent-Length: 12abc\r\n\r\n", '400 Bad Request', ], 'Content-Length must be digits (empty value)' => [ "GET / HTTP/1.1\r\nContent-Length: \r\n\r\n", '400 Bad Request', ], 'Content-Length must be digits (comma list)' => [ "GET / HTTP/1.1\r\nContent-Length: 1,2\r\n\r\n", '400 Bad Request', ], 'duplicate Content-Length (adjacent)' => [ "GET / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 1\r\n\r\nx", '400 Bad Request', ], 'duplicate Content-Length (separated by other header, case-insensitive)' => [ "GET / HTTP/1.1\r\ncontent-length: 1\r\nX: y\r\nContent-Length: 1\r\n\r\nx", '400 Bad Request', ], 'very large numeric Content-Length should be rejected by maxPackageSize (413)' => [ "GET / HTTP/1.1\r\nContent-Length: 999999999999999999999999999999999999\r\n\r\n", '413 Payload Too Large', ], ]); it('tests ::encode for non-object response', function () { /** @var TcpConnection $tcpConnection */ $tcpConnection = Mockery::mock(TcpConnection::class); $tcpConnection->headers = [ 'foo' => 'bar', 'jhdxr' => ['a', 'b'], ]; $extHeader = "foo: bar\r\n" . "jhdxr: a\r\n" . "jhdxr: b\r\n"; expect(Http::encode('xiami', $tcpConnection)) ->toBe("HTTP/1.1 200 OK\r\n" . "Server: workerman\r\n" . "{$extHeader}Connection: keep-alive\r\n" . "Content-Type: text/html;charset=utf-8\r\n" . "Content-Length: 5\r\n\r\nxiami"); }); it('tests ::encode for ' . Response::class, function () { /** @var TcpConnection $tcpConnection */ $tcpConnection = Mockery::mock(TcpConnection::class); $tcpConnection->headers = [ 'foo' => 'bar', 'jhdxr' => ['a', 'b'], ]; $extHeader = "foo: bar\r\n" . "jhdxr: a\r\n" . "jhdxr: b\r\n"; $response = new Response(body: 'xiami'); expect(Http::encode($response, $tcpConnection)) ->toBe("HTTP/1.1 200 OK\r\n" . "Server: workerman\r\n" . "{$extHeader}Connection: keep-alive\r\n" . "Content-Type: text/html;charset=utf-8\r\n" . "Content-Length: 5\r\n\r\nxiami"); }); it('tests ::decode', function () { /** @var TcpConnection $tcpConnection */ $tcpConnection = Mockery::mock(TcpConnection::class); //example request from ChatGPT :) $buffer = "POST /path/to/resource HTTP/1.1\r\n" . "Host: example.com\r\n" . "Content-Type: application/json\r\n" . "Content-Length: 27\r\n" . "\r\n" . '{"key": "value", "foo": "bar"}'; $value = expect(Http::decode($buffer, $tcpConnection)) ->toBeInstanceOf(Request::class) ->value; //test cache expect($value == Http::decode($buffer, $tcpConnection)) ->toBeTrue(); }); ================================================ FILE: tests/Unit/Protocols/TextTest.php ================================================ maxPackageSize = 5; expect(Text::input('abcdefgh', $connection)) ->toBe(0); }); //input without "\n" expect(Text::input('jhdxr', $connection)) ->toBe(0) //input with "\n" ->and(Text::input("jhdxr\n", $connection)) ->toBe(6) //::encode ->and(Text::encode('jhdxr')) ->toBe("jhdxr\n") //::decode ->and(Text::decode("jhdxr\n")) ->toBe('jhdxr'); });