walkor il y a 5 ans
Parent
commit
8ad4acc585

+ 12 - 1
Lib/Constants.php

@@ -30,8 +30,19 @@ const OS_TYPE_LINUX   = 'linux';
 const OS_TYPE_WINDOWS = 'windows';
 
 // Compatible with php7
-if ( ! class_exists('Error')) {
+if (!class_exists('Error')) {
     class Error extends Exception
     {
     }
 }
+
+if (!interface_exists('SessionHandlerInterface')) {
+    interface SessionHandlerInterface {
+        public function close();
+        public function destroy($session_id);
+        public function gc($maxlifetime);
+        public function open($save_path ,$session_name);
+        public function read($session_id);
+        public function write($session_id , $session_data);
+    }
+}

+ 5 - 165
Lib/Timer.php

@@ -13,170 +13,10 @@
  */
 namespace Workerman\Lib;
 
-use Workerman\Events\EventInterface;
-use Workerman\Worker;
-use \Exception;
-
 /**
- * Timer.
- *
- * example:
- * Workerman\Lib\Timer::add($time_interval, callback, array($arg1, $arg2..));
+ * Do not use Workerman\Lib\Timer.
+ * Please use Workerman\Timer.
+ * This class is only used for compatibility with workerman 3.*
+ * @package Workerman\Lib
  */
-class Timer
-{
-    /**
-     * Tasks that based on ALARM signal.
-     * [
-     *   run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
-     *   run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
-     *   ..
-     * ]
-     *
-     * @var array
-     */
-    protected static $_tasks = array();
-
-    /**
-     * event
-     *
-     * @var EventInterface
-     */
-    protected static $_event = null;
-
-    /**
-     * Init.
-     *
-     * @param EventInterface $event
-     * @return void
-     */
-    public static function init($event = null)
-    {
-        if ($event) {
-            self::$_event = $event;
-            return;
-        }
-        if (\function_exists('pcntl_signal')) {
-            \pcntl_signal(\SIGALRM, array('\Workerman\Lib\Timer', 'signalHandle'), false);
-        }
-    }
-
-    /**
-     * ALARM signal handler.
-     *
-     * @return void
-     */
-    public static function signalHandle()
-    {
-        if (!self::$_event) {
-            \pcntl_alarm(1);
-            self::tick();
-        }
-    }
-
-    /**
-     * Add a timer.
-     *
-     * @param float    $time_interval
-     * @param callable $func
-     * @param mixed    $args
-     * @param bool     $persistent
-     * @return int|false
-     */
-    public static function add($time_interval, $func, $args = array(), $persistent = true)
-    {
-        if ($time_interval <= 0) {
-            Worker::safeEcho(new Exception("bad time_interval"));
-            return false;
-        }
-
-        if ($args === null) {
-            $args = array();
-        }
-
-        if (self::$_event) {
-            return self::$_event->add($time_interval,
-                $persistent ? EventInterface::EV_TIMER : EventInterface::EV_TIMER_ONCE, $func, $args);
-        }
-
-        if (!\is_callable($func)) {
-            Worker::safeEcho(new Exception("not callable"));
-            return false;
-        }
-
-        if (empty(self::$_tasks)) {
-            \pcntl_alarm(1);
-        }
-
-        $run_time = \time() + $time_interval;
-        if (!isset(self::$_tasks[$run_time])) {
-            self::$_tasks[$run_time] = array();
-        }
-        self::$_tasks[$run_time][] = array($func, (array)$args, $persistent, $time_interval);
-        return 1;
-    }
-
-
-    /**
-     * Tick.
-     *
-     * @return void
-     */
-    public static function tick()
-    {
-        if (empty(self::$_tasks)) {
-            \pcntl_alarm(0);
-            return;
-        }
-
-        $time_now = \time();
-        foreach (self::$_tasks as $run_time => $task_data) {
-            if ($time_now >= $run_time) {
-                foreach ($task_data as $index => $one_task) {
-                    $task_func     = $one_task[0];
-                    $task_args     = $one_task[1];
-                    $persistent    = $one_task[2];
-                    $time_interval = $one_task[3];
-                    try {
-                        \call_user_func_array($task_func, $task_args);
-                    } catch (\Exception $e) {
-                        Worker::safeEcho($e);
-                    }
-                    if ($persistent) {
-                        self::add($time_interval, $task_func, $task_args);
-                    }
-                }
-                unset(self::$_tasks[$run_time]);
-            }
-        }
-    }
-
-    /**
-     * Remove a timer.
-     *
-     * @param mixed $timer_id
-     * @return bool
-     */
-    public static function del($timer_id)
-    {
-        if (self::$_event) {
-            return self::$_event->del($timer_id, EventInterface::EV_TIMER);
-        }
-
-        return false;
-    }
-
-    /**
-     * Remove all timers.
-     *
-     * @return void
-     */
-    public static function delAll()
-    {
-        self::$_tasks = array();
-        \pcntl_alarm(0);
-        if (self::$_event) {
-            self::$_event->clearAllTimer();
-        }
-    }
-}
+class Timer extends \Workerman\Timer {}

+ 175 - 647
Protocols/Http.php

@@ -14,743 +14,271 @@
 namespace Workerman\Protocols;
 
 use Workerman\Connection\TcpConnection;
+use Workerman\Protocols\Http\Request;
+use Workerman\Protocols\Http\Response;
 use Workerman\Protocols\Websocket;
 use Workerman\Worker;
 
 /**
- * http protocol
+ * Class Http.
+ * @package Workerman\Protocols
  */
 class Http
 {
     /**
-      * The supported HTTP methods
-      * @var array
-      */
-    public static $methods = array('GET'=>'GET', 'POST'=>'POST', 'PUT'=>'PUT', 'DELETE'=>'DELETE', 'HEAD'=>'HEAD', 'OPTIONS'=>'OPTIONS');
-
-    /**
-     * Cache.
-     * @var array
-     */
-    protected static $_cache = array();
-
-    /**
-     * Check the integrity of the package.
+     * Request class name.
      *
-     * @param string        $recv_buffer
-     * @param TcpConnection $connection
-     * @return int
+     * @var string
      */
-    public static function input($recv_buffer, TcpConnection $connection)
-    {
-        if (isset(static::$_cache[$recv_buffer]['input'])) {
-            return static::$_cache[$recv_buffer]['input'];
-        }
-        $recv_len = \strlen($recv_buffer);
-        $crlf_post = \strpos($recv_buffer, "\r\n\r\n");
-        if (!$crlf_post) {
-            // Judge whether the package length exceeds the limit.
-            if ($recv_len >= $connection->maxPackageSize) {
-                $connection->close();
-            }
-            return 0;
-        }
-        $head_len = $crlf_post + 4;
-
-        $method = \substr($recv_buffer, 0, \strpos($recv_buffer, ' '));
-        if (!isset(static::$methods[$method])) {
-            $connection->send("HTTP/1.1 400 Bad Request\r\n\r\n", true);
-            $connection->consumeRecvBuffer($recv_len);
-            return 0;
-        }
-
-        if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD') {
-            static::$_cache[$recv_buffer]['input'] = $head_len;
-            return $head_len;
-        }
-
-        $match = array();
-        if (\preg_match("/\r\nContent-Length: ?(\d+)/i", $recv_buffer, $match)) {
-            $content_length = isset($match[1]) ? $match[1] : 0;
-            $total_length = $content_length + $head_len;
-            static::$_cache[$recv_buffer]['input'] = $total_length;
-            return $total_length;
-        }
-
-        return $method === 'DELETE' ? $head_len : 0;
-    }
-
-    /**
-     * Parse $_POST、$_GET、$_COOKIE.
-     *
-     * @param string        $recv_buffer
-     * @param TcpConnection $connection
-     * @return array
-     */
-    public static function decode($recv_buffer, TcpConnection $connection)
-    {
-        if (isset(static::$_cache[$recv_buffer]['decode'])) {
-            HttpCache::reset();
-            $cache = static::$_cache[$recv_buffer]['decode'];
-            //$cache['server']['REQUEST_TIME_FLOAT'] =  \microtime(true);
-            //$cache['server']['REQUEST_TIME'] =  (int)$cache['server']['REQUEST_TIME_FLOAT'];
-            $_SERVER = $cache['server'];
-            $_POST = $cache['post'];
-            $_GET = $cache['get'];
-            $_COOKIE = $cache['cookie'];
-            $_REQUEST = $cache['request'];
-            $GLOBALS['HTTP_RAW_POST_DATA'] = $GLOBALS['HTTP_RAW_REQUEST_DATA'] = '';
-            return static::$_cache[$recv_buffer]['decode'];
-        }
-        // Init.
-        $_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = array();
-        $GLOBALS['HTTP_RAW_POST_DATA'] = '';
-        // Clear cache.
-        HttpCache::reset();
-        //$microtime = \microtime(true);
-        // $_SERVER
-        $_SERVER = array(
-            'QUERY_STRING'         => '',
-            'REQUEST_METHOD'       => '',
-            'REQUEST_URI'          => '',
-            'SERVER_PROTOCOL'      => '',
-            'SERVER_SOFTWARE'      => 'workerman/'.Worker::VERSION,
-            'SERVER_NAME'          => '',
-            'HTTP_HOST'            => '',
-            'HTTP_USER_AGENT'      => '',
-            'HTTP_ACCEPT'          => '',
-            'HTTP_ACCEPT_LANGUAGE' => '',
-            'HTTP_ACCEPT_ENCODING' => '',
-            'HTTP_COOKIE'          => '',
-            'HTTP_CONNECTION'      => '',
-            'CONTENT_TYPE'         => '',
-            'REMOTE_ADDR'          => '',
-            'REMOTE_PORT'          => '0',
-            //'REQUEST_TIME'         => (int)$microtime,
-            //'REQUEST_TIME_FLOAT'   => $microtime //compatible php5.4
-        );
-
-        // Parse headers.
-        list($http_header, $http_body) = \explode("\r\n\r\n", $recv_buffer, 2);
-        $header_data = \explode("\r\n", $http_header);
-
-        list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = \explode(' ',
-            $header_data[0]);
-
-        $http_post_boundary = '';
-        unset($header_data[0]);
-        foreach ($header_data as $content) {
-            // \r\n\r\n
-            if (empty($content)) {
-                continue;
-            }
-            list($key, $value)       = \explode(':', $content, 2);
-            $key                     = \str_replace('-', '_', strtoupper($key));
-            $value                   = \trim($value);
-            $_SERVER['HTTP_' . $key] = $value;
-            switch ($key) {
-                // HTTP_HOST
-                case 'HOST':
-                    $tmp                    = \explode(':', $value);
-                    $_SERVER['SERVER_NAME'] = $tmp[0];
-                    if (isset($tmp[1])) {
-                        $_SERVER['SERVER_PORT'] = $tmp[1];
-                    }
-                    break;
-                // cookie
-                case 'COOKIE':
-                    \parse_str(\str_replace('; ', '&', $_SERVER['HTTP_COOKIE']), $_COOKIE);
-                    break;
-                // content-type
-                case 'CONTENT_TYPE':
-                    if (!\preg_match('/boundary="?(\S+)"?/', $value, $match)) {
-                        if ($pos = \strpos($value, ';')) {
-                            $_SERVER['CONTENT_TYPE'] = \substr($value, 0, $pos);
-                        } else {
-                            $_SERVER['CONTENT_TYPE'] = $value;
-                        }
-                    } else {
-                        $_SERVER['CONTENT_TYPE'] = 'multipart/form-data';
-                        $http_post_boundary      = '--' . $match[1];
-                    }
-                    break;
-                case 'CONTENT_LENGTH':
-                    $_SERVER['CONTENT_LENGTH'] = $value;
-                    break;
-                case 'UPGRADE':
-					if($value === 'websocket'){
-						$connection->protocol = '\Workerman\Protocols\Websocket';
-						return Websocket::input($recv_buffer,$connection);
-					}
-                    break;
-            }
-        }
-
-        // Parse $_POST.
-        if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['CONTENT_TYPE']) {
-            switch ($_SERVER['CONTENT_TYPE']) {
-                case 'multipart/form-data':
-                    self::parseUploadFiles($http_body, $http_post_boundary);
-                    break;
-                case 'application/json':
-                    $_POST = \json_decode($http_body, true);
-                    break;
-                case 'application/x-www-form-urlencoded':
-                    \parse_str($http_body, $_POST);
-                    break;
-            }
-        }
-
-        // Parse other HTTP action parameters
-        if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== "POST") {
-            $data = array();
-            if ($_SERVER['CONTENT_TYPE'] === "application/x-www-form-urlencoded") {
-                \parse_str($http_body, $data);
-            } elseif ($_SERVER['CONTENT_TYPE'] === "application/json") {
-                $data = \json_decode($http_body, true);
-            }
-            $_REQUEST = \array_merge($_REQUEST, $data);
-        }
-
-        // HTTP_RAW_REQUEST_DATA HTTP_RAW_POST_DATA
-        $GLOBALS['HTTP_RAW_REQUEST_DATA'] = $GLOBALS['HTTP_RAW_POST_DATA'] = $http_body;
-
-        // QUERY_STRING
-        $_SERVER['QUERY_STRING'] = \parse_url($_SERVER['REQUEST_URI'], \PHP_URL_QUERY);
-        if ($_SERVER['QUERY_STRING']) {
-            // $GET
-            \parse_str($_SERVER['QUERY_STRING'], $_GET);
-        } else {
-            $_SERVER['QUERY_STRING'] = '';
-        }
-
-        if (\is_array($_POST)) {
-            // REQUEST
-            $_REQUEST = \array_merge($_GET, $_POST, $_REQUEST);
-        } else {
-            // REQUEST
-            $_REQUEST = \array_merge($_GET, $_REQUEST);
-        }
-
-        // REMOTE_ADDR REMOTE_PORT
-        $_SERVER['REMOTE_ADDR'] = $connection->getRemoteIp();
-        $_SERVER['REMOTE_PORT'] = $connection->getRemotePort();
-        $ret = array('get' => $_GET, 'post' => $_POST, 'cookie' => $_COOKIE, 'server' => $_SERVER, 'files' => $_FILES, 'request'=>$_REQUEST);
-        if ($_SERVER['REQUEST_METHOD'] === 'GET') {
-            static::$_cache[$recv_buffer]['decode'] = $ret;
-            if (\count(static::$_cache) > 256) {
-                unset(static::$_cache[key(static::$_cache)]);
-            }
-        }
-
-        return $ret;
-    }
+    protected static $_requestClass = 'Workerman\Protocols\Http\Request';
 
     /**
-     * Http encode.
+     * Session name.
      *
-     * @param string        $content
-     * @param TcpConnection $connection
-     * @return string
+     * @var string
      */
-    public static function encode($content, TcpConnection $connection)
-    {
-        // http-code status line.
-        $header = HttpCache::$status . "\r\n";
-
-        // Cookie headers
-        if(HttpCache::$cookie) {
-            $header .= \implode("\r\n", HttpCache::$cookie) . "\r\n";
-        }
-        
-        // other headers
-        if (HttpCache::$header) {
-            $header .= \implode("\r\n", HttpCache::$header) . "\r\n";
-        }
-
-        if(!empty($connection->gzip)) {
-            $header .= "Content-Encoding: gzip\r\n";
-            $content = \gzencode($content,$connection->gzip);
-        }
-        // header
-        $header .= 'Content-Length: ' . \strlen($content) . "\r\n\r\n";
-
-        // save session
-        self::sessionWriteClose();
-
-        // the whole http package
-        return $header . $content;
-    }
+    protected static $_sessionName = 'PHPSID';
 
     /**
-     * Send a raw HTTP header
-     * 
-     * @param string $content
-     * @param bool   $replace
-     * @param int    $http_response_code
-     * 
-     * @return bool|void
-     */
-    public static function header($content, $replace = true, $http_response_code = null)
-    {
-        if (NO_CLI) {
-            \header($content, $replace, $http_response_code);
-            return;
-        }
-
-        if (\strpos($content, 'HTTP') === 0) {
-            HttpCache::$status = $content;
-            return true;
-        }
-
-        $key = \strstr($content, ':', true);
-        if (empty($key)) {
-            return false;
-        }
-
-        if ('location' === \strtolower($key)) {
-            if (!$http_response_code) {
-            $http_response_code = 302;
-            }
-            self::responseCode($http_response_code);
-        }
-
-        if ($key === 'Set-Cookie') {
-            HttpCache::$cookie[] = $content;
-        } else {
-            HttpCache::$header[$key] = $content;
-        }
-
-        return true;
-    }
-
-    /**
-     * Remove previously set headers
+     * Upload tmp dir.
      *
-     * @param string $name
-     * @return void
+     * @var string
      */
-    public static function headerRemove($name)
-    {
-        if (NO_CLI) {
-            \header_remove($name);
-            return;
-        }
-        unset(HttpCache::$header[$name]);
-    }
+    protected static $_uploadTmpDir = '';
 
     /**
-     * Sets the HTTP response status code.
-     *
-     * @param int $code The response code
-     * @return boolean|int The valid status code or FALSE if code is not provided and it is not invoked in a web server environment
-     */
-    public static function responseCode($code) 
-    {
-        if (NO_CLI) {
-            return \http_response_code($code);
-        }
-        if (isset(HttpCache::$codes[$code])) {
-            HttpCache::$status = "HTTP/1.1 $code " . HttpCache::$codes[$code];
-            return $code;
-        }
-        return false;
-    }
-
-    /**
-     * Set cookie.
-     *
-     * @param string  $name
-     * @param string  $value
-     * @param integer $maxage
-     * @param string  $path
-     * @param string  $domain
-     * @param bool    $secure
-     * @param bool    $HTTPOnly
-     * @return bool|void
-     */
-    public static function setcookie(
-        $name,
-        $value = '',
-        $maxage = 0,
-        $path = '',
-        $domain = '',
-        $secure = false,
-        $HTTPOnly = false
-    ) {
-        if (NO_CLI) {
-            return \setcookie($name, $value, $maxage, $path, $domain, $secure, $HTTPOnly);
-        }
-
-        HttpCache::$cookie[] = 'Set-Cookie: ' . $name . '=' . rawurlencode($value)
-                                . (empty($domain) ? '' : '; Domain=' . $domain)
-                                . (empty($maxage) ? '' : '; Max-Age=' . $maxage)
-                                . (empty($path) ? '' : '; Path=' . $path)
-                                . (!$secure ? '' : '; Secure')
-                                . (!$HTTPOnly ? '' : '; HttpOnly');
-        
-        return true;
-    }
-
-    /**
-     * sessionCreateId
+     * Cache.
      *
-     * @return string
+     * @var array
      */
-    public static function sessionCreateId()
-    {
-        \mt_srand();
-        return bin2hex(\pack('d', \microtime(true)) . \pack('N',\mt_rand(0, 2147483647)));
-    }
+    protected static $_cache = array();
 
     /**
-     * Get and/or set the current session id
-     *
-     * @param string  $id
+     * Open cache.
      *
-     * @return string|null
+     * @var bool.
      */
-    public static function sessionId($id = null)
-    {
-        if (NO_CLI) {
-            return $id ? \session_id($id) : \session_id();
-        }
-        if (static::sessionStarted() && HttpCache::$instance->sessionFile) {
-            return \str_replace('ses_', '', \basename(HttpCache::$instance->sessionFile));
-        }
-        return '';
-    }
+    public static $_enableCache = true;
 
     /**
-     * Get and/or set the current session name
-     *
-     * @param string  $name
+     * Get or set session name.
      *
+     * @param null $name
      * @return string
      */
     public static function sessionName($name = null)
     {
-        if (NO_CLI) {
-            return $name ? \session_name($name) : \session_name();
-        }
-        $session_name = HttpCache::$sessionName;
-        if ($name && ! static::sessionStarted()) {
-            HttpCache::$sessionName = $name;
+        if ($name !== null && $name !== '') {
+            static::$_sessionName = (string)$name;
         }
-        return $session_name;
+        return static::$_sessionName;
     }
 
     /**
-     * Get and/or set the current session save path
-     *
-     * @param string  $path
+     * Get or set the request class name.
      *
+     * @param null $class_name
      * @return string
      */
-    public static function sessionSavePath($path = null)
+    public static function requestClass($class_name = null)
     {
-        if (NO_CLI) {
-            return $path ? \session_save_path($path) : \session_save_path();
-        }
-        if ($path && \is_dir($path) && \is_writable($path) && !static::sessionStarted()) {
-            HttpCache::$sessionPath = $path;
+        if ($class_name) {
+            static::$_requestClass = $class_name;
         }
-        return HttpCache::$sessionPath;
+        return static::$_requestClass;
     }
 
     /**
-     * sessionStarted
+     * Enable or disable Cache.
      *
-     * @return bool
+     * @param $value
      */
-    public static function sessionStarted()
+    public static function enableCache($value)
     {
-        if (!HttpCache::$instance) return false;
-
-        return HttpCache::$instance->sessionStarted;
+        static::$_enableCache = (bool)$value;
     }
 
     /**
-     * sessionStart
+     * Check the integrity of the package.
      *
-     * @return bool
+     * @param string $recv_buffer
+     * @param TcpConnection $connection
+     * @return int
      */
-    public static function sessionStart()
+    public static function input($recv_buffer, TcpConnection $connection)
     {
-        if (NO_CLI) {
-            return \session_start();
+        $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->send("HTTP/1.1 413 Request Entity Too Large\r\n\r\n");
+                $connection->consumeRecvBuffer($recv_len);
+                return 0;
+            }
+            return 0;
         }
 
-        self::tryGcSessions();
+        $head_len = $crlf_pos + 4;
+        $method = \strstr($recv_buffer, ' ', true);
 
-        if (HttpCache::$instance->sessionStarted) {
-            Worker::safeEcho("already sessionStarted\n");
-            return true;
-        }
-        HttpCache::$instance->sessionStarted = true;
-        // Generate a SID.
-        if (!isset($_COOKIE[HttpCache::$sessionName]) || !\is_file(HttpCache::$sessionPath . '/ses_' . $_COOKIE[HttpCache::$sessionName])) {
-            // Create a unique session_id and the associated file name.
-            while (true) {
-                $session_id = static::sessionCreateId();
-                if (!\is_file($file_name = HttpCache::$sessionPath . '/ses_' . $session_id)) break;
-            }
-            HttpCache::$instance->sessionFile = $file_name;
-            return self::setcookie(
-                HttpCache::$sessionName
-                , $session_id
-                , \ini_get('session.cookie_lifetime')
-                , \ini_get('session.cookie_path')
-                , \ini_get('session.cookie_domain')
-                , \ini_get('session.cookie_secure')
-                , \ini_get('session.cookie_httponly')
-            );
-        }
-        if (!HttpCache::$instance->sessionFile) {
-            HttpCache::$instance->sessionFile = HttpCache::$sessionPath . '/ses_' . $_COOKIE[HttpCache::$sessionName];
-        }
-        // Read session from session file.
-        if (HttpCache::$instance->sessionFile) {
-            $raw = \file_get_contents(HttpCache::$instance->sessionFile);
-            if ($raw) {
-                $_SESSION = \unserialize($raw);
-            }
+        if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD') {
+            return $head_len;
+        } else if ($method !== 'POST' && $method !== 'PUT' && $method !== 'DELETE') {
+            $connection->send("HTTP/1.1 400 Bad Request\r\n\r\n", true);
+            $connection->consumeRecvBuffer(\strlen($recv_buffer));
+            return 0;
         }
-        return true;
-    }
 
-    /**
-     * Save session.
-     *
-     * @return bool
-     */
-    public static function sessionWriteClose()
-    {
-        if (NO_CLI) {
-            \session_write_close();
-            return true;
-        }
-        if (!empty(HttpCache::$instance->sessionStarted) && !empty($_SESSION)) {
-            $session_str = \serialize($_SESSION);
-            if ($session_str && HttpCache::$instance->sessionFile) {
-                return (bool) \file_put_contents(HttpCache::$instance->sessionFile, $session_str);
-            }
+        $header = \substr($recv_buffer, 0, $crlf_pos);
+        if ($pos = \strpos($header, "\r\nContent-Length: ")) {
+            return  $head_len + (int)\substr($header, $pos + 18, 10);
+        } else if (\preg_match("/\r\ncontent-length: ?(\d+)/i", $header, $match)) {
+            return $head_len + $match[1];
         }
-        return empty($_SESSION);
+
+        return $method === 'DELETE' ? $head_len : 0;
     }
 
     /**
-     * End, like call exit in php-fpm.
+     * Http decode.
      *
-     * @param string $msg
-     * @throws \Exception
+     * @param string $recv_buffer
+     * @param TcpConnection $connection
+     * @return \Workerman\Protocols\Http\Request
      */
-    public static function end($msg = '')
+    public static function decode($recv_buffer, TcpConnection $connection)
     {
-        if (NO_CLI) {
-            exit($msg);
-        }
-        if ($msg) {
-            echo $msg;
+        $cacheable = static::$_enableCache && \strlen($recv_buffer) < 512;
+        if (true === $cacheable && isset(static::$_cache[$recv_buffer])) {
+            $request = static::$_cache[$recv_buffer];
+            $request->connection = $connection;
+            $connection->__request = $request;
+            $request->properties = array();
+            return $request;
+        }
+        $request = new static::$_requestClass($recv_buffer);
+        $request->connection = $connection;
+        $connection->__request = $request;
+        if (true === $cacheable) {
+            static::$_cache[$recv_buffer] = $request;
+            if (\count(static::$_cache) > 512) {
+                unset(static::$_cache[key(static::$_cache)]);
+            }
         }
-        throw new \Exception('jump_exit');
+        return $request;
     }
 
     /**
-     * Get mime types.
+     * Http encode.
      *
+     * @param string|Response $response
+     * @param TcpConnection $connection
      * @return string
      */
-    public static function getMimeTypesFile()
-    {
-        return __DIR__ . '/Http/mime.types';
-    }
-
-    /**
-     * Parse $_FILES.
-     *
-     * @param string $http_body
-     * @param string $http_post_boundary
-     * @return void
-     */
-    protected static function parseUploadFiles($http_body, $http_post_boundary)
+    public static function encode($response, TcpConnection $connection)
     {
-        $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] === '') {
-            unset($boundary_data_array[0]);
-        }
-        $key = -1;
-        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);
-            $key ++;
-            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="(.*?)"$/', $header_value, $match)) {
-                            // Parse $_FILES.
-                            $_FILES[$key] = array(
-                                'name' => $match[1],
-                                'file_name' => $match[2],
-                                'file_data' => $boundary_value,
-                                'file_size' => \strlen($boundary_value),
-                            );
-                            break;
-                        } // Is post field.
-                        else {
-                            // Parse $_POST.
-                            if (\preg_match('/name="(.*?)"$/', $header_value, $match)) {
-                                $_POST[$match[1]] = $boundary_value;
-                            }
+        if (isset($connection->__request)) {
+            $connection->__request->session = null;
+            $connection->__request->connection = null;
+            $connection->__request = null;
+        }
+        if (\is_scalar($response) || null === $response) {
+            $ext_header = '';
+            if (isset($connection->__header)) {
+                foreach ($connection->__header as $name => $value) {
+                    if (\is_array($value)) {
+                        foreach ($value as $item) {
+                            $ext_header = "$name: $item\r\n";
                         }
-                        break;
-                    case "content-type":
-                        // add file_type
-                        $_FILES[$key]['file_type'] = \trim($header_value);
-                        break;
+                    } else {
+                        $ext_header = "$name: $value\r\n";
+                    }
                 }
+                unset($connection->__header);
             }
+            $body_len = \strlen($response);
+            return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$ext_header}Connection: keep-alive\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $body_len\r\n\r\n$response";
         }
-    }
 
-    /**
-     * Try GC sessions.
-     *
-     * @return void
-     */
-    public static function tryGcSessions()
-    {
-        if (HttpCache::$sessionGcProbability <= 0 ||
-            HttpCache::$sessionGcDivisor     <= 0 ||
-            \rand(1, HttpCache::$sessionGcDivisor) > HttpCache::$sessionGcProbability) {
-            return;
+        if (isset($connection->__header)) {
+            $response->withHeaders($connection->__header);
+            unset($connection->__header);
         }
 
-        $time_now = \time();
-        foreach(glob(HttpCache::$sessionPath.'/ses*') as $file) {
-            if(\is_file($file) && $time_now - \filemtime($file) > HttpCache::$sessionGcMaxLifeTime) {
-                \unlink($file);
+        if (isset($response->file)) {
+            $file = $response->file;
+            $body_len = (int)\filesize($file);
+            $response->header('Content-Length', $body_len);
+            if ($body_len < 1024 * 1024) {
+                $connection->send((string)$response . file_get_contents($file, false, null, -1, $body_len), true);
+                return '';
+            }
+            $handler = \fopen($file, 'r');
+            if (false === $handler) {
+                $connection->close(new Response(403, null, '403 Forbidden'));
+                return '';
             }
+            $connection->send((string)$response, true);
+            static::sendStream($connection, $handler);
+            return '';
         }
-    }
-}
-
-/**
- * Http cache for the current http response.
- */
-class HttpCache
-{
-    public static $codes = array(
-        100 => 'Continue',
-        101 => 'Switching Protocols',
-        200 => 'OK',
-        201 => 'Created',
-        202 => 'Accepted',
-        203 => 'Non-Authoritative Information',
-        204 => 'No Content',
-        205 => 'Reset Content',
-        206 => 'Partial Content',
-        300 => 'Multiple Choices',
-        301 => 'Moved Permanently',
-        302 => 'Found',
-        303 => 'See Other',
-        304 => 'Not Modified',
-        305 => 'Use Proxy',
-        306 => '(Unused)',
-        307 => 'Temporary Redirect',
-        400 => 'Bad Request',
-        401 => 'Unauthorized',
-        402 => 'Payment Required',
-        403 => 'Forbidden',
-        404 => 'Not Found',
-        405 => 'Method Not Allowed',
-        406 => 'Not Acceptable',
-        407 => 'Proxy Authentication Required',
-        408 => 'Request Timeout',
-        409 => 'Conflict',
-        410 => 'Gone',
-        411 => 'Length Required',
-        412 => 'Precondition Failed',
-        413 => 'Request Entity Too Large',
-        414 => 'Request-URI Too Long',
-        415 => 'Unsupported Media Type',
-        416 => 'Requested Range Not Satisfiable',
-        417 => 'Expectation Failed',
-        422 => 'Unprocessable Entity',
-        423 => 'Locked',
-        500 => 'Internal Server Error',
-        501 => 'Not Implemented',
-        502 => 'Bad Gateway',
-        503 => 'Service Unavailable',
-        504 => 'Gateway Timeout',
-        505 => 'HTTP Version Not Supported',
-    );
 
-    public static $default = array(
-        'Content-Type' => 'Content-Type: text/html;charset=utf-8',
-        'Connection'   => 'Connection: keep-alive',
-        'Server'       => 'Server: workerman'
-    );
+        return (string)$response;
+    }
 
     /**
-     * @var HttpCache
+     * Send remainder of a stream to client.
+     *
+     * @param TcpConnection $connection
+     * @param $handler
      */
-    public static $instance             = null;
-    public static $status               = '';
-    public static $header               = array();
-    public static $cookie               = array();
-    public static $sessionPath          = '';
-    public static $sessionName          = '';
-    public static $sessionGcProbability = 1;
-    public static $sessionGcDivisor     = 1000;
-    public static $sessionGcMaxLifeTime = 1440;
-    public $sessionStarted = false;
-    public $sessionFile = '';
-
-    public static function reset()
+    protected static function sendStream(TcpConnection $connection, $handler)
     {
-        self::$status   = 'HTTP/1.1 200 OK';
-        self::$header   = self::$default;
-        self::$cookie   = array();
-        self::$instance->sessionFile = '';
-        self::$instance->sessionStarted = false;
+        $connection->bufferFull = false;
+        // Read file content from disk piece by piece and send to client.
+        $do_write = function () use ($connection, $handler) {
+            // Send buffer not full.
+            while ($connection->bufferFull === false) {
+                // Read from disk.
+                $buffer = \fread($handler, 8192);
+                // Read eof.
+                if ($buffer === '' || $buffer === false) {
+                    fclose($handler);
+                    $connection->onBufferDrain = null;
+                    return;
+                }
+                $connection->send($buffer, true);
+            }
+        };
+        // Send buffer full.
+        $connection->onBufferFull = function ($connection) {
+            $connection->bufferFull = true;
+        };
+        // Send buffer drain.
+        $connection->onBufferDrain = function ($connection) use ($do_write) {
+            $connection->bufferFull = false;
+            $do_write();
+        };
+        $do_write();
     }
 
-    public static function init()
+    /**
+     * Set or get uploadTmpDir.
+     *
+     * @return bool|string
+     */
+    public static function uploadTmpDir($dir = null)
     {
-        if (!self::$sessionName) {
-            self::$sessionName = \ini_get('session.name');
-        }
-
-        if (!self::$sessionPath) {
-            self::$sessionPath = @\session_save_path();
-        }
-
-        if (!self::$sessionPath || \strpos(self::$sessionPath, 'tcp://') === 0) {
-            self::$sessionPath = \sys_get_temp_dir();
-        }
-
-        if ($gc_probability = \ini_get('session.gc_probability')) {
-            self::$sessionGcProbability = $gc_probability;
-        }
-
-        if ($gc_divisor = \ini_get('session.gc_divisor')) {
-            self::$sessionGcDivisor = $gc_divisor;
-        }
-
-        if ($gc_max_life_time = \ini_get('session.gc_maxlifetime')) {
-            self::$sessionGcMaxLifeTime = $gc_max_life_time;
+        if (null !== $dir) {
+            static::$_uploadTmpDir = $dir;
+        }
+        if (static::$_uploadTmpDir === '') {
+            if ($upload_tmp_dir = \ini_get('upload_tmp_dir')) {
+                static::$_uploadTmpDir = $upload_tmp_dir;
+            } else if ($upload_tmp_dir = \sys_get_temp_dir()) {
+                static::$_uploadTmpDir = $upload_tmp_dir;
+            }
         }
-
-        self::$instance = new HttpCache();
+        static::$_uploadTmpDir;
     }
-}
-
-HttpCache::init();
-
-define('NO_CLI', \PHP_SAPI !== 'cli');
+}

+ 48 - 0
Protocols/Http/Chunk.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http;
+
+
+/**
+ * Class Chunk
+ * @package Workerman\Protocols\Http
+ */
+class Chunk
+{
+    /**
+     * Chunk buffer.
+     *
+     * @var string
+     */
+    protected $_buffer = null;
+
+    /**
+     * Chunk constructor.
+     * @param $buffer
+     */
+    public function __construct($buffer)
+    {
+        $this->_buffer = $buffer;
+    }
+
+    /**
+     * __toString
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return \dechex(\strlen($this->_buffer))."\r\n$this->_buffer\r\n";
+    }
+}

+ 617 - 0
Protocols/Http/Request.php

@@ -0,0 +1,617 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http;
+
+use Workerman\Connection\TcpConnection;
+use Workerman\Protocols\Http\Session;
+use Workerman\Protocols\Http;
+use Workerman\Worker;
+
+/**
+ * Class Request
+ * @package Workerman\Protocols\Http
+ */
+class Request
+{
+    /**
+     * Connection.
+     *
+     * @var TcpConnection
+     */
+    public $connection = null;
+
+    /**
+     * Session instance.
+     *
+     * @var Session
+     */
+    public $session = null;
+
+    /**
+     * Properties.
+     *
+     * @var array
+     */
+    public $properties = array();
+
+    /**
+     * Http buffer.
+     *
+     * @var string
+     */
+    protected $_buffer = null;
+
+    /**
+     * Request data.
+     *
+     * @var array
+     */
+    protected $_data = null;
+
+    /**
+     * Header cache.
+     *
+     * @var array
+     */
+    protected static $_headerCache = array();
+
+    /**
+     * Get cache.
+     *
+     * @var array
+     */
+    protected static $_getCache = array();
+
+    /**
+     * Post cache.
+     *
+     * @var array
+     */
+    protected static $_postCache = array();
+
+    /**
+     * Enable cache.
+     *
+     * @var bool
+     */
+    protected static $_enableCache = true;
+
+
+    /**
+     * Request constructor.
+     *
+     * @param $buffer
+     */
+    public function __construct($buffer)
+    {
+        $this->_buffer = $buffer;
+    }
+
+    /**
+     * $_GET.
+     *
+     * @param null $name
+     * @param null $default
+     * @return mixed|null
+     */
+    public function get($name = null, $default = null)
+    {
+        if (!isset($this->_data['get'])) {
+            $this->parseGet();
+        }
+        if (null === $name) {
+            return $this->_data['get'];
+        }
+        return isset($this->_data['get'][$name]) ? $this->_data['get'][$name] : $default;
+    }
+
+    /**
+     * $_POST.
+     *
+     * @param $name
+     * @param null $default
+     * @return mixed|null
+     */
+    public function post($name = null, $default = null)
+    {
+        if (!isset($this->_data['post'])) {
+            $this->parsePost();
+        }
+        if (null === $name) {
+            return $this->_data['post'];
+        }
+        return isset($this->_data['post'][$name]) ? $this->_data['post'][$name] : $default;
+    }
+
+    /**
+     * Get header item by name.
+     *
+     * @param null $name
+     * @param null $default
+     * @return string|null
+     */
+    public function header($name = null, $default = null)
+    {
+        if (!isset($this->_data['headers'])) {
+            $this->parseHeaders();
+        }
+        if (null === $name) {
+            return $this->_data['headers'];
+        }
+        $name = \strtolower($name);
+        return isset($this->_data['headers'][$name]) ? $this->_data['headers'][$name] : $default;
+    }
+
+    /**
+     * Get cookie item by name.
+     *
+     * @param null $name
+     * @param null $default
+     * @return string|null
+     */
+    public function cookie($name = null, $default = null)
+    {
+        if (!isset($this->_data['cookie'])) {
+            \parse_str(\str_replace('; ', '&', $this->header('cookie')), $this->_data['cookie']);
+        }
+        if ($name === null) {
+            return $this->_data['cookie'];
+        }
+        return isset($this->_data['cookie'][$name]) ? $this->_data['cookie'][$name] : $default;
+    }
+
+    /**
+     * Get upload files.
+     *
+     * @param null $name
+     * @return array|null
+     */
+    public function file($name = null)
+    {
+        if (!isset($this->_data['files'])) {
+            $this->parsePost();
+        }
+        if (null === $name) {
+            return $this->_data['files'];
+        }
+        return isset($this->_data['files'][$name]) ? $this->_data['files'][$name] : null;
+    }
+
+    /**
+     * Get method.
+     *
+     * @return string
+     */
+    public function method()
+    {
+        if (!isset($this->_data['method'])) {
+            $this->parseHeadFirstLine();
+        }
+        return $this->_data['method'];
+    }
+
+    /**
+     * Get http protocol version.
+     *
+     * @return string.
+     */
+    public function protocolVersion()
+    {
+        if (!isset($this->_data['protocolVersion'])) {
+            $this->parseProtocolVersion();
+        }
+        return $this->_data['protocolVersion'];
+    }
+
+    /**
+     * Get host.
+     *
+     * @param bool $without_port
+     * @return string
+     */
+    public function host($without_port = false)
+    {
+        $host = $this->header('host');
+        if ($without_port && $pos = \strpos($host, ':')) {
+            return \substr($host, 0, $pos);
+        }
+        return $host;
+    }
+
+    /**
+     * Get uri.
+     *
+     * @return mixed
+     */
+    public function uri()
+    {
+        if (!isset($this->_data['uri'])) {
+            $this->parseHeadFirstLine();
+        }
+        return $this->_data['uri'];
+    }
+
+    /**
+     * Get path.
+     *
+     * @return mixed
+     */
+    public function path()
+    {
+        if (!isset($this->_data['path'])) {
+            $this->_data['path'] = \parse_url($this->uri(), PHP_URL_PATH);
+        }
+        return $this->_data['path'];
+    }
+
+    /**
+     * Get query string.
+     *
+     * @return mixed
+     */
+    public function queryString()
+    {
+        if (!isset($this->_data['query_string'])) {
+            $this->_data['query_string'] = \parse_url($this->uri(), PHP_URL_QUERY);
+        }
+        return $this->_data['query_string'];
+    }
+
+    /**
+     * Get session.
+     *
+     * @return bool|\Workerman\Protocols\Http\Session
+     */
+    public function session()
+    {
+        if ($this->session === null) {
+            $session_id = $this->sessionId();
+            if ($session_id === false) {
+                return false;
+            }
+            $this->session = new Session($session_id);
+        }
+        return $this->session;
+    }
+
+    /**
+     * Get session id.
+     *
+     * @return bool|mixed
+     */
+    public function sessionId()
+    {
+        if (!isset($this->_data['sid'])) {
+            $session_name = Http::sessionName();
+            $sid = $this->cookie($session_name);
+            if ($sid === '' || $sid === null) {
+                if ($this->connection === null) {
+                    Worker::safeEcho('Request->session() fail, header already send');
+                    return false;
+                }
+                $sid = static::createSessionId();
+                $cookie_params = \session_get_cookie_params();
+                $this->connection->__header['Set-Cookie'] = array($session_name . '=' . $sid
+                    . (empty($cookie_params['domain']) ? '' : '; Domain=' . $cookie_params['domain'])
+                    . (empty($cookie_params['lifetime']) ? '' : '; Max-Age=' . ($cookie_params['lifetime'] + \time()))
+                    . (empty($cookie_params['path']) ? '' : '; Path=' . $cookie_params['path'])
+                    . (!$cookie_params['secure'] ? '' : '; Secure')
+                    . (!$cookie_params['httponly'] ? '' : '; HttpOnly'));
+            }
+            $this->_data['sid'] = $sid;
+        }
+        return $this->_data['sid'];
+    }
+
+    /**
+     * Get http raw head.
+     *
+     * @return string
+     */
+    public function rawHead()
+    {
+        if (!isset($this->_data['head'])) {
+            $this->_data['head'] = \strstr($this->_buffer, "\r\n\r\n", true);
+        }
+        return $this->_data['head'];
+    }
+
+    /**
+     * Get http raw body.
+     *
+     * @return string
+     */
+    public function rawBody()
+    {
+        return \substr($this->_buffer, \strpos($this->_buffer, "\r\n\r\n") + 4);
+    }
+
+    /**
+     * Get raw buffer.
+     *
+     * @return string
+     */
+    public function rawBuffer()
+    {
+        return $this->_buffer;
+    }
+
+    /**
+     * Enable or disable cache.
+     *
+     * @param $value
+     */
+    public static function enableCache($value)
+    {
+        static::$_enableCache = (bool)$value;
+    }
+
+    /**
+     * Parse first line of http header buffer.
+     *
+     * @return void
+     */
+    protected function parseHeadFirstLine()
+    {
+        $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] : '/';
+    }
+
+    /**
+     * Parse protocol version.
+     *
+     * @return void
+     */
+    protected function parseProtocolVersion()
+    {
+        $first_line = \strstr($this->_buffer, "\r\n", true);
+        $protoco_version = substr(\strstr($first_line, 'HTTP/'), 5);
+        $this->_data['protocolVersion'] = $protoco_version ? $protoco_version : '1.0';
+    }
+
+    /**
+     * Parse headers.
+     *
+     * @return void
+     */
+    protected function parseHeaders()
+    {
+        $this->_data['headers'] = array();
+        $raw_head = $this->rawHead();
+        $head_buffer = \substr($raw_head, \strpos($raw_head, "\r\n") + 2);
+        $cacheable = static::$_enableCache && \strlen($head_buffer) < 2048;
+        if ($cacheable && isset(static::$_headerCache[$head_buffer])) {
+            $this->_data['headers'] = static::$_headerCache[$head_buffer];
+            return;
+        }
+        $head_data = \explode("\r\n", $head_buffer);
+        foreach ($head_data as $content) {
+            if (false !== \strpos($content, ':')) {
+                list($key, $value) = \explode(':', $content, 2);
+                $this->_data['headers'][\strtolower($key)] = \ltrim($value);
+            } else {
+                $this->_data['headers'][\strtolower($content)] = '';
+            }
+        }
+        if ($cacheable) {
+            static::$_headerCache[$head_buffer] = $this->_data['headers'];
+            if (\count(static::$_headerCache) > 128) {
+                unset(static::$_headerCache[key(static::$_headerCache)]);
+            }
+        }
+    }
+
+    /**
+     * Parse head.
+     *
+     * @return void
+     */
+    protected function parseGet()
+    {
+        $query_string = $this->queryString();
+        $this->_data['get'] = array();
+        if ($query_string === '') {
+            return;
+        }
+        $cacheable = static::$_enableCache && \strlen($query_string) < 1024;
+        if ($cacheable && isset(static::$_getCache[$query_string])) {
+            $this->_data['get'] = static::$_getCache[$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)]);
+            }
+        }
+    }
+
+    /**
+     * Parse post.
+     *
+     * @return void
+     */
+    public function parsePost()
+    {
+        $body_buffer = $this->rawBody();
+        $this->_data['post'] = $this->_data['files'] = array();
+        if ($body_buffer === '') {
+            return;
+        }
+        $cacheable = static::$_enableCache && \strlen($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 ($content_type !== null && \preg_match('/boundary="?(\S+)"?/', $content_type, $match)) {
+            $http_post_boundary = '--' . $match[1];
+            $this->parseUploadFiles($http_post_boundary);
+            return;
+        }
+        \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)]);
+            }
+        }
+    }
+
+    /**
+     * Parse upload files.
+     *
+     * @param $http_post_boundary
+     * @return void
+     */
+    protected function parseUploadFiles($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] === '') {
+            unset($boundary_data_array[0]);
+        }
+        $key = -1;
+        $files = array();
+        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);
+            $key++;
+            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 {
+                                $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;
+                                }
+                            }
+                            // Parse upload files.
+                            $files[$key] = array(
+                                'key' => $match[1],
+                                'name' => $match[2],
+                                'tmp_name' => $tmp_file,
+                                'size' => $size,
+                                'error' => $error
+                            );
+                            break;
+                        } // Is post field.
+                        else {
+                            // Parse $_POST.
+                            if (\preg_match('/name="(.*?)"$/', $header_value, $match)) {
+                                $this->_data['post'][$match[1]] = $boundary_value;
+                            }
+                        }
+                        break;
+                    case "content-type":
+                        // add file_type
+                        $files[$key]['type'] = \trim($header_value);
+                        break;
+                }
+            }
+        }
+
+        foreach ($files as $file) {
+            $key = $file['key'];
+            unset($file['key']);
+            $this->_data['files'][$key] = $file;
+        }
+    }
+
+    /**
+     * Create session id.
+     *
+     * @return string
+     */
+    protected static function createSessionId()
+    {
+        return \bin2hex(\pack('d', \microtime(true)) . \pack('N', \mt_rand()));
+    }
+
+    /**
+     * Setter.
+     *
+     * @param $name
+     * @param $value
+     * @return void
+     */
+    public function __set($name, $value)
+    {
+        $this->properties[$name] = $value;
+    }
+
+    /**
+     * Getter.
+     *
+     * @param $name
+     * @return mixed|null
+     */
+    public function __get($name)
+    {
+        return isset($this->properties[$name]) ? $this->properties[$name] : null;
+    }
+
+    /**
+     * Isset.
+     *
+     * @param $name
+     * @return bool
+     */
+    public function __isset($name)
+    {
+        return isset($this->properties[$name]);
+    }
+
+    /**
+     * Unset.
+     *
+     * @param $name
+     * @return void
+     */
+    public function __unset($name)
+    {
+        unset($this->properties[$name]);
+    }
+
+    /**
+     * __destruct.
+     *
+     * @return void
+     */
+    public function __destruct()
+    {
+        if (isset($this->_data['files'])) {
+            foreach ($this->_data['files'] as $item) {
+                if (\is_file($item['tmp_name'])) {
+                    \unlink($item['tmp_name']);
+                }
+            }
+        }
+    }
+}

+ 375 - 0
Protocols/Http/Response.php

@@ -0,0 +1,375 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http;
+
+/**
+ * Class Response
+ * @package Workerman\Protocols\Http
+ */
+class Response
+{
+    /**
+     * Header data.
+     *
+     * @var array
+     */
+    protected $_header = null;
+
+    /**
+     * Http status.
+     *
+     * @var int
+     */
+    protected $_status = null;
+
+    /**
+     * Http reason.
+     *
+     * @var string
+     */
+    protected $_reason = null;
+
+    /**
+     * Http version.
+     *
+     * @var string
+     */
+    protected $_version = '1.1';
+
+    /**
+     * Http body.
+     *
+     * @var string
+     */
+    protected $_body = null;
+
+    /**
+     * Mine type map.
+     * @var array
+     */
+    protected static $_mimeTypeMap = null;
+
+    /**
+     * Phrases.
+     *
+     * @var array
+     */
+    protected static $_phrases = array(
+        100 => 'Continue',
+        101 => 'Switching Protocols',
+        102 => 'Processing',
+        200 => 'OK',
+        201 => 'Created',
+        202 => 'Accepted',
+        203 => 'Non-Authoritative Information',
+        204 => 'No Content',
+        205 => 'Reset Content',
+        206 => 'Partial Content',
+        207 => 'Multi-status',
+        208 => 'Already Reported',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        305 => 'Use Proxy',
+        306 => 'Switch Proxy',
+        307 => 'Temporary Redirect',
+        400 => 'Bad Request',
+        401 => 'Unauthorized',
+        402 => 'Payment Required',
+        403 => 'Forbidden',
+        404 => 'Not Found',
+        405 => 'Method Not Allowed',
+        406 => 'Not Acceptable',
+        407 => 'Proxy Authentication Required',
+        408 => 'Request Time-out',
+        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',
+        417 => 'Expectation Failed',
+        418 => 'I\'m a teapot',
+        422 => 'Unprocessable Entity',
+        423 => 'Locked',
+        424 => 'Failed Dependency',
+        425 => 'Unordered Collection',
+        426 => 'Upgrade Required',
+        428 => 'Precondition Required',
+        429 => 'Too Many Requests',
+        431 => 'Request Header Fields Too Large',
+        451 => 'Unavailable For Legal Reasons',
+        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',
+    );
+
+    /**
+     * Init.
+     *
+     * @return void
+     */
+    public static function init() {
+        static::initMimeTypeMap();
+    }
+
+    /**
+     * Response constructor.
+     *
+     * @param int $status
+     * @param array $headers
+     * @param string $body
+     */
+    public function __construct(
+        $status = 200,
+        array $headers = array(),
+        $body = ''
+    ) {
+        $this->_status = $status;
+        $this->_header = $headers;
+        $this->_body = $body;
+    }
+
+    /**
+     * Set header.
+     *
+     * @param $name
+     * @param $value
+     * @return $this
+     */
+    public function header($name, $value) {
+        $this->_header[$name] = $value;
+        return $this;
+    }
+
+    /**
+     * Set headers.
+     *
+     * @param $headers
+     * @return $this
+     */
+    public function withHeaders($headers) {
+        $this->_header = \array_merge($this->_header, $headers);
+        return $this;
+    }
+
+    /**
+     * Set status.
+     *
+     * @param $code
+     * @param null $reason_phrase
+     * @return $this
+     */
+    public function withStatus($code, $reason_phrase = null) {
+        $this->_status = $code;
+        $this->_reason = $reason_phrase;
+        return $this;
+    }
+
+    /**
+     * Set protocol version.
+     *
+     * @param $version
+     * @return $this
+     */
+    public function withProtocolVersion($version) {
+        $this->_version = $version;
+        return $this;
+    }
+
+    /**
+     * Set http body.
+     *
+     * @param $body
+     * @return $this
+     */
+    public function withBody($body) {
+        $this->_body = $body;
+        return $this;
+    }
+
+    /**
+     * Set file.
+     *
+     * @param $file
+     * @return $this
+     */
+    public function withFile($file) {
+        if (!\is_file($file)) {
+            return $this->withStatus(404)->withBody('<h3>404 Not Found</h3>');
+        }
+        $this->file = $file;
+        return $this;
+    }
+
+    /**
+     * Set cookie.
+     *
+     * @param $name
+     * @param string $value
+     * @param int $maxage
+     * @param string $path
+     * @param string $domain
+     * @param bool $secure
+     * @param bool $http_only
+     * @return $this
+     */
+    public function cookie($name, $value = '', $max_age = 0, $path = '', $domain = '', $secure = false, $http_only = false)
+    {
+        $this->_header['Set-Cookie'][] = $name . '=' . \rawurlencode($value)
+            . (empty($domain) ? '' : '; Domain=' . $domain)
+            . (empty($max_age) ? '' : '; Max-Age=' . $max_age)
+            . (empty($path) ? '' : '; Path=' . $path)
+            . (!$secure ? '' : '; Secure')
+            . (!$http_only ? '' : '; HttpOnly');
+        return $this;
+    }
+
+    /**
+     * Create header for file.
+     *
+     * @param $file
+     * @return string
+     */
+    protected function createHeadForFile($file)
+    {
+        $reason = $this->_reason ? $this->_reason : static::$_phrases[$this->_status];
+        $head = "HTTP/{$this->_version} {$this->_status} $reason\r\n";
+        $headers = $this->_header;
+        if (!isset($headers['Server'])) {
+            $head .= "Server: workerman\r\n";
+        }
+        foreach ($headers as $name => $value) {
+            if (\is_array($value)) {
+                foreach ($value as $item) {
+                    $head .= "$name: $item\r\n";
+                }
+                continue;
+            }
+            $head .= "$name: $value\r\n";
+        }
+
+        if (!isset($headers['Connection'])) {
+            $head .= "Connection: keep-alive\r\n";
+        }
+
+        $file_info = \pathinfo($file);
+        $extension = isset($file_info['extension']) ? $file_info['extension'] : '';
+        $base_name = isset($file_info['basename']) ? $file_info['basename'] : 'unknown';
+        if (!isset($headers['Content-Type'])) {
+            if (isset(self::$_mimeTypeMap[$extension])) {
+                $head .= "Content-Type: " . self::$_mimeTypeMap[$extension] . "\r\n";
+            } else {
+                $head .= "Content-Type: application/octet-stream\r\n";
+            }
+        }
+
+        if (!isset($headers['Content-Disposition']) && !isset(self::$_mimeTypeMap[$extension])) {
+            $head .= "Content-Disposition: attachment; filename=\"$base_name\"\r\n";
+        }
+
+        if (!isset($headers['Last-Modified'])) {
+            if ($mtime = \filemtime($file)) {
+                $head .= 'Last-Modified: '.\date('D, d M Y H:i:s', $mtime) . ' ' . \date_default_timezone_get() ."\r\n";
+            }
+        }
+
+        return "{$head}\r\n";
+    }
+
+    /**
+     * __toString.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        if (isset($this->file)) {
+            return $this->createHeadForFile($this->file);
+        }
+
+        $reason = $this->_reason ? $this->_reason : static::$_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}";
+        }
+
+        $head = "HTTP/{$this->_version} {$this->_status} $reason\r\n";
+        $headers = $this->_header;
+        if (!isset($headers['Server'])) {
+            $head .= "Server: workerman\r\n";
+        }
+        foreach ($headers as $name => $value) {
+            if (\is_array($value)) {
+                foreach ($value as $item) {
+                    $head .= "$name: $item\r\n";
+                }
+                continue;
+            }
+            $head .= "$name: $value\r\n";
+        }
+
+        if (!isset($headers['Connection'])) {
+            $head .= "Connection: keep-alive\r\n";
+        }
+
+        if (!isset($headers['Content-Type'])) {
+            $head .= "Content-Type: text/html;charset=utf-8\r\n";
+        } else if ($headers['Content-Type'] === 'text/event-stream') {
+            return $head . $this->_body;
+        }
+
+        if (!isset($headers['Transfer-Encoding'])) {
+            $head .= "Content-Length: $body_len\r\n\r\n";
+        } else {
+            return "$head\r\n".dechex($body_len)."\r\n{$this->_body}\r\n";
+        }
+
+        // The whole http package
+        return $head . $this->_body;
+    }
+
+    /**
+     * Init mime map.
+     *
+     * @return void
+     */
+    public static function initMimeTypeMap()
+    {
+        $mime_file = __DIR__ . '/mime.types';
+        $items = \file($mime_file, \FILE_IGNORE_NEW_LINES | \FILE_SKIP_EMPTY_LINES);
+        foreach ($items as $content) {
+            if (\preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) {
+                $mime_type       = $match[1];
+                $extension_var   = $match[2];
+                $extension_array = \explode(' ', \substr($extension_var, 0, -1));
+                foreach ($extension_array as $file_extension) {
+                    static::$_mimeTypeMap[$file_extension] = $mime_type;
+                }
+            }
+        }
+    }
+}
+Response::init();

+ 64 - 0
Protocols/Http/ServerSentEvents.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http;
+
+/**
+ * Class ServerSentEvents
+ * @package Workerman\Protocols\Http
+ */
+class ServerSentEvents
+{
+    /**
+     * Data.
+     * @var array
+     */
+    protected $_data = null;
+
+    /**
+     * ServerSentEvents constructor.
+     * $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000]
+     * @param array $data
+     */
+    public function __construct(array $data)
+    {
+        $this->_data = $data;
+    }
+
+    /**
+     * __toString.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        $buffer = '';
+        $data = $this->_data;
+        if (isset($data[''])) {
+            $buffer = ": {$data['']}\n";
+        }
+        if (isset($data['event'])) {
+            $buffer .= "event: {$data['event']}\n";
+        }
+        if (isset($data['data'])) {
+            $buffer .= 'data: ' . \str_replace("\n", "\ndata: ", $data['data']) . "\n\n";
+        }
+        if (isset($data['id'])) {
+            $buffer .= "id: {$data['id']}\n";
+        }
+        if (isset($data['retry'])) {
+            $buffer .= "retry: {$data['retry']}\n";
+        }
+        return $buffer;
+    }
+}

+ 359 - 0
Protocols/Http/Session.php

@@ -0,0 +1,359 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http;
+
+
+/**
+ * Class Session
+ * @package Workerman\Protocols\Http
+ */
+class Session
+{
+    /**
+     * Session andler class which implements SessionHandlerInterface.
+     *
+     * @var string
+     */
+    protected static $_handlerClass = 'Workerman\Protocols\Http\Session\FileSessionHandler';
+
+    /**
+     * Parameters of __constructor for session handler class.
+     *
+     * @var null
+     */
+    protected static $_handlerConfig = null;
+
+    /**
+     * Session.gc_probability
+     *
+     * @var int
+     */
+    protected static $_sessionGcProbability = 1;
+
+    /**
+     * Session.gc_divisor
+     *
+     * @var int
+     */
+    protected static $_sessionGcDivisor = 1000;
+
+    /**
+     * Session.gc_maxlifetime
+     *
+     * @var int
+     */
+    protected static $_sessionGcMaxLifeTime = 1440;
+
+    /**
+     * Session handler instance.
+     *
+     * @var \SessionHandlerInterface
+     */
+    protected static $_handler = null;
+
+    /**
+     * Session data.
+     *
+     * @var array
+     */
+    protected $_data = array();
+
+    /**
+     * Session changed and need to save.
+     *
+     * @var bool
+     */
+    protected $_needSave = false;
+
+    /**
+     * Session id.
+     *
+     * @var null
+     */
+    protected $_sessionId = null;
+
+    /**
+     * Session constructor.
+     *
+     * @param $session_id
+     */
+    public function __construct($session_id)
+    {
+        static::checkSessionId($session_id);
+        if (static::$_handler === null) {
+            static::initHandler();
+        }
+        $this->_sessionId = $session_id;
+        if ($data = static::$_handler->read($session_id)) {
+            $this->_data = \unserialize($data);
+        }
+    }
+
+    /**
+     * Get session id.
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->_sessionId;
+    }
+
+    /**
+     * Get session.
+     *
+     * @param $name
+     * @param null $default
+     * @return mixed|null
+     */
+    public function get($name, $default = null)
+    {
+        return isset($this->_data[$name]) ? $this->_data[$name] : $default;
+    }
+
+    /**
+     * Store data in the session.
+     *
+     * @param $name
+     * @param $value
+     */
+    public function set($name, $value)
+    {
+        $this->_data[$name] = $value;
+        $this->_needSave = true;
+    }
+
+    /**
+     * Delete an item from the session.
+     *
+     * @param $name
+     */
+    public function delete($name)
+    {
+        unset($this->_data[$name]);
+        $this->_needSave = true;
+    }
+
+    /**
+     * Retrieve and delete an item from the session.
+     *
+     * @param $name
+     * @param null $default
+     * @return mixed|null
+     */
+    public function pull($name, $default = null)
+    {
+        $value = $this->get($name, $default);
+        $this->delete($name);
+        return $value;
+    }
+
+    /**
+     * Store data in the session.
+     *
+     * @param $key
+     * @param null $value
+     */
+    public function put($key, $value = null)
+    {
+        if (!\is_array($key)) {
+            $this->set($key, $value);
+            return;
+        }
+
+        foreach ($key as $k => $v) {
+            $this->_data[$k] = $v;
+        }
+        $this->_needSave = true;
+    }
+
+    /**
+     * Remove a piece of data from the session.
+     *
+     * @param $name
+     */
+    public function forget($name)
+    {
+        if (\is_scalar($name)) {
+            $this->delete($name);
+            return;
+        }
+        if (\is_array($name)) {
+            foreach ($name as $key) {
+                unset($this->_data[$key]);
+            }
+        }
+        $this->_needSave = true;
+    }
+
+    /**
+     * Retrieve all the data in the session.
+     *
+     * @return array
+     */
+    public function all()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * Remove all data from the session.
+     *
+     * @return void
+     */
+    public function flush()
+    {
+        $this->_needSave = true;
+        $this->_data = array();
+    }
+
+    /**
+     * Determining If An Item Exists In The Session.
+     *
+     * @param $name
+     * @return bool
+     */
+    public function has($name)
+    {
+        return isset($this->_data[$name]);
+    }
+
+    /**
+     * To determine if an item is present in the session, even if its value is null.
+     *
+     * @param $name
+     * @return bool
+     */
+    public function exists($name)
+    {
+        return \array_key_exists($name, $this->_data);
+    }
+
+    /**
+     * Save session to store.
+     *
+     * @return void
+     */
+    public function save()
+    {
+        if ($this->_needSave) {
+            if (empty($this->_data)) {
+                static::$_handler->destroy($this->_sessionId);
+            } else {
+                static::$_handler->write($this->_sessionId, \serialize($this->_data));
+            }
+        }
+        $this->_needSave = false;
+    }
+
+    /**
+     * Init.
+     *
+     * @return void
+     */
+    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_max_life_time = \ini_get('session.gc_maxlifetime')) {
+            self::$_sessionGcMaxLifeTime = (int)$gc_max_life_time;
+        }
+    }
+
+    /**
+     * Set session handler class.
+     *
+     * @param null $class_name
+     * @param null $config
+     * @return string
+     */
+    public static function handlerClass($class_name = null, $config = null)
+    {
+        if ($class_name) {
+            static::$_handlerClass = $class_name;
+        }
+        if ($config) {
+            static::$_handlerConfig = $config;
+        }
+        return static::$_handlerClass;
+    }
+
+    /**
+     * Init handler.
+     *
+     * @return void
+     */
+    protected static function initHandler()
+    {
+        if (static::$_handlerConfig === null) {
+            static::$_handler = new static::$_handlerClass();
+        } else {
+            static::$_handler = new static::$_handlerClass(static::$_handlerConfig);
+        }
+    }
+
+    /**
+     * Try GC sessions.
+     *
+     * @return void
+     */
+    public function tryGcSessions()
+    {
+        if (\rand(1, static::$_sessionGcDivisor) > static::$_sessionGcProbability) {
+            return;
+        }
+        static::$_handler->gc(static::$_sessionGcMaxLifeTime);
+    }
+
+    /**
+     * __destruct.
+     *
+     * @return void
+     */
+    public function __destruct()
+    {
+        $this->save();
+        $this->tryGcSessions();
+    }
+
+    /**
+     * Check session id.
+     *
+     * @param $session_id
+     */
+    protected static function checkSessionId($session_id)
+    {
+        if (!\preg_match('/^[a-zA-Z0-9]+$/', $session_id)) {
+            throw new SessionException("session_id $session_id is invalid");
+        }
+    }
+}
+
+/**
+ * Class SessionException
+ * @package Workerman\Protocols\Http
+ */
+class SessionException extends \RuntimeException
+{
+
+}
+
+// Init session.
+Session::init();

+ 172 - 0
Protocols/Http/Session/FileSessionHandler.php

@@ -0,0 +1,172 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman\Protocols\Http\Session;
+
+/**
+ * Class FileSessionHandler
+ * @package Workerman\Protocols\Http\Session
+ */
+class FileSessionHandler implements \SessionHandlerInterface
+{
+    /**
+     * Session save path.
+     *
+     * @var string
+     */
+    protected static $_sessionSavePath = null;
+
+    /**
+     * Session file prefix.
+     *
+     * @var string
+     */
+    protected static $_sessionFilePrefix = 'session_';
+
+    /**
+     * Init.
+     */
+    public static function init() {
+        $save_path = @\session_save_path();
+        if (!$save_path || \strpos($save_path, 'tcp://') === 0) {
+            $save_path = \sys_get_temp_dir();
+        }
+        static::sessionSavePath($save_path);
+    }
+
+    /**
+     * FileSessionHandler constructor.
+     * @param array $config
+     */
+    public function __construct($config = array()) {
+        if (isset($config['save_path'])) {
+            static::sessionSavePath($config['save_path']);
+        }
+    }
+
+    /**
+     * Nothing.
+     *
+     * @param string $save_path
+     * @param string $name
+     * @return bool
+     */
+    public function open($save_path, $name)
+    {
+        return true;
+    }
+
+    /**
+     * Reads the session data from the session storage.
+     *
+     * @param string $session_id
+     * @return string
+     */
+    public function read($session_id)
+    {
+        $session_file = static::sessionFile($session_id);
+        \clearstatcache();
+        if (\is_file($session_file)) {
+            $data = \file_get_contents($session_file);
+            return $data ? $data : '';
+        }
+        return '';
+    }
+
+    /**
+     * Writes the session data to the session storage.
+     *
+     * @param string $session_id
+     * @param string $session_data
+     * @return bool
+     */
+    public function write($session_id, $session_data)
+    {
+        $temp_file = static::$_sessionSavePath.uniqid(mt_rand(), true);
+        if (!\file_put_contents($temp_file, $session_data)) {
+            return false;
+        }
+        return \rename($temp_file, static::sessionFile($session_id));
+    }
+
+    /**
+     * Nothing.
+     *
+     * @return bool
+     */
+    public function close()
+    {
+        return true;
+    }
+
+    /**
+     * Destroys a session.
+     *
+     * @param string $session_id
+     * @return bool
+     */
+    public function destroy($session_id)
+    {
+        $session_file = static::sessionFile($session_id);
+        if (\is_file($session_file)) {
+            \unlink($session_file);
+        }
+        return true;
+    }
+
+    /**
+     * Cleanup old sessions.
+     *
+     * @param int $maxlifetime
+     * @return void
+     */
+    public function gc($maxlifetime) {
+        $time_now = \time();
+        foreach (\glob(static::$_sessionSavePath . static::$_sessionFilePrefix . '*') as $file) {
+            if(\is_file($file) && $time_now - \filemtime($file) > $maxlifetime) {
+                \unlink($file);
+            }
+        }
+    }
+
+    /**
+     * Get session file path.
+     *
+     * @param $session_id
+     * @return string
+     */
+    protected static function sessionFile($session_id) {
+        return static::$_sessionSavePath.static::$_sessionFilePrefix.$session_id;
+    }
+
+    /**
+     * Get or set session file path.
+     *
+     * @param $path
+     * @return string
+     */
+    public static function sessionSavePath($path) {
+        if ($path) {
+            if ($path[\strlen($path)-1] !== DIRECTORY_SEPARATOR) {
+                $path .= DIRECTORY_SEPARATOR;
+            }
+            static::$_sessionSavePath = $path;
+            if (!\is_dir($path)) {
+                \mkdir($path, 0777, true);
+            }
+        }
+        return $path;
+    }
+}
+
+FileSessionHandler::init();

+ 1 - 1
Protocols/Websocket.php

@@ -339,7 +339,7 @@ class Websocket implements \Workerman\Protocols\ProtocolInterface
      * @param TcpConnection $connection
      * @return int
      */
-    protected static function dealHandshake($buffer, TcpConnection $connection)
+    public static function dealHandshake($buffer, TcpConnection $connection)
     {
         // HTTP protocol.
         if (0 === \strpos($buffer, 'GET')) {

+ 182 - 0
Timer.php

@@ -0,0 +1,182 @@
+<?php
+/**
+ * This file is part of workerman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Workerman;
+
+use Workerman\Events\EventInterface;
+use Workerman\Worker;
+use \Exception;
+
+/**
+ * Timer.
+ *
+ * example:
+ * Workerman\Timer::add($time_interval, callback, array($arg1, $arg2..));
+ */
+class Timer
+{
+    /**
+     * Tasks that based on ALARM signal.
+     * [
+     *   run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
+     *   run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
+     *   ..
+     * ]
+     *
+     * @var array
+     */
+    protected static $_tasks = array();
+
+    /**
+     * event
+     *
+     * @var EventInterface
+     */
+    protected static $_event = null;
+
+    /**
+     * Init.
+     *
+     * @param EventInterface $event
+     * @return void
+     */
+    public static function init($event = null)
+    {
+        if ($event) {
+            self::$_event = $event;
+            return;
+        }
+        if (\function_exists('pcntl_signal')) {
+            \pcntl_signal(\SIGALRM, array('\Workerman\Lib\Timer', 'signalHandle'), false);
+        }
+    }
+
+    /**
+     * ALARM signal handler.
+     *
+     * @return void
+     */
+    public static function signalHandle()
+    {
+        if (!self::$_event) {
+            \pcntl_alarm(1);
+            self::tick();
+        }
+    }
+
+    /**
+     * Add a timer.
+     *
+     * @param float    $time_interval
+     * @param callable $func
+     * @param mixed    $args
+     * @param bool     $persistent
+     * @return int|false
+     */
+    public static function add($time_interval, $func, $args = array(), $persistent = true)
+    {
+        if ($time_interval <= 0) {
+            Worker::safeEcho(new Exception("bad time_interval"));
+            return false;
+        }
+
+        if ($args === null) {
+            $args = array();
+        }
+
+        if (self::$_event) {
+            return self::$_event->add($time_interval,
+                $persistent ? EventInterface::EV_TIMER : EventInterface::EV_TIMER_ONCE, $func, $args);
+        }
+
+        if (!\is_callable($func)) {
+            Worker::safeEcho(new Exception("not callable"));
+            return false;
+        }
+
+        if (empty(self::$_tasks)) {
+            \pcntl_alarm(1);
+        }
+
+        $run_time = \time() + $time_interval;
+        if (!isset(self::$_tasks[$run_time])) {
+            self::$_tasks[$run_time] = array();
+        }
+        self::$_tasks[$run_time][] = array($func, (array)$args, $persistent, $time_interval);
+        return 1;
+    }
+
+
+    /**
+     * Tick.
+     *
+     * @return void
+     */
+    public static function tick()
+    {
+        if (empty(self::$_tasks)) {
+            \pcntl_alarm(0);
+            return;
+        }
+
+        $time_now = \time();
+        foreach (self::$_tasks as $run_time => $task_data) {
+            if ($time_now >= $run_time) {
+                foreach ($task_data as $index => $one_task) {
+                    $task_func     = $one_task[0];
+                    $task_args     = $one_task[1];
+                    $persistent    = $one_task[2];
+                    $time_interval = $one_task[3];
+                    try {
+                        \call_user_func_array($task_func, $task_args);
+                    } catch (\Exception $e) {
+                        Worker::safeEcho($e);
+                    }
+                    if ($persistent) {
+                        self::add($time_interval, $task_func, $task_args);
+                    }
+                }
+                unset(self::$_tasks[$run_time]);
+            }
+        }
+    }
+
+    /**
+     * Remove a timer.
+     *
+     * @param mixed $timer_id
+     * @return bool
+     */
+    public static function del($timer_id)
+    {
+        if (self::$_event) {
+            return self::$_event->del($timer_id, EventInterface::EV_TIMER);
+        }
+
+        return false;
+    }
+
+    /**
+     * Remove all timers.
+     *
+     * @return void
+     */
+    public static function delAll()
+    {
+        self::$_tasks = array();
+        \pcntl_alarm(0);
+        if (self::$_event) {
+            self::$_event->clearAllTimer();
+        }
+    }
+}

+ 1 - 1
Worker.php

@@ -33,7 +33,7 @@ class Worker
      *
      * @var string
      */
-    const VERSION = '3.5.29';
+    const VERSION = '4.0.0';
 
     /**
      * Status starting.