diff --git a/CHANGELOG.md b/CHANGELOG.md index 93aa6fd..74a5c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to ext-websocket are documented here. +## Unreleased + +### Changed + +- Moved benchmarks to the separate [`php-websocket-bench`](https://github.com/axcherednikov/php-websocket-bench) repository. + ## 1.1.0 - 2026-05-23 ### Added @@ -52,7 +58,7 @@ All notable changes to ext-websocket are documented here. ### Changed -- Simplified the root README and moved detailed benchmark output to `bench/README.md`. +- Simplified the root README and moved detailed benchmark output out of the root README. ## 0.7.0 - 2026-05-18 diff --git a/README.md b/README.md index c66f256..b5f55bb 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ use WebSocket\Server; $server = new Server(); $server->listen('127.0.0.1', 8080); +$server->subprotocols('chat.v1'); $server->onOpen(static function (Connection $connection): void { - echo "open {$connection->id}\n"; + echo "open {$connection->id}"; + echo $connection->subprotocol ? " ({$connection->subprotocol})\n" : "\n"; }); $server->onMessage(static function (Connection $connection, string $message): void { @@ -113,6 +115,7 @@ Methods: |---|---| | `__construct(ServerOptions\|array $options = [])` | Create a server | | `listen(string $host, int $port): void` | Bind address for `run()` | +| `subprotocols(string ...$protocols): void` | Configure supported `Sec-WebSocket-Protocol` tokens | | `onOpen(Closure $handler): void` | Register upgraded connection callback | | `onMessage(Closure $handler): void` | Register text/binary message callback | | `onClose(Closure $handler): void` | Register close callback | @@ -130,6 +133,7 @@ Methods: | `isOpen(): bool` | Check connection state | | `readonly string $id` | Connection id | | `readonly string $remoteAddress` | Remote peer address | +| `readonly ?string $subprotocol` | Negotiated subprotocol, or `null` | ### `WebSocket\Protocol` @@ -151,9 +155,9 @@ Methods: ## Benchmarks -Detailed benchmark results and commands live in [bench/README.md](bench/README.md). +Detailed benchmark results and commands live in the separate [php-websocket-bench](https://github.com/axcherednikov/php-websocket-bench) repository. -The current benchmark suite covers protocol encode/decode, server accept/upgrade runtime, and real `ws://` / `wss://` message runtime against AMPHP, Workerman, and OpenSwoole. +The benchmark suite covers protocol encode/decode, server upgrade runtime, and real `ws://` / `wss://` message runtime against AMPHP, Workerman, OpenSwoole, and Ratchet. ## Production diff --git a/bench/README.md b/bench/README.md deleted file mode 100644 index 8e73b2d..0000000 --- a/bench/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Benchmarks - -These benchmarks compare the current native protocol helpers and native server runtime with PHP WebSocket libraries. -The protocol AMPHP entry point installs `amphp/websocket-server` and measures the RFC 6455 parser/compiler it uses through `amphp/websocket`. -The protocol OpenSwoole entry point measures `OpenSwoole\WebSocket\Server::pack()` and `OpenSwoole\WebSocket\Server::unpack()`. -OpenSwoole is optional because it is a PHP extension installed outside Composer. - -The protocol suite measures hot paths: - -- server-side text frame encoding -- masked client text frame decoding - -The server runtime suite measures raw TCP accept loops and the native HTTP Upgrade close path. - -The message runtime suite measures complete WebSocket server scenarios: - -- upgraded idle connections -- pipelined echo messages -- broadcast fanout deliveries -- direct `ws://` transport and `wss://` through the same local TLS terminator - -## Results - -### Protocol - -**Environment:** PHP 8.4.21, `xdebug.mode=off`, `zend.assertions=-1`, Apple Silicon macOS, 100,000 iterations for 64B payloads and 20,000 iterations for 1024B payloads. Results from May 18, 2026. -AMPHP WebSocket Server v4.0.0, Ratchet v0.4.0, and Workerman v5.2.0 were installed from Composer. OpenSwoole v26.2.0 was installed from PECL. - -| Benchmark | amphp/websocket-server | ratchet/rfc6455 | workerman/workerman | openswoole | ext-websocket | -|---|--:|--:|--:|--:|--:| -| `encode text 64B` | 3,102,715 ops/sec | 1,493,649 ops/sec | 4,406,847 ops/sec | 2,679,178 ops/sec | **15,134,317 ops/sec** | -| `decode masked text 64B` | 592,968 ops/sec | 566,960 ops/sec | 1,776,096 ops/sec | 4,058,675 ops/sec | **7,020,335 ops/sec** | -| `encode text 1024B` | 2,467,892 ops/sec | 1,323,974 ops/sec | 3,537,032 ops/sec | 6,056,783 ops/sec | **12,041,548 ops/sec** | -| `decode masked text 1024B` | 358,676 ops/sec | 278,047 ops/sec | 846,768 ops/sec | 3,694,837 ops/sec | **5,155,027 ops/sec** | - -### Server Runtime - -**Environment:** PHP 8.4.21, `xdebug.mode=off`, `zend.assertions=-1`, Apple Silicon macOS, 1,000 connections, average of 3 runs. Results from May 18, 2026. ext-websocket used the native `kqueue` driver; OpenSwoole was built with `kqueue` enabled. - -| Benchmark | amphp/socket | workerman/workerman | openswoole | ext-websocket | -|---|--:|--:|--:|--:| -| `tcp accept/close` | **16,595 connections/sec** | 16,580 connections/sec | 16,032 connections/sec | n/a | -| `client connect loop` | **7,621 connections/sec** | 7,078 connections/sec | 3,555 connections/sec | n/a | -| `websocket upgrade/close` | n/a | n/a | n/a | 11,055 connections/sec | -| `client upgrade loop` | n/a | n/a | n/a | 6,260 connections/sec | - -> This starts a fresh server process, then measures the current server runtime surface up to the last accepted connection. The ext-websocket entry performs a real HTTP Upgrade before `onOpen(): false` closes the connection; the TCP-only comparison rows are kept as raw accept-loop references. Application message throughput lives in the message-runtime table below. `ratchet/rfc6455` is not listed here because the benchmarked package exposes protocol helpers, not a TCP server runtime. - -### Real `ws`/`wss` Message Runtime - -**Environment:** PHP 8.4.21, `xdebug.mode=off`, `zend.assertions=-1`, Apple Silicon macOS, 50 upgraded connections, 1,000 pipelined echo/broadcast source messages, 1024B text payloads, average of 3 runs. Results from May 18, 2026. The `wss` rows use the same local TLS terminator in front of each server, so the comparison isolates WebSocket runtime behavior under encrypted transport rather than comparing different TLS implementations. - -`ws://` - -| Benchmark | amphp/websocket-server | workerman/workerman | openswoole | ext-websocket | -|---|--:|--:|--:|--:| -| `idle upgraded connections` | 2,785 connections/sec | **5,363 connections/sec** | 4,783 connections/sec | 5,082 connections/sec | -| `echo pipelined messages` | 45,502 messages/sec | 51,332 messages/sec | 50,515 messages/sec | **70,059 messages/sec** | -| `broadcast fanout deliveries` | 186,907 deliveries/sec | 227,944 deliveries/sec | 229,098 deliveries/sec | **818,959 deliveries/sec** | - -`wss://` with the shared local TLS terminator: - -| Benchmark | amphp/websocket-server | workerman/workerman | openswoole | ext-websocket | -|---|--:|--:|--:|--:| -| `idle upgraded connections` | 356 connections/sec | 428 connections/sec | 412 connections/sec | **442 connections/sec** | -| `echo pipelined messages` | 24,783 messages/sec | 29,501 messages/sec | **31,391 messages/sec** | 29,735 messages/sec | -| `broadcast fanout deliveries` | 105,500 deliveries/sec | 163,485 deliveries/sec | 210,075 deliveries/sec | **610,215 deliveries/sec** | - -## Install Dependencies - -```bash -(cd bench && composer install) -``` - -OpenSwoole is optional and is installed as a PHP extension: - -```bash -pecl install openswoole-26.2.0 -``` - -On Homebrew macOS, if `pcre2.h` is not found during compilation: - -```bash -CPPFLAGS="-I/opt/homebrew/include" LDFLAGS="-L/opt/homebrew/lib" pecl install -f openswoole-26.2.0 -``` - -## Run - -From the repository root, build the extension first, then run the benchmark with the same PHP binary: - -```bash -# ext-websocket -php -d xdebug.mode=off -d zend.assertions=-1 -d extension="$PWD/modules/websocket.so" bench/protocol/websocket.php [iterations] -php -d xdebug.mode=off -d zend.assertions=-1 -d extension="$PWD/modules/websocket.so" bench/server-runtime/websocket.php [connections] [rounds] -php -d xdebug.mode=off -d zend.assertions=-1 -d extension="$PWD/modules/websocket.so" bench/message-runtime/websocket.php [connections] [messages] [rounds] [ws|wss|both] [payload-bytes] - -# PHP libraries -php -d xdebug.mode=off -d zend.assertions=-1 bench/protocol/amphp.php [iterations] -php -d xdebug.mode=off -d zend.assertions=-1 bench/server-runtime/amphp.php [connections] [rounds] -php -d xdebug.mode=off -d zend.assertions=-1 bench/message-runtime/amphp.php [connections] [messages] [rounds] [ws|wss|both] [payload-bytes] -php -d xdebug.mode=off -d zend.assertions=-1 bench/protocol/ratchet.php [iterations] -php -d xdebug.mode=off -d zend.assertions=-1 bench/protocol/workerman.php [iterations] -php -d xdebug.mode=off -d zend.assertions=-1 bench/server-runtime/workerman.php [connections] [rounds] -php -d xdebug.mode=off -d zend.assertions=-1 bench/message-runtime/workerman.php [connections] [messages] [rounds] [ws|wss|both] [payload-bytes] - -# Native OpenSwoole extension, when installed -php -d xdebug.mode=off -d zend.assertions=-1 bench/protocol/openswoole.php [iterations] -php -d xdebug.mode=off -d zend.assertions=-1 bench/server-runtime/openswoole.php [connections] [rounds] -php -d xdebug.mode=off -d zend.assertions=-1 bench/message-runtime/openswoole.php [connections] [messages] [rounds] [ws|wss|both] [payload-bytes] -``` - -With Homebrew PHP 8.3: - -```bash -/opt/homebrew/opt/php@8.3/bin/phpize -./configure --enable-websocket --with-php-config=/opt/homebrew/opt/php@8.3/bin/php-config -make -j"$(sysctl -n hw.ncpu)" - -/opt/homebrew/opt/php@8.3/bin/php \ - -d zend.assertions=-1 \ - -d extension="$PWD/modules/websocket.so" \ - bench/protocol/websocket.php -``` - -Default: 100,000 protocol iterations, 1,000 server-runtime connections over 3 rounds, or 50 message-runtime connections with 1,000 messages, 3 rounds, `ws`, and 64B payloads. - -## What is measured - -### Protocol - -| Benchmark | What it tests | -|---|---| -| `encode text 64B` | Server-side text frame encoding for a small payload | -| `decode masked text 64B` | Masked client text frame decoding for a small payload | -| `encode text 1024B` | Server-side text frame encoding for a larger payload | -| `decode masked text 1024B` | Masked client text frame decoding for a larger payload | - -### Server Runtime - -| Benchmark | What it tests | -|---|---| -| `tcp accept/close` | Raw TCP accept loop and connection cleanup | -| `websocket upgrade/close` | Native `WebSocket\Server::run()` HTTP Upgrade path and connection cleanup | -| `client connect loop` | Client-side loop overhead while opening raw TCP benchmark connections | -| `client upgrade loop` | Client-side loop overhead while opening and upgrading benchmark WebSocket connections | - -### Message Runtime - -| Benchmark | What it tests | -|---|---| -| `idle upgraded connections` | Open and hold upgraded WebSocket connections | -| `echo pipelined messages` | Receive many outstanding text messages, dispatch `onMessage`, send replies, and read them client-side | -| `broadcast fanout deliveries` | One incoming message fanned out to all connected clients | - -## File Structure - -| File | Description | -|---|---| -| `protocol/common.php` | Shared RFC 6455 encode/decode benchmark logic | -| `protocol/websocket.php` | ext-websocket protocol benchmark | -| `protocol/amphp.php` | amphp/websocket-server protocol benchmark | -| `protocol/ratchet.php` | ratchet/rfc6455 protocol benchmark | -| `protocol/workerman.php` | workerman/workerman protocol benchmark | -| `protocol/openswoole.php` | OpenSwoole protocol benchmark | -| `server-runtime/common.php` | Shared TCP accept-loop benchmark runner | -| `server-runtime/websocket.php` | ext-websocket TCP accept-loop benchmark | -| `server-runtime/amphp.php` | AMPHP socket accept-loop benchmark | -| `server-runtime/workerman.php` | Workerman accept-loop benchmark | -| `server-runtime/openswoole.php` | OpenSwoole accept-loop benchmark | -| `server-runtime/servers/*.php` | Isolated server processes used by the runtime benchmarks | -| `message-runtime/common.php` | Shared WebSocket message-runtime benchmark runner | -| `message-runtime/tls_proxy.php` | Local TLS terminator used by `wss` message-runtime benchmarks | -| `message-runtime/websocket.php` | ext-websocket message-runtime benchmark | -| `message-runtime/amphp.php` | AMPHP WebSocket Server message-runtime benchmark | -| `message-runtime/workerman.php` | Workerman message-runtime benchmark | -| `message-runtime/openswoole.php` | OpenSwoole message-runtime benchmark | -| `message-runtime/servers/*.php` | Isolated server processes used by the message-runtime benchmarks | diff --git a/bench/composer.json b/bench/composer.json deleted file mode 100644 index 93c0986..0000000 --- a/bench/composer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "axcherednikov/php-websocket-bench", - "description": "Protocol benchmarks for ext-websocket", - "type": "project", - "license": "MIT", - "require": { - "php": ">=8.1", - "amphp/websocket-server": "^4.0", - "guzzlehttp/psr7": "^2.7", - "ratchet/rfc6455": "^0.4", - "workerman/workerman": "^5.1" - }, - "require-dev": { - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-strict-rules": "^2.0" - } -} diff --git a/bench/composer.lock b/bench/composer.lock deleted file mode 100644 index b49026f..0000000 --- a/bench/composer.lock +++ /dev/null @@ -1,2280 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "df1503a60a46420c22e5f55f2cb20776", - "packages": [ - { - "name": "amphp/amp", - "version": "v3.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Future/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", - "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" - ], - "support": { - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-08-27T21:42:00+00:00" - }, - { - "name": "amphp/byte-stream", - "version": "v2.1.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/parser": "^1.1", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2.3" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.22.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php" - ], - "psr-4": { - "Amp\\ByteStream\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "https://amphp.org/byte-stream", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-03-16T17:10:27+00:00" - }, - { - "name": "amphp/cache", - "version": "v2.0.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/cache.git", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", - "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/serialization": "^1", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Cache\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - } - ], - "description": "A fiber-aware cache API based on Amp and Revolt.", - "homepage": "https://amphp.org/cache", - "support": { - "issues": "https://github.com/amphp/cache/issues", - "source": "https://github.com/amphp/cache/tree/v2.0.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:38:06+00:00" - }, - { - "name": "amphp/dns", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/dns.git", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/parser": "^1", - "amphp/process": "^2", - "daverandom/libdns": "^2.0.2", - "ext-filter": "*", - "ext-json": "*", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.20" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Dns\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Wright", - "email": "addr@daverandom.com" - }, - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Async DNS resolution for Amp.", - "homepage": "https://github.com/amphp/dns", - "keywords": [ - "amp", - "amphp", - "async", - "client", - "dns", - "resolve" - ], - "support": { - "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2025-01-19T15:43:40+00:00" - }, - { - "name": "amphp/hpack", - "version": "v3.2.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/hpack.git", - "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/hpack/zipball/291da27078e7e149a9bad4d08ff05bf7d81c89f4", - "reference": "291da27078e7e149a9bad4d08ff05bf7d81c89f4", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "http2jp/hpack-test-case": "^1", - "nikic/php-fuzzer": "^0.0.11", - "phpunit/phpunit": "^7 | ^8 | ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Amp\\Http\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "HTTP/2 HPack implementation.", - "homepage": "https://github.com/amphp/hpack", - "keywords": [ - "headers", - "hpack", - "http-2" - ], - "support": { - "issues": "https://github.com/amphp/hpack/issues", - "source": "https://github.com/amphp/hpack/tree/v3.2.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2026-05-03T19:28:59+00:00" - }, - { - "name": "amphp/http", - "version": "v2.1.2", - "source": { - "type": "git", - "url": "https://github.com/amphp/http.git", - "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/http/zipball/3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", - "reference": "3680d80bd38b5d6f3c2cef2214ca6dd6cef26588", - "shasum": "" - }, - "require": { - "amphp/hpack": "^3", - "amphp/parser": "^1.1", - "league/uri-components": "^2.4.2 | ^7.1", - "php": ">=8.1", - "psr/http-message": "^1 | ^2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "league/uri": "^6.8 | ^7.1", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.26.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/constants.php" - ], - "psr-4": { - "Amp\\Http\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Basic HTTP primitives which can be shared by servers and clients.", - "support": { - "issues": "https://github.com/amphp/http/issues", - "source": "https://github.com/amphp/http/tree/v2.1.2" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-11-23T14:57:26+00:00" - }, - { - "name": "amphp/http-server", - "version": "v3.4.5", - "source": { - "type": "git", - "url": "https://github.com/amphp/http-server.git", - "reference": "ae0fd01e16aba336247852df0c3f8c649a31896d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/http-server/zipball/ae0fd01e16aba336247852df0c3f8c649a31896d", - "reference": "ae0fd01e16aba336247852df0c3f8c649a31896d", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/cache": "^2", - "amphp/hpack": "^3", - "amphp/http": "^2", - "amphp/pipeline": "^1", - "amphp/socket": "^2.1", - "amphp/sync": "^2.2", - "league/uri": "^7.1", - "league/uri-interfaces": "^7.1", - "php": ">=8.1", - "psr/http-message": "^1 | ^2", - "psr/log": "^1 | ^2 | ^3", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/http-client": "^5", - "amphp/log": "^2", - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "league/uri-components": "^7.1", - "monolog/monolog": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "6.16.1" - }, - "suggest": { - "ext-zlib": "Allows GZip compression of response bodies" - }, - "type": "library", - "autoload": { - "files": [ - "src/Driver/functions.php", - "src/Middleware/functions.php", - "src/functions.php" - ], - "psr-4": { - "Amp\\Http\\Server\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "A non-blocking HTTP application server for PHP based on Amp.", - "homepage": "https://github.com/amphp/http-server", - "keywords": [ - "amp", - "amphp", - "async", - "http", - "non-blocking", - "server" - ], - "support": { - "issues": "https://github.com/amphp/http-server/issues", - "source": "https://github.com/amphp/http-server/tree/v3.4.5" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2026-05-01T03:55:07+00:00" - }, - { - "name": "amphp/parser", - "version": "v1.1.1", - "source": { - "type": "git", - "url": "https://github.com/amphp/parser.git", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", - "shasum": "" - }, - "require": { - "php": ">=7.4" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Parser\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A generator parser to make streaming parsers simple.", - "homepage": "https://github.com/amphp/parser", - "keywords": [ - "async", - "non-blocking", - "parser", - "stream" - ], - "support": { - "issues": "https://github.com/amphp/parser/issues", - "source": "https://github.com/amphp/parser/tree/v1.1.1" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-03-21T19:16:53+00:00" - }, - { - "name": "amphp/pipeline", - "version": "v1.2.4", - "source": { - "type": "git", - "url": "https://github.com/amphp/pipeline.git", - "reference": "a044733e080940d1483f56caff0c412ad6982776" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", - "reference": "a044733e080940d1483f56caff0c412ad6982776", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "6.16.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Pipeline\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Asynchronous iterators and operators.", - "homepage": "https://amphp.org/pipeline", - "keywords": [ - "amp", - "amphp", - "async", - "io", - "iterator", - "non-blocking" - ], - "support": { - "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.4" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2026-05-06T05:37:57+00:00" - }, - { - "name": "amphp/process", - "version": "v2.0.3", - "source": { - "type": "git", - "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/sync": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Process\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "A fiber-aware process manager based on Amp and Revolt.", - "homepage": "https://amphp.org/process", - "support": { - "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-04-19T03:13:44+00:00" - }, - { - "name": "amphp/serialization", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/serialization.git", - "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", - "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", - "shasum": "" - }, - "require": { - "php": ">=7.4" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "ext-json": "*", - "ext-zlib": "*", - "phpunit/phpunit": "^9", - "psalm/phar": "6.16.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Serialization\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Serialization tools for IPC and data storage in PHP.", - "homepage": "https://github.com/amphp/serialization", - "keywords": [ - "async", - "asynchronous", - "serialization", - "serialize" - ], - "support": { - "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/v1.1.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2026-04-05T15:59:53+00:00" - }, - { - "name": "amphp/socket", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/socket.git", - "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", - "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/dns": "^2", - "ext-openssl": "*", - "kelunik/certificate": "^1.1", - "league/uri": "^7", - "league/uri-interfaces": "^7", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "amphp/process": "^2", - "phpunit/phpunit": "^9", - "psalm/phar": "6.16.1" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php", - "src/Internal/functions.php", - "src/SocketAddress/functions.php" - ], - "psr-4": { - "Amp\\Socket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", - "homepage": "https://github.com/amphp/socket", - "keywords": [ - "amp", - "async", - "encryption", - "non-blocking", - "sockets", - "tcp", - "tls" - ], - "support": { - "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2026-04-19T15:09:56+00:00" - }, - { - "name": "amphp/sync", - "version": "v2.3.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/sync.git", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", - "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/pipeline": "^1", - "amphp/serialization": "^1", - "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "5.23" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Sync\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Stephen Coakley", - "email": "me@stephencoakley.com" - } - ], - "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", - "homepage": "https://github.com/amphp/sync", - "keywords": [ - "async", - "asynchronous", - "mutex", - "semaphore", - "synchronization" - ], - "support": { - "issues": "https://github.com/amphp/sync/issues", - "source": "https://github.com/amphp/sync/tree/v2.3.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-08-03T19:31:26+00:00" - }, - { - "name": "amphp/websocket", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/amphp/websocket.git", - "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/websocket/zipball/963904b6a883c4b62d9222d1d9749814fac96a3b", - "reference": "963904b6a883c4b62d9222d1d9749814fac96a3b", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2", - "amphp/parser": "^1", - "amphp/pipeline": "^1", - "amphp/socket": "^2", - "php": ">=8.1", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "suggest": { - "ext-zlib": "Required for compression" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Amp\\Websocket\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - } - ], - "description": "Shared code for websocket servers and clients.", - "homepage": "https://github.com/amphp/websocket", - "keywords": [ - "amp", - "amphp", - "async", - "http", - "non-blocking", - "websocket" - ], - "support": { - "issues": "https://github.com/amphp/websocket/issues", - "source": "https://github.com/amphp/websocket/tree/v2.0.4" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2024-10-28T21:28:45+00:00" - }, - { - "name": "amphp/websocket-server", - "version": "v4.0.0", - "source": { - "type": "git", - "url": "https://github.com/amphp/websocket-server.git", - "reference": "d713c75b2625142dfa23c21b31d970c00a2ad789" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amphp/websocket-server/zipball/d713c75b2625142dfa23c21b31d970c00a2ad789", - "reference": "d713c75b2625142dfa23c21b31d970c00a2ad789", - "shasum": "" - }, - "require": { - "amphp/amp": "^3", - "amphp/byte-stream": "^2.1", - "amphp/http": "^2.1", - "amphp/http-server": "^3.2", - "amphp/socket": "^2.2", - "amphp/websocket": "^2", - "php": ">=8.1", - "psr/log": "^1|^2|^3", - "revolt/event-loop": "^1" - }, - "require-dev": { - "amphp/http-client": "^5", - "amphp/http-server-router": "^2", - "amphp/http-server-static-content": "^2", - "amphp/log": "^2", - "amphp/php-cs-fixer-config": "^2", - "amphp/phpunit-util": "^3", - "amphp/websocket-client": "^2", - "colinodell/psr-testlogger": "^1.2", - "league/climate": "^3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" - }, - "suggest": { - "ext-zlib": "Required for compression" - }, - "type": "library", - "autoload": { - "psr-4": { - "Amp\\Websocket\\Server\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Bob Weinand" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - } - ], - "description": "Websocket server for Amp's HTTP server.", - "homepage": "https://github.com/amphp/websocket-server", - "keywords": [ - "http", - "server", - "websocket" - ], - "support": { - "issues": "https://github.com/amphp/websocket-server/issues", - "source": "https://github.com/amphp/websocket-server/tree/v4.0.0" - }, - "funding": [ - { - "url": "https://github.com/amphp", - "type": "github" - } - ], - "time": "2023-12-29T00:58:49+00:00" - }, - { - "name": "daverandom/libdns", - "version": "v2.1.0", - "source": { - "type": "git", - "url": "https://github.com/DaveRandom/LibDNS.git", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "Required for IDN support" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "LibDNS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "DNS protocol implementation written in pure PHP", - "keywords": [ - "dns" - ], - "support": { - "issues": "https://github.com/DaveRandom/LibDNS/issues", - "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" - }, - "time": "2024-04-12T12:12:48+00:00" - }, - { - "name": "guzzlehttp/psr7", - "version": "2.9.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2026-03-10T16:41:02+00:00" - }, - { - "name": "kelunik/certificate", - "version": "v1.1.3", - "source": { - "type": "git", - "url": "https://github.com/kelunik/certificate.git", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", - "shasum": "" - }, - "require": { - "ext-openssl": "*", - "php": ">=7.0" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "^2", - "phpunit/phpunit": "^6 | 7 | ^8 | ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Kelunik\\Certificate\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Access certificate details and transform between different formats.", - "keywords": [ - "DER", - "certificate", - "certificates", - "openssl", - "pem", - "x509" - ], - "support": { - "issues": "https://github.com/kelunik/certificate/issues", - "source": "https://github.com/kelunik/certificate/tree/v1.1.3" - }, - "time": "2023-02-03T21:26:53+00:00" - }, - { - "name": "league/uri", - "version": "7.8.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri.git", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", - "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", - "shasum": "" - }, - "require": { - "league/uri-interfaces": "^7.8.1", - "php": "^8.1", - "psr/http-factory": "^1" - }, - "conflict": { - "league/uri-schemes": "^1.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-dom": "to convert the URI into an HTML anchor tag", - "ext-fileinfo": "to create Data URI from file contennts", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", - "league/uri-components": "to provide additional tools to manipulate URI objects components", - "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "URI manipulation library", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "URN", - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "middleware", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc2141", - "rfc3986", - "rfc3987", - "rfc6570", - "rfc8141", - "uri", - "uri-template", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.1" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-03-15T20:22:25+00:00" - }, - { - "name": "league/uri-components", - "version": "7.8.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri-components.git", - "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-components/zipball/848ff9db2f0be06229d6034b7c2e33d41b4fd675", - "reference": "848ff9db2f0be06229d6034b7c2e33d41b4fd675", - "shasum": "" - }, - "require": { - "league/uri": "^7.8.1", - "php": "^8.1" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-fileinfo": "to create Data URI from file contennts", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "ext-mbstring": "to use the sorting algorithm of URLSearchParams", - "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", - "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "URI components manipulation library", - "homepage": "http://uri.thephpleague.com", - "keywords": [ - "authority", - "components", - "fragment", - "host", - "middleware", - "modifier", - "path", - "port", - "query", - "rfc3986", - "scheme", - "uri", - "url", - "userinfo" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-components/tree/7.8.1" - }, - "funding": [ - { - "url": "https://github.com/nyamsprod", - "type": "github" - } - ], - "time": "2026-03-15T20:22:25+00:00" - }, - { - "name": "league/uri-interfaces", - "version": "7.8.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", - "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^8.1", - "psr/http-message": "^1.1 || ^2.0" - }, - "suggest": { - "ext-bcmath": "to improve IPV4 host parsing", - "ext-gmp": "to improve IPV4 host parsing", - "ext-intl": "to handle IDN host with the best performance", - "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", - "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Uri\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ignace Nyamagana Butera", - "email": "nyamsprod@gmail.com", - "homepage": "https://nyamsprod.com" - } - ], - "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", - "homepage": "https://uri.thephpleague.com", - "keywords": [ - "data-uri", - "file-uri", - "ftp", - "hostname", - "http", - "https", - "parse_str", - "parse_url", - "psr-7", - "query-string", - "querystring", - "rfc3986", - "rfc3987", - "rfc6570", - "uri", - "url", - "ws" - ], - "support": { - "docs": "https://uri.thephpleague.com", - "forum": "https://thephpleague.slack.com", - "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" - }, - "funding": [ - { - "url": "https://github.com/sponsors/nyamsprod", - "type": "github" - } - ], - "time": "2026-03-08T20:05:35+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "ratchet/rfc6455", - "version": "v0.4.0", - "source": { - "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", - "shasum": "" - }, - "require": { - "php": ">=7.4", - "psr/http-factory-implementation": "^1.0", - "symfony/polyfill-php80": "^1.15" - }, - "require-dev": { - "guzzlehttp/psr7": "^2.7", - "phpunit/phpunit": "^9.5", - "react/socket": "^1.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Ratchet\\RFC6455\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" - } - ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", - "keywords": [ - "WebSockets", - "rfc6455", - "websocket" - ], - "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" - }, - "time": "2025-02-24T01:18:22+00:00" - }, - { - "name": "revolt/event-loop", - "version": "v1.0.8", - "source": { - "type": "git", - "url": "https://github.com/revoltphp/event-loop.git", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Revolt\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "ceesjank@gmail.com" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Rock-solid event loop for concurrent PHP applications.", - "keywords": [ - "async", - "asynchronous", - "concurrency", - "event", - "event-loop", - "non-blocking", - "scheduler" - ], - "support": { - "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" - }, - "time": "2025-08-27T21:33:23+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.37.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-04-10T16:19:22+00:00" - }, - { - "name": "workerman/coroutine", - "version": "v1.1.5", - "source": { - "type": "git", - "url": "https://github.com/workerman-php/coroutine.git", - "reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/workerman-php/coroutine/zipball/b60e44267b90d398dbfa7a320f3e97b46357ac9f", - "reference": "b60e44267b90d398dbfa7a320f3e97b46357ac9f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "workerman/workerman": "^5.1" - }, - "require-dev": { - "phpunit/phpunit": "^11.0", - "psr/log": "*" - }, - "type": "library", - "autoload": { - "psr-4": { - "Workerman\\": "src", - "Workerman\\Coroutine\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Workerman coroutine", - "support": { - "issues": "https://github.com/workerman-php/coroutine/issues", - "source": "https://github.com/workerman-php/coroutine/tree/v1.1.5" - }, - "time": "2026-03-12T02:07:37+00:00" - }, - { - "name": "workerman/workerman", - "version": "v5.2.0", - "source": { - "type": "git", - "url": "https://github.com/walkor/workerman.git", - "reference": "1d8694c945bc64a5bc11ad753ec7220bcba37cb1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/walkor/workerman/zipball/1d8694c945bc64a5bc11ad753ec7220bcba37cb1", - "reference": "1d8694c945bc64a5bc11ad753ec7220bcba37cb1", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=8.1", - "workerman/coroutine": "^1.1 || dev-main" - }, - "conflict": { - "ext-swow": "=8.1" - }, - "platform-dev": {}, - "plugin-api-version": "2.9.0" -} diff --git a/bench/message-runtime/amphp.php b/bench/message-runtime/amphp.php deleted file mode 100644 index 2ef0b5e..0000000 --- a/bench/message-runtime/amphp.php +++ /dev/null @@ -1,23 +0,0 @@ - $phpArgs - * @param list $scriptArgs - */ -function runMessageRuntimeBenchmark( - string $adapterName, - string $adapterVersion, - string $serverFile, - array $phpArgs = [], - array $scriptArgs = [], -): void { - if (!function_exists('proc_open')) { - fwrite(STDERR, "proc_open() is required for the message runtime benchmark.\n"); - exit(1); - } - - if (!function_exists('stream_socket_server') || !function_exists('stream_socket_client')) { - fwrite(STDERR, "PHP stream sockets are required for the message runtime benchmark.\n"); - exit(1); - } - - if (!is_file($serverFile)) { - fwrite(STDERR, "Benchmark server file not found: {$serverFile}\n"); - exit(1); - } - - $arguments = $_SERVER['argv'] ?? []; - if (!is_array($arguments)) { - $arguments = []; - } - - $connections = messageRuntimeArgumentInt($arguments, 1, 50); - $messages = messageRuntimeArgumentInt($arguments, 2, 1000); - $rounds = messageRuntimeArgumentInt($arguments, 3, 3); - $transport = messageRuntimeArgumentString($arguments, 4, 'ws'); - $payloadBytes = messageRuntimeArgumentInt($arguments, 5, 64); - if ($connections <= 0 || $messages <= 0 || $rounds <= 0 || $payloadBytes <= 0) { - fwrite(STDERR, "Connections, messages, rounds, and payload bytes must be positive integers.\n"); - exit(1); - } - - $transports = messageRuntimeTransports($transport); - if ($transports === []) { - fwrite(STDERR, "Transport must be ws, wss, or both.\n"); - exit(1); - } - - $workDir = dirname(__DIR__) . '/var/message-runtime-' . messageRuntimeSlug($adapterName) . '-' . getmypid(); - if (!is_dir($workDir) && !mkdir($workDir, 0777, true) && !is_dir($workDir)) { - fwrite(STDERR, "Unable to create benchmark directory {$workDir}.\n"); - exit(1); - } - - try { - $results = []; - foreach ($transports as $currentTransport) { - $results[$currentTransport] = [ - 'idle' => messageRuntimeAverageScenario($workDir, $currentTransport, 'idle', $rounds, $connections, $messages, $payloadBytes, $serverFile, $phpArgs, $scriptArgs), - 'echo' => messageRuntimeAverageScenario($workDir, $currentTransport, 'echo', $rounds, $connections, $messages, $payloadBytes, $serverFile, $phpArgs, $scriptArgs), - 'broadcast' => messageRuntimeAverageScenario($workDir, $currentTransport, 'broadcast', $rounds, $connections, $messages, $payloadBytes, $serverFile, $phpArgs, $scriptArgs), - ]; - } - } finally { - messageRuntimeRemoveWorkDir($workDir); - } - - printf("Library: %s %s\n", $adapterName, $adapterVersion); - printf("Connections: %d\n", $connections); - printf("Messages: %d\n", $messages); - printf("Payload: %d bytes\n", $payloadBytes); - printf("Transport: %s\n", implode(',', $transports)); - printf("Rounds: %d\n\n", $rounds); - - foreach ($transports as $currentTransport) { - if (count($transports) > 1) { - printf("[%s]\n", $currentTransport); - } - - if (!isset($results[$currentTransport])) { - throw new RuntimeException("Missing benchmark results for {$currentTransport}"); - } - - $transportResults = $results[$currentTransport]; - messageRuntimePrintMetric('idle upgraded connections', $transportResults['idle'], 'connections'); - messageRuntimePrintMetric('echo pipelined messages', $transportResults['echo'], 'messages'); - messageRuntimePrintMetric('broadcast fanout deliveries', $transportResults['broadcast'], 'deliveries'); - - if (count($transports) > 1) { - echo "\n"; - } - } -} - -/** - * @param array $arguments - */ -function messageRuntimeArgumentInt(array $arguments, int $index, int $default): int -{ - if (!isset($arguments[$index])) { - return $default; - } - - if (!is_numeric($arguments[$index])) { - return 0; - } - - return (int) $arguments[$index]; -} - -/** - * @param array $arguments - */ -function messageRuntimeArgumentString(array $arguments, int $index, string $default): string -{ - if (!isset($arguments[$index])) { - return $default; - } - - if (!is_scalar($arguments[$index])) { - return ''; - } - - return strtolower((string) $arguments[$index]); -} - -/** - * @return list<'ws'|'wss'> - */ -function messageRuntimeTransports(string $transport): array -{ - return match ($transport) { - 'ws' => ['ws'], - 'wss' => ['wss'], - 'both' => ['ws', 'wss'], - default => [], - }; -} - -/** - * @param list $phpArgs - * @param list $scriptArgs - * @return array{operations: int, serverElapsed: float, clientElapsed: float} - */ -function messageRuntimeAverageScenario( - string $workDir, - string $transport, - string $scenario, - int $rounds, - int $connections, - int $messages, - int $payloadBytes, - string $serverFile, - array $phpArgs, - array $scriptArgs, -): array { - $operations = 0; - $serverElapsed = 0.0; - $clientElapsed = 0.0; - - for ($round = 1; $round <= $rounds; $round++) { - $result = messageRuntimeRunOnce($workDir, $transport, $scenario, $round, $connections, $messages, $payloadBytes, $serverFile, $phpArgs, $scriptArgs); - $operations = $result['operations']; - $serverElapsed += $result['serverElapsed']; - $clientElapsed += $result['clientElapsed']; - } - - return [ - 'operations' => $operations, - 'serverElapsed' => $serverElapsed / $rounds, - 'clientElapsed' => $clientElapsed / $rounds, - ]; -} - -/** - * @param list $phpArgs - * @param list $scriptArgs - * @return array{operations: int, serverElapsed: float, clientElapsed: float} - */ -function messageRuntimeRunOnce( - string $workDir, - string $transport, - string $scenario, - int $round, - int $connections, - int $messages, - int $payloadBytes, - string $serverFile, - array $phpArgs, - array $scriptArgs, -): array { - $roundDir = $workDir . '/' . $transport . '-' . $scenario . '-round-' . $round; - if (!is_dir($roundDir) && !mkdir($roundDir, 0777, true) && !is_dir($roundDir)) { - throw new RuntimeException("Unable to create benchmark round directory {$roundDir}"); - } - - $resultFile = $roundDir . '/result.json'; - $stdoutFile = $roundDir . '/stdout.log'; - $stderrFile = $roundDir . '/stderr.log'; - $backendPort = messageRuntimeAllocateTcpPort(); - $clientPort = $backendPort; - $environment = [ - 'WEBSOCKET_BENCH_PORT' => (string) $backendPort, - 'WEBSOCKET_BENCH_CONNECTIONS' => (string) $connections, - 'WEBSOCKET_BENCH_MESSAGES' => (string) $messages, - 'WEBSOCKET_BENCH_PAYLOAD_BYTES' => (string) $payloadBytes, - 'WEBSOCKET_BENCH_SCENARIO' => $scenario, - 'WEBSOCKET_BENCH_RESULT_FILE' => $resultFile, - 'WEBSOCKET_BENCH_ROUND_DIR' => $roundDir, - ]; - - $process = messageRuntimeStartProcess($phpArgs, $scriptArgs, $serverFile, $stdoutFile, $stderrFile, $environment); - $proxyProcess = null; - $clients = []; - - try { - if ($transport === 'wss') { - $clientPort = messageRuntimeAllocateTcpPort(); - $proxyProcess = messageRuntimeStartTlsProxy($roundDir, $backendPort, $clientPort, $stdoutFile . '.tls', $stderrFile . '.tls'); - } - - $clients = messageRuntimeConnectClients($clientPort, $connections, $process, $transport); - $clientStart = hrtime(true); - - $operations = match ($scenario) { - 'idle' => messageRuntimeRunIdle($clients), - 'echo' => messageRuntimeRunEcho($clients, $messages, $payloadBytes), - 'broadcast' => messageRuntimeRunBroadcast($clients, $messages, $payloadBytes), - default => throw new RuntimeException("Unsupported message runtime scenario {$scenario}"), - }; - - $clientElapsed = (hrtime(true) - $clientStart) / 1e9; - messageRuntimeWaitForResult($process, $resultFile, 15.0); - $result = messageRuntimeReadResult($resultFile, $scenario); - - return [ - 'operations' => $result['operations'] > 0 ? $result['operations'] : $operations, - 'serverElapsed' => $result['serverElapsed'], - 'clientElapsed' => $clientElapsed, - ]; - } catch (Throwable $exception) { - messageRuntimePrintProcessLogs($stdoutFile, $stderrFile); - if ($proxyProcess !== null) { - messageRuntimePrintProcessLogs($stdoutFile . '.tls', $stderrFile . '.tls'); - } - throw $exception; - } finally { - messageRuntimeCloseClients($clients); - if ($proxyProcess !== null) { - messageRuntimeStopProcess($proxyProcess); - } - messageRuntimeStopProcess($process); - } -} - -function messageRuntimeAllocateTcpPort(): int -{ - $errno = 0; - $error = ''; - $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $error); - if ($server === false) { - throw new RuntimeException("Unable to allocate TCP port: {$error}", is_int($errno) ? $errno : 0); - } - - $name = stream_socket_get_name($server, false); - fclose($server); - - if ($name === false) { - throw new RuntimeException('Unable to read allocated TCP port'); - } - - $separator = strrpos($name, ':'); - if ($separator === false) { - throw new RuntimeException("Unable to parse allocated TCP address {$name}"); - } - - return (int) substr($name, $separator + 1); -} - -/** - * @return resource - */ -function messageRuntimeStartTlsProxy(string $roundDir, int $backendPort, int $frontendPort, string $stdoutFile, string $stderrFile) -{ - [$certificateFile, $keyFile] = messageRuntimeCreateTlsCertificate($roundDir); - - return messageRuntimeStartProcess( - ['-d', 'xdebug.mode=off'], - [], - __DIR__ . '/tls_proxy.php', - $stdoutFile, - $stderrFile, - [ - 'WEBSOCKET_BENCH_TLS_PORT' => (string) $frontendPort, - 'WEBSOCKET_BENCH_BACKEND_PORT' => (string) $backendPort, - 'WEBSOCKET_BENCH_CERT_FILE' => $certificateFile, - 'WEBSOCKET_BENCH_KEY_FILE' => $keyFile, - ], - ); -} - -/** - * @return array{0: string, 1: string} - */ -function messageRuntimeCreateTlsCertificate(string $roundDir): array -{ - if (!function_exists('openssl_pkey_new') || !function_exists('openssl_csr_new') || !function_exists('openssl_csr_sign')) { - throw new RuntimeException('The openssl extension is required for the wss message runtime benchmark.'); - } - - $key = openssl_pkey_new([ - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - if ($key === false) { - throw new RuntimeException('Unable to create benchmark TLS key.'); - } - - $csr = openssl_csr_new([ - 'commonName' => '127.0.0.1', - ], $key, [ - 'digest_alg' => 'sha256', - ]); - if ($csr === false || $csr === true) { - throw new RuntimeException('Unable to create benchmark TLS CSR.'); - } - - $certificate = openssl_csr_sign($csr, null, $key, 1, [ - 'digest_alg' => 'sha256', - ]); - if ($certificate === false) { - throw new RuntimeException('Unable to sign benchmark TLS certificate.'); - } - - $certificatePem = ''; - $keyPem = ''; - if (!openssl_x509_export($certificate, $certificatePem) || !openssl_pkey_export($key, $keyPem)) { - throw new RuntimeException('Unable to export benchmark TLS certificate.'); - } - - $certificateFile = $roundDir . '/tls.crt'; - $keyFile = $roundDir . '/tls.key'; - if (file_put_contents($certificateFile, $certificatePem) === false || file_put_contents($keyFile, $keyPem) === false) { - throw new RuntimeException('Unable to write benchmark TLS certificate.'); - } - - return [$certificateFile, $keyFile]; -} - -/** - * @param list $phpArgs - * @param list $scriptArgs - * @param array $environment - * @return resource - */ -function messageRuntimeStartProcess(array $phpArgs, array $scriptArgs, string $serverFile, string $stdoutFile, string $stderrFile, array $environment) -{ - $pipes = []; - - $process = proc_open( - [PHP_BINARY, ...$phpArgs, $serverFile, ...$scriptArgs], - [ - 0 => ['file', '/dev/null', 'r'], - 1 => ['file', $stdoutFile, 'w'], - 2 => ['file', $stderrFile, 'w'], - ], - $pipes, - null, - array_replace(getenv(), $environment), - ); - - if (!is_resource($process)) { - throw new RuntimeException('Unable to start benchmark server process'); - } - - return $process; -} - -/** - * @param resource $process - * @return list - */ -function messageRuntimeConnectClients(int $port, int $connections, $process, string $transport): array -{ - $clients = []; - - for ($i = 0; $i < $connections; $i++) { - $clients[] = [ - 'stream' => messageRuntimeConnectOnce($port, $i === 0 ? 5.0 : 1.0, $process, $transport), - 'buffer' => '', - ]; - } - - return $clients; -} - -/** - * @param resource $process - * @return resource - */ -function messageRuntimeConnectOnce(int $port, float $timeout, $process, string $transport) -{ - $deadline = microtime(true) + $timeout; - $errno = 0; - $error = ''; - - do { - $context = null; - $socketUrl = 'tcp://127.0.0.1:' . $port; - if ($transport === 'wss') { - $socketUrl = 'tls://127.0.0.1:' . $port; - $context = stream_context_create([ - 'ssl' => [ - 'allow_self_signed' => true, - 'verify_peer' => false, - 'verify_peer_name' => false, - ], - ]); - } - - $client = @stream_socket_client($socketUrl, $errno, $error, 0.1, STREAM_CLIENT_CONNECT, $context); - if ($client !== false) { - stream_set_timeout($client, 2); - $request = implode("\r\n", [ - 'GET / HTTP/1.1', - 'Host: 127.0.0.1:' . $port, - 'Upgrade: websocket', - 'Connection: Upgrade', - 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version: 13', - '', - '', - ]); - - fwrite($client, $request); - messageRuntimeReadHandshake($client); - - return $client; - } - - $status = proc_get_status($process); - if (!$status['running']) { - throw new RuntimeException('Benchmark server stopped before accepting all message runtime connections'); - } - - usleep(1000); - } while (microtime(true) < $deadline); - - throw new RuntimeException("Unable to connect to benchmark server: {$error}", is_int($errno) ? $errno : 0); -} - -/** - * @param resource $client - */ -function messageRuntimeReadHandshake($client): void -{ - $buffer = ''; - $deadline = microtime(true) + 5.0; - - do { - $chunk = fread($client, 4096); - if (is_string($chunk) && $chunk !== '') { - $buffer .= $chunk; - if (str_contains($buffer, "\r\n\r\n")) { - if (!str_starts_with($buffer, 'HTTP/1.1 101')) { - throw new RuntimeException('Benchmark server did not complete WebSocket upgrade'); - } - - return; - } - } - - usleep(1000); - } while (microtime(true) < $deadline); - - throw new RuntimeException('Timed out waiting for WebSocket upgrade response'); -} - -/** - * @param list $clients - */ -function messageRuntimeRunIdle(array $clients): int -{ - usleep(100000); - - return count($clients); -} - -/** - * @param list $clients - */ -function messageRuntimeRunEcho(array &$clients, int $messages, int $payloadBytes): int -{ - return messageRuntimeRunPipelinedMessages($clients, $messages, $payloadBytes, 1); -} - -/** - * @param list $clients - */ -function messageRuntimeRunBroadcast(array &$clients, int $messages, int $payloadBytes): int -{ - return messageRuntimeRunPipelinedMessages($clients, $messages, $payloadBytes, count($clients)); -} - -/** - * @param list $clients - */ -function messageRuntimeRunPipelinedMessages(array &$clients, int $messages, int $payloadBytes, int $deliveriesPerMessage): int -{ - $count = count($clients); - $payload = messageRuntimePayload($payloadBytes); - $frame = messageRuntimeClientFrame($payload); - $expectedDeliveries = $messages * $deliveriesPerMessage; - $maxOutstandingDeliveries = max($deliveriesPerMessage, min($expectedDeliveries, $deliveriesPerMessage * 64)); - $sentMessages = 0; - $receivedDeliveries = 0; - $writeBuffers = array_fill(0, $count, ''); - $streamIndexes = []; - $deadline = microtime(true) + max(15.0, $expectedDeliveries / 1000.0); - - for ($i = 0; $i < $count; $i++) { - stream_set_blocking($clients[$i]['stream'], false); - $streamIndexes[messageRuntimeStreamId($clients[$i]['stream'])] = $i; - } - - while ($receivedDeliveries < $expectedDeliveries) { - while ($sentMessages < $messages && ($sentMessages * $deliveriesPerMessage) - $receivedDeliveries < $maxOutstandingDeliveries) { - $sender = $sentMessages % $count; - $writeBuffers[$sender] .= $frame; - $sentMessages++; - } - - $readStreams = []; - foreach ($clients as $client) { - $readStreams[] = $client['stream']; - } - - $writeStreams = []; - foreach ($writeBuffers as $index => $buffer) { - if ($buffer !== '') { - $writeStreams[] = $clients[$index]['stream']; - } - } - - $readReady = $readStreams; - $writeReady = $writeStreams !== [] ? $writeStreams : null; - $exceptReady = null; - $ready = @stream_select($readReady, $writeReady, $exceptReady, 0, 10000); - if ($ready === false) { - throw new RuntimeException('Unable to wait for benchmark client sockets'); - } - - if ($ready === 0) { - if (microtime(true) > $deadline) { - throw new RuntimeException('Timed out waiting for pipelined WebSocket benchmark frames'); - } - continue; - } - - if ($writeReady !== null) { - foreach ($writeReady as $stream) { - $index = $streamIndexes[messageRuntimeStreamId($stream)] ?? null; - if ($index === null || $writeBuffers[$index] === '') { - continue; - } - - $written = @fwrite($stream, $writeBuffers[$index]); - if ($written === false) { - throw new RuntimeException('Unable to write WebSocket benchmark frame'); - } - - if ($written > 0) { - $writeBuffers[$index] = substr($writeBuffers[$index], $written); - } - } - } - - foreach ($readReady as $stream) { - $index = $streamIndexes[messageRuntimeStreamId($stream)] ?? null; - if ($index === null) { - continue; - } - - $chunk = @fread($stream, 65536); - if ($chunk === false) { - throw new RuntimeException('Unable to read WebSocket benchmark frame'); - } - - if ($chunk === '') { - if (feof($stream)) { - throw new RuntimeException('Benchmark connection closed while reading pipelined frames'); - } - continue; - } - - $buffer = $clients[$index]['buffer'] . $chunk; - while (($response = messageRuntimeTryReadFrame($buffer)) !== null) { - if ($response !== $payload) { - throw new RuntimeException('Unexpected pipelined benchmark payload'); - } - - $receivedDeliveries++; - if ($receivedDeliveries > $expectedDeliveries) { - throw new RuntimeException('Benchmark received more frames than expected'); - } - } - - $clients[$index] = [ - 'stream' => $clients[$index]['stream'], - 'buffer' => $buffer, - ]; - } - } - - return $expectedDeliveries; -} - -/** - * @param resource $stream - */ -function messageRuntimeStreamId($stream): int -{ - return (int) $stream; -} - -function messageRuntimeClientFrame(string $payload): string -{ - $length = strlen($payload); - $mask = "\x12\x34\x56\x78"; - $header = "\x81"; - - if ($length < 126) { - $header .= chr(0x80 | $length); - } elseif ($length <= 0xffff) { - $header .= chr(0x80 | 126) . pack('n', $length); - } else { - $header .= chr(0x80 | 127) . pack('NN', 0, $length); - } - - return $header . $mask . messageRuntimeMaskPayload($payload, $mask); -} - -function messageRuntimeMaskPayload(string $payload, string $mask): string -{ - $masked = ''; - $length = strlen($payload); - - for ($i = 0; $i < $length; $i++) { - $masked .= $payload[$i] ^ $mask[$i % 4]; - } - - return $masked; -} - -/** - * @param resource $client - */ -function messageRuntimeReadFrame($client, string &$buffer): string -{ - $deadline = microtime(true) + 5.0; - - do { - $payload = messageRuntimeTryReadFrame($buffer); - if ($payload !== null) { - return $payload; - } - - $chunk = fread($client, 4096); - if (is_string($chunk) && $chunk !== '') { - $buffer .= $chunk; - continue; - } - - usleep(1000); - } while (microtime(true) < $deadline); - - throw new RuntimeException('Timed out waiting for WebSocket benchmark frame'); -} - -function messageRuntimeTryReadFrame(string &$buffer): ?string -{ - $length = strlen($buffer); - if ($length < 2) { - return null; - } - - $pos = 2; - $opcode = ord($buffer[0]) & 0x0f; - $masked = (ord($buffer[1]) & 0x80) !== 0; - $payloadLength = ord($buffer[1]) & 0x7f; - - if ($payloadLength === 126) { - if ($length < 4) { - return null; - } - $unpacked = unpack('nlength', substr($buffer, 2, 2)); - if ($unpacked === false) { - throw new RuntimeException('Unable to parse WebSocket benchmark frame length'); - } - $payloadLength = $unpacked['length']; - $pos = 4; - } elseif ($payloadLength === 127) { - if ($length < 10) { - return null; - } - $parts = unpack('Nhigh/Nlow', substr($buffer, 2, 8)); - if ($parts === false) { - throw new RuntimeException('Unable to parse WebSocket benchmark frame length'); - } - $payloadLength = ((int) $parts['high'] << 32) | (int) $parts['low']; - $pos = 10; - } - - $mask = ''; - if ($masked) { - if ($length < $pos + 4) { - return null; - } - $mask = substr($buffer, $pos, 4); - $pos += 4; - } - - if ($length < $pos + $payloadLength) { - return null; - } - - $payload = substr($buffer, $pos, $payloadLength); - $buffer = substr($buffer, $pos + $payloadLength); - - if ($opcode === 0x8) { - throw new RuntimeException('Benchmark connection closed by server'); - } - - if ($masked) { - $payload = messageRuntimeMaskPayload($payload, $mask); - } - - return $payload; -} - -function messageRuntimePayload(int $size): string -{ - return substr(str_repeat('abcdefghijklmnopqrstuvwxyz0123456789', intdiv($size, 36) + 1), 0, $size); -} - -/** - * @param resource $process - */ -function messageRuntimeWaitForResult($process, string $resultFile, float $timeout): void -{ - $deadline = microtime(true) + $timeout; - - do { - if (is_file($resultFile) && filesize($resultFile) !== 0) { - return; - } - - usleep(10000); - } while (microtime(true) < $deadline); - - $status = proc_get_status($process); - if (!$status['running']) { - throw new RuntimeException(sprintf( - 'Benchmark server stopped without writing a result file (exitcode=%d, signaled=%s, termsig=%d)', - $status['exitcode'], - $status['signaled'] ? 'yes' : 'no', - $status['termsig'], - )); - } - - throw new RuntimeException('Benchmark server did not write result before timeout'); -} - -/** - * @return array{operations: int, serverElapsed: float} - */ -function messageRuntimeReadResult(string $resultFile, string $expectedScenario): array -{ - if (!is_file($resultFile)) { - throw new RuntimeException('Benchmark server did not write a result file'); - } - - $json = file_get_contents($resultFile); - if ($json === false) { - throw new RuntimeException('Unable to read benchmark result file'); - } - - $result = json_decode($json, true, flags: JSON_THROW_ON_ERROR); - if (!is_array($result)) { - throw new RuntimeException('Invalid benchmark result payload'); - } - - $scenario = $result['scenario'] ?? null; - $operations = $result['operations'] ?? null; - $serverElapsed = $result['serverElapsed'] ?? null; - if ($scenario !== $expectedScenario || !is_int($operations) || (!is_int($serverElapsed) && !is_float($serverElapsed))) { - throw new RuntimeException('Invalid benchmark result shape'); - } - - return [ - 'operations' => $operations, - 'serverElapsed' => (float) $serverElapsed, - ]; -} - -function messageRuntimePrintProcessLogs(string $stdoutFile, string $stderrFile): void -{ - $stdout = is_file($stdoutFile) ? trim((string) file_get_contents($stdoutFile)) : ''; - $stderr = is_file($stderrFile) ? trim((string) file_get_contents($stderrFile)) : ''; - - if ($stdout !== '') { - fwrite(STDERR, "\n--- server stdout ---\n{$stdout}\n"); - } - - if ($stderr !== '') { - fwrite(STDERR, "\n--- server stderr ---\n{$stderr}\n"); - } -} - -/** - * @param list $clients - */ -function messageRuntimeCloseClients(array $clients): void -{ - foreach ($clients as $client) { - fclose($client['stream']); - } -} - -/** - * @param resource $process - */ -function messageRuntimeStopProcess($process): void -{ - $status = proc_get_status($process); - if ($status['running']) { - proc_terminate($process); - } - - proc_close($process); -} - -/** - * @param array{operations: int, serverElapsed: float, clientElapsed: float} $result - */ -function messageRuntimePrintMetric(string $name, array $result, string $unit): void -{ - printf( - "%s: %d %s avg in %.4fs server / %.4fs client (%.0f %s/sec server, %.0f %s/sec client)\n", - $name, - $result['operations'], - $unit, - $result['serverElapsed'], - $result['clientElapsed'], - $result['operations'] / max($result['serverElapsed'], 0.000001), - $unit, - $result['operations'] / max($result['clientElapsed'], 0.000001), - $unit, - ); -} - -function messageRuntimeSlug(string $name): string -{ - return trim((string) preg_replace('/[^a-z0-9]+/', '-', strtolower($name)), '-'); -} - -function messageRuntimeRemoveWorkDir(string $workDir): void -{ - if (!is_dir($workDir)) { - return; - } - - $entries = scandir($workDir); - if ($entries === false) { - @rmdir($workDir); - return; - } - - foreach ($entries as $entry) { - if ($entry === '.' || $entry === '..') { - continue; - } - - $path = $workDir . DIRECTORY_SEPARATOR . $entry; - if (is_dir($path) && !is_link($path)) { - messageRuntimeRemoveWorkDir($path); - continue; - } - - @unlink($path); - } - - @rmdir($workDir); -} diff --git a/bench/message-runtime/openswoole.php b/bench/message-runtime/openswoole.php deleted file mode 100644 index 81f8d8e..0000000 --- a/bench/message-runtime/openswoole.php +++ /dev/null @@ -1,26 +0,0 @@ - */ - private array $clients = []; - - private int $opened = 0; - - private int $messages = 0; - - private int|float|null $idleStartedAt = null; - - private int|float|null $messageStartedAt = null; - - public function __construct( - private readonly string $scenario, - private readonly int $connectionsExpected, - private readonly int $messagesExpected, - ) { - } - - public function handleClient(WebsocketClient $client, Request $request, Response $response): void - { - if ($this->idleStartedAt === null) { - $this->idleStartedAt = hrtime(true); - } - - $this->clients[$client->getId()] = $client; - $this->opened++; - - if ($this->scenario === 'idle' && $this->opened >= $this->connectionsExpected) { - messageRuntimeBenchmarkWriteResult('idle', $this->opened, messageRuntimeBenchmarkElapsed($this->idleStartedAt)); - } - - while ($message = $client->receive()) { - $payload = $message->buffer(); - - if ($this->messageStartedAt === null) { - $this->messageStartedAt = hrtime(true); - } - - if ($this->scenario === 'broadcast') { - foreach ($this->clients as $peer) { - $peer->sendText($payload); - } - - $this->messages++; - if ($this->messages >= $this->messagesExpected) { - messageRuntimeBenchmarkWriteResult('broadcast', $this->messages * $this->connectionsExpected, messageRuntimeBenchmarkElapsed($this->messageStartedAt)); - } - - continue; - } - - $client->sendText($payload); - $this->messages++; - - if ($this->scenario === 'echo' && $this->messages >= $this->messagesExpected) { - messageRuntimeBenchmarkWriteResult('echo', $this->messages, messageRuntimeBenchmarkElapsed($this->messageStartedAt)); - } - } - - unset($this->clients[$client->getId()]); - } -}; - -$websocket = new Websocket( - httpServer: $server, - logger: $logger, - acceptor: new Rfc6455Acceptor(), - clientHandler: $handler, - clientFactory: new Rfc6455ClientFactory( - heartbeatQueue: null, - rateLimit: null, - parserFactory: new Rfc6455ParserFactory( - validateUtf8: true, - messageSizeLimit: PHP_INT_MAX, - frameSizeLimit: PHP_INT_MAX, - ), - ), -); - -$server->expose(new InternetAddress('127.0.0.1', messageRuntimeBenchmarkInternetPort())); -$server->start($websocket, new DefaultErrorHandler()); - -Amp\trapSignal([SIGTERM, SIGINT]); -$server->stop(); diff --git a/bench/message-runtime/servers/bootstrap.php b/bench/message-runtime/servers/bootstrap.php deleted file mode 100644 index 25ef512..0000000 --- a/bench/message-runtime/servers/bootstrap.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ -function messageRuntimeBenchmarkInternetPort(): int -{ - $port = messageRuntimeBenchmarkPort(); - if ($port < 1 || $port > 65535) { - fwrite(STDERR, "WEBSOCKET_BENCH_PORT must be between 1 and 65535.\n"); - exit(1); - } - - return $port; -} - -function messageRuntimeBenchmarkConnections(): int -{ - return messageRuntimeBenchmarkEnvInt('WEBSOCKET_BENCH_CONNECTIONS'); -} - -function messageRuntimeBenchmarkMessages(): int -{ - return messageRuntimeBenchmarkEnvInt('WEBSOCKET_BENCH_MESSAGES'); -} - -function messageRuntimeBenchmarkScenario(): string -{ - $scenario = messageRuntimeBenchmarkEnv('WEBSOCKET_BENCH_SCENARIO'); - if (!in_array($scenario, ['idle', 'echo', 'broadcast'], true)) { - fwrite(STDERR, "Unsupported WEBSOCKET_BENCH_SCENARIO {$scenario}.\n"); - exit(1); - } - - return $scenario; -} - -function messageRuntimeBenchmarkResultFile(): string -{ - return messageRuntimeBenchmarkEnv('WEBSOCKET_BENCH_RESULT_FILE'); -} - -function messageRuntimeBenchmarkRoundDir(): string -{ - return messageRuntimeBenchmarkEnv('WEBSOCKET_BENCH_ROUND_DIR'); -} - -function messageRuntimeBenchmarkWriteResult(string $scenario, int $operations, float $serverElapsed): void -{ - $resultFile = messageRuntimeBenchmarkResultFile(); - if (is_file($resultFile) && filesize($resultFile) !== 0) { - return; - } - - $tmpFile = $resultFile . '.' . getmypid() . '.tmp'; - file_put_contents($tmpFile, json_encode([ - 'scenario' => $scenario, - 'operations' => $operations, - 'serverElapsed' => $serverElapsed, - ], JSON_THROW_ON_ERROR)); - rename($tmpFile, $resultFile); -} - -function messageRuntimeBenchmarkElapsed(int|float|null $startedAt): float -{ - return $startedAt === null ? 0.0 : (hrtime(true) - $startedAt) / 1e9; -} diff --git a/bench/message-runtime/servers/openswoole.php b/bench/message-runtime/servers/openswoole.php deleted file mode 100644 index 040a74d..0000000 --- a/bench/message-runtime/servers/openswoole.php +++ /dev/null @@ -1,75 +0,0 @@ -set([ - 'worker_num' => 1, - 'log_file' => '/dev/null', -]); - -$server->on('Open', static function (Server $server, Request $request) use ($scenario, $connectionsExpected, &$connections, &$opened, &$idleStartedAt): void { - if ($idleStartedAt === null) { - $idleStartedAt = hrtime(true); - } - - $connections[$request->fd] = $request->fd; - $opened++; - - if ($scenario === 'idle' && $opened >= $connectionsExpected) { - messageRuntimeBenchmarkWriteResult('idle', $opened, messageRuntimeBenchmarkElapsed($idleStartedAt)); - } -}); - -$server->on('Message', static function (Server $server, Frame $frame) use ($scenario, $connectionsExpected, $messagesExpected, &$connections, &$messages, &$messageStartedAt): void { - if ($messageStartedAt === null) { - $messageStartedAt = hrtime(true); - } - - if ($scenario === 'broadcast') { - foreach ($connections as $fd) { - $server->push($fd, $frame->data); - } - - $messages++; - if ($messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('broadcast', $messages * $connectionsExpected, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } - - return; - } - - $server->push($frame->fd, $frame->data); - $messages++; - - if ($scenario === 'echo' && $messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('echo', $messages, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } -}); - -$server->on('Close', static function (Server $server, int $fd) use (&$connections): void { - unset($connections[$fd]); -}); - -$server->start(); diff --git a/bench/message-runtime/servers/websocket.php b/bench/message-runtime/servers/websocket.php deleted file mode 100644 index 5266953..0000000 --- a/bench/message-runtime/servers/websocket.php +++ /dev/null @@ -1,73 +0,0 @@ - 64 * 1024 * 1024]); -$server->listen('127.0.0.1', messageRuntimeBenchmarkInternetPort()); - -$server->onOpen(static function (Connection $connection) use ($scenario, $connectionsExpected, &$connections, &$opened, &$idleStartedAt): void { - if ($idleStartedAt === null) { - $idleStartedAt = hrtime(true); - } - - $connections[$connection->id] = $connection; - $opened++; - - if ($scenario === 'idle' && $opened >= $connectionsExpected) { - messageRuntimeBenchmarkWriteResult('idle', $opened, messageRuntimeBenchmarkElapsed($idleStartedAt)); - } -}); - -$server->onMessage(static function (Connection $connection, string $message) use ($scenario, $connectionsExpected, $messagesExpected, &$connections, &$messages, &$messageStartedAt): void { - if ($messageStartedAt === null) { - $messageStartedAt = hrtime(true); - } - - if ($scenario === 'broadcast') { - foreach ($connections as $client) { - if ($client->isOpen()) { - $client->send($message); - } - } - - $messages++; - if ($messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('broadcast', $messages * $connectionsExpected, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } - - return; - } - - $connection->send($message); - $messages++; - - if ($scenario === 'echo' && $messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('echo', $messages, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } -}); - -$server->onClose(static function (Connection $connection) use (&$connections): void { - unset($connections[$connection->id]); -}); - -$server->run(); diff --git a/bench/message-runtime/servers/workerman.php b/bench/message-runtime/servers/workerman.php deleted file mode 100644 index 63829fd..0000000 --- a/bench/message-runtime/servers/workerman.php +++ /dev/null @@ -1,78 +0,0 @@ -count = 1; - -$worker->onWebSocketConnected = static function (TcpConnection $connection) use ($scenario, $connectionsExpected, &$connections, &$opened, &$idleStartedAt): void { - if ($idleStartedAt === null) { - $idleStartedAt = hrtime(true); - } - - $connections[$connection->id] = $connection; - $opened++; - - if ($scenario === 'idle' && $opened >= $connectionsExpected) { - messageRuntimeBenchmarkWriteResult('idle', $opened, messageRuntimeBenchmarkElapsed($idleStartedAt)); - } -}; - -$worker->onMessage = static function (TcpConnection $connection, string $message) use ($scenario, $connectionsExpected, $messagesExpected, &$connections, &$messages, &$messageStartedAt): void { - if ($messageStartedAt === null) { - $messageStartedAt = hrtime(true); - } - - if ($scenario === 'broadcast') { - foreach ($connections as $client) { - $client->send($message); - } - - $messages++; - if ($messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('broadcast', $messages * $connectionsExpected, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } - - return; - } - - $connection->send($message); - $messages++; - - if ($scenario === 'echo' && $messages >= $messagesExpected) { - messageRuntimeBenchmarkWriteResult('echo', $messages, messageRuntimeBenchmarkElapsed($messageStartedAt)); - } -}; - -$worker->onClose = static function (TcpConnection $connection) use (&$connections): void { - unset($connections[$connection->id]); -}; - -Worker::runAll(); diff --git a/bench/message-runtime/tls_proxy.php b/bench/message-runtime/tls_proxy.php deleted file mode 100644 index 9a501b6..0000000 --- a/bench/message-runtime/tls_proxy.php +++ /dev/null @@ -1,216 +0,0 @@ - $streams - * @param array $peers - * @param array $writeBuffers - */ -function tlsProxyClose($stream, array &$streams, array &$peers, array &$writeBuffers): void -{ - $id = tlsProxyStreamId($stream); - $peerId = $peers[$id] ?? null; - - unset($streams[$id], $peers[$id], $writeBuffers[$id]); - @fclose($stream); - - if ($peerId !== null && isset($streams[$peerId])) { - $peer = $streams[$peerId]; - unset($streams[$peerId], $peers[$peerId], $writeBuffers[$peerId]); - @fclose($peer); - } -} - -/** - * @param array $streams - * @param array $peers - * @param array $writeBuffers - */ -function tlsProxyFlushWritable(array &$streams, array &$peers, array &$writeBuffers): void -{ - $write = []; - foreach ($writeBuffers as $id => $buffer) { - if ($buffer !== '' && isset($streams[$id])) { - $write[] = $streams[$id]; - } - } - - if ($write === []) { - return; - } - - $read = null; - $except = null; - $ready = @stream_select($read, $write, $except, 0, 0); - if ($ready === false || $ready <= 0) { - return; - } - - foreach ($write as $stream) { - $id = tlsProxyStreamId($stream); - $buffer = $writeBuffers[$id] ?? ''; - if ($buffer === '') { - continue; - } - - $written = @fwrite($stream, $buffer); - if ($written === false || $written === 0) { - tlsProxyClose($stream, $streams, $peers, $writeBuffers); - continue; - } - - $writeBuffers[$id] = substr($buffer, $written); - } -} - -$frontendPort = tlsProxyEnvInt('WEBSOCKET_BENCH_TLS_PORT'); -$backendPort = tlsProxyEnvInt('WEBSOCKET_BENCH_BACKEND_PORT'); -$certificateFile = tlsProxyEnv('WEBSOCKET_BENCH_CERT_FILE'); -$keyFile = tlsProxyEnv('WEBSOCKET_BENCH_KEY_FILE'); - -$context = stream_context_create([ - 'ssl' => [ - 'local_cert' => $certificateFile, - 'local_pk' => $keyFile, - 'allow_self_signed' => true, - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -]); - -$errno = 0; -$error = ''; -$server = @stream_socket_server( - 'tcp://127.0.0.1:' . $frontendPort, - $errno, - $error, - STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, - $context, -); -if ($server === false) { - tlsProxyFail("Unable to start TLS benchmark proxy: {$error}"); -} - -stream_set_blocking($server, false); - -/** @var array $streams */ -$streams = []; -/** @var array $peers */ -$peers = []; -/** @var array $writeBuffers */ -$writeBuffers = []; - -while (getenv('WEBSOCKET_BENCH_TLS_PROXY_STOP') !== '1') { - $read = [$server, ...array_values($streams)]; - $write = null; - $except = null; - $ready = @stream_select($read, $write, $except, 0, 10000); - if ($ready === false) { - continue; - } - - if ($ready > 0) { - foreach ($read as $stream) { - if ($stream === $server) { - $client = @stream_socket_accept($server, 0); - if ($client === false) { - continue; - } - stream_set_blocking($client, true); - if (@stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER) !== true) { - @fclose($client); - continue; - } - - $backend = @stream_socket_client('tcp://127.0.0.1:' . $backendPort, $errno, $error, 1.0); - if ($backend === false) { - @fclose($client); - continue; - } - - stream_set_blocking($client, false); - stream_set_blocking($backend, false); - - $clientId = tlsProxyStreamId($client); - $backendId = tlsProxyStreamId($backend); - $streams[$clientId] = $client; - $streams[$backendId] = $backend; - $peers[$clientId] = $backendId; - $peers[$backendId] = $clientId; - $writeBuffers[$clientId] = ''; - $writeBuffers[$backendId] = ''; - continue; - } - - $id = tlsProxyStreamId($stream); - if (!isset($streams[$id])) { - continue; - } - - $chunk = @fread($stream, 16384); - if ($chunk === false || $chunk === '') { - if (feof($stream)) { - tlsProxyClose($stream, $streams, $peers, $writeBuffers); - } - continue; - } - - $peerId = $peers[$id] ?? null; - if ($peerId === null || !isset($streams[$peerId])) { - tlsProxyClose($stream, $streams, $peers, $writeBuffers); - continue; - } - - $writeBuffers[$peerId] .= $chunk; - } - } - - tlsProxyFlushWritable($streams, $peers, $writeBuffers); -} diff --git a/bench/message-runtime/websocket.php b/bench/message-runtime/websocket.php deleted file mode 100644 index 458b7a5..0000000 --- a/bench/message-runtime/websocket.php +++ /dev/null @@ -1,34 +0,0 @@ -payload = $data; - } - } -}; -$parser = new Rfc6455Parser( - frameHandler: $handler, - masked: false, - validateUtf8: false, -); - -$adapterName = 'amphp/websocket-server'; -$adapterVersion = Composer\InstalledVersions::getPrettyVersion('amphp/websocket-server') ?? 'installed'; -$encode = static fn (string $payload): string => $compiler->compileFrame(WebsocketFrameType::Text, $payload, true); -$decode = static function (string $frame) use ($parser, $handler): string { - $parser->push($frame); - - return $handler->payload; -}; - -require __DIR__ . '/common.php'; -runBenchmarkSuite($adapterName, $adapterVersion, $encode, $decode); diff --git a/bench/protocol/common.php b/bench/protocol/common.php deleted file mode 100644 index 3842a16..0000000 --- a/bench/protocol/common.php +++ /dev/null @@ -1,122 +0,0 @@ - $arguments - */ -function benchmarkIterations(array $arguments): int -{ - if (!isset($arguments[1])) { - return 100000; - } - - if (!is_numeric($arguments[1])) { - return 0; - } - - return (int) $arguments[1]; -} - -/** - * @param Closure(string): string $callback - */ -function runBenchmark(string $name, string $subject, int $iterations, Closure $callback): void -{ - $warmup = min(10000, max(100, intdiv($iterations, 20))); - $sink = 0; - - for ($i = 0; $i < $warmup; $i++) { - $sink += strlen($callback($subject)); - } - - $start = hrtime(true); - for ($i = 0; $i < $iterations; $i++) { - $sink += strlen($callback($subject)); - } - $elapsed = (hrtime(true) - $start) / 1e9; - - printf( - "%s: %d ops in %.4fs (%.0f ops/sec)\n", - $name, - $iterations, - $elapsed, - $iterations / $elapsed - ); - - if ($sink === 0) { - fwrite(STDERR, "Unexpected empty benchmark result.\n"); - } -} - -function makePayload(int $size): string -{ - return substr(str_repeat('abcdefghijklmnopqrstuvwxyz0123456789', intdiv($size, 36) + 1), 0, $size); -} - -function clientFrame(string $payload): string -{ - $length = strlen($payload); - $mask = "\x12\x34\x56\x78"; - $header = "\x81"; - - if ($length < 126) { - $header .= chr(0x80 | $length); - } elseif ($length <= 0xffff) { - $header .= chr(0x80 | 126) . pack('n', $length); - } else { - $header .= chr(0x80 | 127) . pack('NN', 0, $length); - } - - return $header . $mask . maskPayload($payload, $mask); -} - -function maskPayload(string $payload, string $mask): string -{ - $masked = ''; - $length = strlen($payload); - - for ($i = 0; $i < $length; $i++) { - $masked .= $payload[$i] ^ $mask[$i % 4]; - } - - return $masked; -} diff --git a/bench/protocol/openswoole.php b/bench/protocol/openswoole.php deleted file mode 100644 index 4efb0ae..0000000 --- a/bench/protocol/openswoole.php +++ /dev/null @@ -1,65 +0,0 @@ - $serverClass::pack($payload, $opcodeText, $flagFin); -$decode = static function (string $frame) use ($serverClass): string { - $decoded = $serverClass::unpack($frame); - if ($decoded === false) { - throw new RuntimeException('OpenSwoole failed to unpack the WebSocket frame'); - } - - return $decoded->data; -}; - -require __DIR__ . '/common.php'; -runBenchmarkSuite($adapterName, $adapterVersion, $encode, $decode); - -function websocketServerConstant(string $class, string $classConstant, string $globalConstant, int $fallback): int -{ - $constant = $class . '::' . $classConstant; - if (defined($constant)) { - $value = constant($constant); - if (is_int($value)) { - return $value; - } - } - - if (defined($globalConstant)) { - $value = constant($globalConstant); - if (is_int($value)) { - return $value; - } - } - - return $fallback; -} diff --git a/bench/protocol/ratchet.php b/bench/protocol/ratchet.php deleted file mode 100644 index 5f74fbf..0000000 --- a/bench/protocol/ratchet.php +++ /dev/null @@ -1,34 +0,0 @@ -getContents(); -}; -$decode = static function (string $frame): string { - $decoded = new Ratchet\RFC6455\Messaging\Frame(); - $decoded->addBuffer($frame); - - return $decoded->getPayload(); -}; - -require __DIR__ . '/common.php'; -runBenchmarkSuite($adapterName, $adapterVersion, $encode, $decode); diff --git a/bench/protocol/websocket.php b/bench/protocol/websocket.php deleted file mode 100644 index 341f805..0000000 --- a/bench/protocol/websocket.php +++ /dev/null @@ -1,36 +0,0 @@ - Protocol::encode($payload); -$decode = static function (string $frame): string { - $decoded = Protocol::decode($frame); - if (!$decoded instanceof WebSocket\Frame) { - throw new RuntimeException('The websocket extension did not decode a data frame'); - } - - return $decoded->payload; -}; - -require __DIR__ . '/common.php'; -runBenchmarkSuite($adapterName, $adapterVersion, $encode, $decode); diff --git a/bench/protocol/workerman.php b/bench/protocol/workerman.php deleted file mode 100644 index a214e38..0000000 --- a/bench/protocol/workerman.php +++ /dev/null @@ -1,104 +0,0 @@ - Websocket::encode($payload, $connection); -$decode = static function (string $frame) use ($connection): string { - $context = workermanContext($connection); - $context->websocketCurrentFrameLength = 0; - $context->websocketDataBuffer = ''; - - return Websocket::decode($frame, $connection); -}; - -require __DIR__ . '/common.php'; -runBenchmarkSuite($adapterName, $adapterVersion, $encode, $decode); - -function makeWorkermanConnection(): TcpConnection -{ - $loop = new class implements EventInterface { - /** - * @param callable(mixed...): void $func - * @param array $args - */ - public function delay(float $delay, callable $func, array $args = []): int { return 0; } - public function offDelay(int $timerId): bool { return true; } - /** - * @param callable(mixed...): void $func - * @param array $args - */ - public function repeat(float $interval, callable $func, array $args = []): int { return 0; } - public function offRepeat(int $timerId): bool { return true; } - /** - * @param resource $stream - * @param callable(resource): void $func - */ - public function onReadable($stream, callable $func): void {} - /** - * @param resource $stream - */ - public function offReadable($stream): bool { return true; } - /** - * @param resource $stream - * @param callable(resource): void $func - */ - public function onWritable($stream, callable $func): void {} - /** - * @param resource $stream - */ - public function offWritable($stream): bool { return true; } - /** - * @param callable(int): void $func - */ - public function onSignal(int $signal, callable $func): void {} - public function offSignal(int $signal): bool { return true; } - public function deleteAllTimer(): void {} - public function run(): void {} - public function stop(): void {} - public function getTimerCount(): int { return 0; } - /** - * @param callable(\Throwable): void $errorHandler - */ - public function setErrorHandler(callable $errorHandler): void {} - }; - - $stream = fopen('php://temp', 'r+'); - if ($stream === false) { - throw new RuntimeException('Unable to open temporary stream for Workerman benchmark'); - } - - $connection = new TcpConnection($loop, $stream); - $context = workermanContext($connection); - $context->websocketHandshake = true; - $context->websocketCurrentFrameLength = 0; - $context->websocketDataBuffer = ''; - $connection->websocketType = Websocket::BINARY_TYPE_BLOB; - - return $connection; -} - -function workermanContext(TcpConnection $connection): \stdClass -{ - if (!$connection->context instanceof \stdClass) { - $connection->context = new \stdClass(); - } - - return $connection->context; -} diff --git a/bench/server-runtime/amphp.php b/bench/server-runtime/amphp.php deleted file mode 100644 index 1bc3cbb..0000000 --- a/bench/server-runtime/amphp.php +++ /dev/null @@ -1,25 +0,0 @@ - $phpArgs - * @param list $scriptArgs - */ -function runServerAcceptBenchmark( - string $adapterName, - string $adapterVersion, - string $serverFile, - array $phpArgs = [], - array $scriptArgs = [], - bool $clientUpgrade = false, -): void -{ - if (!function_exists('proc_open')) { - fwrite(STDERR, "proc_open() is required for the server accept benchmark.\n"); - exit(1); - } - - if (!function_exists('stream_socket_server') || !function_exists('stream_socket_client')) { - fwrite(STDERR, "PHP stream sockets are required for the server accept benchmark.\n"); - exit(1); - } - - if (!is_file($serverFile)) { - fwrite(STDERR, "Benchmark server file not found: {$serverFile}\n"); - exit(1); - } - - $arguments = $_SERVER['argv'] ?? []; - if (!is_array($arguments)) { - $arguments = []; - } - - $connections = serverAcceptConnections($arguments); - if ($connections <= 0) { - fwrite(STDERR, "Connections must be a positive integer.\n"); - exit(1); - } - - $rounds = serverAcceptRounds($arguments); - if ($rounds <= 0) { - fwrite(STDERR, "Rounds must be a positive integer.\n"); - exit(1); - } - - $workDir = dirname(__DIR__) . '/var/server-accept-' . serverAcceptSlug($adapterName) . '-' . getmypid(); - if (!is_dir($workDir) && !mkdir($workDir, 0777, true) && !is_dir($workDir)) { - fwrite(STDERR, "Unable to create benchmark directory {$workDir}.\n"); - exit(1); - } - - $accepted = 0; - $connected = 0; - $serverElapsed = 0.0; - $clientElapsed = 0.0; - - try { - for ($round = 1; $round <= $rounds; $round++) { - $result = serverAcceptRunOnce($workDir, $round, $connections, $serverFile, $phpArgs, $scriptArgs, $clientUpgrade); - $accepted = $result['accepted']; - $connected = $result['connected']; - $serverElapsed += $result['serverElapsed']; - $clientElapsed += $result['clientElapsed']; - } - } finally { - serverAcceptRemoveWorkDir($workDir); - } - - $serverElapsed /= $rounds; - $clientElapsed /= $rounds; - - $serverBenchmark = $clientUpgrade ? 'websocket upgrade/close' : 'tcp accept/close'; - $clientBenchmark = $clientUpgrade ? 'client upgrade loop' : 'client connect loop'; - - printf("Library: %s %s\n", $adapterName, $adapterVersion); - printf("Connections: %d\n", $connections); - printf("Rounds: %d\n\n", $rounds); - printf( - "%s: %d connections avg in %.4fs (%.0f connections/sec)\n", - $serverBenchmark, - $accepted, - $serverElapsed, - $accepted / max($serverElapsed, 0.000001) - ); - printf( - "%s: %d connections avg in %.4fs (%.0f connections/sec)\n", - $clientBenchmark, - $connected, - $clientElapsed, - $connected / max($clientElapsed, 0.000001) - ); -} - -/** - * @param array $arguments - */ -function serverAcceptConnections(array $arguments): int -{ - if (!isset($arguments[1])) { - return 1000; - } - - if (!is_numeric($arguments[1])) { - return 0; - } - - return (int) $arguments[1]; -} - -/** - * @param array $arguments - */ -function serverAcceptRounds(array $arguments): int -{ - if (!isset($arguments[2])) { - return 3; - } - - if (!is_numeric($arguments[2])) { - return 0; - } - - return (int) $arguments[2]; -} - -/** - * @param list $phpArgs - * @param list $scriptArgs - * @return array{accepted: int, connected: int, serverElapsed: float, clientElapsed: float} - */ -function serverAcceptRunOnce( - string $workDir, - int $round, - int $connections, - string $serverFile, - array $phpArgs, - array $scriptArgs, - bool $clientUpgrade, -): array { - $roundDir = $workDir . '/round-' . $round; - if (!is_dir($roundDir) && !mkdir($roundDir, 0777, true) && !is_dir($roundDir)) { - throw new RuntimeException("Unable to create benchmark round directory {$roundDir}"); - } - - $resultFile = $roundDir . '/result.json'; - $stdoutFile = $roundDir . '/stdout.log'; - $stderrFile = $roundDir . '/stderr.log'; - $port = serverAcceptAllocateTcpPort(); - $environment = [ - 'WEBSOCKET_BENCH_PORT' => (string) $port, - 'WEBSOCKET_BENCH_CONNECTIONS' => (string) $connections, - 'WEBSOCKET_BENCH_RESULT_FILE' => $resultFile, - 'WEBSOCKET_BENCH_ROUND_DIR' => $roundDir, - ]; - - $process = serverAcceptStartProcess($phpArgs, $scriptArgs, $serverFile, $stdoutFile, $stderrFile, $environment); - $clientStart = hrtime(true); - $connected = 0; - - try { - for ($i = 0; $i < $connections; $i++) { - serverAcceptConnectOnce($port, $i === 0 ? 5.0 : 1.0, $process, $clientUpgrade); - $connected++; - } - - serverAcceptWaitForResult($process, $resultFile, 10.0); - } catch (Throwable $exception) { - serverAcceptPrintProcessLogs($stdoutFile, $stderrFile); - throw $exception; - } finally { - serverAcceptStopProcess($process); - } - - $clientElapsed = (hrtime(true) - $clientStart) / 1e9; - $result = serverAcceptReadResult($resultFile); - - return [ - 'accepted' => $result['accepted'], - 'connected' => $connected, - 'serverElapsed' => $result['serverElapsed'], - 'clientElapsed' => $clientElapsed, - ]; -} - -function serverAcceptAllocateTcpPort(): int -{ - $errno = 0; - $error = ''; - $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $error); - if ($server === false) { - throw new RuntimeException("Unable to allocate TCP port: {$error}", is_int($errno) ? $errno : 0); - } - - $name = stream_socket_get_name($server, false); - fclose($server); - - if ($name === false) { - throw new RuntimeException('Unable to read allocated TCP port'); - } - - $separator = strrpos($name, ':'); - if ($separator === false) { - throw new RuntimeException("Unable to parse allocated TCP address {$name}"); - } - - return (int) substr($name, $separator + 1); -} - -/** - * @param list $phpArgs - * @param list $scriptArgs - * @param array $environment - * @return resource - */ -function serverAcceptStartProcess(array $phpArgs, array $scriptArgs, string $serverFile, string $stdoutFile, string $stderrFile, array $environment) -{ - $pipes = []; - - $process = proc_open( - [PHP_BINARY, ...$phpArgs, $serverFile, ...$scriptArgs], - [ - 0 => ['file', '/dev/null', 'r'], - 1 => ['file', $stdoutFile, 'w'], - 2 => ['file', $stderrFile, 'w'], - ], - $pipes, - null, - array_replace(getenv(), $environment), - ); - - if (!is_resource($process)) { - throw new RuntimeException('Unable to start benchmark server process'); - } - - return $process; -} - -/** - * @param resource $process - */ -function serverAcceptConnectOnce(int $port, float $timeout, $process, bool $clientUpgrade): void -{ - $deadline = microtime(true) + $timeout; - $errno = 0; - $error = ''; - - do { - $client = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $error, 0.1); - if ($client !== false) { - if ($clientUpgrade) { - $request = implode("\r\n", [ - 'GET / HTTP/1.1', - 'Host: 127.0.0.1:' . $port, - 'Upgrade: websocket', - 'Connection: Upgrade', - 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version: 13', - '', - '', - ]); - - fwrite($client, $request); - stream_set_timeout($client, 1); - $response = fread($client, 4096); - if (!is_string($response) || !str_contains($response, "HTTP/1.1 101 Switching Protocols\r\n")) { - fclose($client); - throw new RuntimeException('Benchmark server did not complete WebSocket upgrade'); - } - } - - fclose($client); - return; - } - - $status = proc_get_status($process); - if (!$status['running']) { - throw new RuntimeException('Benchmark server stopped before accepting all connections'); - } - - usleep(1000); - } while (microtime(true) < $deadline); - - throw new RuntimeException("Unable to connect to benchmark server: {$error}", is_int($errno) ? $errno : 0); -} - -/** - * @param resource $process - */ -function serverAcceptWaitForResult($process, string $resultFile, float $timeout): void -{ - $deadline = microtime(true) + $timeout; - - do { - if (is_file($resultFile)) { - return; - } - - usleep(10000); - } while (microtime(true) < $deadline); - - $status = proc_get_status($process); - if (!$status['running']) { - throw new RuntimeException(sprintf( - 'Benchmark server stopped without writing a result file (exitcode=%d, signaled=%s, termsig=%d)', - $status['exitcode'], - $status['signaled'] ? 'yes' : 'no', - $status['termsig'], - )); - } - - throw new RuntimeException('Benchmark server did not stop before timeout'); -} - -function serverAcceptPrintProcessLogs(string $stdoutFile, string $stderrFile): void -{ - $stdout = is_file($stdoutFile) ? trim((string) file_get_contents($stdoutFile)) : ''; - $stderr = is_file($stderrFile) ? trim((string) file_get_contents($stderrFile)) : ''; - - if ($stdout !== '') { - fwrite(STDERR, "\n--- server stdout ---\n{$stdout}\n"); - } - - if ($stderr !== '') { - fwrite(STDERR, "\n--- server stderr ---\n{$stderr}\n"); - } -} - -/** - * @param resource $process - */ -function serverAcceptStopProcess($process): void -{ - $status = proc_get_status($process); - if ($status['running']) { - proc_terminate($process); - } - - proc_close($process); -} - -/** - * @return array{accepted: int, serverElapsed: float} - */ -function serverAcceptReadResult(string $resultFile): array -{ - if (!is_file($resultFile)) { - throw new RuntimeException('Benchmark server did not write a result file'); - } - - $json = file_get_contents($resultFile); - if ($json === false) { - throw new RuntimeException('Unable to read benchmark result file'); - } - - $result = json_decode($json, true, flags: JSON_THROW_ON_ERROR); - if (!is_array($result)) { - throw new RuntimeException('Invalid benchmark result payload'); - } - - $accepted = $result['accepted'] ?? null; - $serverElapsed = $result['serverElapsed'] ?? null; - if (!is_int($accepted) || (!is_int($serverElapsed) && !is_float($serverElapsed))) { - throw new RuntimeException('Invalid benchmark result shape'); - } - - return [ - 'accepted' => $accepted, - 'serverElapsed' => (float) $serverElapsed, - ]; -} - -function serverAcceptSlug(string $name): string -{ - return trim((string) preg_replace('/[^a-z0-9]+/', '-', strtolower($name)), '-'); -} - -function serverAcceptRemoveWorkDir(string $workDir): void -{ - if (!is_dir($workDir)) { - return; - } - - $entries = scandir($workDir); - if ($entries === false) { - @rmdir($workDir); - return; - } - - foreach ($entries as $entry) { - if ($entry === '.' || $entry === '..') { - continue; - } - - $path = $workDir . DIRECTORY_SEPARATOR . $entry; - if (is_dir($path) && !is_link($path)) { - serverAcceptRemoveWorkDir($path); - continue; - } - - @unlink($path); - } - - @rmdir($workDir); -} diff --git a/bench/server-runtime/openswoole.php b/bench/server-runtime/openswoole.php deleted file mode 100644 index c309b2d..0000000 --- a/bench/server-runtime/openswoole.php +++ /dev/null @@ -1,26 +0,0 @@ -close(); - - if ($accepted >= $connections) { - serverAcceptBenchmarkWriteResult($accepted, (hrtime(true) - $startedAt) / 1e9); - } -}; - -while ($accepted < $connections) { - $socket = $server->accept(); - if ($socket === null) { - break; - } - - $onOpen($socket); -} - -$server->close(); - -serverAcceptBenchmarkWriteFallbackResult($accepted, $startedAt); diff --git a/bench/server-runtime/servers/bootstrap.php b/bench/server-runtime/servers/bootstrap.php deleted file mode 100644 index eae6c74..0000000 --- a/bench/server-runtime/servers/bootstrap.php +++ /dev/null @@ -1,70 +0,0 @@ - $accepted, - 'serverElapsed' => $serverElapsed, - ], JSON_THROW_ON_ERROR)); -} - -function serverAcceptBenchmarkWriteFallbackResult(int $accepted, ?int $startedAt): void -{ - $resultFile = serverAcceptBenchmarkResultFile(); - if (is_file($resultFile)) { - return; - } - - $elapsed = $startedAt === null ? 0.0 : (hrtime(true) - $startedAt) / 1e9; - serverAcceptBenchmarkWriteResult($accepted, $elapsed); -} diff --git a/bench/server-runtime/servers/openswoole.php b/bench/server-runtime/servers/openswoole.php deleted file mode 100644 index 5865203..0000000 --- a/bench/server-runtime/servers/openswoole.php +++ /dev/null @@ -1,43 +0,0 @@ -set([ - 'worker_num' => 1, - 'log_file' => '/dev/null', -]); - -$accepted = 0; -$startedAt = null; - -$server->on('Receive', static function (Server $server, int $fd, int $reactorId, string $data): void { -}); - -$server->on('Connect', static function (Server $server, int $fd) use ($connections, &$accepted, &$startedAt): void { - if ($startedAt === null) { - $startedAt = hrtime(true); - } - - $accepted++; - $server->close($fd); - - if ($accepted >= $connections) { - serverAcceptBenchmarkWriteResult($accepted, (hrtime(true) - $startedAt) / 1e9); - $server->shutdown(); - } -}); - -$server->start(); diff --git a/bench/server-runtime/servers/websocket.php b/bench/server-runtime/servers/websocket.php deleted file mode 100644 index 5821e40..0000000 --- a/bench/server-runtime/servers/websocket.php +++ /dev/null @@ -1,39 +0,0 @@ -listen('127.0.0.1', serverAcceptBenchmarkPort()); - -$connections = serverAcceptBenchmarkConnections(); -$accepted = 0; -$startedAt = null; - -$server->onOpen(static function () use ($server, $connections, &$accepted, &$startedAt): bool { - if ($startedAt === null) { - $startedAt = hrtime(true); - } - - $accepted++; - - if ($accepted >= $connections) { - serverAcceptBenchmarkWriteResult($accepted, (hrtime(true) - $startedAt) / 1e9); - $server->stop(); - } - - return false; -}); - -$server->run(); - -serverAcceptBenchmarkWriteFallbackResult($accepted, $startedAt); diff --git a/bench/server-runtime/servers/workerman.php b/bench/server-runtime/servers/workerman.php deleted file mode 100644 index 692b9bf..0000000 --- a/bench/server-runtime/servers/workerman.php +++ /dev/null @@ -1,45 +0,0 @@ -count = 1; - -$accepted = 0; -$startedAt = null; - -$worker->onConnect = static function (TcpConnection $connection) use ($connections, &$accepted, &$startedAt): void { - if ($startedAt === null) { - $startedAt = hrtime(true); - } - - $accepted++; - $connection->close(); - - if ($accepted >= $connections) { - serverAcceptBenchmarkWriteResult($accepted, (hrtime(true) - $startedAt) / 1e9); - Worker::stopAll(); - } -}; - -Worker::runAll(); diff --git a/bench/server-runtime/websocket.php b/bench/server-runtime/websocket.php deleted file mode 100644 index 91e6342..0000000 --- a/bench/server-runtime/websocket.php +++ /dev/null @@ -1,36 +0,0 @@ - $settings - */ - public function set(array $settings): void {} - - public function on(string $event, callable $callback): void {} - - public function push(int $fd, string $data): bool {} - - public function start(): bool {} - - public static function pack(string $data, int $opcode = self::WEBSOCKET_OPCODE_TEXT, int $flags = self::WEBSOCKET_FLAG_FIN): string {} - - public static function unpack(string $data): Frame|false {} -} - -namespace OpenSwoole\Http; - -class Request -{ - public int $fd = 0; -} - -namespace Swoole\WebSocket; - -class Frame extends \OpenSwoole\WebSocket\Frame -{ -} - -class Server extends \OpenSwoole\WebSocket\Server -{ -} diff --git a/php_websocket.h b/php_websocket.h index fd01cc8..e5971cd 100644 --- a/php_websocket.h +++ b/php_websocket.h @@ -88,6 +88,7 @@ ZEND_TSRMLS_CACHE_EXTERN() typedef struct _websocket_server_object { zval options; + zval subprotocols; zval on_open; zval on_message; zval on_close; @@ -110,6 +111,7 @@ typedef struct _websocket_server_object { typedef struct _websocket_connection_object { zend_string *id; zend_string *remote_address; + zend_string *selected_subprotocol; struct sockaddr_storage remote_addr; socklen_t remote_addr_len; uint64_t numeric_id; @@ -186,8 +188,9 @@ bool websocket_protocol_close_code_is_valid(zend_long code); bool websocket_protocol_is_valid_utf8(const char *payload, size_t payload_len); zend_string *websocket_protocol_pack_payload(zend_string *payload, uint8_t opcode, uint8_t flags); zend_string *websocket_protocol_close_payload(zend_long code, zend_string *reason); -websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, size_t len, zend_string **accept_key, size_t *bytes_consumed); -zend_string *websocket_http_upgrade_response(zend_string *accept_key); +websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, size_t len, HashTable *supported_subprotocols, zend_string **accept_key, zend_string **selected_subprotocol, size_t *bytes_consumed); +zend_string *websocket_http_upgrade_response(zend_string *accept_key, zend_string *selected_subprotocol); +bool websocket_http_validate_subprotocol_token(const char *value, size_t value_len); static zend_always_inline websocket_server_object *Z_WEBSOCKET_SERVER_P(zval *zv) { diff --git a/tests/001-contracts.phpt b/tests/001-contracts.phpt index d13a3b4..170255b 100644 --- a/tests/001-contracts.phpt +++ b/tests/001-contracts.phpt @@ -13,7 +13,10 @@ var_dump(class_exists(WebSocket\CloseFrame::class)); var_dump(class_exists(WebSocket\Protocol::class)); var_dump(method_exists(WebSocket\Server::class, 'send')); var_dump(method_exists(WebSocket\Server::class, 'close')); +var_dump(method_exists(WebSocket\Server::class, 'subprotocols')); +var_dump((new ReflectionMethod(WebSocket\Server::class, 'subprotocols'))->isVariadic()); var_dump((new ReflectionMethod(WebSocket\Connection::class, 'send'))->getNumberOfParameters()); +var_dump((new ReflectionProperty(WebSocket\Connection::class, 'subprotocol'))->getType()->allowsNull()); var_dump((new ReflectionMethod(WebSocket\ServerOptions::class, '__construct'))->getNumberOfParameters()); $options = new WebSocket\ServerOptions(maxMessageSize: 1024, maxQueuedBytes: 2048, maxConnections: 16, handshakeTimeoutMs: 250, idleTimeoutMs: 500); var_dump($options->maxMessageSize); @@ -31,6 +34,12 @@ var_dump((new ReflectionMethod(WebSocket\CloseFrame::class, '__construct'))->get $server = new WebSocket\Server(); $server->listen('127.0.0.1', 8080); +$server->subprotocols('chat.v1', 'superchat'); +try { + $server->subprotocols('bad protocol'); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} $server->onOpen(static function () {}); $server->onMessage(static function () {}); $server->onClose(static function () {}); @@ -47,7 +56,10 @@ bool(true) bool(true) bool(false) bool(false) +bool(true) +bool(true) int(2) +bool(true) int(5) int(1024) int(2048) @@ -57,4 +69,5 @@ int(500) WebSocket\ServerOptions::__construct(): Argument #1 ($maxMessageSize) must be at least 1 int(3) int(2) +WebSocket\Server::subprotocols(): Argument #1 must be a valid WebSocket subprotocol token bool(true) diff --git a/tests/015-server-subprotocols.phpt b/tests/015-server-subprotocols.phpt new file mode 100644 index 0000000..962d3c1 --- /dev/null +++ b/tests/015-server-subprotocols.phpt @@ -0,0 +1,185 @@ +--TEST-- +WebSocket\Server negotiates Sec-WebSocket-Protocol +--EXTENSIONS-- +websocket +--SKIPIF-- + +--FILE-- +listen('127.0.0.1', PORT_PLACEHOLDER); +$server->subprotocols('chat.v1', 'superchat'); + +$opened = 0; +$server->onOpen(static function (Connection $connection) use ($server, &$opened): void { + $opened++; + file_put_contents(EVENTS_PLACEHOLDER, ($connection->subprotocol ?? '(none)') . "\n", FILE_APPEND); + + if ($opened >= 2) { + $server->stop(); + } +}); + +$server->run(); +file_put_contents(EVENTS_PLACEHOLDER, "returned\n", FILE_APPEND); +PHP; + +$serverCode = str_replace( + ['PORT_PLACEHOLDER', 'EVENTS_PLACEHOLDER'], + [(string) $port, var_export($eventsFile, true)], + $serverCode, +); +file_put_contents($serverFile, $serverCode); + +$process = proc_open( + [PHP_BINARY, '-n', '-d', 'extension=' . $extension, $serverFile], + [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, +); + +if (!is_resource($process)) { + echo "cannot start server\n"; + exit; +} + +$connect = static function () use ($port, $process): mixed { + $client = false; + $deadline = microtime(true) + 5.0; + + do { + $client = @stream_socket_client('tcp://127.0.0.1:' . $port, $errno, $errstr, 0.1); + if ($client !== false) { + return $client; + } + + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + + usleep(10000); + } while (microtime(true) < $deadline); + + return false; +}; + +$handshake = static function (?string $protocolHeader) use ($connect, $port): string { + $client = $connect(); + if ($client === false) { + return ''; + } + + $lines = [ + 'GET /chat HTTP/1.1', + 'Host: 127.0.0.1:' . $port, + 'Upgrade: websocket', + 'Connection: Upgrade', + 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version: 13', + ]; + + if ($protocolHeader !== null) { + $lines[] = 'Sec-WebSocket-Protocol: ' . $protocolHeader; + } + + $lines[] = ''; + $lines[] = ''; + + fwrite($client, implode("\r\n", $lines)); + stream_set_timeout($client, 1); + $response = fread($client, 4096); + fclose($client); + + return $response; +}; + +$invalidResponse = $handshake('chat.v1, '); +$unmatchedResponse = $handshake('video'); +$matchedResponse = $handshake('video, superchat, chat.v1'); + +$deadline = microtime(true) + 5.0; +do { + $status = proc_get_status($process); + if (!$status['running']) { + break; + } + + usleep(10000); +} while (microtime(true) < $deadline); + +$status = proc_get_status($process); +if ($status['running']) { + proc_terminate($process); +} + +$stdout = stream_get_contents($pipes[1]); +$stderr = stream_get_contents($pipes[2]); +fclose($pipes[1]); +fclose($pipes[2]); +proc_close($process); + +$events = file_exists($eventsFile) ? file($eventsFile, FILE_IGNORE_NEW_LINES) : []; + +var_dump(str_contains($invalidResponse, "HTTP/1.1 400 Bad Request\r\n")); +var_dump(str_contains($unmatchedResponse, "HTTP/1.1 101 Switching Protocols\r\n")); +var_dump(! str_contains($unmatchedResponse, "Sec-WebSocket-Protocol:")); +var_dump(str_contains($matchedResponse, "HTTP/1.1 101 Switching Protocols\r\n")); +var_dump(str_contains($matchedResponse, "Sec-WebSocket-Protocol: superchat\r\n")); +var_dump($events); +var_dump($stdout === ''); +var_dump($stderr === ''); + +@unlink($eventsFile); +@unlink($serverFile); +@rmdir($tmpDir); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +array(3) { + [0]=> + string(6) "(none)" + [1]=> + string(9) "superchat" + [2]=> + string(8) "returned" +} +bool(true) +bool(true) diff --git a/websocket.stub.php b/websocket.stub.php index 737ab39..348f6f6 100644 --- a/websocket.stub.php +++ b/websocket.stub.php @@ -39,6 +39,17 @@ public function __construct(ServerOptions|array $options = []) {} */ public function listen(string $host, int $port): void {} + /** + * Configure server-supported WebSocket subprotocols. + * + * The HTTP Upgrade response selects the first client-offered token that is + * present in this list. Invalid or duplicate tokens are rejected. + * + * @throws \ValueError If a protocol is not a valid RFC token or appears more than once. + * @throws \Error If called while the server is running. + */ + public function subprotocols(string ...$protocols): void {} + /** * Register a callback called after a successful HTTP Upgrade. * @@ -166,6 +177,11 @@ final class Connection */ public readonly string $remoteAddress; + /** + * Selected WebSocket subprotocol, or null when none was negotiated. + */ + public readonly ?string $subprotocol; + /** * Send a text, binary, ping, pong, or close frame. * diff --git a/websocket_arginfo.h b/websocket_arginfo.h index 4256180..56ba822 100644 --- a/websocket_arginfo.h +++ b/websocket_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 0b1d9b784f9f267416396b70e5c2d1523bc87820 */ + * Stub hash: 3b15a096ef5cc7eadc01a0f97ff128ced381089e */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_WebSocket_Server___construct, 0, 0, 0) ZEND_ARG_OBJ_TYPE_MASK(0, options, WebSocket\\ServerOptions, MAY_BE_ARRAY, "[]") @@ -10,6 +10,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_listen, 0 ZEND_ARG_TYPE_INFO(0, port, IS_LONG, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_subprotocols, 0, 0, IS_VOID, 0) + ZEND_ARG_VARIADIC_TYPE_INFO(0, protocols, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_WebSocket_Server_onOpen, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handler, Closure, 0) ZEND_END_ARG_INFO() @@ -84,6 +88,7 @@ ZEND_END_ARG_INFO() ZEND_METHOD(WebSocket_Server, __construct); ZEND_METHOD(WebSocket_Server, listen); +ZEND_METHOD(WebSocket_Server, subprotocols); ZEND_METHOD(WebSocket_Server, onOpen); ZEND_METHOD(WebSocket_Server, onMessage); ZEND_METHOD(WebSocket_Server, onClose); @@ -106,6 +111,7 @@ ZEND_METHOD(WebSocket_Protocol, unpack); static const zend_function_entry class_WebSocket_Server_methods[] = { ZEND_ME(WebSocket_Server, __construct, arginfo_class_WebSocket_Server___construct, ZEND_ACC_PUBLIC) ZEND_ME(WebSocket_Server, listen, arginfo_class_WebSocket_Server_listen, ZEND_ACC_PUBLIC) + ZEND_ME(WebSocket_Server, subprotocols, arginfo_class_WebSocket_Server_subprotocols, ZEND_ACC_PUBLIC) ZEND_ME(WebSocket_Server, onOpen, arginfo_class_WebSocket_Server_onOpen, ZEND_ACC_PUBLIC) ZEND_ME(WebSocket_Server, onMessage, arginfo_class_WebSocket_Server_onMessage, ZEND_ACC_PUBLIC) ZEND_ME(WebSocket_Server, onClose, arginfo_class_WebSocket_Server_onClose, ZEND_ACC_PUBLIC) @@ -216,6 +222,12 @@ static zend_class_entry *register_class_WebSocket_Connection(void) zend_declare_typed_property(class_entry, property_remoteAddress_name, &property_remoteAddress_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); zend_string_release(property_remoteAddress_name); + zval property_subprotocol_default_value; + ZVAL_UNDEF(&property_subprotocol_default_value); + zend_string *property_subprotocol_name = zend_string_init("subprotocol", sizeof("subprotocol") - 1, 1); + zend_declare_typed_property(class_entry, property_subprotocol_name, &property_subprotocol_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); + zend_string_release(property_subprotocol_name); + return class_entry; } diff --git a/websocket_connection.c b/websocket_connection.c index 441fad0..d86c0ca 100644 --- a/websocket_connection.c +++ b/websocket_connection.c @@ -34,6 +34,7 @@ static zend_object *websocket_connection_create_object(zend_class_entry *ce) intern->id = NULL; intern->remote_address = NULL; + intern->selected_subprotocol = NULL; intern->remote_addr_len = 0; intern->numeric_id = 0; intern->fd = -1; @@ -383,9 +384,13 @@ void websocket_connection_open(websocket_connection_object *intern, uint64_t id, if (intern->remote_address) { zend_string_release(intern->remote_address); } + if (intern->selected_subprotocol) { + zend_string_release(intern->selected_subprotocol); + } intern->id = NULL; intern->remote_address = NULL; + intern->selected_subprotocol = NULL; intern->numeric_id = id; intern->has_remote_addr = false; intern->remote_addr_len = 0; @@ -424,6 +429,9 @@ static void websocket_connection_free_object(zend_object *object) if (intern->remote_address) { zend_string_release(intern->remote_address); } + if (intern->selected_subprotocol) { + zend_string_release(intern->selected_subprotocol); + } if (intern->read_buffer) { efree(intern->read_buffer); } @@ -451,6 +459,15 @@ static zval *websocket_connection_read_property(zend_object *object, zend_string return rv; } + if (zend_string_equals_literal(name, "subprotocol")) { + if (intern->selected_subprotocol) { + ZVAL_STR_COPY(rv, intern->selected_subprotocol); + } else { + ZVAL_NULL(rv); + } + return rv; + } + return zend_std_read_property(object, name, type, cache_slot, rv); } diff --git a/websocket_http.c b/websocket_http.c index 2f6991f..6093725 100644 --- a/websocket_http.c +++ b/websocket_http.c @@ -81,6 +81,90 @@ static bool websocket_http_header_contains_token(const char *value, const size_t return false; } +bool websocket_http_validate_subprotocol_token(const char *value, const size_t value_len) +{ + size_t i; + + if (value_len == 0) { + return false; + } + + for (i = 0; i < value_len; i++) { + const unsigned char ch = (unsigned char) value[i]; + + if (ch <= 32 || ch >= 127) { + return false; + } + + switch (ch) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + case '{': + case '}': + return false; + default: + break; + } + } + + return true; +} + +static bool websocket_http_select_subprotocol(const char *value, const size_t value_len, HashTable *supported_subprotocols, zend_string **selected_subprotocol) +{ + const char *part = value; + size_t offset = 0; + + while (offset <= value_len) { + const char *token_start = part; + size_t token_len; + const char *comma = memchr(part, ',', value_len - offset); + + if (comma) { + token_len = (size_t) (comma - part); + } else { + token_len = value_len - offset; + } + + websocket_http_trim(&token_start, &token_len); + if (!websocket_http_validate_subprotocol_token(token_start, token_len)) { + return false; + } + + if (!*selected_subprotocol && supported_subprotocols && zend_hash_num_elements(supported_subprotocols) > 0) { + zend_string *offered = zend_string_init(token_start, token_len, false); + + if (zend_hash_exists(supported_subprotocols, offered)) { + *selected_subprotocol = zend_string_copy(offered); + } + + zend_string_release(offered); + } + + if (!comma) { + break; + } + + part = comma + 1; + offset = (size_t) (part - value); + } + + return true; +} + static bool websocket_http_validate_request_line(const char *line, const size_t line_len) { const char *method_end; @@ -142,7 +226,7 @@ static bool websocket_http_validate_key(const char *value, const size_t value_le return true; } -websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, const size_t len, zend_string **accept_key, size_t *bytes_consumed) +websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, const size_t len, HashTable *supported_subprotocols, zend_string **accept_key, zend_string **selected_subprotocol, size_t *bytes_consumed) { const char *header_end; const char *line; @@ -152,6 +236,7 @@ websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, c bool has_version = false; *accept_key = NULL; + *selected_subprotocol = NULL; *bytes_consumed = 0; if (len > WEBSOCKET_HTTP_MAX_REQUEST_SIZE) { @@ -188,6 +273,10 @@ websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, c zend_string_release(*accept_key); *accept_key = NULL; } + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } return WEBSOCKET_HTTP_UPGRADE_INVALID; } @@ -202,6 +291,10 @@ websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, c zend_string_release(*accept_key); *accept_key = NULL; } + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } return WEBSOCKET_HTTP_UPGRADE_INVALID; } @@ -222,10 +315,30 @@ websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, c if (*accept_key) { zend_string_release(*accept_key); *accept_key = NULL; + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } return WEBSOCKET_HTTP_UPGRADE_INVALID; } if (!websocket_http_validate_key(value, value_len, accept_key)) { + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } + return WEBSOCKET_HTTP_UPGRADE_INVALID; + } + } else if (websocket_http_equals_ci(name, name_len, "Sec-WebSocket-Protocol", strlen("Sec-WebSocket-Protocol"))) { + if (!websocket_http_select_subprotocol(value, value_len, supported_subprotocols, selected_subprotocol)) { + if (*accept_key) { + zend_string_release(*accept_key); + *accept_key = NULL; + } + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } return WEBSOCKET_HTTP_UPGRADE_INVALID; } } @@ -238,14 +351,30 @@ websocket_http_upgrade_result websocket_http_parse_upgrade(const char *buffer, c zend_string_release(*accept_key); *accept_key = NULL; } + if (*selected_subprotocol) { + zend_string_release(*selected_subprotocol); + *selected_subprotocol = NULL; + } return WEBSOCKET_HTTP_UPGRADE_INVALID; } return WEBSOCKET_HTTP_UPGRADE_OK; } -zend_string *websocket_http_upgrade_response(zend_string *accept_key) +zend_string *websocket_http_upgrade_response(zend_string *accept_key, zend_string *selected_subprotocol) { + if (selected_subprotocol) { + return strpprintf(0, + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "Sec-WebSocket-Protocol: %s\r\n" + "\r\n", + ZSTR_VAL(accept_key), + ZSTR_VAL(selected_subprotocol)); + } + return strpprintf(0, "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" diff --git a/websocket_server.c b/websocket_server.c index 1600d4f..7e1046a 100644 --- a/websocket_server.c +++ b/websocket_server.c @@ -29,6 +29,7 @@ static zend_object *websocket_server_create_object(zend_class_entry *ce) object_properties_init(&intern->std, ce); ZVAL_UNDEF(&intern->options); + ZVAL_UNDEF(&intern->subprotocols); ZVAL_UNDEF(&intern->on_open); ZVAL_UNDEF(&intern->on_message); ZVAL_UNDEF(&intern->on_close); @@ -56,6 +57,7 @@ static void websocket_server_free_object(zend_object *object) websocket_server_object *intern = websocket_server_from_obj(object); zval_ptr_dtor(&intern->options); + zval_ptr_dtor(&intern->subprotocols); zval_ptr_dtor(&intern->on_open); zval_ptr_dtor(&intern->on_message); zval_ptr_dtor(&intern->on_close); @@ -95,6 +97,65 @@ PHP_METHOD(WebSocket_Server, __construct) } else { array_init(&intern->options); } + + array_init(&intern->subprotocols); +} + +PHP_METHOD(WebSocket_Server, subprotocols) +{ + zval *protocols; + uint32_t protocol_count; + zval normalized; + HashTable seen; + websocket_server_object *intern = Z_WEBSOCKET_SERVER_P(ZEND_THIS); + + ZEND_PARSE_PARAMETERS_START(0, -1) + Z_PARAM_VARIADIC('*', protocols, protocol_count) + ZEND_PARSE_PARAMETERS_END(); + + if (intern->running) { + zend_throw_error(NULL, "Cannot change WebSocket subprotocols while the server is running"); + RETURN_THROWS(); + } + + array_init(&normalized); + zend_hash_init(&seen, protocol_count, NULL, NULL, 0); + + for (uint32_t i = 0; i < protocol_count; i++) { + zval *protocol = &protocols[i]; + zend_string *protocol_str; + zval value; + + if (Z_TYPE_P(protocol) != IS_STRING) { + zend_hash_destroy(&seen); + zval_ptr_dtor(&normalized); + zend_argument_type_error(i + 1, "must be of type string, %s given", websocket_zval_value_name(protocol)); + RETURN_THROWS(); + } + + protocol_str = Z_STR_P(protocol); + if (!websocket_http_validate_subprotocol_token(ZSTR_VAL(protocol_str), ZSTR_LEN(protocol_str))) { + zend_hash_destroy(&seen); + zval_ptr_dtor(&normalized); + zend_argument_value_error(i + 1, "must be a valid WebSocket subprotocol token"); + RETURN_THROWS(); + } + + if (zend_hash_exists(&seen, protocol_str)) { + zend_hash_destroy(&seen); + zval_ptr_dtor(&normalized); + zend_argument_value_error(i + 1, "must not duplicate a previous subprotocol"); + RETURN_THROWS(); + } + + zend_hash_add_empty_element(&seen, protocol_str); + ZVAL_STR_COPY(&value, protocol_str); + zend_hash_add_new(Z_ARRVAL(normalized), protocol_str, &value); + } + + zend_hash_destroy(&seen); + zval_ptr_dtor(&intern->subprotocols); + ZVAL_COPY_VALUE(&intern->subprotocols, &normalized); } PHP_METHOD(WebSocket_ServerOptions, __construct) diff --git a/websocket_server_runtime.c b/websocket_server_runtime.c index b9e8f6d..61c982b 100644 --- a/websocket_server_runtime.c +++ b/websocket_server_runtime.c @@ -830,14 +830,14 @@ static bool websocket_server_process_buffered_frames(websocket_server_object *in return true; } -static bool websocket_server_finish_upgrade(websocket_server_object *intern, zval *connection, websocket_connection_object *connection_obj, zend_string *accept_key, const size_t bytes_consumed) +static bool websocket_server_finish_upgrade(websocket_server_object *intern, zval *connection, websocket_connection_object *connection_obj, zend_string *accept_key, zend_string *selected_subprotocol, const size_t bytes_consumed) { zend_string *response; bool close_requested = false; bool ok; const bool needs_connection = Z_ISUNDEF(intern->on_open) || intern->on_open_param_count > 0; - response = websocket_http_upgrade_response(accept_key); + response = websocket_http_upgrade_response(accept_key, selected_subprotocol); ok = websocket_server_send_bytes(connection_obj->fd, ZSTR_VAL(response), ZSTR_LEN(response)); zend_string_release(response); @@ -846,6 +846,14 @@ static bool websocket_server_finish_upgrade(websocket_server_object *intern, zva return true; } + if (connection_obj->selected_subprotocol) { + zend_string_release(connection_obj->selected_subprotocol); + connection_obj->selected_subprotocol = NULL; + } + if (selected_subprotocol) { + connection_obj->selected_subprotocol = zend_string_copy(selected_subprotocol); + } + connection_obj->upgraded = true; websocket_connection_discard_read_bytes(connection_obj, bytes_consumed); @@ -882,6 +890,7 @@ static bool websocket_server_process_handshake(websocket_server_object *intern, if (bytes_read > 0) { zend_string *accept_key = NULL; + zend_string *selected_subprotocol = NULL; size_t bytes_consumed = 0; websocket_http_upgrade_result result; @@ -896,7 +905,7 @@ static bool websocket_server_process_handshake(websocket_server_object *intern, memcpy(connection_obj->read_buffer + connection_obj->read_buffer_len, chunk, (size_t) bytes_read); connection_obj->read_buffer_len += (size_t) bytes_read; - result = websocket_http_parse_upgrade(connection_obj->read_buffer, connection_obj->read_buffer_len, &accept_key, &bytes_consumed); + result = websocket_http_parse_upgrade(connection_obj->read_buffer, connection_obj->read_buffer_len, Z_TYPE(intern->subprotocols) == IS_ARRAY ? Z_ARRVAL(intern->subprotocols) : NULL, &accept_key, &selected_subprotocol, &bytes_consumed); if (result == WEBSOCKET_HTTP_UPGRADE_INCOMPLETE) { continue; } @@ -907,12 +916,18 @@ static bool websocket_server_process_handshake(websocket_server_object *intern, return true; } - if (!websocket_server_finish_upgrade(intern, connection, connection_obj, accept_key, bytes_consumed)) { + if (!websocket_server_finish_upgrade(intern, connection, connection_obj, accept_key, selected_subprotocol, bytes_consumed)) { zend_string_release(accept_key); + if (selected_subprotocol) { + zend_string_release(selected_subprotocol); + } return false; } zend_string_release(accept_key); + if (selected_subprotocol) { + zend_string_release(selected_subprotocol); + } return true; }