Browse Source

Merge branch 'walkor:master' into master

1923998238 2 năm trước cách đây
mục cha
commit
7bde2d6395

+ 3 - 0
.gitattributes

@@ -0,0 +1,3 @@
+/.gitattributes export-ignore
+/.github export-ignore
+/.gitignore export-ignore

+ 3 - 0
README.md

@@ -375,10 +375,13 @@ proxy supports TLS1.3, no Sniproxy channel
 ```php start.php start  ```  
 ```php start.php start -d  ```  
 ```php start.php status  ```  
+```php start.php status -d  ```  
 ```php start.php connections```  
 ```php start.php stop  ```  
+```php start.php stop -g  ```  
 ```php start.php restart  ```  
 ```php start.php reload  ```  
+```php start.php reload -g  ```
 
 ## Documentation
 

+ 6 - 0
SECURITY.md

@@ -0,0 +1,6 @@
+# Security Policy
+
+
+## Reporting a Vulnerability
+
+Please contact by email walkor@workerman.net

+ 5 - 5
src/Connection/AsyncTcpConnection.php

@@ -105,9 +105,9 @@ class AsyncTcpConnection extends TcpConnection
     /**
      * PHP built-in protocols.
      *
-     * @var array
+     * @var array<string,string>
      */
-    protected static $_builtinTransports = [
+    const BUILD_IN_TRANSPORTS = [
         'tcp' => 'tcp',
         'udp' => 'udp',
         'unix' => 'unix',
@@ -150,7 +150,7 @@ class AsyncTcpConnection extends TcpConnection
             $this->_remoteHost = $address_info['host'];
             $this->_remotePort = $address_info['port'];
             $this->_remoteURI = "{$address_info['path']}{$address_info['query']}";
-            $scheme = isset($address_info['scheme']) ? $address_info['scheme'] : 'tcp';
+            $scheme = $address_info['scheme'] ?? 'tcp';
             $this->_remoteAddress = 'unix' === strtolower($scheme)
                 ? substr($remote_address, strpos($remote_address, '/') + 2)
                 : $this->_remoteHost . ':' . $this->_remotePort;
@@ -161,7 +161,7 @@ class AsyncTcpConnection extends TcpConnection
             self::$_idRecorder = 0;
         }
         // Check application layer protocol class.
-        if (!isset(self::$_builtinTransports[$scheme])) {
+        if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) {
             $scheme = \ucfirst($scheme);
             $this->protocol = '\\Protocols\\' . $scheme;
             if (!\class_exists($this->protocol)) {
@@ -171,7 +171,7 @@ class AsyncTcpConnection extends TcpConnection
                 }
             }
         } else {
-            $this->transport = self::$_builtinTransports[$scheme];
+            $this->transport = self::BUILD_IN_TRANSPORTS[$scheme];
         }
 
         // For statistics.

+ 1 - 1
src/Connection/AsyncUdpConnection.php

@@ -181,7 +181,7 @@ class AsyncUdpConnection extends UdpConnection
         \stream_set_blocking($this->_socket, false);
 
         if ($this->onMessage) {
-            Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, [$this, 'baseRead']);
+            Worker::$globalEvent->onWritable($this->_socket, [$this, 'baseRead']);
         }
         $this->connected = true;
         // Try to emit onConnect callback.

+ 1 - 0
src/Connection/ConnectionInterface.php

@@ -17,6 +17,7 @@ namespace Workerman\Connection;
 /**
  * ConnectionInterface.
  */
+#[\AllowDynamicProperties]
 abstract class ConnectionInterface
 {
     /**

+ 36 - 2
src/Connection/TcpConnection.php

@@ -21,7 +21,7 @@ use Workerman\Worker;
 /**
  * TcpConnection.
  */
-class TcpConnection extends ConnectionInterface
+class TcpConnection extends ConnectionInterface implements \JsonSerializable
 {
     /**
      * Read buffer size.
@@ -157,6 +157,13 @@ class TcpConnection extends ConnectionInterface
      * @var int
      */
     public $maxSendBufferSize = 1048576;
+    
+    /**
+     * Context.
+     *
+     * @var object|null
+     */
+    public $context = null;
 
     /**
      * Default send buffer size.
@@ -293,6 +300,7 @@ class TcpConnection extends ConnectionInterface
         $this->maxPackageSize = self::$defaultMaxPackageSize;
         $this->_remoteAddress = $remote_address;
         static::$connections[$this->id] = $this;
+        $this->context = new \stdClass;
     }
 
     /**
@@ -587,7 +595,7 @@ class TcpConnection extends ConnectionInterface
         } else {
             $this->bytesRead += \strlen($buffer);
             if ($this->_recvBuffer === '') {
-                if (static::$_enableCache && !isset($requests[512]) && isset($requests[$buffer])) {
+                if (static::$_enableCache && !isset($buffer[512]) && isset($requests[$buffer])) {
                     ++self::$statistics['total_request'];
                     $request = $requests[$buffer];
                     if ($request instanceof Request) {
@@ -715,6 +723,9 @@ class TcpConnection extends ConnectionInterface
                 }
             }
             if ($this->_status === self::STATUS_CLOSING) {
+                if ($this->context->streamSending) {
+                    return true;
+                }
                 $this->destroy();
             }
             return true;
@@ -969,6 +980,29 @@ class TcpConnection extends ConnectionInterface
     {
         static::$_enableCache = (bool)$value;
     }
+    
+    /**
+     * Get the json_encode information.
+     *
+     * @return mixed
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {        
+        return [
+            'id' => $this->id,
+            'status' => $this->getStatus(),
+            'transport' => $this->transport,
+            'getRemoteIp' => $this->getRemoteIp(),
+            'remotePort' => $this->getRemotePort(),
+            'getRemoteAddress' => $this->getRemoteAddress(),
+            'getLocalIp' => $this->getLocalIp(),
+            'getLocalPort' => $this->getLocalPort(),
+            'getLocalAddress' => $this->getLocalAddress(),
+            'isIpV4' => $this->isIpV4(),
+            'isIpV6' => $this->isIpV6(),
+        ];
+    }
 
     /**
      * Destruct.

+ 21 - 1
src/Connection/UdpConnection.php

@@ -17,7 +17,7 @@ namespace Workerman\Connection;
 /**
  * UdpConnection.
  */
-class UdpConnection extends ConnectionInterface
+class UdpConnection extends ConnectionInterface implements \JsonSerializable
 {
     /**
      * Application layer protocol.
@@ -206,4 +206,24 @@ class UdpConnection extends ConnectionInterface
     {
         return $this->_socket;
     }
+    
+    /**
+     * Get the json_encode informattion.
+     *
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return [
+            'transport' => $this->transport,
+            'getRemoteIp' => $this->getRemoteIp(),
+            'remotePort' => $this->getRemotePort(),
+            'getRemoteAddress' => $this->getRemoteAddress(),
+            'getLocalIp' => $this->getLocalIp(),
+            'getLocalPort' => $this->getLocalPort(),
+            'getLocalAddress' => $this->getLocalAddress(),
+            'isIpV4' => $this->isIpV4(),
+            'isIpV6' => $this->isIpV6(),
+        ];
+    }
 }

+ 11 - 8
src/Events/Event.php

@@ -56,7 +56,7 @@ class Event implements EventInterface
      * Timer id.
      * @var int
      */
-    protected $_timerId = 1;
+    protected $_timerId = 0;
 
     /**
      * Event class name.
@@ -90,8 +90,10 @@ class Event implements EventInterface
     public function delay(float $delay, $func, $args)
     {
         $class_name = $this->_eventClassName;
-        $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT, function () use ($func, $args) {
+        $timer_id = $this->_timerId++;
+        $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT, function () use ($func, $args, $timer_id) {
             try {
+                $this->deleteTimer($timer_id);
                 $func(...$args);
             } catch (\Throwable $e) {
                 Worker::stopAll(250, $e);
@@ -100,8 +102,8 @@ class Event implements EventInterface
         if (!$event || !$event->addTimer($delay)) {
             return false;
         }
-        $this->_eventTimer[$this->_timerId] = $event;
-        return $this->_timerId++;
+        $this->_eventTimer[$timer_id] = $event;
+        return $timer_id;
     }
 
     /**
@@ -123,7 +125,8 @@ class Event implements EventInterface
     public function repeat(float $interval, $func, $args)
     {
         $class_name = $this->_eventClassName;
-        $event = new $this->_eventClassName($this->_eventBase, -1, $class_name::TIMEOUT | $class_name::PERSIST, function () use ($func, $args) {
+        $timer_id = $this->_timerId++;
+        $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT | $class_name::PERSIST, function () use ($func, $args) {
             try {
                 $func(...$args);
             } catch (\Throwable $e) {
@@ -133,8 +136,8 @@ class Event implements EventInterface
         if (!$event || !$event->addTimer($interval)) {
             return false;
         }
-        $this->_eventTimer[$this->_timerId] = $event;
-        return $this->_timerId++;
+        $this->_eventTimer[$timer_id] = $event;
+        return $timer_id;
     }
 
     /**
@@ -175,7 +178,7 @@ class Event implements EventInterface
         if (!$event || !$event->add()) {
             return false;
         }
-        $this->_readEvents[$fd_key] = $event;
+        $this->_writeEvents[$fd_key] = $event;
         return true;
     }
 

+ 24 - 6
src/Events/Select.php

@@ -14,6 +14,9 @@
 
 namespace Workerman\Events;
 
+use Throwable;
+use Workerman\Worker;
+
 /**
  * select eventloop
  */
@@ -133,6 +136,7 @@ class Select implements EventInterface
         $this->_scheduler->insert($timer_id, -$run_time);
         $this->_eventTimer[$timer_id] = [$func, (array)$args, $delay];
         $select_timeout = ($run_time - \microtime(true)) * 1000000;
+        $select_timeout = $select_timeout <= 0 ? 1 : (int)$select_timeout;
         if ($this->_selectTimeout > $select_timeout) {
             $this->_selectTimeout = $select_timeout;
         }
@@ -258,12 +262,13 @@ class Select implements EventInterface
      */
     protected function tick()
     {
+        $tasks_to_insert = [];
         while (!$this->_scheduler->isEmpty()) {
             $scheduler_data = $this->_scheduler->top();
             $timer_id = $scheduler_data['data'];
             $next_run_time = -$scheduler_data['priority'];
             $time_now = \microtime(true);
-            $this->_selectTimeout = (int)($next_run_time - $time_now) * 1000000;
+            $this->_selectTimeout = (int)(($next_run_time - $time_now) * 1000000);
             if ($this->_selectTimeout <= 0) {
                 $this->_scheduler->extract();
 
@@ -275,13 +280,27 @@ class Select implements EventInterface
                 $task_data = $this->_eventTimer[$timer_id];
                 if (isset($task_data[2])) {
                     $next_run_time = $time_now + $task_data[2];
-                    $this->_scheduler->insert($timer_id, -$next_run_time);
+                    $tasks_to_insert[] = [$timer_id, -$next_run_time];
                 } else {
                     unset($this->_eventTimer[$timer_id]);
                 }
-                $task_data[0]($task_data[1]);
-                continue;
+                try {
+                    $task_data[0]($task_data[1]);
+                } catch (Throwable $e) {
+                    Worker::stopAll(250, $e);
+                }
+            } else {
+                break;
             }
+        }
+        foreach ($tasks_to_insert as $item) {
+            $this->_scheduler->insert($item[0], $item[1]);
+        }
+        if (!$this->_scheduler->isEmpty()) {
+            $scheduler_data = $this->_scheduler->top();
+            $next_run_time = -$scheduler_data['priority'];
+            $time_now = \microtime(true);
+            $this->_selectTimeout = \max((int)(($next_run_time - $time_now) * 1000000), 0);
             return;
         }
         $this->_selectTimeout = 100000000;
@@ -316,12 +335,11 @@ class Select implements EventInterface
                 // Waiting read/write/signal/timeout events.
                 try {
                     @stream_select($read, $write, $except, 0, $this->_selectTimeout);
-                } catch (\Throwable $e) {
+                } catch (Throwable $e) {
                 }
 
             } else {
                 $this->_selectTimeout >= 1 && usleep($this->_selectTimeout);
-                $ret = false;
             }
 
             if (!$this->_scheduler->isEmpty()) {

+ 320 - 0
src/Events/Swow.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace Workerman\Events;
+
+use RuntimeException;
+use Swow\Coroutine;
+use Swow\Signal;
+use Swow\SignalException;
+use Workerman\Worker;
+use function getmypid;
+use function max;
+use function msleep;
+use function stream_poll_one;
+use function Swow\Sync\waitAll;
+use const STREAM_POLLHUP;
+use const STREAM_POLLIN;
+use const STREAM_POLLNONE;
+use const STREAM_POLLOUT;
+
+class Swow implements EventInterface
+{
+    /**
+     * All listeners for read timer
+     * @var array
+     */
+    protected $_eventTimer = [];
+
+    /**
+     * All listeners for read event.
+     * @var array<Coroutine>
+     */
+    protected $_readEvents = [];
+
+    /**
+     * All listeners for write event.
+     * @var array<Coroutine>
+     */
+    protected $_writeEvents = [];
+
+    /**
+     * All listeners for signal.
+     * @var array<Coroutine>
+     */
+    protected $_signalListener = [];
+
+    /**
+     * Get timer count.
+     *
+     * @return integer
+     */
+    public function getTimerCount()
+    {
+        return \count($this->_eventTimer);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function delay(float $delay, $func, $args)
+    {
+        $t = (int) ($delay * 1000);
+        $t = max($t, 1);
+        $coroutine = Coroutine::run(function () use ($t, $func, $args): void {
+            msleep($t);
+            unset($this->_eventTimer[Coroutine::getCurrent()->getId()]);
+            try {
+                $func(...(array) $args);
+            } catch (\Throwable $e) {
+                Worker::stopAll(250, $e);
+            }
+        });
+        $timer_id = $coroutine->getId();
+        $this->_eventTimer[$timer_id] = $timer_id;
+        return $timer_id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function repeat(float $interval, $func, $args)
+    {
+        $t = (int) ($interval * 1000);
+        $t = max($t, 1);
+        $coroutine = Coroutine::run(static function () use ($t, $func, $args): void {
+            while (true) {
+                msleep($t);
+                try {
+                    $func(...(array) $args);
+                } catch (\Throwable $e) {
+                    Worker::stopAll(250, $e);
+                }
+            }
+        });
+        $timer_id = $coroutine->getId();
+        $this->_eventTimer[$timer_id] = $timer_id;
+        return $timer_id;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function deleteTimer($timer_id)
+    {
+        if (isset($this->_eventTimer[$timer_id])) {
+            try {
+                (Coroutine::getAll()[$timer_id])->kill();
+                return true;
+            } finally {
+                unset($this->_eventTimer[$timer_id]);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function deleteAllTimer()
+    {
+        foreach ($this->_eventTimer as $timer_id) {
+            $this->deleteTimer($timer_id);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function onReadable($stream, $func)
+    {
+        if (isset($this->_readEvents[(int) $stream])) {
+            $this->offReadable($stream);
+        }
+        $this->_readEvents[(int) $stream] = Coroutine::run(function () use ($stream, $func): void {
+            try {
+                while (true) {
+                    $rEvent = stream_poll_one($stream, STREAM_POLLIN | STREAM_POLLHUP);
+                    if ($rEvent !== STREAM_POLLNONE) {
+                        $func($stream);
+                    }
+                    if ($rEvent !== STREAM_POLLIN) {
+                        $this->offReadable($stream, bySelf: true);
+                        break;
+                    }
+                }
+            } catch (RuntimeException) {
+                $this->offReadable($stream, bySelf: true);
+            }
+        });
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function offReadable($stream, bool $bySelf = false)
+    {
+        $fd = (int) $stream;
+        if (!isset($this->_readEvents[$fd])) {
+            return;
+        }
+        if (!$bySelf) {
+            $coroutine = $this->_readEvents[$fd];
+            if (!$coroutine->isExecuting()) {
+                return;
+            }
+            $coroutine->kill();
+        }
+        unset($this->_readEvents[$fd]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function onWritable($stream, $func)
+    {
+        if (isset($this->_writeEvents[(int) $stream])) {
+            $this->offWritable($stream);
+        }
+        $this->_writeEvents[(int) $stream] = Coroutine::run(function () use ($stream, $func): void {
+            try {
+                while (true) {
+                    $rEvent = stream_poll_one($stream, STREAM_POLLOUT | STREAM_POLLHUP);
+                    if ($rEvent !== STREAM_POLLNONE) {
+                        $func($stream);
+                    }
+                    if ($rEvent !== STREAM_POLLOUT) {
+                        $this->offWritable($stream, bySelf: true);
+                        break;
+                    }
+                }
+            } catch (RuntimeException) {
+                $this->offWritable($stream, bySelf: true);
+            }
+        });
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function offWritable($stream, bool $bySelf = false)
+    {
+        $fd = (int) $stream;
+        if (!isset($this->_writeEvents[$fd])) {
+            return;
+        }
+        if (!$bySelf) {
+            $coroutine = $this->_writeEvents[$fd];
+            if (!$coroutine->isExecuting()) {
+                return;
+            }
+            $coroutine->kill();
+        }
+        unset($this->_writeEvents[$fd]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function onSignal($signal, $func)
+    {
+        if (isset($this->_signalListener[$signal])) {
+            return false;
+        }
+        $coroutine = Coroutine::run(static function () use ($signal, $func): void {
+            try {
+                Signal::wait($signal);
+                $func($signal);
+            } catch (SignalException) {
+            }
+        });
+        $this->_signalListener[$signal] = $coroutine;
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function offSignal($signal)
+    {
+        if (!isset($this->_signalListener[$signal])) {
+            return false;
+        }
+        $this->_signalListener[$signal]->kill();
+        unset($this->_signalListener[$signal]);
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function run()
+    {
+        waitAll();
+    }
+
+    /**
+     * Destroy loop.
+     *
+     * @return void
+     */
+    public function stop()
+    {
+        Coroutine::getMain()->kill();
+        Signal::kill(getmypid(), Signal::INT);
+    }
+
+    public function destroy()
+    {
+        $this->stop();
+    }
+
+    public function add($fd, $flag, $func, $args = [])
+    {
+        switch ($flag) {
+            case self::EV_SIGNAL:
+                return $this->onSignal($fd, $func);
+            case self::EV_TIMER:
+            case self::EV_TIMER_ONCE:
+                $method = self::EV_TIMER === $flag ? 'tick' : 'after';
+                if ($method === 'tick') {
+                    return $this->repeat($fd, $func, $args);
+                } else {
+                    return $this->delay($fd, $func, $args);
+                }
+            case self::EV_READ:
+                return $this->onReadable($fd, $func);
+            case self::EV_WRITE:
+                return $this->onWritable($fd, $func);
+        }
+    }
+
+    public function del($fd, $flag)
+    {
+        switch ($flag) {
+            case self::EV_SIGNAL:
+                return $this->offSignal($fd);
+            case self::EV_TIMER:
+            case self::EV_TIMER_ONCE:
+                return $this->deleteTimer($fd);
+            case self::EV_READ:
+            case self::EV_WRITE:
+                if ($flag === self::EV_READ) {
+                    $this->offReadable($fd);
+                } else {
+                    $this->offWritable($fd);
+                }
+        }
+    }
+
+    public function clearAllTimer()
+    {
+        $this->deleteAllTimer();
+    }
+
+    public function loop()
+    {
+        waitAll();
+    }
+}

+ 36 - 49
src/Protocols/Http.php

@@ -32,13 +32,6 @@ class Http
     protected static $_requestClass = Request::class;
 
     /**
-     * Session name.
-     *
-     * @var string
-     */
-    protected static $_sessionName = 'PHPSID';
-
-    /**
      * Upload tmp dir.
      *
      * @var string
@@ -53,20 +46,6 @@ class Http
     protected static $_enableCache = true;
 
     /**
-     * Get or set session name.
-     *
-     * @param string|null $name
-     * @return string
-     */
-    public static function sessionName($name = null)
-    {
-        if ($name !== null && $name !== '') {
-            static::$_sessionName = (string)$name;
-        }
-        return static::$_sessionName;
-    }
-
-    /**
      * Get or set the request class name.
      *
      * @param string|null $class_name
@@ -97,7 +76,7 @@ class Http
      * @param TcpConnection $connection
      * @return int
      */
-    public static function input($recv_buffer, TcpConnection $connection)
+    public static function input(string $recv_buffer, TcpConnection $connection)
     {
         static $input = [];
         if (!isset($recv_buffer[512]) && isset($input[$recv_buffer])) {
@@ -106,53 +85,58 @@ class Http
         $crlf_pos = \strpos($recv_buffer, "\r\n\r\n");
         if (false === $crlf_pos) {
             // Judge whether the package length exceeds the limit.
-            if ($recv_len = \strlen($recv_buffer) >= 16384) {
-                $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n");
+            if (\strlen($recv_buffer) >= 16384) {
+                $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
                 return 0;
             }
             return 0;
         }
 
-        $head_len = $crlf_pos + 4;
-        $method = \strstr($recv_buffer, ' ', true);
+        $length = $crlf_pos + 4;
+        $firstLine = \explode(" ", \strstr($recv_buffer, "\r\n", true), 3);
 
-        if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD' || $method === 'DELETE') {
-            if (!isset($recv_buffer[512])) {
-                $input[$recv_buffer] = $head_len;
-                if (\count($input) > 512) {
-                    unset($input[\key($input)]);
-                }
-            }
-            return $head_len;
-        } else if ($method !== 'POST' && $method !== 'PUT' && $method !== 'PATCH') {
+        if (!\in_array($firstLine[0], ['GET', 'POST', 'OPTIONS', 'HEAD', 'DELETE', 'PUT', 'PATCH'])) {
             $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
             return 0;
         }
 
         $header = \substr($recv_buffer, 0, $crlf_pos);
-        $length = false;
+        $hostHeaderPosition = \strpos($header, "\r\nHost: ");
+
+        if (false === $hostHeaderPosition && $firstLine[2] === "HTTP/1.1") {
+            $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
+            return 0;
+        }
+
         if ($pos = \strpos($header, "\r\nContent-Length: ")) {
-            $length = $head_len + (int)\substr($header, $pos + 18, 10);
+            $length = $length + (int)\substr($header, $pos + 18, 10);
+            $has_content_length = true;
         } else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) {
-            $length = $head_len + $match[1];
+            $length = $length + $match[1];
+            $has_content_length = true;
+        } else {
+            $has_content_length = false;
+            if (false !== stripos($header, "\r\nTransfer-Encoding:")) {
+                $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
+                return 0;
+            }
         }
 
-        if ($length !== false) {
-            if (!isset($recv_buffer[512])) {
-                $input[$recv_buffer] = $length;
-                if (\count($input) > 512) {
-                    unset($input[\key($input)]);
-                }
-            }
+        if ($has_content_length) {
             if ($length > $connection->maxPackageSize) {
-                $connection->close("HTTP/1.1 413 Request Entity Too Large\r\n\r\n");
+                $connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
                 return 0;
             }
-            return $length;
         }
 
-        $connection->close("HTTP/1.1 400 Bad Request\r\n\r\n", true);
-        return 0;
+        if (!isset($recv_buffer[512])) {
+            $input[$recv_buffer] = $length;
+            if (\count($input) > 512) {
+                unset($input[key($input)]);
+            }
+        }
+
+        return $length;
     }
 
     /**
@@ -226,6 +210,7 @@ class Http
             $file = $response->file['file'];
             $offset = $response->file['offset'];
             $length = $response->file['length'];
+            \clearstatcache();
             $file_size = (int)\filesize($file);
             $body_len = $length > 0 ? $length : $file_size - $offset;
             $response->withHeaders([
@@ -264,6 +249,7 @@ class Http
     protected static function sendStream(TcpConnection $connection, $handler, $offset = 0, $length = 0)
     {
         $connection->bufferFull = false;
+        $connection->context->streamSending = true;
         if ($offset !== 0) {
             \fseek($handler, $offset);
         }
@@ -290,6 +276,7 @@ class Http
                 if ($buffer === '' || $buffer === false) {
                     fclose($handler);
                     $connection->onBufferDrain = null;
+                    $connection->context->streamSending = false;
                     return;
                 }
                 $connection->send($buffer, true);

+ 135 - 136
src/Protocols/Http/Request.php

@@ -47,6 +47,11 @@ class Request
     public $properties = [];
 
     /**
+     * @var int
+     */
+    public static $maxFileUploads = 1024;
+
+    /**
      * Http buffer.
      *
      * @var string
@@ -61,27 +66,6 @@ class Request
     protected $_data = null;
 
     /**
-     * Header cache.
-     *
-     * @var array
-     */
-    protected static $_headerCache = [];
-
-    /**
-     * Get cache.
-     *
-     * @var array
-     */
-    protected static $_getCache = [];
-
-    /**
-     * Post cache.
-     *
-     * @var array
-     */
-    protected static $_postCache = [];
-
-    /**
      * Enable cache.
      *
      * @var bool
@@ -114,7 +98,7 @@ class Request
         if (null === $name) {
             return $this->_data['get'];
         }
-        return isset($this->_data['get'][$name]) ? $this->_data['get'][$name] : $default;
+        return $this->_data['get'][$name] ?? $default;
     }
 
     /**
@@ -132,7 +116,7 @@ class Request
         if (null === $name) {
             return $this->_data['post'];
         }
-        return isset($this->_data['post'][$name]) ? $this->_data['post'][$name] : $default;
+        return $this->_data['post'][$name] ?? $default;
     }
 
     /**
@@ -151,7 +135,7 @@ class Request
             return $this->_data['headers'];
         }
         $name = \strtolower($name);
-        return isset($this->_data['headers'][$name]) ? $this->_data['headers'][$name] : $default;
+        return $this->_data['headers'][$name] ?? $default;
     }
 
     /**
@@ -170,7 +154,7 @@ class Request
         if ($name === null) {
             return $this->_data['cookie'];
         }
-        return isset($this->_data['cookie'][$name]) ? $this->_data['cookie'][$name] : $default;
+        return $this->_data['cookie'][$name] ?? $default;
     }
 
     /**
@@ -187,7 +171,7 @@ class Request
         if (null === $name) {
             return $this->_data['files'];
         }
-        return isset($this->_data['files'][$name]) ? $this->_data['files'][$name] : null;
+        return $this->_data['files'][$name] ?? null;
     }
 
     /**
@@ -299,7 +283,7 @@ class Request
             unset($this->sid);
         }
         if (!isset($this->sid)) {
-            $session_name = Http::sessionName();
+            $session_name = Session::$name;
             $sid = $session_id ? '' : $this->cookie($session_name);
             if ($sid === '' || $sid === null) {
                 if ($this->connection === null) {
@@ -307,7 +291,7 @@ class Request
                     return false;
                 }
                 $sid = $session_id ?: static::createSessionId();
-                $cookie_params = \session_get_cookie_params();
+                $cookie_params = Session::getCookieParams();
                 $this->setSidCookie($session_name, $sid, $cookie_params);
             }
             $this->sid = $sid;
@@ -330,8 +314,8 @@ class Request
         $new_sid = static::createSessionId();
         $session = new Session($new_sid);
         $session->put($session_data);
-        $cookie_params = \session_get_cookie_params();
-        $session_name = Http::sessionName();
+        $cookie_params = Session::getCookieParams();
+        $session_name = Session::$name;
         $this->setSidCookie($session_name, $new_sid, $cookie_params);
     }
 
@@ -388,7 +372,7 @@ class Request
         $first_line = \strstr($this->_buffer, "\r\n", true);
         $tmp = \explode(' ', $first_line, 3);
         $this->_data['method'] = $tmp[0];
-        $this->_data['uri'] = isset($tmp[1]) ? $tmp[1] : '/';
+        $this->_data['uri'] = $tmp[1] ?? '/';
     }
 
     /**
@@ -410,6 +394,7 @@ class Request
      */
     protected function parseHeaders()
     {
+        static $cache = [];
         $this->_data['headers'] = [];
         $raw_head = $this->rawHead();
         $end_line_position = \strpos($raw_head, "\r\n");
@@ -418,8 +403,8 @@ class Request
         }
         $head_buffer = \substr($raw_head, $end_line_position + 2);
         $cacheable = static::$_enableCache && !isset($head_buffer[2048]);
-        if ($cacheable && isset(static::$_headerCache[$head_buffer])) {
-            $this->_data['headers'] = static::$_headerCache[$head_buffer];
+        if ($cacheable && isset($cache[$head_buffer])) {
+            $this->_data['headers'] = $cache[$head_buffer];
             return;
         }
         $head_data = \explode("\r\n", $head_buffer);
@@ -439,9 +424,9 @@ class Request
             }
         }
         if ($cacheable) {
-            static::$_headerCache[$head_buffer] = $this->_data['headers'];
-            if (\count(static::$_headerCache) > 128) {
-                unset(static::$_headerCache[key(static::$_headerCache)]);
+            $cache[$head_buffer] = $this->_data['headers'];
+            if (\count($cache) > 128) {
+                unset($cache[key($cache)]);
             }
         }
     }
@@ -453,21 +438,22 @@ class Request
      */
     protected function parseGet()
     {
+        static $cache = [];
         $query_string = $this->queryString();
         $this->_data['get'] = [];
         if ($query_string === '') {
             return;
         }
         $cacheable = static::$_enableCache && !isset($query_string[1024]);
-        if ($cacheable && isset(static::$_getCache[$query_string])) {
-            $this->_data['get'] = static::$_getCache[$query_string];
+        if ($cacheable && isset($cache[$query_string])) {
+            $this->_data['get'] = $cache[$query_string];
             return;
         }
         \parse_str($query_string, $this->_data['get']);
         if ($cacheable) {
-            static::$_getCache[$query_string] = $this->_data['get'];
-            if (\count(static::$_getCache) > 256) {
-                unset(static::$_getCache[key(static::$_getCache)]);
+            $cache[$query_string] = $this->_data['get'];
+            if (\count($cache) > 256) {
+                unset($cache[key($cache)]);
             }
         }
     }
@@ -479,31 +465,32 @@ class Request
      */
     protected function parsePost()
     {
-        $body_buffer = $this->rawBody();
+        static $cache = [];
         $this->_data['post'] = $this->_data['files'] = [];
-        if ($body_buffer === '') {
-            return;
-        }
-        $cacheable = static::$_enableCache && !isset($body_buffer[1024]);
-        if ($cacheable && isset(static::$_postCache[$body_buffer])) {
-            $this->_data['post'] = static::$_postCache[$body_buffer];
-            return;
-        }
         $content_type = $this->header('content-type', '');
         if (\preg_match('/boundary="?(\S+)"?/', $content_type, $match)) {
             $http_post_boundary = '--' . $match[1];
             $this->parseUploadFiles($http_post_boundary);
             return;
         }
+        $body_buffer = $this->rawBody();
+        if ($body_buffer === '') {
+            return;
+        }
+        $cacheable = static::$_enableCache && !isset($body_buffer[1024]);
+        if ($cacheable && isset($cache[$body_buffer])) {
+            $this->_data['post'] = $cache[$body_buffer];
+            return;
+        }
         if (\preg_match('/\bjson\b/i', $content_type)) {
             $this->_data['post'] = (array)\json_decode($body_buffer, true);
         } else {
             \parse_str($body_buffer, $this->_data['post']);
         }
         if ($cacheable) {
-            static::$_postCache[$body_buffer] = $this->_data['post'];
-            if (\count(static::$_postCache) > 256) {
-                unset(static::$_postCache[key(static::$_postCache)]);
+            $cache[$body_buffer] = $this->_data['post'];
+            if (\count($cache) > 256) {
+                unset($cache[key($cache)]);
             }
         }
     }
@@ -517,95 +504,107 @@ class Request
     protected function parseUploadFiles($http_post_boundary)
     {
         $http_post_boundary = \trim($http_post_boundary, '"');
-        $http_body = $this->rawBody();
-        $http_body = \substr($http_body, 0, \strlen($http_body) - (\strlen($http_post_boundary) + 4));
-        $boundary_data_array = \explode($http_post_boundary . "\r\n", $http_body);
-        if ($boundary_data_array[0] === '' || $boundary_data_array[0] === "\r\n") {
-            unset($boundary_data_array[0]);
-        }
-        $index = -1;
+        $buffer = $this->_buffer;
+        $post_encode_string = '';
+        $files_encode_string = '';
         $files = [];
-        $post_str = '';
-        foreach ($boundary_data_array as $boundary_data_buffer) {
-            list($boundary_header_buffer, $boundary_value) = \explode("\r\n\r\n", $boundary_data_buffer, 2);
-            // Remove \r\n from the end of buffer.
-            $boundary_value = \substr($boundary_value, 0, -2);
-            $index++;
-            foreach (\explode("\r\n", $boundary_header_buffer) as $item) {
-                list($header_key, $header_value) = \explode(": ", $item);
-                $header_key = \strtolower($header_key);
-                switch ($header_key) {
-                    case "content-disposition":
-                        // Is file data.
-                        if (\preg_match('/name="(.*?)"; filename="(.*?)"/i', $header_value, $match)) {
-                            $error = 0;
-                            $tmp_file = '';
-                            $size = \strlen($boundary_value);
-                            $tmp_upload_dir = HTTP::uploadTmpDir();
-                            if (!$tmp_upload_dir) {
-                                $error = UPLOAD_ERR_NO_TMP_DIR;
-                            } else if ($boundary_value === '') {
-                                $error = UPLOAD_ERR_NO_FILE;
-                            } else {
-                                $tmp_file = \tempnam($tmp_upload_dir, 'workerman.upload.');
-                                if ($tmp_file === false || false == \file_put_contents($tmp_file, $boundary_value)) {
-                                    $error = UPLOAD_ERR_CANT_WRITE;
-                                }
-                            }
-                            if (!isset($files[$index])) {
-                                $files[$index] = [];
-                            }
-                            // Parse upload files.
-                            $files[$index] += [
-                                'key' => $match[1],
-                                'name' => $match[2],
-                                'tmp_name' => $tmp_file,
-                                'size' => $size,
-                                'error' => $error,
-                                'type' => null,
-                            ];
-                            break;
-                        } // Is post field.
-                        else {
-                            // Parse $_POST.
-                            if (\preg_match('/name="(.*?)"$/', $header_value, $match)) {
-                                $k = $match[1];
-                                $post_str .= \urlencode($k) . "=" . \urlencode($boundary_value) . '&';
+        $boday_position = strpos($buffer, "\r\n\r\n") + 4;
+        $offset = $boday_position + strlen($http_post_boundary) + 2;
+        $max_count = static::$maxFileUploads;
+        while ($max_count-- > 0 && $offset) {
+            $offset = $this->parseUploadFile($http_post_boundary, $offset, $post_encode_string, $files_encode_string, $files);
+        }
+        if ($post_encode_string) {
+            parse_str($post_encode_string, $this->_data['post']);
+        }
+
+        if ($files_encode_string) {
+            parse_str($files_encode_string, $this->_data['files']);
+            \array_walk_recursive($this->_data['files'], function (&$value) use ($files) {
+                $value = $files[$value];
+            });
+        }
+    }
+
+    /**
+     * @param $boundary
+     * @param $section_start_offset
+     * @return int
+     */
+    protected function parseUploadFile($boundary, $section_start_offset, &$post_encode_string, &$files_encode_str, &$files)
+    {
+        $file = [];
+        $boundary = "\r\n$boundary";
+        if (\strlen($this->_buffer) < $section_start_offset) {
+            return 0;
+        }
+        $section_end_offset = \strpos($this->_buffer, $boundary, $section_start_offset);
+        if (!$section_end_offset) {
+            return 0;
+        }
+        $content_lines_end_offset = \strpos($this->_buffer, "\r\n\r\n", $section_start_offset);
+        if (!$content_lines_end_offset || $content_lines_end_offset + 4 > $section_end_offset) {
+            return 0;
+        }
+        $content_lines_str = \substr($this->_buffer, $section_start_offset, $content_lines_end_offset - $section_start_offset);
+        $content_lines = \explode("\r\n", trim($content_lines_str . "\r\n"));
+        $boundary_value = \substr($this->_buffer, $content_lines_end_offset + 4, $section_end_offset - $content_lines_end_offset - 4);
+        $upload_key = false;
+        foreach ($content_lines as $content_line) {
+            if (!\strpos($content_line, ': ')) {
+                return 0;
+            }
+            list($key, $value) = \explode(': ', $content_line);
+            switch (strtolower($key)) {
+                case "content-disposition":
+                    // Is file data.
+                    if (\preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
+                        $error = 0;
+                        $tmp_file = '';
+                        $size = \strlen($boundary_value);
+                        $tmp_upload_dir = HTTP::uploadTmpDir();
+                        if (!$tmp_upload_dir) {
+                            $error = UPLOAD_ERR_NO_TMP_DIR;
+                        } else if ($boundary_value === '') {
+                            $error = UPLOAD_ERR_NO_FILE;
+                        } else {
+                            $tmp_file = \tempnam($tmp_upload_dir, 'workerman.upload.');
+                            if ($tmp_file === false || false == \file_put_contents($tmp_file, $boundary_value)) {
+                                $error = UPLOAD_ERR_CANT_WRITE;
                             }
                         }
+                        $upload_key = $match[1];
+                        // Parse upload files.
+                        $file = [
+                            'name' => $match[2],
+                            'tmp_name' => $tmp_file,
+                            'size' => $size,
+                            'error' => $error,
+                            'type' => '',
+                        ];
                         break;
-                    case "content-type":
-                        // add file_type
-                        if (!isset($files[$index])) {
-                            $files[$index] = [];
+                    } // Is post field.
+                    else {
+                        // Parse $_POST.
+                        if (\preg_match('/name="(.*?)"$/', $value, $match)) {
+                            $k = $match[1];
+                            $post_encode_string .= \urlencode($k) . "=" . \urlencode($boundary_value) . '&';
                         }
-                        $files[$index]['type'] = \trim($header_value);
-                        break;
-                }
-            }
-        }
-        $files_unique = [];
-        foreach ($files as $index => $file) {
-            $key = $file['key'];
-            if (\substr($key, -2) === '[]') {
-                $key = $index;
+                        return $section_end_offset + \strlen($boundary) + 2;
+                    }
+                    break;
+                case "content-type":
+                    $file['type'] = \trim($value);
+                    break;
             }
-            $files_unique[$key] = $file;
-        }
-        foreach ($files_unique as $file) {
-            $key = $file['key'];
-            unset($file['key']);
-            $str = \urlencode($key) . "=1";
-            $result = [];
-            \parse_str($str, $result);
-            \array_walk_recursive($result, function (&$value) use ($file) {
-                $value = $file;
-            });
-            $this->_data['files'] = \array_merge_recursive($this->_data['files'], $result);
         }
-        if ($post_str) {
-            parse_str($post_str, $this->_data['post']);
+        if ($upload_key === false) {
+            return 0;
         }
+        $files_encode_str .= \urlencode($upload_key) . '=' . \count($files) . '&';
+        $files[] = $file;
+
+        return $section_end_offset + \strlen($boundary) + 2;
     }
 
     /**
@@ -615,7 +614,7 @@ class Request
      */
     public static function createSessionId()
     {
-        return \bin2hex(\pack('d', \microtime(true)) . \pack('N', \mt_rand()));
+        return \bin2hex(\pack('d', \microtime(true)) . random_bytes(8));
     }
 
     /**
@@ -638,7 +637,7 @@ class Request
      */
     public function __get($name)
     {
-        return isset($this->properties[$name]) ? $this->properties[$name] : null;
+        return $this->properties[$name] ?? null;
     }
 
     /**

+ 54 - 45
src/Protocols/Http/Response.php

@@ -71,67 +71,78 @@ class Response
     /**
      * Phrases.
      *
-     * @var array
+     * @var array<int,string>
+     * 
+     * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
      */
-    protected static $_phrases = [
+    const PHRASES = [
         100 => 'Continue',
         101 => 'Switching Protocols',
-        102 => 'Processing',
+        102 => 'Processing', // WebDAV; RFC 2518
+        103 => 'Early Hints', // RFC 8297
+
         200 => 'OK',
         201 => 'Created',
         202 => 'Accepted',
-        203 => 'Non-Authoritative Information',
+        203 => 'Non-Authoritative Information', // since HTTP/1.1
         204 => 'No Content',
         205 => 'Reset Content',
-        206 => 'Partial Content',
-        207 => 'Multi-status',
-        208 => 'Already Reported',
+        206 => 'Partial Content', // RFC 7233
+        207 => 'Multi-Status', // WebDAV; RFC 4918
+        208 => 'Already Reported', // WebDAV; RFC 5842
+        226 => 'IM Used', // RFC 3229
+
         300 => 'Multiple Choices',
         301 => 'Moved Permanently',
-        302 => 'Found',
-        303 => 'See Other',
-        304 => 'Not Modified',
-        305 => 'Use Proxy',
+        302 => 'Found', // Previously "Moved temporarily"
+        303 => 'See Other', // since HTTP/1.1
+        304 => 'Not Modified', // RFC 7232
+        305 => 'Use Proxy', // since HTTP/1.1
         306 => 'Switch Proxy',
-        307 => 'Temporary Redirect',
+        307 => 'Temporary Redirect', // since HTTP/1.1
+        308 => 'Permanent Redirect', // RFC 7538
+
         400 => 'Bad Request',
-        401 => 'Unauthorized',
+        401 => 'Unauthorized', // RFC 7235
         402 => 'Payment Required',
         403 => 'Forbidden',
         404 => 'Not Found',
         405 => 'Method Not Allowed',
         406 => 'Not Acceptable',
-        407 => 'Proxy Authentication Required',
-        408 => 'Request Time-out',
+        407 => 'Proxy Authentication Required', // RFC 7235
+        408 => 'Request Timeout',
         409 => 'Conflict',
         410 => 'Gone',
         411 => 'Length Required',
-        412 => 'Precondition Failed',
-        413 => 'Request Entity Too Large',
-        414 => 'Request-URI Too Large',
-        415 => 'Unsupported Media Type',
-        416 => 'Requested range not satisfiable',
+        412 => 'Precondition Failed', // RFC 7232
+        413 => 'Payload Too Large', // RFC 7231
+        414 => 'URI Too Long', // RFC 7231
+        415 => 'Unsupported Media Type', // RFC 7231
+        416 => 'Range Not Satisfiable', // RFC 7233
         417 => 'Expectation Failed',
-        418 => 'I\'m a teapot',
-        422 => 'Unprocessable Entity',
-        423 => 'Locked',
-        424 => 'Failed Dependency',
-        425 => 'Unordered Collection',
+        418 => 'I\'m a teapot', // RFC 2324, RFC 7168
+        421 => 'Misdirected Request', // RFC 7540
+        422 => 'Unprocessable Entity', // WebDAV; RFC 4918
+        423 => 'Locked', // WebDAV; RFC 4918
+        424 => 'Failed Dependency', // WebDAV; RFC 4918
+        425 => 'Too Early', // RFC 8470
         426 => 'Upgrade Required',
-        428 => 'Precondition Required',
-        429 => 'Too Many Requests',
-        431 => 'Request Header Fields Too Large',
-        451 => 'Unavailable For Legal Reasons',
+        428 => 'Precondition Required', // RFC 6585
+        429 => 'Too Many Requests', // RFC 6585
+        431 => 'Request Header Fields Too Large', // RFC 6585
+        451 => 'Unavailable For Legal Reasons', // RFC 7725
+        
         500 => 'Internal Server Error',
         501 => 'Not Implemented',
         502 => 'Bad Gateway',
         503 => 'Service Unavailable',
-        504 => 'Gateway Time-out',
-        505 => 'HTTP Version not supported',
-        506 => 'Variant Also Negotiates',
-        507 => 'Insufficient Storage',
-        508 => 'Loop Detected',
-        511 => 'Network Authentication Required',
+        504 => 'Gateway Timeout',
+        505 => 'HTTP Version Not Supported',
+        506 => 'Variant Also Negotiates', // RFC 2295
+        507 => 'Insufficient Storage', // WebDAV; RFC 4918
+        508 => 'Loop Detected', // WebDAV; RFC 5842
+        510 => 'Not Extended', // RFC 2774
+        511 => 'Network Authentication Required', // RFC 6585
     ];
 
     /**
@@ -219,10 +230,8 @@ class Response
      */
     public function getHeader($name)
     {
-        if (!isset($this->_header[$name])) {
-            return null;
-        }
-        return $this->_header[$name];
+
+        return $this->_header[$name] ?? null;
     }
 
     /**
@@ -333,11 +342,11 @@ class Response
      * @param bool $same_site
      * @return $this
      */
-    public function cookie($name, $value = '', $max_age = 0, $path = '', $domain = '', $secure = false, $http_only = false, $same_site = false)
+    public function cookie($name, $value = '', $max_age = null, $path = '', $domain = '', $secure = false, $http_only = false, $same_site  = false)
     {
         $this->_header['Set-Cookie'][] = $name . '=' . \rawurlencode($value)
             . (empty($domain) ? '' : '; Domain=' . $domain)
-            . (empty($max_age) ? '' : '; Max-Age=' . $max_age)
+            . ($max_age === null ? '' : '; Max-Age=' . $max_age)
             . (empty($path) ? '' : '; Path=' . $path)
             . (!$secure ? '' : '; Secure')
             . (!$http_only ? '' : '; HttpOnly')
@@ -354,7 +363,7 @@ class Response
     protected function createHeadForFile($file_info)
     {
         $file = $file_info['file'];
-        $reason = $this->_reason ?: static::$_phrases[$this->_status];
+        $reason = $this->_reason ?: self::PHRASES[$this->_status];
         $head = "HTTP/{$this->_version} {$this->_status} $reason\r\n";
         $headers = $this->_header;
         if (!isset($headers['Server'])) {
@@ -375,8 +384,8 @@ class Response
         }
 
         $file_info = \pathinfo($file);
-        $extension = isset($file_info['extension']) ? $file_info['extension'] : '';
-        $base_name = isset($file_info['basename']) ? $file_info['basename'] : 'unknown';
+        $extension = $file_info['extension'] ?? '';
+        $base_name = $file_info['basename'] ?? 'unknown';
         if (!isset($headers['Content-Type'])) {
             if (isset(self::$_mimeTypeMap[$extension])) {
                 $head .= "Content-Type: " . self::$_mimeTypeMap[$extension] . "\r\n";
@@ -409,7 +418,7 @@ class Response
             return $this->createHeadForFile($this->file);
         }
 
-        $reason = $this->_reason ?: static::$_phrases[$this->_status];
+        $reason = $this->_reason ?: self::PHRASES[$this->_status] ?? '';
         $body_len = \strlen($this->_body);
         if (empty($this->_header)) {
             return "HTTP/{$this->_version} {$this->_status} $reason\r\nServer: workerman\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $body_len\r\nConnection: keep-alive\r\n\r\n{$this->_body}";

+ 96 - 25
src/Protocols/Http/Session.php

@@ -14,6 +14,7 @@
 
 namespace Workerman\Protocols\Http;
 
+use Workerman\Protocols\Http\Session\FileSessionHandler;
 use Workerman\Protocols\Http\Session\SessionHandlerInterface;
 
 /**
@@ -27,7 +28,7 @@ class Session
      *
      * @var string
      */
-    protected static $_handlerClass = 'Workerman\Protocols\Http\Session\FileSessionHandler';
+    protected static $_handlerClass = FileSessionHandler::class;
 
     /**
      * Parameters of __constructor for session handler class.
@@ -37,25 +38,74 @@ class Session
     protected static $_handlerConfig = null;
 
     /**
-     * Session.gc_probability
+     * Session name.
      *
-     * @var int
+     * @var string
+     */
+    public static $name = 'PHPSID';
+
+    /**
+     * Auto update timestamp.
+     *
+     * @var bool
      */
-    protected static $_sessionGcProbability = 1;
+    public static $autoUpdateTimestamp = false;
 
     /**
-     * Session.gc_divisor
+     * Session lifetime.
      *
      * @var int
      */
-    protected static $_sessionGcDivisor = 1000;
+    public static $lifetime = 1440;
 
     /**
-     * Session.gc_maxlifetime
+     * Cookie lifetime.
      *
      * @var int
      */
-    protected static $_sessionGcMaxLifeTime = 1440;
+    public static $cookieLifetime = 1440;
+
+    /**
+     * Session cookie path.
+     *
+     * @var string
+     */
+    public static $cookiePath = '/';
+
+    /**
+     * Session cookie domain.
+     *
+     * @var string
+     */
+    public static $domain = '';
+
+    /**
+     * HTTPS only cookies.
+     *
+     * @var bool
+     */
+    public static $secure = false;
+
+    /**
+     * HTTP access only.
+     *
+     * @var bool
+     */
+    public static $httpOnly = true;
+
+    /**
+     * Same-site cookies.
+     *
+     * @var string
+     */
+    public static $sameSite = '';
+
+    /**
+     * Gc probability.
+     *
+     * @var int[]
+     */
+    public static $gcProbability = [1, 1000];
 
     /**
      * Session handler instance.
@@ -121,7 +171,7 @@ class Session
      */
     public function get($name, $default = null)
     {
-        return isset($this->_data[$name]) ? $this->_data[$name] : $default;
+        return $this->_data[$name] ?? $default;
     }
 
     /**
@@ -255,6 +305,8 @@ class Session
             } else {
                 static::$_handler->write($this->_sessionId, \serialize($this->_data));
             }
+        } elseif (static::$autoUpdateTimestamp) {
+            static::refresh();
         }
         $this->_needSave = false;
     }
@@ -266,7 +318,7 @@ class Session
      */
     public function refresh()
     {
-        static::$_handler->updateTimestamp($this->getId());
+        return static::$_handler->updateTimestamp($this->getId());
     }
 
     /**
@@ -276,17 +328,20 @@ class Session
      */
     public static function init()
     {
-        if ($gc_probability = \ini_get('session.gc_probability')) {
-            self::$_sessionGcProbability = (int)$gc_probability;
-        }
-
-        if ($gc_divisor = \ini_get('session.gc_divisor')) {
-            self::$_sessionGcDivisor = (int)$gc_divisor;
+        if (($gc_probability = (int)\ini_get('session.gc_probability')) && ($gc_divisor = (int)\ini_get('session.gc_divisor'))) {
+            static::$gcProbability = [$gc_probability, $gc_divisor];
         }
 
         if ($gc_max_life_time = \ini_get('session.gc_maxlifetime')) {
-            self::$_sessionGcMaxLifeTime = (int)$gc_max_life_time;
+            self::$lifetime = (int)$gc_max_life_time;
         }
+
+        $session_cookie_params = \session_get_cookie_params();
+        static::$cookieLifetime = $session_cookie_params['lifetime'];
+        static::$cookiePath = $session_cookie_params['path'];
+        static::$domain = $session_cookie_params['domain'];
+        static::$secure = $session_cookie_params['secure'];
+        static::$httpOnly = $session_cookie_params['httponly'];
     }
 
     /**
@@ -308,6 +363,23 @@ class Session
     }
 
     /**
+     * Get cookie params.
+     *
+     * @return array
+     */
+    public static function getCookieParams()
+    {
+        return [
+            'lifetime' => static::$cookieLifetime,
+            'path' => static::$cookiePath,
+            'domain' => static::$domain,
+            'secure' => static::$secure,
+            'httponly' => static::$httpOnly,
+            'samesite' => static::$sameSite,
+        ];
+    }
+
+    /**
      * Init handler.
      *
      * @return void
@@ -322,16 +394,13 @@ class Session
     }
 
     /**
-     * Try GC sessions.
+     * GC sessions.
      *
      * @return void
      */
-    public function tryGcSessions()
+    public function gc()
     {
-        if (\rand(1, static::$_sessionGcDivisor) > static::$_sessionGcProbability) {
-            return;
-        }
-        static::$_handler->gc(static::$_sessionGcMaxLifeTime);
+        static::$_handler->gc(static::$lifetime);
     }
 
     /**
@@ -342,7 +411,9 @@ class Session
     public function __destruct()
     {
         $this->save();
-        $this->tryGcSessions();
+        if (\random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) {
+            $this->gc();
+        }
     }
 
     /**
@@ -368,4 +439,4 @@ class SessionException extends \RuntimeException
 }
 
 // Init session.
-Session::init();
+Session::init();

+ 8 - 2
src/Protocols/Http/Session/FileSessionHandler.php

@@ -14,6 +14,8 @@
 
 namespace Workerman\Protocols\Http\Session;
 
+use Workerman\Protocols\Http\Session;
+
 /**
  * Class FileSessionHandler
  * @package Workerman\Protocols\Http\Session
@@ -73,8 +75,12 @@ class FileSessionHandler implements SessionHandlerInterface
         $session_file = static::sessionFile($session_id);
         \clearstatcache();
         if (\is_file($session_file)) {
+            if (\time() - \filemtime($session_file) > Session::$lifetime) {
+                \unlink($session_file);
+                return '';
+            }
             $data = \file_get_contents($session_file);
-            return $data ? $data : '';
+            return $data ?: '';
         }
         return '';
     }
@@ -84,7 +90,7 @@ class FileSessionHandler implements SessionHandlerInterface
      */
     public function write($session_id, $session_data)
     {
-        $temp_file = static::$_sessionSavePath . uniqid(mt_rand(), true);
+        $temp_file = static::$_sessionSavePath . uniqid(bin2hex(random_bytes(8)), true);
         if (!\file_put_contents($temp_file, $session_data)) {
             return false;
         }

+ 2 - 1
src/Protocols/Http/Session/RedisClusterSessionHandler.php

@@ -14,11 +14,12 @@
 
 namespace Workerman\Protocols\Http\Session;
 
+use Workerman\Protocols\Http\Session;
+
 class RedisClusterSessionHandler extends RedisSessionHandler
 {
     public function __construct($config)
     {
-        $this->_maxLifeTime = (int)ini_get('session.gc_maxlifetime');
         $timeout = $config['timeout'] ?? 2;
         $read_timeout = $config['read_timeout'] ?? $timeout;
         $persistent = $config['persistent'] ?? false;

+ 4 - 9
src/Protocols/Http/Session/RedisSessionHandler.php

@@ -11,8 +11,10 @@
  * @link      http://www.workerman.net/
  * @license   http://www.opensource.org/licenses/mit-license.php MIT License
  */
+
 namespace Workerman\Protocols\Http\Session;
 
+use Workerman\Protocols\Http\Session;
 use Workerman\Timer;
 use RedisException;
 
@@ -29,11 +31,6 @@ class RedisSessionHandler implements SessionHandlerInterface
     protected $_redis;
 
     /**
-     * @var int
-     */
-    protected $_maxLifeTime;
-
-    /**
      * @var array
      */
     protected $_config;
@@ -55,7 +52,6 @@ class RedisSessionHandler implements SessionHandlerInterface
         if (false === extension_loaded('redis')) {
             throw new \RuntimeException('Please install redis extension.');
         }
-        $this->_maxLifeTime = (int)ini_get('session.gc_maxlifetime');
 
         if (!isset($config['timeout'])) {
             $config['timeout'] = 2;
@@ -113,7 +109,6 @@ class RedisSessionHandler implements SessionHandlerInterface
             }
             throw $e;
         }
-
     }
 
     /**
@@ -121,7 +116,7 @@ class RedisSessionHandler implements SessionHandlerInterface
      */
     public function write($session_id, $session_data)
     {
-        return true === $this->_redis->setex($session_id, $this->_maxLifeTime, $session_data);
+        return true === $this->_redis->setex($session_id, Session::$lifetime, $session_data);
     }
 
     /**
@@ -129,7 +124,7 @@ class RedisSessionHandler implements SessionHandlerInterface
      */
     public function updateTimestamp($id, $data = "")
     {
-        return true === $this->_redis->expire($id, $this->_maxLifeTime);
+        return true === $this->_redis->expire($id, Session::$lifetime);
     }
 
     /**

+ 56 - 56
src/Protocols/Websocket.php

@@ -55,14 +55,14 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
         }
 
         // Has not yet completed the handshake.
-        if (empty($connection->websocketHandshake)) {
+        if (empty($connection->context->websocketHandshake)) {
             return static::dealHandshake($buffer, $connection);
         }
 
         // Buffer websocket frame data.
-        if ($connection->websocketCurrentFrameLength) {
+        if ($connection->context->websocketCurrentFrameLength) {
             // We need more frame data.
-            if ($connection->websocketCurrentFrameLength > $recv_len) {
+            if ($connection->context->websocketCurrentFrameLength > $recv_len) {
                 // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
                 return 0;
             }
@@ -110,7 +110,7 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                 // Pong package.
                 case 0xa:
                     break;
-                // Wrong opcode. 
+                // Wrong opcode.
                 default :
                     Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n");
                     $connection->close();
@@ -138,7 +138,7 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
             }
             $current_frame_length = $head_len + $data_len;
 
-            $total_package_size = \strlen($connection->websocketDataBuffer) + $current_frame_length;
+            $total_package_size = \strlen($connection->context->websocketDataBuffer) + $current_frame_length;
             if ($total_package_size > $connection->maxPackageSize) {
                 Worker::safeEcho("error package. package_length=$total_package_size\n");
                 $connection->close();
@@ -150,7 +150,7 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                     if ($recv_len >= $current_frame_length) {
                         $ping_data = static::decode(\substr($buffer, 0, $current_frame_length), $connection);
                         $connection->consumeRecvBuffer($current_frame_length);
-                        $tmp_connection_type = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB;
+                        $tmp_connection_type = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
                         $connection->websocketType = "\x8a";
                         $ping_cb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false;
                         if ($ping_cb) {
@@ -172,7 +172,7 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                     if ($recv_len >= $current_frame_length) {
                         $pong_data = static::decode(\substr($buffer, 0, $current_frame_length), $connection);
                         $connection->consumeRecvBuffer($current_frame_length);
-                        $tmp_connection_type = isset($connection->websocketType) ? $connection->websocketType : static::BINARY_TYPE_BLOB;
+                        $tmp_connection_type = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
                         $connection->websocketType = "\x8a";
                         // Try to emit onWebSocketPong callback.
                         $pong_cb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false;
@@ -192,22 +192,22 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                 }
                 return $current_frame_length;
             } else {
-                $connection->websocketCurrentFrameLength = $current_frame_length;
+                $connection->context->websocketCurrentFrameLength = $current_frame_length;
             }
         }
 
         // Received just a frame length data.
-        if ($connection->websocketCurrentFrameLength === $recv_len) {
+        if ($connection->context->websocketCurrentFrameLength === $recv_len) {
             static::decode($buffer, $connection);
-            $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
-            $connection->websocketCurrentFrameLength = 0;
+            $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
+            $connection->context->websocketCurrentFrameLength = 0;
             return 0;
         } // The length of the received data is greater than the length of a frame.
-        elseif ($connection->websocketCurrentFrameLength < $recv_len) {
-            static::decode(\substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection);
-            $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength);
-            $current_frame_length = $connection->websocketCurrentFrameLength;
-            $connection->websocketCurrentFrameLength = 0;
+        elseif ($connection->context->websocketCurrentFrameLength < $recv_len) {
+            static::decode(\substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection);
+            $connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
+            $current_frame_length = $connection->context->websocketCurrentFrameLength;
+            $connection->context->websocketCurrentFrameLength = 0;
             // Continue to read next frame.
             return static::input(\substr($buffer, $current_frame_length), $connection);
         } // The length of the received data is less than the length of a frame.
@@ -246,12 +246,12 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
         }
 
         // Handshake not completed so temporary buffer websocket data waiting for send.
-        if (empty($connection->websocketHandshake)) {
-            if (empty($connection->tmpWebsocketData)) {
-                $connection->tmpWebsocketData = '';
+        if (empty($connection->context->websocketHandshake)) {
+            if (empty($connection->context->tmpWebsocketData)) {
+                $connection->context->tmpWebsocketData = '';
             }
             // If buffer has already full then discard the current package.
-            if (\strlen($connection->tmpWebsocketData) > $connection->maxSendBufferSize) {
+            if (\strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) {
                 if ($connection->onError) {
                     try {
                         ($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package');
@@ -261,9 +261,9 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                 }
                 return '';
             }
-            $connection->tmpWebsocketData .= $encode_buffer;
+            $connection->context->tmpWebsocketData .= $encode_buffer;
             // Check buffer is full.
-            if ($connection->maxSendBufferSize <= \strlen($connection->tmpWebsocketData)) {
+            if ($connection->maxSendBufferSize <= \strlen($connection->context->tmpWebsocketData)) {
                 if ($connection->onBufferFull) {
                     try {
                         ($connection->onBufferFull)($connection);
@@ -288,7 +288,10 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
      */
     public static function decode($buffer, ConnectionInterface $connection)
     {
-        $len = \ord($buffer[1]) & 127;
+        $first_byte = \ord($buffer[1]);
+        $len = $first_byte & 127;
+        $rsv1 = $first_byte & 64;
+
         if ($len === 126) {
             $masks = \substr($buffer, 4, 4);
             $data = \substr($buffer, 8);
@@ -304,13 +307,13 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
         $dataLength = \strlen($data);
         $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
         $decoded = $data ^ $masks;
-        if ($connection->websocketCurrentFrameLength) {
-            $connection->websocketDataBuffer .= $decoded;
-            return $connection->websocketDataBuffer;
+        if ($connection->context->websocketCurrentFrameLength) {
+            $connection->context->websocketDataBuffer .= $decoded;
+            return $connection->context->websocketDataBuffer;
         } else {
-            if ($connection->websocketDataBuffer !== '') {
-                $decoded = $connection->websocketDataBuffer . $decoded;
-                $connection->websocketDataBuffer = '';
+            if ($connection->context->websocketDataBuffer !== '') {
+                $decoded = $connection->context->websocketDataBuffer . $decoded;
+                $connection->context->websocketDataBuffer = '';
             }
             return $decoded;
         }
@@ -328,11 +331,11 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
         // HTTP protocol.
         if (0 === \strpos($buffer, 'GET')) {
             // Find \r\n\r\n.
-            $heder_end_pos = \strpos($buffer, "\r\n\r\n");
-            if (!$heder_end_pos) {
+            $header_end_pos = \strpos($buffer, "\r\n\r\n");
+            if (!$header_end_pos) {
                 return 0;
             }
-            $header_length = $heder_end_pos + 4;
+            $header_length = $header_end_pos + 4;
 
             // Get Sec-WebSocket-Key.
             $Sec_WebSocket_Key = '';
@@ -353,14 +356,24 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
                 . "Sec-WebSocket-Accept: " . $new_key . "\r\n";
 
             // Websocket data buffer.
-            $connection->websocketDataBuffer = '';
+            $connection->context->websocketDataBuffer = '';
             // Current websocket frame length.
-            $connection->websocketCurrentFrameLength = 0;
+            $connection->context->websocketCurrentFrameLength = 0;
             // Current websocket frame data.
-            $connection->websocketCurrentFrameBuffer = '';
+            $connection->context->websocketCurrentFrameBuffer = '';
             // Consume handshake data.
             $connection->consumeRecvBuffer($header_length);
 
+            // Try to emit onWebSocketConnect callback.
+            $on_websocket_connect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false;
+            if ($on_websocket_connect) {
+                try {
+                    $on_websocket_connect($connection, new Request($buffer));
+                } catch (\Throwable $e) {
+                    Worker::stopAll(250, $e);
+                }
+            }
+
             // blob or arraybuffer
             if (empty($connection->websocketType)) {
                 $connection->websocketType = static::BINARY_TYPE_BLOB;
@@ -371,12 +384,15 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
             if (isset($connection->headers)) {
                 if (\is_array($connection->headers)) {
                     foreach ($connection->headers as $header) {
-                        if (\strpos($header, 'Server:') === 0) {
+                        if (\stripos($header, 'Server:') === 0) {
                             $has_server_header = true;
                         }
                         $handshake_message .= "$header\r\n";
                     }
                 } else {
+                    if (\stripos($connection->headers, 'Server:') !== false) {
+                        $has_server_header = true;
+                    }
                     $handshake_message .= "$connection->headers\r\n";
                 }
             }
@@ -387,33 +403,17 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
             // Send handshake response.
             $connection->send($handshake_message, true);
             // Mark handshake complete..
-            $connection->websocketHandshake = true;
-
-            // Try to emit onWebSocketConnect callback.
-            $on_websocket_connect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false;
-            if ($on_websocket_connect) {
-                try {
-                    $on_websocket_connect($connection, new Request($buffer));
-                } catch (\Throwable $e) {
-                    Worker::stopAll(250, $e);
-                }
-            }
+            $connection->context->websocketHandshake = true;
 
             // There are data waiting to be sent.
-            if (!empty($connection->tmpWebsocketData)) {
-                $connection->send($connection->tmpWebsocketData, true);
-                $connection->tmpWebsocketData = '';
+            if (!empty($connection->context->tmpWebsocketData)) {
+                $connection->send($connection->context->tmpWebsocketData, true);
+                $connection->context->tmpWebsocketData = '';
             }
             if (\strlen($buffer) > $header_length) {
                 return static::input(\substr($buffer, $header_length), $connection);
             }
             return 0;
-        } // Is flash policy-file-request.
-        elseif (0 === \strpos($buffer, '<polic')) {
-            $policy_xml = '<?xml version="1.0"?><cross-domain-policy><site-control permitted-cross-domain-policies="all"/><allow-access-from domain="*" to-ports="*"/></cross-domain-policy>' . "\0";
-            $connection->send($policy_xml, true);
-            $connection->consumeRecvBuffer(\strlen($buffer));
-            return 0;
         }
         // Bad websocket handshake request.
         $connection->close("HTTP/1.1 200 WebSocket\r\nServer: workerman/" . Worker::VERSION . "\r\n\r\n<div style=\"text-align:center\"><h1>WebSocket</h1><hr>workerman/" . Worker::VERSION . "</div>",

+ 2 - 3
src/Protocols/Ws.php

@@ -346,9 +346,8 @@ class Ws
         $port = $connection->getRemotePort();
         $host = $port === 80 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port;
         // Handshake header.
-        $connection->websocketSecKey = \base64_encode(\md5(\mt_rand(), true));
-        $user_header = isset($connection->headers) ? $connection->headers :
-            (isset($connection->wsHttpHeader) ? $connection->wsHttpHeader : null);
+        $connection->websocketSecKey = \base64_encode(random_bytes(16));
+        $user_header = $connection->headers ?? $connection->wsHttpHeader ?? null;
         $user_header_str = '';
         if (!empty($user_header)) {
             if (\is_array($user_header)) {

+ 10 - 3
src/Timer.php

@@ -114,6 +114,11 @@ class Timer
         if (self::$_event) {
             return $persistent ? self::$_event->repeat($time_interval, $func, $args) : self::$_event->delay($time_interval, $func, $args);
         }
+        
+        // If not workerman runtime just return.
+        if (!Worker::getAllWorkers()) {
+            return;
+        }
 
         if (!\is_callable($func)) {
             Worker::safeEcho(new Exception("not callable"));
@@ -142,9 +147,9 @@ class Timer
      * @param array $args
      * @return bool|int
      */
-    public function delay(float $delay, $func, $args = [])
+    public static function delay(float $delay, $func, $args = [])
     {
-        return $this->add($delay, $func, $args);
+        return static::add($delay, $func, $args, false);
     }
 
 
@@ -213,7 +218,9 @@ class Timer
     public static function delAll()
     {
         self::$_tasks = self::$_status = [];
-        \pcntl_alarm(0);
+        if (\function_exists('pcntl_alarm')) {
+            \pcntl_alarm(0);
+        }
         if (self::$_event) {
             self::$_event->deleteAllTimer();
         }

+ 95 - 62
src/Worker.php

@@ -13,19 +13,20 @@
  */
 namespace Workerman;
 
-use Workerman\Events\Event;
-use Workerman\Events\EventInterface;
+use Exception;
+use Throwable;
 use Workerman\Connection\ConnectionInterface;
 use Workerman\Connection\TcpConnection;
 use Workerman\Connection\UdpConnection;
+use Workerman\Events\Event;
 use Workerman\Events\Select;
-use Exception;
 
 
 /**
  * Worker class
  * A container for listening ports
  */
+#[\AllowDynamicProperties]
 class Worker
 {
     /**
@@ -182,7 +183,7 @@ class Worker
     public $onBufferDrain = null;
 
     /**
-     * Emitted when worker processes stoped.
+     * Emitted when worker processes stopped.
      *
      * @var callable
      */
@@ -196,6 +197,13 @@ class Worker
     public $onWorkerReload = null;
 
     /**
+     * Emitted when worker processes exited.
+     *
+     * @var callable
+     */
+    public $onWorkerExit = null;
+
+    /**
      * Transport layer protocol.
      *
      * @var string
@@ -471,9 +479,9 @@ class Worker
     /**
      * PHP built-in protocols.
      *
-     * @var array
+     * @var array<string,string>
      */
-    protected static $_builtinTransports = [
+    const BUILD_IN_TRANSPORTS = [
         'tcp'   => 'tcp',
         'udp'   => 'udp',
         'unix'  => 'unix',
@@ -483,9 +491,9 @@ class Worker
     /**
      * PHP built-in error types.
      *
-     * @var array
+     * @var array<int,string>
      */
-    protected static $_errorType = [
+    const ERROR_TYPE = [
         \E_ERROR             => 'E_ERROR',             // 1
         \E_WARNING           => 'E_WARNING',           // 2
         \E_PARSE             => 'E_PARSE',             // 4
@@ -538,11 +546,13 @@ class Worker
     {
         static::checkSapiEnv();
         static::init();
+        static::lock();
         static::parseCommand();
         static::daemonize();
         static::initWorkers();
         static::installSignal();
         static::saveMasterPid();
+        static::lock(\LOCK_UN);
         static::displayUI();
         static::forkWorkers();
         static::resetStd();
@@ -619,24 +629,25 @@ class Worker
      *
      * @return void
      */
-    protected static function lock()
+    protected static function lock($flag = \LOCK_EX)
     {
-        $fd = \fopen(static::$_startFile, 'r');
-        if ($fd && !flock($fd, LOCK_EX)) {
-            static::log('Workerman['.static::$_startFile.'] already running.');
-            exit;
+        static $fd;
+        if (\DIRECTORY_SEPARATOR !== '/') {
+            return;
+        }
+        $lock_file = static::$pidFile . '.lock';
+        $fd = $fd ?: \fopen($lock_file, 'a+');
+        if ($fd) {
+            flock($fd, $flag);
+            if ($flag === \LOCK_UN) {
+                fclose($fd);
+                $fd = null;
+                clearstatcache();
+                if (\is_file($lock_file)) {
+                    unlink($lock_file);
+                }
+            }
         }
-    }
-
-    /**
-     * Unlock.
-     *
-     * @return void
-     */
-    protected static function unlock()
-    {
-        $fd = \fopen(static::$_startFile, 'r');
-        $fd && flock($fd, \LOCK_UN);
     }
 
     /**
@@ -702,7 +713,7 @@ class Worker
     /**
      * Get all worker instances.
      *
-     * @return array
+     * @return Worker[]
      */
     public static function getAllWorkers()
     {
@@ -1155,7 +1166,7 @@ class Worker
             case \SIGINT:
             case \SIGTERM:
             case \SIGHUP:
-            case \SIGTSTP;
+            case \SIGTSTP:
                 static::$_gracefulStop = false;
                 static::stopAll();
                 break;
@@ -1214,9 +1225,11 @@ class Worker
     /**
      * Redirect standard input and output.
      *
+     * @param bool $throw_exception
+     * @return void
      * @throws Exception
      */
-    public static function resetStd()
+    public static function resetStd(bool $throw_exception = true)
     {
         if (!static::$daemonize || \DIRECTORY_SEPARATOR !== '/') {
             return;
@@ -1232,18 +1245,29 @@ class Worker
             if ($STDERR) {
                 \fclose($STDERR);
             }
-            \fclose(\STDOUT);
-            \fclose(\STDERR);
+            if (\is_resource(\STDOUT)) {
+                \fclose(\STDOUT);
+            }
+            if (\is_resource(\STDERR)) {
+                \fclose(\STDERR);
+            }
             $STDOUT = \fopen(static::$stdoutFile, "a");
             $STDERR = \fopen(static::$stdoutFile, "a");
+            // Fix standard output cannot redirect of PHP 8.1.8's bug
+            if (\function_exists('posix_isatty') && \posix_isatty(2)) {
+                \ob_start(function ($string) {
+                    \file_put_contents(static::$stdoutFile, $string, FILE_APPEND);
+                }, 1);
+            }
             // change output stream
             static::$_outputStream = null;
             static::outputStream($STDOUT);
             \restore_error_handler();
             return;
         }
-
-        throw new Exception('Can not open stdoutFile ' . static::$stdoutFile);
+        if ($throw_exception) {
+            throw new Exception('Can not open stdoutFile ' . static::$stdoutFile);
+        }
     }
 
     /**
@@ -1479,13 +1503,14 @@ class Worker
         elseif (0 === $pid) {
             \srand();
             \mt_srand();
+            static::$_gracefulStop = false;
             if ($worker->reusePort) {
                 $worker->listen();
             }
             if (static::$_status === static::STATUS_STARTING) {
                 static::resetStd();
             }
-            static::$_pidMap  = [];
+            static::$_pidsToRestart = static::$_pidMap  = [];
             // Remove other listener.
             foreach(static::$_workers as $key => $one_worker) {
                 if ($one_worker->workerId !== $worker->workerId) {
@@ -1532,7 +1557,7 @@ class Worker
         // Get uid.
         $user_info = \posix_getpwnam($this->user);
         if (!$user_info) {
-            static::log("Warning: User {$this->user} not exsits");
+            static::log("Warning: User {$this->user} not exists");
             return;
         }
         $uid = $user_info['uid'];
@@ -1540,7 +1565,7 @@ class Worker
         if ($this->group) {
             $group_info = \posix_getgrnam($this->group);
             if (!$group_info) {
-                static::log("Warning: Group {$this->group} not exsits");
+                static::log("Warning: Group {$this->group} not exists");
                 return;
             }
             $gid = $group_info['gid'];
@@ -1607,7 +1632,16 @@ class Worker
                         $worker = static::$_workers[$worker_id];
                         // Exit status.
                         if ($status !== 0) {
-                            static::log("worker[" . $worker->name . ":$pid] exit with status $status");
+                            static::log("worker[{$worker->name}:$pid] exit with status $status");
+                        }
+
+                        // onWorkerExit
+                        if ($worker->onWorkerExit) {
+                            try {
+                                ($worker->onWorkerExit)($worker, $status, $pid);
+                            } catch (Throwable $exception) {
+                                static::log("worker[{$worker->name}] onWorkerExit $exception");
+                            }
                         }
 
                         // For Statistics.
@@ -1692,22 +1726,20 @@ class Worker
             if (static::$_status !== static::STATUS_RELOADING && static::$_status !== static::STATUS_SHUTDOWN) {
                 static::log("Workerman[" . \basename(static::$_startFile) . "] reloading");
                 static::$_status = static::STATUS_RELOADING;
+
+                static::resetStd(false);
                 // Try to emit onMasterReload callback.
                 if (static::$onMasterReload) {
                     try {
                         \call_user_func(static::$onMasterReload);
-                    } catch (\Throwable $e) {
+                    } catch (Throwable $e) {
                         static::stopAll(250, $e);
                     }
                     static::initId();
                 }
             }
 
-            if (static::$_gracefulStop) {
-                $sig = \SIGUSR2;
-            } else {
-                $sig = \SIGUSR1;
-            }
+            $sig = static::$_gracefulStop ? \SIGUSR2 : \SIGUSR1;
 
             // Send reload signal to all child processes.
             $reloadable_pid_array = [];
@@ -1751,13 +1783,15 @@ class Worker
             if ($worker->onWorkerReload) {
                 try {
                     \call_user_func($worker->onWorkerReload, $worker);
-                } catch (\Throwable $e) {
+                } catch (Throwable $e) {
                     static::stopAll(250, $e);
                 }
             }
 
             if ($worker->reloadable) {
                 static::stopAll();
+            } else {
+                static::resetStd(false);
             }
         }
     }
@@ -1780,11 +1814,9 @@ class Worker
             static::log("Workerman[" . \basename(static::$_startFile) . "] stopping ...");
             $worker_pid_array = static::getAllWorkerPids();
             // Send stop signal to all child processes.
-            if (static::$_gracefulStop) {
-                $sig = \SIGQUIT;
-            } else {
-                $sig = \SIGINT;
-            }
+            $sig = static::$_gracefulStop ? \SIGQUIT : \SIGINT;
+            // Fix exit with status 2
+            usleep(50000);
             foreach ($worker_pid_array as $worker_pid) {
                 \posix_kill($worker_pid, $sig);
                 if(!static::$_gracefulStop){
@@ -1920,10 +1952,14 @@ class Worker
         }
 
         // For child processes.
+        \gc_collect_cycles();
+        if (\function_exists('gc_mem_caches')) {
+            \gc_mem_caches();
+        }
         \reset(static::$_workers);
         /** @var static $worker */
         $worker            = current(static::$_workers);
-        $worker_status_str = \posix_getpid() . "\t" . \str_pad(round(memory_get_usage(true) / (1024 * 1024), 2) . "M", 7)
+        $worker_status_str = \posix_getpid() . "\t" . \str_pad(round(memory_get_usage() / (1024 * 1024), 2) . "M", 7)
             . " " . \str_pad($worker->getSocketName(), static::$_maxSocketNameLength) . " "
             . \str_pad(($worker->name === $worker->getSocketName() ? 'none' : $worker->name), static::$_maxWorkerNameLength)
             . " ";
@@ -2042,11 +2078,8 @@ class Worker
      */
     protected static function getErrorType($type)
     {
-        if(isset(self::$_errorType[$type])) {
-            return self::$_errorType[$type];
-        }
 
-        return '';
+        return self::ERROR_TYPE[$type] ?? '';
     }
 
     /**
@@ -2148,7 +2181,7 @@ class Worker
         }
 
         // Try to turn reusePort on.
-        if (\DIRECTORY_SEPARATOR === '/'  // if linux
+        /*if (\DIRECTORY_SEPARATOR === '/'  // if linux
             && $socket_name
             && \version_compare(php_uname('r'), '3.9', 'ge') // if kernel >=3.9
             && \strtolower(\php_uname('s')) !== 'darwin' // if not Mac OS
@@ -2168,7 +2201,7 @@ class Worker
                     \restore_error_handler();
                 } catch (\Throwable $e) {}
             }
-        }
+        }*/
     }
 
 
@@ -2215,7 +2248,7 @@ class Worker
             }
 
             // Try to open keepalive for tcp and disable Nagle algorithm.
-            if (\function_exists('socket_import_stream') && static::$_builtinTransports[$this->transport] === 'tcp') {
+            if (\function_exists('socket_import_stream') && self::BUILD_IN_TRANSPORTS[$this->transport] === 'tcp') {
                 \set_error_handler(function(){});
                 $socket = \socket_import_stream($this->_mainSocket);
                 \socket_set_option($socket, \SOL_SOCKET, \SO_KEEPALIVE, 1);
@@ -2257,7 +2290,7 @@ class Worker
         // Get the application layer communication protocol and listening address.
         list($scheme, $address) = \explode(':', $this->_socketName, 2);
         // Check application layer protocol class.
-        if (!isset(static::$_builtinTransports[$scheme])) {
+        if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) {
             $scheme         = \ucfirst($scheme);
             $this->protocol = \substr($scheme,0,1)==='\\' ? $scheme : 'Protocols\\' . $scheme;
             if (!\class_exists($this->protocol)) {
@@ -2267,7 +2300,7 @@ class Worker
                 }
             }
 
-            if (!isset(static::$_builtinTransports[$this->transport])) {
+            if (!isset(self::BUILD_IN_TRANSPORTS[$this->transport])) {
                 throw new Exception('Bad worker->transport ' . \var_export($this->transport, true));
             }
         } else {
@@ -2276,7 +2309,7 @@ class Worker
             }
         }
         //local socket
-        return static::$_builtinTransports[$this->transport] . ":" . $address;
+        return self::BUILD_IN_TRANSPORTS[$this->transport] . ":" . $address;
     }
 
     /**
@@ -2357,7 +2390,7 @@ class Worker
         if ($this->onWorkerStart) {
             try {
                 ($this->onWorkerStart)($this);
-            } catch (\Throwable $e) {
+            } catch (Throwable $e) {
                 // Avoid rapid infinite loop exit.
                 sleep(1);
                 static::stopAll(250, $e);
@@ -2379,7 +2412,7 @@ class Worker
         if ($this->onWorkerStop) {
             try {
                 ($this->onWorkerStop)($this);
-            } catch (\Throwable $e) {
+            } catch (Throwable $e) {
                 Worker::log($e);
             }
         }
@@ -2429,7 +2462,7 @@ class Worker
         if ($this->onConnect) {
             try {
                 ($this->onConnect)($connection);
-            } catch (\Throwable $e) {
+            } catch (Throwable $e) {
                 static::stopAll(250, $e);
             }
         }
@@ -2483,7 +2516,7 @@ class Worker
                     $message_cb($connection, $recv_buffer);
                 }
                 ++ConnectionInterface::$statistics['total_request'];
-            } catch (\Throwable $e) {
+            } catch (Throwable $e) {
                 static::stopAll(250, $e);
             }
         }