├── Core ├── Handler.class.php ├── Async │ ├── FutureIntf.class.php │ ├── Future.class.php │ ├── ResponseFuture.class.php │ ├── PromiseContext.class.php │ ├── PromiseGroup.class.php │ ├── Timer.class.php │ ├── HttpClientFuture.class.php │ └── Promise.class.php ├── autoload.php ├── View.class.php └── Controller.class.php ├── MyController.class.php ├── Handler ├── Sync.class.php └── Index.class.php ├── run.php ├── MIT-LICENSE └── README.md /Core/Handler.class.php: -------------------------------------------------------------------------------- 1 | 'Handler_Index', 10 | '/sync' => 'Handler_Sync', 11 | ); 12 | } -------------------------------------------------------------------------------- /Core/Async/Future.class.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 12 | } 13 | public function run(Promise &$promise) { 14 | $cb = $this->callback; 15 | return $cb ( $promise ); 16 | } 17 | } -------------------------------------------------------------------------------- /Core/Async/ResponseFuture.class.php: -------------------------------------------------------------------------------- 1 | response = $response; 13 | } 14 | 15 | public function run(Promise &$promise) { 16 | $data = json_encode($promise->getData()); 17 | $this->response->end($data); 18 | //echo "Mem: ",\memory_get_usage() / 1024,"k \n"; 19 | $promise->accept (); 20 | } 21 | } -------------------------------------------------------------------------------- /Core/Async/PromiseContext.class.php: -------------------------------------------------------------------------------- 1 | data [$k] = $v; 12 | } 13 | public function merge($data){ 14 | if(is_array($data)){ 15 | $this->data = array_merge($this->data, $data); 16 | }elseif($data instanceof PromiseContext){ 17 | $this->data = array_merge($this->data, $data->data); 18 | } 19 | } 20 | public function get($k) { 21 | return $this->data [$k]; 22 | } 23 | public function getAll() { 24 | return $this->data; 25 | } 26 | } -------------------------------------------------------------------------------- /Handler/Sync.class.php: -------------------------------------------------------------------------------- 1 | end(123);return; 14 | $ctx = new PromiseContext (); 15 | $ctx->set('aaa','bbddddddddddddddddddddddddddddddddddddddddddddddddddddd 16 | ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssb'); 17 | 18 | Promise::create([ 19 | new ResponseFuture ($response) 20 | ])->start($ctx); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Core/View.class.php: -------------------------------------------------------------------------------- 1 | controller; 13 | $http = new \swoole_http_server($host, $port, SWOOLE_BASE); 14 | $http->on('request', array($this, 'onRequest')); 15 | $http->set([ 16 | 'worker_num' => 2, 17 | ]); 18 | $this->controller = new MyController(); 19 | $http->start(); 20 | } 21 | 22 | public function onRequest($request, $response){ 23 | //echo "worker onRequest\n"; 24 | $this->controller->run($request, $response); 25 | 26 | static $i=0; 27 | 28 | if($i++ >= 1000){ 29 | echo "----->Mem: ", memory_get_usage(), "b\n"; 30 | $i = 0; 31 | } 32 | } 33 | } 34 | 35 | $app = new App(); 36 | $app->start('', 9502); -------------------------------------------------------------------------------- /Core/Controller.class.php: -------------------------------------------------------------------------------- 1 | server['request_uri']; 11 | $handler = $this->getHandler($route); 12 | $handler->run($request, $response); 13 | } 14 | 15 | ////////////////////////////////////////////////////////////////// 16 | 17 | protected $handlerMap = array(); 18 | protected $handlerCache = array(); //将handler实例缓存住 19 | 20 | /** 21 | * @param String $route 22 | * @throws \Exception 23 | * @return \Core\Handler 24 | */ 25 | protected function getHandler($route){ 26 | if(!array_key_exists($route, $this->handlerCache)){ 27 | if(!array_key_exists($route, $this->handlerMap)){ 28 | $route = '/404'; 29 | } 30 | $handlerName = $this->handlerMap[$route]; 31 | $this->handlerCache[$route] = new $handlerName(); 32 | } 33 | return $this->handlerCache[$route]; 34 | } 35 | } -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 coooold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Core/Async/PromiseGroup.class.php: -------------------------------------------------------------------------------- 1 | nextPromise = $promiseGroup; 23 | $promiseGroup->subPromiseArray[] = $promise; 24 | } 25 | 26 | return $promiseGroup; 27 | } 28 | 29 | // ///////////////////////////////////////////// 30 | 31 | protected $phase = 0; //执行阶段 0:map 1:reduce 32 | protected $runCount = 0; 33 | protected function run(PromiseContext $context) { 34 | if($this->phase == 0){ 35 | $this->context = $context; 36 | $this->phase = 1; 37 | $this->runCount = count($this->subPromiseArray); 38 | foreach($this->subPromiseArray as $subPromise){ 39 | $subPromise->start ( $this->context ); 40 | unset($subPromise); 41 | } 42 | unset($this->subPromiseArray); 43 | }else{ 44 | $this->context->merge($context); 45 | $this->runCount --; 46 | if($this->runCount == 0){//都执行完了 47 | $this->accept(); 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Handler/Index.class.php: -------------------------------------------------------------------------------- 1 | then( 17 | new ResponseFuture ($response) 18 | )->start(new PromiseContext ()); 19 | } 20 | } 21 | 22 | 23 | //////////////////////////////////////////辅助类 Model Service 24 | 25 | class Service { 26 | static public function get($api, $params = array()) { 27 | $url = $api . '?' . http_build_query($params); 28 | $proxy=null; 29 | return Promise::create ( new HttpClientFuture ( $url, null, $proxy, 0.2) ); 30 | } 31 | } 32 | class Model { 33 | static public function getUrl($ret, $url, $params = array()) { 34 | return Service::get ( $url,$params )->then ( function ($promise) use($ret) { 35 | $data = $promise->get ( 'http_data' ); 36 | $promise->accept ([$ret=>$data]); 37 | } ); 38 | } 39 | 40 | static public function getUserInfo($ret, $usr){ 41 | $params = array(); 42 | $url = 'http://127.0.0.1:9502/sync'; 43 | //$url = 'http://192.112.121.122/store.php?id=3269'; 44 | //$url = 'http://localhost/'; 45 | 46 | return Service::get ( $url,$params )->then ( function ($promise) use($ret) { 47 | $data = $promise->get ( 'http_data' ); 48 | $promise->accept ([$ret=>$data]); 49 | } ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Core/Async/Timer.class.php: -------------------------------------------------------------------------------- 1 | $callback){ 36 | $callback(); 37 | unset(self::$sockSlotIndex[$sock]); 38 | unset(self::$eventSlots[self::$tick][$sock]); 39 | } 40 | 41 | self::$eventSlots[self::$tick] = array(); 42 | } 43 | 44 | static public function init(){ 45 | if (self::$timer === null){ 46 | for($i=0; $iurl = $url; 17 | $this->post = $post; 18 | if($proxy){ 19 | $this->proxy = $proxy; 20 | } 21 | $this->timeout = $timeout; 22 | } 23 | 24 | 25 | 26 | public function run(Promise &$promise) { 27 | $urlInfo = parse_url ( $this->url ); 28 | $timeout = $this->timeout; 29 | if(!isset($urlInfo ['port']))$urlInfo ['port'] = 80; 30 | 31 | $cli = new \swoole_http_client($urlInfo['host'], $urlInfo ['port']); 32 | $cli->set(array( 33 | 'timeout' => $timeout, 34 | 'keepalive' => 0, 35 | )); 36 | $cli->on ( "error", function($cli)use(&$promise){ 37 | Timer::del($cli->sock); 38 | $promise->accept(['http_data'=>null, 'http_error'=>'Connect error']); 39 | } ); 40 | $cli->on ( "close", function ($cli)use(&$promise) { 41 | } ); 42 | $cli->execute( $this->url, function ($cli)use(&$promise) { 43 | Timer::del($cli->sock); 44 | $cli->isDone = true; 45 | $promise->accept(['http_data'=>$cli->body]); 46 | } ); 47 | 48 | $cli->isConnected = false; 49 | 50 | if(!$cli->errCode){ 51 | Timer::add($cli->sock, $this->timeout, function()use($cli, &$promise){ 52 | @$cli->close(); 53 | if($cli->isConnected){ 54 | $promise->accept(['http_data'=>null, 'http_error'=>'Http client read timeout']); 55 | }else{ 56 | $promise->accept(['http_data'=>null, 'http_error'=>'Http client connect timeout']); 57 | } 58 | }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Core/Async/Promise.class.php: -------------------------------------------------------------------------------- 1 | future = $future; 15 | } 16 | static public function create($sth) { 17 | if (is_callable ( $sth )) { 18 | $future = new Future ( $sth ); 19 | return new self ( $future ); 20 | } elseif ($sth instanceof FutureIntf) { 21 | return new self ( $sth ); 22 | } elseif ($sth instanceof Promise) { 23 | return $sth; 24 | } elseif (is_array($sth)) { 25 | return PromiseGroup::create($sth); 26 | } else { 27 | throw new \Exception ( 'error sth type' ); 28 | } 29 | } 30 | 31 | public function then($sth) { 32 | if (is_callable ( $sth )) { 33 | $future = new Future ( $sth ); 34 | $nextPromise = new self ( $future ); 35 | $this->nextPromise = $nextPromise; 36 | $nextPromise->lastPromise = $this; 37 | return $nextPromise; 38 | } elseif ($sth instanceof FutureIntf) { 39 | $nextPromise = new self ( $sth ); 40 | $this->nextPromise = $nextPromise; 41 | $nextPromise->lastPromise = $this; 42 | return $nextPromise; 43 | } elseif ($sth instanceof Promise) { 44 | // 拿到的sth一定是尾promise,把头promise挂上主promise 45 | $headPromise = $sth->getHeadPromise (); 46 | $this->nextPromise = $headPromise; 47 | $headPromise->lastPromise = $this; 48 | return $sth; 49 | } elseif (is_array($sth)){ 50 | $nextPromise = PromiseGroup::create($sth); 51 | $this->nextPromise = $nextPromise; 52 | $nextPromise->lastPromise = $this; 53 | return $nextPromise; 54 | }else { 55 | throw new Exception ( 'error sth type' ); 56 | } 57 | } 58 | 59 | // 找到第一个promise然后执行 60 | public function start($context) { 61 | $headPromise = $this->getHeadPromise (); 62 | $headPromise->run ( $context ); 63 | unset($headPromise); 64 | } 65 | 66 | protected $accepted = false; 67 | // 成功后执行 68 | public function accept($ret = null) { 69 | if($this->accepted)return; //仅执行一次 70 | $this->accepted = true; 71 | 72 | if ($this->nextPromise !== null) { 73 | if(is_array($ret)){ 74 | $this->context->merge($ret); 75 | } 76 | $this->nextPromise->run ( $this->context ); 77 | unset($this->nextPromise); 78 | } 79 | } 80 | 81 | //设置上下文数据 82 | public function set($key, $val){ 83 | return $this->context->set($key, $val); 84 | } 85 | 86 | //获取上下文数据 87 | public function get($key){ 88 | return $this->context->get($key); 89 | } 90 | 91 | //获取全部上下文数据 92 | public function getData(){ 93 | return $this->context->getAll(); 94 | } 95 | 96 | // 失败后执行 97 | public function reject() { 98 | } 99 | 100 | // ///////////////////////////////////////////// 101 | 102 | // 取得第一个promise 103 | protected function getHeadPromise() { 104 | for($i = $this; $i->lastPromise != null; $i = $i->lastPromise) 105 | ; 106 | return $i; 107 | } 108 | protected function run(PromiseContext $context) { 109 | $this->context = $context; 110 | $ret = $this->future->run ( $this, $context ); 111 | unset($this->future); 112 | 113 | // 如果返回值是个promise,那么把后续的promise链条挂载到这个promise后面,然后继续执行 114 | if ($ret instanceof Promise) { 115 | $ret->nextPromise = $this->nextPromise; 116 | if ($this->nextPromise) { 117 | $this->nextPromise->lastPromise = $ret; 118 | } 119 | $ret->start ( $context ); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swPromise,基于swoole的PHP promise框架 2 | 3 | 在日常的使用场景中,PHP一般用作接口聚合层。 4 | 一个业务请求可能会串行的请求多个接口A->B->C,此时如果接口B的响应时间较慢(关键性业务,需要有较长的timeout等待时间),则会导致请求整体的时间过长,严重降低系统的响应能力。 5 | 考虑到这个业务场景下,进程的主要时间用在等待网络io返回。 6 | 如果能够使用异步编程的方式,则会极大的提升服务的吞吐量(NodeJS的优势)。 7 | 8 | 如果某接口响应时间超过往常,会导致php-fpm进程数急剧升高,从而导致大量cpu资源浪费在进程调度上面,甚至导致服务崩溃。swPromise框架是为了解决该问题而开发的。 9 | 10 | 11 | 异步非阻塞模式是实现高性能网络编程的一种方法。 12 | 传统上,为进行异步调用,会在代码中实现大量的回调函数,导致代码可读性与可维护性的急剧下降。 13 | 为了解决这个问题,主流方案有以下几种: 14 | 15 | - 自定义事件式方案 16 | - Promise/Deferred 17 | - 高阶函数篡改回调函数 18 | - 协程(Generator) 19 | 20 | [Swoole](http://www.swoole.com/)是PHP语言的高性能网络通信框架,提供了PHP语言的异步多线程服务器。 21 | swoole采用自定义事件式方案,为我们提供网络层基本封装。基于swoole,可以扩展出业务层的异步开发框架。 22 | 23 | [tsf (Tencent Server Framework)](https://github.com/tencent-php) 是腾讯公司推出的 PHP 协程方案,基于 Swoole+PHP Generator 实现的 Coroutine。 24 | 该框架使用协程模式,基于swoole与swoole framework开发。 25 | 实现了真正的异步非阻塞开发模式,同时具有极高的性能。 26 | 其核心代码来源于该文章[Cooperative multitasking using coroutines (in PHP!) ](http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html)。 27 | tsf使用了较为复杂的用户态任务调度逻辑,在腾讯openapi中使用。另外由于使用了swoole framework,略显重量级。 28 | 29 | 30 | swPromise的主要处理流程在Core\Async\Promise类中。 31 | 该类实现了基本的then方法,并通过对promise流程的延迟计算,保证了异步流程的动态控制能力。 32 | 该框架是一个非常基础的web框架,目前仅实现通用Future(通用延迟计算)、HttpClientFuture、ResponseFuture三个延迟计算类。 33 | 34 | 该框架需要配合[Swoole](https://github.com/swoole/swoole-src) master版本使用,编译参数./configure --enable-async-httpclient,开启异步http client。 35 | 36 | ## 演示代码 37 | 38 | ```php 39 | class Handler_Index extends \Core\Handler{ 40 | public function run($request, $response){ 41 | Promise::create ( Model::getUserInfo ( 'user1', 'haha' ) ) 42 | ->then (function(&$promise){ 43 | $user1 = $promise->get('user1'); 44 | if($user1){ 45 | return Model::getUserInfo ( 'user2', 'haha2' ) 46 | ->then(function(&$promise){ 47 | $user2 = $promise->get('user2'); 48 | $promise->accept(['user3'=>$user2['body']]); 49 | }); 50 | } 51 | else $promise->accept(); 52 | }) 53 | ->then ( Model::getUserInfo ( 'user4', 'haha4' ) ) 54 | ->then ( Model::getUserInfo ( 'user5', 'haha5' ) ) 55 | ->then ( new ResponseFuture ($response) ) 56 | ->start ( new PromiseContext () ); 57 | } 58 | } 59 | ``` 60 | 61 | 这段流程表明了,先获取haha这个用户的信息,写入上下文的user1字段中。 62 | 如果获取到了数据,再获取haha2这个用户的信息,写入上下文user2字段中。 63 | 并将user2的body字段放入user3字段中。然后获取haha4和haha5的信息。 64 | 最后将所有数据输出到网页。 65 | 66 | 可以看到,在第一个then中,通过if条件返回promise对象,实现了对异步流程的动态控制。 67 | 同样的,整个流程通过then串联起来,已经较为接近同步代码的书写了。 68 | 而使用回调的方式,代码会变得极为恐怖。 69 | 70 | 71 | ## 并行请求 72 | 73 | ```php 74 | class Handler_Index extends \Core\Handler{ 75 | public function run($request, $response){ 76 | Promise::create([ 77 | Model::getUserInfo ( 'user1', 'haha' ), 78 | Model::getUserInfo ( 'user2', 'haha2' ), 79 | ])->then( 80 | new ResponseFuture ($response) 81 | )->start(new PromiseContext ()); 82 | } 83 | } 84 | ``` 85 | 这个请求并行获得haha与hah2两个用户的数据,分布放到user1和user2两个字段中。 86 | 87 | ## 存在问题 88 | 其中Handler_Sync实现的就是该框架同步的使用方式。 89 | 另外,目前reject方法以及异常处理流程均没有实现,有兴趣的朋友可以自行扩展。 90 | 91 | 目前有一个比较严重的bug,如果大量http request没有完成就自行中断的话,会导致swoole http server发生错误,从而退出。在swoole前面放一个nginx就可以解决问题。 92 | 93 | ## 测试方法 94 | 启动 95 | 96 | php run.php 97 | 98 | 测试: 99 | 100 | ab -n 10000 -c 100 "http://localhost:9502/async" 101 | ab -n 10000 -c 100 "http://localhost:9502/sync" 102 | 103 | 经过测试,在后端接口响应性能有问题的情况下,swPromise可以同时处理大量连接,用很低的cpu负载等待接口数据返回。 104 | 105 | --- 106 | 107 | 引用资料: 108 | - [Swoole](http://www.swoole.com/) 109 | - [tsf (Tencent Server Framework)](https://github.com/tencent-php) 110 | - [Generator与异步编程](http://www.infoq.com/cn/articles/generator-and-asynchronous-programming/) 111 | - [Node.js回调黑洞全解:Async、Promise 和 Generato](http://zhuanlan.zhihu.com/FrontendMagazine/19750470) 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | --------------------------------------------------------------------------------