├── MIT-LICENSE.txt ├── README.md ├── SECURITY.md ├── composer.json └── src ├── Connection ├── AsyncTcpConnection.php ├── AsyncUdpConnection.php ├── ConnectionInterface.php ├── TcpConnection.php └── UdpConnection.php ├── Events ├── Ev.php ├── Event.php ├── EventInterface.php ├── Fiber.php ├── Select.php ├── Swoole.php └── Swow.php ├── Protocols ├── Frame.php ├── Http.php ├── Http │ ├── Chunk.php │ ├── Request.php │ ├── Response.php │ ├── ServerSentEvents.php │ ├── Session.php │ ├── Session │ │ ├── FileSessionHandler.php │ │ ├── RedisClusterSessionHandler.php │ │ ├── RedisSessionHandler.php │ │ └── SessionHandlerInterface.php │ └── mime.types ├── ProtocolInterface.php ├── Text.php ├── Websocket.php └── Ws.php ├── Timer.php └── Worker.php /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009-2025 walkor and contributors (see https://github.com/walkor/workerman/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workerman 2 | [![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) 3 | [![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman) 4 | [![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman) 5 | [![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman) 6 | [![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman) 7 | [![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman) 8 | 9 | ## What is it 10 | Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. It supports HTTP, WebSocket, custom protocols, coroutines, and connection pools, making it ideal for handling high-concurrency scenarios efficiently. 11 | 12 | ## Requires 13 | A POSIX compatible operating system (Linux, OSX, BSD) 14 | POSIX and PCNTL extensions required 15 | Event/Swoole/Swow extension recommended for better performance 16 | 17 | ## Installation 18 | 19 | ``` 20 | composer require workerman/workerman 21 | ``` 22 | 23 | ## Documentation 24 | 25 | [https://manual.workerman.net](https://manual.workerman.net) 26 | 27 | ## Basic Usage 28 | 29 | ### A websocket server 30 | ```php 31 | onConnect = function ($connection) { 42 | echo "New connection\n"; 43 | }; 44 | 45 | // Emitted when data received 46 | $ws_worker->onMessage = function ($connection, $data) { 47 | // Send hello $data 48 | $connection->send('Hello ' . $data); 49 | }; 50 | 51 | // Emitted when connection closed 52 | $ws_worker->onClose = function ($connection) { 53 | echo "Connection closed\n"; 54 | }; 55 | 56 | // Run worker 57 | Worker::runAll(); 58 | ``` 59 | 60 | ### An http server 61 | ```php 62 | use Workerman\Worker; 63 | 64 | require_once __DIR__ . '/vendor/autoload.php'; 65 | 66 | // #### http worker #### 67 | $http_worker = new Worker('http://0.0.0.0:2345'); 68 | 69 | // 4 processes 70 | $http_worker->count = 4; 71 | 72 | // Emitted when data received 73 | $http_worker->onMessage = function ($connection, $request) { 74 | //$request->get(); 75 | //$request->post(); 76 | //$request->header(); 77 | //$request->cookie(); 78 | //$request->session(); 79 | //$request->uri(); 80 | //$request->path(); 81 | //$request->method(); 82 | 83 | // Send data to client 84 | $connection->send("Hello World"); 85 | }; 86 | 87 | // Run all workers 88 | Worker::runAll(); 89 | ``` 90 | 91 | ### A tcp server 92 | ```php 93 | use Workerman\Worker; 94 | 95 | require_once __DIR__ . '/vendor/autoload.php'; 96 | 97 | // #### create socket and listen 1234 port #### 98 | $tcp_worker = new Worker('tcp://0.0.0.0:1234'); 99 | 100 | // 4 processes 101 | $tcp_worker->count = 4; 102 | 103 | // Emitted when new connection come 104 | $tcp_worker->onConnect = function ($connection) { 105 | echo "New Connection\n"; 106 | }; 107 | 108 | // Emitted when data received 109 | $tcp_worker->onMessage = function ($connection, $data) { 110 | // Send data to client 111 | $connection->send("Hello $data \n"); 112 | }; 113 | 114 | // Emitted when connection is closed 115 | $tcp_worker->onClose = function ($connection) { 116 | echo "Connection closed\n"; 117 | }; 118 | 119 | Worker::runAll(); 120 | ``` 121 | 122 | ### Enable SSL 123 | ```php 124 | [ 133 | 'local_cert' => '/your/path/of/server.pem', 134 | 'local_pk' => '/your/path/of/server.key', 135 | 'verify_peer' => false, 136 | ] 137 | ]; 138 | 139 | // Create a Websocket server with ssl context. 140 | $ws_worker = new Worker('websocket://0.0.0.0:2346', $context); 141 | 142 | // Enable SSL. WebSocket+SSL means that Secure WebSocket (wss://). 143 | // The similar approaches for Https etc. 144 | $ws_worker->transport = 'ssl'; 145 | 146 | $ws_worker->onMessage = function ($connection, $data) { 147 | // Send hello $data 148 | $connection->send('Hello ' . $data); 149 | }; 150 | 151 | Worker::runAll(); 152 | ``` 153 | 154 | ### AsyncTcpConnection (tcp/ws/text/frame etc...) 155 | ```php 156 | 157 | use Workerman\Worker; 158 | use Workerman\Connection\AsyncTcpConnection; 159 | 160 | require_once __DIR__ . '/vendor/autoload.php'; 161 | 162 | $worker = new Worker(); 163 | $worker->onWorkerStart = function () { 164 | // Websocket protocol for client. 165 | $ws_connection = new AsyncTcpConnection('ws://echo.websocket.org:80'); 166 | $ws_connection->onConnect = function ($connection) { 167 | $connection->send('Hello'); 168 | }; 169 | $ws_connection->onMessage = function ($connection, $data) { 170 | echo "Recv: $data\n"; 171 | }; 172 | $ws_connection->onError = function ($connection, $code, $msg) { 173 | echo "Error: $msg\n"; 174 | }; 175 | $ws_connection->onClose = function ($connection) { 176 | echo "Connection closed\n"; 177 | }; 178 | $ws_connection->connect(); 179 | }; 180 | 181 | Worker::runAll(); 182 | ``` 183 | 184 | ### Coroutine 185 | 186 | Coroutine is used to create coroutines, enabling the execution of asynchronous tasks to improve concurrency performance. 187 | 188 | ```php 189 | eventLoop = Swoole::class; // Or Swow::class or Fiber::class 200 | 201 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 202 | Coroutine::create(function () { 203 | echo file_get_contents("http://www.example.com/event/notify"); 204 | }); 205 | $connection->send('ok'); 206 | }; 207 | 208 | Worker::runAll(); 209 | ``` 210 | 211 | > Note: Coroutine require Swoole extension or Swow extension or [Fiber revolt/event-loop](https://github.com/revoltphp/event-loop), and the same applies below 212 | 213 | ### Barrier 214 | Barrier is used to manage concurrency and synchronization in coroutines. It allows tasks to run concurrently and waits until all tasks are completed, ensuring process synchronization. 215 | 216 | ```php 217 | eventLoop = Swoole::class; // Or Swow::class or Fiber::class 229 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 230 | $barrier = Barrier::create(); 231 | for ($i=1; $i<5; $i++) { 232 | Coroutine::create(function () use ($barrier, $i) { 233 | file_get_contents("http://127.0.0.1:8002?task_id=$i"); 234 | }); 235 | } 236 | // Wait all coroutine done 237 | Barrier::wait($barrier); 238 | $connection->send('All Task Done'); 239 | }; 240 | 241 | // Task Server 242 | $task = new Worker('http://0.0.0.0:8002'); 243 | $task->onMessage = function (TcpConnection $connection, Request $request) { 244 | $task_id = $request->get('task_id'); 245 | $message = "Task $task_id Done"; 246 | echo $message . PHP_EOL; 247 | $connection->close($message); 248 | }; 249 | 250 | Worker::runAll(); 251 | ``` 252 | 253 | ### Parallel 254 | Parallel executes multiple tasks concurrently and collects results. Use add to add tasks and wait to wait for completion and get results. Unlike Barrier, Parallel directly returns the results of each task. 255 | 256 | ```php 257 | eventLoop = Swoole::class; // Or Swow::class or Fiber::class 268 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 269 | $parallel = new Parallel(); 270 | for ($i=1; $i<5; $i++) { 271 | $parallel->add(function () use ($i) { 272 | return file_get_contents("http://127.0.0.1:8002?task_id=$i"); 273 | }); 274 | } 275 | $results = $parallel->wait(); 276 | $connection->send(json_encode($results)); // Response: ["Task 1 Done","Task 2 Done","Task 3 Done","Task 4 Done"] 277 | }; 278 | 279 | // Task Server 280 | $task = new Worker('http://0.0.0.0:8002'); 281 | $task->onMessage = function (TcpConnection $connection, Request $request) { 282 | $task_id = $request->get('task_id'); 283 | $message = "Task $task_id Done"; 284 | $connection->close($message); 285 | }; 286 | 287 | Worker::runAll(); 288 | ``` 289 | 290 | ### Channel 291 | 292 | Channel is a mechanism for communication between coroutines. One coroutine can push data into the channel, while another can pop data from it, enabling synchronization and data sharing between coroutines. 293 | 294 | ```php 295 | eventLoop = Swoole::class; // Or Swow::class or Fiber::class 307 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 308 | $channel = new Channel(2); 309 | Coroutine::create(function () use ($channel) { 310 | $channel->push('Task 1 Done'); 311 | }); 312 | Coroutine::create(function () use ($channel) { 313 | $channel->push('Task 2 Done'); 314 | }); 315 | $result = []; 316 | for ($i = 0; $i < 2; $i++) { 317 | $result[] = $channel->pop(); 318 | } 319 | $connection->send(json_encode($result)); // Response: ["Task 1 Done","Task 2 Done"] 320 | }; 321 | Worker::runAll(); 322 | ``` 323 | 324 | ### Pool 325 | 326 | Pool is used to manage connection or resource pools, improving performance by reusing resources (e.g., database connections). It supports acquiring, returning, creating, and destroying resources. 327 | 328 | ```php 329 | setConnectionCreator(function () use ($host, $port) { 344 | $redis = new \Redis(); 345 | $redis->connect($host, $port); 346 | return $redis; 347 | }); 348 | $pool->setConnectionCloser(function ($redis) { 349 | $redis->close(); 350 | }); 351 | $pool->setHeartbeatChecker(function ($redis) { 352 | $redis->ping(); 353 | }); 354 | $this->pool = $pool; 355 | } 356 | public function get(): \Redis 357 | { 358 | return $this->pool->get(); 359 | } 360 | public function put($redis): void 361 | { 362 | $this->pool->put($redis); 363 | } 364 | } 365 | 366 | // Http Server 367 | $worker = new Worker('http://0.0.0.0:8001'); 368 | $worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class 369 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 370 | static $pool; 371 | if (!$pool) { 372 | $pool = new RedisPool('127.0.0.1', 6379, 10); 373 | } 374 | $redis = $pool->get(); 375 | $redis->set('key', 'hello'); 376 | $value = $redis->get('key'); 377 | $pool->put($redis); 378 | $connection->send($value); 379 | }; 380 | 381 | Worker::runAll(); 382 | ``` 383 | 384 | 385 | ### Pool for automatic acquisition and release 386 | 387 | ```php 388 | get(); 412 | Context::set('pdo', $pdo); 413 | // When the coroutine is destroyed, return the connection to the pool 414 | Coroutine::defer(function () use ($pdo) { 415 | self::$pool->put($pdo); 416 | }); 417 | } 418 | return call_user_func_array([$pdo, $name], $arguments); 419 | } 420 | private static function initializePool(): void 421 | { 422 | self::$pool = new Pool(10); 423 | self::$pool->setConnectionCreator(function () { 424 | return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password'); 425 | }); 426 | self::$pool->setConnectionCloser(function ($pdo) { 427 | $pdo = null; 428 | }); 429 | self::$pool->setHeartbeatChecker(function ($pdo) { 430 | $pdo->query('SELECT 1'); 431 | }); 432 | } 433 | } 434 | 435 | // Http Server 436 | $worker = new Worker('http://0.0.0.0:8001'); 437 | $worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class 438 | $worker->onMessage = function (TcpConnection $connection, Request $request) { 439 | $value = Db::query('SELECT NOW() as now')->fetchAll(); 440 | $connection->send(json_encode($value)); 441 | }; 442 | 443 | Worker::runAll(); 444 | ``` 445 | 446 | ## Available commands 447 | ```php start.php start ``` 448 | ```php start.php start -d ``` 449 | ```php start.php status ``` 450 | ```php start.php status -d ``` 451 | ```php start.php connections``` 452 | ```php start.php stop ``` 453 | ```php start.php stop -g ``` 454 | ```php start.php restart ``` 455 | ```php start.php reload ``` 456 | ```php start.php reload -g ``` 457 | 458 | # Benchmarks 459 | https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext&l=zik073-1r 460 | 461 | 462 | ### Supported by 463 | 464 | [![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport) 465 | 466 | 467 | ## Other links with workerman 468 | 469 | [webman](https://github.com/walkor/webman) 470 | [AdapterMan](https://github.com/joanhey/AdapterMan) 471 | 472 | ## Donate 473 | PayPal 474 | 475 | ## LICENSE 476 | 477 | Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt). 478 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 4 | ## Reporting a Vulnerability 5 | 6 | Please contact by email walkor@workerman.net 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workerman/workerman", 3 | "type": "library", 4 | "keywords": [ 5 | "event-loop", 6 | "asynchronous", 7 | "http", 8 | "framework" 9 | ], 10 | "homepage": "https://www.workerman.net", 11 | "license": "MIT", 12 | "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", 13 | "authors": [ 14 | { 15 | "name": "walkor", 16 | "email": "walkor@workerman.net", 17 | "homepage": "https://www.workerman.net", 18 | "role": "Developer" 19 | } 20 | ], 21 | "support": { 22 | "email": "walkor@workerman.net", 23 | "issues": "https://github.com/walkor/workerman/issues", 24 | "forum": "https://www.workerman.net/questions", 25 | "wiki": "https://www.workerman.net/doc/workerman/", 26 | "source": "https://github.com/walkor/workerman" 27 | }, 28 | "require": { 29 | "php": ">=8.1", 30 | "ext-json": "*", 31 | "workerman/coroutine": "^1.1 || dev-main" 32 | }, 33 | "suggest": { 34 | "ext-event": "For better performance. " 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Workerman\\": "src" 39 | } 40 | }, 41 | "minimum-stability": "dev", 42 | "conflict": { 43 | "ext-swow": " 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Connection; 18 | 19 | use Exception; 20 | use RuntimeException; 21 | use stdClass; 22 | use Throwable; 23 | use Workerman\Timer; 24 | use Workerman\Worker; 25 | use function class_exists; 26 | use function explode; 27 | use function function_exists; 28 | use function is_resource; 29 | use function method_exists; 30 | use function microtime; 31 | use function parse_url; 32 | use function socket_import_stream; 33 | use function socket_set_option; 34 | use function stream_context_create; 35 | use function stream_set_blocking; 36 | use function stream_set_read_buffer; 37 | use function stream_socket_client; 38 | use function stream_socket_get_name; 39 | use function ucfirst; 40 | use const DIRECTORY_SEPARATOR; 41 | use const PHP_INT_MAX; 42 | use const SO_KEEPALIVE; 43 | use const SOL_SOCKET; 44 | use const SOL_TCP; 45 | use const STREAM_CLIENT_ASYNC_CONNECT; 46 | use const TCP_NODELAY; 47 | 48 | /** 49 | * AsyncTcpConnection. 50 | */ 51 | class AsyncTcpConnection extends TcpConnection 52 | { 53 | /** 54 | * PHP built-in protocols. 55 | * 56 | * @var array 57 | */ 58 | public const BUILD_IN_TRANSPORTS = [ 59 | 'tcp' => 'tcp', 60 | 'udp' => 'udp', 61 | 'unix' => 'unix', 62 | 'ssl' => 'ssl', 63 | 'sslv2' => 'sslv2', 64 | 'sslv3' => 'sslv3', 65 | 'tls' => 'tls' 66 | ]; 67 | 68 | /** 69 | * Emitted when socket connection is successfully established. 70 | * 71 | * @var ?callable 72 | */ 73 | public $onConnect = null; 74 | 75 | /** 76 | * Emitted when websocket handshake completed (Only work when protocol is ws). 77 | * 78 | * @var ?callable 79 | */ 80 | public $onWebSocketConnect = null; 81 | 82 | /** 83 | * Transport layer protocol. 84 | * 85 | * @var string 86 | */ 87 | public string $transport = 'tcp'; 88 | 89 | /** 90 | * Socks5 proxy. 91 | * 92 | * @var string 93 | */ 94 | public string $proxySocks5 = ''; 95 | 96 | /** 97 | * Http proxy. 98 | * 99 | * @var string 100 | */ 101 | public string $proxyHttp = ''; 102 | 103 | /** 104 | * Status. 105 | * 106 | * @var int 107 | */ 108 | protected int $status = self::STATUS_INITIAL; 109 | 110 | /** 111 | * Remote host. 112 | * 113 | * @var string 114 | */ 115 | protected string $remoteHost = ''; 116 | 117 | /** 118 | * Remote port. 119 | * 120 | * @var int 121 | */ 122 | protected int $remotePort = 80; 123 | 124 | /** 125 | * Connect start time. 126 | * 127 | * @var float 128 | */ 129 | protected float $connectStartTime = 0; 130 | 131 | /** 132 | * Remote URI. 133 | * 134 | * @var string 135 | */ 136 | protected string $remoteURI = ''; 137 | 138 | /** 139 | * Context option. 140 | * 141 | * @var array 142 | */ 143 | protected array $socketContext = []; 144 | 145 | /** 146 | * Reconnect timer. 147 | * 148 | * @var int 149 | */ 150 | protected int $reconnectTimer = 0; 151 | 152 | /** 153 | * Construct. 154 | * 155 | * @param string $remoteAddress 156 | * @param array $socketContext 157 | */ 158 | public function __construct(string $remoteAddress, array $socketContext = []) 159 | { 160 | $addressInfo = parse_url($remoteAddress); 161 | if (!$addressInfo) { 162 | [$scheme, $this->remoteAddress] = explode(':', $remoteAddress, 2); 163 | if ('unix' === strtolower($scheme)) { 164 | $this->remoteAddress = substr($remoteAddress, strpos($remoteAddress, '/') + 2); 165 | } 166 | if (!$this->remoteAddress) { 167 | throw new RuntimeException('Bad remoteAddress'); 168 | } 169 | } else { 170 | $addressInfo['port'] ??= 0; 171 | $addressInfo['path'] ??= '/'; 172 | if (!isset($addressInfo['query'])) { 173 | $addressInfo['query'] = ''; 174 | } else { 175 | $addressInfo['query'] = '?' . $addressInfo['query']; 176 | } 177 | $this->remoteHost = $addressInfo['host']; 178 | $this->remotePort = $addressInfo['port']; 179 | $this->remoteURI = "{$addressInfo['path']}{$addressInfo['query']}"; 180 | $scheme = $addressInfo['scheme'] ?? 'tcp'; 181 | $this->remoteAddress = 'unix' === strtolower($scheme) 182 | ? substr($remoteAddress, strpos($remoteAddress, '/') + 2) 183 | : $this->remoteHost . ':' . $this->remotePort; 184 | } 185 | 186 | $this->id = $this->realId = self::$idRecorder++; 187 | if (PHP_INT_MAX === self::$idRecorder) { 188 | self::$idRecorder = 0; 189 | } 190 | // Check application layer protocol class. 191 | if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) { 192 | $scheme = ucfirst($scheme); 193 | $this->protocol = '\\Protocols\\' . $scheme; 194 | if (!class_exists($this->protocol)) { 195 | $this->protocol = "\\Workerman\\Protocols\\$scheme"; 196 | if (!class_exists($this->protocol)) { 197 | throw new RuntimeException("class \\Protocols\\$scheme not exist"); 198 | } 199 | } 200 | } else { 201 | $this->transport = self::BUILD_IN_TRANSPORTS[$scheme]; 202 | } 203 | 204 | // For statistics. 205 | ++self::$statistics['connection_count']; 206 | $this->maxSendBufferSize = self::$defaultMaxSendBufferSize; 207 | $this->maxPackageSize = self::$defaultMaxPackageSize; 208 | $this->socketContext = $socketContext; 209 | static::$connections[$this->realId] = $this; 210 | $this->context = new stdClass; 211 | } 212 | 213 | /** 214 | * Reconnect. 215 | * 216 | * @param int $after 217 | * @return void 218 | */ 219 | public function reconnect(int $after = 0): void 220 | { 221 | $this->status = self::STATUS_INITIAL; 222 | static::$connections[$this->realId] = $this; 223 | if ($this->reconnectTimer) { 224 | Timer::del($this->reconnectTimer); 225 | } 226 | if ($after > 0) { 227 | $this->reconnectTimer = Timer::add($after, $this->connect(...), null, false); 228 | return; 229 | } 230 | $this->connect(); 231 | } 232 | 233 | /** 234 | * Do connect. 235 | * 236 | * @return void 237 | */ 238 | public function connect(): void 239 | { 240 | if ($this->status !== self::STATUS_INITIAL && $this->status !== self::STATUS_CLOSING && 241 | $this->status !== self::STATUS_CLOSED) { 242 | return; 243 | } 244 | 245 | if (!$this->eventLoop) { 246 | $this->eventLoop = Worker::$globalEvent; 247 | } 248 | 249 | $this->status = self::STATUS_CONNECTING; 250 | $this->connectStartTime = microtime(true); 251 | set_error_handler(fn() => false); 252 | if ($this->transport !== 'unix') { 253 | if (!$this->remotePort) { 254 | $this->remotePort = $this->transport === 'ssl' ? 443 : 80; 255 | $this->remoteAddress = $this->remoteHost . ':' . $this->remotePort; 256 | } 257 | // Open socket connection asynchronously. 258 | if ($this->proxySocks5) { 259 | $this->socketContext['ssl']['peer_name'] = $this->remoteHost; 260 | $context = stream_context_create($this->socketContext); 261 | $this->socket = stream_socket_client("tcp://$this->proxySocks5", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context); 262 | } else if ($this->proxyHttp) { 263 | $this->socketContext['ssl']['peer_name'] = $this->remoteHost; 264 | $context = stream_context_create($this->socketContext); 265 | $this->socket = stream_socket_client("tcp://$this->proxyHttp", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context); 266 | } else if ($this->socketContext) { 267 | $context = stream_context_create($this->socketContext); 268 | $this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort", 269 | $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context); 270 | } else { 271 | $this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort", 272 | $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT); 273 | } 274 | } else { 275 | $this->socket = stream_socket_client("$this->transport://$this->remoteAddress", $errno, $err_str, 0, 276 | STREAM_CLIENT_ASYNC_CONNECT); 277 | } 278 | restore_error_handler(); 279 | // If failed attempt to emit onError callback. 280 | if (!$this->socket || !is_resource($this->socket)) { 281 | $this->emitError(static::CONNECT_FAIL, $err_str); 282 | if ($this->status === self::STATUS_CLOSING) { 283 | $this->destroy(); 284 | } 285 | if ($this->status === self::STATUS_CLOSED) { 286 | $this->onConnect = null; 287 | } 288 | return; 289 | } 290 | // Add socket to global event loop waiting connection is successfully established or failed. 291 | $this->eventLoop->onWritable($this->socket, $this->checkConnection(...)); 292 | // For windows. 293 | if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'onExcept')) { 294 | $this->eventLoop->onExcept($this->socket, $this->checkConnection(...)); 295 | } 296 | } 297 | 298 | /** 299 | * Try to emit onError callback. 300 | * 301 | * @param int $code 302 | * @param mixed $msg 303 | * @return void 304 | */ 305 | protected function emitError(int $code, mixed $msg): void 306 | { 307 | $this->status = self::STATUS_CLOSING; 308 | if ($this->onError) { 309 | try { 310 | ($this->onError)($this, $code, $msg); 311 | } catch (Throwable $e) { 312 | $this->error($e); 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * CancelReconnect. 319 | */ 320 | public function cancelReconnect(): void 321 | { 322 | if ($this->reconnectTimer) { 323 | Timer::del($this->reconnectTimer); 324 | $this->reconnectTimer = 0; 325 | } 326 | } 327 | 328 | /** 329 | * Get remote address. 330 | * 331 | * @return string 332 | */ 333 | public function getRemoteHost(): string 334 | { 335 | return $this->remoteHost; 336 | } 337 | 338 | /** 339 | * Get remote URI. 340 | * 341 | * @return string 342 | */ 343 | public function getRemoteURI(): string 344 | { 345 | return $this->remoteURI; 346 | } 347 | 348 | /** 349 | * Check connection is successfully established or failed. 350 | * 351 | * @return void 352 | */ 353 | public function checkConnection(): void 354 | { 355 | // Remove EV_EXPECT for windows. 356 | if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'offExcept')) { 357 | $this->eventLoop->offExcept($this->socket); 358 | } 359 | // Remove write listener. 360 | $this->eventLoop->offWritable($this->socket); 361 | 362 | if ($this->status !== self::STATUS_CONNECTING) { 363 | return; 364 | } 365 | 366 | // Check socket state. 367 | if ($address = stream_socket_get_name($this->socket, true)) { 368 | // Proxy 369 | if ($this->proxySocks5 && $address === $this->proxySocks5) { 370 | fwrite($this->socket, chr(5) . chr(1) . chr(0)); 371 | fread($this->socket, 512); 372 | fwrite($this->socket, chr(5) . chr(1) . chr(0) . chr(3) . chr(strlen($this->remoteHost)) . $this->remoteHost . pack("n", $this->remotePort)); 373 | fread($this->socket, 512); 374 | } 375 | if ($this->proxyHttp && $address === $this->proxyHttp) { 376 | $str = "CONNECT $this->remoteHost:$this->remotePort HTTP/1.1\r\n"; 377 | $str .= "Host: $this->remoteHost:$this->remotePort\r\n"; 378 | $str .= "Proxy-Connection: keep-alive\r\n\r\n"; 379 | fwrite($this->socket, $str); 380 | fread($this->socket, 512); 381 | } 382 | // Nonblocking. 383 | stream_set_blocking($this->socket, false); 384 | stream_set_read_buffer($this->socket, 0); 385 | // Try to open keepalive for tcp and disable Nagle algorithm. 386 | if (function_exists('socket_import_stream') && $this->transport === 'tcp') { 387 | $socket = socket_import_stream($this->socket); 388 | socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1); 389 | socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1); 390 | if (defined('TCP_KEEPIDLE') && defined('TCP_KEEPINTVL') && defined('TCP_KEEPCNT')) { 391 | socket_set_option($socket, SOL_TCP, TCP_KEEPIDLE, static::TCP_KEEPALIVE_INTERVAL); 392 | socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, static::TCP_KEEPALIVE_INTERVAL); 393 | socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 1); 394 | } 395 | } 396 | // SSL handshake. 397 | if ($this->transport === 'ssl') { 398 | $this->sslHandshakeCompleted = $this->doSslHandshake($this->socket); 399 | if ($this->sslHandshakeCompleted === false) { 400 | return; 401 | } 402 | } else { 403 | // There are some data waiting to send. 404 | if ($this->sendBuffer) { 405 | $this->eventLoop->onWritable($this->socket, $this->baseWrite(...)); 406 | } 407 | } 408 | // Register a listener waiting read event. 409 | $this->eventLoop->onReadable($this->socket, $this->baseRead(...)); 410 | 411 | $this->status = self::STATUS_ESTABLISHED; 412 | $this->remoteAddress = $address; 413 | 414 | // Try to emit onConnect callback. 415 | if ($this->onConnect) { 416 | try { 417 | ($this->onConnect)($this); 418 | } catch (Throwable $e) { 419 | $this->error($e); 420 | } 421 | } 422 | // Try to emit protocol::onConnect 423 | if ($this->protocol && method_exists($this->protocol, 'onConnect')) { 424 | try { 425 | $this->protocol::onConnect($this); 426 | } catch (Throwable $e) { 427 | $this->error($e); 428 | } 429 | } 430 | } else { 431 | // Connection failed. 432 | $this->emitError(static::CONNECT_FAIL, 'connect ' . $this->remoteAddress . ' fail after ' . round(microtime(true) - $this->connectStartTime, 4) . ' seconds'); 433 | if ($this->status === self::STATUS_CLOSING) { 434 | $this->destroy(); 435 | } 436 | if ($this->status === self::STATUS_CLOSED) { 437 | $this->onConnect = null; 438 | } 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /src/Connection/AsyncUdpConnection.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Connection; 18 | 19 | use Exception; 20 | use RuntimeException; 21 | use Throwable; 22 | use Workerman\Protocols\ProtocolInterface; 23 | use Workerman\Worker; 24 | use function class_exists; 25 | use function explode; 26 | use function fclose; 27 | use function stream_context_create; 28 | use function stream_set_blocking; 29 | use function stream_socket_client; 30 | use function stream_socket_recvfrom; 31 | use function stream_socket_sendto; 32 | use function strlen; 33 | use function substr; 34 | use function ucfirst; 35 | use const STREAM_CLIENT_CONNECT; 36 | 37 | /** 38 | * AsyncUdpConnection. 39 | */ 40 | class AsyncUdpConnection extends UdpConnection 41 | { 42 | /** 43 | * Emitted when socket connection is successfully established. 44 | * 45 | * @var ?callable 46 | */ 47 | public $onConnect = null; 48 | 49 | /** 50 | * Emitted when socket connection closed. 51 | * 52 | * @var ?callable 53 | */ 54 | public $onClose = null; 55 | 56 | /** 57 | * Connected or not. 58 | * 59 | * @var bool 60 | */ 61 | protected bool $connected = false; 62 | 63 | /** 64 | * Context option. 65 | * 66 | * @var array 67 | */ 68 | protected array $contextOption = []; 69 | 70 | /** 71 | * Construct. 72 | * 73 | * @param string $remoteAddress 74 | * @throws Throwable 75 | */ 76 | public function __construct($remoteAddress, $contextOption = []) 77 | { 78 | // Get the application layer communication protocol and listening address. 79 | [$scheme, $address] = explode(':', $remoteAddress, 2); 80 | // Check application layer protocol class. 81 | if ($scheme !== 'udp') { 82 | $scheme = ucfirst($scheme); 83 | $this->protocol = '\\Protocols\\' . $scheme; 84 | if (!class_exists($this->protocol)) { 85 | $this->protocol = "\\Workerman\\Protocols\\$scheme"; 86 | if (!class_exists($this->protocol)) { 87 | throw new RuntimeException("class \\Protocols\\$scheme not exist"); 88 | } 89 | } 90 | } 91 | 92 | $this->remoteAddress = substr($address, 2); 93 | $this->contextOption = $contextOption; 94 | } 95 | 96 | /** 97 | * For udp package. 98 | * 99 | * @param resource $socket 100 | * @return void 101 | */ 102 | public function baseRead($socket): void 103 | { 104 | $recvBuffer = stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress); 105 | if (false === $recvBuffer || empty($remoteAddress)) { 106 | return; 107 | } 108 | 109 | if ($this->onMessage) { 110 | if ($this->protocol) { 111 | $recvBuffer = $this->protocol::decode($recvBuffer, $this); 112 | } 113 | ++ConnectionInterface::$statistics['total_request']; 114 | try { 115 | ($this->onMessage)($this, $recvBuffer); 116 | } catch (Throwable $e) { 117 | $this->error($e); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Close connection. 124 | * 125 | * @param mixed $data 126 | * @param bool $raw 127 | * @return void 128 | */ 129 | public function close(mixed $data = null, bool $raw = false): void 130 | { 131 | if ($data !== null) { 132 | $this->send($data, $raw); 133 | } 134 | $this->eventLoop->offReadable($this->socket); 135 | fclose($this->socket); 136 | $this->connected = false; 137 | // Try to emit onClose callback. 138 | if ($this->onClose) { 139 | try { 140 | ($this->onClose)($this); 141 | } catch (Throwable $e) { 142 | $this->error($e); 143 | } 144 | } 145 | $this->onConnect = $this->onMessage = $this->onClose = $this->eventLoop = $this->errorHandler = null; 146 | } 147 | 148 | /** 149 | * Sends data on the connection. 150 | * 151 | * @param mixed $sendBuffer 152 | * @param bool $raw 153 | * @return bool|null 154 | */ 155 | public function send(mixed $sendBuffer, bool $raw = false): bool|null 156 | { 157 | if (false === $raw && $this->protocol) { 158 | $sendBuffer = $this->protocol::encode($sendBuffer, $this); 159 | if ($sendBuffer === '') { 160 | return null; 161 | } 162 | } 163 | if ($this->connected === false) { 164 | $this->connect(); 165 | } 166 | return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer); 167 | } 168 | 169 | /** 170 | * Connect. 171 | * 172 | * @return void 173 | */ 174 | public function connect(): void 175 | { 176 | if ($this->connected === true) { 177 | return; 178 | } 179 | if (!$this->eventLoop) { 180 | $this->eventLoop = Worker::$globalEvent; 181 | } 182 | if ($this->contextOption) { 183 | $context = stream_context_create($this->contextOption); 184 | $this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg, 185 | 30, STREAM_CLIENT_CONNECT, $context); 186 | } else { 187 | $this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg); 188 | } 189 | 190 | if (!$this->socket) { 191 | Worker::safeEcho((string)(new Exception($errmsg))); 192 | $this->eventLoop = null; 193 | return; 194 | } 195 | 196 | stream_set_blocking($this->socket, false); 197 | if ($this->onMessage) { 198 | $this->eventLoop->onReadable($this->socket, $this->baseRead(...)); 199 | } 200 | $this->connected = true; 201 | // Try to emit onConnect callback. 202 | if ($this->onConnect) { 203 | try { 204 | ($this->onConnect)($this); 205 | } catch (Throwable $e) { 206 | $this->error($e); 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Connection/ConnectionInterface.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Connection; 18 | 19 | use Throwable; 20 | use Workerman\Events\Event; 21 | use Workerman\Events\EventInterface; 22 | use Workerman\Worker; 23 | use AllowDynamicProperties; 24 | 25 | /** 26 | * ConnectionInterface. 27 | */ 28 | #[AllowDynamicProperties] 29 | abstract class ConnectionInterface 30 | { 31 | /** 32 | * Connect failed. 33 | * 34 | * @var int 35 | */ 36 | public const CONNECT_FAIL = 1; 37 | 38 | /** 39 | * Send failed. 40 | * 41 | * @var int 42 | */ 43 | public const SEND_FAIL = 2; 44 | 45 | /** 46 | * Statistics for status command. 47 | * 48 | * @var array 49 | */ 50 | public static array $statistics = [ 51 | 'connection_count' => 0, 52 | 'total_request' => 0, 53 | 'throw_exception' => 0, 54 | 'send_fail' => 0, 55 | ]; 56 | 57 | /** 58 | * Application layer protocol. 59 | * The format is like this Workerman\\Protocols\\Http. 60 | * 61 | * @var ?class-string 62 | */ 63 | public ?string $protocol = null; 64 | 65 | /** 66 | * Emitted when data is received. 67 | * 68 | * @var ?callable 69 | */ 70 | public $onMessage = null; 71 | 72 | /** 73 | * Emitted when the other end of the socket sends a FIN packet. 74 | * 75 | * @var ?callable 76 | */ 77 | public $onClose = null; 78 | 79 | /** 80 | * Emitted when an error occurs with connection. 81 | * 82 | * @var ?callable 83 | */ 84 | public $onError = null; 85 | 86 | /** 87 | * @var ?EventInterface 88 | */ 89 | public ?EventInterface $eventLoop = null; 90 | 91 | /** 92 | * @var ?callable 93 | */ 94 | public $errorHandler = null; 95 | 96 | /** 97 | * Sends data on the connection. 98 | * 99 | * @param mixed $sendBuffer 100 | * @param bool $raw 101 | * @return bool|null 102 | */ 103 | abstract public function send(mixed $sendBuffer, bool $raw = false): bool|null; 104 | 105 | /** 106 | * Get remote IP. 107 | * 108 | * @return string 109 | */ 110 | abstract public function getRemoteIp(): string; 111 | 112 | /** 113 | * Get remote port. 114 | * 115 | * @return int 116 | */ 117 | abstract public function getRemotePort(): int; 118 | 119 | /** 120 | * Get remote address. 121 | * 122 | * @return string 123 | */ 124 | abstract public function getRemoteAddress(): string; 125 | 126 | /** 127 | * Get local IP. 128 | * 129 | * @return string 130 | */ 131 | abstract public function getLocalIp(): string; 132 | 133 | /** 134 | * Get local port. 135 | * 136 | * @return int 137 | */ 138 | abstract public function getLocalPort(): int; 139 | 140 | /** 141 | * Get local address. 142 | * 143 | * @return string 144 | */ 145 | abstract public function getLocalAddress(): string; 146 | 147 | /** 148 | * Close connection. 149 | * 150 | * @param mixed $data 151 | * @param bool $raw 152 | * @return void 153 | */ 154 | abstract public function close(mixed $data = null, bool $raw = false): void; 155 | 156 | /** 157 | * Is ipv4. 158 | * 159 | * return bool. 160 | */ 161 | abstract public function isIpV4(): bool; 162 | 163 | /** 164 | * Is ipv6. 165 | * 166 | * return bool. 167 | */ 168 | abstract public function isIpV6(): bool; 169 | 170 | /** 171 | * @param Throwable $exception 172 | * @return void 173 | */ 174 | public function error(Throwable $exception): void 175 | { 176 | if (!$this->errorHandler) { 177 | Worker::stopAll(250, $exception); 178 | return; 179 | } 180 | try { 181 | ($this->errorHandler)($exception); 182 | } catch (Throwable $exception) { 183 | Worker::stopAll(250, $exception); 184 | return; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Connection/UdpConnection.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Connection; 18 | 19 | use JsonSerializable; 20 | use Workerman\Protocols\ProtocolInterface; 21 | use function stream_socket_get_name; 22 | use function stream_socket_sendto; 23 | use function strlen; 24 | use function strrchr; 25 | use function strrpos; 26 | use function substr; 27 | use function trim; 28 | 29 | /** 30 | * UdpConnection. 31 | */ 32 | class UdpConnection extends ConnectionInterface implements JsonSerializable 33 | { 34 | /** 35 | * Max udp package size. 36 | * 37 | * @var int 38 | */ 39 | public const MAX_UDP_PACKAGE_SIZE = 65535; 40 | 41 | /** 42 | * Transport layer protocol. 43 | * 44 | * @var string 45 | */ 46 | public string $transport = 'udp'; 47 | 48 | /** 49 | * Construct. 50 | * 51 | * @param resource $socket 52 | * @param string $remoteAddress 53 | */ 54 | public function __construct( 55 | protected $socket, 56 | protected string $remoteAddress) {} 57 | 58 | /** 59 | * Sends data on the connection. 60 | * 61 | * @param mixed $sendBuffer 62 | * @param bool $raw 63 | * @return bool|null 64 | */ 65 | public function send(mixed $sendBuffer, bool $raw = false): bool|null 66 | { 67 | if (false === $raw && $this->protocol) { 68 | $sendBuffer = $this->protocol::encode($sendBuffer, $this); 69 | if ($sendBuffer === '') { 70 | return null; 71 | } 72 | } 73 | return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer, 0, $this->isIpV6() ? '[' . $this->getRemoteIp() . ']:' . $this->getRemotePort() : $this->remoteAddress); 74 | } 75 | 76 | /** 77 | * Get remote IP. 78 | * 79 | * @return string 80 | */ 81 | public function getRemoteIp(): string 82 | { 83 | $pos = strrpos($this->remoteAddress, ':'); 84 | if ($pos) { 85 | return trim(substr($this->remoteAddress, 0, $pos), '[]'); 86 | } 87 | return ''; 88 | } 89 | 90 | /** 91 | * Get remote port. 92 | * 93 | * @return int 94 | */ 95 | public function getRemotePort(): int 96 | { 97 | if ($this->remoteAddress) { 98 | return (int)substr(strrchr($this->remoteAddress, ':'), 1); 99 | } 100 | return 0; 101 | } 102 | 103 | /** 104 | * Get remote address. 105 | * 106 | * @return string 107 | */ 108 | public function getRemoteAddress(): string 109 | { 110 | return $this->remoteAddress; 111 | } 112 | 113 | /** 114 | * Get local IP. 115 | * 116 | * @return string 117 | */ 118 | public function getLocalIp(): string 119 | { 120 | $address = $this->getLocalAddress(); 121 | $pos = strrpos($address, ':'); 122 | if (!$pos) { 123 | return ''; 124 | } 125 | return substr($address, 0, $pos); 126 | } 127 | 128 | /** 129 | * Get local port. 130 | * 131 | * @return int 132 | */ 133 | public function getLocalPort(): int 134 | { 135 | $address = $this->getLocalAddress(); 136 | $pos = strrpos($address, ':'); 137 | if (!$pos) { 138 | return 0; 139 | } 140 | return (int)substr(strrchr($address, ':'), 1); 141 | } 142 | 143 | /** 144 | * Get local address. 145 | * 146 | * @return string 147 | */ 148 | public function getLocalAddress(): string 149 | { 150 | return (string)@stream_socket_get_name($this->socket, false); 151 | } 152 | 153 | 154 | /** 155 | * Close connection. 156 | * 157 | * @param mixed $data 158 | * @param bool $raw 159 | * @return void 160 | */ 161 | public function close(mixed $data = null, bool $raw = false): void 162 | { 163 | if ($data !== null) { 164 | $this->send($data, $raw); 165 | } 166 | $this->eventLoop = $this->errorHandler = null; 167 | } 168 | 169 | /** 170 | * Is ipv4. 171 | * 172 | * return bool. 173 | */ 174 | public function isIpV4(): bool 175 | { 176 | if ($this->transport === 'unix') { 177 | return false; 178 | } 179 | return !str_contains($this->getRemoteIp(), ':'); 180 | } 181 | 182 | /** 183 | * Is ipv6. 184 | * 185 | * return bool. 186 | */ 187 | public function isIpV6(): bool 188 | { 189 | if ($this->transport === 'unix') { 190 | return false; 191 | } 192 | return str_contains($this->getRemoteIp(), ':'); 193 | } 194 | 195 | /** 196 | * Get the real socket. 197 | * 198 | * @return resource 199 | */ 200 | public function getSocket() 201 | { 202 | return $this->socket; 203 | } 204 | 205 | /** 206 | * Get the json_encode information. 207 | * 208 | * @return array 209 | */ 210 | public function jsonSerialize(): array 211 | { 212 | return [ 213 | 'transport' => $this->transport, 214 | 'getRemoteIp' => $this->getRemoteIp(), 215 | 'remotePort' => $this->getRemotePort(), 216 | 'getRemoteAddress' => $this->getRemoteAddress(), 217 | 'getLocalIp' => $this->getLocalIp(), 218 | 'getLocalPort' => $this->getLocalPort(), 219 | 'getLocalAddress' => $this->getLocalAddress(), 220 | 'isIpV4' => $this->isIpV4(), 221 | 'isIpV6' => $this->isIpV6(), 222 | ]; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Events/Ev.php: -------------------------------------------------------------------------------- 1 | 10 | * @link http://www.workerman.net/ 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Workerman\Events; 17 | 18 | /** 19 | * Ev eventloop 20 | */ 21 | final class Ev implements EventInterface 22 | { 23 | /** 24 | * All listeners for read event. 25 | * 26 | * @var array 27 | */ 28 | private array $readEvents = []; 29 | 30 | /** 31 | * All listeners for write event. 32 | * 33 | * @var array 34 | */ 35 | private array $writeEvents = []; 36 | 37 | /** 38 | * Event listeners of signal. 39 | * 40 | * @var array 41 | */ 42 | private array $eventSignal = []; 43 | 44 | /** 45 | * All timer event listeners. 46 | * 47 | * @var array 48 | */ 49 | private array $eventTimer = []; 50 | 51 | /** 52 | * @var ?callable 53 | */ 54 | private $errorHandler = null; 55 | 56 | /** 57 | * Timer id. 58 | * 59 | * @var int 60 | */ 61 | private static int $timerId = 1; 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function delay(float $delay, callable $func, array $args = []): int 67 | { 68 | $timerId = self::$timerId; 69 | $event = new \EvTimer($delay, 0, function () use ($func, $args, $timerId) { 70 | unset($this->eventTimer[$timerId]); 71 | $this->safeCall($func, $args); 72 | }); 73 | $this->eventTimer[self::$timerId] = $event; 74 | return self::$timerId++; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function offDelay(int $timerId): bool 81 | { 82 | if (isset($this->eventTimer[$timerId])) { 83 | $this->eventTimer[$timerId]->stop(); 84 | unset($this->eventTimer[$timerId]); 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function offRepeat(int $timerId): bool 94 | { 95 | return $this->offDelay($timerId); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function repeat(float $interval, callable $func, array $args = []): int 102 | { 103 | $event = new \EvTimer($interval, $interval, fn () => $this->safeCall($func, $args)); 104 | $this->eventTimer[self::$timerId] = $event; 105 | return self::$timerId++; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function onReadable($stream, callable $func): void 112 | { 113 | $fdKey = (int)$stream; 114 | $event = new \EvIo($stream, \Ev::READ, fn () => $this->safeCall($func, [$stream])); 115 | $this->readEvents[$fdKey] = $event; 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function offReadable($stream): bool 122 | { 123 | $fdKey = (int)$stream; 124 | if (isset($this->readEvents[$fdKey])) { 125 | $this->readEvents[$fdKey]->stop(); 126 | unset($this->readEvents[$fdKey]); 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function onWritable($stream, callable $func): void 136 | { 137 | $fdKey = (int)$stream; 138 | $event = new \EvIo($stream, \Ev::WRITE, fn () => $this->safeCall($func, [$stream])); 139 | $this->writeEvents[$fdKey] = $event; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function offWritable($stream): bool 146 | { 147 | $fdKey = (int)$stream; 148 | if (isset($this->writeEvents[$fdKey])) { 149 | $this->writeEvents[$fdKey]->stop(); 150 | unset($this->writeEvents[$fdKey]); 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function onSignal(int $signal, callable $func): void 160 | { 161 | $event = new \EvSignal($signal, fn () => $this->safeCall($func, [$signal])); 162 | $this->eventSignal[$signal] = $event; 163 | } 164 | 165 | /** 166 | * {@inheritdoc} 167 | */ 168 | public function offSignal(int $signal): bool 169 | { 170 | if (isset($this->eventSignal[$signal])) { 171 | $this->eventSignal[$signal]->stop(); 172 | unset($this->eventSignal[$signal]); 173 | return true; 174 | } 175 | return false; 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function deleteAllTimer(): void 182 | { 183 | foreach ($this->eventTimer as $event) { 184 | $event->stop(); 185 | } 186 | $this->eventTimer = []; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function run(): void 193 | { 194 | \Ev::run(); 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function stop(): void 201 | { 202 | \Ev::stop(); 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function getTimerCount(): int 209 | { 210 | return count($this->eventTimer); 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function setErrorHandler(callable $errorHandler): void 217 | { 218 | $this->errorHandler = $errorHandler; 219 | } 220 | 221 | /** 222 | * @param callable $func 223 | * @param array $args 224 | * @return void 225 | */ 226 | private function safeCall(callable $func, array $args = []): void 227 | { 228 | try { 229 | $func(...$args); 230 | } catch (\Throwable $e) { 231 | if ($this->errorHandler === null) { 232 | echo $e; 233 | } else { 234 | ($this->errorHandler)($e); 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Events/Event.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Events; 18 | 19 | /** 20 | * libevent eventloop 21 | */ 22 | final class Event implements EventInterface 23 | { 24 | /** 25 | * Event base. 26 | * 27 | * @var \EventBase 28 | */ 29 | private \EventBase $eventBase; 30 | 31 | /** 32 | * All listeners for read event. 33 | * 34 | * @var array 35 | */ 36 | private array $readEvents = []; 37 | 38 | /** 39 | * All listeners for write event. 40 | * 41 | * @var array 42 | */ 43 | private array $writeEvents = []; 44 | 45 | /** 46 | * Event listeners of signal. 47 | * 48 | * @var array 49 | */ 50 | private array $eventSignal = []; 51 | 52 | /** 53 | * All timer event listeners. 54 | * 55 | * @var array 56 | */ 57 | private array $eventTimer = []; 58 | 59 | /** 60 | * Timer id. 61 | * 62 | * @var int 63 | */ 64 | private int $timerId = 0; 65 | 66 | /** 67 | * Event class name. 68 | * 69 | * @var string 70 | */ 71 | private string $eventClassName = ''; 72 | 73 | /** 74 | * @var ?callable 75 | */ 76 | private $errorHandler = null; 77 | 78 | /** 79 | * Construct. 80 | */ 81 | public function __construct() 82 | { 83 | if (\class_exists('\\\\Event', false)) { 84 | $className = '\\\\Event'; 85 | } else { 86 | $className = '\Event'; 87 | } 88 | $this->eventClassName = $className; 89 | if (\class_exists('\\\\EventBase', false)) { 90 | $className = '\\\\EventBase'; 91 | } else { 92 | $className = '\EventBase'; 93 | } 94 | $this->eventBase = new $className(); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function delay(float $delay, callable $func, array $args = []): int 101 | { 102 | $className = $this->eventClassName; 103 | $timerId = $this->timerId++; 104 | $event = new $className($this->eventBase, -1, $className::TIMEOUT, function () use ($func, $args, $timerId) { 105 | unset($this->eventTimer[$timerId]); 106 | $this->safeCall($func, $args); 107 | }); 108 | if (!$event->addTimer($delay)) { 109 | throw new \RuntimeException("Event::addTimer($delay) failed"); 110 | } 111 | $this->eventTimer[$timerId] = $event; 112 | return $timerId; 113 | } 114 | 115 | /** 116 | * {@inheritdoc} 117 | */ 118 | public function offDelay(int $timerId): bool 119 | { 120 | if (isset($this->eventTimer[$timerId])) { 121 | $this->eventTimer[$timerId]->del(); 122 | unset($this->eventTimer[$timerId]); 123 | return true; 124 | } 125 | return false; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function offRepeat(int $timerId): bool 132 | { 133 | return $this->offDelay($timerId); 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function repeat(float $interval, callable $func, array $args = []): int 140 | { 141 | $className = $this->eventClassName; 142 | $timerId = $this->timerId++; 143 | $event = new $className($this->eventBase, -1, $className::TIMEOUT | $className::PERSIST, function () use ($func, $args) { 144 | $this->safeCall($func, $args); 145 | }); 146 | if (!$event->addTimer($interval)) { 147 | throw new \RuntimeException("Event::addTimer($interval) failed"); 148 | } 149 | $this->eventTimer[$timerId] = $event; 150 | return $timerId; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | public function onReadable($stream, callable $func): void 157 | { 158 | $className = $this->eventClassName; 159 | $fdKey = (int)$stream; 160 | $event = new $className($this->eventBase, $stream, $className::READ | $className::PERSIST, $func); 161 | if ($event->add()) { 162 | $this->readEvents[$fdKey] = $event; 163 | } 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | */ 169 | public function offReadable($stream): bool 170 | { 171 | $fdKey = (int)$stream; 172 | if (isset($this->readEvents[$fdKey])) { 173 | $this->readEvents[$fdKey]->del(); 174 | unset($this->readEvents[$fdKey]); 175 | return true; 176 | } 177 | return false; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function onWritable($stream, callable $func): void 184 | { 185 | $className = $this->eventClassName; 186 | $fdKey = (int)$stream; 187 | $event = new $className($this->eventBase, $stream, $className::WRITE | $className::PERSIST, $func); 188 | if ($event->add()) { 189 | $this->writeEvents[$fdKey] = $event; 190 | } 191 | } 192 | 193 | /** 194 | * {@inheritdoc} 195 | */ 196 | public function offWritable($stream): bool 197 | { 198 | $fdKey = (int)$stream; 199 | if (isset($this->writeEvents[$fdKey])) { 200 | $this->writeEvents[$fdKey]->del(); 201 | unset($this->writeEvents[$fdKey]); 202 | return true; 203 | } 204 | return false; 205 | } 206 | 207 | /** 208 | * {@inheritdoc} 209 | */ 210 | public function onSignal(int $signal, callable $func): void 211 | { 212 | $className = $this->eventClassName; 213 | $fdKey = $signal; 214 | $event = $className::signal($this->eventBase, $signal, fn () => $this->safeCall($func, [$signal])); 215 | if ($event->add()) { 216 | $this->eventSignal[$fdKey] = $event; 217 | } 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function offSignal(int $signal): bool 224 | { 225 | $fdKey = $signal; 226 | if (isset($this->eventSignal[$fdKey])) { 227 | $this->eventSignal[$fdKey]->del(); 228 | unset($this->eventSignal[$fdKey]); 229 | return true; 230 | } 231 | return false; 232 | } 233 | 234 | /** 235 | * {@inheritdoc} 236 | */ 237 | public function deleteAllTimer(): void 238 | { 239 | foreach ($this->eventTimer as $event) { 240 | $event->del(); 241 | } 242 | $this->eventTimer = []; 243 | } 244 | 245 | /** 246 | * {@inheritdoc} 247 | */ 248 | public function run(): void 249 | { 250 | $this->eventBase->loop(); 251 | } 252 | 253 | /** 254 | * {@inheritdoc} 255 | */ 256 | public function stop(): void 257 | { 258 | $this->eventBase->exit(); 259 | } 260 | 261 | /** 262 | * {@inheritdoc} 263 | */ 264 | public function getTimerCount(): int 265 | { 266 | return \count($this->eventTimer); 267 | } 268 | 269 | /** 270 | * {@inheritdoc} 271 | */ 272 | public function setErrorHandler(callable $errorHandler): void 273 | { 274 | $this->errorHandler = $errorHandler; 275 | } 276 | 277 | /** 278 | * @param callable $func 279 | * @param array $args 280 | * @return void 281 | */ 282 | private function safeCall(callable $func, array $args = []): void 283 | { 284 | try { 285 | $func(...$args); 286 | } catch (\Throwable $e) { 287 | if ($this->errorHandler === null) { 288 | echo $e; 289 | } else { 290 | ($this->errorHandler)($e); 291 | } 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/Events/EventInterface.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Events; 18 | 19 | interface EventInterface 20 | { 21 | /** 22 | * Delay the execution of a callback. 23 | * 24 | * @param float $delay 25 | * @param callable(mixed...): void $func 26 | * @param array $args 27 | * @return int 28 | */ 29 | public function delay(float $delay, callable $func, array $args = []): int; 30 | 31 | /** 32 | * Delete a delay timer. 33 | * 34 | * @param int $timerId 35 | * @return bool 36 | */ 37 | public function offDelay(int $timerId): bool; 38 | 39 | /** 40 | * Repeatedly execute a callback. 41 | * 42 | * @param float $interval 43 | * @param callable(mixed...): void $func 44 | * @param array $args 45 | * @return int 46 | */ 47 | public function repeat(float $interval, callable $func, array $args = []): int; 48 | 49 | /** 50 | * Delete a repeat timer. 51 | * 52 | * @param int $timerId 53 | * @return bool 54 | */ 55 | public function offRepeat(int $timerId): bool; 56 | 57 | /** 58 | * Execute a callback when a stream resource becomes readable or is closed for reading. 59 | * 60 | * @param resource $stream 61 | * @param callable(resource): void $func 62 | * @return void 63 | */ 64 | public function onReadable($stream, callable $func): void; 65 | 66 | /** 67 | * Cancel a callback of stream readable. 68 | * 69 | * @param resource $stream 70 | * @return bool 71 | */ 72 | public function offReadable($stream): bool; 73 | 74 | /** 75 | * Execute a callback when a stream resource becomes writable or is closed for writing. 76 | * 77 | * @param resource $stream 78 | * @param callable(resource): void $func 79 | * @return void 80 | */ 81 | public function onWritable($stream, callable $func): void; 82 | 83 | /** 84 | * Cancel a callback of stream writable. 85 | * 86 | * @param resource $stream 87 | * @return bool 88 | */ 89 | public function offWritable($stream): bool; 90 | 91 | /** 92 | * Execute a callback when a signal is received. 93 | * 94 | * @param int $signal 95 | * @param callable(int): void $func 96 | * @return void 97 | */ 98 | public function onSignal(int $signal, callable $func): void; 99 | 100 | /** 101 | * Cancel a callback of signal. 102 | * 103 | * @param int $signal 104 | * @return bool 105 | */ 106 | public function offSignal(int $signal): bool; 107 | 108 | /** 109 | * Delete all timer. 110 | * 111 | * @return void 112 | */ 113 | public function deleteAllTimer(): void; 114 | 115 | /** 116 | * Run the event loop. 117 | * 118 | * @return void 119 | */ 120 | public function run(): void; 121 | 122 | /** 123 | * Stop event loop. 124 | * 125 | * @return void 126 | */ 127 | public function stop(): void; 128 | 129 | /** 130 | * Get Timer count. 131 | * 132 | * @return int 133 | */ 134 | public function getTimerCount(): int; 135 | 136 | /** 137 | * Set error handler. 138 | * 139 | * @param callable(\Throwable): void $errorHandler 140 | * @return void 141 | */ 142 | public function setErrorHandler(callable $errorHandler): void; 143 | } 144 | -------------------------------------------------------------------------------- /src/Events/Fiber.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Events; 18 | 19 | use Fiber as BaseFiber; 20 | use Revolt\EventLoop; 21 | use Revolt\EventLoop\Driver; 22 | use function count; 23 | use function function_exists; 24 | use function pcntl_signal; 25 | 26 | /** 27 | * Revolt eventloop 28 | */ 29 | final class Fiber implements EventInterface 30 | { 31 | /** 32 | * @var Driver 33 | */ 34 | private Driver $driver; 35 | 36 | /** 37 | * All listeners for read event. 38 | * 39 | * @var array 40 | */ 41 | private array $readEvents = []; 42 | 43 | /** 44 | * All listeners for write event. 45 | * 46 | * @var array 47 | */ 48 | private array $writeEvents = []; 49 | 50 | /** 51 | * Event listeners of signal. 52 | * 53 | * @var array 54 | */ 55 | private array $eventSignal = []; 56 | 57 | /** 58 | * Event listeners of timer. 59 | * 60 | * @var array 61 | */ 62 | private array $eventTimer = []; 63 | 64 | /** 65 | * Timer id. 66 | * 67 | * @var int 68 | */ 69 | private int $timerId = 1; 70 | 71 | /** 72 | * Construct. 73 | */ 74 | public function __construct() 75 | { 76 | $this->driver = EventLoop::getDriver(); 77 | } 78 | 79 | /** 80 | * Get driver. 81 | * 82 | * @return Driver 83 | */ 84 | public function driver(): Driver 85 | { 86 | return $this->driver; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function run(): void 93 | { 94 | $this->driver->run(); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function stop(): void 101 | { 102 | foreach ($this->eventSignal as $cbId) { 103 | $this->driver->cancel($cbId); 104 | } 105 | $this->driver->stop(); 106 | if (function_exists('pcntl_signal')) { 107 | pcntl_signal(SIGINT, SIG_IGN); 108 | } 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function delay(float $delay, callable $func, array $args = []): int 115 | { 116 | $timerId = $this->timerId++; 117 | $closure = function () use ($func, $args, $timerId) { 118 | unset($this->eventTimer[$timerId]); 119 | $this->safeCall($func, ...$args); 120 | }; 121 | $cbId = $this->driver->delay($delay, $closure); 122 | $this->eventTimer[$timerId] = $cbId; 123 | return $timerId; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function repeat(float $interval, callable $func, array $args = []): int 130 | { 131 | $timerId = $this->timerId++; 132 | $cbId = $this->driver->repeat($interval, fn() => $this->safeCall($func, ...$args)); 133 | $this->eventTimer[$timerId] = $cbId; 134 | return $timerId; 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function onReadable($stream, callable $func): void 141 | { 142 | $fdKey = (int)$stream; 143 | if (isset($this->readEvents[$fdKey])) { 144 | $this->driver->cancel($this->readEvents[$fdKey]); 145 | } 146 | 147 | $this->readEvents[$fdKey] = $this->driver->onReadable($stream, fn() => $this->safeCall($func, $stream)); 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function offReadable($stream): bool 154 | { 155 | $fdKey = (int)$stream; 156 | if (isset($this->readEvents[$fdKey])) { 157 | $this->driver->cancel($this->readEvents[$fdKey]); 158 | unset($this->readEvents[$fdKey]); 159 | return true; 160 | } 161 | return false; 162 | } 163 | 164 | /** 165 | * {@inheritdoc} 166 | */ 167 | public function onWritable($stream, callable $func): void 168 | { 169 | $fdKey = (int)$stream; 170 | if (isset($this->writeEvents[$fdKey])) { 171 | $this->driver->cancel($this->writeEvents[$fdKey]); 172 | unset($this->writeEvents[$fdKey]); 173 | } 174 | $this->writeEvents[$fdKey] = $this->driver->onWritable($stream, fn() => $this->safeCall($func, $stream)); 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function offWritable($stream): bool 181 | { 182 | $fdKey = (int)$stream; 183 | if (isset($this->writeEvents[$fdKey])) { 184 | $this->driver->cancel($this->writeEvents[$fdKey]); 185 | unset($this->writeEvents[$fdKey]); 186 | return true; 187 | } 188 | return false; 189 | } 190 | 191 | /** 192 | * {@inheritdoc} 193 | */ 194 | public function onSignal(int $signal, callable $func): void 195 | { 196 | $fdKey = $signal; 197 | if (isset($this->eventSignal[$fdKey])) { 198 | $this->driver->cancel($this->eventSignal[$fdKey]); 199 | unset($this->eventSignal[$fdKey]); 200 | } 201 | $this->eventSignal[$fdKey] = $this->driver->onSignal($signal, fn() => $this->safeCall($func, $signal)); 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | */ 207 | public function offSignal(int $signal): bool 208 | { 209 | $fdKey = $signal; 210 | if (isset($this->eventSignal[$fdKey])) { 211 | $this->driver->cancel($this->eventSignal[$fdKey]); 212 | unset($this->eventSignal[$fdKey]); 213 | return true; 214 | } 215 | return false; 216 | } 217 | 218 | /** 219 | * {@inheritdoc} 220 | */ 221 | public function offDelay(int $timerId): bool 222 | { 223 | if (isset($this->eventTimer[$timerId])) { 224 | $this->driver->cancel($this->eventTimer[$timerId]); 225 | unset($this->eventTimer[$timerId]); 226 | return true; 227 | } 228 | return false; 229 | } 230 | 231 | /** 232 | * {@inheritdoc} 233 | */ 234 | public function offRepeat(int $timerId): bool 235 | { 236 | return $this->offDelay($timerId); 237 | } 238 | 239 | /** 240 | * {@inheritdoc} 241 | */ 242 | public function deleteAllTimer(): void 243 | { 244 | foreach ($this->eventTimer as $cbId) { 245 | $this->driver->cancel($cbId); 246 | } 247 | $this->eventTimer = []; 248 | } 249 | 250 | /** 251 | * {@inheritdoc} 252 | */ 253 | public function getTimerCount(): int 254 | { 255 | return count($this->eventTimer); 256 | } 257 | 258 | /** 259 | * {@inheritdoc} 260 | */ 261 | public function setErrorHandler(callable $errorHandler): void 262 | { 263 | $this->driver->setErrorHandler($errorHandler); 264 | } 265 | 266 | /** 267 | * @param callable $func 268 | * @param ...$args 269 | * @return void 270 | * @throws \Throwable 271 | */ 272 | protected function safeCall(callable $func, ...$args): void 273 | { 274 | (new BaseFiber(fn() => $func(...$args)))->start(); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/Events/Select.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Events; 18 | 19 | use SplPriorityQueue; 20 | use Throwable; 21 | use function count; 22 | use function max; 23 | use function microtime; 24 | use function pcntl_signal; 25 | use function pcntl_signal_dispatch; 26 | use const DIRECTORY_SEPARATOR; 27 | 28 | /** 29 | * select eventloop 30 | */ 31 | final class Select implements EventInterface 32 | { 33 | /** 34 | * Running. 35 | * 36 | * @var bool 37 | */ 38 | private bool $running = true; 39 | 40 | /** 41 | * All listeners for read/write event. 42 | * 43 | * @var array 44 | */ 45 | private array $readEvents = []; 46 | 47 | /** 48 | * All listeners for read/write event. 49 | * 50 | * @var array 51 | */ 52 | private array $writeEvents = []; 53 | 54 | /** 55 | * @var array 56 | */ 57 | private array $exceptEvents = []; 58 | 59 | /** 60 | * Event listeners of signal. 61 | * 62 | * @var array 63 | */ 64 | private array $signalEvents = []; 65 | 66 | /** 67 | * Fds waiting for read event. 68 | * 69 | * @var array 70 | */ 71 | private array $readFds = []; 72 | 73 | /** 74 | * Fds waiting for write event. 75 | * 76 | * @var array 77 | */ 78 | private array $writeFds = []; 79 | 80 | /** 81 | * Fds waiting for except event. 82 | * 83 | * @var array 84 | */ 85 | private array $exceptFds = []; 86 | 87 | /** 88 | * Timer scheduler. 89 | * {['data':timer_id, 'priority':run_timestamp], ..} 90 | * 91 | * @var SplPriorityQueue 92 | */ 93 | private SplPriorityQueue $scheduler; 94 | 95 | /** 96 | * All timer event listeners. 97 | * [[func, args, flag, timer_interval], ..] 98 | * 99 | * @var array 100 | */ 101 | private array $eventTimer = []; 102 | 103 | /** 104 | * Timer id. 105 | * 106 | * @var int 107 | */ 108 | private int $timerId = 1; 109 | 110 | /** 111 | * Select timeout. 112 | * 113 | * @var int 114 | */ 115 | private int $selectTimeout = self::MAX_SELECT_TIMOUT_US; 116 | 117 | /** 118 | * Next run time of the timer. 119 | * 120 | * @var float 121 | */ 122 | private float $nextTickTime = 0; 123 | 124 | /** 125 | * @var ?callable 126 | */ 127 | private $errorHandler = null; 128 | 129 | /** 130 | * Select timeout. 131 | * 132 | * @var int 133 | */ 134 | const MAX_SELECT_TIMOUT_US = 800000; 135 | 136 | /** 137 | * Construct. 138 | */ 139 | public function __construct() 140 | { 141 | $this->scheduler = new SplPriorityQueue(); 142 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function delay(float $delay, callable $func, array $args = []): int 149 | { 150 | $timerId = $this->timerId++; 151 | $runTime = microtime(true) + $delay; 152 | $this->scheduler->insert($timerId, -$runTime); 153 | $this->eventTimer[$timerId] = [$func, $args]; 154 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) { 155 | $this->setNextTickTime($runTime); 156 | } 157 | return $timerId; 158 | } 159 | 160 | /** 161 | * {@inheritdoc} 162 | */ 163 | public function repeat(float $interval, callable $func, array $args = []): int 164 | { 165 | $timerId = $this->timerId++; 166 | $runTime = microtime(true) + $interval; 167 | $this->scheduler->insert($timerId, -$runTime); 168 | $this->eventTimer[$timerId] = [$func, $args, $interval]; 169 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) { 170 | $this->setNextTickTime($runTime); 171 | } 172 | return $timerId; 173 | } 174 | 175 | /** 176 | * {@inheritdoc} 177 | */ 178 | public function offDelay(int $timerId): bool 179 | { 180 | if (isset($this->eventTimer[$timerId])) { 181 | unset($this->eventTimer[$timerId]); 182 | return true; 183 | } 184 | return false; 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function offRepeat(int $timerId): bool 191 | { 192 | return $this->offDelay($timerId); 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | public function onReadable($stream, callable $func): void 199 | { 200 | $count = count($this->readFds); 201 | if ($count >= 1024) { 202 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING); 203 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) { 204 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING); 205 | } 206 | $fdKey = (int)$stream; 207 | $this->readEvents[$fdKey] = $func; 208 | $this->readFds[$fdKey] = $stream; 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | public function offReadable($stream): bool 215 | { 216 | $fdKey = (int)$stream; 217 | if (isset($this->readEvents[$fdKey])) { 218 | unset($this->readEvents[$fdKey], $this->readFds[$fdKey]); 219 | return true; 220 | } 221 | return false; 222 | } 223 | 224 | /** 225 | * {@inheritdoc} 226 | */ 227 | public function onWritable($stream, callable $func): void 228 | { 229 | $count = count($this->writeFds); 230 | if ($count >= 1024) { 231 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING); 232 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) { 233 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING); 234 | } 235 | $fdKey = (int)$stream; 236 | $this->writeEvents[$fdKey] = $func; 237 | $this->writeFds[$fdKey] = $stream; 238 | } 239 | 240 | /** 241 | * {@inheritdoc} 242 | */ 243 | public function offWritable($stream): bool 244 | { 245 | $fdKey = (int)$stream; 246 | if (isset($this->writeEvents[$fdKey])) { 247 | unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]); 248 | return true; 249 | } 250 | return false; 251 | } 252 | 253 | /** 254 | * On except. 255 | * 256 | * @param resource $stream 257 | * @param callable $func 258 | */ 259 | public function onExcept($stream, callable $func): void 260 | { 261 | $fdKey = (int)$stream; 262 | $this->exceptEvents[$fdKey] = $func; 263 | $this->exceptFds[$fdKey] = $stream; 264 | } 265 | 266 | /** 267 | * Off except. 268 | * 269 | * @param resource $stream 270 | * @return bool 271 | */ 272 | public function offExcept($stream): bool 273 | { 274 | $fdKey = (int)$stream; 275 | if (isset($this->exceptEvents[$fdKey])) { 276 | unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]); 277 | return true; 278 | } 279 | return false; 280 | } 281 | 282 | /** 283 | * {@inheritdoc} 284 | */ 285 | public function onSignal(int $signal, callable $func): void 286 | { 287 | if (!function_exists('pcntl_signal')) { 288 | return; 289 | } 290 | $this->signalEvents[$signal] = $func; 291 | pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal])); 292 | } 293 | 294 | /** 295 | * {@inheritdoc} 296 | */ 297 | public function offSignal(int $signal): bool 298 | { 299 | if (!function_exists('pcntl_signal')) { 300 | return false; 301 | } 302 | pcntl_signal($signal, SIG_IGN); 303 | if (isset($this->signalEvents[$signal])) { 304 | unset($this->signalEvents[$signal]); 305 | return true; 306 | } 307 | return false; 308 | } 309 | 310 | /** 311 | * Tick for timer. 312 | * 313 | * @return void 314 | */ 315 | protected function tick(): void 316 | { 317 | $tasksToInsert = []; 318 | while (!$this->scheduler->isEmpty()) { 319 | $schedulerData = $this->scheduler->top(); 320 | $timerId = $schedulerData['data']; 321 | $nextRunTime = -$schedulerData['priority']; 322 | $timeNow = microtime(true); 323 | $this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000); 324 | 325 | if ($this->selectTimeout <= 0) { 326 | $this->scheduler->extract(); 327 | 328 | if (!isset($this->eventTimer[$timerId])) { 329 | continue; 330 | } 331 | 332 | // [func, args, timer_interval] 333 | $taskData = $this->eventTimer[$timerId]; 334 | if (isset($taskData[2])) { 335 | $nextRunTime = $timeNow + $taskData[2]; 336 | $tasksToInsert[] = [$timerId, -$nextRunTime]; 337 | } else { 338 | unset($this->eventTimer[$timerId]); 339 | } 340 | $this->safeCall($taskData[0], $taskData[1]); 341 | } else { 342 | break; 343 | } 344 | } 345 | foreach ($tasksToInsert as $item) { 346 | $this->scheduler->insert($item[0], $item[1]); 347 | } 348 | if (!$this->scheduler->isEmpty()) { 349 | $schedulerData = $this->scheduler->top(); 350 | $nextRunTime = -$schedulerData['priority']; 351 | $this->setNextTickTime($nextRunTime); 352 | return; 353 | } 354 | $this->setNextTickTime(0); 355 | } 356 | 357 | /** 358 | * Set next tick time. 359 | * 360 | * @param float $nextTickTime 361 | * @return void 362 | */ 363 | protected function setNextTickTime(float $nextTickTime): void 364 | { 365 | $this->nextTickTime = $nextTickTime; 366 | if ($nextTickTime == 0) { 367 | // Swow will affect the signal interruption characteristics of stream_select, 368 | // so a shorter timeout should be used to detect signals. 369 | $this->selectTimeout = self::MAX_SELECT_TIMOUT_US; 370 | return; 371 | } 372 | $this->selectTimeout = min(max((int)(($nextTickTime - microtime(true)) * 1000000), 0), self::MAX_SELECT_TIMOUT_US); 373 | } 374 | 375 | /** 376 | * {@inheritdoc} 377 | */ 378 | public function deleteAllTimer(): void 379 | { 380 | $this->scheduler = new SplPriorityQueue(); 381 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH); 382 | $this->eventTimer = []; 383 | } 384 | 385 | /** 386 | * {@inheritdoc} 387 | */ 388 | public function run(): void 389 | { 390 | while ($this->running) { 391 | $read = $this->readFds; 392 | $write = $this->writeFds; 393 | $except = $this->exceptFds; 394 | if ($read || $write || $except) { 395 | // Waiting read/write/signal/timeout events. 396 | try { 397 | @stream_select($read, $write, $except, 0, $this->selectTimeout); 398 | } catch (Throwable) { 399 | // do nothing 400 | } 401 | } else { 402 | $this->selectTimeout >= 1 && usleep($this->selectTimeout); 403 | } 404 | 405 | foreach ($read as $fd) { 406 | $fdKey = (int)$fd; 407 | if (isset($this->readEvents[$fdKey])) { 408 | $this->readEvents[$fdKey]($fd); 409 | } 410 | } 411 | 412 | foreach ($write as $fd) { 413 | $fdKey = (int)$fd; 414 | if (isset($this->writeEvents[$fdKey])) { 415 | $this->writeEvents[$fdKey]($fd); 416 | } 417 | } 418 | 419 | foreach ($except as $fd) { 420 | $fdKey = (int)$fd; 421 | if (isset($this->exceptEvents[$fdKey])) { 422 | $this->exceptEvents[$fdKey]($fd); 423 | } 424 | } 425 | 426 | if ($this->nextTickTime > 0 && microtime(true) >= $this->nextTickTime) { 427 | $this->tick(); 428 | } 429 | 430 | // The $this->signalEvents are empty under Windows, make sure not to call pcntl_signal_dispatch. 431 | if ($this->signalEvents) { 432 | // Calls signal handlers for pending signals 433 | pcntl_signal_dispatch(); 434 | } 435 | } 436 | } 437 | 438 | /** 439 | * {@inheritdoc} 440 | */ 441 | public function stop(): void 442 | { 443 | $this->running = false; 444 | $this->deleteAllTimer(); 445 | foreach ($this->signalEvents as $signal => $item) { 446 | $this->offsignal($signal); 447 | } 448 | $this->readFds = []; 449 | $this->writeFds = []; 450 | $this->exceptFds = []; 451 | $this->readEvents = []; 452 | $this->writeEvents = []; 453 | $this->exceptEvents = []; 454 | $this->signalEvents = []; 455 | } 456 | 457 | /** 458 | * {@inheritdoc} 459 | */ 460 | public function getTimerCount(): int 461 | { 462 | return count($this->eventTimer); 463 | } 464 | 465 | /** 466 | * {@inheritdoc} 467 | */ 468 | public function setErrorHandler(callable $errorHandler): void 469 | { 470 | $this->errorHandler = $errorHandler; 471 | } 472 | 473 | /** 474 | * @param callable $func 475 | * @param array $args 476 | * @return void 477 | */ 478 | private function safeCall(callable $func, array $args = []): void 479 | { 480 | try { 481 | $func(...$args); 482 | } catch (Throwable $e) { 483 | if ($this->errorHandler === null) { 484 | echo $e; 485 | } else { 486 | ($this->errorHandler)($e); 487 | } 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/Events/Swoole.php: -------------------------------------------------------------------------------- 1 | 10 | * @link http://www.workerman.net/ 11 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Workerman\Events; 17 | 18 | use Swoole\Coroutine; 19 | use Swoole\Event; 20 | use Swoole\Process; 21 | use Swoole\Timer; 22 | use Throwable; 23 | 24 | final class Swoole implements EventInterface 25 | { 26 | /** 27 | * All listeners for read timer 28 | * 29 | * @var array 30 | */ 31 | private array $eventTimer = []; 32 | 33 | /** 34 | * All listeners for read event. 35 | * 36 | * @var array 37 | */ 38 | private array $readEvents = []; 39 | 40 | /** 41 | * All listeners for write event. 42 | * 43 | * @var array 44 | */ 45 | private array $writeEvents = []; 46 | 47 | /** 48 | * @var ?callable 49 | */ 50 | private $errorHandler = null; 51 | 52 | private bool $stopping = false; 53 | 54 | /** 55 | * Constructor. 56 | */ 57 | public function __construct() 58 | { 59 | Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function delay(float $delay, callable $func, array $args = []): int 66 | { 67 | $t = (int)($delay * 1000); 68 | $t = max($t, 1); 69 | $timerId = Timer::after($t, function () use ($func, $args, &$timerId) { 70 | unset($this->eventTimer[$timerId]); 71 | $this->safeCall($func, $args); 72 | }); 73 | $this->eventTimer[$timerId] = $timerId; 74 | return $timerId; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function offDelay(int $timerId): bool 81 | { 82 | if (isset($this->eventTimer[$timerId])) { 83 | Timer::clear($timerId); 84 | unset($this->eventTimer[$timerId]); 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function offRepeat(int $timerId): bool 94 | { 95 | return $this->offDelay($timerId); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function repeat(float $interval, callable $func, array $args = []): int 102 | { 103 | $t = (int)($interval * 1000); 104 | $t = max($t, 1); 105 | $timerId = Timer::tick($t, function () use ($func, $args) { 106 | $this->safeCall($func, $args); 107 | }); 108 | $this->eventTimer[$timerId] = $timerId; 109 | return $timerId; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function onReadable($stream, callable $func): void 116 | { 117 | $fd = (int)$stream; 118 | if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) { 119 | Event::add($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ); 120 | } elseif (isset($this->writeEvents[$fd])) { 121 | Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); 122 | } else { 123 | Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ); 124 | } 125 | 126 | $this->readEvents[$fd] = [$func, [$stream]]; 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function offReadable($stream): bool 133 | { 134 | $fd = (int)$stream; 135 | if (!isset($this->readEvents[$fd])) { 136 | return false; 137 | } 138 | unset($this->readEvents[$fd]); 139 | if (!isset($this->writeEvents[$fd])) { 140 | Event::del($stream); 141 | return true; 142 | } 143 | Event::set($stream, null, null, SWOOLE_EVENT_WRITE); 144 | return true; 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function onWritable($stream, callable $func): void 151 | { 152 | $fd = (int)$stream; 153 | if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) { 154 | Event::add($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE); 155 | } elseif (isset($this->readEvents[$fd])) { 156 | Event::set($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE | SWOOLE_EVENT_READ); 157 | } else { 158 | Event::set($stream, null, fn () =>$this->callWrite($fd), SWOOLE_EVENT_WRITE); 159 | } 160 | 161 | $this->writeEvents[$fd] = [$func, [$stream]]; 162 | } 163 | 164 | /** 165 | * {@inheritdoc} 166 | */ 167 | public function offWritable($stream): bool 168 | { 169 | $fd = (int)$stream; 170 | if (!isset($this->writeEvents[$fd])) { 171 | return false; 172 | } 173 | unset($this->writeEvents[$fd]); 174 | if (!isset($this->readEvents[$fd])) { 175 | Event::del($stream); 176 | return true; 177 | } 178 | Event::set($stream, null, null, SWOOLE_EVENT_READ); 179 | return true; 180 | } 181 | 182 | /** 183 | * {@inheritdoc} 184 | */ 185 | public function onSignal(int $signal, callable $func): void 186 | { 187 | Process::signal($signal, fn () => $this->safeCall($func, [$signal])); 188 | } 189 | 190 | /** 191 | * Please see https://wiki.swoole.com/#/process/process?id=signal 192 | * {@inheritdoc} 193 | */ 194 | public function offSignal(int $signal): bool 195 | { 196 | return Process::signal($signal, null); 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function deleteAllTimer(): void 203 | { 204 | foreach ($this->eventTimer as $timerId) { 205 | Timer::clear($timerId); 206 | } 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | */ 212 | public function run(): void 213 | { 214 | // Avoid process exit due to no listening 215 | Timer::tick(100000000, static fn() => null); 216 | Event::wait(); 217 | } 218 | 219 | /** 220 | * Destroy loop. 221 | * 222 | * @return void 223 | */ 224 | public function stop(): void 225 | { 226 | if ($this->stopping) { 227 | return; 228 | } 229 | $this->stopping = true; 230 | // Cancel all coroutines before Event::exit 231 | foreach (Coroutine::listCoroutines() as $coroutine) { 232 | Coroutine::cancel($coroutine); 233 | } 234 | // Wait for coroutines to exit 235 | usleep(200000); 236 | Event::exit(); 237 | } 238 | 239 | /** 240 | * Get timer count. 241 | * 242 | * @return integer 243 | */ 244 | public function getTimerCount(): int 245 | { 246 | return count($this->eventTimer); 247 | } 248 | 249 | /** 250 | * {@inheritdoc} 251 | */ 252 | public function setErrorHandler(callable $errorHandler): void 253 | { 254 | $this->errorHandler = $errorHandler; 255 | } 256 | 257 | /** 258 | * @param $fd 259 | * @return void 260 | */ 261 | private function callRead($fd) 262 | { 263 | if (isset($this->readEvents[$fd])) { 264 | $this->safeCall($this->readEvents[$fd][0], $this->readEvents[$fd][1]); 265 | } 266 | } 267 | 268 | /** 269 | * @param $fd 270 | * @return void 271 | */ 272 | private function callWrite($fd) 273 | { 274 | if (isset($this->writeEvents[$fd])) { 275 | $this->safeCall($this->writeEvents[$fd][0], $this->writeEvents[$fd][1]); 276 | } 277 | } 278 | 279 | /** 280 | * @param callable $func 281 | * @param array $args 282 | * @return void 283 | */ 284 | private function safeCall(callable $func, array $args = []): void 285 | { 286 | Coroutine::create(function() use ($func, $args) { 287 | try { 288 | $func(...$args); 289 | } catch (Throwable $e) { 290 | if ($this->errorHandler === null) { 291 | echo $e; 292 | } else { 293 | ($this->errorHandler)($e); 294 | } 295 | } 296 | }); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Events/Swow.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $eventTimer = []; 21 | 22 | /** 23 | * All listeners for read event. 24 | * 25 | * @var array 26 | */ 27 | private array $readEvents = []; 28 | 29 | /** 30 | * All listeners for write event. 31 | * 32 | * @var array 33 | */ 34 | private array $writeEvents = []; 35 | 36 | /** 37 | * All listeners for signal. 38 | * 39 | * @var array 40 | */ 41 | private array $signalListener = []; 42 | 43 | /** 44 | * @var ?callable 45 | */ 46 | private $errorHandler = null; 47 | 48 | /** 49 | * Get timer count. 50 | * 51 | * @return integer 52 | */ 53 | public function getTimerCount(): int 54 | { 55 | return count($this->eventTimer); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function delay(float $delay, callable $func, array $args = []): int 62 | { 63 | $t = (int)($delay * 1000); 64 | $t = max($t, 1); 65 | $coroutine = Coroutine::run(function () use ($t, $func, $args): void { 66 | msleep($t); 67 | unset($this->eventTimer[Coroutine::getCurrent()->getId()]); 68 | $this->safeCall($func, $args); 69 | }); 70 | $timerId = $coroutine->getId(); 71 | $this->eventTimer[$timerId] = $timerId; 72 | return $timerId; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function repeat(float $interval, callable $func, array $args = []): int 79 | { 80 | $t = (int)($interval * 1000); 81 | $t = max($t, 1); 82 | $coroutine = Coroutine::run(function () use ($t, $func, $args): void { 83 | // @phpstan-ignore-next-line While loop condition is always true. 84 | while (true) { 85 | msleep($t); 86 | $this->safeCall($func, $args); 87 | } 88 | }); 89 | $timerId = $coroutine->getId(); 90 | $this->eventTimer[$timerId] = $timerId; 91 | return $timerId; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function offDelay(int $timerId): bool 98 | { 99 | if (isset($this->eventTimer[$timerId])) { 100 | try { 101 | (Coroutine::getAll()[$timerId])->kill(); 102 | return true; 103 | } finally { 104 | unset($this->eventTimer[$timerId]); 105 | } 106 | } 107 | return false; 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function offRepeat(int $timerId): bool 114 | { 115 | return $this->offDelay($timerId); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function deleteAllTimer(): void 122 | { 123 | foreach ($this->eventTimer as $timerId) { 124 | $this->offDelay($timerId); 125 | } 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function onReadable($stream, callable $func): void 132 | { 133 | $fd = (int)$stream; 134 | if (isset($this->readEvents[$fd])) { 135 | $this->offReadable($stream); 136 | } 137 | Coroutine::run(function () use ($stream, $func, $fd): void { 138 | try { 139 | $this->readEvents[$fd] = Coroutine::getCurrent(); 140 | while (true) { 141 | if (!is_resource($stream)) { 142 | $this->offReadable($stream); 143 | break; 144 | } 145 | // Under Windows, setting a timeout is necessary; otherwise, the accept cannot be listened to. 146 | // Setting it to 1000ms will result in a 1-second delay for the first accept under Windows. 147 | $rEvent = stream_poll_one($stream, STREAM_POLLIN | STREAM_POLLHUP, 1000); 148 | if (!isset($this->readEvents[$fd]) || $this->readEvents[$fd] !== Coroutine::getCurrent()) { 149 | break; 150 | } 151 | if ($rEvent !== STREAM_POLLNONE) { 152 | $this->safeCall($func, [$stream]); 153 | } 154 | if ($rEvent !== STREAM_POLLIN && $rEvent !== STREAM_POLLNONE) { 155 | $this->offReadable($stream); 156 | break; 157 | } 158 | } 159 | } catch (RuntimeException) { 160 | $this->offReadable($stream); 161 | } 162 | }); 163 | } 164 | 165 | /** 166 | * {@inheritdoc} 167 | */ 168 | public function offReadable($stream): bool 169 | { 170 | // 在当前协程执行 $coroutine->kill() 会导致不可预知问题,所以没有使用$coroutine->kill() 171 | $fd = (int)$stream; 172 | if (isset($this->readEvents[$fd])) { 173 | unset($this->readEvents[$fd]); 174 | return true; 175 | } 176 | return false; 177 | } 178 | 179 | /** 180 | * {@inheritdoc} 181 | */ 182 | public function onWritable($stream, callable $func): void 183 | { 184 | $fd = (int)$stream; 185 | if (isset($this->writeEvents[$fd])) { 186 | $this->offWritable($stream); 187 | } 188 | Coroutine::run(function () use ($stream, $func, $fd): void { 189 | try { 190 | $this->writeEvents[$fd] = Coroutine::getCurrent(); 191 | while (true) { 192 | $rEvent = stream_poll_one($stream, STREAM_POLLOUT | STREAM_POLLHUP); 193 | if (!isset($this->writeEvents[$fd]) || $this->writeEvents[$fd] !== Coroutine::getCurrent()) { 194 | break; 195 | } 196 | if ($rEvent !== STREAM_POLLNONE) { 197 | $this->safeCall($func, [$stream]); 198 | } 199 | if ($rEvent !== STREAM_POLLOUT) { 200 | $this->offWritable($stream); 201 | break; 202 | } 203 | } 204 | } catch (RuntimeException) { 205 | $this->offWritable($stream); 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * {@inheritdoc} 212 | */ 213 | public function offWritable($stream): bool 214 | { 215 | $fd = (int)$stream; 216 | if (isset($this->writeEvents[$fd])) { 217 | unset($this->writeEvents[$fd]); 218 | return true; 219 | } 220 | return false; 221 | } 222 | 223 | /** 224 | * {@inheritdoc} 225 | */ 226 | public function onSignal(int $signal, callable $func): void 227 | { 228 | Coroutine::run(function () use ($signal, $func): void { 229 | $this->signalListener[$signal] = Coroutine::getCurrent(); 230 | while (1) { 231 | try { 232 | Signal::wait($signal); 233 | if (!isset($this->signalListener[$signal]) || 234 | $this->signalListener[$signal] !== Coroutine::getCurrent()) { 235 | break; 236 | } 237 | $this->safeCall($func, [$signal]); 238 | } catch (SignalException) { 239 | // do nothing 240 | } 241 | } 242 | }); 243 | } 244 | 245 | /** 246 | * {@inheritdoc} 247 | */ 248 | public function offSignal(int $signal): bool 249 | { 250 | if (!isset($this->signalListener[$signal])) { 251 | return false; 252 | } 253 | unset($this->signalListener[$signal]); 254 | return true; 255 | } 256 | 257 | /** 258 | * {@inheritdoc} 259 | */ 260 | public function run(): void 261 | { 262 | waitAll(); 263 | } 264 | 265 | /** 266 | * Destroy loop. 267 | * 268 | * @return void 269 | */ 270 | public function stop(): void 271 | { 272 | Coroutine::killAll(); 273 | } 274 | 275 | /** 276 | * {@inheritdoc} 277 | */ 278 | public function setErrorHandler(callable $errorHandler): void 279 | { 280 | $this->errorHandler = $errorHandler; 281 | } 282 | 283 | /** 284 | * @param callable $func 285 | * @param array $args 286 | * @return void 287 | */ 288 | private function safeCall(callable $func, array $args = []): void 289 | { 290 | Coroutine::run(function () use ($func, $args): void { 291 | try { 292 | $func(...$args); 293 | } catch (\Throwable $e) { 294 | if ($this->errorHandler === null) { 295 | echo $e; 296 | } else { 297 | ($this->errorHandler)($e); 298 | } 299 | } 300 | }); 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /src/Protocols/Frame.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use function pack; 20 | use function strlen; 21 | use function substr; 22 | use function unpack; 23 | 24 | /** 25 | * Frame Protocol. 26 | */ 27 | class Frame 28 | { 29 | /** 30 | * Check the integrity of the package. 31 | * 32 | * @param string $buffer 33 | * @return int 34 | */ 35 | public static function input(string $buffer): int 36 | { 37 | if (strlen($buffer) < 4) { 38 | return 0; 39 | } 40 | $unpackData = unpack('Ntotal_length', $buffer); 41 | return $unpackData['total_length']; 42 | } 43 | 44 | /** 45 | * Decode. 46 | * 47 | * @param string $buffer 48 | * @return string 49 | */ 50 | public static function decode(string $buffer): string 51 | { 52 | return substr($buffer, 4); 53 | } 54 | 55 | /** 56 | * Encode. 57 | * 58 | * @param string $data 59 | * @return string 60 | */ 61 | public static function encode(string $data): string 62 | { 63 | $totalLength = 4 + strlen($data); 64 | return pack('N', $totalLength) . $data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Protocols/Http.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use Workerman\Connection\TcpConnection; 20 | use Workerman\Protocols\Http\Request; 21 | use Workerman\Protocols\Http\Response; 22 | use function clearstatcache; 23 | use function count; 24 | use function explode; 25 | use function filesize; 26 | use function fopen; 27 | use function fread; 28 | use function fseek; 29 | use function ftell; 30 | use function in_array; 31 | use function ini_get; 32 | use function is_array; 33 | use function is_object; 34 | use function preg_match; 35 | use function str_starts_with; 36 | use function strlen; 37 | use function strpos; 38 | use function strstr; 39 | use function substr; 40 | use function sys_get_temp_dir; 41 | 42 | /** 43 | * Class Http. 44 | * @package Workerman\Protocols 45 | */ 46 | class Http 47 | { 48 | /** 49 | * Request class name. 50 | * 51 | * @var string 52 | */ 53 | protected static string $requestClass = Request::class; 54 | 55 | /** 56 | * Upload tmp dir. 57 | * 58 | * @var string 59 | */ 60 | protected static string $uploadTmpDir = ''; 61 | 62 | /** 63 | * Get or set the request class name. 64 | * 65 | * @param class-string|null $className 66 | * @return string 67 | */ 68 | public static function requestClass(?string $className = null): string 69 | { 70 | if ($className !== null) { 71 | static::$requestClass = $className; 72 | } 73 | return static::$requestClass; 74 | } 75 | 76 | /** 77 | * Check the integrity of the package. 78 | * 79 | * @param string $buffer 80 | * @param TcpConnection $connection 81 | * @return int 82 | */ 83 | public static function input(string $buffer, TcpConnection $connection): int 84 | { 85 | $crlfPos = strpos($buffer, "\r\n\r\n"); 86 | if (false === $crlfPos) { 87 | // Judge whether the package length exceeds the limit. 88 | if (strlen($buffer) >= 16384) { 89 | $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true); 90 | } 91 | return 0; 92 | } 93 | 94 | $length = $crlfPos + 4; 95 | $header = substr($buffer, 0, $crlfPos); 96 | 97 | if ( 98 | !str_starts_with($header, 'GET ') && 99 | !str_starts_with($header, 'POST ') && 100 | !str_starts_with($header, 'OPTIONS ') && 101 | !str_starts_with($header, 'HEAD ') && 102 | !str_starts_with($header, 'DELETE ') && 103 | !str_starts_with($header, 'PUT ') && 104 | !str_starts_with($header, 'PATCH ') 105 | ) { 106 | $connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true); 107 | return 0; 108 | } 109 | 110 | if (preg_match('/\b(?:Transfer-Encoding\b.*)|(?:Content-Length:\s*(\d+)(?!.*\bTransfer-Encoding\b))/is', $header, $matches)) { 111 | if (!isset($matches[1])) { 112 | $connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true); 113 | return 0; 114 | } 115 | $length += (int)$matches[1]; 116 | } 117 | 118 | if ($length > $connection->maxPackageSize) { 119 | $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true); 120 | return 0; 121 | } 122 | 123 | return $length; 124 | } 125 | 126 | 127 | /** 128 | * Http decode. 129 | * 130 | * @param string $buffer 131 | * @param TcpConnection $connection 132 | * @return Request 133 | */ 134 | public static function decode(string $buffer, TcpConnection $connection): Request 135 | { 136 | static $requests = []; 137 | if (isset($requests[$buffer])) { 138 | $request = $requests[$buffer]; 139 | $request->connection = $connection; 140 | $connection->request = $request; 141 | $request->destroy(); 142 | return $request; 143 | } 144 | $request = new static::$requestClass($buffer); 145 | if (!isset($buffer[TcpConnection::MAX_CACHE_STRING_LENGTH])) { 146 | $requests[$buffer] = $request; 147 | if (count($requests) > TcpConnection::MAX_CACHE_SIZE) { 148 | unset($requests[key($requests)]); 149 | } 150 | $request = clone $request; 151 | } 152 | $request->connection = $connection; 153 | $connection->request = $request; 154 | return $request; 155 | } 156 | 157 | /** 158 | * Http encode. 159 | * 160 | * @param string|Response $response 161 | * @param TcpConnection $connection 162 | * @return string 163 | */ 164 | public static function encode(mixed $response, TcpConnection $connection): string 165 | { 166 | if (isset($connection->request)) { 167 | $request = $connection->request; 168 | $request->connection = $connection->request = null; 169 | } 170 | 171 | if (!is_object($response)) { 172 | $extHeader = ''; 173 | $contentType = 'text/html;charset=utf-8'; 174 | foreach ($connection->headers as $name => $value) { 175 | if ($name === 'Content-Type') { 176 | $contentType = $value; 177 | continue; 178 | } 179 | if (is_array($value)) { 180 | foreach ($value as $item) { 181 | $extHeader .= "$name: $item\r\n"; 182 | } 183 | } else { 184 | $extHeader .= "$name: $value\r\n"; 185 | } 186 | } 187 | $connection->headers = []; 188 | $response = (string)$response; 189 | $bodyLen = strlen($response); 190 | return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$extHeader}Connection: keep-alive\r\nContent-Type: $contentType\r\nContent-Length: $bodyLen\r\n\r\n$response"; 191 | } 192 | 193 | if ($connection->headers) { 194 | $response->withHeaders($connection->headers); 195 | $connection->headers = []; 196 | } 197 | 198 | if (isset($response->file)) { 199 | $file = $response->file['file']; 200 | $offset = $response->file['offset']; 201 | $length = $response->file['length']; 202 | clearstatcache(); 203 | $fileSize = (int)filesize($file); 204 | $bodyLen = $length > 0 ? $length : $fileSize - $offset; 205 | $response->withHeaders([ 206 | 'Content-Length' => $bodyLen, 207 | 'Accept-Ranges' => 'bytes', 208 | ]); 209 | if ($offset || $length) { 210 | $offsetEnd = $offset + $bodyLen - 1; 211 | $response->header('Content-Range', "bytes $offset-$offsetEnd/$fileSize"); 212 | } 213 | if ($bodyLen < 2 * 1024 * 1024) { 214 | $connection->send($response . file_get_contents($file, false, null, $offset, $bodyLen), true); 215 | return ''; 216 | } 217 | $handler = fopen($file, 'r'); 218 | if (false === $handler) { 219 | $connection->close(new Response(403, [], '403 Forbidden')); 220 | return ''; 221 | } 222 | $connection->send((string)$response, true); 223 | static::sendStream($connection, $handler, $offset, $length); 224 | return ''; 225 | } 226 | 227 | return (string)$response; 228 | } 229 | 230 | /** 231 | * Send remainder of a stream to client. 232 | * 233 | * @param TcpConnection $connection 234 | * @param resource $handler 235 | * @param int $offset 236 | * @param int $length 237 | */ 238 | protected static function sendStream(TcpConnection $connection, $handler, int $offset = 0, int $length = 0): void 239 | { 240 | $connection->context->bufferFull = false; 241 | $connection->context->streamSending = true; 242 | if ($offset !== 0) { 243 | fseek($handler, $offset); 244 | } 245 | $offsetEnd = $offset + $length; 246 | // Read file content from disk piece by piece and send to client. 247 | $doWrite = function () use ($connection, $handler, $length, $offsetEnd) { 248 | // Send buffer not full. 249 | while ($connection->context->bufferFull === false) { 250 | // Read from disk. 251 | $size = 1024 * 1024; 252 | if ($length !== 0) { 253 | $tell = ftell($handler); 254 | $remainSize = $offsetEnd - $tell; 255 | if ($remainSize <= 0) { 256 | fclose($handler); 257 | $connection->onBufferDrain = null; 258 | return; 259 | } 260 | $size = min($remainSize, $size); 261 | } 262 | 263 | $buffer = fread($handler, $size); 264 | // Read eof. 265 | if ($buffer === '' || $buffer === false) { 266 | fclose($handler); 267 | $connection->onBufferDrain = null; 268 | $connection->context->streamSending = false; 269 | return; 270 | } 271 | $connection->send($buffer, true); 272 | } 273 | }; 274 | // Send buffer full. 275 | $connection->onBufferFull = function ($connection) { 276 | $connection->context->bufferFull = true; 277 | }; 278 | // Send buffer drain. 279 | $connection->onBufferDrain = function ($connection) use ($doWrite) { 280 | $connection->context->bufferFull = false; 281 | $doWrite(); 282 | }; 283 | $doWrite(); 284 | } 285 | 286 | /** 287 | * Set or get uploadTmpDir. 288 | * 289 | * @param string|null $dir 290 | * @return string 291 | */ 292 | public static function uploadTmpDir(string|null $dir = null): string 293 | { 294 | if (null !== $dir) { 295 | static::$uploadTmpDir = $dir; 296 | } 297 | if (static::$uploadTmpDir === '') { 298 | if ($uploadTmpDir = ini_get('upload_tmp_dir')) { 299 | static::$uploadTmpDir = $uploadTmpDir; 300 | } else if ($uploadTmpDir = sys_get_temp_dir()) { 301 | static::$uploadTmpDir = $uploadTmpDir; 302 | } 303 | } 304 | return static::$uploadTmpDir; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Protocols/Http/Chunk.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http; 18 | 19 | use Stringable; 20 | 21 | use function dechex; 22 | use function strlen; 23 | 24 | /** 25 | * Class Chunk 26 | * @package Workerman\Protocols\Http 27 | */ 28 | class Chunk implements Stringable 29 | { 30 | 31 | public function __construct(protected string $buffer) {} 32 | 33 | public function __toString(): string 34 | { 35 | return dechex(strlen($this->buffer)) . "\r\n$this->buffer\r\n"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Protocols/Http/Response.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http; 18 | 19 | use Stringable; 20 | 21 | use function array_merge_recursive; 22 | use function explode; 23 | use function file; 24 | use function filemtime; 25 | use function gmdate; 26 | use function is_array; 27 | use function is_file; 28 | use function pathinfo; 29 | use function preg_match; 30 | use function rawurlencode; 31 | use function strlen; 32 | use function substr; 33 | use const FILE_IGNORE_NEW_LINES; 34 | use const FILE_SKIP_EMPTY_LINES; 35 | 36 | /** 37 | * Class Response 38 | * @package Workerman\Protocols\Http 39 | */ 40 | class Response implements Stringable 41 | { 42 | 43 | /** 44 | * Http reason. 45 | * 46 | * @var ?string 47 | */ 48 | protected ?string $reason = null; 49 | 50 | /** 51 | * Http version. 52 | * 53 | * @var string 54 | */ 55 | protected string $version = '1.1'; 56 | 57 | /** 58 | * Send file info 59 | * 60 | * @var ?array 61 | */ 62 | public ?array $file = null; 63 | 64 | /** 65 | * Mine type map. 66 | * @var array 67 | */ 68 | protected static array $mimeTypeMap = []; 69 | 70 | /** 71 | * Phrases. 72 | * 73 | * @var array 74 | * 75 | * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 76 | */ 77 | public const PHRASES = [ 78 | 100 => 'Continue', 79 | 101 => 'Switching Protocols', 80 | 102 => 'Processing', // WebDAV; RFC 2518 81 | 103 => 'Early Hints', // RFC 8297 82 | 83 | 200 => 'OK', 84 | 201 => 'Created', 85 | 202 => 'Accepted', 86 | 203 => 'Non-Authoritative Information', // since HTTP/1.1 87 | 204 => 'No Content', 88 | 205 => 'Reset Content', 89 | 206 => 'Partial Content', // RFC 7233 90 | 207 => 'Multi-Status', // WebDAV; RFC 4918 91 | 208 => 'Already Reported', // WebDAV; RFC 5842 92 | 226 => 'IM Used', // RFC 3229 93 | 94 | 300 => 'Multiple Choices', 95 | 301 => 'Moved Permanently', 96 | 302 => 'Found', // Previously "Moved temporarily" 97 | 303 => 'See Other', // since HTTP/1.1 98 | 304 => 'Not Modified', // RFC 7232 99 | 305 => 'Use Proxy', // since HTTP/1.1 100 | 306 => 'Switch Proxy', 101 | 307 => 'Temporary Redirect', // since HTTP/1.1 102 | 308 => 'Permanent Redirect', // RFC 7538 103 | 104 | 400 => 'Bad Request', 105 | 401 => 'Unauthorized', // RFC 7235 106 | 402 => 'Payment Required', 107 | 403 => 'Forbidden', 108 | 404 => 'Not Found', 109 | 405 => 'Method Not Allowed', 110 | 406 => 'Not Acceptable', 111 | 407 => 'Proxy Authentication Required', // RFC 7235 112 | 408 => 'Request Timeout', 113 | 409 => 'Conflict', 114 | 410 => 'Gone', 115 | 411 => 'Length Required', 116 | 412 => 'Precondition Failed', // RFC 7232 117 | 413 => 'Payload Too Large', // RFC 7231 118 | 414 => 'URI Too Long', // RFC 7231 119 | 415 => 'Unsupported Media Type', // RFC 7231 120 | 416 => 'Range Not Satisfiable', // RFC 7233 121 | 417 => 'Expectation Failed', 122 | 418 => 'I\'m a teapot', // RFC 2324, RFC 7168 123 | 421 => 'Misdirected Request', // RFC 7540 124 | 422 => 'Unprocessable Entity', // WebDAV; RFC 4918 125 | 423 => 'Locked', // WebDAV; RFC 4918 126 | 424 => 'Failed Dependency', // WebDAV; RFC 4918 127 | 425 => 'Too Early', // RFC 8470 128 | 426 => 'Upgrade Required', 129 | 428 => 'Precondition Required', // RFC 6585 130 | 429 => 'Too Many Requests', // RFC 6585 131 | 431 => 'Request Header Fields Too Large', // RFC 6585 132 | 451 => 'Unavailable For Legal Reasons', // RFC 7725 133 | 134 | 500 => 'Internal Server Error', 135 | 501 => 'Not Implemented', 136 | 502 => 'Bad Gateway', 137 | 503 => 'Service Unavailable', 138 | 504 => 'Gateway Timeout', 139 | 505 => 'HTTP Version Not Supported', 140 | 506 => 'Variant Also Negotiates', // RFC 2295 141 | 507 => 'Insufficient Storage', // WebDAV; RFC 4918 142 | 508 => 'Loop Detected', // WebDAV; RFC 5842 143 | 510 => 'Not Extended', // RFC 2774 144 | 511 => 'Network Authentication Required', // RFC 6585 145 | ]; 146 | 147 | /** 148 | * Init. 149 | * 150 | * @return void 151 | */ 152 | public static function init(): void 153 | { 154 | static::initMimeTypeMap(); 155 | } 156 | 157 | /** 158 | * Response constructor. 159 | * 160 | * @param int $status 161 | * @param array $headers 162 | * @param string $body 163 | */ 164 | public function __construct( 165 | protected int $status = 200, 166 | protected array $headers = [], 167 | protected string $body = '' 168 | ) {} 169 | 170 | /** 171 | * Set header. 172 | * 173 | * @param string $name 174 | * @param string $value 175 | * @return $this 176 | */ 177 | public function header(string $name, string $value): static 178 | { 179 | $this->headers[$name] = $value; 180 | return $this; 181 | } 182 | 183 | /** 184 | * Set header. 185 | * 186 | * @param string $name 187 | * @param string $value 188 | * @return $this 189 | */ 190 | public function withHeader(string $name, string $value): static 191 | { 192 | return $this->header($name, $value); 193 | } 194 | 195 | /** 196 | * Set headers. 197 | * 198 | * @param array $headers 199 | * @return $this 200 | */ 201 | public function withHeaders(array $headers): static 202 | { 203 | $this->headers = array_merge_recursive($this->headers, $headers); 204 | return $this; 205 | } 206 | 207 | /** 208 | * Remove header. 209 | * 210 | * @param string $name 211 | * @return $this 212 | */ 213 | public function withoutHeader(string $name): static 214 | { 215 | unset($this->headers[$name]); 216 | return $this; 217 | } 218 | 219 | /** 220 | * Get header. 221 | * 222 | * @param string $name 223 | * @return null|array|string 224 | */ 225 | public function getHeader(string $name): array|string|null 226 | { 227 | return $this->headers[$name] ?? null; 228 | } 229 | 230 | /** 231 | * Get headers. 232 | * 233 | * @return array 234 | */ 235 | public function getHeaders(): array 236 | { 237 | return $this->headers; 238 | } 239 | 240 | /** 241 | * Set status. 242 | * 243 | * @param int $code 244 | * @param string|null $reasonPhrase 245 | * @return $this 246 | */ 247 | public function withStatus(int $code, ?string $reasonPhrase = null): static 248 | { 249 | $this->status = $code; 250 | $this->reason = $reasonPhrase; 251 | return $this; 252 | } 253 | 254 | /** 255 | * Get status code. 256 | * 257 | * @return int 258 | */ 259 | public function getStatusCode(): int 260 | { 261 | return $this->status; 262 | } 263 | 264 | /** 265 | * Get reason phrase. 266 | * 267 | * @return ?string 268 | */ 269 | public function getReasonPhrase(): ?string 270 | { 271 | return $this->reason; 272 | } 273 | 274 | /** 275 | * Set protocol version. 276 | * 277 | * @param string $version 278 | * @return $this 279 | */ 280 | public function withProtocolVersion(string $version): static 281 | { 282 | $this->version = $version; 283 | return $this; 284 | } 285 | 286 | /** 287 | * Set http body. 288 | * 289 | * @param string $body 290 | * @return $this 291 | */ 292 | public function withBody(string $body): static 293 | { 294 | $this->body = $body; 295 | return $this; 296 | } 297 | 298 | /** 299 | * Get http raw body. 300 | * 301 | * @return string 302 | */ 303 | public function rawBody(): string 304 | { 305 | return $this->body; 306 | } 307 | 308 | /** 309 | * Send file. 310 | * 311 | * @param string $file 312 | * @param int $offset 313 | * @param int $length 314 | * @return $this 315 | */ 316 | public function withFile(string $file, int $offset = 0, int $length = 0): static 317 | { 318 | if (!is_file($file)) { 319 | return $this->withStatus(404)->withBody('

404 Not Found

'); 320 | } 321 | $this->file = ['file' => $file, 'offset' => $offset, 'length' => $length]; 322 | return $this; 323 | } 324 | 325 | /** 326 | * Set cookie. 327 | * 328 | * @param string $name 329 | * @param string $value 330 | * @param int|null $maxAge 331 | * @param string $path 332 | * @param string $domain 333 | * @param bool $secure 334 | * @param bool $httpOnly 335 | * @param string $sameSite 336 | * @return $this 337 | */ 338 | public function cookie(string $name, string $value = '', ?int $maxAge = null, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, string $sameSite = ''): static 339 | { 340 | $this->headers['Set-Cookie'][] = $name . '=' . rawurlencode($value) 341 | . (empty($domain) ? '' : '; Domain=' . $domain) 342 | . ($maxAge === null ? '' : '; Max-Age=' . $maxAge) 343 | . (empty($path) ? '' : '; Path=' . $path) 344 | . (!$secure ? '' : '; Secure') 345 | . (!$httpOnly ? '' : '; HttpOnly') 346 | . (empty($sameSite) ? '' : '; SameSite=' . $sameSite); 347 | return $this; 348 | } 349 | 350 | /** 351 | * Create header for file. 352 | * 353 | * @param array $fileInfo 354 | * @return string 355 | */ 356 | protected function createHeadForFile(array $fileInfo): string 357 | { 358 | $file = $fileInfo['file']; 359 | $reason = $this->reason ?: self::PHRASES[$this->status]; 360 | $head = "HTTP/$this->version $this->status $reason\r\n"; 361 | $headers = $this->headers; 362 | if (!isset($headers['Server'])) { 363 | $head .= "Server: workerman\r\n"; 364 | } 365 | foreach ($headers as $name => $value) { 366 | if (is_array($value)) { 367 | foreach ($value as $item) { 368 | $head .= "$name: $item\r\n"; 369 | } 370 | continue; 371 | } 372 | $head .= "$name: $value\r\n"; 373 | } 374 | 375 | if (!isset($headers['Connection'])) { 376 | $head .= "Connection: keep-alive\r\n"; 377 | } 378 | 379 | $fileInfo = pathinfo($file); 380 | $extension = $fileInfo['extension'] ?? ''; 381 | $baseName = $fileInfo['basename'] ?: 'unknown'; 382 | if (!isset($headers['Content-Type'])) { 383 | if (isset(self::$mimeTypeMap[$extension])) { 384 | $head .= "Content-Type: " . self::$mimeTypeMap[$extension] . "\r\n"; 385 | } else { 386 | $head .= "Content-Type: application/octet-stream\r\n"; 387 | } 388 | } 389 | 390 | if (!isset($headers['Content-Disposition']) && !isset(self::$mimeTypeMap[$extension])) { 391 | $head .= "Content-Disposition: attachment; filename=\"$baseName\"\r\n"; 392 | } 393 | 394 | if (!isset($headers['Last-Modified']) && $mtime = filemtime($file)) { 395 | $head .= 'Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT' . "\r\n"; 396 | } 397 | 398 | return "$head\r\n"; 399 | } 400 | 401 | /** 402 | * __toString. 403 | * 404 | * @return string 405 | */ 406 | public function __toString(): string 407 | { 408 | if ($this->file) { 409 | return $this->createHeadForFile($this->file); 410 | } 411 | 412 | $reason = $this->reason ?: self::PHRASES[$this->status] ?? ''; 413 | $bodyLen = strlen($this->body); 414 | if (empty($this->headers)) { 415 | return "HTTP/$this->version $this->status $reason\r\nServer: workerman\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $bodyLen\r\nConnection: keep-alive\r\n\r\n$this->body"; 416 | } 417 | 418 | $head = "HTTP/$this->version $this->status $reason\r\n"; 419 | $headers = $this->headers; 420 | if (!isset($headers['Server'])) { 421 | $head .= "Server: workerman\r\n"; 422 | } 423 | foreach ($headers as $name => $value) { 424 | if (is_array($value)) { 425 | foreach ($value as $item) { 426 | $head .= "$name: $item\r\n"; 427 | } 428 | continue; 429 | } 430 | $head .= "$name: $value\r\n"; 431 | } 432 | 433 | if (!isset($headers['Connection'])) { 434 | $head .= "Connection: keep-alive\r\n"; 435 | } 436 | 437 | if (!isset($headers['Content-Type'])) { 438 | $head .= "Content-Type: text/html;charset=utf-8\r\n"; 439 | } else if ($headers['Content-Type'] === 'text/event-stream') { 440 | return $head . $this->body; 441 | } 442 | 443 | if (!isset($headers['Transfer-Encoding'])) { 444 | $head .= "Content-Length: $bodyLen\r\n\r\n"; 445 | } else { 446 | return $bodyLen ? "$head\r\n" . dechex($bodyLen) . "\r\n{$this->body}\r\n" : "$head\r\n"; 447 | } 448 | 449 | // The whole http package 450 | return $head . $this->body; 451 | } 452 | 453 | /** 454 | * Init mime map. 455 | * 456 | * @return void 457 | */ 458 | public static function initMimeTypeMap(): void 459 | { 460 | $mimeFile = __DIR__ . '/mime.types'; 461 | $items = file($mimeFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 462 | foreach ($items as $content) { 463 | if (preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) { 464 | $mimeType = $match[1]; 465 | $extensionVar = $match[2]; 466 | $extensionArray = explode(' ', substr($extensionVar, 0, -1)); 467 | foreach ($extensionArray as $fileExtension) { 468 | static::$mimeTypeMap[$fileExtension] = $mimeType; 469 | } 470 | } 471 | } 472 | } 473 | } 474 | 475 | Response::init(); 476 | -------------------------------------------------------------------------------- /src/Protocols/Http/ServerSentEvents.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http; 18 | 19 | use Stringable; 20 | 21 | use function str_replace; 22 | 23 | /** 24 | * Class ServerSentEvents 25 | * @package Workerman\Protocols\Http 26 | */ 27 | class ServerSentEvents implements Stringable 28 | { 29 | /** 30 | * ServerSentEvents constructor. 31 | * $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000] 32 | */ 33 | public function __construct(protected array $data) {} 34 | 35 | public function __toString(): string 36 | { 37 | $buffer = ''; 38 | $data = $this->data; 39 | if (isset($data[''])) { 40 | $buffer = ": {$data['']}\n"; 41 | } 42 | if (isset($data['event'])) { 43 | $buffer .= "event: {$data['event']}\n"; 44 | } 45 | if (isset($data['id'])) { 46 | $buffer .= "id: {$data['id']}\n"; 47 | } 48 | if (isset($data['retry'])) { 49 | $buffer .= "retry: {$data['retry']}\n"; 50 | } 51 | if (isset($data['data'])) { 52 | $buffer .= 'data: ' . str_replace("\n", "\ndata: ", $data['data']) . "\n"; 53 | } 54 | return "$buffer\n"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Protocols/Http/Session.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http; 18 | 19 | use Exception; 20 | use Random\RandomException; 21 | use RuntimeException; 22 | use Workerman\Protocols\Http\Session\FileSessionHandler; 23 | use Workerman\Protocols\Http\Session\SessionHandlerInterface; 24 | use function array_key_exists; 25 | use function ini_get; 26 | use function is_array; 27 | use function is_scalar; 28 | use function preg_match; 29 | use function random_int; 30 | use function serialize; 31 | use function session_get_cookie_params; 32 | use function unserialize; 33 | 34 | /** 35 | * Class Session 36 | * @package Workerman\Protocols\Http 37 | */ 38 | class Session 39 | { 40 | /** 41 | * Session andler class which implements SessionHandlerInterface. 42 | * 43 | * @var string 44 | */ 45 | protected static string $handlerClass = FileSessionHandler::class; 46 | 47 | /** 48 | * Parameters of __constructor for session handler class. 49 | * 50 | * @var mixed 51 | */ 52 | protected static mixed $handlerConfig = null; 53 | 54 | /** 55 | * Session name. 56 | * 57 | * @var string 58 | */ 59 | public static string $name = 'PHPSID'; 60 | 61 | /** 62 | * Auto update timestamp. 63 | * 64 | * @var bool 65 | */ 66 | public static bool $autoUpdateTimestamp = false; 67 | 68 | /** 69 | * Session lifetime. 70 | * 71 | * @var int 72 | */ 73 | public static int $lifetime = 1440; 74 | 75 | /** 76 | * Cookie lifetime. 77 | * 78 | * @var int 79 | */ 80 | public static int $cookieLifetime = 1440; 81 | 82 | /** 83 | * Session cookie path. 84 | * 85 | * @var string 86 | */ 87 | public static string $cookiePath = '/'; 88 | 89 | /** 90 | * Session cookie domain. 91 | * 92 | * @var string 93 | */ 94 | public static string $domain = ''; 95 | 96 | /** 97 | * HTTPS only cookies. 98 | * 99 | * @var bool 100 | */ 101 | public static bool $secure = false; 102 | 103 | /** 104 | * HTTP access only. 105 | * 106 | * @var bool 107 | */ 108 | public static bool $httpOnly = true; 109 | 110 | /** 111 | * Same-site cookies. 112 | * 113 | * @var string 114 | */ 115 | public static string $sameSite = ''; 116 | 117 | /** 118 | * Gc probability. 119 | * 120 | * @var int[] 121 | */ 122 | public static array $gcProbability = [1, 20000]; 123 | 124 | /** 125 | * Session handler instance. 126 | * 127 | * @var ?SessionHandlerInterface 128 | */ 129 | protected static ?SessionHandlerInterface $handler = null; 130 | 131 | /** 132 | * Session data. 133 | * 134 | * @var array 135 | */ 136 | protected mixed $data = []; 137 | 138 | /** 139 | * Session changed and need to save. 140 | * 141 | * @var bool 142 | */ 143 | protected bool $needSave = false; 144 | 145 | /** 146 | * Session id. 147 | * 148 | * @var string 149 | */ 150 | protected string $sessionId; 151 | 152 | /** 153 | * Is safe. 154 | * 155 | * @var bool 156 | */ 157 | protected bool $isSafe = true; 158 | 159 | /** 160 | * Session constructor. 161 | * 162 | * @param string $sessionId 163 | */ 164 | public function __construct(string $sessionId) 165 | { 166 | if (static::$handler === null) { 167 | static::initHandler(); 168 | } 169 | $this->sessionId = $sessionId; 170 | if ($data = static::$handler->read($sessionId)) { 171 | $this->data = unserialize($data); 172 | } 173 | } 174 | 175 | /** 176 | * Get session id. 177 | * 178 | * @return string 179 | */ 180 | public function getId(): string 181 | { 182 | return $this->sessionId; 183 | } 184 | 185 | /** 186 | * Get session. 187 | * 188 | * @param string $name 189 | * @param mixed $default 190 | * @return mixed 191 | */ 192 | public function get(string $name, mixed $default = null): mixed 193 | { 194 | return $this->data[$name] ?? $default; 195 | } 196 | 197 | /** 198 | * Store data in the session. 199 | * 200 | * @param string $name 201 | * @param mixed $value 202 | */ 203 | public function set(string $name, mixed $value): void 204 | { 205 | $this->data[$name] = $value; 206 | $this->needSave = true; 207 | } 208 | 209 | /** 210 | * Delete an item from the session. 211 | * 212 | * @param string $name 213 | */ 214 | public function delete(string $name): void 215 | { 216 | unset($this->data[$name]); 217 | $this->needSave = true; 218 | } 219 | 220 | /** 221 | * Retrieve and delete an item from the session. 222 | * 223 | * @param string $name 224 | * @param mixed $default 225 | * @return mixed 226 | */ 227 | public function pull(string $name, mixed $default = null): mixed 228 | { 229 | $value = $this->get($name, $default); 230 | $this->delete($name); 231 | return $value; 232 | } 233 | 234 | /** 235 | * Store data in the session. 236 | * 237 | * @param array|string $key 238 | * @param mixed $value 239 | */ 240 | public function put(array|string $key, mixed $value = null): void 241 | { 242 | if (!is_array($key)) { 243 | $this->set($key, $value); 244 | return; 245 | } 246 | 247 | foreach ($key as $k => $v) { 248 | $this->data[$k] = $v; 249 | } 250 | $this->needSave = true; 251 | } 252 | 253 | /** 254 | * Remove a piece of data from the session. 255 | * 256 | * @param array|string $name 257 | */ 258 | public function forget(array|string $name): void 259 | { 260 | if (is_scalar($name)) { 261 | $this->delete($name); 262 | return; 263 | } 264 | foreach ($name as $key) { 265 | unset($this->data[$key]); 266 | } 267 | $this->needSave = true; 268 | } 269 | 270 | /** 271 | * Retrieve all the data in the session. 272 | * 273 | * @return array 274 | */ 275 | public function all(): array 276 | { 277 | return $this->data; 278 | } 279 | 280 | /** 281 | * Remove all data from the session. 282 | * 283 | * @return void 284 | */ 285 | public function flush(): void 286 | { 287 | $this->needSave = true; 288 | $this->data = []; 289 | } 290 | 291 | /** 292 | * Determining If An Item Exists In The Session. 293 | * 294 | * @param string $name 295 | * @return bool 296 | */ 297 | public function has(string $name): bool 298 | { 299 | return isset($this->data[$name]); 300 | } 301 | 302 | /** 303 | * To determine if an item is present in the session, even if its value is null. 304 | * 305 | * @param string $name 306 | * @return bool 307 | */ 308 | public function exists(string $name): bool 309 | { 310 | return array_key_exists($name, $this->data); 311 | } 312 | 313 | /** 314 | * Save session to store. 315 | * 316 | * @return void 317 | */ 318 | public function save(): void 319 | { 320 | if ($this->needSave) { 321 | if (empty($this->data)) { 322 | static::$handler->destroy($this->sessionId); 323 | } else { 324 | static::$handler->write($this->sessionId, serialize($this->data)); 325 | } 326 | } elseif (static::$autoUpdateTimestamp) { 327 | $this->refresh(); 328 | } 329 | $this->needSave = false; 330 | } 331 | 332 | /** 333 | * Refresh session expire time. 334 | * 335 | * @return bool 336 | */ 337 | public function refresh(): bool 338 | { 339 | return static::$handler->updateTimestamp($this->getId()); 340 | } 341 | 342 | /** 343 | * Init. 344 | * 345 | * @return void 346 | */ 347 | public static function init(): void 348 | { 349 | if (($gcProbability = (int)ini_get('session.gc_probability')) && ($gcDivisor = (int)ini_get('session.gc_divisor'))) { 350 | static::$gcProbability = [$gcProbability, $gcDivisor]; 351 | } 352 | 353 | if ($gcMaxLifeTime = ini_get('session.gc_maxlifetime')) { 354 | self::$lifetime = (int)$gcMaxLifeTime; 355 | } 356 | 357 | $sessionCookieParams = session_get_cookie_params(); 358 | static::$cookieLifetime = $sessionCookieParams['lifetime']; 359 | static::$cookiePath = $sessionCookieParams['path']; 360 | static::$domain = $sessionCookieParams['domain']; 361 | static::$secure = $sessionCookieParams['secure']; 362 | static::$httpOnly = $sessionCookieParams['httponly']; 363 | } 364 | 365 | /** 366 | * Set session handler class. 367 | * 368 | * @param mixed $className 369 | * @param mixed $config 370 | * @return string 371 | */ 372 | public static function handlerClass(mixed $className = null, mixed $config = null): string 373 | { 374 | if ($className) { 375 | static::$handlerClass = $className; 376 | } 377 | if ($config) { 378 | static::$handlerConfig = $config; 379 | } 380 | return static::$handlerClass; 381 | } 382 | 383 | /** 384 | * Get cookie params. 385 | * 386 | * @return array 387 | */ 388 | public static function getCookieParams(): array 389 | { 390 | return [ 391 | 'lifetime' => static::$cookieLifetime, 392 | 'path' => static::$cookiePath, 393 | 'domain' => static::$domain, 394 | 'secure' => static::$secure, 395 | 'httponly' => static::$httpOnly, 396 | 'samesite' => static::$sameSite, 397 | ]; 398 | } 399 | 400 | /** 401 | * Init handler. 402 | * 403 | * @return void 404 | */ 405 | protected static function initHandler(): void 406 | { 407 | if (static::$handlerConfig === null) { 408 | static::$handler = new static::$handlerClass(); 409 | } else { 410 | static::$handler = new static::$handlerClass(static::$handlerConfig); 411 | } 412 | } 413 | 414 | /** 415 | * GC sessions. 416 | * 417 | * @return void 418 | */ 419 | public function gc(): void 420 | { 421 | static::$handler->gc(static::$lifetime); 422 | } 423 | 424 | /** 425 | * __wakeup. 426 | * 427 | * @return void 428 | */ 429 | public function __wakeup() 430 | { 431 | $this->isSafe = false; 432 | } 433 | 434 | /** 435 | * __destruct. 436 | * 437 | * @return void 438 | * @throws RandomException 439 | */ 440 | public function __destruct() 441 | { 442 | if (!$this->isSafe) { 443 | return; 444 | } 445 | $this->save(); 446 | if (random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) { 447 | $this->gc(); 448 | } 449 | } 450 | 451 | } 452 | 453 | // Init session. 454 | Session::init(); 455 | -------------------------------------------------------------------------------- /src/Protocols/Http/Session/FileSessionHandler.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http\Session; 18 | 19 | use Exception; 20 | use Workerman\Protocols\Http\Session; 21 | use function clearstatcache; 22 | use function file_get_contents; 23 | use function file_put_contents; 24 | use function filemtime; 25 | use function glob; 26 | use function is_dir; 27 | use function is_file; 28 | use function mkdir; 29 | use function rename; 30 | use function session_save_path; 31 | use function strlen; 32 | use function sys_get_temp_dir; 33 | use function time; 34 | use function touch; 35 | use function unlink; 36 | 37 | /** 38 | * Class FileSessionHandler 39 | * @package Workerman\Protocols\Http\Session 40 | */ 41 | class FileSessionHandler implements SessionHandlerInterface 42 | { 43 | /** 44 | * Session save path. 45 | * 46 | * @var string 47 | */ 48 | protected static string $sessionSavePath; 49 | 50 | /** 51 | * Session file prefix. 52 | * 53 | * @var string 54 | */ 55 | protected static string $sessionFilePrefix = 'session_'; 56 | 57 | /** 58 | * Init. 59 | */ 60 | public static function init() 61 | { 62 | $savePath = @session_save_path(); 63 | if (!$savePath || str_starts_with($savePath, 'tcp://')) { 64 | $savePath = sys_get_temp_dir(); 65 | } 66 | static::sessionSavePath($savePath); 67 | } 68 | 69 | /** 70 | * FileSessionHandler constructor. 71 | * @param array $config 72 | */ 73 | public function __construct(array $config = []) 74 | { 75 | if (isset($config['save_path'])) { 76 | static::sessionSavePath($config['save_path']); 77 | } 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function open(string $savePath, string $name): bool 84 | { 85 | return true; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function read(string $sessionId): string|false 92 | { 93 | $sessionFile = static::sessionFile($sessionId); 94 | clearstatcache(); 95 | if (is_file($sessionFile)) { 96 | if (time() - filemtime($sessionFile) > Session::$lifetime) { 97 | unlink($sessionFile); 98 | return false; 99 | } 100 | $data = file_get_contents($sessionFile); 101 | return $data ?: false; 102 | } 103 | return false; 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | * @throws Exception 109 | */ 110 | public function write(string $sessionId, string $sessionData): bool 111 | { 112 | $tempFile = static::$sessionSavePath . uniqid(bin2hex(random_bytes(8)), true); 113 | if (!file_put_contents($tempFile, $sessionData)) { 114 | return false; 115 | } 116 | return rename($tempFile, static::sessionFile($sessionId)); 117 | } 118 | 119 | /** 120 | * Update session modify time. 121 | * 122 | * @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php 123 | * @see https://www.php.net/manual/zh/function.touch.php 124 | * 125 | * @param string $sessionId Session id. 126 | * @param string $data Session Data. 127 | * 128 | * @return bool 129 | */ 130 | public function updateTimestamp(string $sessionId, string $data = ""): bool 131 | { 132 | $sessionFile = static::sessionFile($sessionId); 133 | if (!file_exists($sessionFile)) { 134 | return false; 135 | } 136 | // set file modify time to current time 137 | $setModifyTime = touch($sessionFile); 138 | // clear file stat cache 139 | clearstatcache(); 140 | return $setModifyTime; 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | public function close(): bool 147 | { 148 | return true; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function destroy(string $sessionId): bool 155 | { 156 | $sessionFile = static::sessionFile($sessionId); 157 | if (is_file($sessionFile)) { 158 | unlink($sessionFile); 159 | } 160 | return true; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function gc(int $maxLifetime): bool 167 | { 168 | $timeNow = time(); 169 | foreach (glob(static::$sessionSavePath . static::$sessionFilePrefix . '*') as $file) { 170 | if (is_file($file) && $timeNow - filemtime($file) > $maxLifetime) { 171 | unlink($file); 172 | } 173 | } 174 | return true; 175 | } 176 | 177 | /** 178 | * Get session file path. 179 | * 180 | * @param string $sessionId 181 | * @return string 182 | */ 183 | protected static function sessionFile(string $sessionId): string 184 | { 185 | return static::$sessionSavePath . static::$sessionFilePrefix . $sessionId; 186 | } 187 | 188 | /** 189 | * Get or set session file path. 190 | * 191 | * @param string $path 192 | * @return string 193 | */ 194 | public static function sessionSavePath(string $path): string 195 | { 196 | if ($path) { 197 | if ($path[strlen($path) - 1] !== DIRECTORY_SEPARATOR) { 198 | $path .= DIRECTORY_SEPARATOR; 199 | } 200 | static::$sessionSavePath = $path; 201 | if (!is_dir($path)) { 202 | mkdir($path, 0777, true); 203 | } 204 | } 205 | return $path; 206 | } 207 | } 208 | 209 | FileSessionHandler::init(); -------------------------------------------------------------------------------- /src/Protocols/Http/Session/RedisClusterSessionHandler.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http\Session; 18 | 19 | use Redis; 20 | use RedisCluster; 21 | use RedisClusterException; 22 | use RedisException; 23 | 24 | class RedisClusterSessionHandler extends RedisSessionHandler 25 | { 26 | /** 27 | * @param $config 28 | * @throws RedisClusterException 29 | * @throws RedisException 30 | */ 31 | public function __construct($config) 32 | { 33 | $timeout = $config['timeout'] ?? 2; 34 | $readTimeout = $config['read_timeout'] ?? $timeout; 35 | $persistent = $config['persistent'] ?? false; 36 | $auth = $config['auth'] ?? ''; 37 | $args = [null, $config['host'], $timeout, $readTimeout, $persistent]; 38 | if ($auth) { 39 | $args[] = $auth; 40 | } 41 | $this->redis = new RedisCluster(...$args); 42 | if (empty($config['prefix'])) { 43 | $config['prefix'] = 'redis_session_'; 44 | } 45 | $this->redis->setOption(Redis::OPT_PREFIX, $config['prefix']); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function read(string $sessionId): string|false 52 | { 53 | return $this->redis->get($sessionId); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Protocols/Http/Session/RedisSessionHandler.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http\Session; 18 | 19 | use Redis; 20 | use RedisCluster; 21 | use RedisException; 22 | use RuntimeException; 23 | use Throwable; 24 | use Workerman\Protocols\Http\Session; 25 | use Workerman\Timer; 26 | 27 | /** 28 | * Class RedisSessionHandler 29 | * @package Workerman\Protocols\Http\Session 30 | */ 31 | class RedisSessionHandler implements SessionHandlerInterface 32 | { 33 | /** 34 | * @var Redis|RedisCluster 35 | */ 36 | protected Redis|RedisCluster $redis; 37 | 38 | /** 39 | * @var array 40 | */ 41 | protected array $config; 42 | 43 | /** 44 | * RedisSessionHandler constructor. 45 | * @param array $config = [ 46 | * 'host' => '127.0.0.1', 47 | * 'port' => 6379, 48 | * 'timeout' => 2, 49 | * 'auth' => '******', 50 | * 'database' => 2, 51 | * 'prefix' => 'redis_session_', 52 | * 'ping' => 55, 53 | * ] 54 | * @throws RedisException 55 | */ 56 | public function __construct(array $config) 57 | { 58 | if (false === extension_loaded('redis')) { 59 | throw new RuntimeException('Please install redis extension.'); 60 | } 61 | 62 | $config['timeout'] ??= 2; 63 | $this->config = $config; 64 | $this->connect(); 65 | 66 | Timer::add($config['ping'] ?? 55, function () { 67 | $this->redis->get('ping'); 68 | }); 69 | } 70 | 71 | /** 72 | * @throws RedisException 73 | */ 74 | public function connect() 75 | { 76 | $config = $this->config; 77 | 78 | $this->redis = new Redis(); 79 | if (false === $this->redis->connect($config['host'], $config['port'], $config['timeout'])) { 80 | throw new RuntimeException("Redis connect {$config['host']}:{$config['port']} fail."); 81 | } 82 | if (!empty($config['auth'])) { 83 | $this->redis->auth($config['auth']); 84 | } 85 | if (!empty($config['database'])) { 86 | $this->redis->select($config['database']); 87 | } 88 | if (empty($config['prefix'])) { 89 | $config['prefix'] = 'redis_session_'; 90 | } 91 | $this->redis->setOption(Redis::OPT_PREFIX, $config['prefix']); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function open(string $savePath, string $name): bool 98 | { 99 | return true; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | * @param string $sessionId 105 | * @return string|false 106 | * @throws RedisException 107 | * @throws Throwable 108 | */ 109 | public function read(string $sessionId): string|false 110 | { 111 | try { 112 | return $this->redis->get($sessionId); 113 | } catch (Throwable $e) { 114 | $msg = strtolower($e->getMessage()); 115 | if ($msg === 'connection lost' || strpos($msg, 'went away')) { 116 | $this->connect(); 117 | return $this->redis->get($sessionId); 118 | } 119 | throw $e; 120 | } 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | * @throws RedisException 126 | */ 127 | public function write(string $sessionId, string $sessionData): bool 128 | { 129 | return true === $this->redis->setex($sessionId, Session::$lifetime, $sessionData); 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | * @throws RedisException 135 | */ 136 | public function updateTimestamp(string $sessionId, string $data = ""): bool 137 | { 138 | return true === $this->redis->expire($sessionId, Session::$lifetime); 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | * @throws RedisException 144 | */ 145 | public function destroy(string $sessionId): bool 146 | { 147 | $this->redis->del($sessionId); 148 | return true; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function close(): bool 155 | { 156 | return true; 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | */ 162 | public function gc(int $maxLifetime): bool 163 | { 164 | return true; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Protocols/Http/Session/SessionHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols\Http\Session; 18 | 19 | interface SessionHandlerInterface 20 | { 21 | /** 22 | * Close the session 23 | * @link http://php.net/manual/en/sessionhandlerinterface.close.php 24 | * @return bool

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

28 | * @since 5.4.0 29 | */ 30 | public function close(): bool; 31 | 32 | /** 33 | * Destroy a session 34 | * @link http://php.net/manual/en/sessionhandlerinterface.destroy.php 35 | * @param string $sessionId The session ID being destroyed. 36 | * @return bool

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

40 | * @since 5.4.0 41 | */ 42 | public function destroy(string $sessionId): bool; 43 | 44 | /** 45 | * Cleanup old sessions 46 | * @link http://php.net/manual/en/sessionhandlerinterface.gc.php 47 | * @param int $maxLifetime

48 | * Sessions that have not updated for 49 | * the last maxlifetime seconds will be removed. 50 | *

51 | * @return bool

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

55 | * @since 5.4.0 56 | */ 57 | public function gc(int $maxLifetime): bool; 58 | 59 | /** 60 | * Initialize session 61 | * @link http://php.net/manual/en/sessionhandlerinterface.open.php 62 | * @param string $savePath The path where to store/retrieve the session. 63 | * @param string $name The session name. 64 | * @return bool

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

68 | * @since 5.4.0 69 | */ 70 | public function open(string $savePath, string $name): bool; 71 | 72 | 73 | /** 74 | * Read session data 75 | * @link http://php.net/manual/en/sessionhandlerinterface.read.php 76 | * @param string $sessionId The session id to read data for. 77 | * @return string|false

78 | * Returns an encoded string of the read data. 79 | * If nothing was read, it must return false. 80 | * Note this value is returned internally to PHP for processing. 81 | *

82 | * @since 5.4.0 83 | */ 84 | public function read(string $sessionId): string|false; 85 | 86 | /** 87 | * Write session data 88 | * @link http://php.net/manual/en/sessionhandlerinterface.write.php 89 | * @param string $sessionId The session id. 90 | * @param string $sessionData

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

97 | * @return bool

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

101 | * @since 5.4.0 102 | */ 103 | public function write(string $sessionId, string $sessionData): bool; 104 | 105 | /** 106 | * Update session modify time. 107 | * 108 | * @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php 109 | * 110 | * @param string $sessionId 111 | * @param string $data Session Data. 112 | * 113 | * @return bool 114 | */ 115 | public function updateTimestamp(string $sessionId, string $data = ""): bool; 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Protocols/Http/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | application/wasm wasm; 12 | 13 | text/mathml mml; 14 | text/plain txt; 15 | text/vnd.sun.j2me.app-descriptor jad; 16 | text/vnd.wap.wml wml; 17 | text/x-component htc; 18 | 19 | image/png png; 20 | image/tiff tif tiff; 21 | image/vnd.wap.wbmp wbmp; 22 | image/x-icon ico; 23 | image/x-jng jng; 24 | image/x-ms-bmp bmp; 25 | image/svg+xml svg svgz; 26 | image/webp webp; 27 | 28 | application/font-woff woff; 29 | application/java-archive jar war ear; 30 | application/json json; 31 | application/mac-binhex40 hqx; 32 | application/msword doc; 33 | application/pdf pdf; 34 | application/postscript ps eps ai; 35 | application/rtf rtf; 36 | application/vnd.apple.mpegurl m3u8; 37 | application/vnd.ms-excel xls; 38 | application/vnd.ms-fontobject eot; 39 | application/vnd.ms-powerpoint ppt; 40 | application/vnd.wap.wmlc wmlc; 41 | application/vnd.google-earth.kml+xml kml; 42 | application/vnd.google-earth.kmz kmz; 43 | application/x-7z-compressed 7z; 44 | application/x-cocoa cco; 45 | application/x-java-archive-diff jardiff; 46 | application/x-java-jnlp-file jnlp; 47 | application/x-makeself run; 48 | application/x-perl pl pm; 49 | application/x-pilot prc pdb; 50 | application/x-rar-compressed rar; 51 | application/x-redhat-package-manager rpm; 52 | application/x-sea sea; 53 | application/x-shockwave-flash swf; 54 | application/x-stuffit sit; 55 | application/x-tcl tcl tk; 56 | application/x-x509-ca-cert der pem crt; 57 | application/x-xpinstall xpi; 58 | application/xhtml+xml xhtml; 59 | application/xspf+xml xspf; 60 | application/zip zip; 61 | 62 | application/octet-stream bin exe dll; 63 | application/octet-stream deb; 64 | application/octet-stream dmg; 65 | application/octet-stream iso img; 66 | application/octet-stream msi msp msm; 67 | 68 | application/vnd.openxmlformats-officedocument.wordprocessingml.document docx; 69 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx; 70 | application/vnd.openxmlformats-officedocument.presentationml.presentation pptx; 71 | 72 | audio/midi mid midi kar; 73 | audio/mpeg mp3; 74 | audio/ogg ogg; 75 | audio/x-m4a m4a; 76 | audio/x-realaudio ra; 77 | 78 | video/3gpp 3gpp 3gp; 79 | video/mp2t ts; 80 | video/mp4 mp4; 81 | video/mpeg mpeg mpg; 82 | video/quicktime mov; 83 | video/webm webm; 84 | video/x-flv flv; 85 | video/x-m4v m4v; 86 | video/x-mng mng; 87 | video/x-ms-asf asx asf; 88 | video/x-ms-wmv wmv; 89 | video/x-msvideo avi; 90 | font/ttf ttf; 91 | } 92 | -------------------------------------------------------------------------------- /src/Protocols/ProtocolInterface.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use Workerman\Connection\ConnectionInterface; 20 | 21 | /** 22 | * Protocol interface 23 | */ 24 | interface ProtocolInterface 25 | { 26 | /** 27 | * Check the integrity of the package. 28 | * Please return the length of package. 29 | * If length is unknown please return 0 that means waiting for more data. 30 | * If the package has something wrong please return -1 the connection will be closed. 31 | * 32 | * @param string $buffer 33 | * @param ConnectionInterface $connection 34 | * @return int 35 | */ 36 | public static function input(string $buffer, ConnectionInterface $connection): int; 37 | 38 | /** 39 | * Decode package and emit onMessage($message) callback, $message is the result that decode returned. 40 | * 41 | * @param string $buffer 42 | * @param ConnectionInterface $connection 43 | * @return mixed 44 | */ 45 | public static function decode(string $buffer, ConnectionInterface $connection): mixed; 46 | 47 | /** 48 | * Encode package before sending to client. 49 | * 50 | * @param mixed $data 51 | * @param ConnectionInterface $connection 52 | * @return string 53 | */ 54 | public static function encode(mixed $data, ConnectionInterface $connection): string; 55 | } 56 | -------------------------------------------------------------------------------- /src/Protocols/Text.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use Workerman\Connection\ConnectionInterface; 20 | use function rtrim; 21 | use function strlen; 22 | use function strpos; 23 | 24 | /** 25 | * Text Protocol. 26 | */ 27 | class Text 28 | { 29 | /** 30 | * Check the integrity of the package. 31 | * 32 | * @param string $buffer 33 | * @param ConnectionInterface $connection 34 | * @return int 35 | */ 36 | public static function input(string $buffer, ConnectionInterface $connection): int 37 | { 38 | // Judge whether the package length exceeds the limit. 39 | if (isset($connection->maxPackageSize) && strlen($buffer) >= $connection->maxPackageSize) { 40 | $connection->close(); 41 | return 0; 42 | } 43 | // Find the position of "\n". 44 | $pos = strpos($buffer, "\n"); 45 | // No "\n", packet length is unknown, continue to wait for the data so return 0. 46 | if ($pos === false) { 47 | return 0; 48 | } 49 | // Return the current package length. 50 | return $pos + 1; 51 | } 52 | 53 | /** 54 | * Encode. 55 | * 56 | * @param string $buffer 57 | * @return string 58 | */ 59 | public static function encode(string $buffer): string 60 | { 61 | // Add "\n" 62 | return $buffer . "\n"; 63 | } 64 | 65 | /** 66 | * Decode. 67 | * 68 | * @param string $buffer 69 | * @return string 70 | */ 71 | public static function decode(string $buffer): string 72 | { 73 | // Remove "\n" 74 | return rtrim($buffer, "\r\n"); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Protocols/Websocket.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use Throwable; 20 | use Workerman\Connection\ConnectionInterface; 21 | use Workerman\Connection\TcpConnection; 22 | use Workerman\Protocols\Http\Request; 23 | use Workerman\Worker; 24 | use function base64_encode; 25 | use function chr; 26 | use function deflate_add; 27 | use function deflate_init; 28 | use function floor; 29 | use function inflate_add; 30 | use function inflate_init; 31 | use function is_scalar; 32 | use function ord; 33 | use function pack; 34 | use function preg_match; 35 | use function sha1; 36 | use function str_repeat; 37 | use function stripos; 38 | use function strlen; 39 | use function strpos; 40 | use function substr; 41 | use function unpack; 42 | use const ZLIB_DEFAULT_STRATEGY; 43 | use const ZLIB_ENCODING_RAW; 44 | 45 | /** 46 | * WebSocket protocol. 47 | */ 48 | class Websocket 49 | { 50 | /** 51 | * Websocket blob type. 52 | * 53 | * @var string 54 | */ 55 | public const BINARY_TYPE_BLOB = "\x81"; 56 | 57 | /** 58 | * Websocket blob type. 59 | * 60 | * @var string 61 | */ 62 | const BINARY_TYPE_BLOB_DEFLATE = "\xc1"; 63 | 64 | /** 65 | * Websocket arraybuffer type. 66 | * 67 | * @var string 68 | */ 69 | public const BINARY_TYPE_ARRAYBUFFER = "\x82"; 70 | 71 | /** 72 | * Websocket arraybuffer type. 73 | * 74 | * @var string 75 | */ 76 | const BINARY_TYPE_ARRAYBUFFER_DEFLATE = "\xc2"; 77 | 78 | /** 79 | * Check the integrity of the package. 80 | * 81 | * @param string $buffer 82 | * @param TcpConnection $connection 83 | * @return int 84 | */ 85 | public static function input(string $buffer, TcpConnection $connection): int 86 | { 87 | $connection->websocketOrigin = $connection->websocketOrigin ?? null; 88 | $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; 89 | // Receive length. 90 | $recvLen = strlen($buffer); 91 | // We need more data. 92 | if ($recvLen < 6) { 93 | return 0; 94 | } 95 | 96 | // Has not yet completed the handshake. 97 | if (empty($connection->context->websocketHandshake)) { 98 | return static::dealHandshake($buffer, $connection); 99 | } 100 | 101 | // Buffer websocket frame data. 102 | if ($connection->context->websocketCurrentFrameLength) { 103 | // We need more frame data. 104 | if ($connection->context->websocketCurrentFrameLength > $recvLen) { 105 | // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. 106 | return 0; 107 | } 108 | } else { 109 | $firstByte = ord($buffer[0]); 110 | $secondByte = ord($buffer[1]); 111 | $dataLen = $secondByte & 127; 112 | $isFinFrame = $firstByte >> 7; 113 | $masked = $secondByte >> 7; 114 | 115 | if (!$masked) { 116 | Worker::safeEcho("frame not masked so close the connection\n"); 117 | $connection->close(); 118 | return 0; 119 | } 120 | 121 | $opcode = $firstByte & 0xf; 122 | switch ($opcode) { 123 | case 0x0: 124 | // Blob type. 125 | case 0x1: 126 | // Arraybuffer type. 127 | case 0x2: 128 | // Ping package. 129 | case 0x9: 130 | // Pong package. 131 | case 0xa: 132 | break; 133 | // Close package. 134 | case 0x8: 135 | // Try to emit onWebSocketClose callback. 136 | $closeCb = $connection->onWebSocketClose ?? $connection->worker->onWebSocketClose ?? false; 137 | if ($closeCb) { 138 | try { 139 | $closeCb($connection); 140 | } catch (Throwable $e) { 141 | Worker::stopAll(250, $e); 142 | } 143 | } // Close connection. 144 | else { 145 | $connection->close("\x88\x02\x03\xe8", true); 146 | } 147 | return 0; 148 | // Wrong opcode. 149 | default : 150 | Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n"); 151 | $connection->close(); 152 | return 0; 153 | } 154 | 155 | // Calculate packet length. 156 | $headLen = 6; 157 | if ($dataLen === 126) { 158 | $headLen = 8; 159 | if ($headLen > $recvLen) { 160 | return 0; 161 | } 162 | $pack = unpack('nn/ntotal_len', $buffer); 163 | $dataLen = $pack['total_len']; 164 | } else { 165 | if ($dataLen === 127) { 166 | $headLen = 14; 167 | if ($headLen > $recvLen) { 168 | return 0; 169 | } 170 | $arr = unpack('n/N2c', $buffer); 171 | $dataLen = $arr['c1'] * 4294967296 + $arr['c2']; 172 | } 173 | } 174 | $currentFrameLength = $headLen + $dataLen; 175 | 176 | $totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength; 177 | if ($totalPackageSize > $connection->maxPackageSize) { 178 | Worker::safeEcho("error package. package_length=$totalPackageSize\n"); 179 | $connection->close(); 180 | return 0; 181 | } 182 | 183 | if ($isFinFrame) { 184 | if ($opcode === 0x9) { 185 | if ($recvLen >= $currentFrameLength) { 186 | $pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); 187 | $connection->consumeRecvBuffer($currentFrameLength); 188 | $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; 189 | $connection->websocketType = "\x8a"; 190 | $pingCb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false; 191 | if ($pingCb) { 192 | try { 193 | $pingCb($connection, $pingData); 194 | } catch (Throwable $e) { 195 | Worker::stopAll(250, $e); 196 | } 197 | } else { 198 | $connection->send($pingData); 199 | } 200 | $connection->websocketType = $tmpConnectionType; 201 | if ($recvLen > $currentFrameLength) { 202 | return static::input(substr($buffer, $currentFrameLength), $connection); 203 | } 204 | } 205 | return 0; 206 | } 207 | 208 | if ($opcode === 0xa) { 209 | if ($recvLen >= $currentFrameLength) { 210 | $pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); 211 | $connection->consumeRecvBuffer($currentFrameLength); 212 | $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; 213 | $connection->websocketType = "\x8a"; 214 | // Try to emit onWebSocketPong callback. 215 | $pongCb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false; 216 | if ($pongCb) { 217 | try { 218 | $pongCb($connection, $pongData); 219 | } catch (Throwable $e) { 220 | Worker::stopAll(250, $e); 221 | } 222 | } 223 | $connection->websocketType = $tmpConnectionType; 224 | if ($recvLen > $currentFrameLength) { 225 | return static::input(substr($buffer, $currentFrameLength), $connection); 226 | } 227 | } 228 | return 0; 229 | } 230 | return $currentFrameLength; 231 | } 232 | 233 | $connection->context->websocketCurrentFrameLength = $currentFrameLength; 234 | } 235 | 236 | // Received just a frame length data. 237 | if ($connection->context->websocketCurrentFrameLength === $recvLen) { 238 | static::decode($buffer, $connection); 239 | $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); 240 | $connection->context->websocketCurrentFrameLength = 0; 241 | return 0; 242 | } 243 | 244 | // The length of the received data is greater than the length of a frame. 245 | if ($connection->context->websocketCurrentFrameLength < $recvLen) { 246 | static::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); 247 | $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); 248 | $currentFrameLength = $connection->context->websocketCurrentFrameLength; 249 | $connection->context->websocketCurrentFrameLength = 0; 250 | // Continue to read next frame. 251 | return static::input(substr($buffer, $currentFrameLength), $connection); 252 | } 253 | 254 | // The length of the received data is less than the length of a frame. 255 | return 0; 256 | } 257 | 258 | /** 259 | * Websocket encode. 260 | * 261 | * @param mixed $buffer 262 | * @param TcpConnection $connection 263 | * @return string 264 | */ 265 | public static function encode(mixed $buffer, TcpConnection $connection): string 266 | { 267 | if (!is_scalar($buffer)) { 268 | $buffer = json_encode($buffer, JSON_UNESCAPED_UNICODE); 269 | } 270 | 271 | if (empty($connection->websocketType)) { 272 | $connection->websocketType = static::BINARY_TYPE_BLOB; 273 | } 274 | 275 | if (ord($connection->websocketType) & 64) { 276 | $buffer = static::deflate($connection, $buffer); 277 | } 278 | 279 | $firstByte = $connection->websocketType; 280 | $len = strlen($buffer); 281 | 282 | if ($len <= 125) { 283 | $encodeBuffer = $firstByte . chr($len) . $buffer; 284 | } else { 285 | if ($len <= 65535) { 286 | $encodeBuffer = $firstByte . chr(126) . pack("n", $len) . $buffer; 287 | } else { 288 | $encodeBuffer = $firstByte . chr(127) . pack("xxxxN", $len) . $buffer; 289 | } 290 | } 291 | 292 | // Handshake not completed so temporary buffer websocket data waiting for send. 293 | if (empty($connection->context->websocketHandshake)) { 294 | if (empty($connection->context->tmpWebsocketData)) { 295 | $connection->context->tmpWebsocketData = ''; 296 | } 297 | // If buffer has already full then discard the current package. 298 | if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { 299 | if ($connection->onError) { 300 | try { 301 | ($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package'); 302 | } catch (Throwable $e) { 303 | Worker::stopAll(250, $e); 304 | } 305 | } 306 | return ''; 307 | } 308 | $connection->context->tmpWebsocketData .= $encodeBuffer; 309 | // Check buffer is full. 310 | if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) { 311 | try { 312 | ($connection->onBufferFull)($connection); 313 | } catch (Throwable $e) { 314 | Worker::stopAll(250, $e); 315 | } 316 | } 317 | // Return empty string. 318 | return ''; 319 | } 320 | 321 | return $encodeBuffer; 322 | } 323 | 324 | /** 325 | * Websocket decode. 326 | * 327 | * @param string $buffer 328 | * @param TcpConnection $connection 329 | * @return string 330 | */ 331 | public static function decode(string $buffer, TcpConnection $connection): string 332 | { 333 | $firstByte = ord($buffer[0]); 334 | $secondByte = ord($buffer[1]); 335 | $len = $secondByte & 127; 336 | $isFinFrame = (bool)($firstByte >> 7); 337 | $rsv1 = 64 === ($firstByte & 64); 338 | 339 | if ($len === 126) { 340 | $masks = substr($buffer, 4, 4); 341 | $data = substr($buffer, 8); 342 | } else { 343 | if ($len === 127) { 344 | $masks = substr($buffer, 10, 4); 345 | $data = substr($buffer, 14); 346 | } else { 347 | $masks = substr($buffer, 2, 4); 348 | $data = substr($buffer, 6); 349 | } 350 | } 351 | $dataLength = strlen($data); 352 | $masks = str_repeat($masks, (int)floor($dataLength / 4)) . substr($masks, 0, $dataLength % 4); 353 | $decoded = $data ^ $masks; 354 | if ($connection->context->websocketCurrentFrameLength) { 355 | $connection->context->websocketDataBuffer .= $decoded; 356 | if ($rsv1) { 357 | return static::inflate($connection, $connection->context->websocketDataBuffer, $isFinFrame); 358 | } 359 | return $connection->context->websocketDataBuffer; 360 | } 361 | if ($connection->context->websocketDataBuffer !== '') { 362 | $decoded = $connection->context->websocketDataBuffer . $decoded; 363 | $connection->context->websocketDataBuffer = ''; 364 | } 365 | if ($rsv1) { 366 | return static::inflate($connection, $decoded, $isFinFrame); 367 | } 368 | return $decoded; 369 | } 370 | 371 | /** 372 | * Inflate. 373 | * 374 | * @param TcpConnection $connection 375 | * @param string $buffer 376 | * @param bool $isFinFrame 377 | * @return false|string 378 | */ 379 | protected static function inflate(TcpConnection $connection, string $buffer, bool $isFinFrame): bool|string 380 | { 381 | if (!isset($connection->context->inflator)) { 382 | $connection->context->inflator = inflate_init( 383 | ZLIB_ENCODING_RAW, 384 | [ 385 | 'level' => -1, 386 | 'memory' => 8, 387 | 'window' => 15, 388 | 'strategy' => ZLIB_DEFAULT_STRATEGY 389 | ] 390 | ); 391 | } 392 | if ($isFinFrame) { 393 | $buffer .= "\x00\x00\xff\xff"; 394 | } 395 | return inflate_add($connection->context->inflator, $buffer); 396 | } 397 | 398 | /** 399 | * Deflate. 400 | * 401 | * @param TcpConnection $connection 402 | * @param string $buffer 403 | * @return false|string 404 | */ 405 | protected static function deflate(TcpConnection $connection, string $buffer): bool|string 406 | { 407 | if (!isset($connection->context->deflator)) { 408 | $connection->context->deflator = deflate_init( 409 | ZLIB_ENCODING_RAW, 410 | [ 411 | 'level' => -1, 412 | 'memory' => 8, 413 | 'window' => 15, 414 | 'strategy' => ZLIB_DEFAULT_STRATEGY 415 | ] 416 | ); 417 | } 418 | return substr(deflate_add($connection->context->deflator, $buffer), 0, -4); 419 | } 420 | 421 | /** 422 | * Websocket handshake. 423 | * 424 | * @param string $buffer 425 | * @param TcpConnection $connection 426 | * @return int 427 | */ 428 | public static function dealHandshake(string $buffer, TcpConnection $connection): int 429 | { 430 | // HTTP protocol. 431 | if (str_starts_with($buffer, 'GET')) { 432 | // Find \r\n\r\n. 433 | $headerEndPos = strpos($buffer, "\r\n\r\n"); 434 | if (!$headerEndPos) { 435 | return 0; 436 | } 437 | $headerLength = $headerEndPos + 4; 438 | 439 | // Get Sec-WebSocket-Key. 440 | if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) { 441 | $SecWebSocketKey = $match[1]; 442 | } else { 443 | $connection->close( 444 | "HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

WebSocket


workerman
", true); 445 | return 0; 446 | } 447 | // Calculation websocket key. 448 | $newKey = base64_encode(sha1($SecWebSocketKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); 449 | // Handshake response data. 450 | $handshakeMessage = "HTTP/1.1 101 Switching Protocol\r\n" 451 | . "Upgrade: websocket\r\n" 452 | . "Sec-WebSocket-Version: 13\r\n" 453 | . "Connection: Upgrade\r\n" 454 | . "Sec-WebSocket-Accept: " . $newKey . "\r\n"; 455 | 456 | // Websocket data buffer. 457 | $connection->context->websocketDataBuffer = ''; 458 | // Current websocket frame length. 459 | $connection->context->websocketCurrentFrameLength = 0; 460 | // Current websocket frame data. 461 | $connection->context->websocketCurrentFrameBuffer = ''; 462 | // Consume handshake data. 463 | $connection->consumeRecvBuffer($headerLength); 464 | // Request from buffer 465 | $request = new Request($buffer); 466 | 467 | // Try to emit onWebSocketConnect callback. 468 | $onWebsocketConnect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false; 469 | if ($onWebsocketConnect) { 470 | try { 471 | $onWebsocketConnect($connection, $request); 472 | } catch (Throwable $e) { 473 | Worker::stopAll(250, $e); 474 | } 475 | } 476 | 477 | // blob or arraybuffer 478 | if (empty($connection->websocketType)) { 479 | $connection->websocketType = static::BINARY_TYPE_BLOB; 480 | } 481 | 482 | $hasServerHeader = false; 483 | 484 | if ($connection->headers) { 485 | foreach ($connection->headers as $header) { 486 | if (stripos($header, 'Server:') === 0) { 487 | $hasServerHeader = true; 488 | } 489 | $handshakeMessage .= "$header\r\n"; 490 | } 491 | } 492 | if (!$hasServerHeader) { 493 | $handshakeMessage .= "Server: workerman\r\n"; 494 | } 495 | $handshakeMessage .= "\r\n"; 496 | // Send handshake response. 497 | $connection->send($handshakeMessage, true); 498 | // Mark handshake complete. 499 | $connection->context->websocketHandshake = true; 500 | 501 | // Try to emit onWebSocketConnected callback. 502 | $onWebsocketConnected = $connection->onWebSocketConnected ?? $connection->worker->onWebSocketConnected ?? false; 503 | if ($onWebsocketConnected) { 504 | try { 505 | $onWebsocketConnected($connection, $request); 506 | } catch (Throwable $e) { 507 | Worker::stopAll(250, $e); 508 | } 509 | } 510 | 511 | // There are data waiting to be sent. 512 | if (!empty($connection->context->tmpWebsocketData)) { 513 | $connection->send($connection->context->tmpWebsocketData, true); 514 | $connection->context->tmpWebsocketData = ''; 515 | } 516 | if (strlen($buffer) > $headerLength) { 517 | return static::input(substr($buffer, $headerLength), $connection); 518 | } 519 | return 0; 520 | } 521 | // Bad websocket handshake request. 522 | $connection->close( 523 | "HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n

400 Bad Request


workerman
", true); 524 | return 0; 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /src/Protocols/Ws.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman\Protocols; 18 | 19 | use Throwable; 20 | use Workerman\Connection\AsyncTcpConnection; 21 | use Workerman\Connection\ConnectionInterface; 22 | use Workerman\Protocols\Http\Response; 23 | use Workerman\Timer; 24 | use Workerman\Worker; 25 | use function base64_encode; 26 | use function bin2hex; 27 | use function explode; 28 | use function floor; 29 | use function ord; 30 | use function pack; 31 | use function preg_match; 32 | use function sha1; 33 | use function str_repeat; 34 | use function strlen; 35 | use function strpos; 36 | use function substr; 37 | use function trim; 38 | use function unpack; 39 | 40 | /** 41 | * Websocket protocol for client. 42 | */ 43 | class Ws 44 | { 45 | /** 46 | * Websocket blob type. 47 | * 48 | * @var string 49 | */ 50 | public const BINARY_TYPE_BLOB = "\x81"; 51 | 52 | /** 53 | * Websocket arraybuffer type. 54 | * 55 | * @var string 56 | */ 57 | public const BINARY_TYPE_ARRAYBUFFER = "\x82"; 58 | 59 | /** 60 | * Check the integrity of the package. 61 | * 62 | * @param string $buffer 63 | * @param AsyncTcpConnection $connection 64 | * @return int 65 | */ 66 | public static function input(string $buffer, AsyncTcpConnection $connection): int 67 | { 68 | if (empty($connection->context->handshakeStep)) { 69 | Worker::safeEcho("recv data before handshake. Buffer:" . bin2hex($buffer) . "\n"); 70 | return -1; 71 | } 72 | // Recv handshake response 73 | if ($connection->context->handshakeStep === 1) { 74 | return self::dealHandshake($buffer, $connection); 75 | } 76 | $recvLen = strlen($buffer); 77 | if ($recvLen < 2) { 78 | return 0; 79 | } 80 | // Buffer websocket frame data. 81 | if ($connection->context->websocketCurrentFrameLength) { 82 | // We need more frame data. 83 | if ($connection->context->websocketCurrentFrameLength > $recvLen) { 84 | // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. 85 | return 0; 86 | } 87 | } else { 88 | 89 | $firstByte = ord($buffer[0]); 90 | $secondByte = ord($buffer[1]); 91 | $dataLen = $secondByte & 127; 92 | $isFinFrame = $firstByte >> 7; 93 | $masked = $secondByte >> 7; 94 | 95 | if ($masked) { 96 | Worker::safeEcho("frame masked so close the connection\n"); 97 | $connection->close(); 98 | return 0; 99 | } 100 | 101 | $opcode = $firstByte & 0xf; 102 | 103 | switch ($opcode) { 104 | case 0x0: 105 | // Blob type. 106 | case 0x1: 107 | // Arraybuffer type. 108 | case 0x2: 109 | // Ping package. 110 | case 0x9: 111 | // Pong package. 112 | case 0xa: 113 | break; 114 | // Close package. 115 | case 0x8: 116 | // Try to emit onWebSocketClose callback. 117 | if (isset($connection->onWebSocketClose)) { 118 | try { 119 | ($connection->onWebSocketClose)($connection, self::decode($buffer, $connection)); 120 | } catch (Throwable $e) { 121 | Worker::stopAll(250, $e); 122 | } 123 | } // Close connection. 124 | else { 125 | $connection->close(); 126 | } 127 | return 0; 128 | // Wrong opcode. 129 | default : 130 | Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n"); 131 | $connection->close(); 132 | return 0; 133 | } 134 | // Calculate packet length. 135 | if ($dataLen === 126) { 136 | if (strlen($buffer) < 4) { 137 | return 0; 138 | } 139 | $pack = unpack('nn/ntotal_len', $buffer); 140 | $currentFrameLength = $pack['total_len'] + 4; 141 | } else if ($dataLen === 127) { 142 | if (strlen($buffer) < 10) { 143 | return 0; 144 | } 145 | $arr = unpack('n/N2c', $buffer); 146 | $currentFrameLength = $arr['c1'] * 4294967296 + $arr['c2'] + 10; 147 | } else { 148 | $currentFrameLength = $dataLen + 2; 149 | } 150 | 151 | $totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength; 152 | if ($totalPackageSize > $connection->maxPackageSize) { 153 | Worker::safeEcho("error package. package_length=$totalPackageSize\n"); 154 | $connection->close(); 155 | return 0; 156 | } 157 | 158 | if ($isFinFrame) { 159 | if ($opcode === 0x9) { 160 | if ($recvLen >= $currentFrameLength) { 161 | $pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); 162 | $connection->consumeRecvBuffer($currentFrameLength); 163 | $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; 164 | $connection->websocketType = "\x8a"; 165 | if (isset($connection->onWebSocketPing)) { 166 | try { 167 | ($connection->onWebSocketPing)($connection, $pingData); 168 | } catch (Throwable $e) { 169 | Worker::stopAll(250, $e); 170 | } 171 | } else { 172 | $connection->send($pingData); 173 | } 174 | $connection->websocketType = $tmpConnectionType; 175 | if ($recvLen > $currentFrameLength) { 176 | return static::input(substr($buffer, $currentFrameLength), $connection); 177 | } 178 | } 179 | return 0; 180 | 181 | } 182 | 183 | if ($opcode === 0xa) { 184 | if ($recvLen >= $currentFrameLength) { 185 | $pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection); 186 | $connection->consumeRecvBuffer($currentFrameLength); 187 | $tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB; 188 | $connection->websocketType = "\x8a"; 189 | // Try to emit onWebSocketPong callback. 190 | if (isset($connection->onWebSocketPong)) { 191 | try { 192 | ($connection->onWebSocketPong)($connection, $pongData); 193 | } catch (Throwable $e) { 194 | Worker::stopAll(250, $e); 195 | } 196 | } 197 | $connection->websocketType = $tmpConnectionType; 198 | if ($recvLen > $currentFrameLength) { 199 | return static::input(substr($buffer, $currentFrameLength), $connection); 200 | } 201 | } 202 | return 0; 203 | } 204 | return $currentFrameLength; 205 | } 206 | 207 | $connection->context->websocketCurrentFrameLength = $currentFrameLength; 208 | } 209 | // Received just a frame length data. 210 | if ($connection->context->websocketCurrentFrameLength === $recvLen) { 211 | self::decode($buffer, $connection); 212 | $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); 213 | $connection->context->websocketCurrentFrameLength = 0; 214 | return 0; 215 | } // The length of the received data is greater than the length of a frame. 216 | elseif ($connection->context->websocketCurrentFrameLength < $recvLen) { 217 | self::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection); 218 | $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength); 219 | $currentFrameLength = $connection->context->websocketCurrentFrameLength; 220 | $connection->context->websocketCurrentFrameLength = 0; 221 | // Continue to read next frame. 222 | return self::input(substr($buffer, $currentFrameLength), $connection); 223 | } // The length of the received data is less than the length of a frame. 224 | else { 225 | return 0; 226 | } 227 | } 228 | 229 | /** 230 | * Websocket encode. 231 | * 232 | * @param string $payload 233 | * @param AsyncTcpConnection $connection 234 | * @return string 235 | * @throws Throwable 236 | */ 237 | public static function encode(string $payload, AsyncTcpConnection $connection): string 238 | { 239 | if (empty($connection->websocketType)) { 240 | $connection->websocketType = self::BINARY_TYPE_BLOB; 241 | } 242 | $connection->websocketOrigin = $connection->websocketOrigin ?? null; 243 | $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; 244 | if (empty($connection->context->handshakeStep)) { 245 | static::sendHandshake($connection); 246 | } 247 | 248 | $maskKey = "\x00\x00\x00\x00"; 249 | $length = strlen($payload); 250 | 251 | if (strlen($payload) < 126) { 252 | $head = chr(0x80 | $length); 253 | } elseif ($length < 0xFFFF) { 254 | $head = chr(0x80 | 126) . pack("n", $length); 255 | } else { 256 | $head = chr(0x80 | 127) . pack("N", 0) . pack("N", $length); 257 | } 258 | 259 | $frame = $connection->websocketType . $head . $maskKey; 260 | // append payload to frame: 261 | $maskKey = str_repeat($maskKey, (int)floor($length / 4)) . substr($maskKey, 0, $length % 4); 262 | $frame .= $payload ^ $maskKey; 263 | if ($connection->context->handshakeStep === 1) { 264 | // If buffer has already full then discard the current package. 265 | if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) { 266 | if ($connection->onError) { 267 | try { 268 | ($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package'); 269 | } catch (Throwable $e) { 270 | Worker::stopAll(250, $e); 271 | } 272 | } 273 | return ''; 274 | } 275 | $connection->context->tmpWebsocketData .= $frame; 276 | // Check buffer is full. 277 | if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) { 278 | try { 279 | ($connection->onBufferFull)($connection); 280 | } catch (Throwable $e) { 281 | Worker::stopAll(250, $e); 282 | } 283 | } 284 | return ''; 285 | } 286 | return $frame; 287 | } 288 | 289 | /** 290 | * Websocket decode. 291 | * 292 | * @param string $bytes 293 | * @param AsyncTcpConnection $connection 294 | * @return string 295 | */ 296 | public static function decode(string $bytes, AsyncTcpConnection $connection): string 297 | { 298 | $dataLength = ord($bytes[1]); 299 | 300 | if ($dataLength === 126) { 301 | $decodedData = substr($bytes, 4); 302 | } else if ($dataLength === 127) { 303 | $decodedData = substr($bytes, 10); 304 | } else { 305 | $decodedData = substr($bytes, 2); 306 | } 307 | if ($connection->context->websocketCurrentFrameLength) { 308 | $connection->context->websocketDataBuffer .= $decodedData; 309 | return $connection->context->websocketDataBuffer; 310 | } 311 | 312 | if ($connection->context->websocketDataBuffer !== '') { 313 | $decodedData = $connection->context->websocketDataBuffer . $decodedData; 314 | $connection->context->websocketDataBuffer = ''; 315 | } 316 | return $decodedData; 317 | } 318 | 319 | /** 320 | * Send websocket handshake data. 321 | * 322 | * @param AsyncTcpConnection $connection 323 | * @return void 324 | * @throws Throwable 325 | */ 326 | public static function onConnect(AsyncTcpConnection $connection): void 327 | { 328 | $connection->websocketOrigin = $connection->websocketOrigin ?? null; 329 | $connection->websocketClientProtocol = $connection->websocketClientProtocol ?? null; 330 | static::sendHandshake($connection); 331 | } 332 | 333 | /** 334 | * Clean 335 | * 336 | * @param AsyncTcpConnection $connection 337 | */ 338 | public static function onClose(AsyncTcpConnection $connection): void 339 | { 340 | $connection->context->handshakeStep = null; 341 | $connection->context->websocketCurrentFrameLength = 0; 342 | $connection->context->tmpWebsocketData = ''; 343 | $connection->context->websocketDataBuffer = ''; 344 | if (!empty($connection->context->websocketPingTimer)) { 345 | Timer::del($connection->context->websocketPingTimer); 346 | $connection->context->websocketPingTimer = null; 347 | } 348 | } 349 | 350 | /** 351 | * Send websocket handshake. 352 | * 353 | * @param AsyncTcpConnection $connection 354 | * @return void 355 | * @throws Throwable 356 | */ 357 | public static function sendHandshake(AsyncTcpConnection $connection): void 358 | { 359 | if (!empty($connection->context->handshakeStep)) { 360 | return; 361 | } 362 | // Get Host. 363 | $port = $connection->getRemotePort(); 364 | $host = $port === 80 || $port === 443 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port; 365 | // Handshake header. 366 | $connection->context->websocketSecKey = base64_encode(random_bytes(16)); 367 | $userHeader = $connection->headers ?? null; 368 | $userHeaderStr = ''; 369 | if (!empty($userHeader)) { 370 | foreach ($userHeader as $k => $v) { 371 | $userHeaderStr .= "$k: $v\r\n"; 372 | } 373 | $userHeaderStr = "\r\n" . trim($userHeaderStr); 374 | } 375 | $header = 'GET ' . $connection->getRemoteURI() . " HTTP/1.1\r\n" . 376 | (!preg_match("/\nHost:/i", $userHeaderStr) ? "Host: $host\r\n" : '') . 377 | "Connection: Upgrade\r\n" . 378 | "Upgrade: websocket\r\n" . 379 | (($connection->websocketOrigin ?? null) ? "Origin: " . $connection->websocketOrigin . "\r\n" : '') . 380 | (($connection->websocketClientProtocol ?? null) ? "Sec-WebSocket-Protocol: " . $connection->websocketClientProtocol . "\r\n" : '') . 381 | "Sec-WebSocket-Version: 13\r\n" . 382 | "Sec-WebSocket-Key: " . $connection->context->websocketSecKey . $userHeaderStr . "\r\n\r\n"; 383 | $connection->send($header, true); 384 | $connection->context->handshakeStep = 1; 385 | $connection->context->websocketCurrentFrameLength = 0; 386 | $connection->context->websocketDataBuffer = ''; 387 | $connection->context->tmpWebsocketData = ''; 388 | } 389 | 390 | /** 391 | * Websocket handshake. 392 | * 393 | * @param string $buffer 394 | * @param AsyncTcpConnection $connection 395 | * @return bool|int 396 | */ 397 | public static function dealHandshake(string $buffer, AsyncTcpConnection $connection): bool|int 398 | { 399 | $pos = strpos($buffer, "\r\n\r\n"); 400 | if ($pos) { 401 | //checking Sec-WebSocket-Accept 402 | if (preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) { 403 | if ($match[1] !== base64_encode(sha1($connection->context->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) { 404 | Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . substr($buffer, 0, $pos) . "\n"); 405 | $connection->close(); 406 | return 0; 407 | } 408 | } else { 409 | Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . substr($buffer, 0, $pos) . "\n"); 410 | $connection->close(); 411 | return 0; 412 | } 413 | 414 | // handshake complete 415 | $connection->context->handshakeStep = 2; 416 | $handshakeResponseLength = $pos + 4; 417 | $buffer = substr($buffer, 0, $handshakeResponseLength); 418 | $response = static::parseResponse($buffer); 419 | // Try to emit onWebSocketConnect callback. 420 | if (isset($connection->onWebSocketConnect)) { 421 | try { 422 | ($connection->onWebSocketConnect)($connection, $response); 423 | } catch (Throwable $e) { 424 | Worker::stopAll(250, $e); 425 | } 426 | } 427 | // Headbeat. 428 | if (!empty($connection->websocketPingInterval)) { 429 | $connection->context->websocketPingTimer = Timer::add($connection->websocketPingInterval, function () use ($connection) { 430 | if (false === $connection->send(pack('H*', '898000000000'), true)) { 431 | Timer::del($connection->context->websocketPingTimer); 432 | $connection->context->websocketPingTimer = null; 433 | } 434 | }); 435 | } 436 | 437 | $connection->consumeRecvBuffer($handshakeResponseLength); 438 | if (!empty($connection->context->tmpWebsocketData)) { 439 | $connection->send($connection->context->tmpWebsocketData, true); 440 | $connection->context->tmpWebsocketData = ''; 441 | } 442 | if (strlen($buffer) > $handshakeResponseLength) { 443 | return self::input(substr($buffer, $handshakeResponseLength), $connection); 444 | } 445 | } 446 | return 0; 447 | } 448 | 449 | /** 450 | * Parse response. 451 | * 452 | * @param string $buffer 453 | * @return Response 454 | */ 455 | protected static function parseResponse(string $buffer): Response 456 | { 457 | [$http_header, ] = explode("\r\n\r\n", $buffer, 2); 458 | $header_data = explode("\r\n", $http_header); 459 | [$protocol, $status, $phrase] = explode(' ', $header_data[0], 3); 460 | $protocolVersion = substr($protocol, 5); 461 | unset($header_data[0]); 462 | $headers = []; 463 | foreach ($header_data as $content) { 464 | // \r\n\r\n 465 | if (empty($content)) { 466 | continue; 467 | } 468 | list($key, $value) = explode(':', $content, 2); 469 | $value = trim($value); 470 | $headers[$key] = $value; 471 | } 472 | return (new Response())->withStatus((int)$status, $phrase)->withHeaders($headers)->withProtocolVersion($protocolVersion); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/Timer.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Workerman; 18 | 19 | use RuntimeException; 20 | use Throwable; 21 | use Workerman\Events\EventInterface; 22 | use Workerman\Events\Fiber; 23 | use Workerman\Events\Swoole; 24 | use Revolt\EventLoop; 25 | use Swoole\Coroutine\System; 26 | use function function_exists; 27 | use function pcntl_alarm; 28 | use function pcntl_signal; 29 | use function time; 30 | use const PHP_INT_MAX; 31 | use const SIGALRM; 32 | 33 | /** 34 | * Timer. 35 | */ 36 | class Timer 37 | { 38 | /** 39 | * Tasks that based on ALARM signal. 40 | * [ 41 | * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], 42 | * run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]], 43 | * .. 44 | * ] 45 | * 46 | * @var array 47 | */ 48 | protected static array $tasks = []; 49 | 50 | /** 51 | * Event 52 | * 53 | * @var ?EventInterface 54 | */ 55 | protected static ?EventInterface $event = null; 56 | 57 | /** 58 | * Timer id 59 | * 60 | * @var int 61 | */ 62 | protected static int $timerId = 0; 63 | 64 | /** 65 | * Timer status 66 | * [ 67 | * timer_id1 => bool, 68 | * timer_id2 => bool, 69 | * ...................., 70 | * ] 71 | * 72 | * @var array 73 | */ 74 | protected static array $status = []; 75 | 76 | /** 77 | * Init. 78 | * 79 | * @param EventInterface|null $event 80 | * @return void 81 | */ 82 | public static function init(?EventInterface $event = null): void 83 | { 84 | if ($event) { 85 | self::$event = $event; 86 | return; 87 | } 88 | if (function_exists('pcntl_signal')) { 89 | pcntl_signal(SIGALRM, self::signalHandle(...), false); 90 | } 91 | } 92 | 93 | /** 94 | * Repeat. 95 | * 96 | * @param float $timeInterval 97 | * @param callable $func 98 | * @param array $args 99 | * @return int 100 | */ 101 | public static function repeat(float $timeInterval, callable $func, array $args = []): int 102 | { 103 | return self::$event->repeat($timeInterval, $func, $args); 104 | } 105 | 106 | /** 107 | * Delay. 108 | * 109 | * @param float $timeInterval 110 | * @param callable $func 111 | * @param array $args 112 | * @return int 113 | */ 114 | public static function delay(float $timeInterval, callable $func, array $args = []): int 115 | { 116 | return self::$event->delay($timeInterval, $func, $args); 117 | } 118 | 119 | /** 120 | * ALARM signal handler. 121 | * 122 | * @return void 123 | */ 124 | public static function signalHandle(): void 125 | { 126 | if (!self::$event) { 127 | pcntl_alarm(1); 128 | self::tick(); 129 | } 130 | } 131 | 132 | /** 133 | * Add a timer. 134 | * 135 | * @param float $timeInterval 136 | * @param callable $func 137 | * @param null|array $args 138 | * @param bool $persistent 139 | * @return int 140 | */ 141 | public static function add(float $timeInterval, callable $func, ?array $args = [], bool $persistent = true): int 142 | { 143 | if ($timeInterval < 0) { 144 | throw new RuntimeException('$timeInterval can not less than 0'); 145 | } 146 | 147 | if ($args === null) { 148 | $args = []; 149 | } 150 | 151 | if (self::$event) { 152 | return $persistent ? self::$event->repeat($timeInterval, $func, $args) : self::$event->delay($timeInterval, $func, $args); 153 | } 154 | 155 | // If not workerman runtime just return. 156 | if (!Worker::getAllWorkers()) { 157 | throw new RuntimeException('Timer can only be used in workerman running environment'); 158 | } 159 | 160 | if (empty(self::$tasks)) { 161 | pcntl_alarm(1); 162 | } 163 | 164 | $runTime = time() + $timeInterval; 165 | if (!isset(self::$tasks[$runTime])) { 166 | self::$tasks[$runTime] = []; 167 | } 168 | 169 | self::$timerId = self::$timerId == PHP_INT_MAX ? 1 : ++self::$timerId; 170 | self::$status[self::$timerId] = true; 171 | self::$tasks[$runTime][self::$timerId] = [$func, (array)$args, $persistent, $timeInterval]; 172 | 173 | return self::$timerId; 174 | } 175 | 176 | /** 177 | * Coroutine sleep. 178 | * 179 | * @param float $delay 180 | * @return void 181 | */ 182 | public static function sleep(float $delay): void 183 | { 184 | switch (Worker::$eventLoopClass) { 185 | // Fiber 186 | case Fiber::class: 187 | $suspension = EventLoop::getSuspension(); 188 | static::add($delay, function () use ($suspension) { 189 | $suspension->resume(); 190 | }, null, false); 191 | $suspension->suspend(); 192 | return; 193 | // Swoole 194 | case Swoole::class: 195 | System::sleep($delay); 196 | return; 197 | } 198 | usleep((int)($delay * 1000 * 1000)); 199 | } 200 | 201 | /** 202 | * Tick. 203 | * 204 | * @return void 205 | */ 206 | protected static function tick(): void 207 | { 208 | if (empty(self::$tasks)) { 209 | pcntl_alarm(0); 210 | return; 211 | } 212 | $timeNow = time(); 213 | foreach (self::$tasks as $runTime => $taskData) { 214 | if ($timeNow >= $runTime) { 215 | foreach ($taskData as $index => $oneTask) { 216 | $taskFunc = $oneTask[0]; 217 | $taskArgs = $oneTask[1]; 218 | $persistent = $oneTask[2]; 219 | $timeInterval = $oneTask[3]; 220 | try { 221 | $taskFunc(...$taskArgs); 222 | } catch (Throwable $e) { 223 | Worker::safeEcho((string)$e); 224 | } 225 | if ($persistent && !empty(self::$status[$index])) { 226 | $newRunTime = time() + $timeInterval; 227 | if (!isset(self::$tasks[$newRunTime])) { 228 | self::$tasks[$newRunTime] = []; 229 | } 230 | self::$tasks[$newRunTime][$index] = [$taskFunc, (array)$taskArgs, $persistent, $timeInterval]; 231 | } 232 | } 233 | unset(self::$tasks[$runTime]); 234 | } 235 | } 236 | } 237 | 238 | /** 239 | * Remove a timer. 240 | * 241 | * @param int $timerId 242 | * @return bool 243 | */ 244 | public static function del(int $timerId): bool 245 | { 246 | if (self::$event) { 247 | return self::$event->offDelay($timerId); 248 | } 249 | foreach (self::$tasks as $runTime => $taskData) { 250 | if (array_key_exists($timerId, $taskData)) { 251 | unset(self::$tasks[$runTime][$timerId]); 252 | } 253 | } 254 | if (array_key_exists($timerId, self::$status)) { 255 | unset(self::$status[$timerId]); 256 | } 257 | return true; 258 | } 259 | 260 | /** 261 | * Remove all timers. 262 | * 263 | * @return void 264 | */ 265 | public static function delAll(): void 266 | { 267 | self::$tasks = self::$status = []; 268 | if (function_exists('pcntl_alarm')) { 269 | pcntl_alarm(0); 270 | } 271 | self::$event?->deleteAllTimer(); 272 | } 273 | } 274 | --------------------------------------------------------------------------------