├── README.md ├── classes ├── Evenement │ ├── EventEmitter.php │ ├── EventEmitterInterface.php │ └── EventEmitterTrait.php ├── React │ ├── Cache │ │ ├── ArrayCache.php │ │ ├── CacheInterface.php │ │ ├── README.md │ │ └── composer.json │ ├── Dns │ │ ├── BadServerException.php │ │ ├── Config │ │ │ ├── Config.php │ │ │ └── FilesystemFactory.php │ │ ├── Model │ │ │ ├── HeaderBag.php │ │ │ ├── Message.php │ │ │ └── Record.php │ │ ├── Protocol │ │ │ ├── BinaryDumper.php │ │ │ └── Parser.php │ │ ├── Query │ │ │ ├── CachedExecutor.php │ │ │ ├── Executor.php │ │ │ ├── ExecutorInterface.php │ │ │ ├── Query.php │ │ │ ├── RecordBag.php │ │ │ ├── RecordCache.php │ │ │ ├── RetryExecutor.php │ │ │ └── TimeoutException.php │ │ ├── README.md │ │ ├── RecordNotFoundException.php │ │ ├── Resolver │ │ │ ├── Factory.php │ │ │ └── Resolver.php │ │ ├── composer.json │ │ └── doc │ │ │ ├── rfc1034.txt │ │ │ └── rfc1035.txt │ ├── EventLoop │ │ ├── Factory.php │ │ ├── LibEvLoop.php │ │ ├── LibEventLoop.php │ │ ├── LoopInterface.php │ │ ├── README.md │ │ ├── StreamSelectLoop.php │ │ ├── Timer │ │ │ └── Timers.php │ │ └── composer.json │ ├── Http │ │ ├── README.md │ │ ├── Request.php │ │ ├── RequestHeaderParser.php │ │ ├── Response.php │ │ ├── Server.php │ │ ├── ServerInterface.php │ │ └── composer.json │ ├── HttpClient │ │ ├── Client.php │ │ ├── ConnectionManager.php │ │ ├── ConnectionManagerInterface.php │ │ ├── Factory.php │ │ ├── README.md │ │ ├── Request.php │ │ ├── Response.php │ │ ├── SecureConnectionManager.php │ │ └── composer.json │ ├── Socket │ │ ├── Connection.php │ │ ├── ConnectionException.php │ │ ├── ConnectionInterface.php │ │ ├── README.md │ │ ├── Server.php │ │ ├── ServerInterface.php │ │ └── composer.json │ └── Stream │ │ ├── Buffer.php │ │ ├── BufferedSink.php │ │ ├── CompositeStream.php │ │ ├── README.md │ │ ├── ReadableStream.php │ │ ├── ReadableStreamInterface.php │ │ ├── Stream.php │ │ ├── StreamInterface.php │ │ ├── ThroughStream.php │ │ ├── Util.php │ │ ├── WritableStream.php │ │ ├── WritableStreamInterface.php │ │ └── composer.json ├── ctrl │ ├── ChatCtrl.php │ ├── IndexCtrl.php │ └── MemcacheCtrl.php ├── framework │ ├── config │ │ ├── BeanStalkConfiguration.php │ │ ├── MemcachedConfiguration.php │ │ ├── PDOConfiguration.php │ │ ├── RedisConfiguration.php │ │ ├── SmartyConfiguration.php │ │ └── TokyoTyrantConfiguration.php │ ├── core │ │ ├── Context.php │ │ ├── IController.php │ │ ├── IRequestDispatcher.php │ │ └── IView.php │ ├── dispatcher │ │ ├── HTTPRequestDispatcher.php │ │ ├── RequestDispatcherBase.php │ │ └── ShellRequestDispatcher.php │ ├── helper │ │ ├── CacheHelper.php │ │ ├── PDOHelper.php │ │ ├── QueueHelper.php │ │ ├── StorageHelper.php │ │ ├── cache │ │ │ ├── ApcHelper.php │ │ │ ├── ICacheHelper.php │ │ │ ├── MemcachedHelper.php │ │ │ ├── RedisHelper.php │ │ │ └── XcacheHelper.php │ │ ├── queue │ │ │ ├── BeanStalkHelper.php │ │ │ ├── IQueue.php │ │ │ └── RedisHelper.php │ │ └── storage │ │ │ ├── IStorage.php │ │ │ ├── LRedisHelper.php │ │ │ ├── RedisHelper.php │ │ │ └── TTHelper.php │ ├── manager │ │ ├── BeanStalkManager.php │ │ ├── LRedisManager.php │ │ ├── MemcachedManager.php │ │ ├── PDOManager.php │ │ ├── RedisManager.php │ │ └── TokyoTyrantManager.php │ ├── setup.php │ ├── socket │ │ └── Socket.php │ ├── util │ │ ├── Daemon.php │ │ ├── FileUtil.php │ │ ├── Formater.php │ │ ├── Serialize.php │ │ └── Singleton.php │ └── view │ │ ├── JSONView.php │ │ ├── MsgPackView.php │ │ ├── SmartyView.php │ │ └── StringView.php └── socket │ ├── Chat.php │ └── Memcache.php ├── inf └── default │ ├── daemon.php │ └── define.php └── webroot └── index.php /README.md: -------------------------------------------------------------------------------- 1 | zphp 2 | ==== 3 | 4 | @author: shenzhe (泽泽,半桶水) 5 | 6 | @email: shenzhe163@gmail.com 7 | 8 | a php framework, 专用于社交游戏 && 网页游戏的服务器端开发框架 9 | 10 | zphp是一个极轻的框架,核心只提供类自动载入,路由功能,跟据游戏的特性,提供:存储(ttserver, redis, redis-storage),cache(apc, memcache, redis, xcache), db(mysql),队列(beanstalk, redis),socket功能,你可能会发现存储居然没有mysql,这就是游戏,特别是社交游戏的特性:高并发,读写几乎都是并存的,没有明显冷数据,mysql不太适合这个场景 11 | 12 | 要求:php5.3+ 13 | 14 | 更新 15 | =================== 16 | 17 | 2012-12-29: 更换socket层为:react, 独立于框架,类node语法,使socket使用更稳定和方便。 18 | 19 | 2012-12-29: 增加daemon支持(命令行后加 -d 即可),可以把服务变成一个daemon, 可接收进程控制信号,进行服务关闭,重启,重载等 20 | 21 | socket需要libevent扩展 : 22 | ======================== 23 | 24 | 地址:https://github.com/shenzhe/php-libevent 25 | 26 | 27 | 特别支持redis-storage : 28 | ===================== 29 | 30 | redis-stroage地址: https://github.com/qiye/redis-storage 31 | 32 | 增强版phpredis扩展:https://github.com/shenzhe/phpredis 33 | 34 | 35 | 聊天室demo: 36 | ============= 37 | 38 | cd 程序目录 39 | php webroot/index.php Chat.new -d (以daemon方式启动) 40 | 41 | 客户端: telnet host ip (host ,ip 在 inf/default/define.php 里设置 ) 42 | 43 | php webroot/index.php Chat.stop (关闭服务) 44 | 45 | php版key=>value数据库Demo (基于memcache协议): 46 | ===================== 47 | 48 | cd 程序目录 49 | php webroot/index.php Memcache.new 50 | 51 | 客户端: 可以像操作memcache一样操作,目前支持的命令(get ,set delete) 52 | 53 | 54 | 55 | 一个典型的框架目录结构 56 | ================== 57 | 58 | classes 59 | -- ctrl //ctrl目录 60 | IndexCtrl.php 61 | -- framework //框架目录 62 | 63 | inf //配置目录 64 | --default //默认配置目录 65 | define.php 66 | 67 | webroot //网站根目录 68 | index.php 69 | 70 | 71 | index.php代码示例: 72 | 73 | dispatch(); 84 | 85 | IndexCtrl.php代码示例: 86 | 87 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Evenement; 13 | 14 | class EventEmitter implements EventEmitterInterface 15 | { 16 | use EventEmitterTrait; 17 | } 18 | -------------------------------------------------------------------------------- /classes/Evenement/EventEmitterInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Evenement; 13 | 14 | interface EventEmitterInterface 15 | { 16 | public function on($event, callable $listener); 17 | public function once($event, callable $listener); 18 | public function removeListener($event, callable $listener); 19 | public function removeAllListeners($event = null); 20 | public function listeners($event); 21 | public function emit($event, array $arguments = []); 22 | } 23 | -------------------------------------------------------------------------------- /classes/Evenement/EventEmitterTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Evenement; 13 | 14 | trait EventEmitterTrait 15 | { 16 | protected $listeners = []; 17 | 18 | public function on($event, callable $listener) 19 | { 20 | if (!isset($this->listeners[$event])) { 21 | $this->listeners[$event] = []; 22 | } 23 | 24 | $this->listeners[$event][] = $listener; 25 | } 26 | 27 | public function once($event, callable $listener) 28 | { 29 | $onceListener = function () use (&$onceListener, $event, $listener) { 30 | $this->removeListener($event, $onceListener); 31 | 32 | call_user_func_array($listener, func_get_args()); 33 | }; 34 | 35 | $this->on($event, $onceListener); 36 | } 37 | 38 | public function removeListener($event, callable $listener) 39 | { 40 | if (isset($this->listeners[$event])) { 41 | if (false !== $index = array_search($listener, $this->listeners[$event], true)) { 42 | unset($this->listeners[$event][$index]); 43 | } 44 | } 45 | } 46 | 47 | public function removeAllListeners($event = null) 48 | { 49 | if ($event !== null) { 50 | unset($this->listeners[$event]); 51 | } else { 52 | $this->listeners = []; 53 | } 54 | } 55 | 56 | public function listeners($event) 57 | { 58 | return isset($this->listeners[$event]) ? $this->listeners[$event] : []; 59 | } 60 | 61 | public function emit($event, array $arguments = []) 62 | { 63 | foreach ($this->listeners($event) as $listener) { 64 | call_user_func_array($listener, $arguments); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /classes/React/Cache/ArrayCache.php: -------------------------------------------------------------------------------- 1 | data[$key])) { 14 | return When::reject(); 15 | } 16 | 17 | return When::resolve($this->data[$key]); 18 | } 19 | 20 | public function set($key, $value) 21 | { 22 | $this->data[$key] = $value; 23 | } 24 | 25 | public function remove($key) 26 | { 27 | unset($this->data[$key]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /classes/React/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | get('foo') 15 | ->then('var_dump'); 16 | 17 | This example fetches the value of the key `foo` and passes it to the 18 | `var_dump` function. You can use any of the composition provided by 19 | [promises](https://github.com/reactphp/promise). 20 | 21 | If the key `foo` does not exist, the promise will be rejected. 22 | 23 | ### set 24 | 25 | $cache->set('foo', 'bar'); 26 | 27 | This example eventually sets the value of the key `foo` to `bar`. If it 28 | already exists, it is overridden. No guarantees are made as to when the cache 29 | value is set. If the cache implementation has to go over the network to store 30 | it, it may take a while. 31 | 32 | ### remove 33 | 34 | $cache->remove('foo'); 35 | 36 | This example eventually removes the key `foo` from the cache. As with `set`, 37 | this may not happen instantly. 38 | 39 | ## Common usage 40 | 41 | ### Fallback get 42 | 43 | A common use case of caches is to attempt fetching a cached value and as a 44 | fallback retrieve it from the original data source if not found. Here is an 45 | example of that: 46 | 47 | $cache 48 | ->get('foo') 49 | ->then(null, 'getFooFromDb') 50 | ->then('var_dump'); 51 | 52 | First an attempt is made to retrieve the value of `foo`. A promise rejection 53 | handler of the function `getFooFromDb` is registered. `getFooFromDb` is a 54 | function (can be any PHP callable) that will be called if the key does not 55 | exist in the cache. 56 | 57 | `getFooFromDb` can handle the missing key by returning a promise for the 58 | actual value from the database (or any other data source). As a result, this 59 | chain will correctly fall back, and provide the value in both cases. 60 | 61 | ### Fallback get and set 62 | 63 | To expand on the fallback get example, often you want to set the value on the 64 | cache after fetching it from the data source. 65 | 66 | $cache 67 | ->get('foo') 68 | ->then(null, array($this, 'getAndCacheFooFromDb')) 69 | ->then('var_dump'); 70 | 71 | public function getAndCacheFooFromDb() 72 | { 73 | return $this->db 74 | ->get('foo') 75 | ->then(array($this, 'cacheFooFromDb')); 76 | } 77 | 78 | public function cacheFooFromDb($foo) 79 | { 80 | $this->cache->set('foo', $foo); 81 | 82 | return $foo; 83 | } 84 | 85 | By using chaining you can easily conditionally cache the value if it is 86 | fetched from the database. 87 | -------------------------------------------------------------------------------- /classes/React/Cache/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/cache", 3 | "description": "Async caching.", 4 | "keywords": ["cache"], 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=5.3.2", 8 | "react/promise": "1.0.*" 9 | }, 10 | "autoload": { 11 | "psr-0": { "React\\Cache": "" } 12 | }, 13 | "target-dir": "React/Cache", 14 | "extra": { 15 | "branch-alias": { 16 | "dev-master": "0.2-dev" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /classes/React/Dns/BadServerException.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 17 | } 18 | 19 | public function create($filename) 20 | { 21 | return $this 22 | ->loadEtcResolvConf($filename) 23 | ->then(array($this, 'parseEtcResolvConf')); 24 | } 25 | 26 | public function parseEtcResolvConf($contents) 27 | { 28 | $nameservers = array(); 29 | 30 | $contents = preg_replace('/^#/', '', $contents); 31 | $lines = preg_split('/\r?\n/is', $contents); 32 | foreach ($lines as $line) { 33 | if (preg_match('/^nameserver (.+)/', $line, $match)) { 34 | $nameservers[] = $match[1]; 35 | } 36 | } 37 | 38 | $config = new Config(); 39 | $config->nameservers = $nameservers; 40 | 41 | return When::resolve($config); 42 | } 43 | 44 | public function loadEtcResolvConf($filename) 45 | { 46 | if (!file_exists($filename)) { 47 | return When::reject(new \InvalidArgumentException("The filename for /etc/resolv.conf given does not exist: $filename")); 48 | } 49 | 50 | try { 51 | $deferred = new Deferred(); 52 | 53 | $fd = fopen($filename, 'r'); 54 | stream_set_blocking($fd, 0); 55 | 56 | $contents = ''; 57 | 58 | $stream = new Stream($fd, $this->loop); 59 | $stream->on('data', function ($data) use (&$contents) { 60 | $contents .= $data; 61 | }); 62 | $stream->on('end', function () use (&$contents, $deferred) { 63 | $deferred->resolve($contents); 64 | }); 65 | $stream->on('error', function ($error) use ($deferred) { 66 | $deferred->reject($error); 67 | }); 68 | 69 | return $deferred->promise(); 70 | } catch (\Exception $e) { 71 | return When::reject($e); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /classes/React/Dns/Model/HeaderBag.php: -------------------------------------------------------------------------------- 1 | 0, 11 | 'anCount' => 0, 12 | 'nsCount' => 0, 13 | 'arCount' => 0, 14 | 'qr' => 0, 15 | 'opcode' => Message::OPCODE_QUERY, 16 | 'aa' => 0, 17 | 'tc' => 0, 18 | 'rd' => 0, 19 | 'ra' => 0, 20 | 'z' => 0, 21 | 'rcode' => Message::RCODE_OK, 22 | ); 23 | 24 | public function get($name) 25 | { 26 | return isset($this->attributes[$name]) ? $this->attributes[$name] : null; 27 | } 28 | 29 | public function set($name, $value) 30 | { 31 | $this->attributes[$name] = $value; 32 | } 33 | 34 | public function isQuery() 35 | { 36 | return 0 === $this->attributes['qr']; 37 | } 38 | 39 | public function isResponse() 40 | { 41 | return 1 === $this->attributes['qr']; 42 | } 43 | 44 | public function isTruncated() 45 | { 46 | return 1 === $this->attributes['tc']; 47 | } 48 | 49 | public function populateCounts(Message $message) 50 | { 51 | $this->attributes['qdCount'] = count($message->questions); 52 | $this->attributes['anCount'] = count($message->answers); 53 | $this->attributes['nsCount'] = count($message->authority); 54 | $this->attributes['arCount'] = count($message->additional); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /classes/React/Dns/Model/Message.php: -------------------------------------------------------------------------------- 1 | header = new HeaderBag(); 41 | } 42 | 43 | public function prepare() 44 | { 45 | $this->header->populateCounts($this); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /classes/React/Dns/Model/Record.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->type = $type; 17 | $this->class = $class; 18 | $this->ttl = $ttl; 19 | $this->data = $data; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /classes/React/Dns/Protocol/BinaryDumper.php: -------------------------------------------------------------------------------- 1 | headerToBinary($message->header); 15 | $data .= $this->questionToBinary($message->questions); 16 | 17 | return $data; 18 | } 19 | 20 | private function headerToBinary(HeaderBag $header) 21 | { 22 | $data = ''; 23 | 24 | $data .= pack('n', $header->get('id')); 25 | 26 | $flags = 0x00; 27 | $flags = ($flags << 1) | $header->get('qr'); 28 | $flags = ($flags << 4) | $header->get('opcode'); 29 | $flags = ($flags << 1) | $header->get('aa'); 30 | $flags = ($flags << 1) | $header->get('tc'); 31 | $flags = ($flags << 1) | $header->get('rd'); 32 | $flags = ($flags << 1) | $header->get('ra'); 33 | $flags = ($flags << 3) | $header->get('z'); 34 | $flags = ($flags << 4) | $header->get('rcode'); 35 | 36 | $data .= pack('n', $flags); 37 | 38 | $data .= pack('n', $header->get('qdCount')); 39 | $data .= pack('n', $header->get('anCount')); 40 | $data .= pack('n', $header->get('nsCount')); 41 | $data .= pack('n', $header->get('arCount')); 42 | 43 | return $data; 44 | } 45 | 46 | private function questionToBinary(array $questions) 47 | { 48 | $data = ''; 49 | 50 | foreach ($questions as $question) { 51 | $labels = explode('.', $question['name']); 52 | foreach ($labels as $label) { 53 | $data .= chr(strlen($label)).$label; 54 | } 55 | $data .= "\x00"; 56 | 57 | $data .= pack('n*', $question['type'], $question['class']); 58 | } 59 | 60 | return $data; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /classes/React/Dns/Protocol/Parser.php: -------------------------------------------------------------------------------- 1 | data .= $data; 18 | 19 | if (!$message->header->get('id')) { 20 | if (!$this->parseHeader($message)) { 21 | return; 22 | } 23 | } 24 | 25 | if ($message->header->get('qdCount') != count($message->questions)) { 26 | if (!$this->parseQuestion($message)) { 27 | return; 28 | } 29 | } 30 | 31 | if ($message->header->get('anCount') != count($message->answers)) { 32 | if (!$this->parseAnswer($message)) { 33 | return; 34 | } 35 | } 36 | 37 | return $message; 38 | } 39 | 40 | public function parseHeader(Message $message) 41 | { 42 | if (strlen($message->data) < 12) { 43 | return; 44 | } 45 | 46 | $header = substr($message->data, 0, 12); 47 | $message->consumed += 12; 48 | 49 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); 50 | 51 | $rcode = $fields & bindec('1111'); 52 | $z = ($fields >> 4) & bindec('111'); 53 | $ra = ($fields >> 7) & 1; 54 | $rd = ($fields >> 8) & 1; 55 | $tc = ($fields >> 9) & 1; 56 | $aa = ($fields >> 10) & 1; 57 | $opcode = ($fields >> 11) & bindec('1111'); 58 | $qr = ($fields >> 15) & 1; 59 | 60 | $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', 61 | 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); 62 | 63 | 64 | foreach ($vars as $name => $value) { 65 | $message->header->set($name, $value); 66 | } 67 | 68 | return $message; 69 | } 70 | 71 | public function parseQuestion(Message $message) 72 | { 73 | if (strlen($message->data) < 2) { 74 | return; 75 | } 76 | 77 | $consumed = $message->consumed; 78 | 79 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); 80 | 81 | if (null === $labels) { 82 | return; 83 | } 84 | 85 | if (strlen($message->data) - $consumed < 4) { 86 | return; 87 | } 88 | 89 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); 90 | $consumed += 4; 91 | 92 | $message->consumed = $consumed; 93 | 94 | $message->questions[] = array( 95 | 'name' => implode('.', $labels), 96 | 'type' => $type, 97 | 'class' => $class, 98 | ); 99 | 100 | if ($message->header->get('qdCount') != count($message->questions)) { 101 | return $this->parseQuestion($message); 102 | } 103 | 104 | return $message; 105 | } 106 | 107 | public function parseAnswer(Message $message) 108 | { 109 | if (strlen($message->data) < 2) { 110 | return; 111 | } 112 | 113 | $consumed = $message->consumed; 114 | 115 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); 116 | 117 | if (null === $labels) { 118 | return; 119 | } 120 | 121 | if (strlen($message->data) - $consumed < 10) { 122 | return; 123 | } 124 | 125 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); 126 | $consumed += 4; 127 | 128 | list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4))); 129 | $consumed += 4; 130 | 131 | list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); 132 | $consumed += 2; 133 | 134 | $rdata = null; 135 | 136 | if (Message::TYPE_A === $type) { 137 | $ip = substr($message->data, $consumed, $rdLength); 138 | $consumed += $rdLength; 139 | 140 | $rdata = inet_ntop($ip); 141 | } 142 | 143 | if (Message::TYPE_CNAME === $type) { 144 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); 145 | 146 | $rdata = implode('.', $bodyLabels); 147 | } 148 | 149 | $message->consumed = $consumed; 150 | 151 | $name = implode('.', $labels); 152 | $ttl = $this->signedLongToUnsignedLong($ttl); 153 | $record = new Record($name, $type, $class, $ttl, $rdata); 154 | 155 | $message->answers[] = $record; 156 | 157 | if ($message->header->get('anCount') != count($message->answers)) { 158 | return $this->parseAnswer($message); 159 | } 160 | 161 | return $message; 162 | } 163 | 164 | private function readLabels($data, $consumed) 165 | { 166 | $labels = array(); 167 | 168 | while (true) { 169 | if ($this->isEndOfLabels($data, $consumed)) { 170 | $consumed += 1; 171 | break; 172 | } 173 | 174 | if ($this->isCompressedLabel($data, $consumed)) { 175 | list($newLabels, $consumed) = $this->getCompressedLabel($data, $consumed); 176 | $labels = array_merge($labels, $newLabels); 177 | break; 178 | } 179 | 180 | $length = ord(substr($data, $consumed, 1)); 181 | $consumed += 1; 182 | 183 | if (strlen($data) - $consumed < $length) { 184 | return array(null, null); 185 | } 186 | 187 | $labels[] = substr($data, $consumed, $length); 188 | $consumed += $length; 189 | } 190 | 191 | return array($labels, $consumed); 192 | } 193 | 194 | public function isEndOfLabels($data, $consumed) 195 | { 196 | $length = ord(substr($data, $consumed, 1)); 197 | return 0 === $length; 198 | } 199 | 200 | public function getCompressedLabel($data, $consumed) 201 | { 202 | list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); 203 | list($labels) = $this->readLabels($data, $nameOffset); 204 | 205 | return array($labels, $consumed); 206 | } 207 | 208 | public function isCompressedLabel($data, $consumed) 209 | { 210 | $mask = 0xc000; // 1100000000000000 211 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); 212 | 213 | return (bool) ($peek & $mask); 214 | } 215 | 216 | public function getCompressedLabelOffset($data, $consumed) 217 | { 218 | $mask = 0x3fff; // 0011111111111111 219 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); 220 | 221 | return array($peek & $mask, $consumed + 2); 222 | } 223 | 224 | public function signedLongToUnsignedLong($i) 225 | { 226 | return $i & 0x80000000 ? $i - 0xffffffff : $i; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/CachedExecutor.php: -------------------------------------------------------------------------------- 1 | executor = $executor; 17 | $this->cache = $cache; 18 | } 19 | 20 | public function query($nameserver, Query $query) 21 | { 22 | $that = $this; 23 | $executor = $this->executor; 24 | $cache = $this->cache; 25 | 26 | return $this->cache 27 | ->lookup($query) 28 | ->then( 29 | function ($cachedRecords) use ($that, $query) { 30 | return $that->buildResponse($query, $cachedRecords); 31 | }, 32 | function () use ($executor, $cache, $nameserver, $query) { 33 | return $executor 34 | ->query($nameserver, $query) 35 | ->then(function ($response) use ($cache, $query) { 36 | $cache->storeResponseMessage($query->currentTime, $response); 37 | return $response; 38 | }); 39 | } 40 | ); 41 | } 42 | 43 | public function buildResponse(Query $query, array $cachedRecords) 44 | { 45 | $response = new Message(); 46 | 47 | $response->header->set('id', $this->generateId()); 48 | $response->header->set('qr', 1); 49 | $response->header->set('opcode', Message::OPCODE_QUERY); 50 | $response->header->set('rd', 1); 51 | $response->header->set('rcode', Message::RCODE_OK); 52 | 53 | $response->questions[] = new Record($query->name, $query->type, $query->class); 54 | 55 | foreach ($cachedRecords as $record) { 56 | $response->answers[] = $record; 57 | } 58 | 59 | $response->prepare(); 60 | 61 | return $response; 62 | } 63 | 64 | protected function generateId() 65 | { 66 | return mt_rand(0, 0xffff); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/Executor.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 23 | $this->parser = $parser; 24 | $this->dumper = $dumper; 25 | $this->timeout = $timeout; 26 | } 27 | 28 | public function query($nameserver, Query $query) 29 | { 30 | $request = $this->prepareRequest($query); 31 | 32 | $queryData = $this->dumper->toBinary($request); 33 | $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; 34 | 35 | return $this->doQuery($nameserver, $transport, $queryData, $query->name); 36 | } 37 | 38 | public function prepareRequest(Query $query) 39 | { 40 | $request = new Message(); 41 | $request->header->set('id', $this->generateId()); 42 | $request->header->set('rd', 1); 43 | $request->questions[] = (array) $query; 44 | $request->prepare(); 45 | 46 | return $request; 47 | } 48 | 49 | public function doQuery($nameserver, $transport, $queryData, $name) 50 | { 51 | $that = $this; 52 | $parser = $this->parser; 53 | $loop = $this->loop; 54 | 55 | $response = new Message(); 56 | $deferred = new Deferred(); 57 | 58 | $retryWithTcp = function () use ($that, $nameserver, $queryData, $name) { 59 | return $that->doQuery($nameserver, 'tcp', $queryData, $name); 60 | }; 61 | 62 | $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { 63 | $conn->close(); 64 | $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); 65 | }); 66 | 67 | $conn = $this->createConnection($nameserver, $transport); 68 | $conn->on('data', function ($data) use ($retryWithTcp, $conn, $parser, $response, $transport, $deferred, $loop, $timer) { 69 | $responseReady = $parser->parseChunk($data, $response); 70 | 71 | if (!$responseReady) { 72 | return; 73 | } 74 | 75 | $loop->cancelTimer($timer); 76 | 77 | if ($response->header->isTruncated()) { 78 | if ('tcp' === $transport) { 79 | $deferred->reject(new BadServerException('The server set the truncated bit although we issued a TCP request')); 80 | } else { 81 | $conn->end(); 82 | $deferred->resolve($retryWithTcp()); 83 | } 84 | 85 | return; 86 | } 87 | 88 | $conn->end(); 89 | $deferred->resolve($response); 90 | }); 91 | $conn->write($queryData); 92 | 93 | return $deferred->promise(); 94 | } 95 | 96 | protected function generateId() 97 | { 98 | return mt_rand(0, 0xffff); 99 | } 100 | 101 | protected function createConnection($nameserver, $transport) 102 | { 103 | $fd = stream_socket_client("$transport://$nameserver"); 104 | $conn = new Connection($fd, $this->loop); 105 | 106 | return $conn; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/ExecutorInterface.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->type = $type; 16 | $this->class = $class; 17 | $this->currentTime = $currentTime; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/RecordBag.php: -------------------------------------------------------------------------------- 1 | records[$record->data] = array($currentTime + $record->ttl, $record); 15 | } 16 | 17 | public function all() 18 | { 19 | return array_values(array_map( 20 | function ($value) { 21 | list($expiresAt, $record) = $value; 22 | return $record; 23 | }, 24 | $this->records 25 | )); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/RecordCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 18 | } 19 | 20 | public function lookup(Query $query) 21 | { 22 | $id = $this->serializeQueryToIdentity($query); 23 | 24 | $expiredAt = $this->expiredAt; 25 | 26 | return $this->cache 27 | ->get($id) 28 | ->then(function ($value) use ($query, $expiredAt) { 29 | $recordBag = unserialize($value); 30 | 31 | if (null !== $expiredAt && $expiredAt <= $query->currentTime) { 32 | return When::reject(); 33 | } 34 | 35 | return $recordBag->all(); 36 | }); 37 | } 38 | 39 | public function storeResponseMessage($currentTime, Message $message) 40 | { 41 | foreach ($message->answers as $record) { 42 | $this->storeRecord($currentTime, $record); 43 | } 44 | } 45 | 46 | public function storeRecord($currentTime, Record $record) 47 | { 48 | $id = $this->serializeRecordToIdentity($record); 49 | 50 | $cache = $this->cache; 51 | 52 | $this->cache 53 | ->get($id) 54 | ->then( 55 | function ($value) { 56 | return unserialize($value); 57 | }, 58 | function ($e) { 59 | return new RecordBag(); 60 | } 61 | ) 62 | ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) { 63 | $recordBag->set($currentTime, $record); 64 | $cache->set($id, serialize($recordBag)); 65 | }); 66 | } 67 | 68 | public function expire($currentTime) 69 | { 70 | $this->expiredAt = $currentTime; 71 | } 72 | 73 | public function serializeQueryToIdentity(Query $query) 74 | { 75 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); 76 | } 77 | 78 | public function serializeRecordToIdentity(Record $record) 79 | { 80 | return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/RetryExecutor.php: -------------------------------------------------------------------------------- 1 | executor = $executor; 15 | $this->retries = $retries; 16 | } 17 | 18 | public function query($nameserver, Query $query) 19 | { 20 | $deferred = new Deferred(); 21 | 22 | $this->tryQuery($nameserver, $query, $this->retries, $deferred->resolver()); 23 | 24 | return $deferred->promise(); 25 | } 26 | 27 | public function tryQuery($nameserver, Query $query, $retries, $resolver) 28 | { 29 | $that = $this; 30 | $errorback = function ($error) use ($nameserver, $query, $retries, $resolver, $that) { 31 | if (!$error instanceof TimeoutException) { 32 | $resolver->reject($error); 33 | return; 34 | } 35 | if (0 >= $retries) { 36 | $error = new \RuntimeException( 37 | sprintf("DNS query for %s failed: too many retries", $query->name), 38 | 0, 39 | $error 40 | ); 41 | $resolver->reject($error); 42 | return; 43 | } 44 | $that->tryQuery($nameserver, $query, $retries-1, $resolver); 45 | }; 46 | 47 | $this->executor 48 | ->query($nameserver, $query) 49 | ->then(array($resolver, 'resolve'), $errorback); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /classes/React/Dns/Query/TimeoutException.php: -------------------------------------------------------------------------------- 1 | create('8.8.8.8', $loop); 18 | 19 | $dns->resolve('igor.io')->then(function ($ip) { 20 | echo "Host: $ip\n"; 21 | }); 22 | 23 | But there's more. 24 | 25 | ## Caching 26 | 27 | You can cache results by configuring the resolver to use a `CachedExecutor`: 28 | 29 | $loop = React\EventLoop\Factory::create(); 30 | $factory = new React\Dns\Resolver\Factory(); 31 | $dns = $factory->createCached('8.8.8.8', $loop); 32 | 33 | $dns->resolve('igor.io')->then(function ($ip) { 34 | echo "Host: $ip\n"; 35 | }); 36 | 37 | ... 38 | 39 | $dns->resolve('igor.io')->then(function ($ip) { 40 | echo "Host: $ip\n"; 41 | }); 42 | 43 | If the first call returns before the second, only one query will be executed. 44 | The second result will be served from cache. 45 | 46 | ## Todo 47 | 48 | * Implement message body parsing for types other than A and CNAME: NS, SOA, PTR, MX, TXT, AAAA 49 | * Implement `authority` and `additional` message parts 50 | * Respect /etc/hosts 51 | 52 | # References 53 | 54 | * [RFC1034](http://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities 55 | * [RFC1035](http://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification 56 | -------------------------------------------------------------------------------- /classes/React/Dns/RecordNotFoundException.php: -------------------------------------------------------------------------------- 1 | addPortToServerIfMissing($nameserver); 19 | $executor = $this->createRetryExecutor($loop); 20 | 21 | return new Resolver($nameserver, $executor); 22 | } 23 | 24 | public function createCached($nameserver, LoopInterface $loop) 25 | { 26 | $nameserver = $this->addPortToServerIfMissing($nameserver); 27 | $executor = $this->createCachedExecutor($loop); 28 | 29 | return new Resolver($nameserver, $executor); 30 | } 31 | 32 | protected function createExecutor(LoopInterface $loop) 33 | { 34 | return new Executor($loop, new Parser(), new BinaryDumper()); 35 | } 36 | 37 | protected function createRetryExecutor(LoopInterface $loop) 38 | { 39 | return new RetryExecutor($this->createExecutor($loop)); 40 | } 41 | 42 | protected function createCachedExecutor(LoopInterface $loop) 43 | { 44 | return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache(new ArrayCache())); 45 | } 46 | 47 | protected function addPortToServerIfMissing($nameserver) 48 | { 49 | return false === strpos($nameserver, ':') ? "$nameserver:53" : $nameserver; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /classes/React/Dns/Resolver/Resolver.php: -------------------------------------------------------------------------------- 1 | nameserver = $nameserver; 18 | $this->executor = $executor; 19 | } 20 | 21 | public function resolve($domain) 22 | { 23 | $that = $this; 24 | 25 | $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time()); 26 | 27 | return $this->executor 28 | ->query($this->nameserver, $query) 29 | ->then(function (Message $response) use ($that) { 30 | return $that->extractAddress($response, Message::TYPE_A); 31 | }); 32 | } 33 | 34 | public function extractAddress(Message $response, $type) 35 | { 36 | $answer = $this->pickRandomAnswerOfType($response, $type); 37 | $address = $answer->data; 38 | return $address; 39 | } 40 | 41 | public function pickRandomAnswerOfType(Message $response, $type) 42 | { 43 | // TODO: filter by name to make sure domain matches 44 | // TODO: resolve CNAME aliases 45 | 46 | $filteredAnswers = array_filter($response->answers, function ($answer) use ($type) { 47 | return $type === $answer->type; 48 | }); 49 | 50 | if (0 === count($filteredAnswers)) { 51 | $message = sprintf('DNS Request did not return valid answer. Received answers: %s', json_encode($response->answers)); 52 | throw new RecordNotFoundException($message); 53 | } 54 | 55 | $answer = $filteredAnswers[array_rand($filteredAnswers)]; 56 | 57 | return $answer; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /classes/React/Dns/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/dns", 3 | "description": "Async DNS resolver.", 4 | "keywords": ["dns", "dns-resolver"], 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=5.3.2", 8 | "react/cache": "0.2.*", 9 | "react/socket": "0.2.*", 10 | "react/promise": "1.0.*" 11 | }, 12 | "autoload": { 13 | "psr-0": { "React\\Dns": "" } 14 | }, 15 | "target-dir": "React/Dns", 16 | "extra": { 17 | "branch-alias": { 18 | "dev-master": "0.2-dev" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /classes/React/EventLoop/Factory.php: -------------------------------------------------------------------------------- 1 | loop = new \libev\EventLoop(); 19 | } 20 | 21 | public function addReadStream($stream, $listener) 22 | { 23 | $this->addStream($stream, $listener, \libev\IOEvent::READ); 24 | } 25 | 26 | public function addWriteStream($stream, $listener) 27 | { 28 | $this->addStream($stream, $listener, \libev\IOEvent::WRITE); 29 | } 30 | 31 | public function removeReadStream($stream) 32 | { 33 | $this->readEvents[(int)$stream]->stop(); 34 | unset($this->readEvents[(int)$stream]); 35 | } 36 | 37 | public function removeWriteStream($stream) 38 | { 39 | $this->writeEvents[(int)$stream]->stop(); 40 | unset($this->writeEvents[(int)$stream]); 41 | } 42 | 43 | public function removeStream($stream) 44 | { 45 | if (isset($this->readEvents[(int)$stream])) { 46 | $this->removeReadStream($stream); 47 | } 48 | 49 | if (isset($this->writeEvents[(int)$stream])) { 50 | $this->removeWriteStream($stream); 51 | } 52 | } 53 | 54 | private function addStream($stream, $listener, $flags) 55 | { 56 | $listener = $this->wrapStreamListener($stream, $listener, $flags); 57 | $event = new \libev\IOEvent($listener, $stream, $flags); 58 | $this->loop->add($event); 59 | 60 | if (($flags & \libev\IOEvent::READ) === $flags) { 61 | $this->readEvents[(int)$stream] = $event; 62 | } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { 63 | $this->writeEvents[(int)$stream] = $event; 64 | } 65 | } 66 | 67 | private function wrapStreamListener($stream, $listener, $flags) 68 | { 69 | if (($flags & \libev\IOEvent::READ) === $flags) { 70 | $removeCallback = array($this, 'removeReadStream'); 71 | } elseif (($flags & \libev\IOEvent::WRITE) === $flags) { 72 | $removeCallback = array($this, 'removeWriteStream'); 73 | } 74 | 75 | return function ($event) use ($stream, $listener, $removeCallback) { 76 | if (feof($stream)) { 77 | call_user_func($removeCallback, $stream); 78 | 79 | return; 80 | } 81 | 82 | call_user_func($listener, $stream); 83 | }; 84 | } 85 | 86 | public function addTimer($interval, $callback) 87 | { 88 | $dummyCallback = function () {}; 89 | $timer = new \libev\TimerEvent($dummyCallback, $interval); 90 | 91 | return $this->createTimer($timer, $callback, false); 92 | } 93 | 94 | public function addPeriodicTimer($interval, $callback) 95 | { 96 | $dummyCallback = function () {}; 97 | $timer = new \libev\TimerEvent($dummyCallback, $interval, $interval); 98 | 99 | return $this->createTimer($timer, $callback, true); 100 | } 101 | 102 | public function cancelTimer($signature) 103 | { 104 | $this->loop->remove($this->timers[$signature]); 105 | unset($this->timers[$signature]); 106 | } 107 | 108 | private function createTimer($timer, $callback, $periodic) 109 | { 110 | $signature = spl_object_hash($timer); 111 | $callback = $this->wrapTimerCallback($signature, $callback, $periodic); 112 | $timer->setCallback($callback); 113 | 114 | $this->timers[$signature] = $timer; 115 | $this->loop->add($timer); 116 | 117 | return $signature; 118 | } 119 | 120 | private function wrapTimerCallback($signature, $callback, $periodic) 121 | { 122 | $loop = $this; 123 | 124 | return function ($event) use ($signature, $callback, $periodic, $loop) { 125 | call_user_func($callback, $signature, $loop); 126 | 127 | if (!$periodic) { 128 | $loop->cancelTimer($signature); 129 | } 130 | }; 131 | } 132 | 133 | public function tick() 134 | { 135 | $this->loop->run(\libev\EventLoop::RUN_ONCE); 136 | } 137 | 138 | public function run() 139 | { 140 | $this->loop->run(); 141 | } 142 | 143 | public function stop() 144 | { 145 | $this->loop->breakLoop(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /classes/React/EventLoop/LibEventLoop.php: -------------------------------------------------------------------------------- 1 | base = event_base_new(); 23 | $this->callback = $this->createLibeventCallback(); 24 | $this->timersGc = new \SplQueue(); 25 | $this->timersGc->setIteratorMode(\SplQueue::IT_MODE_DELETE); 26 | } 27 | 28 | protected function createLibeventCallback() 29 | { 30 | $readCallbacks = &$this->readCallbacks; 31 | $writeCallbacks = &$this->writeCallbacks; 32 | 33 | return function ($stream, $flags, $loop) use (&$readCallbacks, &$writeCallbacks) { 34 | $id = (int) $stream; 35 | 36 | try { 37 | if (($flags & EV_READ) === EV_READ && isset($readCallbacks[$id])) { 38 | if (call_user_func($readCallbacks[$id], $stream, $loop) === false) { 39 | $loop->removeReadStream($stream); 40 | } 41 | } 42 | 43 | if (($flags & EV_WRITE) === EV_WRITE && isset($writeCallbacks[$id])) { 44 | if (call_user_func($writeCallbacks[$id], $stream, $loop) === false) { 45 | $loop->removeWriteStream($stream); 46 | } 47 | } 48 | } catch (\Exception $ex) { 49 | // If one of the callbacks throws an exception we must stop the loop 50 | // otherwise libevent will swallow the exception and go berserk. 51 | $loop->stop(); 52 | 53 | throw $ex; 54 | } 55 | }; 56 | } 57 | 58 | public function addReadStream($stream, $listener) 59 | { 60 | $this->addStreamEvent($stream, EV_READ, 'read', $listener); 61 | } 62 | 63 | public function addWriteStream($stream, $listener) 64 | { 65 | $this->addStreamEvent($stream, EV_WRITE, 'write', $listener); 66 | } 67 | 68 | protected function addStreamEvent($stream, $eventClass, $type, $listener) 69 | { 70 | $id = (int) $stream; 71 | 72 | if ($existing = isset($this->events[$id])) { 73 | if (($this->flags[$id] & $eventClass) === $eventClass) { 74 | return; 75 | } 76 | $event = $this->events[$id]; 77 | event_del($event); 78 | } else { 79 | $event = event_new(); 80 | } 81 | 82 | $flags = isset($this->flags[$id]) ? $this->flags[$id] | $eventClass : $eventClass; 83 | event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); 84 | 85 | if (!$existing) { 86 | // Set the base only if $event has been newly created or be ready for segfaults. 87 | event_base_set($event, $this->base); 88 | } 89 | 90 | event_add($event); 91 | 92 | $this->events[$id] = $event; 93 | $this->flags[$id] = $flags; 94 | $this->{"{$type}Callbacks"}[$id] = $listener; 95 | } 96 | 97 | public function removeReadStream($stream) 98 | { 99 | $this->removeStreamEvent($stream, EV_READ, 'read'); 100 | } 101 | 102 | public function removeWriteStream($stream) 103 | { 104 | $this->removeStreamEvent($stream, EV_WRITE, 'write'); 105 | } 106 | 107 | protected function removeStreamEvent($stream, $eventClass, $type) 108 | { 109 | $id = (int) $stream; 110 | 111 | if (isset($this->events[$id])) { 112 | $flags = $this->flags[$id] & ~$eventClass; 113 | 114 | if ($flags === 0) { 115 | // Remove if stream is not subscribed to any event at this point. 116 | return $this->removeStream($stream); 117 | } 118 | 119 | $event = $this->events[$id]; 120 | 121 | event_del($event); 122 | event_free($event); 123 | unset($this->{"{$type}Callbacks"}[$id]); 124 | 125 | $event = event_new(); 126 | event_set($event, $stream, $flags | EV_PERSIST, $this->callback, $this); 127 | event_base_set($event, $this->base); 128 | event_add($event); 129 | 130 | $this->events[$id] = $event; 131 | $this->flags[$id] = $flags; 132 | } 133 | } 134 | 135 | public function removeStream($stream) 136 | { 137 | $id = (int) $stream; 138 | 139 | if (isset($this->events[$id])) { 140 | $event = $this->events[$id]; 141 | 142 | unset( 143 | $this->events[$id], 144 | $this->flags[$id], 145 | $this->readCallbacks[$id], 146 | $this->writeCallbacks[$id] 147 | ); 148 | 149 | event_del($event); 150 | event_free($event); 151 | } 152 | } 153 | 154 | protected function addTimerInternal($interval, $callback, $periodic = false) 155 | { 156 | if ($interval < self::MIN_TIMER_RESOLUTION) { 157 | throw new \InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); 158 | } 159 | 160 | if (!is_callable($callback)) { 161 | throw new \InvalidArgumentException('The callback must be a callable object.'); 162 | } 163 | 164 | $timer = (object) array( 165 | 'loop' => $this, 166 | 'resource' => $resource = event_new(), 167 | 'callback' => $callback, 168 | 'interval' => $interval * 1000000, 169 | 'periodic' => $periodic, 170 | 'cancelled' => false, 171 | ); 172 | 173 | $timer->signature = spl_object_hash($timer); 174 | $timersGc = $this->timersGc; 175 | $callback = function () use ($timer, $timersGc) { 176 | foreach ($timersGc as $resource) { 177 | event_free($resource); 178 | } 179 | 180 | if ($timer->cancelled === false) { 181 | call_user_func($timer->callback, $timer->signature, $timer->loop); 182 | 183 | if ($timer->periodic === true) { 184 | event_add($timer->resource, $timer->interval); 185 | } else { 186 | $this->cancelTimer($timer->signature); 187 | } 188 | } 189 | }; 190 | 191 | event_timer_set($resource, $callback); 192 | event_base_set($resource, $this->base); 193 | event_add($resource, $interval * 1000000); 194 | 195 | $this->timers[$timer->signature] = $timer; 196 | 197 | return $timer->signature; 198 | } 199 | 200 | public function addTimer($interval, $callback) 201 | { 202 | return $this->addTimerInternal($interval, $callback); 203 | } 204 | 205 | public function addPeriodicTimer($interval, $callback) 206 | { 207 | return $this->addTimerInternal($interval, $callback, true); 208 | } 209 | 210 | public function cancelTimer($signature) 211 | { 212 | if (isset($this->timers[$signature])) { 213 | $timer = $this->timers[$signature]; 214 | 215 | $timer->cancelled = true; 216 | event_del($timer->resource); 217 | $this->timersGc->enqueue($timer->resource); 218 | unset($this->timers[$signature]); 219 | } 220 | } 221 | 222 | public function tick() 223 | { 224 | event_base_loop($this->base, EVLOOP_ONCE | EVLOOP_NONBLOCK); 225 | } 226 | 227 | public function run() 228 | { 229 | event_base_loop($this->base); 230 | } 231 | 232 | public function stop() 233 | { 234 | event_base_loopexit($this->base); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /classes/React/EventLoop/LoopInterface.php: -------------------------------------------------------------------------------- 1 | addReadStream($server, function ($server) use ($loop) { 38 | $conn = stream_socket_accept($server); 39 | $data = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nHi\n"; 40 | $loop->addWriteStream($conn, function ($conn) use (&$data, $loop) { 41 | $written = fwrite($conn, $data); 42 | if ($written === strlen($data)) { 43 | fclose($conn); 44 | $loop->removeStream($conn); 45 | } else { 46 | $data = substr($data, 0, $written); 47 | } 48 | }); 49 | }); 50 | 51 | $loop->addPeriodicTimer(5, function () { 52 | $memory = memory_get_usage() / 1024; 53 | $formatted = number_format($memory, 3).'K'; 54 | echo "Current memory usage: {$formatted}\n"; 55 | }); 56 | 57 | $loop->run(); 58 | 59 | **Note:** The factory is just for convenience. It tries to pick the best 60 | available implementation. Libraries `SHOULD` allow the user to inject an 61 | instance of the loop. They `MAY` use the factory when the user did not supply 62 | a loop. 63 | -------------------------------------------------------------------------------- /classes/React/EventLoop/StreamSelectLoop.php: -------------------------------------------------------------------------------- 1 | timers = new Timers($this); 21 | } 22 | 23 | public function addReadStream($stream, $listener) 24 | { 25 | $id = (int) $stream; 26 | 27 | if (!isset($this->readStreams[$id])) { 28 | $this->readStreams[$id] = $stream; 29 | $this->readListeners[$id] = $listener; 30 | } 31 | } 32 | 33 | public function addWriteStream($stream, $listener) 34 | { 35 | $id = (int) $stream; 36 | 37 | if (!isset($this->writeStreams[$id])) { 38 | $this->writeStreams[$id] = $stream; 39 | $this->writeListeners[$id] = $listener; 40 | } 41 | } 42 | 43 | public function removeReadStream($stream) 44 | { 45 | $id = (int) $stream; 46 | 47 | unset( 48 | $this->readStreams[$id], 49 | $this->readListeners[$id] 50 | ); 51 | } 52 | 53 | public function removeWriteStream($stream) 54 | { 55 | $id = (int) $stream; 56 | 57 | unset( 58 | $this->writeStreams[$id], 59 | $this->writeListeners[$id] 60 | ); 61 | } 62 | 63 | public function removeStream($stream) 64 | { 65 | $this->removeReadStream($stream); 66 | $this->removeWriteStream($stream); 67 | } 68 | 69 | public function addTimer($interval, $callback) 70 | { 71 | return $this->timers->add($interval, $callback); 72 | } 73 | 74 | public function addPeriodicTimer($interval, $callback) 75 | { 76 | return $this->timers->add($interval, $callback, true); 77 | } 78 | 79 | public function cancelTimer($signature) 80 | { 81 | $this->timers->cancel($signature); 82 | } 83 | 84 | protected function getNextEventTimeInMicroSeconds() 85 | { 86 | $nextEvent = $this->timers->getFirst(); 87 | 88 | if (null === $nextEvent) { 89 | return self::QUANTUM_INTERVAL; 90 | } 91 | 92 | $currentTime = microtime(true); 93 | if ($nextEvent > $currentTime) { 94 | return ($nextEvent - $currentTime) * 1000000; 95 | } 96 | 97 | return 0; 98 | } 99 | 100 | protected function sleepOnPendingTimers() 101 | { 102 | if ($this->timers->isEmpty()) { 103 | $this->running = false; 104 | } else { 105 | // We use usleep() instead of stream_select() to emulate timeouts 106 | // since the latter fails when there are no streams registered for 107 | // read / write events. Blame PHP for us needing this hack. 108 | usleep($this->getNextEventTimeInMicroSeconds()); 109 | } 110 | } 111 | 112 | protected function runStreamSelect() 113 | { 114 | $read = $this->readStreams ?: null; 115 | $write = $this->writeStreams ?: null; 116 | $except = null; 117 | 118 | if (!$read && !$write) { 119 | $this->sleepOnPendingTimers(); 120 | 121 | return; 122 | } 123 | 124 | if (stream_select($read, $write, $except, 0, $this->getNextEventTimeInMicroSeconds()) > 0) { 125 | if ($read) { 126 | foreach ($read as $stream) { 127 | $listener = $this->readListeners[(int) $stream]; 128 | if (call_user_func($listener, $stream, $this) === false) { 129 | $this->removeReadStream($stream); 130 | } 131 | } 132 | } 133 | 134 | if ($write) { 135 | foreach ($write as $stream) { 136 | if (!isset($this->writeListeners[(int) $stream])) { 137 | continue; 138 | } 139 | 140 | $listener = $this->writeListeners[(int) $stream]; 141 | if (call_user_func($listener, $stream, $this) === false) { 142 | $this->removeWriteStream($stream); 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | public function tick() 150 | { 151 | $this->timers->tick(); 152 | $this->runStreamSelect(); 153 | 154 | return $this->running; 155 | } 156 | 157 | public function run() 158 | { 159 | $this->running = true; 160 | 161 | while ($this->tick()) { 162 | // NOOP 163 | } 164 | } 165 | 166 | public function stop() 167 | { 168 | $this->running = false; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /classes/React/EventLoop/Timer/Timers.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 19 | $this->timers = new \SplPriorityQueue(); 20 | } 21 | 22 | public function updateTime() 23 | { 24 | return $this->time = microtime(true); 25 | } 26 | 27 | public function getTime() 28 | { 29 | return $this->time ?: $this->updateTime(); 30 | } 31 | 32 | public function add($interval, $callback, $periodic = false) 33 | { 34 | if ($interval < self::MIN_RESOLUTION) { 35 | throw new \InvalidArgumentException('Timer events do not support sub-millisecond timeouts.'); 36 | } 37 | 38 | if (!is_callable($callback)) { 39 | throw new \InvalidArgumentException('The callback must be a callable object.'); 40 | } 41 | 42 | $interval = (float) $interval; 43 | 44 | $timer = (object) array( 45 | 'interval' => $interval, 46 | 'callback' => $callback, 47 | 'periodic' => $periodic, 48 | 'scheduled' => $interval + $this->getTime(), 49 | ); 50 | 51 | $timer->signature = spl_object_hash($timer); 52 | $this->timers->insert($timer, -$timer->scheduled); 53 | $this->active[$timer->signature] = $timer; 54 | 55 | return $timer->signature; 56 | } 57 | 58 | public function cancel($signature) 59 | { 60 | unset($this->active[$signature]); 61 | } 62 | 63 | public function getFirst() 64 | { 65 | if ($this->timers->isEmpty()) { 66 | return null; 67 | } 68 | 69 | return $this->timers->top()->scheduled; 70 | } 71 | 72 | public function isEmpty() 73 | { 74 | return !$this->active; 75 | } 76 | 77 | public function tick() 78 | { 79 | $time = $this->updateTime(); 80 | $timers = $this->timers; 81 | 82 | while (!$timers->isEmpty() && $timers->top()->scheduled < $time) { 83 | $timer = $timers->extract(); 84 | 85 | if (isset($this->active[$timer->signature])) { 86 | call_user_func($timer->callback, $timer->signature, $this->loop); 87 | 88 | if ($timer->periodic === true) { 89 | $timer->scheduled = $timer->interval + $time; 90 | $timers->insert($timer, -$timer->scheduled); 91 | } else { 92 | unset($this->active[$timer->signature]); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /classes/React/EventLoop/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/event-loop", 3 | "description": "Event loop abstraction layer that libraries can use for evented I/O.", 4 | "keywords": ["event-loop"], 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=5.3.3" 8 | }, 9 | "suggest": { 10 | "ext-libevent": ">=0.0.5", 11 | "ext-libev": "*" 12 | }, 13 | "autoload": { 14 | "psr-0": { "React\\EventLoop": "" } 15 | }, 16 | "target-dir": "React/EventLoop", 17 | "extra": { 18 | "branch-alias": { 19 | "dev-master": "0.2-dev" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /classes/React/Http/README.md: -------------------------------------------------------------------------------- 1 | # Http Component 2 | 3 | Library for building an evented http server. 4 | 5 | This component builds on top of the `Socket` component to implement HTTP. Here 6 | are the main concepts: 7 | 8 | * **Server**: Attaches itself to an instance of 9 | `React\Socket\ServerInterface`, parses any incoming data as HTTP, emits a 10 | `request` event for each request. 11 | * **Request**: A `ReadableStream` which streams the request body and contains 12 | meta data which was parsed from the request header. 13 | * **Response** A `WritableStream` which streams the response body. You can set 14 | the status code and response headers via the `writeHead()` method. 15 | 16 | ## Usage 17 | 18 | This is an HTTP server which responds with `Hello World` to every request. 19 | 20 | $loop = React\EventLoop\Factory::create(); 21 | $socket = new React\Socket\Server($loop); 22 | 23 | $http = new React\Http\Server($socket); 24 | $http->on('request', function ($request, $response) { 25 | $response->writeHead(200, array('Content-Type' => 'text/plain')); 26 | $response->end("Hello World!\n"); 27 | }); 28 | 29 | $socket->listen(1337); 30 | $loop->run(); 31 | -------------------------------------------------------------------------------- /classes/React/Http/Request.php: -------------------------------------------------------------------------------- 1 | method = $method; 22 | $this->path = $path; 23 | $this->query = $query; 24 | $this->httpVersion = $httpVersion; 25 | $this->headers = $headers; 26 | } 27 | 28 | public function getMethod() 29 | { 30 | return $this->method; 31 | } 32 | 33 | public function getPath() 34 | { 35 | return $this->path; 36 | } 37 | 38 | public function getQuery() 39 | { 40 | return $this->query; 41 | } 42 | 43 | public function getHttpVersion() 44 | { 45 | return $this->httpVersion; 46 | } 47 | 48 | public function getHeaders() 49 | { 50 | return $this->headers; 51 | } 52 | 53 | public function expectsContinue() 54 | { 55 | return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect']; 56 | } 57 | 58 | public function isReadable() 59 | { 60 | return $this->readable; 61 | } 62 | 63 | public function pause() 64 | { 65 | $this->emit('pause'); 66 | } 67 | 68 | public function resume() 69 | { 70 | $this->emit('resume'); 71 | } 72 | 73 | public function close() 74 | { 75 | $this->readable = false; 76 | $this->emit('end'); 77 | $this->removeAllListeners(); 78 | } 79 | 80 | public function pipe(WritableStreamInterface $dest, array $options = array()) 81 | { 82 | Util::pipe($this, $dest, $options); 83 | 84 | return $dest; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /classes/React/Http/RequestHeaderParser.php: -------------------------------------------------------------------------------- 1 | buffer) + strlen($data) > $this->maxSize) { 20 | $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); 21 | 22 | return; 23 | } 24 | 25 | $this->buffer .= $data; 26 | 27 | if (false !== strpos($this->buffer, "\r\n\r\n")) { 28 | list($request, $bodyBuffer) = $this->parseRequest($this->buffer); 29 | 30 | $this->emit('headers', array($request, $bodyBuffer)); 31 | $this->removeAllListeners(); 32 | } 33 | } 34 | 35 | public function parseRequest($data) 36 | { 37 | list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); 38 | 39 | $parser = new MessageParser(); 40 | $parsed = $parser->parseRequest($headers."\r\n\r\n"); 41 | 42 | $parsedQuery = array(); 43 | if ($parsed['request_url']['query']) { 44 | parse_str($parsed['request_url']['query'], $parsedQuery); 45 | } 46 | 47 | $request = new Request( 48 | $parsed['method'], 49 | $parsed['request_url']['path'], 50 | $parsedQuery, 51 | $parsed['version'], 52 | $parsed['headers'] 53 | ); 54 | 55 | return array($request, $bodyBuffer); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /classes/React/Http/Response.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 21 | 22 | $that = $this; 23 | 24 | $this->conn->on('end', function () use ($that) { 25 | $that->close(); 26 | }); 27 | 28 | $this->conn->on('error', function ($error) use ($that) { 29 | $that->emit('error', array($error, $that)); 30 | $that->close(); 31 | }); 32 | 33 | $this->conn->on('drain', function () use ($that) { 34 | $that->emit('drain'); 35 | }); 36 | } 37 | 38 | public function isWritable() 39 | { 40 | return $this->writable; 41 | } 42 | 43 | public function writeContinue() 44 | { 45 | if ($this->headWritten) { 46 | throw new \Exception('Response head has already been written.'); 47 | } 48 | 49 | $this->conn->write("HTTP/1.1 100 Continue\r\n"); 50 | } 51 | 52 | public function writeHead($status = 200, array $headers = array()) 53 | { 54 | if ($this->headWritten) { 55 | throw new \Exception('Response head has already been written.'); 56 | } 57 | 58 | if (isset($headers['Content-Length'])) { 59 | $this->chunkedEncoding = false; 60 | } 61 | 62 | $response = new GuzzleResponse($status); 63 | $response->setHeader('X-Powered-By', 'React/alpha'); 64 | $response->addHeaders($headers); 65 | if ($this->chunkedEncoding) { 66 | $response->setHeader('Transfer-Encoding', 'chunked'); 67 | } 68 | $data = (string) $response; 69 | $this->conn->write($data); 70 | 71 | $this->headWritten = true; 72 | } 73 | 74 | public function write($data) 75 | { 76 | if (!$this->headWritten) { 77 | throw new \Exception('Response head has not yet been written.'); 78 | } 79 | 80 | if ($this->chunkedEncoding) { 81 | $len = strlen($data); 82 | $chunk = dechex($len)."\r\n".$data."\r\n"; 83 | $flushed = $this->conn->write($chunk); 84 | } else { 85 | $flushed = $this->conn->write($data); 86 | } 87 | 88 | return $flushed; 89 | } 90 | 91 | public function end($data = null) 92 | { 93 | if (null !== $data) { 94 | $this->write($data); 95 | } 96 | 97 | if ($this->chunkedEncoding) { 98 | $this->conn->write("0\r\n\r\n"); 99 | } 100 | 101 | $this->emit('end'); 102 | $this->removeAllListeners(); 103 | $this->conn->end(); 104 | } 105 | 106 | public function close() 107 | { 108 | if ($this->closed) { 109 | return; 110 | } 111 | 112 | $this->closed = true; 113 | 114 | $this->writable = false; 115 | $this->emit('close'); 116 | $this->removeAllListeners(); 117 | $this->conn->close(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /classes/React/Http/Server.php: -------------------------------------------------------------------------------- 1 | io = $io; 17 | 18 | $server = $this; 19 | 20 | $this->io->on('connection', function ($conn) use ($server) { 21 | // TODO: http 1.1 keep-alive 22 | // TODO: chunked transfer encoding (also for outgoing data) 23 | // TODO: multipart parsing 24 | 25 | $parser = new RequestHeaderParser(); 26 | $parser->on('headers', function (Request $request, $bodyBuffer) use ($server, $conn, $parser) { 27 | $server->handleRequest($conn, $request, $bodyBuffer); 28 | 29 | $conn->removeListener('data', array($parser, 'feed')); 30 | $conn->on('end', function () use ($request) { 31 | $request->emit('end'); 32 | }); 33 | $conn->on('data', function ($data) use ($request) { 34 | $request->emit('data', array($data)); 35 | }); 36 | $request->on('pause', function () use ($conn) { 37 | $conn->emit('pause'); 38 | }); 39 | $request->on('resume', function () use ($conn) { 40 | $conn->emit('resume'); 41 | }); 42 | }); 43 | 44 | $conn->on('data', array($parser, 'feed')); 45 | }); 46 | } 47 | 48 | public function handleRequest(ConnectionInterface $conn, Request $request, $bodyBuffer) 49 | { 50 | $response = new Response($conn); 51 | $response->on('close', array($request, 'close')); 52 | 53 | if (!$this->listeners('request')) { 54 | $response->end(); 55 | 56 | return; 57 | } 58 | 59 | $this->emit('request', array($request, $response)); 60 | $request->emit('data', array($bodyBuffer)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /classes/React/Http/ServerInterface.php: -------------------------------------------------------------------------------- 1 | =5.3.3", 8 | "guzzle/http": "3.0.*", 9 | "guzzle/parser": "3.0.*", 10 | "react/socket": "0.2.*" 11 | }, 12 | "autoload": { 13 | "psr-0": { "React\\Http": "" } 14 | }, 15 | "target-dir": "React/Http", 16 | "extra": { 17 | "branch-alias": { 18 | "dev-master": "0.2-dev" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /classes/React/HttpClient/Client.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 22 | $this->connectionManager = $connectionManager; 23 | $this->secureConnectionManager = $secureConnectionManager; 24 | } 25 | 26 | public function request($method, $url, array $headers = array()) 27 | { 28 | $guzzleRequest = new GuzzleRequest($method, $url, $headers); 29 | $connectionManager = $this->getConnectionManagerForScheme($guzzleRequest->getScheme()); 30 | return new ClientRequest($this->loop, $connectionManager, $guzzleRequest); 31 | } 32 | 33 | public function setConnectionManager(ConnectionManagerInterface $connectionManager) 34 | { 35 | $this->connectionManager = $connectionManager; 36 | } 37 | 38 | public function getConnectionManager() 39 | { 40 | return $this->connectionManager; 41 | } 42 | 43 | public function setSecureConnectionManager(ConnectionManagerInterface $connectionManager) 44 | { 45 | $this->secureConnectionManager = $connectionManager; 46 | } 47 | 48 | public function getSecureConnectionManager() 49 | { 50 | return $this->secureConnectionManager; 51 | } 52 | 53 | private function getConnectionManagerForScheme($scheme) 54 | { 55 | if ('https' === $scheme) { 56 | return $this->getSecureConnectionManager(); 57 | } else { 58 | return $this->getConnectionManager(); 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /classes/React/HttpClient/ConnectionManager.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 20 | $this->resolver = $resolver; 21 | } 22 | 23 | public function getConnection($host, $port) 24 | { 25 | $that = $this; 26 | 27 | return $this 28 | ->resolveHostname($host) 29 | ->then(function ($address) use ($port, $that) { 30 | return $that->getConnectionForAddress($address, $port); 31 | }); 32 | } 33 | 34 | public function getConnectionForAddress($address, $port) 35 | { 36 | $url = $this->getSocketUrl($address, $port); 37 | 38 | $socket = stream_socket_client($url, $errno, $errstr, ini_get("default_socket_timeout"), STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); 39 | 40 | if (!$socket) { 41 | return new RejectedPromise(new \RuntimeException( 42 | sprintf("connection to %s:%d failed: %s", $address, $port, $errstr), 43 | $errno 44 | )); 45 | } 46 | 47 | stream_set_blocking($socket, 0); 48 | 49 | // wait for connection 50 | 51 | return $this 52 | ->waitForStreamOnce($socket) 53 | ->then(array($this, 'handleConnectedSocket')); 54 | } 55 | 56 | protected function waitForStreamOnce($stream) 57 | { 58 | $deferred = new Deferred(); 59 | 60 | $loop = $this->loop; 61 | 62 | $this->loop->addWriteStream($stream, function ($stream) use ($loop, $deferred) { 63 | $loop->removeWriteStream($stream); 64 | $deferred->resolve($stream); 65 | }); 66 | 67 | return $deferred->promise(); 68 | } 69 | 70 | public function handleConnectedSocket($socket) 71 | { 72 | return new Stream($socket, $this->loop); 73 | } 74 | 75 | protected function getSocketUrl($host, $port) 76 | { 77 | return sprintf('tcp://%s:%s', $host, $port); 78 | } 79 | 80 | protected function resolveHostname($host) 81 | { 82 | if (false !== filter_var($host, FILTER_VALIDATE_IP)) { 83 | return new FulfilledPromise($host); 84 | } 85 | 86 | return $this->resolver->resolve($host); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /classes/React/HttpClient/ConnectionManagerInterface.php: -------------------------------------------------------------------------------- 1 | createCached('8.8.8.8', $loop); 38 | 39 | $factory = new React\HttpClient\Factory(); 40 | $client = $factory->create($loop, $dnsResolver); 41 | 42 | $request = $client->request('GET', 'https://github.com/'); 43 | $request->on('response', function ($response) { 44 | $response->on('data', function ($data) { 45 | // ... 46 | }); 47 | }); 48 | $request->end(); 49 | ``` 50 | 51 | ## TODO 52 | 53 | * gzip content encoding 54 | * chunked transfer encoding 55 | * keep-alive connections 56 | * following redirections 57 | -------------------------------------------------------------------------------- /classes/React/HttpClient/Request.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 39 | $this->connectionManager = $connectionManager; 40 | $this->request = $request; 41 | } 42 | 43 | public function isWritable() 44 | { 45 | return self::STATE_END > $this->state; 46 | } 47 | 48 | public function writeHead() 49 | { 50 | if (self::STATE_WRITING_HEAD <= $this->state) { 51 | throw new \LogicException('Headers already written'); 52 | } 53 | 54 | $this->state = self::STATE_WRITING_HEAD; 55 | 56 | $that = $this; 57 | $request = $this->request; 58 | $streamRef = &$this->stream; 59 | $stateRef = &$this->state; 60 | 61 | $this 62 | ->connect() 63 | ->then( 64 | function ($stream) use ($that, $request, &$streamRef, &$stateRef) { 65 | $streamRef = $stream; 66 | 67 | $stream->on('drain', array($that, 'handleDrain')); 68 | $stream->on('data', array($that, 'handleData')); 69 | $stream->on('end', array($that, 'handleEnd')); 70 | $stream->on('error', array($that, 'handleError')); 71 | 72 | $request->setProtocolVersion('1.0'); 73 | $headers = (string) $request; 74 | 75 | $stream->write($headers); 76 | 77 | $stateRef = Request::STATE_HEAD_WRITTEN; 78 | 79 | $that->emit('headers-written', array($that)); 80 | }, 81 | array($this, 'handleError') 82 | ); 83 | } 84 | 85 | public function write($data) 86 | { 87 | if (!$this->isWritable()) { 88 | return; 89 | } 90 | 91 | if (self::STATE_HEAD_WRITTEN <= $this->state) { 92 | return $this->stream->write($data); 93 | } 94 | 95 | $this->on('headers-written', function ($that) use ($data) { 96 | $that->write($data); 97 | }); 98 | 99 | if (self::STATE_WRITING_HEAD > $this->state) { 100 | $this->writeHead(); 101 | } 102 | 103 | return false; 104 | } 105 | 106 | public function end($data = null) 107 | { 108 | if (null !== $data && !is_scalar($data)) { 109 | throw new \InvalidArgumentException('$data must be null or scalar'); 110 | } 111 | 112 | if (null !== $data) { 113 | $this->write($data); 114 | } else if (self::STATE_WRITING_HEAD > $this->state) { 115 | $this->writeHead(); 116 | } 117 | } 118 | 119 | public function handleDrain() 120 | { 121 | $this->emit('drain', array($this)); 122 | } 123 | 124 | public function handleData($data) 125 | { 126 | $this->buffer .= $data; 127 | 128 | if (false !== strpos($this->buffer, "\r\n\r\n")) { 129 | list($response, $bodyChunk) = $this->parseResponse($this->buffer); 130 | 131 | $this->buffer = null; 132 | 133 | $this->stream->removeListener('drain', array($this, 'handleDrain')); 134 | $this->stream->removeListener('data', array($this, 'handleData')); 135 | $this->stream->removeListener('end', array($this, 'handleEnd')); 136 | $this->stream->removeListener('error', array($this, 'handleError')); 137 | 138 | $this->response = $response; 139 | $that = $this; 140 | 141 | $response->on('end', function () use ($that) { 142 | $that->close(); 143 | }); 144 | $response->on('error', function (\Exception $error) use ($that) { 145 | $that->closeError(new \RuntimeException( 146 | "An error occured in the response", 147 | 0, 148 | $error 149 | )); 150 | }); 151 | 152 | $this->emit('response', array($response, $this)); 153 | 154 | $response->emit('data', array($bodyChunk)); 155 | } 156 | } 157 | 158 | public function handleEnd() 159 | { 160 | $this->closeError(new \RuntimeException( 161 | "Connection closed before receiving response" 162 | )); 163 | } 164 | 165 | public function handleError($error) 166 | { 167 | $this->closeError(new \RuntimeException( 168 | "An error occurred in the underlying stream", 169 | 0, 170 | $error 171 | )); 172 | } 173 | 174 | public function closeError(\Exception $error) 175 | { 176 | if (self::STATE_END <= $this->state) { 177 | return; 178 | } 179 | $this->emit('error', array($error, $this)); 180 | $this->close($error); 181 | } 182 | 183 | public function close(\Exception $error = null) 184 | { 185 | if (self::STATE_END <= $this->state) { 186 | return; 187 | } 188 | 189 | $this->state = self::STATE_END; 190 | 191 | if ($this->stream) { 192 | $this->stream->close(); 193 | } 194 | 195 | $this->emit('end', array($error, $this->response, $this)); 196 | } 197 | 198 | protected function parseResponse($data) 199 | { 200 | $parser = new MessageParser(); 201 | $parsed = $parser->parseResponse($data); 202 | 203 | $factory = $this->getResponseFactory(); 204 | 205 | $response = $factory( 206 | $parsed['protocol'], 207 | $parsed['version'], 208 | $parsed['code'], 209 | $parsed['reason_phrase'], 210 | $parsed['headers'] 211 | ); 212 | 213 | return array($response, $parsed['body']); 214 | } 215 | 216 | protected function connect() 217 | { 218 | $host = $this->request->getHost(); 219 | $port = $this->request->getPort(); 220 | 221 | return $this->connectionManager 222 | ->getConnection($host, $port); 223 | } 224 | 225 | public function setResponseFactory($factory) 226 | { 227 | $this->responseFactory = $factory; 228 | } 229 | 230 | public function getResponseFactory() 231 | { 232 | if (null === $factory = $this->responseFactory) { 233 | $loop = $this->loop; 234 | $stream = $this->stream; 235 | 236 | $factory = function ($protocol, $version, $code, $reasonPhrase, $headers) use ($loop, $stream) { 237 | return new Response( 238 | $loop, 239 | $stream, 240 | $protocol, 241 | $version, 242 | $code, 243 | $reasonPhrase, 244 | $headers 245 | ); 246 | }; 247 | 248 | $this->responseFactory = $factory; 249 | } 250 | 251 | return $factory; 252 | } 253 | } 254 | 255 | -------------------------------------------------------------------------------- /classes/React/HttpClient/Response.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 27 | $this->stream = $stream; 28 | $this->protocol = $protocol; 29 | $this->version = $version; 30 | $this->code = $code; 31 | $this->reasonPhrase = $reasonPhrase; 32 | $this->headers = $headers; 33 | 34 | $stream->on('data', array($this, 'handleData')); 35 | $stream->on('error', array($this, 'handleError')); 36 | $stream->on('end', array($this, 'handleEnd')); 37 | } 38 | 39 | public function getProtocol() 40 | { 41 | return $this->protocol; 42 | } 43 | 44 | public function getVersion() 45 | { 46 | return $this->version; 47 | } 48 | 49 | public function getCode() 50 | { 51 | return $this->code; 52 | } 53 | 54 | public function getReasonPhrase() 55 | { 56 | return $this->reasonPhrase; 57 | } 58 | 59 | public function getHeaders() 60 | { 61 | return $this->headers; 62 | } 63 | 64 | public function getBody() 65 | { 66 | return $this->body; 67 | } 68 | 69 | public function handleData($data) 70 | { 71 | $this->emit('data', array($data, $this)); 72 | } 73 | 74 | public function handleEnd() 75 | { 76 | $this->close(); 77 | } 78 | 79 | public function handleError(\Exception $error) 80 | { 81 | $this->emit('error', array(new \RuntimeException( 82 | "An error occurred in the underlying stream", 83 | 0, 84 | $error 85 | ), $this)); 86 | 87 | $this->close($error); 88 | } 89 | 90 | public function close(\Exception $error = null) 91 | { 92 | if (!$this->readable) { 93 | return; 94 | } 95 | 96 | $this->readable = false; 97 | 98 | $this->emit('end', array($error, $this)); 99 | 100 | $this->removeAllListeners(); 101 | $this->stream->end(); 102 | } 103 | 104 | public function isReadable() 105 | { 106 | return $this->readable; 107 | } 108 | 109 | public function pause() 110 | { 111 | if (!$this->readable) { 112 | return; 113 | } 114 | 115 | $this->stream->pause(); 116 | } 117 | 118 | public function resume() 119 | { 120 | if (!$this->readable) { 121 | return; 122 | } 123 | 124 | $this->stream->resume(); 125 | } 126 | 127 | public function pipe(WritableStreamInterface $dest, array $options = array()) 128 | { 129 | Util::pipe($this, $dest, $options); 130 | 131 | return $dest; 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /classes/React/HttpClient/SecureConnectionManager.php: -------------------------------------------------------------------------------- 1 | enableCrypto($socket, $deferred); 20 | }; 21 | 22 | $this->loop->addWriteStream($socket, $enableCrypto); 23 | $this->loop->addReadStream($socket, $enableCrypto); 24 | $enableCrypto(); 25 | 26 | return $deferred->promise(); 27 | } 28 | 29 | public function enableCrypto($socket, ResolverInterface $resolver) 30 | { 31 | $result = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); 32 | 33 | if (true === $result) { 34 | $this->loop->removeWriteStream($socket); 35 | $this->loop->removeReadStream($socket); 36 | 37 | $resolver->resolve(new Stream($socket, $this->loop)); 38 | } else if (false === $result) { 39 | $this->loop->removeWriteStream($socket); 40 | $this->loop->removeReadStream($socket); 41 | 42 | $resolver->reject(); 43 | } else { 44 | // need more data, will retry 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /classes/React/HttpClient/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react/http-client", 3 | "description": "Asynchronous HTTP client library.", 4 | "keywords": ["http"], 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=5.3.3", 8 | "guzzle/http": "2.8.*", 9 | "guzzle/parser": "2.8.*", 10 | "react/socket": "0.2.*", 11 | "react/dns": "0.2.*" 12 | }, 13 | "autoload": { 14 | "psr-0": { "React\\HttpClient": "" } 15 | }, 16 | "target-dir": "React/HttpClient", 17 | "extra": { 18 | "branch-alias": { 19 | "dev-master": "0.2-dev" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /classes/React/Socket/Connection.php: -------------------------------------------------------------------------------- 1 | bufferSize); 17 | if ('' === $data || false === $data) { 18 | $this->end(); 19 | } else { 20 | $this->emit('data', array($data, $this)); 21 | } 22 | } 23 | 24 | public function handleClose() 25 | { 26 | if (is_resource($this->stream)) { 27 | stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR); 28 | fclose($this->stream); 29 | } 30 | } 31 | 32 | public function getClientAddress() { 33 | return stream_socket_get_name($this->stream, true); 34 | } 35 | 36 | public function getRemoteAddress() 37 | { 38 | return $this->parseAddress($this->getClientAddress()); 39 | } 40 | 41 | private function parseAddress($address) 42 | { 43 | return trim(substr($address, 0, strrpos($address, ':')), '[]'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /classes/React/Socket/ConnectionException.php: -------------------------------------------------------------------------------- 1 | on('connection', function ($conn) { 26 | $conn->write("Hello there!\n"); 27 | $conn->write("Welcome to this amazing server!\n"); 28 | $conn->write("Here's a tip: don't say anything.\n"); 29 | 30 | $conn->on('data', function ($data) use ($conn) { 31 | $conn->close(); 32 | }); 33 | }); 34 | $socket->listen(1337); 35 | 36 | $loop->run(); 37 | 38 | Here's a client that outputs the output of said server and then attempts to 39 | send it a string. 40 | 41 | $loop = React\EventLoop\Factory::create(); 42 | 43 | $client = stream_socket_client('tcp://127.0.0.1:1337'); 44 | $conn = new React\Socket\Connection($client, $loop); 45 | $conn->pipe(new React\Stream\Stream(STDOUT, $loop)); 46 | $conn->write("Hello World!\n"); 47 | 48 | $loop->run(); 49 | -------------------------------------------------------------------------------- /classes/React/Socket/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 17 | } 18 | 19 | public function listen($port, $host = '127.0.0.1') 20 | { 21 | $this->master = @stream_socket_server("tcp://$host:$port", $errno, $errstr); 22 | if (false === $this->master) { 23 | $message = "Could not bind to tcp://$host:$port: $errstr"; 24 | throw new ConnectionException($message, $errno); 25 | } 26 | stream_set_blocking($this->master, 0); 27 | 28 | $that = $this; 29 | 30 | $this->loop->addReadStream($this->master, function ($master) use ($that) { 31 | $newSocket = stream_socket_accept($master); 32 | if (false === $newSocket) { 33 | $that->emit('error', array(new \RuntimeException('Error accepting new connection'))); 34 | 35 | return; 36 | } 37 | $that->handleConnection($newSocket); 38 | }); 39 | } 40 | 41 | public function handleConnection($socket) 42 | { 43 | stream_set_blocking($socket, 0); 44 | 45 | $client = $this->createConnection($socket); 46 | 47 | $this->emit('connection', array($client)); 48 | } 49 | 50 | public function getPort() 51 | { 52 | $name = stream_socket_get_name($this->master, false); 53 | 54 | return (int) substr(strrchr($name, ':'), 1); 55 | } 56 | 57 | public function shutdown() 58 | { 59 | $this->loop->removeStream($this->master); 60 | fclose($this->master); 61 | } 62 | 63 | public function createConnection($socket) 64 | { 65 | return new Connection($socket, $this->loop); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /classes/React/Socket/ServerInterface.php: -------------------------------------------------------------------------------- 1 | =5.3.3", 8 | "evenement/evenement": "1.0.*", 9 | "react/event-loop": "0.2.*", 10 | "react/stream": "0.2.*" 11 | }, 12 | "autoload": { 13 | "psr-0": { "React\\Socket": "" } 14 | }, 15 | "target-dir": "React/Socket", 16 | "extra": { 17 | "branch-alias": { 18 | "dev-master": "0.2-dev" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /classes/React/Stream/Buffer.php: -------------------------------------------------------------------------------- 1 | 0, 20 | 'message' => '', 21 | 'file' => '', 22 | 'line' => 0, 23 | ); 24 | 25 | public function __construct($stream, LoopInterface $loop) 26 | { 27 | $this->stream = $stream; 28 | $this->loop = $loop; 29 | } 30 | 31 | public function isWritable() 32 | { 33 | return $this->writable; 34 | } 35 | 36 | public function write($data) 37 | { 38 | if (!$this->writable) { 39 | return; 40 | } 41 | 42 | $this->data .= $data; 43 | 44 | if (!$this->listening) { 45 | $this->listening = true; 46 | 47 | $this->loop->addWriteStream($this->stream, array($this, 'handleWrite')); 48 | } 49 | 50 | $belowSoftLimit = strlen($this->data) < $this->softLimit; 51 | 52 | return $belowSoftLimit; 53 | } 54 | 55 | public function end($data = null) 56 | { 57 | if (null !== $data) { 58 | $this->write($data); 59 | } 60 | 61 | $this->writable = false; 62 | 63 | if ($this->listening) { 64 | $this->on('full-drain', array($this, 'close')); 65 | } else { 66 | $this->close(); 67 | } 68 | } 69 | 70 | public function close() 71 | { 72 | $this->writable = false; 73 | $this->listening = false; 74 | $this->data = ''; 75 | 76 | $this->emit('close'); 77 | } 78 | 79 | public function handleWrite() 80 | { 81 | if (!is_resource($this->stream) || feof($this->stream)) { 82 | $this->emit('error', array(new \RuntimeException('Tried to write to closed or invalid stream.'))); 83 | 84 | return; 85 | } 86 | 87 | set_error_handler(array($this, 'errorHandler')); 88 | 89 | $sent = fwrite($this->stream, $this->data); 90 | 91 | restore_error_handler(); 92 | 93 | if (false === $sent) { 94 | $this->emit('error', array(new \ErrorException( 95 | $this->lastError['message'], 96 | 0, 97 | $this->lastError['number'], 98 | $this->lastError['file'], 99 | $this->lastError['line'] 100 | ))); 101 | 102 | return; 103 | } 104 | 105 | $len = strlen($this->data); 106 | if ($len >= $this->softLimit && $len - $sent < $this->softLimit) { 107 | $this->emit('drain'); 108 | } 109 | 110 | $this->data = (string) substr($this->data, $sent); 111 | 112 | if (0 === strlen($this->data)) { 113 | $this->loop->removeWriteStream($this->stream); 114 | $this->listening = false; 115 | 116 | $this->emit('full-drain'); 117 | } 118 | } 119 | 120 | private function errorHandler($errno, $errstr, $errfile, $errline) 121 | { 122 | $this->lastError['number'] = $errno; 123 | $this->lastError['message'] = $errstr; 124 | $this->lastError['file'] = $errfile; 125 | $this->lastError['line'] = $errline; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /classes/React/Stream/BufferedSink.php: -------------------------------------------------------------------------------- 1 | deferred = new Deferred(); 17 | 18 | $this->on('pipe', array($this, 'handlePipeEvent')); 19 | $this->on('error', array($this, 'handleErrorEvent')); 20 | } 21 | 22 | public function handlePipeEvent($source) 23 | { 24 | Util::forwardEvents($source, $this, array('error')); 25 | } 26 | 27 | public function handleErrorEvent($e) 28 | { 29 | $this->deferred->reject($e); 30 | } 31 | 32 | public function write($data) 33 | { 34 | $this->buffer .= $data; 35 | $this->deferred->progress($data); 36 | } 37 | 38 | public function close() 39 | { 40 | if ($this->closed) { 41 | return; 42 | } 43 | 44 | parent::close(); 45 | $this->deferred->resolve($this->buffer); 46 | } 47 | 48 | public function promise() 49 | { 50 | return $this->deferred->promise(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /classes/React/Stream/CompositeStream.php: -------------------------------------------------------------------------------- 1 | readable = $readable; 16 | $this->writable = $writable; 17 | 18 | Util::forwardEvents($this->readable, $this, array('data', 'end', 'error', 'close')); 19 | Util::forwardEvents($this->writable, $this, array('drain', 'error', 'close', 'pipe')); 20 | 21 | $this->readable->on('close', array($this, 'close')); 22 | $this->writable->on('close', array($this, 'close')); 23 | 24 | $this->on('pipe', array($this, 'handlePipeEvent')); 25 | } 26 | 27 | public function handlePipeEvent($source) 28 | { 29 | $this->pipeSource = $source; 30 | } 31 | 32 | public function isReadable() 33 | { 34 | return $this->readable->isReadable(); 35 | } 36 | 37 | public function pause() 38 | { 39 | if ($this->pipeSource) { 40 | $this->pipeSource->pause(); 41 | } 42 | 43 | $this->readable->pause(); 44 | } 45 | 46 | public function resume() 47 | { 48 | if ($this->pipeSource) { 49 | $this->pipeSource->resume(); 50 | } 51 | 52 | $this->readable->resume(); 53 | } 54 | 55 | public function pipe(WritableStreamInterface $dest, array $options = array()) 56 | { 57 | Util::pipe($this, $dest, $options); 58 | 59 | return $dest; 60 | } 61 | 62 | public function isWritable() 63 | { 64 | return $this->writable->isWritable(); 65 | } 66 | 67 | public function write($data) 68 | { 69 | return $this->writable->write($data); 70 | } 71 | 72 | public function end($data = null) 73 | { 74 | $this->writable->end($data); 75 | } 76 | 77 | public function close() 78 | { 79 | $this->pipeSource = true; 80 | 81 | $this->readable->close(); 82 | $this->writable->close(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /classes/React/Stream/README.md: -------------------------------------------------------------------------------- 1 | # Stream Component 2 | 3 | Basic readable and writable stream interfaces that support piping. 4 | 5 | In order to make the event loop easier to use, this component introduces the 6 | concept of streams. They are very similar to the streams found in PHP itself, 7 | but have an interface more suited for async I/O. 8 | 9 | Mainly it provides interfaces for readable and writable streams, plus a file 10 | descriptor based implementation with an in-memory write buffer. 11 | 12 | This component depends on `événement`, which is an implementation of the 13 | `EventEmitter`. 14 | 15 | ## Readable Streams 16 | 17 | ### EventEmitter Events 18 | 19 | * `data`: Emitted whenever data was read from the source. 20 | * `end`: Emitted when the source has reached the `eof`. 21 | * `error`: Emitted when an error occurs. 22 | * `close`: Emitted when the connection is closed. 23 | 24 | ### Methods 25 | 26 | * `isReadable()`: Check if the stream is still in a state allowing it to be 27 | read from. It becomes unreadable when the connection ends, closes or an 28 | error occurs. 29 | * `pause()`: Remove the data source file descriptor from the event loop. This 30 | allows you to throttle incoming data. 31 | * `resume()`: Re-attach the data source after a `pause()`. 32 | * `pipe(WritableStreamInterface $dest, array $options = [])`: Pipe this 33 | readable stream into a writable stream. Automatically sends all incoming 34 | data to the destination. Automatically throttles based on what the 35 | destination can handle. 36 | 37 | ## Writable Streams 38 | 39 | ### EventEmitter Events 40 | 41 | * `drain`: Emitted if the write buffer became full previously and is now ready 42 | to accept more data. 43 | * `error`: Emitted whenever an error occurs. 44 | * `close`: Emitted whenever the connection is closed. 45 | * `pipe`: Emitted whenever a readable stream is `pipe()`d into this stream. 46 | 47 | ### Methods 48 | 49 | * `isWritable()`: Check if the stream can still be written to. It cannot be 50 | written to after an error or when it is closed. 51 | * `write($data)`: Write some data into the stream. If the stream cannot handle 52 | it, it should buffer the data or emit and `error` event. If the internal 53 | buffer is full after adding `$data`, `write` should return false, indicating 54 | that the caller should stop sending data until the buffer `drain`s. 55 | * `end($data = null)`: Optionally write some final data to the stream, empty 56 | the buffer, then close it. 57 | 58 | ## Usage 59 | 60 | $loop = React\EventLoop\Factory::create(); 61 | 62 | $source = new React\Stream\Stream(fopen('omg.txt', 'r'), $loop); 63 | $dest = new React\Stream\Stream(fopen('wtf.txt', 'w'), $loop); 64 | 65 | $source->pipe($dest); 66 | 67 | $loop->run(); 68 | -------------------------------------------------------------------------------- /classes/React/Stream/ReadableStream.php: -------------------------------------------------------------------------------- 1 | closed; 14 | } 15 | 16 | public function pause() 17 | { 18 | } 19 | 20 | public function resume() 21 | { 22 | } 23 | 24 | public function pipe(WritableStreamInterface $dest, array $options = array()) 25 | { 26 | Util::pipe($this, $dest, $options); 27 | 28 | return $dest; 29 | } 30 | 31 | public function close() 32 | { 33 | if ($this->closed) { 34 | return; 35 | } 36 | 37 | $this->closed = true; 38 | $this->emit('end', array($this)); 39 | $this->emit('close', array($this)); 40 | $this->removeAllListeners(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /classes/React/Stream/ReadableStreamInterface.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 21 | $this->loop = $loop; 22 | $this->buffer = new Buffer($this->stream, $this->loop); 23 | 24 | $that = $this; 25 | 26 | $this->buffer->on('error', function ($error) use ($that) { 27 | $that->emit('error', array($error, $that)); 28 | $that->close(); 29 | }); 30 | 31 | $this->buffer->on('drain', function () use ($that) { 32 | $that->emit('drain'); 33 | }); 34 | 35 | $this->resume(); 36 | } 37 | 38 | public function isReadable() 39 | { 40 | return $this->readable; 41 | } 42 | 43 | public function isWritable() 44 | { 45 | return $this->writable; 46 | } 47 | 48 | public function pause() 49 | { 50 | $this->loop->removeReadStream($this->stream); 51 | } 52 | 53 | public function resume() 54 | { 55 | $this->loop->addReadStream($this->stream, array($this, 'handleData')); 56 | } 57 | 58 | public function write($data) 59 | { 60 | if (!$this->writable) { 61 | return; 62 | } 63 | 64 | return $this->buffer->write($data); 65 | } 66 | 67 | public function close() 68 | { 69 | if (!$this->writable && !$this->closing) { 70 | return; 71 | } 72 | 73 | $this->closing = false; 74 | 75 | $this->readable = false; 76 | $this->writable = false; 77 | 78 | $this->emit('end', array($this)); 79 | $this->emit('close', array($this)); 80 | $this->loop->removeStream($this->stream); 81 | $this->buffer->removeAllListeners(); 82 | $this->removeAllListeners(); 83 | 84 | $this->handleClose(); 85 | } 86 | 87 | public function end($data = null) 88 | { 89 | if (!$this->writable) { 90 | return; 91 | } 92 | 93 | $this->closing = true; 94 | 95 | $this->readable = false; 96 | $this->writable = false; 97 | 98 | $that = $this; 99 | 100 | $this->buffer->on('close', function () use ($that) { 101 | $that->close(); 102 | }); 103 | 104 | $this->buffer->end($data); 105 | } 106 | 107 | public function pipe(WritableStreamInterface $dest, array $options = array()) 108 | { 109 | Util::pipe($this, $dest, $options); 110 | 111 | return $dest; 112 | } 113 | 114 | public function handleData($stream) 115 | { 116 | $data = fread($stream, $this->bufferSize); 117 | 118 | $this->emit('data', array($data, $this)); 119 | 120 | if (!is_resource($stream) || feof($stream)) { 121 | $this->end(); 122 | } 123 | } 124 | 125 | public function handleClose() 126 | { 127 | if (is_resource($this->stream)) { 128 | fclose($this->stream); 129 | } 130 | } 131 | 132 | public function getBuffer() 133 | { 134 | return $this->buffer; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /classes/React/Stream/StreamInterface.php: -------------------------------------------------------------------------------- 1 | readable->emit('data', array($this->filter($data))); 23 | } 24 | 25 | public function end($data = null) 26 | { 27 | if (null !== $data) { 28 | $this->readable->emit('data', array($this->filter($data))); 29 | } 30 | 31 | $this->writable->end($data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /classes/React/Stream/Util.php: -------------------------------------------------------------------------------- 1 | emit('pipe', array($source)); 16 | 17 | $source->on('data', function ($data) use ($source, $dest) { 18 | $feedMore = $dest->write($data); 19 | 20 | if (false === $feedMore) { 21 | $source->pause(); 22 | } 23 | }); 24 | 25 | $dest->on('drain', function () use ($source) { 26 | $source->resume(); 27 | }); 28 | 29 | $end = isset($options['end']) ? $options['end'] : true; 30 | if ($end && $source !== $dest) { 31 | $source->on('end', function () use ($dest) { 32 | $dest->end(); 33 | }); 34 | } 35 | } 36 | 37 | public static function forwardEvents($source, $target, array $events) 38 | { 39 | foreach ($events as $event) { 40 | $source->on($event, function () use ($event, $target) { 41 | $target->emit($event, func_get_args()); 42 | }); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /classes/React/Stream/WritableStream.php: -------------------------------------------------------------------------------- 1 | write($data); 19 | } 20 | 21 | $this->close(); 22 | } 23 | 24 | public function isWritable() 25 | { 26 | return !$this->closed; 27 | } 28 | 29 | public function close() 30 | { 31 | if ($this->closed) { 32 | return; 33 | } 34 | 35 | $this->closed = true; 36 | $this->emit('end', array($this)); 37 | $this->emit('close', array($this)); 38 | $this->removeAllListeners(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /classes/React/Stream/WritableStreamInterface.php: -------------------------------------------------------------------------------- 1 | =5.3.3", 8 | "evenement/evenement": "1.0.*" 9 | }, 10 | "suggest": { 11 | "react/event-loop": "0.2.*", 12 | "react/promise": "1.0.*" 13 | }, 14 | "autoload": { 15 | "psr-0": { "React\\Stream": "" } 16 | }, 17 | "target-dir": "React/Stream", 18 | "extra": { 19 | "branch-alias": { 20 | "dev-master": "0.2-dev" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /classes/ctrl/ChatCtrl.php: -------------------------------------------------------------------------------- 1 | dispatcher instanceof ShellRequestDispatcher) { 19 | return true; 20 | } 21 | 22 | throw new \Exception('forbidden'); 23 | } 24 | 25 | 26 | 27 | public function start() 28 | { 29 | $socket = new \framework\socket\Socket(HOST, PORT); 30 | $socket->setProtocol(new \socket\Chat()); 31 | $socket->run(); 32 | } 33 | 34 | public function new() { 35 | $loop = \React\EventLoop\Factory::create(); 36 | $socket = new \React\Socket\Server($loop); 37 | 38 | $conns = new \SplObjectStorage(); 39 | 40 | $socket->on('connection', function ($conn) use ($conns) { 41 | echo $conn->getClientAddress(); 42 | echo $conn->getRemoteAddress(); 43 | echo "connection\n"; 44 | $conns->attach($conn); 45 | 46 | $conn->on('data', function ($data) use ($conns, $conn) { 47 | 48 | if('/quit' == trim($data)) { 49 | $conns->detach($conn); 50 | $conn->end(); 51 | return; 52 | } 53 | foreach ($conns as $current) { 54 | if ($conn === $current) { 55 | continue; 56 | } 57 | 58 | $current->write($data); 59 | } 60 | }); 61 | 62 | $conn->on('end', function () use ($conns, $conn) { 63 | $conns->detach($conn); 64 | }); 65 | }); 66 | 67 | $port = PORT; 68 | $host = HOST; 69 | echo "Socket server listening on port {$port}.\n"; 70 | echo "You can connect to it by running: telnet {$host} {$port}\n"; 71 | 72 | $socket->listen($port, $host); 73 | $loop->run(); 74 | } 75 | 76 | public function stop() { 77 | $deamon = new \framework\util\Daemon($GLOBALS['DAEMON_CONFIG']); 78 | $deamon->stop(); 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /classes/ctrl/IndexCtrl.php: -------------------------------------------------------------------------------- 1 | dispatcher instanceof ShellRequestDispatcher) { 19 | return true; 20 | } 21 | 22 | throw new \Exception('forbidden'); 23 | } 24 | 25 | 26 | 27 | public function start() 28 | { 29 | $socket = new \framework\socket\Socket(HOST, PORT); 30 | $socket->setProtocol(new \socket\Memcache()); 31 | $socket->run(); 32 | } 33 | 34 | public function new() { 35 | $loop = \React\EventLoop\Factory::create(); 36 | $socket = new \React\Socket\Server($loop); 37 | 38 | $conns = new \SplObjectStorage(); 39 | static $dataArray = array(); 40 | $socket->on('connection', function ($conn) use ($conns, &$dataArray) { 41 | echo $conn->getClientAddress()."connection\n"; 42 | $conns->attach($conn); 43 | $conn->on('data', function ($data) use ($conns, $conn, &$dataArray) { 44 | if('quit' == trim($data)) { 45 | $conns->detach($conn); 46 | $conn->end(); 47 | return; 48 | } 49 | $ca = explode("\r\n", $data); 50 | $commands = explode(" ", $ca[0]); 51 | switch($commands[0]) { 52 | case 'get': 53 | $returnData = isset($dataArray[$commands[1]]) ? $commands[1]." ".$dataArray[$commands[1]] : "$commands[1] 0 0\r\n\r\n"; 54 | $conn->write("VALUE {$returnData}"); 55 | $conn->write("END\r\n"); 56 | break; 57 | case 'getMulti': 58 | break; 59 | case 'set': 60 | $dataArray[$commands[1]] = $commands[2]." ".$commands[4]." ".substr($data, strpos($data, "\r\n")); 61 | $conn->write("STORED\r\n"); 62 | break; 63 | case 'setMulti': 64 | break; 65 | case 'add': 66 | break; 67 | case 'delete': 68 | if(isset($dataArray[$commands[1]])) { 69 | unset($dataArray[$commands[1]]); 70 | $conn->write("DELETED\r\n"); 71 | } else { 72 | $conn->write("NOT_FOUND\r\n"); 73 | } 74 | break; 75 | } 76 | }); 77 | $conn->on('end', function () use ($conns, $conn, $dataArray) { 78 | $conns->detach($conn); 79 | echo $conn->getClientAddress()."disconnect\n"; 80 | }); 81 | }); 82 | 83 | $port = PORT; 84 | $host = HOST; 85 | echo "Socket server listening on port {$port}.\n"; 86 | echo "You can connect to it by running: telnet {$host} {$port}\n"; 87 | 88 | $socket->listen($port, $host); 89 | $loop->run(); 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /classes/framework/config/BeanStalkConfiguration.php: -------------------------------------------------------------------------------- 1 | host = $host; 13 | $this->port = $port; 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /classes/framework/config/MemcachedConfiguration.php: -------------------------------------------------------------------------------- 1 | host = $host; 32 | $this->port = $port; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /classes/framework/config/PDOConfiguration.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 46 | $this->user = $user; 47 | $this->pass = $pass; 48 | $this->charset = $charset; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /classes/framework/config/RedisConfiguration.php: -------------------------------------------------------------------------------- 1 | host = $host; 31 | $this->port = $port; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /classes/framework/config/SmartyConfiguration.php: -------------------------------------------------------------------------------- 1 | smartyPath = $smartyPath; 56 | $this->cacheDir = $cacheDir; 57 | $this->compileDir = $compileDir; 58 | $this->templateDir = $templateDir; 59 | $this->configDir = $configDir; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /classes/framework/config/TokyoTyrantConfiguration.php: -------------------------------------------------------------------------------- 1 | host = $host; 25 | $this->port = $port; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /classes/framework/core/Context.php: -------------------------------------------------------------------------------- 1 | ctrlClassName = \ucfirst($arr[1]) . 'Ctrl'; 22 | $this->ctrlMethodName = $arr[2]; 23 | } 24 | } else { 25 | $this->ctrlClassName = isset($_REQUEST['c']) ? \str_replace('/', '\\', $_REQUEST['c']) : 'IndexCtrl'; 26 | $this->ctrlMethodName = isset($_REQUEST['m']) ? $_REQUEST['m'] : 'main'; 27 | } 28 | } 29 | 30 | public function getCtrlClassName() { 31 | return $this->ctrlClassName; 32 | } 33 | 34 | public function getCtrlMethodName() { 35 | return $this->ctrlMethodName; 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /classes/framework/dispatcher/RequestDispatcherBase.php: -------------------------------------------------------------------------------- 1 | daemon) { 29 | $this->_daemon(); 30 | } else { 31 | $this->_dispatch(); 32 | } 33 | } 34 | 35 | private function _daemon() { 36 | $deamon = new Daemon($GLOBALS['DAEMON_CONFIG']); 37 | $deamon->start(); 38 | $this->_dispatch(); 39 | } 40 | 41 | public function _dispatch() { 42 | $ctrlClass = Context::getCtrlNamespace() . "\\" . $this->getCtrlClassName(); 43 | $ctrlMethod = $this->getCtrlMethodName(); 44 | 45 | $ctrl = new $ctrlClass(); 46 | $filtered = false; 47 | 48 | if ($ctrl instanceof IController) { 49 | $ctrl->setDispatcher($this); 50 | $filtered = !$ctrl->beforeFilter(); 51 | } 52 | 53 | $exception = null; 54 | 55 | if (!$filtered) { 56 | try { 57 | $view = $ctrl->$ctrlMethod(); 58 | 59 | if ($view instanceof IView) { 60 | $view->display(); 61 | } 62 | } catch (\Exception $e) { 63 | $exception = $e; 64 | } 65 | } 66 | 67 | if ($ctrl instanceof IController) { 68 | $ctrl->afterFilter(); 69 | } 70 | 71 | if ($exception != null) { 72 | throw $exception; 73 | } 74 | } 75 | 76 | /** 77 | * 获取控制器类名 78 | * 79 | * @return String 80 | */ 81 | abstract public function getCtrlClassName(); 82 | 83 | /** 84 | * 获取控制器方法名 85 | * 86 | * @return String 87 | */ 88 | abstract public function getCtrlMethodName(); 89 | } -------------------------------------------------------------------------------- /classes/framework/dispatcher/ShellRequestDispatcher.php: -------------------------------------------------------------------------------- 1 | defaultAction = 'Shell.main'; 16 | if (isset($_SERVER['argv'])) { 17 | if(isset($_SERVER['argv'][1])) { 18 | $act = $_SERVER['argv'][1]; 19 | } 20 | 21 | if("-d" == end($_SERVER['argv'])) { 22 | $this->daemon = true; 23 | } 24 | 25 | } else { 26 | $act = $this->defaultAction; 27 | } 28 | 29 | if (\preg_match('/^([a-z_]+)\.([a-z_]+)$/i', $act, $arr)) { 30 | $this->ctrlClassName = $arr[1] . 'Ctrl'; 31 | $this->ctrlMethodName = $arr[2]; 32 | } 33 | } 34 | 35 | public function getCtrlClassName() { 36 | return $this->ctrlClassName; 37 | } 38 | 39 | public function getCtrlMethodName() { 40 | return $this->ctrlMethodName; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /classes/framework/helper/CacheHelper.php: -------------------------------------------------------------------------------- 1 | memcached = manager\MemcachedManager::getInstance($name, $pconnect); 26 | } 27 | 28 | public function enable() { 29 | return true; 30 | } 31 | 32 | /** 33 | * 取得Memcached对象 34 | * 35 | * @return \Memcached 36 | */ 37 | function getMemcached() { 38 | return $this->memcached; 39 | } 40 | 41 | /** 42 | * 添加新数据(如存在则失败) 43 | * 44 | * @param string $key 45 | * @param mixed $value 46 | * @param int $expiration 47 | * @return bool 48 | */ 49 | public function add($key, $value, $expiration = 0) { 50 | return $this->memcached ? $this->memcached->add($key, $value, $expiration) : false; 51 | } 52 | 53 | /** 54 | * 替换指定键名的数据(如不存在则失败) 55 | * 56 | * @param string $key 57 | * @param mixed $value 58 | * @param int $expiration 59 | * @return bool 60 | */ 61 | public function replace($key, $value, $expiration = 0) { 62 | return $this->memcached ? $this->memcached->replace($key, $value, $expiration) : false; 63 | } 64 | 65 | /** 66 | * 存储指定键名的数据(如存在则覆盖) 67 | * 68 | * @param string $key 69 | * @param mixed $value 70 | * @param int $expiration 71 | * @return bool 72 | */ 73 | public function set($key, $value, $expiration = 0) { 74 | return $this->memcached ? $this->memcached->set($key, $value, $expiration) : false; 75 | } 76 | 77 | /** 78 | * 存储指定数据序列(如存在则覆盖) 79 | * 80 | * @param array $items 81 | * @param int $expiration 82 | * @return bool 83 | */ 84 | public function setMulti($items, $expiration = 0) { 85 | return $this->memcached ? $this->memcached->setMulti($items, $expiration) : false; 86 | } 87 | 88 | /** 89 | * 获取指定键名的数据 90 | * 91 | * @param string $key 92 | * @return mixed 93 | */ 94 | public function get($key) { 95 | return $this->memcached ? $this->memcached->get($key) : null; 96 | } 97 | 98 | /** 99 | * 获取指定键名序列的数据 100 | * 101 | * @param array $keys 102 | * @return array 103 | */ 104 | public function getMulti($keys) { 105 | return $this->memcached ? $this->memcached->getMulti($keys) : null; 106 | } 107 | 108 | /** 109 | * 增加整数数据的值 110 | * 111 | * @param string $key 112 | * @param int $offset 113 | * @return bool 114 | */ 115 | public function increment($key, $offset = 1) { 116 | return $this->memcached ? $this->memcached->increment($key, $offset) : false; 117 | } 118 | 119 | /** 120 | * 减少整数数据的值 121 | * 122 | * @param string $key 123 | * @param int $offset 124 | * @return bool 125 | */ 126 | public function decrement($key, $offset = 1) { 127 | return $this->memcached ? $this->memcached->decrement($key, $offset) : false; 128 | } 129 | 130 | /** 131 | * 删除指定数据 132 | * 133 | * @param string $key 134 | * @return bool 135 | */ 136 | public function delete($key) { 137 | return $this->memcached ? $this->memcached->delete($key) : false; 138 | } 139 | 140 | /** 141 | * 删除指定键名序列的数据 142 | * 143 | * @param array $keys 144 | * @return bool 145 | */ 146 | public function deleteMulti($keys) { 147 | if (!$this->memcached || empty($keys)) { 148 | return false; 149 | } 150 | 151 | foreach ($keys as $key) { 152 | $this->memcached->delete($key); 153 | } 154 | 155 | return true; 156 | } 157 | 158 | /** 159 | * 无效化所有缓存数据(清空缓存,慎用) 160 | * 161 | * @return bool 162 | */ 163 | public function flush() { 164 | return $this->memcached ? $this->memcached->flush() : false; 165 | } 166 | 167 | /** 168 | * 获取服务器统计信息 169 | * 170 | * @return array 171 | */ 172 | public function stat() { 173 | return $this->memcached ? $this->memcached->getStats() : null; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /classes/framework/helper/cache/RedisHelper.php: -------------------------------------------------------------------------------- 1 | setNx($key, $value); 26 | if ($result && $expiration > 0) { 27 | self::$redis->setTimeout($key, $expiration); 28 | } 29 | return $result; 30 | } 31 | 32 | public function set($key, $value, $expiration = 0) { 33 | if ($expiration) { 34 | return $result = self::$redis->setex($key, $expiration, $value); 35 | } else { 36 | return $result = self::$redis->set($key, $value); 37 | } 38 | } 39 | 40 | public function addToCache($key, $value, $expiration = 0) { 41 | $value = \igbinary_serialize($value); 42 | return $this->set($key, $value, $expiration); 43 | } 44 | 45 | public function get($key) { 46 | return self::$redis->get($key); 47 | } 48 | 49 | public function getCache($key) { 50 | return \igbinary_unserialize($this->get($key)); 51 | } 52 | 53 | public function delete($key) { 54 | return self::$redis->delete($key); 55 | } 56 | 57 | public function increment($key, $offset = 1) { 58 | return self::$redis->incrBy($key, $offset); 59 | } 60 | 61 | /** 62 | * 减少整数数据的值 63 | * 64 | * @param string $key 65 | * @param int $offset 66 | * @return bool 67 | */ 68 | public function decrement($key, $offset = 1) { 69 | return self::$redis->decBy($key, $offset); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /classes/framework/helper/cache/XcacheHelper.php: -------------------------------------------------------------------------------- 1 | put($key, $data); 22 | } 23 | 24 | public function getQueue($key) { 25 | $job = self::$beanstalk->reserve($key); 26 | self::$beanstalk->delete($job['id'], $key); 27 | return $job; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /classes/framework/helper/queue/IQueue.php: -------------------------------------------------------------------------------- 1 | rPush($key, $data); 22 | } 23 | 24 | public function getQueue($key) { 25 | return self::$redis->lPop($key); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /classes/framework/helper/storage/IStorage.php: -------------------------------------------------------------------------------- 1 | redis)) { 23 | $this->redis = manager\RedisManager::getInstance($name, $pconnect); 24 | $this->pconnect = $pconnect; 25 | } 26 | } 27 | 28 | public function setSlave($name, $pconnect = false) { 29 | if (empty($this->sRedis)) { 30 | $this->sRedis = manager\RedisManager::getInstance($name, $pconnect); 31 | } 32 | } 33 | 34 | public function setKeySuffix($suffix) { 35 | $this->suffix = $suffix; 36 | } 37 | 38 | private function uKey($userId) { 39 | return $userId . '_' . $this->suffix; 40 | } 41 | 42 | private function dsKey($ukey, $key) { 43 | return $ukey . '_' . $key; 44 | } 45 | 46 | public function getMutilMD($userId, $keys) { 47 | $uKey = $this->uKey($userId); 48 | return $this->redis->hMGet($uKey, $keys); 49 | } 50 | 51 | public function getMD($userId, $key, $slaveName = "") { 52 | $uKey = $this->uKey($userId); 53 | $data = $this->redis->hGet($uKey, $key); 54 | if (false === $data) { 55 | if ($this->redis->hExists($uKey, $key)) { 56 | throw new \Exception("{$key} exist"); 57 | } 58 | //从leveldb取数据 59 | $dsKey = $this->dsKey($uKey, $key); 60 | $data = $this->redis->dsGet($dsKey); 61 | if (!empty($data)) { //取到数据,存回redis 62 | $this->redis->hSet($uKey, $key, $data); 63 | } 64 | } 65 | return $data; 66 | } 67 | 68 | public function getSD($userId, $key, $slaveName = "") { 69 | $uKey = $this->uKey($userId); 70 | $dsKey = $this->dsKey($uKey, $key); 71 | return $this->redis->dsGet($dsKey); 72 | } 73 | 74 | public function getDS($userId, $key) { 75 | $uKey = $this->uKey($userId); 76 | $dsKey = $this->dsKey($uKey, $key); 77 | return $this->redis->dsGet($dsKey); 78 | } 79 | 80 | public function setDS($userId, $key, $data) { 81 | $uKey = $this->uKey($userId); 82 | $dsKey = $this->dsKey($uKey, $key); 83 | return $this->redis->dsSet($dsKey, $data); 84 | } 85 | 86 | public function setMD($userId, $key, $data, $cas = false) { 87 | if ($cas) { 88 | return $this->setMDCAS($userId, $key, $data); 89 | } 90 | $uKey = $this->uKey($userId); 91 | $dsKey = $this->dsKey($uKey, $key); 92 | if ($this->redis->dsSet($dsKey, $data)) { 93 | return $this->redis->hSet($uKey, $key, $data); 94 | } 95 | 96 | return false; 97 | } 98 | 99 | public function addMD($userId, $key, $data) { 100 | $uKey = $this->uKey($userId); 101 | $dsKey = $this->dsKey($uKey, $key); 102 | if ($this->redis->dsGet($dsKey)) { 103 | throw new \Exception("{$dsKey} exist"); 104 | } 105 | if ($this->redis->dsSet($dsKey, $data)) { 106 | return $this->redis->hSetNx($uKey, $key, $data); 107 | } 108 | return false; 109 | } 110 | 111 | public function setMDCAS($userId, $key, $data) { 112 | $uKey = $this->uKey($userId); 113 | $this->redis->watch($uKey); 114 | $result = $this->redis->multi()->hSet($uKey, $key, $data)->exec(); 115 | if (false === $result) { 116 | throw new \Exception('cas error'); 117 | } 118 | $dsKey = $this->dsKey($uKey, $key); 119 | if ($this->redis->dsSet($dsKey, $data)) { 120 | return true; 121 | } 122 | 123 | throw new \Exception('dsSet error'); 124 | } 125 | 126 | public function del($userId, $key) { 127 | 128 | $uKey = $this->uKey($userId); 129 | $dsKey = $this->dsKey($uKey, $key); 130 | if ($this->redis->dsDel($dsKey)) { 131 | return $this->redis->hDel($uKey, $key); 132 | } 133 | 134 | return false; 135 | } 136 | 137 | public function setMultiMD($userId, $keys) { 138 | $uKey = $this->uKey($userId); 139 | $dsKeys = []; 140 | foreach ($keys as $key => $val) { 141 | $dsKey = $this->dsKey($uKey, $key); 142 | $dsKeys[$dsKey] = $val; 143 | } 144 | if ($this->redis->dsMSet($dsKeys)) { 145 | return $this->redis->hMSet($uKey, $keys); 146 | } 147 | 148 | return false; 149 | } 150 | 151 | public function close() { 152 | if ($this->pconnect) { 153 | return true; 154 | } 155 | 156 | $this->redis->close(); 157 | 158 | if (!empty($this->sRedis)) { 159 | $this->sRedis->close(); 160 | } 161 | 162 | return true; 163 | } 164 | 165 | public function getMulti($cmds) { 166 | $this->redis->multi(\Redis::PIPELINE); 167 | foreach ($cmds as $userId => $key) { 168 | $uKey = $this->uKey($userId); 169 | $this->redis->hGet($uKey, $key); 170 | } 171 | 172 | return $this->redis->exec(); 173 | } 174 | 175 | public function setExpire($userId, $key, $time) { 176 | $uKey = $this->uKey($userId); 177 | return $this->redis->setTimeout($uKey, $time); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /classes/framework/helper/storage/RedisHelper.php: -------------------------------------------------------------------------------- 1 | redis)) { 19 | $this->redis = manager\RedisManager::getInstance($name, $pconnect); 20 | $this->pconnect = $pconnect; 21 | } 22 | } 23 | 24 | public function setSlave($name, $pconnect = false) { 25 | if (empty($this->sRedis)) { 26 | $this->sRedis = manager\RedisManager::getInstance($name, $pconnect); 27 | } 28 | } 29 | 30 | public function setKeySuffix($suffix) { 31 | $this->suffix = $suffix; 32 | } 33 | 34 | private function uKey($userId) { 35 | return $userId . '_' . $this->suffix; 36 | } 37 | 38 | public function getMutilMD($userId, $keys) { 39 | $uKey = $this->uKey($userId); 40 | return $this->redis->hMGet($uKey, $keys); 41 | } 42 | 43 | public function getMD($userId, $key, $slaveName = "") { 44 | $uKey = $this->uKey($userId); 45 | $data = $this->redis->hGet($uKey, $key); 46 | /** 47 | if(false === $data) { 48 | if( $this->redis->hExists($uKey, $key) ) { 49 | throw new \Exception("{$key} exist"); 50 | } 51 | } 52 | */ 53 | return $data; 54 | } 55 | 56 | public function getSD($userId, $key, $slaveName = "") { 57 | $uKey = $this->uKey($userId); 58 | $this->setSlave($slaveName); 59 | $data = $this->sRedis->hGet($uKey, $key); 60 | /** 61 | if(false === $data) { 62 | if( $this->sRedis->hExists($uKey, $key) ) { 63 | throw new \Exception("{$key} exist"); 64 | } 65 | } 66 | * 67 | */ 68 | return $data; 69 | } 70 | 71 | public function setMD($userId, $key, $data, $cas = false) { 72 | if ($cas) { 73 | return $this->setMDCAS($userId, $key, $data); 74 | } 75 | $uKey = $this->uKey($userId); 76 | return $this->redis->hSet($uKey, $key, $data); 77 | } 78 | 79 | public function addMD($userId, $key, $data) { 80 | $uKey = $this->uKey($userId); 81 | return $this->redis->hSetNx($uKey, $key, $data); 82 | } 83 | 84 | public function setMDCAS($userId, $key, $data) { 85 | $uKey = $this->uKey($userId); 86 | $this->redis->watch($uKey); 87 | $result = $this->redis->multi()->hSet($uKey, $key, $data)->exec(); 88 | if (false === $result) { 89 | throw new \Exception('cas error'); 90 | } 91 | return $result; 92 | } 93 | 94 | public function del($userId, $key) { 95 | $uKey = $this->uKey($userId); 96 | return $this->redis->hDel($uKey, $key); 97 | } 98 | 99 | public function setMultiMD($userId, $keys) { 100 | $uKey = $this->uKey($userId); 101 | return $this->redis->hMSet($uKey, $keys); 102 | } 103 | 104 | public function close() { 105 | if ($this->pconnect) { 106 | return true; 107 | } 108 | 109 | $this->redis->close(); 110 | 111 | if (!empty($this->sRedis)) { 112 | $this->sRedis->close(); 113 | } 114 | 115 | return true; 116 | } 117 | 118 | public function getMulti($cmds) { 119 | $this->redis->multi(\Redis::PIPELINE); 120 | foreach ($cmds as $userId => $key) { 121 | $uKey = $this->uKey($userId); 122 | $this->redis->hGet($uKey, $key); 123 | } 124 | 125 | return $this->redis->exec(); 126 | } 127 | 128 | public function setExpire($userId, $key, $time) { 129 | return; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /classes/framework/helper/storage/TTHelper.php: -------------------------------------------------------------------------------- 1 | tokyoTyrant)) { 18 | $this->tokyoTyrant = manager\MemcachedManager::getInstance($name, $pconnect); 19 | } 20 | } 21 | 22 | public function setSlave($name) { 23 | if (empty($this->sTokyoTyrant)) { 24 | $this->sTokyoTyrant = manager\MemcachedManager::getInstance($name); 25 | } 26 | } 27 | 28 | public function get($userId, $key) { 29 | $key = $this->uKey($userId, $key); 30 | return $this->tokyoTyrant->get($key); 31 | } 32 | 33 | public function set($userId, $key, $data) { 34 | $key = $this->uKey($userId, $key); 35 | return $this->tokyoTyrant->set($key, $data); 36 | } 37 | 38 | public function del($userId, $key) { 39 | $key = $this->uKey($userId, $key); 40 | return $this->tokyoTyrant->delete($key); 41 | } 42 | 43 | public function delMulti($userId, $keys) { 44 | foreach ($keys as $key) { 45 | $this->del($userId, $key); 46 | } 47 | return true; 48 | } 49 | 50 | public function getMutilMD($userId, $keys) { 51 | $newKeys = array(); 52 | foreach ($keys as $key) { 53 | $newKeys[] = $this->uKey($userId, $key); 54 | } 55 | return $this->tokyoTyrant->getMulti($newKeys); 56 | } 57 | 58 | public function getMD($userId, $key, $slaveName = "") { 59 | $key = $this->uKey($userId, $key); 60 | $data = $this->tokyoTyrant->get($key); 61 | 62 | if (false === $data) { 63 | $code = $this->tokyoTyrant->getResultCode(); 64 | if ($code == \Memcached::RES_NOTFOUND) { 65 | $this->setSlave($slaveName); 66 | $data = $this->sTokyoTyrant->get($key); 67 | if (false === $data) { 68 | $code = $this->sTokyoTyrant->getResultCode(); 69 | if ($code == \Memcached::RES_NOTFOUND) { 70 | return false; 71 | } else { 72 | throw new \common\GameException("null data: {$userId}, {$key}, {$code}", \common\ERROR::DATA_ERROR); 73 | } 74 | } 75 | } else { 76 | throw new \common\GameException("error data: {$userId}, {$key}, {$code}", \common\ERROR::DATA_ERROR); 77 | } 78 | } 79 | return $data; 80 | } 81 | 82 | public function getSD($userId, $key, $slaveName = "") { 83 | $key = $this->uKey($userId, $key); 84 | $data = $this->sTokyoTyrant->get($key); 85 | if (false === $data) { 86 | $code = $this->sTokyoTyrant->getResultCode(); 87 | if ($code == \Memcached::RES_NOTFOUND) { 88 | return false; 89 | } else { 90 | throw new \common\GameException("null data: {$userId}, {$key}, {$code}", \common\ERROR::DATA_ERROR); 91 | } 92 | } 93 | 94 | return $data; 95 | } 96 | 97 | public function setMD($userId, $key, $data) { 98 | $key = $this->uKey($userId, $key); 99 | return $this->tokyoTyrant->set($key, $data); 100 | } 101 | 102 | public function setMDCAS($userId, $key, $data) { 103 | $key = $this->uKey($userId, $key); 104 | return $this->tokyoTyrant->set($key, $data); 105 | } 106 | 107 | public function setMultiMD($userId, $keys) { 108 | foreach ($keys as $key => $value) { 109 | $newKey = $this->uKey($userId, $key); 110 | $keys[$newKey] = $value; 111 | unset($key); 112 | } 113 | return $this->tokyoTyrant->setMulti($keys); 114 | } 115 | 116 | public function setKeySuffix($suffix) { 117 | $this->suffix = $suffix; 118 | } 119 | 120 | private function uKey($userId, $key) { 121 | return $userId . "_" . $this->suffix . "__" . $key; 122 | } 123 | 124 | public function close() { 125 | return true; 126 | } 127 | 128 | public function setExpire($userId, $key, $time) { 129 | return; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /classes/framework/manager/BeanStalkManager.php: -------------------------------------------------------------------------------- 1 | addServer($config->host, $config->port); 34 | } 35 | 36 | self::$instance = $beanstalk; 37 | } 38 | 39 | return self::$instance; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /classes/framework/manager/LRedisManager.php: -------------------------------------------------------------------------------- 1 | array 21 | */ 22 | private static $configs; 23 | 24 | /** 25 | * Redis实例 26 | * 27 | * @var <\Redis>array 28 | */ 29 | private static $instances; 30 | 31 | /** 32 | * 添加Redis配置 33 | * 34 | * @param string $name 35 | * @param RedisConfiguration $config 36 | */ 37 | public static function addConfigration($name, RedisConfiguration $config) { 38 | self::$configs[$name] = $config; 39 | } 40 | 41 | /** 42 | * 获取Redis实例 43 | * 44 | * @param string $name 45 | * @return \Redis 46 | */ 47 | public static function getInstance($name, $pconnect) { 48 | if (empty(self::$instances[$name])) { 49 | if (empty(self::$configs[$name])) { 50 | return null; 51 | } 52 | 53 | $config = self::$configs[$name]; 54 | 55 | if ($pconnect) { 56 | $redis = \phpiredis_pconnect($config->host, $config->port, 5, $name); 57 | } else { 58 | $redis = \phpiredis_connect($config->host, $config->port, 5); 59 | } 60 | //$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); 61 | self::$instances[$name] = $redis; 62 | } 63 | 64 | return self::$instances[$name]; 65 | } 66 | 67 | /** 68 | * 69 | * 手动关闭链接 70 | * @return boolean 71 | */ 72 | public static function closeInstance() { 73 | if (empty(self::$instances)) { 74 | return true; 75 | } 76 | 77 | if (\defined('CACHE_PCONNECT') && \CACHE_PCONNECT) { 78 | return true; 79 | } 80 | 81 | foreach (self::$instances as $redis) { 82 | 83 | \phpiredis_disconnect($redis); 84 | } 85 | 86 | return true; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /classes/framework/manager/MemcachedManager.php: -------------------------------------------------------------------------------- 1 | array 17 | */ 18 | private static $configs; 19 | 20 | /** 21 | * Memcached实例 22 | * 23 | * @var \Memcached 24 | */ 25 | private static $instance; 26 | 27 | /** 28 | * 添加Memcached配置 29 | * 30 | * @param MemcachedConfiguration $config 31 | */ 32 | public static function addConfigration($name, MemcachedConfiguration $config) { 33 | self::$configs[$name][] = $config; 34 | } 35 | 36 | /** 37 | * 获取Memcached实例 38 | * 39 | * @return \Memcached 40 | */ 41 | public static function getInstance($name = "", $pconnect = false) { 42 | 43 | if (empty(self::$instance[$name])) { 44 | if (empty(self::$configs[$name])) { 45 | return null; 46 | } 47 | 48 | if ($pconnect) { 49 | $memcached = new Memcached($name); 50 | } else { 51 | $memcached = new Memcached(); 52 | } 53 | 54 | foreach (self::$configs[$name] as $config) { 55 | $memcached->addServer($config->host, $config->port); 56 | } 57 | 58 | self::$instance = $memcached; 59 | } 60 | 61 | return self::$instance; 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /classes/framework/manager/PDOManager.php: -------------------------------------------------------------------------------- 1 | array 16 | */ 17 | private static $configs; 18 | 19 | /** 20 | * PDO实例 21 | * 22 | * @var <\PDO>array 23 | */ 24 | private static $instances; 25 | 26 | /** 27 | * 添加PDO配置 28 | * 29 | * @param string $name 30 | * @param PDOConfiguration $config 31 | */ 32 | public static function addConfigration($name, PDOConfiguration $config) { 33 | self::$configs[$name] = $config; 34 | } 35 | 36 | /** 37 | * 获取PDO实例 38 | * 39 | * @param string $name 40 | * @return \PDO 41 | */ 42 | public static function getInstance($name) { 43 | if (empty(self::$instances[$name])) { 44 | if (empty(self::$configs[$name])) { 45 | return null; 46 | } 47 | 48 | $config = self::$configs[$name]; 49 | $pdo = new \PDO($config->uri, $config->user, $config->pass, array( 50 | \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '{$config->charset}';", 51 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION 52 | )); 53 | 54 | self::$instances[$name] = $pdo; 55 | } 56 | 57 | return self::$instances[$name]; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /classes/framework/manager/RedisManager.php: -------------------------------------------------------------------------------- 1 | array 17 | */ 18 | private static $configs; 19 | 20 | /** 21 | * Redis实例 22 | * 23 | * @var <\Redis>array 24 | */ 25 | private static $instances; 26 | 27 | /** 28 | * 添加Redis配置 29 | * 30 | * @param string $name 31 | * @param RedisConfiguration $config 32 | */ 33 | public static function addConfigration($name, RedisConfiguration $config) { 34 | self::$configs[$name] = $config; 35 | } 36 | 37 | /** 38 | * 获取Redis实例 39 | * 40 | * @param string $name 41 | * @return \Redis 42 | */ 43 | public static function getInstance($name, $pconnect) { 44 | if (empty(self::$instances[$name])) { 45 | if (empty(self::$configs[$name])) { 46 | return null; 47 | } 48 | 49 | $config = self::$configs[$name]; 50 | 51 | $redis = new Redis(); 52 | if ($pconnect) { 53 | $redis->pconnect($config->host, $config->port, 5, $name); 54 | } else { 55 | $redis->connect($config->host, $config->port, 5); 56 | } 57 | $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); 58 | self::$instances[$name] = $redis; 59 | } 60 | 61 | return self::$instances[$name]; 62 | } 63 | 64 | /** 65 | * 66 | * 手动关闭链接 67 | * @return boolean 68 | */ 69 | public static function closeInstance() { 70 | if (empty(self::$instances)) { 71 | return true; 72 | } 73 | 74 | if (\defined('CACHE_PCONNECT') && \CACHE_PCONNECT) { 75 | return true; 76 | } 77 | 78 | foreach (self::$instances as $redis) { 79 | 80 | $redis->close(); 81 | } 82 | 83 | return true; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /classes/framework/manager/TokyoTyrantManager.php: -------------------------------------------------------------------------------- 1 | array 16 | */ 17 | private static $configs; 18 | 19 | /** 20 | * TokyoTyrant实例 21 | * 22 | * @var <\TokyoTyrant>array 23 | */ 24 | private static $instances; 25 | 26 | /** 27 | * 添加TokyoTyrant配置 28 | * 29 | * @param string $name 30 | * @param TokyoTyrantConfiguration $config 31 | */ 32 | public static function addConfigration($name, TokyoTyrantConfiguration $config) { 33 | self::$configs[$name] = $config; 34 | } 35 | 36 | /** 37 | * 获取TokyoTyrant实例 38 | * 39 | * @param string $name 40 | * @return \TokyoTyrant 41 | */ 42 | public static function getInstance($name) { 43 | if (empty(self::$instances[$name])) { 44 | if (empty(self::$configs[$name])) { 45 | return null; 46 | } 47 | 48 | $config = self::$configs[$name]; 49 | 50 | $tokyoTyrant = new \Memcached(); 51 | $tokyoTyrant->addServer($config->host, $config->port); 52 | 53 | self::$instances[$name] = $tokyoTyrant; 54 | } 55 | 56 | return self::$instances[$name]; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /classes/framework/setup.php: -------------------------------------------------------------------------------- 1 | display(); 28 | } 29 | -------------------------------------------------------------------------------- /classes/framework/socket/Socket.php: -------------------------------------------------------------------------------- 1 | host = $host; 33 | $this->port = $port; 34 | $this->timeout = $timeout; 35 | } 36 | 37 | private function init() { 38 | $this->base_event = \event_base_new(); 39 | $this->server_event = \event_new(); 40 | } 41 | 42 | public function setProtocol($protocol) { 43 | $this->protocol = $protocol; 44 | $this->protocol->server = $this; 45 | } 46 | 47 | private function create($uri, $block = 0) { 48 | $socket = \stream_socket_server($uri, $errno, $errstr); 49 | 50 | if (!$socket) { 51 | throw new \Exception($errno . $errstr); 52 | } 53 | //设置socket为非堵塞或者阻塞 54 | \stream_set_blocking($socket, $block); 55 | return $socket; 56 | } 57 | 58 | public function accept() { 59 | $client_socket = \stream_socket_accept($this->server_sock); 60 | $client_socket_id = (int) $client_socket; 61 | \stream_set_blocking($client_socket, $this->client_block); 62 | $this->client_sock[$client_socket_id] = $client_socket; 63 | $this->client_num++; 64 | if ($this->client_num > $this->max_connect) { 65 | $this->_closeSocket($client_socket); 66 | return false; 67 | } else { 68 | //设置写缓冲区 69 | \stream_set_write_buffer($client_socket, $this->write_buffer_size); 70 | return $client_socket_id; 71 | } 72 | } 73 | 74 | /** 75 | * 运行服务器程序 76 | * @return unknown_type 77 | */ 78 | public function run($num = 1) { 79 | $this->init(); 80 | //建立服务器端Socket 81 | $this->server_sock = $this->create("tcp://{$this->host}:{$this->port}"); 82 | 83 | //设置事件监听,监听到服务器端socket可读,则有连接请求 84 | \event_set($this->server_event, $this->server_sock, EV_READ | EV_PERSIST, '\\framework\\socket\\Socket::server_handle_connect', $this); 85 | \event_base_set($this->server_event, $this->base_event); 86 | \event_add($this->server_event); 87 | if ($num > 1) { 88 | for ($i = 1; $i < $num; $i++) { 89 | $pid = \pcntl_fork(); 90 | if ($pid) { 91 | 92 | } else { 93 | break; 94 | } 95 | } 96 | } 97 | $this->protocol->onStart(); 98 | \event_base_loop($this->base_event); 99 | } 100 | 101 | /** 102 | * 向client发送数据 103 | * @param $client_id 104 | * @param $data 105 | * @return unknown_type 106 | */ 107 | public function _send($client_id, $data) { 108 | $length = \strlen($data); 109 | for ($written = 0; $written < $length; $written += $fwrite) { 110 | $fwrite = \stream_socket_sendto($client_id, substr($data, $written)); 111 | if ($fwrite <= 0 or $fwrite === false) { 112 | return $written; 113 | } 114 | } 115 | return $written; 116 | } 117 | 118 | public function send($cilent_id, $data) { 119 | if (isset($this->client_sock[$cilent_id])) { 120 | return $this->_send($this->client_sock[$cilent_id], $data); 121 | } 122 | } 123 | 124 | /** 125 | * 向所有client发送数据 126 | * @return unknown_type 127 | */ 128 | public function sendAll($client_id, $data) { 129 | foreach ($this->client_sock as $k => $sock) { 130 | if ($client_id and $k == $client_id) { 131 | continue; 132 | } 133 | $this->_send($sock, $data); 134 | } 135 | 136 | return TRUE; 137 | } 138 | 139 | /** 140 | * 关闭服务器程序 141 | * @return unknown_type 142 | */ 143 | public function shutdown() { 144 | //关闭所有客户端 145 | foreach ($this->client_sock as $k => $sock) { 146 | $this->_closeSocket($sock, $this->client_event[$k]); 147 | } 148 | //关闭服务器端 149 | $this->_closeSocket($this->server_sock, $this->server_event); 150 | //关闭事件循环 151 | \event_base_loopexit($this->base_event); 152 | $this->protocol->onShutdown(); 153 | } 154 | 155 | private function _closeSocket($socket, $event = null) { 156 | if ($event) { 157 | \event_del($event); 158 | \event_free($event); 159 | } 160 | \stream_socket_shutdown($socket, STREAM_SHUT_RDWR); 161 | \fclose($socket); 162 | } 163 | 164 | /** 165 | * 关闭某个客户端 166 | * @return unknown_type 167 | */ 168 | public function close($client_id) { 169 | $this->_closeSocket($this->client_sock[$client_id], $this->client_event[$client_id]); 170 | unset($this->client_sock[$client_id], $this->client_event[$client_id]); 171 | $this->protocol->onClose($client_id); 172 | $this->client_num--; 173 | } 174 | 175 | public static function server_handle_connect($server_socket, $events, $server) { 176 | if ($client_id = $server->accept()) { 177 | $client_socket = $server->client_sock[$client_id]; 178 | //新的事件监听,监听客户端发生的事件 179 | $client_event = event_new(); 180 | event_set($client_event, $client_socket, EV_READ | EV_PERSIST, "\\framework\\socket\\Socket::server_handle_receive", array($server, $client_id)); 181 | //设置基本时间系统 182 | event_base_set($client_event, $server->base_event); 183 | //加入事件监听组 184 | event_add($client_event); 185 | $server->client_event[$client_id] = $client_event; 186 | $server->protocol->onConnect($client_id); 187 | } 188 | } 189 | 190 | /** 191 | * 接收到数据后进行处理 192 | * @param $client_socket 193 | * @param $events 194 | * @param $arg 195 | * @return unknown_type 196 | */ 197 | public static function server_handle_receive($client_socket, $events, $arg) { 198 | $server = $arg[0]; 199 | $client_id = $arg[1]; 200 | $data = self::fread_stream($client_socket, $server->buffer_size); 201 | 202 | if ($data !== false) { 203 | $server->protocol->onRecive($client_id, $data); 204 | } else { 205 | $server->close($client_id); 206 | } 207 | } 208 | 209 | private static function fread_stream($fp, $length) { 210 | 211 | //return stream_socket_recvfrom($fp, $length); 212 | $data = false; 213 | while ($buf = stream_socket_recvfrom($fp, $length)) { 214 | $data .= $buf; 215 | if (strlen($buf) < $length) 216 | break; 217 | } 218 | return $data; 219 | } 220 | 221 | } -------------------------------------------------------------------------------- /classes/framework/util/FileUtil.php: -------------------------------------------------------------------------------- 1 | 'Exception', 21 | 'message' => $exception->getMessage(), 22 | 'code' => $exception->getCode(), 23 | 'file' => $exception->getFile(), 24 | 'line' => $exception->getLine(), 25 | 'userAgent' => $_SERVER['HTTP_USER_AGENT'], 26 | 'trace' => array(), 27 | ); 28 | 29 | if (true) { 30 | $traceItems = $exception->getTrace(); 31 | 32 | foreach ($traceItems as $traceItem) { 33 | $traceHash = array( 34 | 'file' => isset($traceItem['file']) ? $traceItem['file'] : 'null', 35 | 'line' => isset($traceItem['line']) ? $traceItem['line'] : 'null', 36 | 'function' => isset($traceItem['function']) ? $traceItem['function'] : 'null', 37 | 'args' => array(), 38 | ); 39 | 40 | if (!empty($traceItem['class'])) { 41 | $traceHash['class'] = $traceItem['class']; 42 | } 43 | 44 | if (!empty($traceItem['type'])) { 45 | $traceHash['type'] = $traceItem['type']; 46 | } 47 | 48 | if (!empty($traceItem['args'])) { 49 | foreach ($traceItem['args'] as $argsItem) { 50 | $traceHash['args'][] = \var_export($argsItem, true); 51 | } 52 | } 53 | 54 | $exceptionHash['trace'][] = $traceHash; 55 | } 56 | } 57 | 58 | return $exceptionHash; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /classes/framework/util/Serialize.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | 24 | /** 25 | * 获取数据 26 | * 27 | * @return mixed 28 | */ 29 | public function getModel() { 30 | return $this->model; 31 | } 32 | 33 | /** 34 | * 设置数据 35 | * 36 | * @param mixed $model 37 | */ 38 | public function setModel($model) { 39 | return $this->model = $model; 40 | } 41 | 42 | /** 43 | * 展示视图 44 | * 45 | */ 46 | public function display() { 47 | header("Content-Type: application/json; charset=utf-8"); 48 | echo json_encode($this->model); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /classes/framework/view/MsgPackView.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | } 23 | 24 | /** 25 | * 获取数据 26 | * 27 | * @return mixed 28 | */ 29 | public function getModel() { 30 | return $this->model; 31 | } 32 | 33 | /** 34 | * 设置数据 35 | * 36 | * @param mixed $model 37 | */ 38 | public function setModel($model) { 39 | return $this->model = $model; 40 | } 41 | 42 | /** 43 | * 展示视图 44 | * 45 | */ 46 | public function display() { 47 | header("Content-Type: application/octet-stream; charset=utf-8"); 48 | echo \msgpack_pack($this->model); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /classes/framework/view/SmartyView.php: -------------------------------------------------------------------------------- 1 | fileName = $fileName; 49 | $this->model = $model; 50 | } 51 | 52 | /** 53 | * 设置Smarty的配置信息 54 | * 55 | * @param SmartyConfiguration $config 56 | */ 57 | public static function setConfiguration(SmartyConfiguration $config) { 58 | self::$configuration = $config; 59 | } 60 | 61 | /** 62 | * 取得Smarty的配置信息 63 | * 64 | * @return SmartyConfiguration 65 | */ 66 | public static function getConfiguration() { 67 | return self::$configuration; 68 | } 69 | 70 | /** 71 | * 获取Smarty实例 72 | * 73 | * @return \Smarty 74 | */ 75 | private static function getSmarty() { 76 | if (!self::$smarty) { 77 | if (empty(self::$configuration)) { 78 | throw new \Exception("please set smarty configuration with SmartyView::setConfiguration()"); 79 | } 80 | 81 | include(self::$configuration->smartyPath . DIRECTORY_SEPARATOR . 'Smarty.class.php'); 82 | 83 | $smarty = new \Smarty(); 84 | $smarty->cache_dir = self::$configuration->cacheDir; 85 | $smarty->compile_dir = self::$configuration->compileDir; 86 | $smarty->template_dir = self::$configuration->templateDir; 87 | $smarty->config_dir = self::$configuration->configDir; 88 | $smarty->left_delimiter = '<{'; 89 | $smarty->right_delimiter = '}>'; 90 | 91 | self::$smarty = $smarty; 92 | } 93 | 94 | return self::$smarty; 95 | } 96 | 97 | /** 98 | * 获取数据 99 | * 100 | * @return mixed 101 | */ 102 | public function getModel() { 103 | return $this->model; 104 | } 105 | 106 | /** 107 | * 设置数据 108 | * 109 | * @param mixed $model 110 | */ 111 | public function setModel($model) { 112 | return $this->model = $model; 113 | } 114 | 115 | /** 116 | * 展示视图 117 | * 118 | */ 119 | public function display() { 120 | header("Content-Type: text/html; charset=utf-8"); 121 | 122 | $this->output(); 123 | } 124 | 125 | /** 126 | * 输出 127 | * 128 | */ 129 | public function output() { 130 | $smarty = self::getSmarty(); 131 | $smarty->assign($this->model); 132 | $smarty->display($this->fileName); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /classes/framework/view/StringView.php: -------------------------------------------------------------------------------- 1 | string = $string; 16 | } 17 | 18 | /** 19 | * 获取数据 20 | * 21 | * @return mixed 22 | */ 23 | public function getModel() { 24 | return $this->model; 25 | } 26 | 27 | /** 28 | * 设置数据 29 | * 30 | * @param mixed $model 31 | */ 32 | public function setModel($model) { 33 | return $this->model = $model; 34 | } 35 | 36 | /** 37 | * 展示视图 38 | * 39 | */ 40 | public function display() { 41 | header("Content-Type:text/plain; charset=utf-8"); 42 | echo $this->string; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /classes/socket/Chat.php: -------------------------------------------------------------------------------- 1 | chat_unames[$client_id]." 说: ".$msg."\r\n"; 25 | $data['from'] = $from; 26 | $data['to'] = $to; 27 | $data['msg'] = $msg; 28 | $data['type'] = 'msg'; 29 | $send = json_encode($data); 30 | if($to==0) $this->server->sendAll($client_id, $msg); 31 | else $this->server->send($client_id, $msg); 32 | } 33 | public function sysNotice($msg, $client_id = null) 34 | { 35 | $msg="系统信息\r\n".$msg."\r\n"; 36 | $data['msg'] = $msg; 37 | $data['type'] = 'sys'; 38 | $send = json_encode($data); 39 | if($client_id == null) { 40 | $this->server->sendAll($client_id,$msg); 41 | } else { 42 | $this->server->send($client_id, $msg); 43 | } 44 | } 45 | 46 | public function onRecive($client_id, $data) 47 | { 48 | $data = trim($data); 49 | $this->log($client_id.$data); 50 | $msg = explode(' ',$data,3); 51 | if($msg[0]=='/setname') 52 | { 53 | $uname = $msg[1]; 54 | foreach($this->chat_unames as $_uname) { 55 | if($_uname == $uname) { 56 | $this->sysNotice('此名字已存在', $client_id); 57 | return ; 58 | } 59 | } 60 | if(isset($this->chat_unames[$client_id])) 61 | { 62 | $this->sysNotice($this->chat_unames[$client_id]."名字变更为:{$uname}"); 63 | } 64 | else 65 | { 66 | $this->sysNotice("{$uname}来到了聊天室"); 67 | } 68 | $this->chat_unames[$client_id] = $uname; 69 | 70 | return; 71 | } elseif($msg[0]=='/quit') 72 | { 73 | $this->server->close($client_id); 74 | } 75 | 76 | if(empty($this->chat_unames[$client_id])) { 77 | $this->sysNotice("请设置用户名", $client_id); 78 | return; 79 | } 80 | 81 | if($msg[0]=='/sendto') 82 | { 83 | $to = (int)$msg[1]; 84 | $from = array_search($client_id,$this->chat_client); 85 | $content = ($msg[2]); 86 | if(isset($this->chat_client[$to])) $this->sendMsg($content,$from,$to,$client_id); 87 | else $this->server->send($client_id,"user is exists\r\n"); 88 | } 89 | elseif($msg[0]=='/getuser') 90 | { 91 | $userList = ""; 92 | foreach($this->chat_unames as $uname) { 93 | $userList.=$uname."\r\n"; 94 | } 95 | $this->sysNotice($userList, $client_id); 96 | } 97 | elseif($msg[0]=='/help') 98 | { 99 | $this->sysNotice($this->helpMsg, $client_id); 100 | } 101 | else 102 | { 103 | $from = array_search($client_id,$this->chat_client); 104 | $content = trim($data); 105 | if(!empty($content)) { 106 | $this->sendMsg($content, $from, 0, $client_id); 107 | } 108 | } 109 | } 110 | 111 | public function onStart() 112 | { 113 | $this->log('chat server start'); 114 | } 115 | 116 | public function onShutdown() 117 | { 118 | $this->log('chat server stop'); 119 | } 120 | 121 | public function onClose($client_id) 122 | { 123 | $uid = array_search($client_id,$this->chat_client); 124 | $this->log('client '.$client_id.': logout!'); 125 | if(isset($this->chat_unames[$client_id])) { 126 | $this->sysNotice($this->chat_unames[$client_id]."退出了聊天室"); 127 | unset($this->chat_unames[$client_id]); 128 | } 129 | } 130 | 131 | function onConnect($client_id) 132 | { 133 | $this->sysNotice($this->helpMsg, $client_id); 134 | $this->log($client_id.'connected'); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /classes/socket/Memcache.php: -------------------------------------------------------------------------------- 1 | time(), 9 | 'cmd_get'=>0, 10 | 'cmd_set'=>0, 11 | 'get_hits'=>0, 12 | 'get_misses'=>0, 13 | ); 14 | 15 | public function log($msg) { 16 | echo $msg."\r\n"; 17 | } 18 | 19 | 20 | public function onRecive($client_id, $data) 21 | { 22 | $ca = explode("\r\n", $data); 23 | $commands = explode(" ", $ca[0]); 24 | $cmd = $commands[0]; 25 | switch($cmd) { 26 | case 'get': 27 | $this->stats['cmd_get'] ++; 28 | $returnData = isset($this->dataArray[$commands[1]]) ? $commands[1]." ".$this->dataArray[$commands[1]] : "$commands[1] 0 0\r\n\r\n"; 29 | 30 | $this->server->send($client_id, "VALUE {$returnData}"); 31 | $this->server->send($client_id, "END\r\n"); 32 | break; 33 | case 'getMulti': 34 | break; 35 | case 'set': 36 | $this->stats['cmd_set'] ++; 37 | $this->dataArray[$commands[1]] = $commands[2]." ".$commands[4]." ".substr($data, strpos($data, "\r\n")); 38 | $this->server->send($client_id, "STORED\r\n"); 39 | break; 40 | case 'setMulti': 41 | break; 42 | case 'add': 43 | break; 44 | case 'delete': 45 | $this->stats['delete'] ++; 46 | if(isset($this->dataArray[$commands[1]])) { 47 | unset($this->dataArray[$commands[1]]); 48 | $this->server->send($client_id, "DELETED\r\n"); 49 | } else { 50 | $this->server->send($client_id, "NOT_FOUND\r\n"); 51 | } 52 | break; 53 | case 'stats': 54 | $sendData = ""; 55 | foreach($this->stats as $key=>$val) { 56 | if('uptime' == $key) { 57 | $sendData.= "STAT {$key} ".(time() - $val)."\r\n"; 58 | } elseif('time' == $key) { 59 | $sendData.= "STAT {$key} ".time()."\r\n"; 60 | } else { 61 | $sendData.= "STAT {$key} {$val}\r\n"; 62 | } 63 | } 64 | $this->server->send($client_id, $sendData); 65 | $this->server->send($client_id, "END\r\n"); 66 | break; 67 | } 68 | } 69 | 70 | public function onStart() 71 | { 72 | $this->log('chat server start'); 73 | } 74 | 75 | public function onShutdown() 76 | { 77 | $this->log('chat server stop'); 78 | } 79 | 80 | public function onClose($client_id) 81 | { 82 | $this->log('client '.$client_id.': logout!'); 83 | } 84 | 85 | function onConnect($client_id) 86 | { 87 | $this->log($client_id.'connected'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /inf/default/daemon.php: -------------------------------------------------------------------------------- 1 | '/var/run', //进程id存放目录 5 | 'pidFileName'=>'random_chat.pid',//进程id名 6 | 'verbose'=>fasle, //是否终终输出 7 | ); 8 | -------------------------------------------------------------------------------- /inf/default/define.php: -------------------------------------------------------------------------------- 1 | dispatch(); --------------------------------------------------------------------------------