commit 327557c28ed918e5e2862df072cabe5d07c04837 Author: sa0ChunLuyu Date: Thu Apr 28 20:26:15 2022 +0800 no message diff --git a/bot/bot_yo.php b/bot/bot_yo.php new file mode 100755 index 0000000..2c39cb9 --- /dev/null +++ b/bot/bot_yo.php @@ -0,0 +1,22 @@ +count = 1; +$bot_dns_loop->name = "2秒后自动触发"; + +function BotFunc() +{ + $res = Tool::post('http://ld.win.sa0.online:81/api/yo', []); + var_dump($res); +} + +$bot_dns_loop->onWorkerStart = function () { + BotFunc(); + Timer::add(2, function () { + BotFunc(); + }); +}; diff --git a/dev/bot_every.php b/dev/bot_every.php new file mode 100755 index 0000000..988631b --- /dev/null +++ b/dev/bot_every.php @@ -0,0 +1,26 @@ +count = 1; +$bot_dns_loop->name = "每10秒自动触发"; +$loop = 10; +$time = time() - $loop; +function BotFunc($loop, &$time) +{ + if (time() - $time >= $loop) { + $res = Tool::post('http://ld.win.sa0.online:81/api/yo', []); + var_dump($res); + $time = $time + $loop; + } +} + +$bot_dns_loop->onWorkerStart = function () use ($loop, &$time) { + BotFunc($loop, $time); + Timer::add(2, function () use ($loop, &$time) { + BotFunc($loop, $time); + }); +}; diff --git a/dev/bot_next.php b/dev/bot_next.php new file mode 100755 index 0000000..2c39cb9 --- /dev/null +++ b/dev/bot_next.php @@ -0,0 +1,22 @@ +count = 1; +$bot_dns_loop->name = "2秒后自动触发"; + +function BotFunc() +{ + $res = Tool::post('http://ld.win.sa0.online:81/api/yo', []); + var_dump($res); +} + +$bot_dns_loop->onWorkerStart = function () { + BotFunc(); + Timer::add(2, function () { + BotFunc(); + }); +}; diff --git a/dev/bot_when.php b/dev/bot_when.php new file mode 100755 index 0000000..6628724 --- /dev/null +++ b/dev/bot_when.php @@ -0,0 +1,26 @@ +count = 1; +$bot_dns_loop->name = "到达规定时间自动触发"; +$loop = 10; +$time = strtotime(date('Y-m-d') . ' 20:21:00'); +function BotFunc($loop, &$time) +{ + if (time() - $time >= 0) { + $res = Tool::post('http://ld.win.sa0.online:81/api/yo', []); + var_dump($res); + $time = $time + $loop; + } +} + +$bot_dns_loop->onWorkerStart = function () use ($loop, &$time) { + BotFunc($loop, $time); + Timer::add(2, function () use ($loop, &$time) { + BotFunc($loop, $time); + }); +}; diff --git a/logs/workerman.log b/logs/workerman.log new file mode 100644 index 0000000..241c088 --- /dev/null +++ b/logs/workerman.log @@ -0,0 +1,64 @@ + +2022-04-28 11:21:31 pid:63537 Workerman[start.php] start in DEBUG mode +2022-04-28 11:21:48 pid:63537 Workerman[start.php] stopping ... +2022-04-28 11:21:48 pid:63537 Workerman[start.php] has been stopped +2022-04-28 11:21:50 pid:63698 Workerman[start.php] start in DEBUG mode +2022-04-28 11:21:54 pid:63698 Workerman[start.php] stopping ... +2022-04-28 11:21:54 pid:63698 Workerman[start.php] has been stopped +2022-04-28 11:22:08 pid:63848 Workerman[start.php] start in DEBUG mode +2022-04-28 11:22:11 pid:63848 Workerman[start.php] stopping ... +2022-04-28 11:22:11 pid:63848 Workerman[start.php] has been stopped +2022-04-28 11:26:04 pid:65785 Workerman[start.php] start in DEBUG mode +2022-04-28 11:26:14 pid:65785 Workerman[start.php] stopping ... +2022-04-28 11:26:14 pid:65785 Workerman[start.php] has been stopped +2022-04-28 11:37:26 pid:71322 Workerman[start.php] start in DEBUG mode +2022-04-28 11:37:29 pid:71322 Workerman[start.php] stopping ... +2022-04-28 11:37:29 pid:71322 Workerman[start.php] has been stopped +2022-04-28 11:40:36 pid:72887 Workerman[start.php] start in DEBUG mode +2022-04-28 11:40:56 pid:72887 Workerman[start.php] stopping ... +2022-04-28 11:40:56 pid:72887 Workerman[start.php] has been stopped +2022-04-28 11:42:20 pid:73744 Workerman[start.php] start in DEBUG mode +2022-04-28 11:42:51 pid:73744 Workerman[start.php] stopping ... +2022-04-28 11:42:51 pid:73744 Workerman[start.php] has been stopped +2022-04-28 11:54:51 pid:79769 Workerman[start.php] start in DEBUG mode +2022-04-28 11:55:01 pid:79769 Workerman[start.php] stopping ... +2022-04-28 11:55:01 pid:79769 Workerman[start.php] has been stopped +2022-04-28 11:55:11 pid:79941 Workerman[start.php] start in DEBUG mode +2022-04-28 11:55:24 pid:79941 Workerman[start.php] stopping ... +2022-04-28 11:55:24 pid:79941 Workerman[start.php] has been stopped +2022-04-28 11:55:43 pid:80213 Workerman[start.php] start in DEBUG mode +2022-04-28 11:55:49 pid:80213 Workerman[start.php] stopping ... +2022-04-28 11:55:49 pid:80213 Workerman[start.php] has been stopped +2022-04-28 19:57:10 pid:80930 Workerman[start.php] start in DEBUG mode +2022-04-28 19:58:18 pid:80930 Workerman[start.php] stopping ... +2022-04-28 19:58:18 pid:80930 Workerman[start.php] has been stopped +2022-04-28 20:00:46 pid:82702 Workerman[start.php] start in DEBUG mode +2022-04-28 20:01:22 pid:82702 Workerman[start.php] stopping ... +2022-04-28 20:01:22 pid:82702 Workerman[start.php] has been stopped +2022-04-28 20:12:18 pid:88327 Workerman[start.php] start in DEBUG mode +2022-04-28 20:12:22 pid:88327 Workerman[start.php] stopping ... +2022-04-28 20:12:22 pid:88327 Workerman[start.php] has been stopped +2022-04-28 20:13:01 pid:88692 Workerman[start.php] start in DEBUG mode +2022-04-28 20:13:03 pid:88692 Workerman[start.php] stopping ... +2022-04-28 20:13:03 pid:88692 Workerman[start.php] has been stopped +2022-04-28 20:17:13 pid:90764 Workerman[start.php] start in DEBUG mode +2022-04-28 20:17:25 pid:90764 Workerman[start.php] stopping ... +2022-04-28 20:17:25 pid:90764 Workerman[start.php] has been stopped +2022-04-28 20:17:33 pid:90941 Workerman[start.php] start in DEBUG mode +2022-04-28 20:17:51 pid:90941 Workerman[start.php] stopping ... +2022-04-28 20:17:51 pid:90941 Workerman[start.php] has been stopped +2022-04-28 20:18:14 pid:91286 Workerman[start.php] start in DEBUG mode +2022-04-28 20:18:30 pid:91286 Workerman[start.php] stopping ... +2022-04-28 20:18:30 pid:91286 Workerman[start.php] has been stopped +2022-04-28 20:20:26 pid:92379 Workerman[start.php] start in DEBUG mode +2022-04-28 20:21:07 pid:92379 Workerman[start.php] stopping ... +2022-04-28 20:21:07 pid:92379 Workerman[start.php] has been stopped +2022-04-28 20:22:37 pid:93452 Workerman[start.php] start in DEBUG mode +2022-04-28 20:22:51 pid:93452 Workerman[start.php] stopping ... +2022-04-28 20:22:51 pid:93452 Workerman[start.php] has been stopped +2022-04-28 20:23:48 pid:94049 Workerman[start.php] start in DEBUG mode +2022-04-28 20:23:57 pid:94049 Workerman[start.php] stopping ... +2022-04-28 20:23:57 pid:94049 Workerman[start.php] has been stopped +2022-04-28 20:24:21 pid:94334 Workerman[start.php] start in DEBUG mode +2022-04-28 20:24:27 pid:94334 Workerman[start.php] stopping ... +2022-04-28 20:24:27 pid:94334 Workerman[start.php] has been stopped diff --git a/logs/给这个文件夹权限 b/logs/给这个文件夹权限 new file mode 100644 index 0000000..e69de29 diff --git a/start.php b/start.php new file mode 100755 index 0000000..1762b71 --- /dev/null +++ b/start.php @@ -0,0 +1,23 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; + +/** + * Autoload. + */ +class Autoloader +{ + /** + * Autoload root path. + * + * @var string + */ + protected static $_autoloadRootPath = ''; + + /** + * Set autoload root path. + * + * @param string $root_path + * @return void + */ + public static function setRootPath($root_path) + { + self::$_autoloadRootPath = $root_path; + } + + /** + * Load files by namespace. + * + * @param string $name + * @return boolean + */ + public static function loadByNamespace($name) + { + $class_path = str_replace('\\', DIRECTORY_SEPARATOR, $name); + if (strpos($name, 'Workerman\\') === 0) { + $class_file = __DIR__ . substr($class_path, strlen('Workerman')) . '.php'; + } else { + if (self::$_autoloadRootPath) { + $class_file = self::$_autoloadRootPath . DIRECTORY_SEPARATOR . $class_path . '.php'; + } + if (empty($class_file) || !is_file($class_file)) { + $class_file = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . "$class_path.php"; + } + } + + if (is_file($class_file)) { + require_once($class_file); + if (class_exists($name, false)) { + return true; + } + } + return false; + } +} + +spl_autoload_register('\Workerman\Autoloader::loadByNamespace'); \ No newline at end of file diff --git a/workerman/Connection/AsyncTcpConnection.php b/workerman/Connection/AsyncTcpConnection.php new file mode 100755 index 0000000..e785bf7 --- /dev/null +++ b/workerman/Connection/AsyncTcpConnection.php @@ -0,0 +1,372 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use Workerman\Events\EventInterface; +use Workerman\Lib\Timer; +use Workerman\Worker; +use Exception; + +/** + * AsyncTcpConnection. + */ +class AsyncTcpConnection extends TcpConnection +{ + /** + * Emitted when socket connection is successfully established. + * + * @var callback + */ + public $onConnect = null; + + /** + * Transport layer protocol. + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Status. + * + * @var int + */ + protected $_status = self::STATUS_INITIAL; + + /** + * Remote host. + * + * @var string + */ + protected $_remoteHost = ''; + + /** + * Remote port. + * + * @var int + */ + protected $_remotePort = 80; + + /** + * Connect start time. + * + * @var string + */ + protected $_connectStartTime = 0; + + /** + * Remote URI. + * + * @var string + */ + protected $_remoteURI = ''; + + /** + * Context option. + * + * @var array + */ + protected $_contextOption = null; + + /** + * Reconnect timer. + * + * @var int + */ + protected $_reconnectTimer = null; + + + /** + * PHP built-in protocols. + * + * @var array + */ + protected static $_builtinTransports = array( + 'tcp' => 'tcp', + 'udp' => 'udp', + 'unix' => 'unix', + 'ssl' => 'ssl', + 'sslv2' => 'sslv2', + 'sslv3' => 'sslv3', + 'tls' => 'tls' + ); + + /** + * Construct. + * + * @param string $remote_address + * @param array $context_option + * @throws Exception + */ + public function __construct($remote_address, $context_option = null) + { + $address_info = parse_url($remote_address); + if (!$address_info) { + list($scheme, $this->_remoteAddress) = explode(':', $remote_address, 2); + if (!$this->_remoteAddress) { + Worker::safeEcho(new \Exception('bad remote_address')); + } + } else { + if (!isset($address_info['port'])) { + $address_info['port'] = 80; + } + if (!isset($address_info['path'])) { + $address_info['path'] = '/'; + } + if (!isset($address_info['query'])) { + $address_info['query'] = ''; + } else { + $address_info['query'] = '?' . $address_info['query']; + } + $this->_remoteAddress = "{$address_info['host']}:{$address_info['port']}"; + $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'; + } + + $this->id = $this->_id = self::$_idRecorder++; + if(PHP_INT_MAX === self::$_idRecorder){ + self::$_idRecorder = 0; + } + // Check application layer protocol class. + if (!isset(self::$_builtinTransports[$scheme])) { + $scheme = ucfirst($scheme); + $this->protocol = '\\Protocols\\' . $scheme; + if (!class_exists($this->protocol)) { + $this->protocol = "\\Workerman\\Protocols\\$scheme"; + if (!class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + } else { + $this->transport = self::$_builtinTransports[$scheme]; + } + + // For statistics. + self::$statistics['connection_count']++; + $this->maxSendBufferSize = self::$defaultMaxSendBufferSize; + $this->_contextOption = $context_option; + static::$connections[$this->_id] = $this; + } + + /** + * Do connect. + * + * @return void + */ + public function connect() + { + if ($this->_status !== self::STATUS_INITIAL && $this->_status !== self::STATUS_CLOSING && + $this->_status !== self::STATUS_CLOSED) { + return; + } + $this->_status = self::STATUS_CONNECTING; + $this->_connectStartTime = microtime(true); + if ($this->transport !== 'unix') { + // Open socket connection asynchronously. + if ($this->_contextOption) { + $context = stream_context_create($this->_contextOption); + $this->_socket = stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", + $errno, $errstr, 0, STREAM_CLIENT_ASYNC_CONNECT, $context); + } else { + $this->_socket = stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", + $errno, $errstr, 0, STREAM_CLIENT_ASYNC_CONNECT); + } + } else { + $this->_socket = stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0, + STREAM_CLIENT_ASYNC_CONNECT); + } + // If failed attempt to emit onError callback. + if (!$this->_socket || !is_resource($this->_socket)) { + $this->emitError(WORKERMAN_CONNECT_FAIL, $errstr); + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + if ($this->_status === self::STATUS_CLOSED) { + $this->onConnect = null; + } + return; + } + // Add socket to global event loop waiting connection is successfully established or faild. + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'checkConnection')); + // For windows. + if(DIRECTORY_SEPARATOR === '\\') { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_EXCEPT, array($this, 'checkConnection')); + } + } + + /** + * Reconnect. + * + * @param int $after + * @return void + */ + public function reconnect($after = 0) + { + $this->_status = self::STATUS_INITIAL; + static::$connections[$this->_id] = $this; + if ($this->_reconnectTimer) { + Timer::del($this->_reconnectTimer); + } + if ($after > 0) { + $this->_reconnectTimer = Timer::add($after, array($this, 'connect'), null, false); + return; + } + $this->connect(); + } + + /** + * CancelReconnect. + */ + public function cancelReconnect() + { + if ($this->_reconnectTimer) { + Timer::del($this->_reconnectTimer); + } + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteHost() + { + return $this->_remoteHost; + } + + /** + * Get remote URI. + * + * @return string + */ + public function getRemoteURI() + { + return $this->_remoteURI; + } + + /** + * Try to emit onError callback. + * + * @param int $code + * @param string $msg + * @return void + */ + protected function emitError($code, $msg) + { + $this->_status = self::STATUS_CLOSING; + if ($this->onError) { + try { + call_user_func($this->onError, $this, $code, $msg); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + + /** + * Check connection is successfully established or faild. + * + * @param resource $socket + * @return void + */ + public function checkConnection() + { + // Remove EV_EXPECT for windows. + if(DIRECTORY_SEPARATOR === '\\') { + Worker::$globalEvent->del($this->_socket, EventInterface::EV_EXCEPT); + } + + // Remove write listener. + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + + if ($this->_status != self::STATUS_CONNECTING) { + return; + } + + // Check socket state. + if ($address = stream_socket_get_name($this->_socket, true)) { + // Nonblocking. + stream_set_blocking($this->_socket, 0); + // Compatible with hhvm + if (function_exists('stream_set_read_buffer')) { + stream_set_read_buffer($this->_socket, 0); + } + // Try to open keepalive for tcp and disable Nagle algorithm. + if (function_exists('socket_import_stream') && $this->transport === 'tcp') { + $raw_socket = socket_import_stream($this->_socket); + socket_set_option($raw_socket, SOL_SOCKET, SO_KEEPALIVE, 1); + socket_set_option($raw_socket, SOL_TCP, TCP_NODELAY, 1); + } + + // SSL handshake. + if ($this->transport === 'ssl') { + $this->_sslHandshakeCompleted = $this->doSslHandshake($this->_socket); + if ($this->_sslHandshakeCompleted === false) { + return; + } + } else { + // There are some data waiting to send. + if ($this->_sendBuffer) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + } + } + + // Register a listener waiting read event. + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + + $this->_status = self::STATUS_ESTABLISHED; + $this->_remoteAddress = $address; + + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + call_user_func($this->onConnect, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + // Try to emit protocol::onConnect + if (method_exists($this->protocol, 'onConnect')) { + try { + call_user_func(array($this->protocol, 'onConnect'), $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } else { + // Connection failed. + $this->emitError(WORKERMAN_CONNECT_FAIL, 'connect ' . $this->_remoteAddress . ' fail after ' . round(microtime(true) - $this->_connectStartTime, 4) . ' seconds'); + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + if ($this->_status === self::STATUS_CLOSED) { + $this->onConnect = null; + } + } + } +} diff --git a/workerman/Connection/AsyncUdpConnection.php b/workerman/Connection/AsyncUdpConnection.php new file mode 100755 index 0000000..fdae348 --- /dev/null +++ b/workerman/Connection/AsyncUdpConnection.php @@ -0,0 +1,209 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use Exception; + +/** + * AsyncTcpConnection. + */ +class AsyncUdpConnection extends UdpConnection +{ + /** + * Emitted when socket connection is successfully established. + * + * @var callback + */ + public $onConnect = null; + + /** + * Emitted when socket connection closed. + * + * @var callback + */ + public $onClose = null; + + /** + * Connected or not. + * + * @var bool + */ + protected $connected = false; + + /** + * Context option. + * + * @var array + */ + protected $_contextOption = null; + + /** + * Construct. + * + * @param string $remote_address + * @throws Exception + */ + public function __construct($remote_address, $context_option = null) + { + // Get the application layer communication protocol and listening address. + list($scheme, $address) = explode(':', $remote_address, 2); + // Check application layer protocol class. + if ($scheme !== 'udp') { + $scheme = ucfirst($scheme); + $this->protocol = '\\Protocols\\' . $scheme; + if (!class_exists($this->protocol)) { + $this->protocol = "\\Workerman\\Protocols\\$scheme"; + if (!class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + } + + $this->_remoteAddress = substr($address, 2); + $this->_contextOption = $context_option; + } + + /** + * For udp package. + * + * @param resource $socket + * @return bool + */ + public function baseRead($socket) + { + $recv_buffer = stream_socket_recvfrom($socket, Worker::MAX_UDP_PACKAGE_SIZE, 0, $remote_address); + if (false === $recv_buffer || empty($remote_address)) { + return false; + } + + if ($this->onMessage) { + if ($this->protocol) { + $parser = $this->protocol; + $recv_buffer = $parser::decode($recv_buffer, $this); + } + ConnectionInterface::$statistics['total_request']++; + try { + call_user_func($this->onMessage, $this, $recv_buffer); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return true; + } + + /** + * Sends data on the connection. + * + * @param string $send_buffer + * @param bool $raw + * @return void|boolean + */ + public function send($send_buffer, $raw = false) + { + if (false === $raw && $this->protocol) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return null; + } + } + if ($this->connected === false) { + $this->connect(); + } + return strlen($send_buffer) === stream_socket_sendto($this->_socket, $send_buffer, 0); + } + + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * + * @return bool + */ + public function close($data = null, $raw = false) + { + if ($data !== null) { + $this->send($data, $raw); + } + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + fclose($this->_socket); + $this->connected = false; + // Try to emit onClose callback. + if ($this->onClose) { + try { + call_user_func($this->onClose, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + $this->onConnect = $this->onMessage = $this->onClose = null; + return true; + } + + /** + * Connect. + * + * @return void + */ + public function connect() + { + if ($this->connected === true) { + return; + } + if ($this->_contextOption) { + $context = stream_context_create($this->_contextOption); + $this->_socket = stream_socket_client("udp://{$this->_remoteAddress}", $errno, $errmsg, + 30, STREAM_CLIENT_CONNECT, $context); + } else { + $this->_socket = stream_socket_client("udp://{$this->_remoteAddress}", $errno, $errmsg); + } + + if (!$this->_socket) { + Worker::safeEcho(new \Exception($errmsg)); + return; + } + + stream_set_blocking($this->_socket, false); + + if ($this->onMessage) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + } + $this->connected = true; + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + call_user_func($this->onConnect, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + +} diff --git a/workerman/Connection/ConnectionInterface.php b/workerman/Connection/ConnectionInterface.php new file mode 100755 index 0000000..622862a --- /dev/null +++ b/workerman/Connection/ConnectionInterface.php @@ -0,0 +1,125 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +/** + * ConnectionInterface. + */ +abstract class ConnectionInterface +{ + /** + * Statistics for status command. + * + * @var array + */ + public static $statistics = array( + 'connection_count' => 0, + 'total_request' => 0, + 'throw_exception' => 0, + 'send_fail' => 0, + ); + + /** + * Emitted when data is received. + * + * @var callback + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callback + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callback + */ + public $onError = null; + + /** + * Sends data on the connection. + * + * @param mixed $send_buffer + * @return void|boolean + */ + abstract public function send($send_buffer); + + /** + * Get remote IP. + * + * @return string + */ + abstract public function getRemoteIp(); + + /** + * Get remote port. + * + * @return int + */ + abstract public function getRemotePort(); + + /** + * Get remote address. + * + * @return string + */ + abstract public function getRemoteAddress(); + + /** + * Get local IP. + * + * @return string + */ + abstract public function getLocalIp(); + + /** + * Get local port. + * + * @return int + */ + abstract public function getLocalPort(); + + /** + * Get local address. + * + * @return string + */ + abstract public function getLocalAddress(); + + /** + * Is ipv4. + * + * @return bool + */ + abstract public function isIPv4(); + + /** + * Is ipv6. + * + * @return bool + */ + abstract public function isIPv6(); + + /** + * Close connection. + * + * @param $data + * @return void + */ + abstract public function close($data = null); +} diff --git a/workerman/Connection/TcpConnection.php b/workerman/Connection/TcpConnection.php new file mode 100755 index 0000000..98419d3 --- /dev/null +++ b/workerman/Connection/TcpConnection.php @@ -0,0 +1,1005 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use Exception; + +/** + * TcpConnection. + */ +class TcpConnection extends ConnectionInterface +{ + /** + * Read buffer size. + * + * @var int + */ + const READ_BUFFER_SIZE = 65535; + + /** + * Status initial. + * + * @var int + */ + const STATUS_INITIAL = 0; + + /** + * Status connecting. + * + * @var int + */ + const STATUS_CONNECTING = 1; + + /** + * Status connection established. + * + * @var int + */ + const STATUS_ESTABLISHED = 2; + + /** + * Status closing. + * + * @var int + */ + const STATUS_CLOSING = 4; + + /** + * Status closed. + * + * @var int + */ + const STATUS_CLOSED = 8; + + /** + * Emitted when data is received. + * + * @var callback + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callback + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callback + */ + public $onError = null; + + /** + * Emitted when the send buffer becomes full. + * + * @var callback + */ + public $onBufferFull = null; + + /** + * Emitted when the send buffer becomes empty. + * + * @var callback + */ + public $onBufferDrain = null; + + /** + * Application layer protocol. + * The format is like this Workerman\\Protocols\\Http. + * + * @var \Workerman\Protocols\ProtocolInterface + */ + public $protocol = null; + + /** + * Transport (tcp/udp/unix/ssl). + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Which worker belong to. + * + * @var Worker + */ + public $worker = null; + + /** + * Bytes read. + * + * @var int + */ + public $bytesRead = 0; + + /** + * Bytes written. + * + * @var int + */ + public $bytesWritten = 0; + + /** + * Connection->id. + * + * @var int + */ + public $id = 0; + + /** + * A copy of $worker->id which used to clean up the connection in worker->connections + * + * @var int + */ + protected $_id = 0; + + /** + * Sets the maximum send buffer size for the current connection. + * OnBufferFull callback will be emited When the send buffer is full. + * + * @var int + */ + public $maxSendBufferSize = 1048576; + + /** + * Default send buffer size. + * + * @var int + */ + public static $defaultMaxSendBufferSize = 1048576; + + /** + * Sets the maximum acceptable packet size for the current connection. + * + * @var int + */ + public $maxPackageSize = 1048576; + + /** + * Default maximum acceptable packet size. + * + * @var int + */ + public static $defaultMaxPackageSize = 10485760; + + /** + * Id recorder. + * + * @var int + */ + protected static $_idRecorder = 1; + + /** + * Socket + * + * @var resource + */ + protected $_socket = null; + + /** + * Send buffer. + * + * @var string + */ + protected $_sendBuffer = ''; + + /** + * Receive buffer. + * + * @var string + */ + protected $_recvBuffer = ''; + + /** + * Current package length. + * + * @var int + */ + protected $_currentPackageLength = 0; + + /** + * Connection status. + * + * @var int + */ + protected $_status = self::STATUS_ESTABLISHED; + + /** + * Remote address. + * + * @var string + */ + protected $_remoteAddress = ''; + + /** + * Is paused. + * + * @var bool + */ + protected $_isPaused = false; + + /** + * SSL handshake completed or not. + * + * @var bool + */ + protected $_sslHandshakeCompleted = false; + + /** + * All connection instances. + * + * @var array + */ + public static $connections = array(); + + /** + * Status to string. + * + * @var array + */ + public static $_statusToString = array( + self::STATUS_INITIAL => 'INITIAL', + self::STATUS_CONNECTING => 'CONNECTING', + self::STATUS_ESTABLISHED => 'ESTABLISHED', + self::STATUS_CLOSING => 'CLOSING', + self::STATUS_CLOSED => 'CLOSED', + ); + + + /** + * Adding support of custom functions within protocols + * + * @param string $name + * @param array $arguments + * @return void + */ + public function __call($name, $arguments) { + // Try to emit custom function within protocol + if (method_exists($this->protocol, $name)) { + try { + return call_user_func(array($this->protocol, $name), $this, $arguments); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + + /** + * Construct. + * + * @param resource $socket + * @param string $remote_address + */ + public function __construct($socket, $remote_address = '') + { + self::$statistics['connection_count']++; + $this->id = $this->_id = self::$_idRecorder++; + if(self::$_idRecorder === PHP_INT_MAX){ + self::$_idRecorder = 0; + } + $this->_socket = $socket; + stream_set_blocking($this->_socket, 0); + // Compatible with hhvm + if (function_exists('stream_set_read_buffer')) { + stream_set_read_buffer($this->_socket, 0); + } + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + $this->maxSendBufferSize = self::$defaultMaxSendBufferSize; + $this->maxPackageSize = self::$defaultMaxPackageSize; + $this->_remoteAddress = $remote_address; + static::$connections[$this->id] = $this; + } + + /** + * Get status. + * + * @param bool $raw_output + * + * @return int + */ + public function getStatus($raw_output = true) + { + if ($raw_output) { + return $this->_status; + } + return self::$_statusToString[$this->_status]; + } + + /** + * Sends data on the connection. + * + * @param mixed $send_buffer + * @param bool $raw + * @return bool|null + */ + public function send($send_buffer, $raw = false) + { + if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { + return false; + } + + // Try to call protocol::encode($send_buffer) before sending. + if (false === $raw && $this->protocol !== null) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return null; + } + } + + if ($this->_status !== self::STATUS_ESTABLISHED || + ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) + ) { + if ($this->_sendBuffer) { + if ($this->bufferIsFull()) { + self::$statistics['send_fail']++; + return false; + } + } + $this->_sendBuffer .= $send_buffer; + $this->checkBufferWillFull(); + return null; + } + + // Attempt to send data directly. + if ($this->_sendBuffer === '') { + if ($this->transport === 'ssl') { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + $this->_sendBuffer = $send_buffer; + $this->checkBufferWillFull(); + return null; + } + set_error_handler(function(){}); + $len = fwrite($this->_socket, $send_buffer); + restore_error_handler(); + // send successful. + if ($len === strlen($send_buffer)) { + $this->bytesWritten += $len; + return true; + } + // Send only part of the data. + if ($len > 0) { + $this->_sendBuffer = substr($send_buffer, $len); + $this->bytesWritten += $len; + } else { + // Connection closed? + if (!is_resource($this->_socket) || feof($this->_socket)) { + self::$statistics['send_fail']++; + if ($this->onError) { + try { + call_user_func($this->onError, $this, WORKERMAN_SEND_FAIL, 'client closed'); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + $this->destroy(); + return false; + } + $this->_sendBuffer = $send_buffer; + } + Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + // Check if the send buffer will be full. + $this->checkBufferWillFull(); + return null; + } else { + if ($this->bufferIsFull()) { + self::$statistics['send_fail']++; + return false; + } + + $this->_sendBuffer .= $send_buffer; + // Check if the send buffer is full. + $this->checkBufferWillFull(); + } + } + + /** + * Get remote IP. + * + * @return string + */ + public function getRemoteIp() + { + $pos = strrpos($this->_remoteAddress, ':'); + if ($pos) { + return substr($this->_remoteAddress, 0, $pos); + } + return ''; + } + + /** + * Get remote port. + * + * @return int + */ + public function getRemotePort() + { + if ($this->_remoteAddress) { + return (int)substr(strrchr($this->_remoteAddress, ':'), 1); + } + return 0; + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteAddress() + { + return $this->_remoteAddress; + } + + /** + * Get local IP. + * + * @return string + */ + public function getLocalIp() + { + $address = $this->getLocalAddress(); + $pos = strrpos($address, ':'); + if (!$pos) { + return ''; + } + return substr($address, 0, $pos); + } + + /** + * Get local port. + * + * @return int + */ + public function getLocalPort() + { + $address = $this->getLocalAddress(); + $pos = strrpos($address, ':'); + if (!$pos) { + return 0; + } + return (int)substr(strrchr($address, ':'), 1); + } + + /** + * Get local address. + * + * @return string + */ + public function getLocalAddress() + { + return (string)@stream_socket_get_name($this->_socket, false); + } + + /** + * Get send buffer queue size. + * + * @return integer + */ + public function getSendBufferQueueSize() + { + return strlen($this->_sendBuffer); + } + + /** + * Get recv buffer queue size. + * + * @return integer + */ + public function getRecvBufferQueueSize() + { + return strlen($this->_recvBuffer); + } + + /** + * Is ipv4. + * + * return bool. + */ + public function isIpV4() + { + if ($this->transport === 'unix') { + return false; + } + return strpos($this->getRemoteIp(), ':') === false; + } + + /** + * Is ipv6. + * + * return bool. + */ + public function isIpV6() + { + if ($this->transport === 'unix') { + return false; + } + return strpos($this->getRemoteIp(), ':') !== false; + } + + /** + * Pauses the reading of data. That is onMessage will not be emitted. Useful to throttle back an upload. + * + * @return void + */ + public function pauseRecv() + { + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + $this->_isPaused = true; + } + + /** + * Resumes reading after a call to pauseRecv. + * + * @return void + */ + public function resumeRecv() + { + if ($this->_isPaused === true) { + Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead')); + $this->_isPaused = false; + $this->baseRead($this->_socket, false); + } + } + + + + /** + * Base read handler. + * + * @param resource $socket + * @param bool $check_eof + * @return void + */ + public function baseRead($socket, $check_eof = true) + { + // SSL handshake. + if ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) { + if ($this->doSslHandshake($socket)) { + $this->_sslHandshakeCompleted = true; + if ($this->_sendBuffer) { + Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); + } + } else { + return; + } + } + + set_error_handler(function(){}); + $buffer = fread($socket, self::READ_BUFFER_SIZE); + restore_error_handler(); + + // Check connection closed. + if ($buffer === '' || $buffer === false) { + if ($check_eof && (feof($socket) || !is_resource($socket) || $buffer === false)) { + $this->destroy(); + return; + } + } else { + $this->bytesRead += strlen($buffer); + $this->_recvBuffer .= $buffer; + } + + // If the application layer protocol has been set up. + if ($this->protocol !== null) { + $parser = $this->protocol; + while ($this->_recvBuffer !== '' && !$this->_isPaused) { + // The current packet length is known. + if ($this->_currentPackageLength) { + // Data is not enough for a package. + if ($this->_currentPackageLength > strlen($this->_recvBuffer)) { + break; + } + } else { + // Get current package length. + set_error_handler(function($code, $msg, $file, $line){ + Worker::safeEcho("$msg in file $file on line $line\n"); + }); + $this->_currentPackageLength = $parser::input($this->_recvBuffer, $this); + restore_error_handler(); + // The packet length is unknown. + if ($this->_currentPackageLength === 0) { + break; + } elseif ($this->_currentPackageLength > 0 && $this->_currentPackageLength <= $this->maxPackageSize) { + // Data is not enough for a package. + if ($this->_currentPackageLength > strlen($this->_recvBuffer)) { + break; + } + } // Wrong package. + else { + Worker::safeEcho('error package. package_length=' . var_export($this->_currentPackageLength, true)); + $this->destroy(); + return; + } + } + + // The data is enough for a packet. + self::$statistics['total_request']++; + // The current packet length is equal to the length of the buffer. + if (strlen($this->_recvBuffer) === $this->_currentPackageLength) { + $one_request_buffer = $this->_recvBuffer; + $this->_recvBuffer = ''; + } else { + // Get a full package from the buffer. + $one_request_buffer = substr($this->_recvBuffer, 0, $this->_currentPackageLength); + // Remove the current package from the receive buffer. + $this->_recvBuffer = substr($this->_recvBuffer, $this->_currentPackageLength); + } + // Reset the current packet length to 0. + $this->_currentPackageLength = 0; + if (!$this->onMessage) { + continue; + } + try { + // Decode request buffer before Emitting onMessage callback. + call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this)); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return; + } + + if ($this->_recvBuffer === '' || $this->_isPaused) { + return; + } + + // Applications protocol is not set. + self::$statistics['total_request']++; + if (!$this->onMessage) { + $this->_recvBuffer = ''; + return; + } + try { + call_user_func($this->onMessage, $this, $this->_recvBuffer); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + // Clean receive buffer. + $this->_recvBuffer = ''; + } + + /** + * Base write handler. + * + * @return void|bool + */ + public function baseWrite() + { + set_error_handler(function(){}); + if ($this->transport === 'ssl') { + $len = fwrite($this->_socket, $this->_sendBuffer, 8192); + } else { + $len = fwrite($this->_socket, $this->_sendBuffer); + } + restore_error_handler(); + if ($len === strlen($this->_sendBuffer)) { + $this->bytesWritten += $len; + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + $this->_sendBuffer = ''; + // Try to emit onBufferDrain callback when the send buffer becomes empty. + if ($this->onBufferDrain) { + try { + call_user_func($this->onBufferDrain, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + if ($this->_status === self::STATUS_CLOSING) { + $this->destroy(); + } + return true; + } + if ($len > 0) { + $this->bytesWritten += $len; + $this->_sendBuffer = substr($this->_sendBuffer, $len); + } else { + self::$statistics['send_fail']++; + $this->destroy(); + } + } + + /** + * SSL handshake. + * + * @param $socket + * @return bool + */ + public function doSslHandshake($socket){ + if (feof($socket)) { + $this->destroy(); + return false; + } + $async = $this instanceof AsyncTcpConnection; + + /** + * We disabled ssl3 because https://blog.qualys.com/ssllabs/2014/10/15/ssl-3-is-dead-killed-by-the-poodle-attack. + * You can enable ssl3 by the codes below. + */ + /*if($async){ + $type = STREAM_CRYPTO_METHOD_SSLv2_CLIENT | STREAM_CRYPTO_METHOD_SSLv23_CLIENT | STREAM_CRYPTO_METHOD_SSLv3_CLIENT; + }else{ + $type = STREAM_CRYPTO_METHOD_SSLv2_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER | STREAM_CRYPTO_METHOD_SSLv3_SERVER; + }*/ + + if($async){ + $type = STREAM_CRYPTO_METHOD_SSLv2_CLIENT | STREAM_CRYPTO_METHOD_SSLv23_CLIENT; + }else{ + $type = STREAM_CRYPTO_METHOD_SSLv2_SERVER | STREAM_CRYPTO_METHOD_SSLv23_SERVER; + } + + // Hidden error. + set_error_handler(function($errno, $errstr, $file){ + if (!Worker::$daemonize) { + Worker::safeEcho("SSL handshake error: $errstr \n"); + } + }); + $ret = stream_socket_enable_crypto($socket, true, $type); + restore_error_handler(); + // Negotiation has failed. + if (false === $ret) { + $this->destroy(); + return false; + } elseif (0 === $ret) { + // There isn't enough data and should try again. + return 0; + } + if (isset($this->onSslHandshake)) { + try { + call_user_func($this->onSslHandshake, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return true; + } + + /** + * This method pulls all the data out of a readable stream, and writes it to the supplied destination. + * + * @param TcpConnection $dest + * @return void + */ + public function pipe($dest) + { + $source = $this; + $this->onMessage = function ($source, $data) use ($dest) { + $dest->send($data); + }; + $this->onClose = function ($source) use ($dest) { + $dest->destroy(); + }; + $dest->onBufferFull = function ($dest) use ($source) { + $source->pauseRecv(); + }; + $dest->onBufferDrain = function ($dest) use ($source) { + $source->resumeRecv(); + }; + } + + /** + * Remove $length of data from receive buffer. + * + * @param int $length + * @return void + */ + public function consumeRecvBuffer($length) + { + $this->_recvBuffer = substr($this->_recvBuffer, $length); + } + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * @return void + */ + public function close($data = null, $raw = false) + { + if($this->_status === self::STATUS_CONNECTING){ + $this->destroy(); + return; + } + if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { + return; + } else { + if ($data !== null) { + $this->send($data, $raw); + } + $this->_status = self::STATUS_CLOSING; + } + if ($this->_sendBuffer === '') { + $this->destroy(); + } else { + $this->pauseRecv(); + } + } + + /** + * Get the real socket. + * + * @return resource + */ + public function getSocket() + { + return $this->_socket; + } + + /** + * Check whether the send buffer will be full. + * + * @return void + */ + protected function checkBufferWillFull() + { + if ($this->maxSendBufferSize <= strlen($this->_sendBuffer)) { + if ($this->onBufferFull) { + try { + call_user_func($this->onBufferFull, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + } + + /** + * Whether send buffer is full. + * + * @return bool + */ + protected function bufferIsFull() + { + // Buffer has been marked as full but still has data to send then the packet is discarded. + if ($this->maxSendBufferSize <= strlen($this->_sendBuffer)) { + if ($this->onError) { + try { + call_user_func($this->onError, $this, WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return true; + } + return false; + } + + /** + * Whether send buffer is Empty. + * + * @return bool + */ + public function bufferIsEmpty() + { + return empty($this->_sendBuffer); + } + + /** + * Destroy connection. + * + * @return void + */ + public function destroy() + { + // Avoid repeated calls. + if ($this->_status === self::STATUS_CLOSED) { + return; + } + // Remove event listener. + Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); + Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); + + // Close socket. + set_error_handler(function(){}); + fclose($this->_socket); + restore_error_handler(); + + $this->_status = self::STATUS_CLOSED; + // Try to emit onClose callback. + if ($this->onClose) { + try { + call_user_func($this->onClose, $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + // Try to emit protocol::onClose + if ($this->protocol && method_exists($this->protocol, 'onClose')) { + try { + call_user_func(array($this->protocol, 'onClose'), $this); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + $this->_sendBuffer = $this->_recvBuffer = ''; + if ($this->_status === self::STATUS_CLOSED) { + // Cleaning up the callback to avoid memory leaks. + $this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = null; + // Remove from worker->connections. + if ($this->worker) { + unset($this->worker->connections[$this->_id]); + } + unset(static::$connections[$this->_id]); + } + } + + /** + * Destruct. + * + * @return void + */ + public function __destruct() + { + static $mod; + self::$statistics['connection_count']--; + if (Worker::getGracefulStop()) { + if (!isset($mod)) { + $mod = ceil((self::$statistics['connection_count'] + 1) / 3); + } + + if (0 === self::$statistics['connection_count'] % $mod) { + Worker::log('worker[' . posix_getpid() . '] remains ' . self::$statistics['connection_count'] . ' connection(s)'); + } + + if(0 === self::$statistics['connection_count']) { + Worker::stopAll(); + } + } + } +} diff --git a/workerman/Connection/UdpConnection.php b/workerman/Connection/UdpConnection.php new file mode 100755 index 0000000..2e7aeee --- /dev/null +++ b/workerman/Connection/UdpConnection.php @@ -0,0 +1,191 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Connection; + +/** + * UdpConnection. + */ +class UdpConnection extends ConnectionInterface +{ + /** + * Application layer protocol. + * The format is like this Workerman\\Protocols\\Http. + * + * @var \Workerman\Protocols\ProtocolInterface + */ + public $protocol = null; + + /** + * Udp socket. + * + * @var resource + */ + protected $_socket = null; + + /** + * Remote address. + * + * @var string + */ + protected $_remoteAddress = ''; + + /** + * Construct. + * + * @param resource $socket + * @param string $remote_address + */ + public function __construct($socket, $remote_address) + { + $this->_socket = $socket; + $this->_remoteAddress = $remote_address; + } + + /** + * Sends data on the connection. + * + * @param string $send_buffer + * @param bool $raw + * @return void|boolean + */ + public function send($send_buffer, $raw = false) + { + if (false === $raw && $this->protocol) { + $parser = $this->protocol; + $send_buffer = $parser::encode($send_buffer, $this); + if ($send_buffer === '') { + return null; + } + } + return strlen($send_buffer) === stream_socket_sendto($this->_socket, $send_buffer, 0, $this->_remoteAddress); + } + + /** + * Get remote IP. + * + * @return string + */ + public function getRemoteIp() + { + $pos = strrpos($this->_remoteAddress, ':'); + if ($pos) { + return trim(substr($this->_remoteAddress, 0, $pos), '[]'); + } + return ''; + } + + /** + * Get remote port. + * + * @return int + */ + public function getRemotePort() + { + if ($this->_remoteAddress) { + return (int)substr(strrchr($this->_remoteAddress, ':'), 1); + } + return 0; + } + + /** + * Get remote address. + * + * @return string + */ + public function getRemoteAddress() + { + return $this->_remoteAddress; + } + + /** + * Get local IP. + * + * @return string + */ + public function getLocalIp() + { + $address = $this->getLocalAddress(); + $pos = strrpos($address, ':'); + if (!$pos) { + return ''; + } + return substr($address, 0, $pos); + } + + /** + * Get local port. + * + * @return int + */ + public function getLocalPort() + { + $address = $this->getLocalAddress(); + $pos = strrpos($address, ':'); + if (!$pos) { + return 0; + } + return (int)substr(strrchr($address, ':'), 1); + } + + /** + * Get local address. + * + * @return string + */ + public function getLocalAddress() + { + return (string)@stream_socket_get_name($this->_socket, false); + } + + /** + * Is ipv4. + * + * return bool. + */ + public function isIpV4() + { + if ($this->transport === 'unix') { + return false; + } + return strpos($this->getRemoteIp(), ':') === false; + } + + /** + * Is ipv6. + * + * return bool. + */ + public function isIpV6() + { + if ($this->transport === 'unix') { + return false; + } + return strpos($this->getRemoteIp(), ':') !== false; + } + + /** + * Close connection. + * + * @param mixed $data + * @param bool $raw + * @return bool + */ + public function close($data = null, $raw = false) + { + if ($data !== null) { + $this->send($data, $raw); + } + return true; + } +} diff --git a/workerman/Events/Ev.php b/workerman/Events/Ev.php new file mode 100755 index 0000000..1e6471d --- /dev/null +++ b/workerman/Events/Ev.php @@ -0,0 +1,194 @@ + + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * ev eventloop + */ +class Ev implements EventInterface +{ + /** + * All listeners for read/write event. + * + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * + * @var int + */ + protected static $_timerId = 1; + + /** + * Add a timer. + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = null) + { + $callback = function ($event, $socket) use ($fd, $func) { + try { + call_user_func($func, $fd); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + }; + switch ($flag) { + case self::EV_SIGNAL: + $event = new \EvSignal($fd, $callback); + $this->_eventSignal[$fd] = $event; + return true; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $repeat = $flag == self::EV_TIMER_ONCE ? 0 : $fd; + $param = array($func, (array)$args, $flag, $fd, self::$_timerId); + $event = new \EvTimer($fd, $repeat, array($this, 'timerCallback'), $param); + $this->_eventTimer[self::$_timerId] = $event; + return self::$_timerId++; + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? \Ev::READ : \Ev::WRITE; + $event = new \EvIo($fd, $real_flag, $callback); + $this->_allEvents[$fd_key][$flag] = $event; + return true; + } + + } + + /** + * Remove a timer. + * {@inheritdoc} + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + $this->_allEvents[$fd_key][$flag]->stop(); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + $this->_eventSignal[$fd_key]->stop(); + unset($this->_eventSignal[$fd_key]); + } + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + if (isset($this->_eventTimer[$fd])) { + $this->_eventTimer[$fd]->stop(); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * + * @param \EvWatcher $event + */ + public function timerCallback($event) + { + $param = $event->data; + $timer_id = $param[4]; + if ($param[2] === self::EV_TIMER_ONCE) { + $this->_eventTimer[$timer_id]->stop(); + unset($this->_eventTimer[$timer_id]); + } + try { + call_user_func_array($param[0], $param[1]); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + + /** + * Remove all timers. + * + * @return void + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $event) { + $event->stop(); + } + $this->_eventTimer = array(); + } + + /** + * Main loop. + * + * @see EventInterface::loop() + */ + public function loop() + { + \Ev::run(); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + foreach ($this->_allEvents as $event) { + $event->stop(); + } + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return count($this->_eventTimer); + } +} diff --git a/workerman/Events/Event.php b/workerman/Events/Event.php new file mode 100755 index 0000000..b4d371a --- /dev/null +++ b/workerman/Events/Event.php @@ -0,0 +1,219 @@ + + * @copyright 有个鬼<42765633@qq.com> + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * libevent eventloop + */ +class Event implements EventInterface +{ + /** + * Event base. + * @var object + */ + protected $_eventBase = null; + + /** + * All listeners for read/write event. + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * @var int + */ + protected static $_timerId = 1; + + /** + * construct + * @return void + */ + public function __construct() + { + if (class_exists('\\\\EventBase', false)) { + $class_name = '\\\\EventBase'; + } else { + $class_name = '\EventBase'; + } + $this->_eventBase = new $class_name(); + } + + /** + * @see EventInterface::add() + */ + public function add($fd, $flag, $func, $args=array()) + { + if (class_exists('\\\\Event', false)) { + $class_name = '\\\\Event'; + } else { + $class_name = '\Event'; + } + switch ($flag) { + case self::EV_SIGNAL: + + $fd_key = (int)$fd; + $event = $class_name::signal($this->_eventBase, $fd, $func); + if (!$event||!$event->add()) { + return false; + } + $this->_eventSignal[$fd_key] = $event; + return true; + + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + + $param = array($func, (array)$args, $flag, $fd, self::$_timerId); + $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT|$class_name::PERSIST, array($this, "timerCallback"), $param); + if (!$event||!$event->addTimer($fd)) { + return false; + } + $this->_eventTimer[self::$_timerId] = $event; + return self::$_timerId++; + + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? $class_name::READ | $class_name::PERSIST : $class_name::WRITE | $class_name::PERSIST; + $event = new $class_name($this->_eventBase, $fd, $real_flag, $func, $fd); + if (!$event||!$event->add()) { + return false; + } + $this->_allEvents[$fd_key][$flag] = $event; + return true; + } + } + + /** + * @see Events\EventInterface::del() + */ + public function del($fd, $flag) + { + switch ($flag) { + + case self::EV_READ: + case self::EV_WRITE: + + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + $this->_allEvents[$fd_key][$flag]->del(); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + $this->_eventSignal[$fd_key]->del(); + unset($this->_eventSignal[$fd_key]); + } + break; + + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + if (isset($this->_eventTimer[$fd])) { + $this->_eventTimer[$fd]->del(); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * @param null $fd + * @param int $what + * @param int $timer_id + */ + public function timerCallback($fd, $what, $param) + { + $timer_id = $param[4]; + + if ($param[2] === self::EV_TIMER_ONCE) { + $this->_eventTimer[$timer_id]->del(); + unset($this->_eventTimer[$timer_id]); + } + + try { + call_user_func_array($param[0], $param[1]); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + + /** + * @see Events\EventInterface::clearAllTimer() + * @return void + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $event) { + $event->del(); + } + $this->_eventTimer = array(); + } + + + /** + * @see EventInterface::loop() + */ + public function loop() + { + $this->_eventBase->loop(); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + foreach ($this->_eventSignal as $event) { + $event->del(); + } + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return count($this->_eventTimer); + } +} diff --git a/workerman/Events/EventInterface.php b/workerman/Events/EventInterface.php new file mode 100755 index 0000000..88f38f2 --- /dev/null +++ b/workerman/Events/EventInterface.php @@ -0,0 +1,107 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +interface EventInterface +{ + /** + * Read event. + * + * @var int + */ + const EV_READ = 1; + + /** + * Write event. + * + * @var int + */ + const EV_WRITE = 2; + + /** + * Except event + * + * @var int + */ + const EV_EXCEPT = 3; + + /** + * Signal event. + * + * @var int + */ + const EV_SIGNAL = 4; + + /** + * Timer event. + * + * @var int + */ + const EV_TIMER = 8; + + /** + * Timer once event. + * + * @var int + */ + const EV_TIMER_ONCE = 16; + + /** + * Add event listener to event loop. + * + * @param mixed $fd + * @param int $flag + * @param callable $func + * @param mixed $args + * @return bool + */ + public function add($fd, $flag, $func, $args = null); + + /** + * Remove event listener from event loop. + * + * @param mixed $fd + * @param int $flag + * @return bool + */ + public function del($fd, $flag); + + /** + * Remove all timers. + * + * @return void + */ + public function clearAllTimer(); + + /** + * Main loop. + * + * @return void + */ + public function loop(); + + /** + * Destroy loop. + * + * @return mixed + */ + public function destroy(); + + /** + * Get Timer count. + * + * @return mixed + */ + public function getTimerCount(); +} diff --git a/workerman/Events/Libevent.php b/workerman/Events/Libevent.php new file mode 100755 index 0000000..0b3f7c0 --- /dev/null +++ b/workerman/Events/Libevent.php @@ -0,0 +1,227 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Workerman\Worker; + +/** + * libevent eventloop + */ +class Libevent implements EventInterface +{ + /** + * Event base. + * + * @var resource + */ + protected $_eventBase = null; + + /** + * All listeners for read/write event. + * + * @var array + */ + protected $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + protected $_eventSignal = array(); + + /** + * All timer event listeners. + * [func, args, event, flag, time_interval] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * construct + */ + public function __construct() + { + $this->_eventBase = event_base_new(); + } + + /** + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = array()) + { + switch ($flag) { + case self::EV_SIGNAL: + $fd_key = (int)$fd; + $real_flag = EV_SIGNAL | EV_PERSIST; + $this->_eventSignal[$fd_key] = event_new(); + if (!event_set($this->_eventSignal[$fd_key], $fd, $real_flag, $func, null)) { + return false; + } + if (!event_base_set($this->_eventSignal[$fd_key], $this->_eventBase)) { + return false; + } + if (!event_add($this->_eventSignal[$fd_key])) { + return false; + } + return true; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $event = event_new(); + $timer_id = (int)$event; + if (!event_set($event, 0, EV_TIMEOUT, array($this, 'timerCallback'), $timer_id)) { + return false; + } + + if (!event_base_set($event, $this->_eventBase)) { + return false; + } + + $time_interval = $fd * 1000000; + if (!event_add($event, $time_interval)) { + return false; + } + $this->_eventTimer[$timer_id] = array($func, (array)$args, $event, $flag, $time_interval); + return $timer_id; + + default : + $fd_key = (int)$fd; + $real_flag = $flag === self::EV_READ ? EV_READ | EV_PERSIST : EV_WRITE | EV_PERSIST; + + $event = event_new(); + + if (!event_set($event, $fd, $real_flag, $func, null)) { + return false; + } + + if (!event_base_set($event, $this->_eventBase)) { + return false; + } + + if (!event_add($event)) { + return false; + } + + $this->_allEvents[$fd_key][$flag] = $event; + + return true; + } + + } + + /** + * {@inheritdoc} + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][$flag])) { + event_del($this->_allEvents[$fd_key][$flag]); + unset($this->_allEvents[$fd_key][$flag]); + } + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + break; + case self::EV_SIGNAL: + $fd_key = (int)$fd; + if (isset($this->_eventSignal[$fd_key])) { + event_del($this->_eventSignal[$fd_key]); + unset($this->_eventSignal[$fd_key]); + } + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + // 这里 fd 为timerid + if (isset($this->_eventTimer[$fd])) { + event_del($this->_eventTimer[$fd][2]); + unset($this->_eventTimer[$fd]); + } + break; + } + return true; + } + + /** + * Timer callback. + * + * @param mixed $_null1 + * @param int $_null2 + * @param mixed $timer_id + */ + protected function timerCallback($_null1, $_null2, $timer_id) + { + if ($this->_eventTimer[$timer_id][3] === self::EV_TIMER) { + event_add($this->_eventTimer[$timer_id][2], $this->_eventTimer[$timer_id][4]); + } + try { + call_user_func_array($this->_eventTimer[$timer_id][0], $this->_eventTimer[$timer_id][1]); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + if (isset($this->_eventTimer[$timer_id]) && $this->_eventTimer[$timer_id][3] === self::EV_TIMER_ONCE) { + $this->del($timer_id, self::EV_TIMER_ONCE); + } + } + + /** + * {@inheritdoc} + */ + public function clearAllTimer() + { + foreach ($this->_eventTimer as $task_data) { + event_del($task_data[2]); + } + $this->_eventTimer = array(); + } + + /** + * {@inheritdoc} + */ + public function loop() + { + event_base_loop($this->_eventBase); + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + foreach ($this->_eventSignal as $event) { + event_del($event); + } + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return count($this->_eventTimer); + } +} + diff --git a/workerman/Events/React/Base.php b/workerman/Events/React/Base.php new file mode 100755 index 0000000..d558b88 --- /dev/null +++ b/workerman/Events/React/Base.php @@ -0,0 +1,262 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; +use Workerman\Events\EventInterface; +use React\EventLoop\TimerInterface; + +/** + * Class StreamSelectLoop + * @package Workerman\Events\React + */ +class Base implements \React\EventLoop\LoopInterface +{ + /** + * @var array + */ + protected $_timerIdMap = array(); + + /** + * @var int + */ + protected $_timerIdIndex = 0; + + /** + * @var array + */ + protected $_signalHandlerMap = array(); + + /** + * @var \React\EventLoop\LoopInterface + */ + protected $_eventLoop = null; + + /** + * Base constructor. + */ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\StreamSelectLoop(); + } + + /** + * Add event listener to event loop. + * + * @param $fd + * @param $flag + * @param $func + * @param array $args + * @return bool + */ + public function add($fd, $flag, $func, $args = array()) + { + $args = (array)$args; + switch ($flag) { + case EventInterface::EV_READ: + return $this->addReadStream($fd, $func); + case EventInterface::EV_WRITE: + return $this->addWriteStream($fd, $func); + case EventInterface::EV_SIGNAL: + if (isset($this->_signalHandlerMap[$fd])) { + $this->removeSignal($fd, $this->_signalHandlerMap[$fd]); + } + $this->_signalHandlerMap[$fd] = $func; + return $this->addSignal($fd, $func); + case EventInterface::EV_TIMER: + $timer_obj = $this->addPeriodicTimer($fd, function() use ($func, $args) { + call_user_func_array($func, $args); + }); + $this->_timerIdMap[++$this->_timerIdIndex] = $timer_obj; + return $this->_timerIdIndex; + case EventInterface::EV_TIMER_ONCE: + $index = ++$this->_timerIdIndex; + $timer_obj = $this->addTimer($fd, function() use ($func, $args, $index) { + $this->del($index,EventInterface::EV_TIMER_ONCE); + call_user_func_array($func, $args); + }); + $this->_timerIdMap[$index] = $timer_obj; + return $this->_timerIdIndex; + } + return false; + } + + /** + * Remove event listener from event loop. + * + * @param mixed $fd + * @param int $flag + * @return bool + */ + public function del($fd, $flag) + { + switch ($flag) { + case EventInterface::EV_READ: + return $this->removeReadStream($fd); + case EventInterface::EV_WRITE: + return $this->removeWriteStream($fd); + case EventInterface::EV_SIGNAL: + if (!isset($this->_eventLoop[$fd])) { + return false; + } + $func = $this->_eventLoop[$fd]; + unset($this->_eventLoop[$fd]); + return $this->removeSignal($fd, $func); + + case EventInterface::EV_TIMER: + case EventInterface::EV_TIMER_ONCE: + if (isset($this->_timerIdMap[$fd])){ + $timer_obj = $this->_timerIdMap[$fd]; + unset($this->_timerIdMap[$fd]); + $this->cancelTimer($timer_obj); + return true; + } + } + return false; + } + + + /** + * Main loop. + * + * @return void + */ + public function loop() + { + $this->run(); + } + + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return count($this->_timerIdMap); + } + + /** + * @param resource $stream + * @param callable $listener + */ + public function addReadStream($stream, $listener) + { + return $this->_eventLoop->addReadStream($stream, $listener); + } + + /** + * @param resource $stream + * @param callable $listener + */ + public function addWriteStream($stream, $listener) + { + return $this->_eventLoop->addWriteStream($stream, $listener); + } + + /** + * @param resource $stream + */ + public function removeReadStream($stream) + { + return $this->_eventLoop->removeReadStream($stream); + } + + /** + * @param resource $stream + */ + public function removeWriteStream($stream) + { + return $this->_eventLoop->removeWriteStream($stream); + } + + /** + * @param float|int $interval + * @param callable $callback + * @return \React\EventLoop\Timer\Timer|TimerInterface + */ + public function addTimer($interval, $callback) + { + return $this->_eventLoop->addTimer($interval, $callback); + } + + /** + * @param float|int $interval + * @param callable $callback + * @return \React\EventLoop\Timer\Timer|TimerInterface + */ + public function addPeriodicTimer($interval, $callback) + { + return $this->_eventLoop->addPeriodicTimer($interval, $callback); + } + + /** + * @param TimerInterface $timer + */ + public function cancelTimer(TimerInterface $timer) + { + return $this->_eventLoop->cancelTimer($timer); + } + + /** + * @param callable $listener + */ + public function futureTick($listener) + { + return $this->_eventLoop->futureTick($listener); + } + + /** + * @param int $signal + * @param callable $listener + */ + public function addSignal($signal, $listener) + { + return $this->_eventLoop->addSignal($signal, $listener); + } + + /** + * @param int $signal + * @param callable $listener + */ + public function removeSignal($signal, $listener) + { + return $this->_eventLoop->removeSignal($signal, $listener); + } + + /** + * Run. + */ + public function run() + { + return $this->_eventLoop->run(); + } + + /** + * Stop. + */ + public function stop() + { + return $this->_eventLoop->stop(); + } +} diff --git a/workerman/Events/React/ExtEventLoop.php b/workerman/Events/React/ExtEventLoop.php new file mode 100755 index 0000000..3dab25b --- /dev/null +++ b/workerman/Events/React/ExtEventLoop.php @@ -0,0 +1,27 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; + +/** + * Class ExtEventLoop + * @package Workerman\Events\React + */ +class ExtEventLoop extends Base +{ + + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\ExtEventLoop(); + } +} diff --git a/workerman/Events/React/ExtLibEventLoop.php b/workerman/Events/React/ExtLibEventLoop.php new file mode 100755 index 0000000..eb02b35 --- /dev/null +++ b/workerman/Events/React/ExtLibEventLoop.php @@ -0,0 +1,27 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; +use Workerman\Events\EventInterface; + +/** + * Class ExtLibEventLoop + * @package Workerman\Events\React + */ +class ExtLibEventLoop extends Base +{ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\ExtLibeventLoop(); + } +} diff --git a/workerman/Events/React/StreamSelectLoop.php b/workerman/Events/React/StreamSelectLoop.php new file mode 100755 index 0000000..7f5f94b --- /dev/null +++ b/workerman/Events/React/StreamSelectLoop.php @@ -0,0 +1,26 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events\React; + +/** + * Class StreamSelectLoop + * @package Workerman\Events\React + */ +class StreamSelectLoop extends Base +{ + public function __construct() + { + $this->_eventLoop = new \React\EventLoop\StreamSelectLoop(); + } +} diff --git a/workerman/Events/Select.php b/workerman/Events/Select.php new file mode 100755 index 0000000..801586b --- /dev/null +++ b/workerman/Events/Select.php @@ -0,0 +1,340 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +/** + * select eventloop + */ +class Select implements EventInterface +{ + /** + * All listeners for read/write event. + * + * @var array + */ + public $_allEvents = array(); + + /** + * Event listeners of signal. + * + * @var array + */ + public $_signalEvents = array(); + + /** + * Fds waiting for read event. + * + * @var array + */ + protected $_readFds = array(); + + /** + * Fds waiting for write event. + * + * @var array + */ + protected $_writeFds = array(); + + /** + * Fds waiting for except event. + * + * @var array + */ + protected $_exceptFds = array(); + + /** + * Timer scheduler. + * {['data':timer_id, 'priority':run_timestamp], ..} + * + * @var \SplPriorityQueue + */ + protected $_scheduler = null; + + /** + * All timer event listeners. + * [[func, args, flag, timer_interval], ..] + * + * @var array + */ + protected $_eventTimer = array(); + + /** + * Timer id. + * + * @var int + */ + protected $_timerId = 1; + + /** + * Select timeout. + * + * @var int + */ + protected $_selectTimeout = 100000000; + + /** + * Paired socket channels + * + * @var array + */ + protected $channel = array(); + + /** + * Construct. + */ + public function __construct() + { + // Create a pipeline and put into the collection of the read to read the descriptor to avoid empty polling. + $this->channel = stream_socket_pair(DIRECTORY_SEPARATOR === '/' ? STREAM_PF_UNIX : STREAM_PF_INET, + STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + if($this->channel) { + stream_set_blocking($this->channel[0], 0); + $this->_readFds[0] = $this->channel[0]; + } + // Init SplPriorityQueue. + $this->_scheduler = new \SplPriorityQueue(); + $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + } + + /** + * {@inheritdoc} + */ + public function add($fd, $flag, $func, $args = array()) + { + switch ($flag) { + case self::EV_READ: + case self::EV_WRITE: + $count = $flag === self::EV_READ ? count($this->_readFds) : count($this->_writeFds); + if ($count >= 1024) { + echo "Warning: system call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.\n"; + } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) { + echo "Warning: system call select exceeded the maximum number of connections 256.\n"; + } + $fd_key = (int)$fd; + $this->_allEvents[$fd_key][$flag] = array($func, $fd); + if ($flag === self::EV_READ) { + $this->_readFds[$fd_key] = $fd; + } else { + $this->_writeFds[$fd_key] = $fd; + } + break; + case self::EV_EXCEPT: + $fd_key = (int)$fd; + $this->_allEvents[$fd_key][$flag] = array($func, $fd); + $this->_exceptFds[$fd_key] = $fd; + break; + case self::EV_SIGNAL: + // Windows not support signal. + if(DIRECTORY_SEPARATOR !== '/') { + return false; + } + $fd_key = (int)$fd; + $this->_signalEvents[$fd_key][$flag] = array($func, $fd); + pcntl_signal($fd, array($this, 'signalHandler')); + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $timer_id = $this->_timerId++; + $run_time = microtime(true) + $fd; + $this->_scheduler->insert($timer_id, -$run_time); + $this->_eventTimer[$timer_id] = array($func, (array)$args, $flag, $fd); + $select_timeout = ($run_time - microtime(true)) * 1000000; + if( $this->_selectTimeout > $select_timeout ){ + $this->_selectTimeout = $select_timeout; + } + return $timer_id; + } + + return true; + } + + /** + * Signal handler. + * + * @param int $signal + */ + public function signalHandler($signal) + { + call_user_func_array($this->_signalEvents[$signal][self::EV_SIGNAL][0], array($signal)); + } + + /** + * {@inheritdoc} + */ + public function del($fd, $flag) + { + $fd_key = (int)$fd; + switch ($flag) { + case self::EV_READ: + unset($this->_allEvents[$fd_key][$flag], $this->_readFds[$fd_key]); + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_WRITE: + unset($this->_allEvents[$fd_key][$flag], $this->_writeFds[$fd_key]); + if (empty($this->_allEvents[$fd_key])) { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_EXCEPT: + unset($this->_allEvents[$fd_key][$flag], $this->_exceptFds[$fd_key]); + if(empty($this->_allEvents[$fd_key])) + { + unset($this->_allEvents[$fd_key]); + } + return true; + case self::EV_SIGNAL: + if(DIRECTORY_SEPARATOR !== '/') { + return false; + } + unset($this->_signalEvents[$fd_key]); + pcntl_signal($fd, SIG_IGN); + break; + case self::EV_TIMER: + case self::EV_TIMER_ONCE; + unset($this->_eventTimer[$fd_key]); + return true; + } + return false; + } + + /** + * Tick for timer. + * + * @return void + */ + protected function tick() + { + 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 = ($next_run_time - $time_now) * 1000000; + if ($this->_selectTimeout <= 0) { + $this->_scheduler->extract(); + + if (!isset($this->_eventTimer[$timer_id])) { + continue; + } + + // [func, args, flag, timer_interval] + $task_data = $this->_eventTimer[$timer_id]; + if ($task_data[2] === self::EV_TIMER) { + $next_run_time = $time_now + $task_data[3]; + $this->_scheduler->insert($timer_id, -$next_run_time); + } + call_user_func_array($task_data[0], $task_data[1]); + if (isset($this->_eventTimer[$timer_id]) && $task_data[2] === self::EV_TIMER_ONCE) { + $this->del($timer_id, self::EV_TIMER_ONCE); + } + continue; + } + return; + } + $this->_selectTimeout = 100000000; + } + + /** + * {@inheritdoc} + */ + public function clearAllTimer() + { + $this->_scheduler = new \SplPriorityQueue(); + $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + $this->_eventTimer = array(); + } + + /** + * {@inheritdoc} + */ + public function loop() + { + while (1) { + if(DIRECTORY_SEPARATOR === '/') { + // Calls signal handlers for pending signals + pcntl_signal_dispatch(); + } + + $read = $this->_readFds; + $write = $this->_writeFds; + $except = $this->_exceptFds; + + // Waiting read/write/signal/timeout events. + set_error_handler(function(){}); + $ret = stream_select($read, $write, $except, 0, $this->_selectTimeout); + restore_error_handler(); + + + if (!$this->_scheduler->isEmpty()) { + $this->tick(); + } + + if (!$ret) { + continue; + } + + if ($read) { + foreach ($read as $fd) { + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][self::EV_READ])) { + call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0], + array($this->_allEvents[$fd_key][self::EV_READ][1])); + } + } + } + + if ($write) { + foreach ($write as $fd) { + $fd_key = (int)$fd; + if (isset($this->_allEvents[$fd_key][self::EV_WRITE])) { + call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0], + array($this->_allEvents[$fd_key][self::EV_WRITE][1])); + } + } + } + + if($except) { + foreach($except as $fd) { + $fd_key = (int) $fd; + if(isset($this->_allEvents[$fd_key][self::EV_EXCEPT])) { + call_user_func_array($this->_allEvents[$fd_key][self::EV_EXCEPT][0], + array($this->_allEvents[$fd_key][self::EV_EXCEPT][1])); + } + } + } + } + } + + /** + * Destroy loop. + * + * @return void + */ + public function destroy() + { + + } + + /** + * Get timer count. + * + * @return integer + */ + public function getTimerCount() + { + return count($this->_eventTimer); + } +} diff --git a/workerman/Events/Swoole.php b/workerman/Events/Swoole.php new file mode 100755 index 0000000..40ef499 --- /dev/null +++ b/workerman/Events/Swoole.php @@ -0,0 +1,221 @@ + + * @link http://www.workerman.net/ + * @link https://github.com/ares333/Workerman + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Events; + +use Swoole\Event; +use Swoole\Timer; + +class Swoole implements EventInterface +{ + + protected $_timer = array(); + + protected $_timerOnceMap = array(); + + protected $mapId = 0; + + protected $_fd = array(); + + // milisecond + public static $signalDispatchInterval = 200; + + protected $_hasSignal = false; + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::add() + */ + public function add($fd, $flag, $func, $args = null) + { + if (! isset($args)) { + $args = array(); + } + switch ($flag) { + case self::EV_SIGNAL: + $res = pcntl_signal($fd, $func, false); + if (! $this->_hasSignal && $res) { + Timer::tick(static::$signalDispatchInterval, + function () { + pcntl_signal_dispatch(); + }); + $this->_hasSignal = true; + } + return $res; + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + $method = self::EV_TIMER == $flag ? 'tick' : 'after'; + if ($this->mapId > PHP_INT_MAX) { + $this->mapId = 0; + } + $mapId = $this->mapId++; + $timer_id = Timer::$method($fd * 1000, + function ($timer_id = null) use ($func, $args, $mapId) { + call_user_func_array($func, $args); + // EV_TIMER_ONCE + if (! isset($timer_id)) { + // may be deleted in $func + if (array_key_exists($mapId, $this->_timerOnceMap)) { + $timer_id = $this->_timerOnceMap[$mapId]; + unset($this->_timer[$timer_id], + $this->_timerOnceMap[$mapId]); + } + } + }); + if ($flag == self::EV_TIMER_ONCE) { + $this->_timerOnceMap[$mapId] = $timer_id; + $this->_timer[$timer_id] = $mapId; + } else { + $this->_timer[$timer_id] = null; + } + return $timer_id; + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int) $fd; + if (! isset($this->_fd[$fd_key])) { + if ($flag == self::EV_READ) { + $res = Event::add($fd, $func, null, SWOOLE_EVENT_READ); + $fd_type = SWOOLE_EVENT_READ; + } else { + $res = Event::add($fd, null, $func, SWOOLE_EVENT_WRITE); + $fd_type = SWOOLE_EVENT_WRITE; + } + if ($res) { + $this->_fd[$fd_key] = $fd_type; + } + } else { + $fd_val = $this->_fd[$fd_key]; + $res = true; + if ($flag == self::EV_READ) { + if (($fd_val & SWOOLE_EVENT_READ) != SWOOLE_EVENT_READ) { + $res = Event::set($fd, $func, null, + SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); + $this->_fd[$fd_key] |= SWOOLE_EVENT_READ; + } + } else { + if (($fd_val & SWOOLE_EVENT_WRITE) != SWOOLE_EVENT_WRITE) { + $res = Event::set($fd, null, $func, + SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE); + $this->_fd[$fd_key] |= SWOOLE_EVENT_WRITE; + } + } + } + return $res; + } + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::del() + */ + public function del($fd, $flag) + { + switch ($flag) { + case self::EV_SIGNAL: + return pcntl_signal($fd, SIG_IGN, false); + case self::EV_TIMER: + case self::EV_TIMER_ONCE: + // already remove in EV_TIMER_ONCE callback. + if (! array_key_exists($fd, $this->_timer)) { + return true; + } + $res = Timer::clear($fd); + if ($res) { + $mapId = $this->_timer[$fd]; + if (isset($mapId)) { + unset($this->_timerOnceMap[$mapId]); + } + unset($this->_timer[$fd]); + } + return $res; + case self::EV_READ: + case self::EV_WRITE: + $fd_key = (int) $fd; + if (isset($this->_fd[$fd_key])) { + $fd_val = $this->_fd[$fd_key]; + if ($flag == self::EV_READ) { + $flag_remove = ~ SWOOLE_EVENT_READ; + } else { + $flag_remove = ~ SWOOLE_EVENT_WRITE; + } + $fd_val &= $flag_remove; + if (0 === $fd_val) { + $res = Event::del($fd); + if ($res) { + unset($this->_fd[$fd_key]); + } + } else { + $res = Event::set($fd, null, null, $fd_val); + if ($res) { + $this->_fd[$fd_key] = $fd_val; + } + } + } else { + $res = true; + } + return $res; + } + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::clearAllTimer() + */ + public function clearAllTimer() + { + foreach (array_keys($this->_timer) as $v) { + Timer::clear($v); + } + $this->_timer = array(); + $this->_timerOnceMap = array(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::loop() + */ + public function loop() + { + Event::wait(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::destroy() + */ + public function destroy() + { + //Event::exit(); + } + + /** + * + * {@inheritdoc} + * + * @see \Workerman\Events\EventInterface::getTimerCount() + */ + public function getTimerCount() + { + return count($this->_timer); + } +} diff --git a/workerman/Lib/Constants.php b/workerman/Lib/Constants.php new file mode 100755 index 0000000..1b3561b --- /dev/null +++ b/workerman/Lib/Constants.php @@ -0,0 +1,40 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +// Display errors. +ini_set('display_errors', 'on'); +// Reporting all. +error_reporting(E_ALL); + +// Reset opcache. +if (function_exists('opcache_reset')) { + opcache_reset(); +} + +// For onError callback. +define('WORKERMAN_CONNECT_FAIL', 1); +// For onError callback. +define('WORKERMAN_SEND_FAIL', 2); + +// Define OS Type +define('OS_TYPE_LINUX', 'linux'); +define('OS_TYPE_WINDOWS', 'windows'); + +// Compatible with php7 +if(!class_exists('Error')) +{ + class Error extends Exception + { + } +} diff --git a/workerman/Lib/Timer.php b/workerman/Lib/Timer.php new file mode 100755 index 0000000..2dc2302 --- /dev/null +++ b/workerman/Lib/Timer.php @@ -0,0 +1,179 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Lib; + +use Workerman\Events\EventInterface; +use Workerman\Worker; +use Exception; + +/** + * Timer. + * + * example: + * Workerman\Lib\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 \Workerman\Events\EventInterface + */ + protected static $_event = null; + + /** + * Init. + * + * @param \Workerman\Events\EventInterface $event + * @return void + */ + public static function init($event = null) + { + if ($event) { + self::$_event = $event; + } else { + 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 (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); + } + + $time_now = time(); + $run_time = $time_now + $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(); + } + } +} diff --git a/workerman/Lib/Tool.php b/workerman/Lib/Tool.php new file mode 100644 index 0000000..2f5ae95 --- /dev/null +++ b/workerman/Lib/Tool.php @@ -0,0 +1,44 @@ + and contributors (see https://github.com/walkor/workerman/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/workerman/Protocols/Frame.php b/workerman/Protocols/Frame.php new file mode 100755 index 0000000..4a6b13e --- /dev/null +++ b/workerman/Protocols/Frame.php @@ -0,0 +1,61 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\TcpConnection; + +/** + * Frame Protocol. + */ +class Frame +{ + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param TcpConnection $connection + * @return int + */ + public static function input($buffer, TcpConnection $connection) + { + if (strlen($buffer) < 4) { + return 0; + } + $unpack_data = unpack('Ntotal_length', $buffer); + return $unpack_data['total_length']; + } + + /** + * Decode. + * + * @param string $buffer + * @return string + */ + public static function decode($buffer) + { + return substr($buffer, 4); + } + + /** + * Encode. + * + * @param string $buffer + * @return string + */ + public static function encode($buffer) + { + $total_length = 4 + strlen($buffer); + return pack('N', $total_length) . $buffer; + } +} diff --git a/workerman/Protocols/Http.php b/workerman/Protocols/Http.php new file mode 100755 index 0000000..12f7a2f --- /dev/null +++ b/workerman/Protocols/Http.php @@ -0,0 +1,701 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\TcpConnection; +use Workerman\Worker; + +/** + * http protocol + */ +class Http +{ + /** + * The supported HTTP methods + * @var array + */ + public static $methods = array('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'); + + /** + * Check the integrity of the package. + * + * @param string $recv_buffer + * @param TcpConnection $connection + * @return int + */ + public static function input($recv_buffer, TcpConnection $connection) + { + if (!strpos($recv_buffer, "\r\n\r\n")) { + // Judge whether the package length exceeds the limit. + if (strlen($recv_buffer) >= $connection->maxPackageSize) { + $connection->close(); + return 0; + } + return 0; + } + + list($header,) = explode("\r\n\r\n", $recv_buffer, 2); + $method = substr($header, 0, strpos($header, ' ')); + + if(in_array($method, static::$methods)) { + return static::getRequestSize($header, $method); + }else{ + $connection->send("HTTP/1.1 400 Bad Request\r\n\r\n", true); + return 0; + } + } + + /** + * Get whole size of the request + * includes the request headers and request body. + * @param string $header The request headers + * @param string $method The request method + * @return integer + */ + protected static function getRequestSize($header, $method) + { + if($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD') { + return strlen($header) + 4; + } + $match = array(); + if (preg_match("/\r\nContent-Length: ?(\d+)/i", $header, $match)) { + $content_length = isset($match[1]) ? $match[1] : 0; + return $content_length + strlen($header) + 4; + } + return $method === 'DELETE' ? strlen($header) + 4 : 0; + } + + /** + * Parse $_POST、$_GET、$_COOKIE. + * + * @param string $recv_buffer + * @param TcpConnection $connection + * @return array + */ + public static function decode($recv_buffer, TcpConnection $connection) + { + // Init. + $_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = array(); + $GLOBALS['HTTP_RAW_POST_DATA'] = ''; + // Clear cache. + HttpCache::$header = array('Connection' => 'Connection: keep-alive'); + HttpCache::$instance = new HttpCache(); + // $_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' => time() + ); + + // 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 \Workerman\Protocols\Websocket::input($recv_buffer,$connection); + } + break; + } + } + if(isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE){ + HttpCache::$gzip = true; + } + // Parse $_POST. + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (isset($_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(); + + return array('get' => $_GET, 'post' => $_POST, 'cookie' => $_COOKIE, 'server' => $_SERVER, 'files' => $_FILES); + } + + /** + * Http encode. + * + * @param string $content + * @param TcpConnection $connection + * @return string + */ + public static function encode($content, TcpConnection $connection) + { + // Default http-code. + if (!isset(HttpCache::$header['Http-Code'])) { + $header = "HTTP/1.1 200 OK\r\n"; + } else { + $header = HttpCache::$header['Http-Code'] . "\r\n"; + unset(HttpCache::$header['Http-Code']); + } + + // Content-Type + if (!isset(HttpCache::$header['Content-Type'])) { + $header .= "Content-Type: text/html;charset=utf-8\r\n"; + } + + // other headers + foreach (HttpCache::$header as $key => $item) { + if ('Set-Cookie' === $key && is_array($item)) { + foreach ($item as $it) { + $header .= $it . "\r\n"; + } + } else { + $header .= $item . "\r\n"; + } + } + if(HttpCache::$gzip && isset($connection->gzip) && $connection->gzip){ + $header .= "Content-Encoding: gzip\r\n"; + $content = gzencode($content,$connection->gzip); + } + // header + $header .= "Server: workerman/" . Worker::VERSION . "\r\nContent-Length: " . strlen($content) . "\r\n\r\n"; + + // save session + self::sessionWriteClose(); + + // the whole http package + return $header . $content; + } + + /** + * 设置http头 + * + * @return bool|void + */ + public static function header($content, $replace = true, $http_response_code = 0) + { + if (PHP_SAPI != 'cli') { + return $http_response_code ? header($content, $replace, $http_response_code) : header($content, $replace); + } + if (strpos($content, 'HTTP') === 0) { + $key = 'Http-Code'; + } else { + $key = strstr($content, ":", true); + if (empty($key)) { + return false; + } + } + + if ('location' === strtolower($key) && !$http_response_code) { + return self::header($content, true, 302); + } + + if (isset(HttpCache::$codes[$http_response_code])) { + HttpCache::$header['Http-Code'] = "HTTP/1.1 $http_response_code " . HttpCache::$codes[$http_response_code]; + if ($key === 'Http-Code') { + return true; + } + } + + if ($key === 'Set-Cookie') { + HttpCache::$header[$key][] = $content; + } else { + HttpCache::$header[$key] = $content; + } + + return true; + } + + /** + * Remove header. + * + * @param string $name + * @return void + */ + public static function headerRemove($name) + { + if (PHP_SAPI != 'cli') { + header_remove($name); + return; + } + unset(HttpCache::$header[$name]); + } + + /** + * 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 (PHP_SAPI != 'cli') { + return setcookie($name, $value, $maxage, $path, $domain, $secure, $HTTPOnly); + } + return self::header( + 'Set-Cookie: ' . $name . '=' . rawurlencode($value) + . (empty($domain) ? '' : '; Domain=' . $domain) + . (empty($maxage) ? '' : '; Max-Age=' . $maxage) + . (empty($path) ? '' : '; Path=' . $path) + . (!$secure ? '' : '; Secure') + . (!$HTTPOnly ? '' : '; HttpOnly'), false); + } + + /** + * sessionCreateId + * + * @return string + */ + public static function sessionCreateId() + { + mt_srand(); + return bin2hex(pack('d', microtime(true)) . pack('N',mt_rand(0, 2147483647))); + } + + /** + * sessionId + * + * @param string $id + * + * @return string|null + */ + public static function sessionId($id = null) + { + if (PHP_SAPI != 'cli') { + return $id ? session_id($id) : session_id(); + } + if (static::sessionStarted() && HttpCache::$instance->sessionFile) { + return str_replace('ses_', '', basename(HttpCache::$instance->sessionFile)); + } + return ''; + } + + /** + * sessionName + * + * @param string $name + * + * @return string + */ + public static function sessionName($name = null) + { + if (PHP_SAPI != 'cli') { + return $name ? session_name($name) : session_name(); + } + $session_name = HttpCache::$sessionName; + if ($name && ! static::sessionStarted()) { + HttpCache::$sessionName = $name; + } + return $session_name; + } + + /** + * sessionSavePath + * + * @param string $path + * + * @return void + */ + public static function sessionSavePath($path = null) + { + if (PHP_SAPI != 'cli') { + return $path ? session_save_path($path) : session_save_path(); + } + if ($path && is_dir($path) && is_writable($path) && !static::sessionStarted()) { + HttpCache::$sessionPath = $path; + } + return HttpCache::$sessionPath; + } + + /** + * sessionStarted + * + * @return bool + */ + public static function sessionStarted() + { + if (!HttpCache::$instance) return false; + + return HttpCache::$instance->sessionStarted; + } + + /** + * sessionStart + * + * @return bool + */ + public static function sessionStart() + { + if (PHP_SAPI != 'cli') { + return session_start(); + } + + self::tryGcSessions(); + + 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); + } + } + return true; + } + + /** + * Save session. + * + * @return bool + */ + public static function sessionWriteClose() + { + if (PHP_SAPI != 'cli') { + return session_write_close(); + } + if (!empty(HttpCache::$instance->sessionStarted) && !empty($_SESSION)) { + $session_str = serialize($_SESSION); + if ($session_str && HttpCache::$instance->sessionFile) { + return file_put_contents(HttpCache::$instance->sessionFile, $session_str); + } + } + return empty($_SESSION); + } + + /** + * End, like call exit in php-fpm. + * + * @param string $msg + * @throws \Exception + */ + public static function end($msg = '') + { + if (PHP_SAPI != 'cli') { + exit($msg); + } + if ($msg) { + echo $msg; + } + throw new \Exception('jump_exit'); + } + + /** + * Get mime types. + * + * @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) + { + $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; + } + } + break; + case "content-type": + // add file_type + $_FILES[$key]['file_type'] = trim($header_value); + break; + } + } + } + } + + /** + * Try GC sessions. + * + * @return void + */ + public static function tryGcSessions() + { + if (HttpCache::$sessionGcProbability <= 0 || + HttpCache::$sessionGcDivisor <= 0 || + rand(1, HttpCache::$sessionGcDivisor) > HttpCache::$sessionGcProbability) { + return; + } + + $time_now = time(); + foreach(glob(HttpCache::$sessionPath.'/ses*') as $file) { + if(is_file($file) && $time_now - filemtime($file) > HttpCache::$sessionGcMaxLifeTime) { + unlink($file); + } + } + } +} + +/** + * 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', + ); + + /** + * @var HttpCache + */ + public static $instance = null; + public static $header = array(); + public static $gzip = false; + 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 init() + { + 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; + } + } +} + +HttpCache::init(); diff --git a/workerman/Protocols/Http/mime.types b/workerman/Protocols/Http/mime.types new file mode 100755 index 0000000..8a218b2 --- /dev/null +++ b/workerman/Protocols/Http/mime.types @@ -0,0 +1,80 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/x-javascript js; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/png png; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + image/svg+xml svg svgz; + image/webp webp; + + application/java-archive jar war ear; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.ms-excel xls; + application/vnd.ms-powerpoint ppt; + application/vnd.wap.wmlc wmlc; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream eot; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/workerman/Protocols/ProtocolInterface.php b/workerman/Protocols/ProtocolInterface.php new file mode 100755 index 0000000..9afe984 --- /dev/null +++ b/workerman/Protocols/ProtocolInterface.php @@ -0,0 +1,52 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\ConnectionInterface; + +/** + * Protocol interface + */ +interface ProtocolInterface +{ + /** + * Check the integrity of the package. + * Please return the length of package. + * If length is unknow please return 0 that mean wating more data. + * If the package has something wrong please return false the connection will be closed. + * + * @param ConnectionInterface $connection + * @param string $recv_buffer + * @return int|false + */ + public static function input($recv_buffer, ConnectionInterface $connection); + + /** + * Decode package and emit onMessage($message) callback, $message is the result that decode returned. + * + * @param ConnectionInterface $connection + * @param string $recv_buffer + * @return mixed + */ + public static function decode($recv_buffer, ConnectionInterface $connection); + + /** + * Encode package brefore sending to client. + * + * @param ConnectionInterface $connection + * @param mixed $data + * @return string + */ + public static function encode($data, ConnectionInterface $connection); +} diff --git a/workerman/Protocols/Text.php b/workerman/Protocols/Text.php new file mode 100755 index 0000000..84b937a --- /dev/null +++ b/workerman/Protocols/Text.php @@ -0,0 +1,70 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\TcpConnection; + +/** + * Text Protocol. + */ +class Text +{ + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param TcpConnection $connection + * @return int + */ + public static function input($buffer, TcpConnection $connection) + { + // Judge whether the package length exceeds the limit. + if (strlen($buffer) >= $connection->maxPackageSize) { + $connection->close(); + return 0; + } + // Find the position of "\n". + $pos = strpos($buffer, "\n"); + // No "\n", packet length is unknown, continue to wait for the data so return 0. + if ($pos === false) { + return 0; + } + // Return the current package length. + return $pos + 1; + } + + /** + * Encode. + * + * @param string $buffer + * @return string + */ + public static function encode($buffer) + { + // Add "\n" + return $buffer . "\n"; + } + + /** + * Decode. + * + * @param string $buffer + * @return string + */ + public static function decode($buffer) + { + // Remove "\n" + return rtrim($buffer, "\r\n"); + } +} diff --git a/workerman/Protocols/Websocket.php b/workerman/Protocols/Websocket.php new file mode 100755 index 0000000..6237f21 --- /dev/null +++ b/workerman/Protocols/Websocket.php @@ -0,0 +1,503 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Connection\ConnectionInterface; +use Workerman\Connection\TcpConnection; +use Workerman\Worker; + +/** + * WebSocket protocol. + */ +class Websocket implements \Workerman\Protocols\ProtocolInterface +{ + /** + * Websocket blob type. + * + * @var string + */ + const BINARY_TYPE_BLOB = "\x81"; + + /** + * Websocket arraybuffer type. + * + * @var string + */ + const BINARY_TYPE_ARRAYBUFFER = "\x82"; + + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return int + */ + public static function input($buffer, ConnectionInterface $connection) + { + // Receive length. + $recv_len = strlen($buffer); + // We need more data. + if ($recv_len < 6) { + return 0; + } + + // Has not yet completed the handshake. + if (empty($connection->websocketHandshake)) { + return static::dealHandshake($buffer, $connection); + } + + // Buffer websocket frame data. + if ($connection->websocketCurrentFrameLength) { + // We need more frame data. + if ($connection->websocketCurrentFrameLength > $recv_len) { + // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. + return 0; + } + } else { + $firstbyte = ord($buffer[0]); + $secondbyte = ord($buffer[1]); + $data_len = $secondbyte & 127; + $is_fin_frame = $firstbyte >> 7; + $masked = $secondbyte >> 7; + + if (!$masked) { + Worker::safeEcho("frame not masked so close the connection\n"); + $connection->close(); + return 0; + } + + $opcode = $firstbyte & 0xf; + switch ($opcode) { + case 0x0: + break; + // Blob type. + case 0x1: + break; + // Arraybuffer type. + case 0x2: + break; + // Close package. + case 0x8: + // Try to emit onWebSocketClose callback. + if (isset($connection->onWebSocketClose) || isset($connection->worker->onWebSocketClose)) { + try { + call_user_func(isset($connection->onWebSocketClose)?$connection->onWebSocketClose:$connection->worker->onWebSocketClose, $connection); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } // Close connection. + else { + $connection->close("\x88\x02\x27\x10", true); + } + return 0; + // Ping package. + case 0x9: + break; + // Pong package. + case 0xa: + break; + // Wrong opcode. + default : + Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n"); + $connection->close(); + return 0; + } + + // Calculate packet length. + $head_len = 6; + if ($data_len === 126) { + $head_len = 8; + if ($head_len > $recv_len) { + return 0; + } + $pack = unpack('nn/ntotal_len', $buffer); + $data_len = $pack['total_len']; + } else { + if ($data_len === 127) { + $head_len = 14; + if ($head_len > $recv_len) { + return 0; + } + $arr = unpack('n/N2c', $buffer); + $data_len = $arr['c1']*4294967296 + $arr['c2']; + } + } + $current_frame_length = $head_len + $data_len; + + $total_package_size = strlen($connection->websocketDataBuffer) + $current_frame_length; + if ($total_package_size > $connection->maxPackageSize) { + Worker::safeEcho("error package. package_length=$total_package_size\n"); + $connection->close(); + return 0; + } + + if ($is_fin_frame) { + if ($opcode === 0x9) { + 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; + $connection->websocketType = "\x8a"; + if (isset($connection->onWebSocketPing) || isset($connection->worker->onWebSocketPing)) { + try { + call_user_func(isset($connection->onWebSocketPing)?$connection->onWebSocketPing:$connection->worker->onWebSocketPing, $connection, $ping_data); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } else { + $connection->send($ping_data); + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(substr($buffer, $current_frame_length), $connection); + } + } + return 0; + } else if ($opcode === 0xa) { + 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; + $connection->websocketType = "\x8a"; + // Try to emit onWebSocketPong callback. + if (isset($connection->onWebSocketPong) || isset($connection->worker->onWebSocketPong)) { + try { + call_user_func(isset($connection->onWebSocketPong)?$connection->onWebSocketPong:$connection->worker->onWebSocketPong, $connection, $pong_data); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(substr($buffer, $current_frame_length), $connection); + } + } + return 0; + } + return $current_frame_length; + } else { + $connection->websocketCurrentFrameLength = $current_frame_length; + } + } + + // Received just a frame length data. + if ($connection->websocketCurrentFrameLength === $recv_len) { + static::decode($buffer, $connection); + $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength); + $connection->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; + // 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. + else { + return 0; + } + } + + /** + * Websocket encode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function encode($buffer, ConnectionInterface $connection) + { + if (!is_scalar($buffer)) { + throw new \Exception("You can't send(" . gettype($buffer) . ") to client, you need to convert it to a string. "); + } + $len = strlen($buffer); + if (empty($connection->websocketType)) { + $connection->websocketType = static::BINARY_TYPE_BLOB; + } + + $first_byte = $connection->websocketType; + + if ($len <= 125) { + $encode_buffer = $first_byte . chr($len) . $buffer; + } else { + if ($len <= 65535) { + $encode_buffer = $first_byte . chr(126) . pack("n", $len) . $buffer; + } else { + $encode_buffer = $first_byte . chr(127) . pack("xxxxN", $len) . $buffer; + } + } + + // Handshake not completed so temporary buffer websocket data waiting for send. + if (empty($connection->websocketHandshake)) { + if (empty($connection->tmpWebsocketData)) { + $connection->tmpWebsocketData = ''; + } + // If buffer has already full then discard the current package. + if (strlen($connection->tmpWebsocketData) > $connection->maxSendBufferSize) { + if ($connection->onError) { + try { + call_user_func($connection->onError, $connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return ''; + } + $connection->tmpWebsocketData .= $encode_buffer; + // Check buffer is full. + if ($connection->maxSendBufferSize <= strlen($connection->tmpWebsocketData)) { + if ($connection->onBufferFull) { + try { + call_user_func($connection->onBufferFull, $connection); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + + // Return empty string. + return ''; + } + + return $encode_buffer; + } + + /** + * Websocket decode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function decode($buffer, ConnectionInterface $connection) + { + $len = ord($buffer[1]) & 127; + if ($len === 126) { + $masks = substr($buffer, 4, 4); + $data = substr($buffer, 8); + } else { + if ($len === 127) { + $masks = substr($buffer, 10, 4); + $data = substr($buffer, 14); + } else { + $masks = substr($buffer, 2, 4); + $data = substr($buffer, 6); + } + } + $dataLength = strlen($data); + $masks = str_repeat($masks, floor($dataLength / 4)) . substr($masks, 0, $dataLength % 4); + $decoded = $data ^ $masks; + if ($connection->websocketCurrentFrameLength) { + $connection->websocketDataBuffer .= $decoded; + return $connection->websocketDataBuffer; + } else { + if ($connection->websocketDataBuffer !== '') { + $decoded = $connection->websocketDataBuffer . $decoded; + $connection->websocketDataBuffer = ''; + } + return $decoded; + } + } + + /** + * Websocket handshake. + * + * @param string $buffer + * @param \Workerman\Connection\TcpConnection $connection + * @return int + */ + protected static function dealHandshake($buffer, $connection) + { + // 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) { + return 0; + } + $header_length = $heder_end_pos + 4; + + // Get Sec-WebSocket-Key. + $Sec_WebSocket_Key = ''; + if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) { + $Sec_WebSocket_Key = $match[1]; + } else { + $connection->send("HTTP/1.1 200 Websocket\r\nServer: workerman/".Worker::VERSION."\r\n\r\n", + true); + $connection->close(); + return 0; + } + // Calculation websocket key. + $new_key = base64_encode(sha1($Sec_WebSocket_Key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); + // Handshake response data. + $handshake_message = "HTTP/1.1 101 Switching Protocols\r\n"; + $handshake_message .= "Upgrade: websocket\r\n"; + $handshake_message .= "Sec-WebSocket-Version: 13\r\n"; + $handshake_message .= "Connection: Upgrade\r\n"; + $handshake_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n"; + + // Websocket data buffer. + $connection->websocketDataBuffer = ''; + // Current websocket frame length. + $connection->websocketCurrentFrameLength = 0; + // Current websocket frame data. + $connection->websocketCurrentFrameBuffer = ''; + // Consume handshake data. + $connection->consumeRecvBuffer($header_length); + + // blob or arraybuffer + if (empty($connection->websocketType)) { + $connection->websocketType = static::BINARY_TYPE_BLOB; + } + + $has_server_header = false; + + // Try to emit onWebSocketConnect callback. + if (isset($connection->onWebSocketConnect) || isset($connection->worker->onWebSocketConnect)) { + static::parseHttpHeader($buffer); + try { + call_user_func(isset($connection->onWebSocketConnect)?$connection->onWebSocketConnect:$connection->worker->onWebSocketConnect, $connection, $buffer); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + if (!empty($_SESSION) && class_exists('\GatewayWorker\Lib\Context')) { + $connection->session = \GatewayWorker\Lib\Context::sessionEncode($_SESSION); + } + $_GET = $_SERVER = $_SESSION = $_COOKIE = array(); + + if (isset($connection->headers)) { + if (is_array($connection->headers)) { + foreach ($connection->headers as $header) { + if (strpos($header, 'Server:') === 0) { + $has_server_header = true; + } + $handshake_message .= "$header\r\n"; + } + } else { + $handshake_message .= "$connection->headers\r\n"; + } + } + } + if (!$has_server_header) { + $handshake_message .= "Server: workerman/".Worker::VERSION."\r\n"; + } + $handshake_message .= "\r\n"; + // Send handshake response. + $connection->send($handshake_message, true); + // Mark handshake complete.. + $connection->websocketHandshake = true; + // There are data waiting to be sent. + if (!empty($connection->tmpWebsocketData)) { + $connection->send($connection->tmpWebsocketData, true); + $connection->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, 'send($policy_xml, true); + $connection->consumeRecvBuffer(strlen($buffer)); + return 0; + } + // Bad websocket handshake request. + $connection->send("HTTP/1.1 200 Websocket\r\nServer: workerman/".Worker::VERSION."\r\n\r\n", + true); + $connection->close(); + return 0; + } + + /** + * Parse http header. + * + * @param string $buffer + * @return void + */ + protected static function parseHttpHeader($buffer) + { + // Parse headers. + list($http_header, ) = explode("\r\n\r\n", $buffer, 2); + $header_data = explode("\r\n", $http_header); + + if ($_SERVER) { + $_SERVER = array(); + } + + list($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']) = explode(' ', + $header_data[0]); + + 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; + } + } + + // 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'] = ''; + } + } +} diff --git a/workerman/Protocols/Ws.php b/workerman/Protocols/Ws.php new file mode 100755 index 0000000..e3ce4a6 --- /dev/null +++ b/workerman/Protocols/Ws.php @@ -0,0 +1,471 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman\Protocols; + +use Workerman\Worker; +use Workerman\Lib\Timer; +use Workerman\Connection\TcpConnection; + +/** + * Websocket protocol for client. + */ +class Ws +{ + /** + * Websocket blob type. + * + * @var string + */ + const BINARY_TYPE_BLOB = "\x81"; + + /** + * Websocket arraybuffer type. + * + * @var string + */ + const BINARY_TYPE_ARRAYBUFFER = "\x82"; + + /** + * Check the integrity of the package. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return int + */ + public static function input($buffer, $connection) + { + if (empty($connection->handshakeStep)) { + Worker::safeEcho("recv data before handshake. Buffer:" . bin2hex($buffer) . "\n"); + return false; + } + // Recv handshake response + if ($connection->handshakeStep === 1) { + return self::dealHandshake($buffer, $connection); + } + $recv_len = strlen($buffer); + if ($recv_len < 2) { + return 0; + } + // Buffer websocket frame data. + if ($connection->websocketCurrentFrameLength) { + // We need more frame data. + if ($connection->websocketCurrentFrameLength > $recv_len) { + // Return 0, because it is not clear the full packet length, waiting for the frame of fin=1. + return 0; + } + } else { + + $firstbyte = ord($buffer[0]); + $secondbyte = ord($buffer[1]); + $data_len = $secondbyte & 127; + $is_fin_frame = $firstbyte >> 7; + $masked = $secondbyte >> 7; + + if ($masked) { + Worker::safeEcho("frame masked so close the connection\n"); + $connection->close(); + return 0; + } + + $opcode = $firstbyte & 0xf; + + switch ($opcode) { + case 0x0: + break; + // Blob type. + case 0x1: + break; + // Arraybuffer type. + case 0x2: + break; + // Close package. + case 0x8: + // Try to emit onWebSocketClose callback. + if (isset($connection->onWebSocketClose)) { + try { + call_user_func($connection->onWebSocketClose, $connection); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } // Close connection. + else { + $connection->close(); + } + return 0; + // Ping package. + case 0x9: + break; + // Pong package. + case 0xa: + break; + // Wrong opcode. + default : + Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n"); + $connection->close(); + return 0; + } + // Calculate packet length. + if ($data_len === 126) { + if (strlen($buffer) < 4) { + return 0; + } + $pack = unpack('nn/ntotal_len', $buffer); + $current_frame_length = $pack['total_len'] + 4; + } else if ($data_len === 127) { + if (strlen($buffer) < 10) { + return 0; + } + $arr = unpack('n/N2c', $buffer); + $current_frame_length = $arr['c1']*4294967296 + $arr['c2'] + 10; + } else { + $current_frame_length = $data_len + 2; + } + + $total_package_size = strlen($connection->websocketDataBuffer) + $current_frame_length; + if ($total_package_size > $connection->maxPackageSize) { + Worker::safeEcho("error package. package_length=$total_package_size\n"); + $connection->close(); + return 0; + } + + if ($is_fin_frame) { + if ($opcode === 0x9) { + 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; + $connection->websocketType = "\x8a"; + if (isset($connection->onWebSocketPing)) { + try { + call_user_func($connection->onWebSocketPing, $connection, $ping_data); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } else { + $connection->send($ping_data); + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(substr($buffer, $current_frame_length), $connection); + } + } + return 0; + + } else if ($opcode === 0xa) { + 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; + $connection->websocketType = "\x8a"; + // Try to emit onWebSocketPong callback. + if (isset($connection->onWebSocketPong)) { + try { + call_user_func($connection->onWebSocketPong, $connection, $pong_data); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + $connection->websocketType = $tmp_connection_type; + if ($recv_len > $current_frame_length) { + return static::input(substr($buffer, $current_frame_length), $connection); + } + } + return 0; + } + return $current_frame_length; + } else { + $connection->websocketCurrentFrameLength = $current_frame_length; + } + } + // Received just a frame length data. + if ($connection->websocketCurrentFrameLength === $recv_len) { + self::decode($buffer, $connection); + $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength); + $connection->websocketCurrentFrameLength = 0; + return 0; + } // The length of the received data is greater than the length of a frame. + elseif ($connection->websocketCurrentFrameLength < $recv_len) { + self::decode(substr($buffer, 0, $connection->websocketCurrentFrameLength), $connection); + $connection->consumeRecvBuffer($connection->websocketCurrentFrameLength); + $current_frame_length = $connection->websocketCurrentFrameLength; + $connection->websocketCurrentFrameLength = 0; + // Continue to read next frame. + return self::input(substr($buffer, $current_frame_length), $connection); + } // The length of the received data is less than the length of a frame. + else { + return 0; + } + } + + /** + * Websocket encode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function encode($payload, $connection) + { + if (empty($connection->websocketType)) { + $connection->websocketType = self::BINARY_TYPE_BLOB; + } + $payload = (string)$payload; + if (empty($connection->handshakeStep)) { + static::sendHandshake($connection); + } + $mask = 1; + $mask_key = "\x00\x00\x00\x00"; + + $pack = ''; + $length = $length_flag = strlen($payload); + if (65535 < $length) { + $pack = pack('NN', ($length & 0xFFFFFFFF00000000) >> 32, $length & 0x00000000FFFFFFFF); + $length_flag = 127; + } else if (125 < $length) { + $pack = pack('n*', $length); + $length_flag = 126; + } + + $head = ($mask << 7) | $length_flag; + $head = $connection->websocketType . chr($head) . $pack; + + $frame = $head . $mask_key; + // append payload to frame: + $mask_key = str_repeat($mask_key, floor($length / 4)) . substr($mask_key, 0, $length % 4); + $frame .= $payload ^ $mask_key; + if ($connection->handshakeStep === 1) { + // If buffer has already full then discard the current package. + if (strlen($connection->tmpWebsocketData) > $connection->maxSendBufferSize) { + if ($connection->onError) { + try { + call_user_func($connection->onError, $connection, WORKERMAN_SEND_FAIL, 'send buffer full and drop package'); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + return ''; + } + $connection->tmpWebsocketData = $connection->tmpWebsocketData . $frame; + // Check buffer is full. + if ($connection->maxSendBufferSize <= strlen($connection->tmpWebsocketData)) { + if ($connection->onBufferFull) { + try { + call_user_func($connection->onBufferFull, $connection); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + } + return ''; + } + return $frame; + } + + /** + * Websocket decode. + * + * @param string $buffer + * @param ConnectionInterface $connection + * @return string + */ + public static function decode($bytes, $connection) + { + $data_length = ord($bytes[1]); + + if ($data_length === 126) { + $decoded_data = substr($bytes, 4); + } else if ($data_length === 127) { + $decoded_data = substr($bytes, 10); + } else { + $decoded_data = substr($bytes, 2); + } + if ($connection->websocketCurrentFrameLength) { + $connection->websocketDataBuffer .= $decoded_data; + return $connection->websocketDataBuffer; + } else { + if ($connection->websocketDataBuffer !== '') { + $decoded_data = $connection->websocketDataBuffer . $decoded_data; + $connection->websocketDataBuffer = ''; + } + return $decoded_data; + } + } + + /** + * Send websocket handshake data. + * + * @return void + */ + public static function onConnect($connection) + { + static::sendHandshake($connection); + } + + /** + * Clean + * + * @param $connection + */ + public static function onClose($connection) + { + $connection->handshakeStep = null; + $connection->websocketCurrentFrameLength = 0; + $connection->tmpWebsocketData = ''; + $connection->websocketDataBuffer = ''; + if (!empty($connection->websocketPingTimer)) { + Timer::del($connection->websocketPingTimer); + $connection->websocketPingTimer = null; + } + } + + /** + * Send websocket handshake. + * + * @param \Workerman\Connection\TcpConnection $connection + * @return void + */ + public static function sendHandshake($connection) + { + if (!empty($connection->handshakeStep)) { + return; + } + // Get Host. + $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); + $user_header_str = ''; + if (!empty($user_header)) { + if (is_array($user_header)){ + foreach($user_header as $k=>$v){ + $user_header_str .= "$k: $v\r\n"; + } + } else { + $user_header_str .= $user_header; + } + $user_header_str = "\r\n".trim($user_header_str); + } + $header = 'GET ' . $connection->getRemoteURI() . " HTTP/1.1\r\n". + (!preg_match("/\nHost:/i", $user_header_str) ? "Host: $host\r\n" : ''). + "Connection: Upgrade\r\n". + "Upgrade: websocket\r\n". + (isset($connection->websocketOrigin) ? "Origin: ".$connection->websocketOrigin."\r\n":''). + (isset($connection->WSClientProtocol)?"Sec-WebSocket-Protocol: ".$connection->WSClientProtocol."\r\n":''). + "Sec-WebSocket-Version: 13\r\n". + "Sec-WebSocket-Key: " . $connection->websocketSecKey . $user_header_str . "\r\n\r\n"; + $connection->send($header, true); + $connection->handshakeStep = 1; + $connection->websocketCurrentFrameLength = 0; + $connection->websocketDataBuffer = ''; + $connection->tmpWebsocketData = ''; + } + + /** + * Websocket handshake. + * + * @param string $buffer + * @param \Workerman\Connection\TcpConnection $connection + * @return int + */ + public static function dealHandshake($buffer, $connection) + { + $pos = strpos($buffer, "\r\n\r\n"); + if ($pos) { + //checking Sec-WebSocket-Accept + if (preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) { + if ($match[1] !== base64_encode(sha1($connection->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) { + Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . substr($buffer, 0, $pos) . "\n"); + $connection->close(); + return 0; + } + } else { + Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . substr($buffer, 0, $pos) . "\n"); + $connection->close(); + return 0; + } + + // handshake complete + + // Get WebSocket subprotocol (if specified by server) + if (preg_match("/Sec-WebSocket-Protocol: *(.*?)\r\n/i", $buffer, $match)) { + $connection->WSServerProtocol = trim($match[1]); + } + + $connection->handshakeStep = 2; + $handshake_response_length = $pos + 4; + // Try to emit onWebSocketConnect callback. + if (isset($connection->onWebSocketConnect)) { + try { + call_user_func($connection->onWebSocketConnect, $connection, substr($buffer, 0, $handshake_response_length)); + } catch (\Exception $e) { + Worker::log($e); + exit(250); + } catch (\Error $e) { + Worker::log($e); + exit(250); + } + } + // Headbeat. + if (!empty($connection->websocketPingInterval)) { + $connection->websocketPingTimer = Timer::add($connection->websocketPingInterval, function() use ($connection){ + if (false === $connection->send(pack('H*', '898000000000'), true)) { + Timer::del($connection->websocketPingTimer); + $connection->websocketPingTimer = null; + } + }); + } + + $connection->consumeRecvBuffer($handshake_response_length); + if (!empty($connection->tmpWebsocketData)) { + $connection->send($connection->tmpWebsocketData, true); + $connection->tmpWebsocketData = ''; + } + if (strlen($buffer) > $handshake_response_length) { + return self::input(substr($buffer, $handshake_response_length), $connection); + } + } + return 0; + } + + public static function WSSetProtocol($connection, $params) { + $connection->WSClientProtocol = $params[0]; + } + + public static function WSGetServerProtocol($connection) { + return (property_exists($connection, 'WSServerProtocol')?$connection->WSServerProtocol:null); + } + +} diff --git a/workerman/README.md b/workerman/README.md new file mode 100755 index 0000000..33869e5 --- /dev/null +++ b/workerman/README.md @@ -0,0 +1,604 @@ +# Workerman +[![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge) +[![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman) +[![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman) +[![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman) +[![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman) +[![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman) + +## What is it +Workerman is an asynchronous event driven PHP framework with high performance for easily building fast, scalable network applications. Supports HTTP, Websocket, SSL and other custom protocols. Supports libevent/event extension, [HHVM](https://github.com/facebook/hhvm) , [ReactPHP](https://github.com/reactphp/react). + +## Requires +PHP 5.3 or Higher +A POSIX compatible operating system (Linux, OSX, BSD) +POSIX and PCNTL extensions required +Event extension recommended for better performance + +## Installation + +``` +composer require workerman/workerman +``` + +## Basic Usage + +### A websocket server +```php +count = 4; + +// Emitted when new connection come +$ws_worker->onConnect = function($connection) +{ + echo "New connection\n"; + }; + +// Emitted when data received +$ws_worker->onMessage = function($connection, $data) +{ + // Send hello $data + $connection->send('hello ' . $data); +}; + +// Emitted when connection closed +$ws_worker->onClose = function($connection) +{ + echo "Connection closed\n"; +}; + +// Run worker +Worker::runAll(); +``` + +### An http server +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; + +// #### http worker #### +$http_worker = new Worker("http://0.0.0.0:2345"); + +// 4 processes +$http_worker->count = 4; + +// Emitted when data received +$http_worker->onMessage = function($connection, $data) +{ + // $_GET, $_POST, $_COOKIE, $_SESSION, $_SERVER, $_FILES are available + var_dump($_GET, $_POST, $_COOKIE, $_SESSION, $_SERVER, $_FILES); + // send data to client + $connection->send("hello world \n"); +}; + +// run all workers +Worker::runAll(); +``` + +### A WebServer +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\WebServer; +use Workerman\Worker; + +// WebServer +$web = new WebServer("http://0.0.0.0:80"); + +// 4 processes +$web->count = 4; + +// Set the root of domains +$web->addRoot('www.your_domain.com', '/your/path/Web'); +$web->addRoot('www.another_domain.com', '/another/path/Web'); +// run all workers +Worker::runAll(); +``` + +### A tcp server +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; + +// #### create socket and listen 1234 port #### +$tcp_worker = new Worker("tcp://0.0.0.0:1234"); + +// 4 processes +$tcp_worker->count = 4; + +// Emitted when new connection come +$tcp_worker->onConnect = function($connection) +{ + echo "New Connection\n"; +}; + +// Emitted when data received +$tcp_worker->onMessage = function($connection, $data) +{ + // send data to client + $connection->send("hello $data \n"); +}; + +// Emitted when new connection come +$tcp_worker->onClose = function($connection) +{ + echo "Connection closed\n"; +}; + +Worker::runAll(); +``` + +### Enable SSL +```php + array( + 'local_cert' => '/your/path/of/server.pem', + 'local_pk' => '/your/path/of/server.key', + 'verify_peer' => false, + ) +); + +// Create a Websocket server with ssl context. +$ws_worker = new Worker("websocket://0.0.0.0:2346", $context); + +// Enable SSL. WebSocket+SSL means that Secure WebSocket (wss://). +// The similar approaches for Https etc. +$ws_worker->transport = 'ssl'; + +$ws_worker->onMessage = function($connection, $data) +{ + // Send hello $data + $connection->send('hello ' . $data); +}; + +Worker::runAll(); +``` + +### Custom protocol +Protocols/MyTextProtocol.php +```php +namespace Protocols; +/** + * User defined protocol + * Format Text+"\n" + */ +class MyTextProtocol +{ + public static function input($recv_buffer) + { + // Find the position of the first occurrence of "\n" + $pos = strpos($recv_buffer, "\n"); + // Not a complete package. Return 0 because the length of package can not be calculated + if($pos === false) + { + return 0; + } + // Return length of the package + return $pos+1; + } + + public static function decode($recv_buffer) + { + return trim($recv_buffer); + } + + public static function encode($data) + { + return $data."\n"; + } +} +``` + +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; + +// #### MyTextProtocol worker #### +$text_worker = new Worker("MyTextProtocol://0.0.0.0:5678"); + +$text_worker->onConnect = function($connection) +{ + echo "New connection\n"; +}; + +$text_worker->onMessage = function($connection, $data) +{ + // send data to client + $connection->send("hello world \n"); +}; + +$text_worker->onClose = function($connection) +{ + echo "Connection closed\n"; +}; + +// run all workers +Worker::runAll(); +``` + +### Timer +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; +use Workerman\Lib\Timer; + +$task = new Worker(); +$task->onWorkerStart = function($task) +{ + // 2.5 seconds + $time_interval = 2.5; + $timer_id = Timer::add($time_interval, + function() + { + echo "Timer run\n"; + } + ); +}; + +// run all workers +Worker::runAll(); +``` + +### AsyncTcpConnection (tcp/ws/text/frame etc...) +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; +use Workerman\Connection\AsyncTcpConnection; + +$worker = new Worker(); +$worker->onWorkerStart = function() +{ + // Websocket protocol for client. + $ws_connection = new AsyncTcpConnection("ws://echo.websocket.org:80"); + $ws_connection->onConnect = function($connection){ + $connection->send('hello'); + }; + $ws_connection->onMessage = function($connection, $data){ + echo "recv: $data\n"; + }; + $ws_connection->onError = function($connection, $code, $msg){ + echo "error: $msg\n"; + }; + $ws_connection->onClose = function($connection){ + echo "connection closed\n"; + }; + $ws_connection->connect(); +}; +Worker::runAll(); +``` + +### Async Mysql of ReactPHP +``` +composer require react/mysql +``` + +```php +onWorkerStart = function() { + global $mysql; + $loop = Worker::getEventLoop(); + $mysql = new React\MySQL\Connection($loop, array( + 'host' => '127.0.0.1', + 'dbname' => 'dbname', + 'user' => 'user', + 'passwd' => 'passwd', + )); + $mysql->on('error', function($e){ + echo $e; + }); + $mysql->connect(function ($e) { + if($e) { + echo $e; + } else { + echo "connect success\n"; + } + }); +}; +$worker->onMessage = function($connection, $data) { + global $mysql; + $mysql->query('show databases' /*trim($data)*/, function ($command, $mysql) use ($connection) { + if ($command->hasError()) { + $error = $command->getError(); + } else { + $results = $command->resultRows; + $fields = $command->resultFields; + $connection->send(json_encode($results)); + } + }); +}; +Worker::runAll(); +``` + +### Async Redis of ReactPHP +``` +composer require clue/redis-react +``` + +```php +onWorkerStart = function() { + global $factory; + $loop = Worker::getEventLoop(); + $factory = new Factory($loop); +}; + +$worker->onMessage = function($connection, $data) { + global $factory; + $factory->createClient('localhost:6379')->then(function (Client $client) use ($connection) { + $client->set('greeting', 'Hello world'); + $client->append('greeting', '!'); + + $client->get('greeting')->then(function ($greeting) use ($connection){ + // Hello world! + echo $greeting . PHP_EOL; + $connection->send($greeting); + }); + + $client->incr('invocation')->then(function ($n) use ($connection){ + echo 'This is invocation #' . $n . PHP_EOL; + $connection->send($n); + }); + }); +}; + +Worker::runAll(); +``` + +### Aysnc dns of ReactPHP +``` +composer require react/dns +``` + +```php +require_once __DIR__ . '/vendor/autoload.php'; +use Workerman\Worker; +$worker = new Worker('tcp://0.0.0.0:6161'); +$worker->onWorkerStart = function() { + global $dns; + // Get event-loop. + $loop = Worker::getEventLoop(); + $factory = new React\Dns\Resolver\Factory(); + $dns = $factory->create('8.8.8.8', $loop); +}; +$worker->onMessage = function($connection, $host) { + global $dns; + $host = trim($host); + $dns->resolve($host)->then(function($ip) use($host, $connection) { + $connection->send("$host: $ip"); + },function($e) use($host, $connection){ + $connection->send("$host: {$e->getMessage()}"); + }); +}; + +Worker::runAll(); +``` + +### Http client of ReactPHP +``` +composer require react/http-client +``` + +```php +onMessage = function($connection, $host) { + $loop = Worker::getEventLoop(); + $client = new \React\HttpClient\Client($loop); + $request = $client->request('GET', trim($host)); + $request->on('error', function(Exception $e) use ($connection) { + $connection->send($e); + }); + $request->on('response', function ($response) use ($connection) { + $response->on('data', function ($data) use ($connection) { + $connection->send($data); + }); + }); + $request->end(); +}; + +Worker::runAll(); +``` + +### ZMQ of ReactPHP +``` +composer require react/zmq +``` + +```php +onWorkerStart = function() { + global $pull; + $loop = Worker::getEventLoop(); + $context = new React\ZMQ\Context($loop); + $pull = $context->getSocket(ZMQ::SOCKET_PULL); + $pull->bind('tcp://127.0.0.1:5555'); + + $pull->on('error', function ($e) { + var_dump($e->getMessage()); + }); + + $pull->on('message', function ($msg) { + echo "Received: $msg\n"; + }); +}; + +Worker::runAll(); +``` + +### STOMP of ReactPHP +``` +composer require react/stomp +``` + +```php +onWorkerStart = function() { + global $client; + $loop = Worker::getEventLoop(); + $factory = new React\Stomp\Factory($loop); + $client = $factory->createClient(array('vhost' => '/', 'login' => 'guest', 'passcode' => 'guest')); + + $client + ->connect() + ->then(function ($client) use ($loop) { + $client->subscribe('/topic/foo', function ($frame) { + echo "Message received: {$frame->body}\n"; + }); + }); +}; + +Worker::runAll(); +``` + + + +## Available commands +```php start.php start ``` +```php start.php start -d ``` +![workerman start](http://www.workerman.net/img/workerman-start.png) +```php start.php status ``` +![workerman satus](http://www.workerman.net/img/workerman-status.png?a=123) +```php start.php connections``` +```php start.php stop ``` +```php start.php restart ``` +```php start.php reload ``` + +## Documentation + +中文主页:[http://www.workerman.net](http://www.workerman.net) + +中文文档: [http://doc.workerman.net](http://doc.workerman.net) + +Documentation:[https://github.com/walkor/workerman-manual](https://github.com/walkor/workerman-manual/blob/master/english/src/SUMMARY.md) + +# Benchmarks +``` +CPU: Intel(R) Core(TM) i3-3220 CPU @ 3.30GHz and 4 processors totally +Memory: 8G +OS: Ubuntu 14.04 LTS +Software: ab +PHP: 5.5.9 +``` + +**Codes** +```php +count=3; +$worker->onMessage = function($connection, $data) +{ + $connection->send("HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nServer: workerman\r\nContent-Length: 5\r\n\r\nhello"); +}; +Worker::runAll(); +``` +**Result** + +```shell +ab -n1000000 -c100 -k http://127.0.0.1:1234/ +This is ApacheBench, Version 2.3 <$Revision: 1528965 $> +Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Licensed to The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100000 requests +Completed 200000 requests +Completed 300000 requests +Completed 400000 requests +Completed 500000 requests +Completed 600000 requests +Completed 700000 requests +Completed 800000 requests +Completed 900000 requests +Completed 1000000 requests +Finished 1000000 requests + + +Server Software: workerman/3.1.4 +Server Hostname: 127.0.0.1 +Server Port: 1234 + +Document Path: / +Document Length: 5 bytes + +Concurrency Level: 100 +Time taken for tests: 7.240 seconds +Complete requests: 1000000 +Failed requests: 0 +Keep-Alive requests: 1000000 +Total transferred: 73000000 bytes +HTML transferred: 5000000 bytes +Requests per second: 138124.14 [#/sec] (mean) +Time per request: 0.724 [ms] (mean) +Time per request: 0.007 [ms] (mean, across all concurrent requests) +Transfer rate: 9846.74 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 0.0 0 5 +Processing: 0 1 0.2 1 9 +Waiting: 0 1 0.2 1 9 +Total: 0 1 0.2 1 9 + +Percentage of the requests served within a certain time (ms) + 50% 1 + 66% 1 + 75% 1 + 80% 1 + 90% 1 + 95% 1 + 98% 1 + 99% 1 + 100% 9 (longest request) + +``` + + +## Other links with workerman + +[PHPSocket.IO](https://github.com/walkor/phpsocket.io) +[php-socks5](https://github.com/walkor/php-socks5) +[php-http-proxy](https://github.com/walkor/php-http-proxy) + +## Donate + + +## LICENSE + +Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt). diff --git a/workerman/WebServer.php b/workerman/WebServer.php new file mode 100755 index 0000000..f529399 --- /dev/null +++ b/workerman/WebServer.php @@ -0,0 +1,327 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; + +use Workerman\Protocols\Http; +use Workerman\Protocols\HttpCache; + +/** + * WebServer. + */ +class WebServer extends Worker +{ + /** + * Virtual host to path mapping. + * + * @var array ['workerman.net'=>'/home', 'www.workerman.net'=>'home/www'] + */ + protected $serverRoot = array(); + + /** + * Mime mapping. + * + * @var array + */ + protected static $mimeTypeMap = array(); + + + /** + * Used to save user OnWorkerStart callback settings. + * + * @var callback + */ + protected $_onWorkerStart = null; + + /** + * Add virtual host. + * + * @param string $domain + * @param string $config + * @return void + */ + public function addRoot($domain, $config) + { + if (is_string($config)) { + $config = array('root' => $config); + } + $this->serverRoot[$domain] = $config; + } + + /** + * Construct. + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name, $context_option = array()) + { + list(, $address) = explode(':', $socket_name, 2); + parent::__construct('http:' . $address, $context_option); + $this->name = 'WebServer'; + } + + /** + * Run webserver instance. + * + * @see Workerman.Worker::run() + */ + public function run() + { + $this->_onWorkerStart = $this->onWorkerStart; + $this->onWorkerStart = array($this, 'onWorkerStart'); + $this->onMessage = array($this, 'onMessage'); + parent::run(); + } + + /** + * Emit when process start. + * + * @throws \Exception + */ + public function onWorkerStart() + { + if (empty($this->serverRoot)) { + Worker::safeEcho(new \Exception('server root not set, please use WebServer::addRoot($domain, $root_path) to set server root path')); + exit(250); + } + + // Init mimeMap. + $this->initMimeTypeMap(); + + // Try to emit onWorkerStart callback. + if ($this->_onWorkerStart) { + try { + call_user_func($this->_onWorkerStart, $this); + } catch (\Exception $e) { + self::log($e); + exit(250); + } catch (\Error $e) { + self::log($e); + exit(250); + } + } + } + + /** + * Init mime map. + * + * @return void + */ + public function initMimeTypeMap() + { + $mime_file = Http::getMimeTypesFile(); + if (!is_file($mime_file)) { + $this->log("$mime_file mime.type file not fond"); + return; + } + $items = file($mime_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!is_array($items)) { + $this->log("get $mime_file mime.type content fail"); + return; + } + foreach ($items as $content) { + if (preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) { + $mime_type = $match[1]; + $workerman_file_extension_var = $match[2]; + $workerman_file_extension_array = explode(' ', substr($workerman_file_extension_var, 0, -1)); + foreach ($workerman_file_extension_array as $workerman_file_extension) { + self::$mimeTypeMap[$workerman_file_extension] = $mime_type; + } + } + } + } + + /** + * Emit when http message coming. + * + * @param Connection\TcpConnection $connection + * @return void + */ + public function onMessage($connection) + { + // REQUEST_URI. + $workerman_url_info = parse_url('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']); + if (!$workerman_url_info) { + Http::header('HTTP/1.1 400 Bad Request'); + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send('

400 Bad Request

'); + } else { + $connection->close('

400 Bad Request

'); + } + return; + } + + $workerman_path = isset($workerman_url_info['path']) ? $workerman_url_info['path'] : '/'; + + $workerman_path_info = pathinfo($workerman_path); + $workerman_file_extension = isset($workerman_path_info['extension']) ? $workerman_path_info['extension'] : ''; + if ($workerman_file_extension === '') { + $workerman_path = ($len = strlen($workerman_path)) && $workerman_path[$len - 1] === '/' ? $workerman_path . 'index.php' : $workerman_path . '/index.php'; + $workerman_file_extension = 'php'; + } + + $workerman_siteConfig = isset($this->serverRoot[$_SERVER['SERVER_NAME']]) ? $this->serverRoot[$_SERVER['SERVER_NAME']] : current($this->serverRoot); + $workerman_root_dir = $workerman_siteConfig['root']; + $workerman_file = "$workerman_root_dir/$workerman_path"; + if(isset($workerman_siteConfig['additionHeader'])){ + Http::header($workerman_siteConfig['additionHeader']); + } + if ($workerman_file_extension === 'php' && !is_file($workerman_file)) { + $workerman_file = "$workerman_root_dir/index.php"; + if (!is_file($workerman_file)) { + $workerman_file = "$workerman_root_dir/index.html"; + $workerman_file_extension = 'html'; + } + } + + // File exsits. + if (is_file($workerman_file)) { + // Security check. + if ((!($workerman_request_realpath = realpath($workerman_file)) || !($workerman_root_dir_realpath = realpath($workerman_root_dir))) || 0 !== strpos($workerman_request_realpath, + $workerman_root_dir_realpath) + ) { + Http::header('HTTP/1.1 400 Bad Request'); + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send('

400 Bad Request

'); + } else { + $connection->close('

400 Bad Request

'); + } + return; + } + + $workerman_file = realpath($workerman_file); + + // Request php file. + if ($workerman_file_extension === 'php') { + $workerman_cwd = getcwd(); + chdir($workerman_root_dir); + ini_set('display_errors', 'off'); + ob_start(); + // Try to include php file. + try { + // $_SERVER. + $_SERVER['REMOTE_ADDR'] = $connection->getRemoteIp(); + $_SERVER['REMOTE_PORT'] = $connection->getRemotePort(); + include $workerman_file; + } catch (\Exception $e) { + // Jump_exit? + if ($e->getMessage() != 'jump_exit') { + Worker::safeEcho($e); + } + } + $content = ob_get_clean(); + ini_set('display_errors', 'on'); + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send($content); + } else { + $connection->close($content); + } + chdir($workerman_cwd); + return; + } + + // Send file to client. + return self::sendFile($connection, $workerman_file); + } else { + // 404 + Http::header("HTTP/1.1 404 Not Found"); + if(isset($workerman_siteConfig['custom404']) && file_exists($workerman_siteConfig['custom404'])){ + $html404 = file_get_contents($workerman_siteConfig['custom404']); + }else{ + $html404 = '404 File not found

404 Not Found

'; + } + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send($html404); + } else { + $connection->close($html404); + } + return; + } + } + + public static function sendFile($connection, $file_path) + { + // Check 304. + $info = stat($file_path); + $modified_time = $info ? date('D, d M Y H:i:s', $info['mtime']) . ' ' . date_default_timezone_get() : ''; + if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $info) { + // Http 304. + if ($modified_time === $_SERVER['HTTP_IF_MODIFIED_SINCE']) { + // 304 + Http::header('HTTP/1.1 304 Not Modified'); + // Send nothing but http headers.. + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send(''); + } else { + $connection->close(''); + } + return; + } + } + + // Http header. + if ($modified_time) { + $modified_time = "Last-Modified: $modified_time\r\n"; + } + $file_size = filesize($file_path); + $file_info = pathinfo($file_path); + $extension = isset($file_info['extension']) ? $file_info['extension'] : ''; + $file_name = isset($file_info['filename']) ? $file_info['filename'] : ''; + $header = "HTTP/1.1 200 OK\r\n"; + if (isset(self::$mimeTypeMap[$extension])) { + $header .= "Content-Type: " . self::$mimeTypeMap[$extension] . "\r\n"; + } else { + $header .= "Content-Type: application/octet-stream\r\n"; + $header .= "Content-Disposition: attachment; filename=\"$file_name\"\r\n"; + } + $header .= "Connection: keep-alive\r\n"; + $header .= $modified_time; + $header .= "Content-Length: $file_size\r\n\r\n"; + $trunk_limit_size = 1024*1024; + if ($file_size < $trunk_limit_size) { + return $connection->send($header.file_get_contents($file_path), true); + } + $connection->send($header, true); + + // Read file content from disk piece by piece and send to client. + $connection->fileHandler = fopen($file_path, 'r'); + $do_write = function()use($connection) + { + // Send buffer not full. + while(empty($connection->bufferFull)) + { + // Read from disk. + $buffer = fread($connection->fileHandler, 8192); + // Read eof. + if($buffer === '' || $buffer === false) + { + 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(); + } +} diff --git a/workerman/Worker.php b/workerman/Worker.php new file mode 100755 index 0000000..eda9a68 --- /dev/null +++ b/workerman/Worker.php @@ -0,0 +1,2492 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace Workerman; +require_once __DIR__ . '/Lib/Constants.php'; + +use Workerman\Events\EventInterface; +use Workerman\Connection\ConnectionInterface; +use Workerman\Connection\TcpConnection; +use Workerman\Connection\UdpConnection; +use Workerman\Lib\Timer; +use Workerman\Events\Select; +use Exception; + +/** + * Worker class + * A container for listening ports + */ +class Worker +{ + /** + * Version. + * + * @var string + */ + const VERSION = '3.5.20'; + + /** + * Status starting. + * + * @var int + */ + const STATUS_STARTING = 1; + + /** + * Status running. + * + * @var int + */ + const STATUS_RUNNING = 2; + + /** + * Status shutdown. + * + * @var int + */ + const STATUS_SHUTDOWN = 4; + + /** + * Status reloading. + * + * @var int + */ + const STATUS_RELOADING = 8; + + /** + * After sending the restart command to the child process KILL_WORKER_TIMER_TIME seconds, + * if the process is still living then forced to kill. + * + * @var int + */ + const KILL_WORKER_TIMER_TIME = 2; + + /** + * Default backlog. Backlog is the maximum length of the queue of pending connections. + * + * @var int + */ + const DEFAULT_BACKLOG = 102400; + /** + * Max udp package size. + * + * @var int + */ + const MAX_UDP_PACKAGE_SIZE = 65535; + + /** + * The safe distance for columns adjacent + * + * @var int + */ + const UI_SAFE_LENGTH = 4; + + /** + * Worker id. + * + * @var int + */ + public $id = 0; + + /** + * Name of the worker processes. + * + * @var string + */ + public $name = 'none'; + + /** + * Number of worker processes. + * + * @var int + */ + public $count = 1; + + /** + * Unix user of processes, needs appropriate privileges (usually root). + * + * @var string + */ + public $user = ''; + + /** + * Unix group of processes, needs appropriate privileges (usually root). + * + * @var string + */ + public $group = ''; + + /** + * reloadable. + * + * @var bool + */ + public $reloadable = true; + + /** + * reuse port. + * + * @var bool + */ + public $reusePort = false; + + /** + * Emitted when worker processes start. + * + * @var callback + */ + public $onWorkerStart = null; + + /** + * Emitted when a socket connection is successfully established. + * + * @var callback + */ + public $onConnect = null; + + /** + * Emitted when data is received. + * + * @var callback + */ + public $onMessage = null; + + /** + * Emitted when the other end of the socket sends a FIN packet. + * + * @var callback + */ + public $onClose = null; + + /** + * Emitted when an error occurs with connection. + * + * @var callback + */ + public $onError = null; + + /** + * Emitted when the send buffer becomes full. + * + * @var callback + */ + public $onBufferFull = null; + + /** + * Emitted when the send buffer becomes empty. + * + * @var callback + */ + public $onBufferDrain = null; + + /** + * Emitted when worker processes stoped. + * + * @var callback + */ + public $onWorkerStop = null; + + /** + * Emitted when worker processes get reload signal. + * + * @var callback + */ + public $onWorkerReload = null; + + /** + * Transport layer protocol. + * + * @var string + */ + public $transport = 'tcp'; + + /** + * Store all connections of clients. + * + * @var array + */ + public $connections = array(); + + /** + * Application layer protocol. + * + * @var string + */ + public $protocol = null; + + /** + * Root path for autoload. + * + * @var string + */ + protected $_autoloadRootPath = ''; + + /** + * Pause accept new connections or not. + * + * @var bool + */ + protected $_pauseAccept = true; + + /** + * Is worker stopping ? + * @var bool + */ + public $stopping = false; + + /** + * Daemonize. + * + * @var bool + */ + public static $daemonize = false; + + /** + * Stdout file. + * + * @var string + */ + public static $stdoutFile = '/dev/null'; + + /** + * The file to store master process PID. + * + * @var string + */ + public static $pidFile = ''; + + /** + * Log file. + * + * @var mixed + */ + public static $logFile = ''; + + /** + * Global event loop. + * + * @var Events\EventInterface + */ + public static $globalEvent = null; + + /** + * Emitted when the master process get reload signal. + * + * @var callback + */ + public static $onMasterReload = null; + + /** + * Emitted when the master process terminated. + * + * @var callback + */ + public static $onMasterStop = null; + + /** + * EventLoopClass + * + * @var string + */ + public static $eventLoopClass = ''; + + /** + * The PID of master process. + * + * @var int + */ + protected static $_masterPid = 0; + + /** + * Listening socket. + * + * @var resource + */ + protected $_mainSocket = null; + + /** + * Socket name. The format is like this http://0.0.0.0:80 . + * + * @var string + */ + protected $_socketName = ''; + + /** + * Context of socket. + * + * @var resource + */ + protected $_context = null; + + /** + * All worker instances. + * + * @var Worker[] + */ + protected static $_workers = array(); + + /** + * All worker processes pid. + * The format is like this [worker_id=>[pid=>pid, pid=>pid, ..], ..] + * + * @var array + */ + protected static $_pidMap = array(); + + /** + * All worker processes waiting for restart. + * The format is like this [pid=>pid, pid=>pid]. + * + * @var array + */ + protected static $_pidsToRestart = array(); + + /** + * Mapping from PID to worker process ID. + * The format is like this [worker_id=>[0=>$pid, 1=>$pid, ..], ..]. + * + * @var array + */ + protected static $_idMap = array(); + + /** + * Current status. + * + * @var int + */ + protected static $_status = self::STATUS_STARTING; + + /** + * Maximum length of the worker names. + * + * @var int + */ + protected static $_maxWorkerNameLength = 12; + + /** + * Maximum length of the socket names. + * + * @var int + */ + protected static $_maxSocketNameLength = 12; + + /** + * Maximum length of the process user names. + * + * @var int + */ + protected static $_maxUserNameLength = 12; + + /** + * Maximum length of the Proto names. + * + * @var int + */ + protected static $_maxProtoNameLength = 4; + + /** + * Maximum length of the Processes names. + * + * @var int + */ + protected static $_maxProcessesNameLength = 9; + + /** + * Maximum length of the Status names. + * + * @var int + */ + protected static $_maxStatusNameLength = 1; + + /** + * The file to store status info of current worker process. + * + * @var string + */ + protected static $_statisticsFile = ''; + + /** + * Start file. + * + * @var string + */ + protected static $_startFile = ''; + + /** + * OS. + * + * @var string + */ + protected static $_OS = OS_TYPE_LINUX; + + /** + * Processes for windows. + * + * @var array + */ + protected static $_processForWindows = array(); + + /** + * Status info of current worker process. + * + * @var array + */ + protected static $_globalStatistics = array( + 'start_timestamp' => 0, + 'worker_exit_info' => array() + ); + + /** + * Available event loops. + * + * @var array + */ + protected static $_availableEventLoops = array( + 'libevent' => '\Workerman\Events\Libevent', + 'event' => '\Workerman\Events\Event' + // Temporarily removed swoole because it is not stable enough + //'swoole' => '\Workerman\Events\Swoole' + ); + + /** + * PHP built-in protocols. + * + * @var array + */ + protected static $_builtinTransports = array( + 'tcp' => 'tcp', + 'udp' => 'udp', + 'unix' => 'unix', + 'ssl' => 'tcp' + ); + + /** + * Graceful stop or not. + * + * @var string + */ + protected static $_gracefulStop = false; + + /** + * Standard output stream + * @var resource + */ + protected static $_outputStream = null; + + /** + * If $outputStream support decorated + * @var bool + */ + protected static $_outputDecorated = null; + + /** + * Run all worker instances. + * + * @return void + */ + public static function runAll() + { + static::checkSapiEnv(); + static::init(); + static::lock(); + static::parseCommand(); + static::daemonize(); + static::initWorkers(); + static::installSignal(); + static::saveMasterPid(); + static::unlock(); + static::displayUI(); + static::forkWorkers(); + static::resetStd(); + static::monitorWorkers(); + } + + /** + * Check sapi. + * + * @return void + */ + protected static function checkSapiEnv() + { + // Only for cli. + if (php_sapi_name() != "cli") { + exit("only run in command line mode \n"); + } + if (DIRECTORY_SEPARATOR === '\\') { + self::$_OS = OS_TYPE_WINDOWS; + } + } + + /** + * Init. + * + * @return void + */ + protected static function init() + { + set_error_handler(function($code, $msg, $file, $line){ + Worker::safeEcho("$msg in file $file on line $line\n"); + }); + + // Start file. + $backtrace = debug_backtrace(); + static::$_startFile = $backtrace[count($backtrace) - 1]['file']; + + + $unique_prefix = str_replace('/', '_', static::$_startFile); + + // Pid file. + if (empty(static::$pidFile)) { + static::$pidFile = __DIR__ . "/../$unique_prefix.pid"; + } + + // Log file. + if (empty(static::$logFile)) { + static::$logFile = __DIR__ . '/../workerman.log'; + } + $log_file = (string)static::$logFile; + if (!is_file($log_file)) { + touch($log_file); + chmod($log_file, 0622); + } + + // State. + static::$_status = static::STATUS_STARTING; + + // For statistics. + static::$_globalStatistics['start_timestamp'] = time(); + static::$_statisticsFile = sys_get_temp_dir() . "/$unique_prefix.status"; + + // Process title. + static::setProcessTitle('WorkerMan: master process start_file=' . static::$_startFile); + + // Init data for worker id. + static::initId(); + + // Timer init. + Timer::init(); + } + + /** + * Lock. + * + * @return void + */ + protected static function lock() + { + $fd = fopen(static::$_startFile, 'r'); + if (!$fd || !flock($fd, LOCK_EX)) { + static::log("Workerman[".static::$_startFile."] already running"); + exit; + } + } + + /** + * Unlock. + * + * @return void + */ + protected static function unlock() + { + $fd = fopen(static::$_startFile, 'r'); + $fd && flock($fd, LOCK_UN); + } + + /** + * Init All worker instances. + * + * @return void + */ + protected static function initWorkers() + { + if (static::$_OS !== OS_TYPE_LINUX) { + return; + } + foreach (static::$_workers as $worker) { + // Worker name. + if (empty($worker->name)) { + $worker->name = 'none'; + } + + // Get unix user of the worker process. + if (empty($worker->user)) { + $worker->user = static::getCurrentUser(); + } else { + if (posix_getuid() !== 0 && $worker->user != static::getCurrentUser()) { + static::log('Warning: You must have the root privileges to change uid and gid.'); + } + } + + // Socket name. + $worker->socket = $worker->getSocketName(); + + // Status name. + $worker->status = ' [OK] '; + + // Get column mapping for UI + foreach(static::getUiColumns() as $column_name => $prop){ + !isset($worker->{$prop}) && $worker->{$prop}= 'NNNN'; + $prop_length = strlen($worker->{$prop}); + $key = '_max' . ucfirst(strtolower($column_name)) . 'NameLength'; + static::$$key = max(static::$$key, $prop_length); + } + + // Listen. + if (!$worker->reusePort) { + $worker->listen(); + } + } + } + + /** + * Get all worker instances. + * + * @return array + */ + public static function getAllWorkers() + { + return static::$_workers; + } + + /** + * Get global event-loop instance. + * + * @return EventInterface + */ + public static function getEventLoop() + { + return static::$globalEvent; + } + + /** + * Get main socket resource + * @return resource + */ + public function getMainSocket(){ + return $this->_mainSocket; + } + + /** + * Init idMap. + * return void + */ + protected static function initId() + { + foreach (static::$_workers as $worker_id => $worker) { + $new_id_map = array(); + $worker->count = $worker->count <= 0 ? 1 : $worker->count; + for($key = 0; $key < $worker->count; $key++) { + $new_id_map[$key] = isset(static::$_idMap[$worker_id][$key]) ? static::$_idMap[$worker_id][$key] : 0; + } + static::$_idMap[$worker_id] = $new_id_map; + } + } + + /** + * Get unix user of current porcess. + * + * @return string + */ + protected static function getCurrentUser() + { + $user_info = posix_getpwuid(posix_getuid()); + return $user_info['name']; + } + + /** + * Display staring UI. + * + * @return void + */ + protected static function displayUI() + { + global $argv; + if (in_array('-q', $argv)) { + return; + } + if (static::$_OS !== OS_TYPE_LINUX) { + static::safeEcho("----------------------- WORKERMAN -----------------------------\r\n"); + static::safeEcho('Workerman version:'. static::VERSION. " PHP version:". PHP_VERSION. "\r\n"); + static::safeEcho("------------------------ WORKERS -------------------------------\r\n"); + static::safeEcho("worker listen processes status\r\n"); + return; + } + + //show version + $line_version = 'Workerman version:' . static::VERSION . str_pad('PHP version:', 22, ' ', STR_PAD_LEFT) . PHP_VERSION . PHP_EOL; + !defined('LINE_VERSIOIN_LENGTH') && define('LINE_VERSIOIN_LENGTH', strlen($line_version)); + $total_length = static::getSingleLineTotalLength(); + $line_one = '' . str_pad(' WORKERMAN ', $total_length + strlen(''), '-', STR_PAD_BOTH) . ''. PHP_EOL; + $line_two = str_pad(' WORKERS ' , $total_length + strlen(''), '-', STR_PAD_BOTH) . PHP_EOL; + static::safeEcho($line_one . $line_version . $line_two); + + //Show title + $title = ''; + foreach(static::getUiColumns() as $column_name => $prop){ + $key = '_max' . ucfirst(strtolower($column_name)) . 'NameLength'; + //just keep compatible with listen name + $column_name == 'socket' && $column_name = 'listen'; + $title.= "{$column_name}" . str_pad('', static::$$key + static::UI_SAFE_LENGTH - strlen($column_name)); + } + $title && static::safeEcho($title . PHP_EOL); + + //Show content + foreach (static::$_workers as $worker) { + $content = ''; + foreach(static::getUiColumns() as $column_name => $prop){ + $key = '_max' . ucfirst(strtolower($column_name)) . 'NameLength'; + preg_match_all("/(|<\/n>||<\/w>||<\/g>)/is", $worker->{$prop}, $matches); + $place_holder_length = !empty($matches) ? strlen(implode('', $matches[0])) : 0; + $content .= str_pad($worker->{$prop}, static::$$key + static::UI_SAFE_LENGTH + $place_holder_length); + } + $content && static::safeEcho($content . PHP_EOL); + } + + //Show last line + $line_last = str_pad('', static::getSingleLineTotalLength(), '-') . PHP_EOL; + !empty($content) && static::safeEcho($line_last); + + if (static::$daemonize) { + static::safeEcho("Input \"php $argv[0] stop\" to stop. Start success.\n\n"); + } else { + static::safeEcho("Press Ctrl+C to stop. Start success.\n"); + } + } + + /** + * Get UI columns to be shown in terminal + * + * 1. $column_map: array('ui_column_name' => 'clas_property_name') + * 2. Consider move into configuration in future + * + * @return array + */ + public static function getUiColumns() + { + $column_map = array( + 'proto' => 'transport', + 'user' => 'user', + 'worker' => 'name', + 'socket' => 'socket', + 'processes' => 'count', + 'status' => 'status', + ); + + return $column_map; + } + + /** + * Get single line total length for ui + * + * @return int + */ + public static function getSingleLineTotalLength() + { + $total_length = 0; + + foreach(static::getUiColumns() as $column_name => $prop){ + $key = '_max' . ucfirst(strtolower($column_name)) . 'NameLength'; + $total_length += static::$$key + static::UI_SAFE_LENGTH; + } + + //keep beauty when show less colums + !defined('LINE_VERSIOIN_LENGTH') && define('LINE_VERSIOIN_LENGTH', 0); + $total_length <= LINE_VERSIOIN_LENGTH && $total_length = LINE_VERSIOIN_LENGTH; + + return $total_length; + } + + /** + * Parse command. + * + * @return void + */ + protected static function parseCommand() + { + if (static::$_OS !== OS_TYPE_LINUX) { + return; + } + global $argv; + // Check argv; + $start_file = $argv[0]; + $available_commands = array( + 'start', + 'stop', + 'restart', + 'reload', + 'status', + 'connections', + ); + $usage = "Usage: php yourfile [mode]\nCommands: \nstart\t\tStart worker in DEBUG mode.\n\t\tUse mode -d to start in DAEMON mode.\nstop\t\tStop worker.\n\t\tUse mode -g to stop gracefully.\nrestart\t\tRestart workers.\n\t\tUse mode -d to start in DAEMON mode.\n\t\tUse mode -g to stop gracefully.\nreload\t\tReload codes.\n\t\tUse mode -g to reload gracefully.\nstatus\t\tGet worker status.\n\t\tUse mode -d to show live status.\nconnections\tGet worker connections.\n"; + if (!isset($argv[1]) || !in_array($argv[1], $available_commands)) { + if (isset($argv[1])) { + static::safeEcho('Unknown command: ' . $argv[1] . "\n"); + } + exit($usage); + } + + // Get command. + $command = trim($argv[1]); + $command2 = isset($argv[2]) ? $argv[2] : ''; + + // Start command. + $mode = ''; + if ($command === 'start') { + if ($command2 === '-d' || static::$daemonize) { + $mode = 'in DAEMON mode'; + } else { + $mode = 'in DEBUG mode'; + } + } + static::log("Workerman[$start_file] $command $mode"); + + // Get master process PID. + $master_pid = is_file(static::$pidFile) ? file_get_contents(static::$pidFile) : 0; + $master_is_alive = $master_pid && posix_kill($master_pid, 0) && posix_getpid() != $master_pid; + // Master is still alive? + if ($master_is_alive) { + if ($command === 'start') { + static::log("Workerman[$start_file] already running"); + exit; + } + } elseif ($command !== 'start' && $command !== 'restart') { + static::log("Workerman[$start_file] not run"); + exit; + } + + // execute command. + switch ($command) { + case 'start': + if ($command2 === '-d') { + static::$daemonize = true; + } + break; + case 'status': + while (1) { + if (is_file(static::$_statisticsFile)) { + @unlink(static::$_statisticsFile); + } + // Master process will send SIGUSR2 signal to all child processes. + posix_kill($master_pid, SIGUSR2); + // Sleep 1 second. + sleep(1); + // Clear terminal. + if ($command2 === '-d') { + static::safeEcho("\33[H\33[2J\33(B\33[m", true); + } + // Echo status data. + static::safeEcho(static::formatStatusData()); + if ($command2 !== '-d') { + exit(0); + } + static::safeEcho("\nPress Ctrl+C to quit.\n\n"); + } + exit(0); + case 'connections': + if (is_file(static::$_statisticsFile) && is_writable(static::$_statisticsFile)) { + unlink(static::$_statisticsFile); + } + // Master process will send SIGIO signal to all child processes. + posix_kill($master_pid, SIGIO); + // Waiting amoment. + usleep(500000); + // Display statisitcs data from a disk file. + if(is_readable(static::$_statisticsFile)) { + readfile(static::$_statisticsFile); + } + exit(0); + case 'restart': + case 'stop': + if ($command2 === '-g') { + static::$_gracefulStop = true; + $sig = SIGTERM; + static::log("Workerman[$start_file] is gracefully stopping ..."); + } else { + static::$_gracefulStop = false; + $sig = SIGINT; + static::log("Workerman[$start_file] is stopping ..."); + } + // Send stop signal to master process. + $master_pid && posix_kill($master_pid, $sig); + // Timeout. + $timeout = 5; + $start_time = time(); + // Check master process is still alive? + while (1) { + $master_is_alive = $master_pid && posix_kill($master_pid, 0); + if ($master_is_alive) { + // Timeout? + if (!static::$_gracefulStop && time() - $start_time >= $timeout) { + static::log("Workerman[$start_file] stop fail"); + exit; + } + // Waiting amoment. + usleep(10000); + continue; + } + // Stop success. + static::log("Workerman[$start_file] stop success"); + if ($command === 'stop') { + exit(0); + } + if ($command2 === '-d') { + static::$daemonize = true; + } + break; + } + break; + case 'reload': + if($command2 === '-g'){ + $sig = SIGQUIT; + }else{ + $sig = SIGUSR1; + } + posix_kill($master_pid, $sig); + exit; + default : + if (isset($command)) { + static::safeEcho('Unknown command: ' . $command . "\n"); + } + exit($usage); + } + } + + /** + * Format status data. + * + * @return string + */ + protected static function formatStatusData() + { + static $total_request_cache = array(); + if (!is_readable(static::$_statisticsFile)) { + return ''; + } + $info = file(static::$_statisticsFile, FILE_IGNORE_NEW_LINES); + if (!$info) { + return ''; + } + $status_str = ''; + $current_total_request = array(); + $worker_info = json_decode($info[0], true); + ksort($worker_info, SORT_NUMERIC); + unset($info[0]); + $data_waiting_sort = array(); + $read_process_status = false; + $total_requests = 0; + $total_qps = 0; + $total_connections = 0; + $total_fails = 0; + $total_memory = 0; + $total_timers = 0; + $maxLen1 = static::$_maxSocketNameLength; + $maxLen2 = static::$_maxWorkerNameLength; + foreach($info as $key => $value) { + if (!$read_process_status) { + $status_str .= $value . "\n"; + if (preg_match('/^pid.*?memory.*?listening/', $value)) { + $read_process_status = true; + } + continue; + } + if(preg_match('/^[0-9]+/', $value, $pid_math)) { + $pid = $pid_math[0]; + $data_waiting_sort[$pid] = $value; + if(preg_match('/^\S+?\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?/', $value, $match)) { + $total_memory += intval(str_ireplace('M','',$match[1])); + $maxLen1 = max($maxLen1,strlen($match[2])); + $maxLen2 = max($maxLen2,strlen($match[3])); + $total_connections += intval($match[4]); + $total_fails += intval($match[5]); + $total_timers += intval($match[6]); + $current_total_request[$pid] = $match[7]; + $total_requests += intval($match[7]); + } + } + } + foreach($worker_info as $pid => $info) { + if (!isset($data_waiting_sort[$pid])) { + $status_str .= "$pid\t" . str_pad('N/A', 7) . " " + . str_pad($info['listen'], static::$_maxSocketNameLength) . " " + . str_pad($info['name'], static::$_maxWorkerNameLength) . " " + . str_pad('N/A', 11) . " " . str_pad('N/A', 9) . " " + . str_pad('N/A', 7) . " " . str_pad('N/A', 13) . " N/A [busy] \n"; + continue; + } + //$qps = isset($total_request_cache[$pid]) ? $current_total_request[$pid] + if (!isset($total_request_cache[$pid]) || !isset($current_total_request[$pid])) { + $qps = 0; + } else { + $qps = $current_total_request[$pid] - $total_request_cache[$pid]; + $total_qps += $qps; + } + $status_str .= $data_waiting_sort[$pid]. " " . str_pad($qps, 6) ." [idle]\n"; + } + $total_request_cache = $current_total_request; + $status_str .= "----------------------------------------------PROCESS STATUS---------------------------------------------------\n"; + $status_str .= "Summary\t" . str_pad($total_memory.'M', 7) . " " + . str_pad('-', $maxLen1) . " " + . str_pad('-', $maxLen2) . " " + . str_pad($total_connections, 11) . " " . str_pad($total_fails, 9) . " " + . str_pad($total_timers, 7) . " " . str_pad($total_requests, 13) . " " + . str_pad($total_qps,6)." [Summary] \n"; + return $status_str; + } + + + /** + * Install signal handler. + * + * @return void + */ + protected static function installSignal() + { + if (static::$_OS !== OS_TYPE_LINUX) { + return; + } + // stop + pcntl_signal(SIGINT, array('\Workerman\Worker', 'signalHandler'), false); + // graceful stop + pcntl_signal(SIGTERM, array('\Workerman\Worker', 'signalHandler'), false); + // reload + pcntl_signal(SIGUSR1, array('\Workerman\Worker', 'signalHandler'), false); + // graceful reload + pcntl_signal(SIGQUIT, array('\Workerman\Worker', 'signalHandler'), false); + // status + pcntl_signal(SIGUSR2, array('\Workerman\Worker', 'signalHandler'), false); + // connection status + pcntl_signal(SIGIO, array('\Workerman\Worker', 'signalHandler'), false); + // ignore + pcntl_signal(SIGPIPE, SIG_IGN, false); + } + + /** + * Reinstall signal handler. + * + * @return void + */ + protected static function reinstallSignal() + { + if (static::$_OS !== OS_TYPE_LINUX) { + return; + } + // uninstall stop signal handler + pcntl_signal(SIGINT, SIG_IGN, false); + // uninstall graceful stop signal handler + pcntl_signal(SIGTERM, SIG_IGN, false); + // uninstall reload signal handler + pcntl_signal(SIGUSR1, SIG_IGN, false); + // uninstall graceful reload signal handler + pcntl_signal(SIGQUIT, SIG_IGN, false); + // uninstall status signal handler + pcntl_signal(SIGUSR2, SIG_IGN, false); + // uninstall connections status signal handler + pcntl_signal(SIGIO, SIG_IGN, false); + // reinstall stop signal handler + static::$globalEvent->add(SIGINT, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + // reinstall graceful stop signal handler + static::$globalEvent->add(SIGTERM, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + // reinstall reload signal handler + static::$globalEvent->add(SIGUSR1, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + // reinstall graceful reload signal handler + static::$globalEvent->add(SIGQUIT, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + // reinstall status signal handler + static::$globalEvent->add(SIGUSR2, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + // reinstall connection status signal handler + static::$globalEvent->add(SIGIO, EventInterface::EV_SIGNAL, array('\Workerman\Worker', 'signalHandler')); + } + + /** + * Signal handler. + * + * @param int $signal + */ + public static function signalHandler($signal) + { + switch ($signal) { + // Stop. + case SIGINT: + static::$_gracefulStop = false; + static::stopAll(); + break; + // Graceful stop. + case SIGTERM: + static::$_gracefulStop = true; + static::stopAll(); + break; + // Reload. + case SIGQUIT: + case SIGUSR1: + if($signal === SIGQUIT){ + static::$_gracefulStop = true; + }else{ + static::$_gracefulStop = false; + } + static::$_pidsToRestart = static::getAllWorkerPids(); + static::reload(); + break; + // Show status. + case SIGUSR2: + static::writeStatisticsToStatusFile(); + break; + // Show connection status. + case SIGIO: + static::writeConnectionsStatisticsToStatusFile(); + break; + } + } + + /** + * Run as deamon mode. + * + * @throws Exception + */ + protected static function daemonize() + { + if (!static::$daemonize || static::$_OS !== OS_TYPE_LINUX) { + return; + } + umask(0); + $pid = pcntl_fork(); + if (-1 === $pid) { + throw new Exception('fork fail'); + } elseif ($pid > 0) { + exit(0); + } + if (-1 === posix_setsid()) { + throw new Exception("setsid fail"); + } + // Fork again avoid SVR4 system regain the control of terminal. + $pid = pcntl_fork(); + if (-1 === $pid) { + throw new Exception("fork fail"); + } elseif (0 !== $pid) { + exit(0); + } + } + + /** + * Redirect standard input and output. + * + * @throws Exception + */ + public static function resetStd() + { + if (!static::$daemonize || static::$_OS !== OS_TYPE_LINUX) { + return; + } + global $STDOUT, $STDERR; + $handle = fopen(static::$stdoutFile, "a"); + if ($handle) { + unset($handle); + set_error_handler(function(){}); + fclose($STDOUT); + fclose($STDERR); + fclose(STDOUT); + fclose(STDERR); + $STDOUT = fopen(static::$stdoutFile, "a"); + $STDERR = fopen(static::$stdoutFile, "a"); + // change output stream + static::$_outputStream = null; + static::outputStream($STDOUT); + restore_error_handler(); + } else { + throw new Exception('can not open stdoutFile ' . static::$stdoutFile); + } + } + + /** + * Save pid. + * + * @throws Exception + */ + protected static function saveMasterPid() + { + if (static::$_OS !== OS_TYPE_LINUX) { + return; + } + + static::$_masterPid = posix_getpid(); + if (false === file_put_contents(static::$pidFile, static::$_masterPid)) { + throw new Exception('can not save pid to ' . static::$pidFile); + } + } + + /** + * Get event loop name. + * + * @return string + */ + protected static function getEventLoopName() + { + if (static::$eventLoopClass) { + return static::$eventLoopClass; + } + + if (!class_exists('\Swoole\Event', false)) { + unset(static::$_availableEventLoops['swoole']); + } + + $loop_name = ''; + foreach (static::$_availableEventLoops as $name=>$class) { + if (extension_loaded($name)) { + $loop_name = $name; + break; + } + } + + if ($loop_name) { + if (interface_exists('\React\EventLoop\LoopInterface')) { + switch ($loop_name) { + case 'libevent': + static::$eventLoopClass = '\Workerman\Events\React\ExtLibEventLoop'; + break; + case 'event': + static::$eventLoopClass = '\Workerman\Events\React\ExtEventLoop'; + break; + default : + static::$eventLoopClass = '\Workerman\Events\React\StreamSelectLoop'; + break; + } + } else { + static::$eventLoopClass = static::$_availableEventLoops[$loop_name]; + } + } else { + static::$eventLoopClass = interface_exists('\React\EventLoop\LoopInterface')? '\Workerman\Events\React\StreamSelectLoop':'\Workerman\Events\Select'; + } + return static::$eventLoopClass; + } + + /** + * Get all pids of worker processes. + * + * @return array + */ + protected static function getAllWorkerPids() + { + $pid_array = array(); + foreach (static::$_pidMap as $worker_pid_array) { + foreach ($worker_pid_array as $worker_pid) { + $pid_array[$worker_pid] = $worker_pid; + } + } + return $pid_array; + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkers() + { + if (static::$_OS === OS_TYPE_LINUX) { + static::forkWorkersForLinux(); + } else { + static::forkWorkersForWindows(); + } + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkersForLinux() + { + + foreach (static::$_workers as $worker) { + if (static::$_status === static::STATUS_STARTING) { + if (empty($worker->name)) { + $worker->name = $worker->getSocketName(); + } + $worker_name_length = strlen($worker->name); + if (static::$_maxWorkerNameLength < $worker_name_length) { + static::$_maxWorkerNameLength = $worker_name_length; + } + } + + while (count(static::$_pidMap[$worker->workerId]) < $worker->count) { + static::forkOneWorkerForLinux($worker); + } + } + } + + /** + * Fork some worker processes. + * + * @return void + */ + protected static function forkWorkersForWindows() + { + $files = static::getStartFilesForWindows(); + global $argv; + if(in_array('-q', $argv) || count($files) === 1) + { + if(count(static::$_workers) > 1) + { + static::safeEcho("@@@ Error: multi workers init in one php file are not support @@@\r\n"); + static::safeEcho("@@@ See http://doc.workerman.net/faq/multi-woker-for-windows.html @@@\r\n"); + } + elseif(count(static::$_workers) <= 0) + { + exit("@@@no worker inited@@@\r\n\r\n"); + } + + reset(static::$_workers); + /** @var Worker $worker */ + $worker = current(static::$_workers); + + // Display UI. + static::safeEcho(str_pad($worker->name, 21) . str_pad($worker->getSocketName(), 36) . str_pad($worker->count, 10) . "[ok]\n"); + $worker->listen(); + $worker->run(); + exit("@@@child exit@@@\r\n"); + } + else + { + static::$globalEvent = new \Workerman\Events\Select(); + Timer::init(static::$globalEvent); + foreach($files as $start_file) + { + static::forkOneWorkerForWindows($start_file); + } + } + } + + /** + * Get start files for windows. + * + * @return array + */ + public static function getStartFilesForWindows() { + global $argv; + $files = array(); + foreach($argv as $file) + { + if(is_file($file)) + { + $files[$file] = $file; + } + } + return $files; + } + + /** + * Fork one worker process. + * + * @param string $start_file + */ + public static function forkOneWorkerForWindows($start_file) + { + $start_file = realpath($start_file); + $std_file = sys_get_temp_dir() . '/'.str_replace(array('/', "\\", ':'), '_', $start_file).'.out.txt'; + + $descriptorspec = array( + 0 => array('pipe', 'a'), // stdin + 1 => array('file', $std_file, 'w'), // stdout + 2 => array('file', $std_file, 'w') // stderr + ); + + + $pipes = array(); + $process = proc_open("php \"$start_file\" -q", $descriptorspec, $pipes); + $std_handler = fopen($std_file, 'a+'); + stream_set_blocking($std_handler, 0); + + if (empty(static::$globalEvent)) { + static::$globalEvent = new Select(); + Timer::init(static::$globalEvent); + } + $timer_id = Timer::add(0.1, function()use($std_handler) + { + Worker::safeEcho(fread($std_handler, 65535)); + }); + + // 保存子进程句柄 + static::$_processForWindows[$start_file] = array($process, $start_file, $timer_id); + } + + /** + * check worker status for windows. + * @return void + */ + public static function checkWorkerStatusForWindows() + { + foreach(static::$_processForWindows as $process_data) + { + $process = $process_data[0]; + $start_file = $process_data[1]; + $timer_id = $process_data[2]; + $status = proc_get_status($process); + if(isset($status['running'])) + { + if(!$status['running']) + { + static::safeEcho("process $start_file terminated and try to restart\n"); + Timer::del($timer_id); + proc_close($process); + static::forkOneWorkerForWindows($start_file); + } + } + else + { + static::safeEcho("proc_get_status fail\n"); + } + } + } + + + /** + * Fork one worker process. + * + * @param \Workerman\Worker $worker + * @throws Exception + */ + protected static function forkOneWorkerForLinux($worker) + { + // Get available worker id. + $id = static::getId($worker->workerId, 0); + if ($id === false) { + return; + } + $pid = pcntl_fork(); + // For master process. + if ($pid > 0) { + static::$_pidMap[$worker->workerId][$pid] = $pid; + static::$_idMap[$worker->workerId][$id] = $pid; + } // For child processes. + elseif (0 === $pid) { + srand(); + mt_srand(); + if ($worker->reusePort) { + $worker->listen(); + } + if (static::$_status === static::STATUS_STARTING) { + static::resetStd(); + } + static::$_pidMap = array(); + // Remove other listener. + foreach(static::$_workers as $key => $one_worker) { + if ($one_worker->workerId !== $worker->workerId) { + $one_worker->unlisten(); + unset(static::$_workers[$key]); + } + } + Timer::delAll(); + static::setProcessTitle('WorkerMan: worker process ' . $worker->name . ' ' . $worker->getSocketName()); + $worker->setUserAndGroup(); + $worker->id = $id; + $worker->run(); + $err = new Exception('event-loop exited'); + static::log($err); + exit(250); + } else { + throw new Exception("forkOneWorker fail"); + } + } + + /** + * Get worker id. + * + * @param int $worker_id + * @param int $pid + * + * @return integer + */ + protected static function getId($worker_id, $pid) + { + return array_search($pid, static::$_idMap[$worker_id]); + } + + /** + * Set unix user and group for current process. + * + * @return void + */ + public function setUserAndGroup() + { + // Get uid. + $user_info = posix_getpwnam($this->user); + if (!$user_info) { + static::log("Warning: User {$this->user} not exsits"); + return; + } + $uid = $user_info['uid']; + // Get gid. + if ($this->group) { + $group_info = posix_getgrnam($this->group); + if (!$group_info) { + static::log("Warning: Group {$this->group} not exsits"); + return; + } + $gid = $group_info['gid']; + } else { + $gid = $user_info['gid']; + } + + // Set uid and gid. + if ($uid != posix_getuid() || $gid != posix_getgid()) { + if (!posix_setgid($gid) || !posix_initgroups($user_info['name'], $gid) || !posix_setuid($uid)) { + static::log("Warning: change gid or uid fail."); + } + } + } + + /** + * Set process name. + * + * @param string $title + * @return void + */ + protected static function setProcessTitle($title) + { + set_error_handler(function(){}); + // >=php 5.5 + if (function_exists('cli_set_process_title')) { + cli_set_process_title($title); + } // Need proctitle when php<=5.5 . + elseif (extension_loaded('proctitle') && function_exists('setproctitle')) { + setproctitle($title); + } + restore_error_handler(); + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkers() + { + if (static::$_OS === OS_TYPE_LINUX) { + static::monitorWorkersForLinux(); + } else { + static::monitorWorkersForWindows(); + } + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkersForLinux() + { + static::$_status = static::STATUS_RUNNING; + while (1) { + // Calls signal handlers for pending signals. + pcntl_signal_dispatch(); + // Suspends execution of the current process until a child has exited, or until a signal is delivered + $status = 0; + $pid = pcntl_wait($status, WUNTRACED); + // Calls signal handlers for pending signals again. + pcntl_signal_dispatch(); + // If a child has already exited. + if ($pid > 0) { + // Find out witch worker process exited. + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + if (isset($worker_pid_array[$pid])) { + $worker = static::$_workers[$worker_id]; + // Exit status. + if ($status !== 0) { + static::log("worker[" . $worker->name . ":$pid] exit with status $status"); + } + + // For Statistics. + if (!isset(static::$_globalStatistics['worker_exit_info'][$worker_id][$status])) { + static::$_globalStatistics['worker_exit_info'][$worker_id][$status] = 0; + } + static::$_globalStatistics['worker_exit_info'][$worker_id][$status]++; + + // Clear process data. + unset(static::$_pidMap[$worker_id][$pid]); + + // Mark id is available. + $id = static::getId($worker_id, $pid); + static::$_idMap[$worker_id][$id] = 0; + + break; + } + } + // Is still running state then fork a new worker process. + if (static::$_status !== static::STATUS_SHUTDOWN) { + static::forkWorkers(); + // If reloading continue. + if (isset(static::$_pidsToRestart[$pid])) { + unset(static::$_pidsToRestart[$pid]); + static::reload(); + } + } + } + + // If shutdown state and all child processes exited then master process exit. + if (static::$_status === static::STATUS_SHUTDOWN && !static::getAllWorkerPids()) { + static::exitAndClearAll(); + } + } + } + + /** + * Monitor all child processes. + * + * @return void + */ + protected static function monitorWorkersForWindows() + { + Timer::add(1, "\\Workerman\\Worker::checkWorkerStatusForWindows"); + + static::$globalEvent->loop(); + } + + /** + * Exit current process. + * + * @return void + */ + protected static function exitAndClearAll() + { + foreach (static::$_workers as $worker) { + $socket_name = $worker->getSocketName(); + if ($worker->transport === 'unix' && $socket_name) { + list(, $address) = explode(':', $socket_name, 2); + @unlink($address); + } + } + @unlink(static::$pidFile); + static::log("Workerman[" . basename(static::$_startFile) . "] has been stopped"); + if (static::$onMasterStop) { + call_user_func(static::$onMasterStop); + } + exit(0); + } + + /** + * Execute reload. + * + * @return void + */ + protected static function reload() + { + // For master process. + if (static::$_masterPid === posix_getpid()) { + // Set reloading state. + if (static::$_status !== static::STATUS_RELOADING && static::$_status !== static::STATUS_SHUTDOWN) { + static::log("Workerman[" . basename(static::$_startFile) . "] reloading"); + static::$_status = static::STATUS_RELOADING; + // Try to emit onMasterReload callback. + if (static::$onMasterReload) { + try { + call_user_func(static::$onMasterReload); + } catch (\Exception $e) { + static::log($e); + exit(250); + } catch (\Error $e) { + static::log($e); + exit(250); + } + static::initId(); + } + } + + if (static::$_gracefulStop) { + $sig = SIGQUIT; + } else { + $sig = SIGUSR1; + } + + // Send reload signal to all child processes. + $reloadable_pid_array = array(); + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + $worker = static::$_workers[$worker_id]; + if ($worker->reloadable) { + foreach ($worker_pid_array as $pid) { + $reloadable_pid_array[$pid] = $pid; + } + } else { + foreach ($worker_pid_array as $pid) { + // Send reload signal to a worker process which reloadable is false. + posix_kill($pid, $sig); + } + } + } + + // Get all pids that are waiting reload. + static::$_pidsToRestart = array_intersect(static::$_pidsToRestart, $reloadable_pid_array); + + // Reload complete. + if (empty(static::$_pidsToRestart)) { + if (static::$_status !== static::STATUS_SHUTDOWN) { + static::$_status = static::STATUS_RUNNING; + } + return; + } + // Continue reload. + $one_worker_pid = current(static::$_pidsToRestart); + // Send reload signal to a worker process. + posix_kill($one_worker_pid, $sig); + // If the process does not exit after static::KILL_WORKER_TIMER_TIME seconds try to kill it. + if(!static::$_gracefulStop){ + Timer::add(static::KILL_WORKER_TIMER_TIME, 'posix_kill', array($one_worker_pid, SIGKILL), false); + } + } // For child processes. + else { + reset(static::$_workers); + $worker = current(static::$_workers); + // Try to emit onWorkerReload callback. + if ($worker->onWorkerReload) { + try { + call_user_func($worker->onWorkerReload, $worker); + } catch (\Exception $e) { + static::log($e); + exit(250); + } catch (\Error $e) { + static::log($e); + exit(250); + } + } + + if ($worker->reloadable) { + static::stopAll(); + } + } + } + + /** + * Stop. + * + * @return void + */ + public static function stopAll() + { + static::$_status = static::STATUS_SHUTDOWN; + // For master process. + if (static::$_masterPid === posix_getpid()) { + static::log("Workerman[" . basename(static::$_startFile) . "] stopping ..."); + $worker_pid_array = static::getAllWorkerPids(); + // Send stop signal to all child processes. + if (static::$_gracefulStop) { + $sig = SIGTERM; + } else { + $sig = SIGINT; + } + foreach ($worker_pid_array as $worker_pid) { + posix_kill($worker_pid, $sig); + if(!static::$_gracefulStop){ + Timer::add(static::KILL_WORKER_TIMER_TIME, 'posix_kill', array($worker_pid, SIGKILL), false); + } + } + Timer::add(1, "\\Workerman\\Worker::checkIfChildRunning"); + // Remove statistics file. + if (is_file(static::$_statisticsFile)) { + @unlink(static::$_statisticsFile); + } + } // For child processes. + else { + // Execute exit. + foreach (static::$_workers as $worker) { + if(!$worker->stopping){ + $worker->stop(); + $worker->stopping = true; + } + } + if (!static::$_gracefulStop || ConnectionInterface::$statistics['connection_count'] <= 0) { + static::$_workers = array(); + if (static::$globalEvent) { + static::$globalEvent->destroy(); + } + exit(0); + } + } + } + + /** + * check if child processes is really running + */ + public static function checkIfChildRunning() + { + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + foreach ($worker_pid_array as $pid => $worker_pid) { + if (!posix_kill($pid, 0)) { + unset(static::$_pidMap[$worker_id][$pid]); + } + } + } + } + + /** + * Get process status. + * + * @return number + */ + public static function getStatus() + { + return static::$_status; + } + + /** + * If stop gracefully. + * + * @return boolean + */ + public static function getGracefulStop() + { + return static::$_gracefulStop; + } + + /** + * Write statistics data to disk. + * + * @return void + */ + protected static function writeStatisticsToStatusFile() + { + // For master process. + if (static::$_masterPid === posix_getpid()) { + $all_worker_info = array(); + foreach(static::$_pidMap as $worker_id => $pid_array) { + /** @var /Workerman/Worker $worker */ + $worker = static::$_workers[$worker_id]; + foreach($pid_array as $pid) { + $all_worker_info[$pid] = array('name' => $worker->name, 'listen' => $worker->getSocketName()); + } + } + + file_put_contents(static::$_statisticsFile, json_encode($all_worker_info)."\n", FILE_APPEND); + $loadavg = function_exists('sys_getloadavg') ? array_map('round', sys_getloadavg(), array(2)) : array('-', '-', '-'); + file_put_contents(static::$_statisticsFile, + "----------------------------------------------GLOBAL STATUS----------------------------------------------------\n", FILE_APPEND); + file_put_contents(static::$_statisticsFile, + 'Workerman version:' . static::VERSION . " PHP version:" . PHP_VERSION . "\n", FILE_APPEND); + file_put_contents(static::$_statisticsFile, 'start time:' . date('Y-m-d H:i:s', + static::$_globalStatistics['start_timestamp']) . ' run ' . floor((time() - static::$_globalStatistics['start_timestamp']) / (24 * 60 * 60)) . ' days ' . floor(((time() - static::$_globalStatistics['start_timestamp']) % (24 * 60 * 60)) / (60 * 60)) . " hours \n", + FILE_APPEND); + $load_str = 'load average: ' . implode(", ", $loadavg); + file_put_contents(static::$_statisticsFile, + str_pad($load_str, 33) . 'event-loop:' . static::getEventLoopName() . "\n", FILE_APPEND); + file_put_contents(static::$_statisticsFile, + count(static::$_pidMap) . ' workers ' . count(static::getAllWorkerPids()) . " processes\n", + FILE_APPEND); + file_put_contents(static::$_statisticsFile, + str_pad('worker_name', static::$_maxWorkerNameLength) . " exit_status exit_count\n", FILE_APPEND); + foreach (static::$_pidMap as $worker_id => $worker_pid_array) { + $worker = static::$_workers[$worker_id]; + if (isset(static::$_globalStatistics['worker_exit_info'][$worker_id])) { + foreach (static::$_globalStatistics['worker_exit_info'][$worker_id] as $worker_exit_status => $worker_exit_count) { + file_put_contents(static::$_statisticsFile, + str_pad($worker->name, static::$_maxWorkerNameLength) . " " . str_pad($worker_exit_status, + 16) . " $worker_exit_count\n", FILE_APPEND); + } + } else { + file_put_contents(static::$_statisticsFile, + str_pad($worker->name, static::$_maxWorkerNameLength) . " " . str_pad(0, 16) . " 0\n", + FILE_APPEND); + } + } + file_put_contents(static::$_statisticsFile, + "----------------------------------------------PROCESS STATUS---------------------------------------------------\n", + FILE_APPEND); + file_put_contents(static::$_statisticsFile, + "pid\tmemory " . str_pad('listening', static::$_maxSocketNameLength) . " " . str_pad('worker_name', + static::$_maxWorkerNameLength) . " connections " . str_pad('send_fail', 9) . " " + . str_pad('timers', 8) . str_pad('total_request', 13) ." qps status\n", FILE_APPEND); + + chmod(static::$_statisticsFile, 0722); + + foreach (static::getAllWorkerPids() as $worker_pid) { + posix_kill($worker_pid, SIGUSR2); + } + return; + } + + // For child processes. + reset(static::$_workers); + /** @var \Workerman\Worker $worker */ + $worker = current(static::$_workers); + $worker_status_str = posix_getpid() . "\t" . str_pad(round(memory_get_usage(true) / (1024 * 1024), 2) . "M", 7) + . " " . str_pad($worker->getSocketName(), static::$_maxSocketNameLength) . " " + . str_pad(($worker->name === $worker->getSocketName() ? 'none' : $worker->name), static::$_maxWorkerNameLength) + . " "; + $worker_status_str .= str_pad(ConnectionInterface::$statistics['connection_count'], 11) + . " " . str_pad(ConnectionInterface::$statistics['send_fail'], 9) + . " " . str_pad(static::$globalEvent->getTimerCount(), 7) + . " " . str_pad(ConnectionInterface::$statistics['total_request'], 13) . "\n"; + file_put_contents(static::$_statisticsFile, $worker_status_str, FILE_APPEND); + } + + /** + * Write statistics data to disk. + * + * @return void + */ + protected static function writeConnectionsStatisticsToStatusFile() + { + // For master process. + if (static::$_masterPid === posix_getpid()) { + file_put_contents(static::$_statisticsFile, "--------------------------------------------------------------------- WORKERMAN CONNECTION STATUS --------------------------------------------------------------------------------\n", FILE_APPEND); + file_put_contents(static::$_statisticsFile, "PID Worker CID Trans Protocol ipv4 ipv6 Recv-Q Send-Q Bytes-R Bytes-W Status Local Address Foreign Address\n", FILE_APPEND); + chmod(static::$_statisticsFile, 0722); + foreach (static::getAllWorkerPids() as $worker_pid) { + posix_kill($worker_pid, SIGIO); + } + return; + } + + // For child processes. + $bytes_format = function($bytes) + { + if($bytes > 1024*1024*1024*1024) { + return round($bytes/(1024*1024*1024*1024), 1)."TB"; + } + if($bytes > 1024*1024*1024) { + return round($bytes/(1024*1024*1024), 1)."GB"; + } + if($bytes > 1024*1024) { + return round($bytes/(1024*1024), 1)."MB"; + } + if($bytes > 1024) { + return round($bytes/(1024), 1)."KB"; + } + return $bytes."B"; + }; + + $pid = posix_getpid(); + $str = ''; + reset(static::$_workers); + $current_worker = current(static::$_workers); + $default_worker_name = $current_worker->name; + + /** @var \Workerman\Worker $worker */ + foreach(TcpConnection::$connections as $connection) { + /** @var \Workerman\Connection\TcpConnection $connection */ + $transport = $connection->transport; + $ipv4 = $connection->isIpV4() ? ' 1' : ' 0'; + $ipv6 = $connection->isIpV6() ? ' 1' : ' 0'; + $recv_q = $bytes_format($connection->getRecvBufferQueueSize()); + $send_q = $bytes_format($connection->getSendBufferQueueSize()); + $local_address = trim($connection->getLocalAddress()); + $remote_address = trim($connection->getRemoteAddress()); + $state = $connection->getStatus(false); + $bytes_read = $bytes_format($connection->bytesRead); + $bytes_written = $bytes_format($connection->bytesWritten); + $id = $connection->id; + $protocol = $connection->protocol ? $connection->protocol : $connection->transport; + $pos = strrpos($protocol, '\\'); + if ($pos) { + $protocol = substr($protocol, $pos+1); + } + if (strlen($protocol) > 15) { + $protocol = substr($protocol, 0, 13) . '..'; + } + $worker_name = isset($connection->worker) ? $connection->worker->name : $default_worker_name; + if (strlen($worker_name) > 14) { + $worker_name = substr($worker_name, 0, 12) . '..'; + } + $str .= str_pad($pid, 9) . str_pad($worker_name, 16) . str_pad($id, 10) . str_pad($transport, 8) + . str_pad($protocol, 16) . str_pad($ipv4, 7) . str_pad($ipv6, 7) . str_pad($recv_q, 13) + . str_pad($send_q, 13) . str_pad($bytes_read, 13) . str_pad($bytes_written, 13) . ' ' + . str_pad($state, 14) . ' ' . str_pad($local_address, 22) . ' ' . str_pad($remote_address, 22) ."\n"; + } + if ($str) { + file_put_contents(static::$_statisticsFile, $str, FILE_APPEND); + } + } + + /** + * Check errors when current process exited. + * + * @return void + */ + public static function checkErrors() + { + if (static::STATUS_SHUTDOWN != static::$_status) { + $error_msg = static::$_OS === OS_TYPE_LINUX ? 'Worker['. posix_getpid() .'] process terminated' : 'Worker process terminated'; + $errors = error_get_last(); + if ($errors && ($errors['type'] === E_ERROR || + $errors['type'] === E_PARSE || + $errors['type'] === E_CORE_ERROR || + $errors['type'] === E_COMPILE_ERROR || + $errors['type'] === E_RECOVERABLE_ERROR) + ) { + $error_msg .= ' with ERROR: ' . static::getErrorType($errors['type']) . " \"{$errors['message']} in {$errors['file']} on line {$errors['line']}\""; + } + static::log($error_msg); + } + } + + /** + * Get error message by error code. + * + * @param integer $type + * @return string + */ + protected static function getErrorType($type) + { + switch ($type) { + case E_ERROR: // 1 // + return 'E_ERROR'; + case E_WARNING: // 2 // + return 'E_WARNING'; + case E_PARSE: // 4 // + return 'E_PARSE'; + case E_NOTICE: // 8 // + return 'E_NOTICE'; + case E_CORE_ERROR: // 16 // + return 'E_CORE_ERROR'; + case E_CORE_WARNING: // 32 // + return 'E_CORE_WARNING'; + case E_COMPILE_ERROR: // 64 // + return 'E_COMPILE_ERROR'; + case E_COMPILE_WARNING: // 128 // + return 'E_COMPILE_WARNING'; + case E_USER_ERROR: // 256 // + return 'E_USER_ERROR'; + case E_USER_WARNING: // 512 // + return 'E_USER_WARNING'; + case E_USER_NOTICE: // 1024 // + return 'E_USER_NOTICE'; + case E_STRICT: // 2048 // + return 'E_STRICT'; + case E_RECOVERABLE_ERROR: // 4096 // + return 'E_RECOVERABLE_ERROR'; + case E_DEPRECATED: // 8192 // + return 'E_DEPRECATED'; + case E_USER_DEPRECATED: // 16384 // + return 'E_USER_DEPRECATED'; + } + return ""; + } + + /** + * Log. + * + * @param string $msg + * @return void + */ + public static function log($msg) + { + $msg = $msg . "\n"; + if (!static::$daemonize) { + static::safeEcho($msg); + } + file_put_contents((string)static::$logFile, date('Y-m-d H:i:s') . ' ' . 'pid:' + . (static::$_OS === OS_TYPE_LINUX ? posix_getpid() : 1) . ' ' . $msg, FILE_APPEND | LOCK_EX); + } + + /** + * Safe Echo. + * @param $msg + * @param bool $decorated + * @return bool + */ + public static function safeEcho($msg, $decorated = false) + { + $stream = static::outputStream(); + if (!$stream) { + return false; + } + if (!$decorated) { + $line = $white = $green = $end = ''; + if (static::$_outputDecorated) { + $line = "\033[1A\n\033[K"; + $white = "\033[47;30m"; + $green = "\033[32;40m"; + $end = "\033[0m"; + } + $msg = str_replace(array('', '', ''), array($line, $white, $green), $msg); + $msg = str_replace(array('', '', ''), $end, $msg); + } elseif (!static::$_outputDecorated) { + return false; + } + fwrite($stream, $msg); + fflush($stream); + return true; + } + + /** + * @param null $stream + * @return bool|resource + */ + private static function outputStream($stream = null) + { + if (!$stream) { + $stream = static::$_outputStream ? static::$_outputStream : STDOUT; + } + if (!$stream || !is_resource($stream) || 'stream' !== get_resource_type($stream)) { + return false; + } + $stat = fstat($stream); + if (($stat['mode'] & 0170000) === 0100000) { + // file + static::$_outputDecorated = false; + } else { + static::$_outputDecorated = + static::$_OS === OS_TYPE_LINUX && + function_exists('posix_isatty') && + posix_isatty($stream); + } + return static::$_outputStream = $stream; + } + + /** + * Construct. + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name = '', $context_option = array()) + { + // Save all worker instances. + $this->workerId = spl_object_hash($this); + static::$_workers[$this->workerId] = $this; + static::$_pidMap[$this->workerId] = array(); + + // Get autoload root path. + $backtrace = debug_backtrace(); + $this->_autoloadRootPath = dirname($backtrace[0]['file']); + + // Context for socket. + if ($socket_name) { + $this->_socketName = $socket_name; + if (!isset($context_option['socket']['backlog'])) { + $context_option['socket']['backlog'] = static::DEFAULT_BACKLOG; + } + $this->_context = stream_context_create($context_option); + } + } + + + /** + * Listen. + * + * @throws Exception + */ + public function listen() + { + if (!$this->_socketName) { + return; + } + + // Autoload. + Autoloader::setRootPath($this->_autoloadRootPath); + + if (!$this->_mainSocket) { + // 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])) { + $scheme = ucfirst($scheme); + $this->protocol = substr($scheme,0,1)==='\\' ? $scheme : '\\Protocols\\' . $scheme; + if (!class_exists($this->protocol)) { + $this->protocol = "\\Workerman\\Protocols\\$scheme"; + if (!class_exists($this->protocol)) { + throw new Exception("class \\Protocols\\$scheme not exist"); + } + } + + if (!isset(static::$_builtinTransports[$this->transport])) { + throw new \Exception('Bad worker->transport ' . var_export($this->transport, true)); + } + } else { + $this->transport = $scheme; + } + + $local_socket = static::$_builtinTransports[$this->transport] . ":" . $address; + + // Flag. + $flags = $this->transport === 'udp' ? STREAM_SERVER_BIND : STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; + $errno = 0; + $errmsg = ''; + // SO_REUSEPORT. + if ($this->reusePort) { + stream_context_set_option($this->_context, 'socket', 'so_reuseport', 1); + } + + // Create an Internet or Unix domain server socket. + $this->_mainSocket = stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context); + if (!$this->_mainSocket) { + throw new Exception($errmsg); + } + + if ($this->transport === 'ssl') { + stream_socket_enable_crypto($this->_mainSocket, false); + } elseif ($this->transport === 'unix') { + $socketFile = substr($address, 2); + if ($this->user) { + chown($socketFile, $this->user); + } + if ($this->group) { + chgrp($socketFile, $this->group); + } + } + + // Try to open keepalive for tcp and disable Nagle algorithm. + if (function_exists('socket_import_stream') && static::$_builtinTransports[$this->transport] === 'tcp') { + set_error_handler(function(){}); + $socket = socket_import_stream($this->_mainSocket); + socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1); + socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1); + restore_error_handler(); + } + + // Non blocking. + stream_set_blocking($this->_mainSocket, 0); + } + + $this->resumeAccept(); + } + + /** + * Unlisten. + * + * @return void + */ + public function unlisten() { + $this->pauseAccept(); + if ($this->_mainSocket) { + set_error_handler(function(){}); + fclose($this->_mainSocket); + restore_error_handler(); + $this->_mainSocket = null; + } + } + + /** + * Pause accept new connections. + * + * @return void + */ + public function pauseAccept() + { + if (static::$globalEvent && false === $this->_pauseAccept && $this->_mainSocket) { + static::$globalEvent->del($this->_mainSocket, EventInterface::EV_READ); + $this->_pauseAccept = true; + } + } + + /** + * Resume accept new connections. + * + * @return void + */ + public function resumeAccept() + { + // Register a listener to be notified when server socket is ready to read. + if (static::$globalEvent && true === $this->_pauseAccept && $this->_mainSocket) { + if ($this->transport !== 'udp') { + static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection')); + } else { + static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptUdpConnection')); + } + $this->_pauseAccept = false; + } + } + + /** + * Get socket name. + * + * @return string + */ + public function getSocketName() + { + return $this->_socketName ? lcfirst($this->_socketName) : 'none'; + } + + /** + * Run worker instance. + * + * @return void + */ + public function run() + { + //Update process state. + static::$_status = static::STATUS_RUNNING; + + // Register shutdown function for checking errors. + register_shutdown_function(array("\\Workerman\\Worker", 'checkErrors')); + + // Set autoload root path. + Autoloader::setRootPath($this->_autoloadRootPath); + + // Create a global event loop. + if (!static::$globalEvent) { + $event_loop_class = static::getEventLoopName(); + static::$globalEvent = new $event_loop_class; + $this->resumeAccept(); + } + + // Reinstall signal. + static::reinstallSignal(); + + // Init Timer. + Timer::init(static::$globalEvent); + + // Set an empty onMessage callback. + if (empty($this->onMessage)) { + $this->onMessage = function () {}; + } + + restore_error_handler(); + + // Try to emit onWorkerStart callback. + if ($this->onWorkerStart) { + try { + call_user_func($this->onWorkerStart, $this); + } catch (\Exception $e) { + static::log($e); + // Avoid rapid infinite loop exit. + sleep(1); + exit(250); + } catch (\Error $e) { + static::log($e); + // Avoid rapid infinite loop exit. + sleep(1); + exit(250); + } + } + + // Main loop. + static::$globalEvent->loop(); + } + + /** + * Stop current worker instance. + * + * @return void + */ + public function stop() + { + // Try to emit onWorkerStop callback. + if ($this->onWorkerStop) { + try { + call_user_func($this->onWorkerStop, $this); + } catch (\Exception $e) { + static::log($e); + exit(250); + } catch (\Error $e) { + static::log($e); + exit(250); + } + } + // Remove listener for server socket. + $this->unlisten(); + // Close all connections for the worker. + if (!static::$_gracefulStop) { + foreach ($this->connections as $connection) { + $connection->close(); + } + } + // Clear callback. + $this->onMessage = $this->onClose = $this->onError = $this->onBufferDrain = $this->onBufferFull = null; + } + + /** + * Accept a connection. + * + * @param resource $socket + * @return void + */ + public function acceptConnection($socket) + { + // Accept a connection on server socket. + set_error_handler(function(){}); + $new_socket = stream_socket_accept($socket, 0, $remote_address); + restore_error_handler(); + + // Thundering herd. + if (!$new_socket) { + return; + } + + // TcpConnection. + $connection = new TcpConnection($new_socket, $remote_address); + $this->connections[$connection->id] = $connection; + $connection->worker = $this; + $connection->protocol = $this->protocol; + $connection->transport = $this->transport; + $connection->onMessage = $this->onMessage; + $connection->onClose = $this->onClose; + $connection->onError = $this->onError; + $connection->onBufferDrain = $this->onBufferDrain; + $connection->onBufferFull = $this->onBufferFull; + + // Try to emit onConnect callback. + if ($this->onConnect) { + try { + call_user_func($this->onConnect, $connection); + } catch (\Exception $e) { + static::log($e); + exit(250); + } catch (\Error $e) { + static::log($e); + exit(250); + } + } + } + + /** + * For udp package. + * + * @param resource $socket + * @return bool + */ + public function acceptUdpConnection($socket) + { + set_error_handler(function(){}); + $recv_buffer = stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remote_address); + restore_error_handler(); + if (false === $recv_buffer || empty($remote_address)) { + return false; + } + // UdpConnection. + $connection = new UdpConnection($socket, $remote_address); + $connection->protocol = $this->protocol; + if ($this->onMessage) { + try { + if ($this->protocol !== null) { + /** @var \Workerman\Protocols\ProtocolInterface $parser */ + $parser = $this->protocol; + if(method_exists($parser,'input')){ + while($recv_buffer !== ''){ + $len = $parser::input($recv_buffer, $connection); + if($len == 0) + return true; + $package = substr($recv_buffer,0,$len); + $recv_buffer = substr($recv_buffer,$len); + $data = $parser::decode($package,$connection); + if ($data === false) + continue; + call_user_func($this->onMessage, $connection, $data); + } + }else{ + $data = $parser::decode($recv_buffer, $connection); + // Discard bad packets. + if ($data === false) + return true; + call_user_func($this->onMessage, $connection, $data); + } + }else{ + call_user_func($this->onMessage, $connection, $recv_buffer); + } + ConnectionInterface::$statistics['total_request']++; + } catch (\Exception $e) { + static::log($e); + exit(250); + } catch (\Error $e) { + static::log($e); + exit(250); + } + } + return true; + } +} diff --git a/workerman/composer.json b/workerman/composer.json new file mode 100755 index 0000000..fdd4808 --- /dev/null +++ b/workerman/composer.json @@ -0,0 +1,38 @@ +{ + "name": "workerman/workerman", + "type": "library", + "keywords": [ + "event-loop", + "asynchronous" + ], + "homepage": "http://www.workerman.net", + "license": "MIT", + "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "support": { + "email": "walkor@workerman.net", + "issues": "https://github.com/walkor/workerman/issues", + "forum": "http://wenda.workerman.net/", + "wiki": "http://doc.workerman.net/", + "source": "https://github.com/walkor/workerman" + }, + "require": { + "php": ">=5.3" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "autoload": { + "psr-4": { + "Workerman\\": "./" + } + }, + "minimum-stability": "dev" +}