├── .gitignore ├── src ├── Exception │ ├── ConnectionCloseException.php │ └── ConnectionErrorException.php ├── Lib │ └── CoTimer.php ├── MySQL │ ├── CoMySQLConnector.php │ └── Connection.php ├── Coroutine │ ├── Promise.php │ └── CoroutineMan.php ├── CoWorker.php ├── Events │ ├── CoExtEvent.php │ └── CoStreamSelect.php └── Connection │ ├── CoTcpConnection.php │ └── CoTcpClient.php ├── composer.json ├── examples ├── example1 │ └── coWorkermanServer.php ├── example3 │ └── coWorkerMySQLtest1.php ├── example2 │ ├── coWorkermanServer.php │ ├── userClient.php │ ├── userClientFork.php │ └── asyncUserClient.php └── example5 │ ├── userClient.php │ ├── userClientFork.php │ ├── otherServer.php │ ├── otherServerFork.php │ └── coCartServer.php ├── LICENSE ├── README.en.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | composer.lock 7 | 8 | storage/*.key 9 | .env 10 | .phpunit.result.cache 11 | -------------------------------------------------------------------------------- /src/Exception/ConnectionCloseException.php: -------------------------------------------------------------------------------- 1 | =5.5", 19 | "workerman/workerman": "4.0.*", 20 | "react/mysql": "0.5.*" 21 | }, 22 | "minimum-stability": "dev" 23 | } 24 | -------------------------------------------------------------------------------- /src/Lib/CoTimer.php: -------------------------------------------------------------------------------- 1 | onConnect = function (CoTcpConnection $connection) { 14 | $clientAdd = "{$connection->getRemoteIp()}:{$connection->getRemotePort()}"; 15 | echo "New Connection, {$clientAdd}" . PHP_EOL; 16 | 17 | $connection->send('hello' . PHP_EOL); 18 | $re = yield from $connection->readAsync(CoTcpConnection::READ_BUFFER_SIZE); 19 | 20 | CoWorker::safeEcho( "I got: [{$re}]" . PHP_EOL); 21 | $connection->close(); 22 | }; 23 | 24 | // 运行worker 25 | CoWorker::runAll(); -------------------------------------------------------------------------------- /examples/example3/coWorkerMySQLtest1.php: -------------------------------------------------------------------------------- 1 | onMessage = function($connection, $data) { 13 | global $mysql; 14 | 15 | $mysql = new Connection(array( 16 | // 'host' => '192.63.0.1', // 不要写localhost 17 | 'host' => '192.63.0.14', // 不要写localhost 18 | 'dbname' => 'mysql', 19 | 'user' => 'root', 20 | 'password' => '123456', 21 | 'port' => '3306' 22 | )); 23 | 24 | $connected = yield from $mysql->connection(); 25 | $re = yield from $mysql->query('show databases'); 26 | 27 | CoWorker::safeEcho(json_encode($re) . PHP_EOL); 28 | }; 29 | 30 | CoWorker::runAll(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 PaulXu-cn 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 | -------------------------------------------------------------------------------- /examples/example2/coWorkermanServer.php: -------------------------------------------------------------------------------- 1 | count = 1; 15 | 16 | /** 17 | * 连接成功时 18 | * 19 | * @param CoTcpConnection $connection 20 | * @return Generator 21 | */ 22 | $worker->onConnect = function (CoTcpConnection $connection) { 23 | try { 24 | $conName = "{$connection->getRemoteIp()}:{$connection->getRemotePort()}"; 25 | echo PHP_EOL . "New Connection, {$conName} \n"; 26 | 27 | $re = yield from $connection->readAsync(1024); 28 | CoWorker::safeEcho('get request msg :' . $re . PHP_EOL ); 29 | 30 | yield from CoTimer::sleepAsync(1000 * 2); 31 | 32 | $connection->send(json_encode(array('productId' => 12, 're' =>true))); 33 | 34 | CoWorker::safeEcho('Response to :' . $conName . PHP_EOL . PHP_EOL); 35 | } catch (ConnectionCloseException $e) { 36 | CoWorker::safeEcho('Connection closed, ' . $e->getMessage() . PHP_EOL); 37 | } 38 | }; 39 | 40 | // 运行worker 41 | CoWorker::runAll(); -------------------------------------------------------------------------------- /examples/example2/userClient.php: -------------------------------------------------------------------------------- 1 | $method, 21 | "data" => array( 22 | 'productId' => $productId, 23 | ), 24 | 'noBlocking' => true 25 | ); 26 | 27 | $message = json_encode($data); 28 | 29 | fwrite(STDOUT, "send to server: $message , time: " . date('Y-m-d H:i:s') . "\n"); 30 | // 发出请求 31 | $len = @fwrite($socket, $message . PHP_EOL); 32 | if ($len === 0) { 33 | fwrite(STDOUT, "socket closed\n"); 34 | break; 35 | } 36 | 37 | // 读取响应 38 | // $msg = @fread($socket, 4096); 39 | // if ($msg) { 40 | // fwrite(STDOUT, "receive server: $msg client time : " . date('Y-m-d H:i:s') . ".\n"); 41 | // } elseif (feof($socket)) { 42 | // fwrite(STDOUT, "socket closed time: " . date('Y-m-d H:i:s') . "\n"); 43 | // break; 44 | // } 45 | 46 | sleep(1); 47 | 48 | // 一个请求完毕,关闭socket 49 | fwrite(STDOUT, "close connection...\n"); 50 | fclose($socket); 51 | } 52 | -------------------------------------------------------------------------------- /src/MySQL/CoMySQLConnector.php: -------------------------------------------------------------------------------- 1 | $method, 22 | "data" => array( 23 | 'productId' => $productId, 24 | ), 25 | 'noBlocking' => true 26 | ); 27 | 28 | $message = json_encode($data); 29 | 30 | fwrite(STDOUT, "send to server: $message , time: " . date('Y-m-d H:i:s') . "\n"); 31 | // 发出请求 32 | $len = @fwrite($socket, $message . PHP_EOL); 33 | if ($len === 0) { 34 | fwrite(STDOUT, "socket closed\n"); 35 | } 36 | 37 | // 读取响应 38 | $msg = @fread($socket, 4096); 39 | if ($msg) { 40 | fwrite(STDOUT, "receive server: $msg client time : " . date('Y-m-d H:i:s') . ".\n"); 41 | } elseif (feof($socket)) { 42 | fwrite(STDOUT, "socket closed time: " . date('Y-m-d H:i:s') . "\n"); 43 | } 44 | 45 | // 一个请求完毕,关闭socket 46 | fwrite(STDOUT, "close connection...\n"); 47 | fclose($socket); 48 | } 49 | 50 | function forkMe($ttl) 51 | { 52 | if (0 > $ttl) { 53 | return; 54 | } 55 | $pid = pcntl_fork(); 56 | if (0 > $pid) { 57 | exit(); 58 | } elseif ($pid > 0) { 59 | sleep(1); 60 | forkMe(-- $ttl); 61 | } elseif (0 == $pid) { 62 | request(); 63 | } 64 | if ($pid > 0) { 65 | pcntl_wait($status); 66 | } 67 | } 68 | 69 | forkMe(3); 70 | -------------------------------------------------------------------------------- /examples/example2/userClientFork.php: -------------------------------------------------------------------------------- 1 | $method, 22 | "data" => array( 23 | 'productId' => $productId, 24 | ), 25 | 'noBlocking' => true 26 | ); 27 | 28 | $message = json_encode($data); 29 | 30 | fwrite(STDOUT, "send to server: $message , time: " . date('Y-m-d H:i:s') . "\n"); 31 | // 发出请求 32 | $len = @fwrite($socket, $message . PHP_EOL); 33 | if ($len === 0) { 34 | fwrite(STDOUT, "socket closed\n"); 35 | } 36 | 37 | // 读取响应 38 | $msg = @fread($socket, 4096); 39 | if ($msg) { 40 | fwrite(STDOUT, "receive server: $msg client time : " . date('Y-m-d H:i:s') . ".\n"); 41 | } elseif (feof($socket)) { 42 | fwrite(STDOUT, "socket closed time: " . date('Y-m-d H:i:s') . "\n"); 43 | } 44 | 45 | // 一个请求完毕,关闭socket 46 | fwrite(STDOUT, "close connection...\n"); 47 | fclose($socket); 48 | } 49 | 50 | function forkMe($ttl) 51 | { 52 | if (0 > $ttl) { 53 | return; 54 | } 55 | $pid = pcntl_fork(); 56 | if (0 > $pid) { 57 | exit(); 58 | } elseif ($pid > 0) { 59 | sleep(1); 60 | forkMe(-- $ttl); 61 | } elseif (0 == $pid) { 62 | request(); 63 | } 64 | if ($pid > 0) { 65 | pcntl_wait($status); 66 | } 67 | } 68 | 69 | forkMe(4); 70 | -------------------------------------------------------------------------------- /examples/example5/userClientFork.php: -------------------------------------------------------------------------------- 1 | $method, 22 | "data" => array( 23 | 'productId' => $productId, 24 | ), 25 | 'noBlocking' => true 26 | ); 27 | 28 | $message = json_encode($data); 29 | 30 | fwrite(STDOUT, "send to server: $message , time: " . date('Y-m-d H:i:s') . "\n"); 31 | // 发出请求 32 | $len = @fwrite($socket, $message . PHP_EOL); 33 | if ($len === 0) { 34 | fwrite(STDOUT, "socket closed\n"); 35 | } 36 | 37 | // 读取响应 38 | $msg = @fread($socket, 4096); 39 | if ($msg) { 40 | fwrite(STDOUT, "receive server: $msg client time : " . date('Y-m-d H:i:s') . ".\n"); 41 | } elseif (feof($socket)) { 42 | fwrite(STDOUT, "socket closed time: " . date('Y-m-d H:i:s') . "\n"); 43 | } 44 | 45 | // 一个请求完毕,关闭socket 46 | fwrite(STDOUT, "close connection...\n"); 47 | fclose($socket); 48 | } 49 | 50 | function forkMe($ttl) 51 | { 52 | if (0 > $ttl) { 53 | return; 54 | } 55 | $pid = pcntl_fork(); 56 | if (0 > $pid) { 57 | exit(); 58 | } elseif ($pid > 0) { 59 | sleep(1); 60 | forkMe(-- $ttl); 61 | } elseif (0 == $pid) { 62 | request(); 63 | } 64 | if ($pid > 0) { 65 | pcntl_wait($status); 66 | } 67 | } 68 | 69 | forkMe(5); 70 | -------------------------------------------------------------------------------- /examples/example2/asyncUserClient.php: -------------------------------------------------------------------------------- 1 | onWorkerStart = function($task) { 14 | for ($i = 0; $i < 9; $i++) { 15 | $connection_to_baidu = new AsyncTcpConnection('tcp://127.0.0.1:8080'); 16 | // 当连接建立成功时,发送http请求数据 17 | $connection_to_baidu->onConnect = function ($connection) { 18 | echo "connect success\n"; 19 | 20 | $method = 'cart'; 21 | $productId = rand(100, 1000); 22 | $data = array( 23 | "method" => $method, 24 | "data" => array( 25 | 'productId' => $productId, 26 | ), 27 | 'noBlocking' => true 28 | ); 29 | $message = json_encode($data); 30 | 31 | 32 | Worker::safeEcho( "send meg: [{$message}]" . PHP_EOL); 33 | $connection->send($message . PHP_EOL); 34 | }; 35 | 36 | $connection_to_baidu->onMessage = function ($connection, $http_buffer) { 37 | Worker::safeEcho( "get meg: [{$http_buffer}]" . PHP_EOL); 38 | // echo $http_buffer . PHP_EOL; 39 | // $connection->close(); 40 | }; 41 | 42 | $connection_to_baidu->onClose = function ($connection) { 43 | echo "connection closed\n"; 44 | }; 45 | 46 | $connection_to_baidu->onError = function ($connection, $code, $msg) { 47 | echo "Error code:$code msg:$msg\n"; 48 | }; 49 | 50 | $connection_to_baidu->connect(); 51 | // sleep(1); 52 | } 53 | 54 | Worker::stopAll(); 55 | }; 56 | 57 | // 运行worker 58 | Worker::runAll(); -------------------------------------------------------------------------------- /examples/example5/otherServer.php: -------------------------------------------------------------------------------- 1 | 20; // 这里不是每一次都是返回的成功,有20%失败 42 | $reMsg = array('method' => $method, 'data' => array('productId' => $productId, 're' => $status)); 43 | } else { 44 | $reMsg = array('method' => $method, 'data' => array('productId' => $productId, 're' => false)); 45 | } 46 | 47 | // 这里休眠 xs,模拟处理耗时 48 | sleep($sleepTime); 49 | 50 | // 响应请求 51 | $json = json_encode($reMsg); 52 | @fwrite($client, $json); 53 | fwrite(STDOUT, "response :" . $json . " time: " . date('Y-m-d H:i:s') . "\n"); 54 | // 关闭socket 55 | fclose($client); 56 | break; 57 | } elseif (feof($client)) { 58 | fwrite(STDOUT, "client:" . (int)$client . " disconnected!\n"); 59 | fclose($client); 60 | break; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /examples/example5/otherServerFork.php: -------------------------------------------------------------------------------- 1 | $pid) { 32 | exit(); 33 | } elseif ($pid > 0) { 34 | 35 | } elseif (0 == $pid) { 36 | 37 | 38 | $msg = @fread($client, 4096); 39 | if ($msg) { 40 | fwrite(STDOUT, "\nreceive client: $msg " . date('Y-m-d H:i:s') . " \n"); 41 | // 解析请求 42 | $request = json_decode($msg, true); 43 | // 获取产品ID 44 | $productId = $request['data']['productId']; 45 | // 检查发放是否是预期 46 | if ($method == $request['method']) { 47 | $randInt = rand(0, 100); 48 | $status = $randInt > 20; // 这里不是每一次都是返回的成功,有20%失败 49 | $reMsg = array('method' => $method, 'data' => array('productId' => $productId, 're' => $status)); 50 | } else { 51 | $reMsg = array('method' => $method, 'data' => array('productId' => $productId, 're' => false)); 52 | } 53 | 54 | // 这里休眠 xs,模拟处理耗时 55 | sleep($sleepTime); 56 | 57 | // 响应请求 58 | $json = json_encode($reMsg); 59 | @fwrite($client, $json); 60 | fwrite(STDOUT, "response :" . $json . " time: " . date('Y-m-d H:i:s') . "\n"); 61 | // 关闭socket 62 | fclose($client); 63 | } elseif (feof($client)) { 64 | fwrite(STDOUT, "client:" . (int)$client . " disconnected!\n"); 65 | fclose($client); 66 | } 67 | 68 | exit(); 69 | } 70 | } 71 | 72 | if ($pid > 0) { 73 | pcntl_wait($status); 74 | } -------------------------------------------------------------------------------- /src/MySQL/Connection.php: -------------------------------------------------------------------------------- 1 | connection = $factory->createLazyConnection($dsn); 43 | } 44 | 45 | /** 46 | * @return \Generator|mixed 47 | */ 48 | public function connection() 49 | { 50 | $coId = CoWorker::getCurrentCoId(); 51 | $re = yield $this->connection->ping()->then(function () use ($coId) { 52 | echo 'OK' . PHP_EOL; 53 | CoWorker::coSend($coId, null, true); 54 | }, function (\Exception $e) use ($coId) { 55 | echo 'Error: ' . $e->getMessage() . PHP_EOL; 56 | CoWorker::coSend($coId, null, false); 57 | }); 58 | return $re['data']; 59 | } 60 | 61 | /** 62 | * @param $sql 63 | * @return \Generator|QueryResult|null 64 | */ 65 | public function query($sql) 66 | { 67 | $coId = CoWorker::getCurrentCoId(); 68 | $re = yield $this->connection->query($sql)->then(function (QueryResult $command) use ($coId) { 69 | /* 70 | * $resultRows = array('resultFields' => array('filed1', 'field2' ...), 71 | * 'resultRows' => array(0 => array(), 1 => array(), 2 => array()), 72 | * 'insertId' => integer, 'affectedRows' => integer); 73 | */ 74 | CoWorker::coSend($coId, null, $command); 75 | }, function (\Exception $error) use ($coId) { 76 | // the query was not executed successfully 77 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 78 | CoWorker::coSend($coId, null, null); 79 | }); 80 | return $re['data']; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/Coroutine/Promise.php: -------------------------------------------------------------------------------- 1 | $gen) { 27 | if ($gen instanceof \Generator) { 28 | $re = $genRe = $gen->current(); 29 | $genInfos[$key] = array('gen' => $gen, 'data' => $re); 30 | } 31 | } 32 | 33 | $ignoreKey = $childReturn = array(); 34 | do { 35 | $collection = array(); 36 | $deepGen = array(); 37 | $finishedC = 0; 38 | 39 | // 都current了,判断下getReturn 40 | foreach ($genInfos as $key => $info) { 41 | try { 42 | /** 43 | * @var \Generator|mixed $return 44 | */ 45 | $return = null; 46 | if (isset($info['return'])) { 47 | $return = $info['return']; 48 | } else { 49 | $return = $info['gen']->getReturn(); 50 | $genInfos[$key]['return'] = $return; 51 | $ignoreKey[] = $key; 52 | } 53 | if ($return instanceof \Generator) { 54 | $deepGen[$key] = $return; 55 | } else { 56 | $result[$key] = $return; 57 | $finishedC ++; 58 | } 59 | } catch (\Exception $e) { 60 | // 那就是没完事, 接受输入 61 | $genOrData = $info['gen']->current(); 62 | if ($genOrData instanceof \Generator) { 63 | $deepGen[$key] = $genOrData; 64 | } else { 65 | $collection[$key] = yield $info['data']; 66 | } 67 | } 68 | } 69 | 70 | // 接收数据搜集完了,就开始放入对应生成器 71 | foreach ($genInfos as $key => $info) { 72 | if (in_array($key, $ignoreKey)) 73 | continue; 74 | 75 | if ($info['data'] instanceof \Generator) { 76 | $deepGen[$key] = $info['gen']; 77 | } else { 78 | foreach ($collection as $data) { 79 | $socket = $data['socket']; 80 | $genSocket = $info['data']; 81 | if ($socket === $genSocket) { 82 | $info['gen']->send($data); 83 | } 84 | } 85 | } 86 | } 87 | 88 | if (!empty($deepGen)) { 89 | $childReturn = yield from self::all($deepGen, 'Promise::all'); 90 | 91 | foreach ($genInfos as $key => $info) { 92 | if (isset($childReturn[$key])) { 93 | $result[$key] = $childReturn[$key]; 94 | $genInfos[$key]['return'] = $childReturn[$key]; 95 | } 96 | } 97 | } 98 | 99 | if ($finishedC >= count($genInfos)) 100 | break; 101 | } while (true); 102 | 103 | return empty($result) ? $childReturn : $result; 104 | } 105 | 106 | /** 107 | * @param $gen 108 | * 109 | * @return \Generator|mixed 110 | * @throws \Exception 111 | */ 112 | public static function wait($gen, $func = '') 113 | { 114 | $result = array(); 115 | $deepGen = array(); 116 | /** 117 | * @var \Generator $call 118 | */ 119 | $call = null; 120 | /** 121 | * @var \Generator $gen 122 | */ 123 | if ($gen instanceof \Generator) { 124 | do { 125 | $socket = $gen->current(); 126 | $data = yield $socket; 127 | $gen->send($data); 128 | 129 | try { 130 | $return = $gen->getReturn(); 131 | 132 | if ($return instanceof \Generator) { 133 | $deepGen = $return; 134 | } else { 135 | $result = $return; 136 | } 137 | // 已到return,可以跳出 138 | break; 139 | } catch (\Exception $e) { 140 | // 还没运行完事, 还不能跳出 141 | } 142 | } while (true); 143 | 144 | if (!empty($deepGen)) { 145 | $result = yield from self::wait($deepGen, 'Promise:await'); 146 | } 147 | return $result; 148 | } 149 | return $result; 150 | } 151 | 152 | public static function race($params, $func = '') 153 | { 154 | 155 | } 156 | 157 | public static function any($params, $func = '') 158 | { 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /examples/example5/coCartServer.php: -------------------------------------------------------------------------------- 1 | count = 1; 19 | 20 | /** 21 | * 发起rpc检查统一接口 22 | * 23 | * @param $host 24 | * @param $port 25 | * @param $method 26 | * @param $data 27 | * @param bool $noBlocking 28 | * @return bool|false|string 29 | */ 30 | function checkClientAsync($connection, $host, $port, $method, $data, $noBlocking = true) 31 | { 32 | CoWorker::safeEcho('try to connect to ' . "tcp://$host:$port" . PHP_EOL); 33 | $con = new CoTcpClient("tcp://$host:$port"); 34 | $ifCon = $con->connectAsync($connection); 35 | $ifCon = (yield from Promise::wait($ifCon, __FUNCTION__)); 36 | if (!$ifCon) { 37 | return null; 38 | } 39 | CoWorker::safeEcho( "connected to other server, status : [{$ifCon}]" . PHP_EOL . PHP_EOL); 40 | 41 | $message = json_encode([ 42 | "method" => $method, 43 | "data" => $data 44 | ]); 45 | 46 | $re = $con->sendAsync($message); 47 | return $re; 48 | } 49 | 50 | function checkInventoryAsync($connection, $productId, $noBlocking = true) 51 | { 52 | // client.php 53 | $host = "127.0.0.1"; 54 | $port = 8081; 55 | 56 | $data = array('productId' => $productId); 57 | 58 | return (yield from checkClientAsync($connection, $host, $port, 'inventory', $data, $noBlocking)); 59 | } 60 | 61 | function checkProductAsync($connection, $productId, $noBlocking = true) 62 | { 63 | // client.php 64 | $host = "127.0.0.1"; 65 | $port = 8082; 66 | 67 | $data = array('productId' => $productId); 68 | 69 | return (yield from checkClientAsync($connection, $host, $port, 'product', $data, $noBlocking)); 70 | } 71 | 72 | function checkPromoAsync($connection, $productId, $noBlocking = true) 73 | { 74 | // client.php 75 | $host = "127.0.0.1"; 76 | $port = 8083; 77 | 78 | $data = array('productId' => $productId); 79 | 80 | return (yield from checkClientAsync($connection, $host, $port, 'promo', $data, $noBlocking)); 81 | } 82 | 83 | $worker->onConnect = function (CoTcpConnection $connection) { 84 | try { 85 | // 接收用户请求 86 | $clientAdd = "{$connection->getRemoteIp()}:{$connection->getRemotePort()}"; 87 | echo "New Connection, {$clientAdd}\n"; 88 | $request = yield from $connection->readAsync(CoTcpConnection::READ_BUFFER_SIZE); 89 | CoWorker::safeEcho('user request msg : ' . $request . PHP_EOL); 90 | $reqData = json_decode($request, true); 91 | $productId = mt_rand(10, 50); 92 | if (isset($reqData['data']['productId'])) { 93 | $productId = $reqData['data']['productId']; 94 | } 95 | 96 | // 开始请求其他服务 97 | $re = checkInventoryAsync($connection, $productId, true); 98 | $re2 = checkProductAsync($connection, $productId, true); 99 | $re3 = checkPromoAsync($connection, $productId, true); 100 | 101 | // 睡一会儿,模拟程序耗时 102 | yield from CoTimer::sleepAsync(1000); 103 | 104 | // 顺序异步执行 ------------------------------------------------------------------+ 105 | // $re3 = yield from Promise::wait($re3); 106 | // $re = yield from Promise::wait($re, 'onConnect'); 107 | // $re2 = yield from Promise::wait($re2, 'onConnect'); 108 | // or 同时异步执行 ---------------------------------------------------------------+ 109 | list($re, $re2) = yield from Promise::all(array($re, $re2), 'onConnect'); 110 | // 选择结束 ----------------------------------------------------------------------+ 111 | 112 | // 数据处理以及校验 113 | CoWorker::safeEcho('received from inventory, product, msg is: ' . $re . '; ' . $re2 . PHP_EOL); 114 | $check = 'failed'; 115 | $data1 = json_decode($re, true); 116 | $data2 = json_decode($re2, true); 117 | if (isset($data1['data']['re']) && isset($data2['data']['re'])) { 118 | $check = $data1['data']['re'] && $data2['data']['re']; 119 | } 120 | 121 | // 响应客户端的请求 122 | $response = json_encode(array('productId' => $productId, 'check' => $check)); 123 | $connection->send($response); 124 | CoWorker::safeEcho('response user client: ' . $response . PHP_EOL . PHP_EOL); 125 | } catch (ConnectionCloseException $e) { 126 | CoWorker::safeEcho('connection closed: ' . $check . PHP_EOL); 127 | } catch (ConnectionErrorException $e) { 128 | CoWorker::safeEcho('connection error, msg: ' . $e->getMessage() . PHP_EOL); 129 | } catch (\Exception $e) { 130 | CoWorker::safeEcho('Exception caught, msg: ' . $e->getMessage() . PHP_EOL); 131 | } 132 | }; 133 | 134 | /** 135 | * 被动接收到的消息 136 | * 137 | * @param CoTcpConnection $connection 138 | * @param string $data 139 | */ 140 | $worker->onMessage = function(CoTcpConnection $connection, $data) 141 | { 142 | $re = $connection->send("hello"); 143 | }; 144 | 145 | // 运行worker 146 | CoWorker::runAll(); -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | English | [中文](README.md) 2 | # CoWorkerman 3 | Coroutine Workerman. Synchronous coding, asynchronous execution 4 | 5 | > Implemented by `yield`,`yield from`. 6 | 7 | ## Requires 8 | PHP 5.5 or Higher 9 | 10 | A POSIX compatible operating system (Linux, OSX, BSD) 11 | 12 | POSIX and PCNTL extensions should be enabled 13 | 14 | Event extension recommended for better performance, like: Event, ev, libevent 15 | 16 | ## Install 17 | 18 | ```shell script 19 | composer require paulxu-cn/co-workerman:0.0.* 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | ### A simple coroutine TCP server, with a timer 25 | 26 | ```php 27 | onConnect = function (CoTcpConnection $connection) { 40 | echo "New Connection, {$connection->getLocalIp()}" . PHP_EOL; 41 | 42 | yield from CoTimer::sleepAsync(1000); // yield, and go back after 1 second. 43 | 44 | $re = yield from $connection->sendAsync('hello'); 45 | 46 | CoWorker::safeEcho( "get re: [{$re}]" . PHP_EOL); 47 | }; 48 | 49 | // 运行worker 50 | CoWorker::runAll(); 51 | ``` 52 | 53 | * start the server 54 | ```shell script 55 | ## start the CoWorkerman Server 56 | $ php ./simpleTimerServer.php start 57 | ## talnet it 58 | $ talnet 127.0.0.1:8686 59 | > hi 60 | hello 61 | ``` 62 | 63 | ### Coroutine TCP server, client. 64 | 65 | ```php 66 | onConnect = function (CoTcpConnection $connection) { 80 | echo "New Connection, {$connection->getLocalIp()} \n"; 81 | 82 | $re = checkInventoryAsync($connection, rand(10, 20), true); 83 | $re2 = checkProductAsync($connection, rand(10, 20), true); 84 | 85 | // 顺序异步执行 ------------------------------------------------------------------+ 86 | // $re = yield from Promise::wait($re, 'onConnect'); 87 | // $re2 = yield from Promise::wait($re2, 'onConnect'); 88 | // or 同时异步执行 ---------------------------------------------------------------+ 89 | list($re, $re2) = yield from Promise::all(array($re, $re2), 'onConnect'); 90 | // 选择结束 ----------------------------------------------------------------------+ 91 | 92 | if (isset($re['re']) && isset($re2['re'])) { 93 | $check = $re['re'] && $re2['re']; 94 | } 95 | $connection->sendAsync($check); 96 | }; 97 | 98 | /** 99 | * 发起rpc检查统一接口 100 | * 101 | * @param $host 102 | * @param $port 103 | * @param $method 104 | * @param $data 105 | * @param bool $noBlocking 106 | * @return bool|false|string 107 | */ 108 | function checkClientAsync($connection, $host, $port, $method, $data, $noBlocking = true) 109 | { 110 | CoWorker::safeEcho('try to connect to ' . "tcp://$host:$port" . PHP_EOL); 111 | 112 | $con = new CoTcpClient("tcp://$host:$port"); 113 | $ifCon = $con->connectAsync($connection); 114 | $ifCon = yield from Promise::wait($ifCon, __FUNCTION__); 115 | 116 | if (!$ifCon) { 117 | return null; 118 | } 119 | CoWorker::safeEcho( "\nconnected to server, re: [{$ifCon}]\n" . PHP_EOL); 120 | $message = json_encode(["method" => $method, "data" => $data]); 121 | $re = $con->sendAsync($message); 122 | return $re; 123 | } 124 | 125 | function checkInventoryAsync($connection, $productId, $noBlocking = true) 126 | { 127 | $host = "127.0.0.1"; 128 | $port = 8081; 129 | $data = array('productId' => $productId); 130 | return yield from checkClientAsync($connection, $host, $port, 'inventory', $data, $noBlocking); 131 | } 132 | 133 | function checkProductAsync($connection, $productId, $noBlocking = true) 134 | { 135 | $host = "127.0.0.1"; 136 | $port = 8082; 137 | $data = array('productId' => $productId); 138 | return yield from checkClientAsync($connection, $host, $port, 'product', $data, $noBlocking); 139 | } 140 | 141 | // 运行worker 142 | CoWorker::runAll(); 143 | ``` 144 | 145 | ### Coroutine MySQL Client 146 | 147 | ```php 148 | onMessage = function($connection, $data) { 159 | global $mysql; 160 | 161 | $mysql = new Connection(array( 162 | // 'host' => '192.63.0.1', // 不要写localhost 163 | 'host' => '192.63.0.14', // 不要写localhost 164 | 'dbname' => 'mysql', 165 | 'user' => 'root', 166 | 'password' => '123456', 167 | 'port' => '3306' 168 | )); 169 | 170 | $connected = yield from $mysql->connection(); 171 | $re = yield from $mysql->query('show databases'); 172 | 173 | CoWorker::safeEcho(json_encode($re) . PHP_EOL); 174 | }; 175 | 176 | CoWorker::runAll(); 177 | ``` 178 | 179 | ## Tips 180 | 181 | The project is still in testing, please do not use it in production environment. 182 | 183 | It is cloned from [workerman](https://github.com/walkor/Workerman) -------------------------------------------------------------------------------- /src/Coroutine/CoroutineMan.php: -------------------------------------------------------------------------------- 1 | send(array('socket' => $socket, 'data' => $data)); 89 | } 90 | } 91 | 92 | /** 93 | * @param integer $coId 94 | * @param resource|null $socket 95 | * @param \Exception $exception 96 | */ 97 | public static function coThrow($coId, $socket, $exception) 98 | { 99 | /** 100 | * @var \Generator $gen 101 | */ 102 | $gen = self::$_coroutines[$coId]; 103 | if ($gen instanceof \Generator) { 104 | $gen->throw($exception); 105 | } 106 | } 107 | 108 | /** 109 | * @param resource $socket 110 | * @return mixed|null 111 | */ 112 | protected static function getCoIdBySocket($socket) 113 | { 114 | $coId = -1; 115 | if (isset(self::$_connectionSocketToCoId[(string)$socket])) { 116 | $coId = self::$_connectionSocketToCoId[(string)$socket]; 117 | } 118 | return $coId; 119 | } 120 | 121 | /** 122 | * @param resource $socket 123 | * 124 | * @return \Generator|null 125 | */ 126 | protected static function getGenBySocket($socket) 127 | { 128 | $coId = self::getCoIdBySocket($socket); 129 | if (isset(self::$_coroutines[$coId])) { 130 | /** 131 | * @var \Generator $gen 132 | */ 133 | $gen = self::$_coroutines[$coId]; 134 | return $gen; 135 | } else { 136 | return null; 137 | } 138 | } 139 | 140 | public static function genCoIdBySocket($socket) 141 | { 142 | 143 | } 144 | 145 | /** 146 | * @param \Generator $generator 147 | * @param integer $coId 148 | */ 149 | public static function addCoroutine($generator, $coId = null) 150 | { 151 | if (null === $coId) { 152 | $coId = self::genCoId(); 153 | } 154 | self::$_coroutines[$coId] = $generator; 155 | } 156 | 157 | /** 158 | * 移除协程 159 | * 160 | * @param integer $coId 161 | * 162 | * @return boolean 163 | */ 164 | public static function removeCoroutine($coId) 165 | { 166 | if (isset(self::$_coroutines[$coId])) { 167 | // 移除生成器 168 | unset(self::$_coroutines[$coId]); 169 | // 移除生成器ID 170 | unset(self::$_coIds[$coId]); 171 | // 回收生成器ID 172 | self::$_recycleCoIds[$coId] = $coId; 173 | return true; 174 | } else { 175 | return false; 176 | } 177 | } 178 | 179 | /** 180 | * 保存当前socket 对应的 生成器 181 | * 182 | * @param resource $socket 183 | */ 184 | public static function setCoIdBySocket($socket, $coId = null) 185 | { 186 | $resId = (string)$socket; 187 | if (isset(self::$_connectionSocketToCoId[$resId])) 188 | return; 189 | if (null === $coId) { 190 | $coId = self::getCurrentCoId(); 191 | } 192 | self::$_connectionSocketToCoId[$resId] = $coId; 193 | } 194 | 195 | /** 196 | * 通过 socket 移除相关生成器 197 | * 198 | * @param resource $socket 199 | * @return bool 200 | */ 201 | protected static function removeGeneratorBySocket($socket) 202 | { 203 | $coId = self::getCoIdBySocket($socket); 204 | return self::removeCoroutine($coId); 205 | } 206 | 207 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](./README.en.md) | 中文 2 | # CoWorkerman 3 | 协程版 Workerman. 同步编码,异步执行 4 | 5 | > 使用PHP原生的 `yield`,`yield from` 实现,协程这块功能没有依赖其他拓展. 6 | 7 | ## 环境要求 8 | 9 | 至少PHP v5.5 10 | 11 | 支持`POSIX`规范的兼容性系统(Linux, OSX, BSD) 12 | 13 | `POSIX` 和 `PCNTL` 这两个模块需要在PHP编译期间就启用。 14 | 15 | 为了更好的性能,推荐安装事件驱动拓展,如:Event,ev,libevent. 16 | 17 | ## 安装 18 | 19 | ```shell script 20 | composer require paulxu-cn/co-workerman:0.0.* 21 | ``` 22 | 23 | ## 基本使用 24 | 25 | ### 一个带有定时器的简单TCP服务 26 | 27 | ```php 28 | onConnect = function (CoTcpConnection $connection) { 41 | echo "New Connection, {$connection->getLocalIp()}" . PHP_EOL; 42 | 43 | yield from CoTimer::sleepAsync(1000); // yield, and go back after 1 second. 44 | 45 | $re = yield from $connection->sendAsync('hello'); 46 | 47 | CoWorker::safeEcho( "get re: [{$re}]" . PHP_EOL); 48 | }; 49 | 50 | // 运行worker 51 | CoWorker::runAll(); 52 | ``` 53 | 54 | * start the server 55 | ```shell script 56 | ## start the CoWorkerman Server 57 | $ php ./simpleTimerServer.php start 58 | ## talnet it 59 | $ talnet 127.0.0.1:8686 60 | > hi 61 | hello 62 | ``` 63 | 64 | ### TCP服务中,使用协程TCP客户端 65 | 66 | ```php 67 | onConnect = function (CoTcpConnection $connection) { 81 | echo "New Connection, {$connection->getLocalIp()} \n"; 82 | 83 | $re = checkInventoryAsync($connection, rand(10, 20), true); 84 | $re2 = checkProductAsync($connection, rand(10, 20), true); 85 | 86 | // 顺序异步执行 ------------------------------------------------------------------+ 87 | // $re = yield from Promise::wait($re, 'onConnect'); 88 | // $re2 = yield from Promise::wait($re2, 'onConnect'); 89 | // or 同时异步执行 ---------------------------------------------------------------+ 90 | list($re, $re2) = yield from Promise::all(array($re, $re2), 'onConnect'); 91 | // 选择结束 ----------------------------------------------------------------------+ 92 | 93 | if (isset($re['re']) && isset($re2['re'])) { 94 | $check = $re['re'] && $re2['re']; 95 | } 96 | $connection->sendAsync($check); 97 | }; 98 | 99 | /** 100 | * 发起rpc检查统一接口 101 | * 102 | * @param $host 103 | * @param $port 104 | * @param $method 105 | * @param $data 106 | * @param bool $noBlocking 107 | * @return bool|false|string 108 | */ 109 | function checkClientAsync($connection, $host, $port, $method, $data, $noBlocking = true) 110 | { 111 | CoWorker::safeEcho('try to connect to ' . "tcp://$host:$port" . PHP_EOL); 112 | 113 | $con = new CoTcpClient("tcp://$host:$port"); 114 | $ifCon = $con->connectAsync($connection); 115 | $ifCon = yield from Promise::wait($ifCon, __FUNCTION__); 116 | 117 | if (!$ifCon) { 118 | return null; 119 | } 120 | CoWorker::safeEcho( "\nconnected to server, re: [{$ifCon}]\n" . PHP_EOL); 121 | $message = json_encode(["method" => $method, "data" => $data]); 122 | $re = $con->sendAsync($message); 123 | return $re; 124 | } 125 | 126 | function checkInventoryAsync($connection, $productId, $noBlocking = true) 127 | { 128 | $host = "127.0.0.1"; 129 | $port = 8081; 130 | $data = array('productId' => $productId); 131 | return yield from checkClientAsync($connection, $host, $port, 'inventory', $data, $noBlocking); 132 | } 133 | 134 | function checkProductAsync($connection, $productId, $noBlocking = true) 135 | { 136 | $host = "127.0.0.1"; 137 | $port = 8082; 138 | $data = array('productId' => $productId); 139 | return yield from checkClientAsync($connection, $host, $port, 'product', $data, $noBlocking); 140 | } 141 | 142 | // 运行worker 143 | CoWorker::runAll(); 144 | ``` 145 | * 启动依赖的三个服务 146 | ```shell script 147 | ## 启动一个处理耗时2s的库存服务 148 | $ php ./examples/example5/otherServerFork.php 8081 inventory 1 149 | 150 | ## 启动一个处理耗时4s的产品服务 151 | $ php ./examples/example5/otherServerFork.php 8082 product 2 152 | 153 | ## 监听8083端口,处理一个请求 耗时6s的 promo 服务 154 | $ php ./examples/example5/otherServerFork.php 8083 promo 3 155 | ``` 156 | * 运行服务端,与客户端 157 | 158 | ```shell script 159 | ## 启动一个非阻塞购物车服务 160 | $ php ./coCartServer.php 161 | 162 | ## 客户端请求 163 | $ php ./examples/example5/userClientFork.php 164 | ``` 165 | 166 | ### 服务中使用协程MySQL客户端 167 | 168 | ```php 169 | onMessage = function($connection, $data) { 180 | global $mysql; 181 | 182 | $mysql = new Connection(array( 183 | 'host' => '127.0.0.1', // 不要写localhost 184 | 'dbname' => 'mysql', 185 | 'user' => 'root', 186 | 'password' => '123456', 187 | 'port' => '3306' 188 | )); 189 | 190 | $connected = yield from $mysql->connection(); 191 | $re = yield from $mysql->query('show databases'); 192 | 193 | CoWorker::safeEcho(json_encode($re) . PHP_EOL); 194 | }; 195 | 196 | CoWorker::runAll(); 197 | ``` 198 | 199 | * 启动服务 200 | ```shell script 201 | ## start the MySQL client server 202 | $ php ./examples/example3/coWorkerMySQLtest1.php start 203 | ``` 204 | * 访问 205 | ```shell script 206 | telnet 127.0.0.1 6161 207 | ``` 208 | 209 | ## 说明 210 | 211 | 目前还在测试开发阶段,不要用在生产环境中. 212 | 213 | 它克隆自[workerman](https://github.com/walkor/Workerman) 214 | -------------------------------------------------------------------------------- /src/CoWorker.php: -------------------------------------------------------------------------------- 1 | $class) { 50 | if (\extension_loaded($name)) { 51 | $loop_name = $name; 52 | break; 53 | } 54 | } 55 | 56 | if ($loop_name) { 57 | if (\interface_exists('\React\EventLoop\LoopInterface')) { 58 | switch ($loop_name) { 59 | case 'libevent': 60 | static::$eventLoopClass = '\Workerman\Events\React\ExtLibEventLoop'; 61 | break; 62 | case 'event': 63 | // static::$eventLoopClass = '\Workerman\Events\React\ExtEventLoop'; 64 | static::$eventLoopClass = '\CoWorkerman\Events\CoExtEvent'; 65 | break; 66 | default : 67 | // static::$eventLoopClass = '\Workerman\Events\React\StreamSelectLoop'; 68 | static::$eventLoopClass = '\CoWorkerman\Events\CoStreamSelect'; 69 | break; 70 | } 71 | } else { 72 | static::$eventLoopClass = static::$_availableEventLoops[$loop_name]; 73 | } 74 | } else { 75 | static::$eventLoopClass = \interface_exists('\React\EventLoop\LoopInterface') ? '\Workerman\Events\React\StreamSelectLoop' : '\Workerman\Events\Select'; 76 | } 77 | return static::$eventLoopClass; 78 | } 79 | 80 | /** 81 | * Accept a connection. 82 | * 83 | * @param resource $socket 84 | * @return void 85 | */ 86 | public function acceptConnection($socket) 87 | { 88 | // Accept a connection on server socket. 89 | \set_error_handler(function(){}); 90 | $new_socket = \stream_socket_accept($socket, 0, $remote_address); 91 | \restore_error_handler(); 92 | 93 | // Thundering herd. 94 | if (!$new_socket) { 95 | return; 96 | } 97 | 98 | // TcpConnection. 99 | /** 100 | * @var CoTcpConnection $connection 101 | */ 102 | $connection = new CoTcpConnection($new_socket, $remote_address); 103 | $this->connections[$connection->id] = $connection; 104 | $connection->worker = $this; 105 | $connection->protocol = $this->protocol; 106 | $connection->transport = $this->transport; 107 | $connection->onConnect = $this->onConnect; 108 | $connection->onMessage = $this->onMessage; 109 | $connection->onClose = $this->onClose; 110 | $connection->onError = $this->onError; 111 | $connection->onBufferDrain = $this->onBufferDrain; 112 | $connection->onBufferFull = $this->onBufferFull; 113 | 114 | // Try to emit onConnect callback. 115 | 116 | \call_user_func(array($connection, 'newConnect'), $connection); 117 | } 118 | 119 | /** 120 | * For udp package. 121 | * 122 | * @param resource $socket 123 | * @return bool 124 | */ 125 | public function acceptUdpConnection($socket) 126 | { 127 | \set_error_handler(function(){}); 128 | $recv_buffer = \stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remote_address); 129 | \restore_error_handler(); 130 | if (false === $recv_buffer || empty($remote_address)) { 131 | return false; 132 | } 133 | // UdpConnection. 134 | $connection = new UdpConnection($socket, $remote_address); 135 | $connection->protocol = $this->protocol; 136 | if ($this->onMessage) { 137 | try { 138 | if ($this->protocol !== null) { 139 | /** @var \Workerman\Protocols\ProtocolInterface $parser */ 140 | $parser = $this->protocol; 141 | if(\method_exists($parser,'input')){ 142 | while($recv_buffer !== ''){ 143 | $len = $parser::input($recv_buffer, $connection); 144 | if($len === 0) 145 | return true; 146 | $package = \substr($recv_buffer,0,$len); 147 | $recv_buffer = \substr($recv_buffer,$len); 148 | $data = $parser::decode($package,$connection); 149 | if ($data === false) 150 | continue; 151 | \call_user_func($this->onMessage, $connection, $data); 152 | } 153 | }else{ 154 | $data = $parser::decode($recv_buffer, $connection); 155 | // Discard bad packets. 156 | if ($data === false) 157 | return true; 158 | \call_user_func($this->onMessage, $connection, $data); 159 | } 160 | }else{ 161 | \call_user_func($this->onMessage, $connection, $recv_buffer); 162 | } 163 | ++ConnectionInterface::$statistics['total_request']; 164 | } catch (\Exception $e) { 165 | static::log($e); 166 | exit(250); 167 | } catch (\Error $e) { 168 | static::log($e); 169 | exit(250); 170 | } 171 | } 172 | return true; 173 | } 174 | 175 | } -------------------------------------------------------------------------------- /src/Events/CoExtEvent.php: -------------------------------------------------------------------------------- 1 | requireFeatures(\EventConfig::FEATURE_FDS); 51 | } 52 | 53 | $this->_eventBase = $this->eventBase = new EventBase($config); 54 | $this->futureTickQueue = new FutureTickQueue(); 55 | $this->timerEvents = new SplObjectStorage(); 56 | $this->signals = new SignalsHandler(); 57 | 58 | $this->createTimerCallback(); 59 | $this->createStreamCallback(); 60 | } 61 | 62 | // public function __construct() 63 | // { 64 | // if (\class_exists('\\\\EventBase', false)) { 65 | // $class_name = '\\\\EventBase'; 66 | // } else { 67 | // $class_name = '\EventBase'; 68 | // } 69 | // $this->_eventBase = new $class_name(); 70 | // } 71 | 72 | public function __destruct() 73 | { 74 | // explicitly clear all references to Event objects to prevent SEGFAULTs on Windows 75 | foreach ($this->timerEvents as $timer) { 76 | $this->timerEvents->detach($timer); 77 | } 78 | 79 | $this->readEvents = array(); 80 | $this->writeEvents = array(); 81 | } 82 | 83 | public function addReadStream($stream, $listener) 84 | { 85 | $key = (int) $stream; 86 | if (isset($this->readListeners[$key])) { 87 | return; 88 | } 89 | 90 | $event = new Event($this->eventBase, $stream, Event::PERSIST | Event::READ, $this->streamCallback); 91 | $event->add(); 92 | $this->readEvents[$key] = $event; 93 | $this->readListeners[$key] = $listener; 94 | 95 | // ext-event does not increase refcount on stream resources for PHP 7+ 96 | // manually keep track of stream resource to prevent premature garbage collection 97 | if (\PHP_VERSION_ID >= 70000) { 98 | $this->readRefs[$key] = $stream; 99 | } 100 | } 101 | 102 | public function addWriteStream($stream, $listener) 103 | { 104 | $key = (int) $stream; 105 | if (isset($this->writeListeners[$key])) { 106 | return; 107 | } 108 | 109 | $event = new Event($this->eventBase, $stream, Event::PERSIST | Event::WRITE, $this->streamCallback); 110 | $event->add(); 111 | $this->writeEvents[$key] = $event; 112 | $this->writeListeners[$key] = $listener; 113 | 114 | // ext-event does not increase refcount on stream resources for PHP 7+ 115 | // manually keep track of stream resource to prevent premature garbage collection 116 | if (\PHP_VERSION_ID >= 70000) { 117 | $this->writeRefs[$key] = $stream; 118 | } 119 | } 120 | 121 | public function removeReadStream($stream) 122 | { 123 | $key = (int) $stream; 124 | 125 | if (isset($this->readEvents[$key])) { 126 | $this->readEvents[$key]->free(); 127 | unset( 128 | $this->readEvents[$key], 129 | $this->readListeners[$key], 130 | $this->readRefs[$key] 131 | ); 132 | } 133 | } 134 | 135 | public function removeWriteStream($stream) 136 | { 137 | $key = (int) $stream; 138 | 139 | if (isset($this->writeEvents[$key])) { 140 | $this->writeEvents[$key]->free(); 141 | unset( 142 | $this->writeEvents[$key], 143 | $this->writeListeners[$key], 144 | $this->writeRefs[$key] 145 | ); 146 | } 147 | } 148 | 149 | public function addTimer($interval, $callback) 150 | { 151 | $timer = new Timer($interval, $callback, false); 152 | 153 | $this->scheduleTimer($timer); 154 | 155 | return $timer; 156 | } 157 | 158 | public function addPeriodicTimer($interval, $callback) 159 | { 160 | $timer = new Timer($interval, $callback, true); 161 | 162 | $this->scheduleTimer($timer); 163 | 164 | return $timer; 165 | } 166 | 167 | public function cancelTimer(TimerInterface $timer) 168 | { 169 | if ($this->timerEvents->contains($timer)) { 170 | $this->timerEvents[$timer]->free(); 171 | $this->timerEvents->detach($timer); 172 | } 173 | } 174 | 175 | public function futureTick($listener) 176 | { 177 | $this->futureTickQueue->add($listener); 178 | } 179 | 180 | public function addSignal($signal, $listener) 181 | { 182 | $this->signals->add($signal, $listener); 183 | 184 | if (!isset($this->signalEvents[$signal])) { 185 | $this->signalEvents[$signal] = Event::signal($this->eventBase, $signal, array($this->signals, 'call')); 186 | $this->signalEvents[$signal]->add(); 187 | } 188 | } 189 | 190 | public function removeSignal($signal, $listener) 191 | { 192 | $this->signals->remove($signal, $listener); 193 | 194 | if (isset($this->signalEvents[$signal]) && $this->signals->count($signal) === 0) { 195 | $this->signalEvents[$signal]->free(); 196 | unset($this->signalEvents[$signal]); 197 | } 198 | } 199 | 200 | public function run() 201 | { 202 | $this->running = true; 203 | 204 | while ($this->running) { 205 | $this->futureTickQueue->tick(); 206 | 207 | $flags = EventBase::LOOP_ONCE; 208 | if (!$this->running || !$this->futureTickQueue->isEmpty()) { 209 | $flags |= EventBase::LOOP_NONBLOCK; 210 | } elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) { 211 | break; 212 | } 213 | 214 | $this->eventBase->loop($flags); 215 | } 216 | } 217 | 218 | public function stop() 219 | { 220 | $this->running = false; 221 | } 222 | 223 | /** 224 | * Schedule a timer for execution. 225 | * 226 | * @param TimerInterface $timer 227 | */ 228 | private function scheduleTimer(TimerInterface $timer) 229 | { 230 | $flags = Event::TIMEOUT; 231 | 232 | if ($timer->isPeriodic()) { 233 | $flags |= Event::PERSIST; 234 | } 235 | 236 | $event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer); 237 | $this->timerEvents[$timer] = $event; 238 | 239 | $event->add($timer->getInterval()); 240 | } 241 | 242 | /** 243 | * Create a callback used as the target of timer events. 244 | * 245 | * A reference is kept to the callback for the lifetime of the loop 246 | * to prevent "Cannot destroy active lambda function" fatal error from 247 | * the event extension. 248 | */ 249 | private function createTimerCallback() 250 | { 251 | $timers = $this->timerEvents; 252 | $this->timerCallback = function ($_, $__, $timer) use ($timers) { 253 | \call_user_func($timer->getCallback(), $timer); 254 | 255 | if (!$timer->isPeriodic() && $timers->contains($timer)) { 256 | $this->cancelTimer($timer); 257 | } 258 | }; 259 | } 260 | 261 | /** 262 | * Create a callback used as the target of stream events. 263 | * 264 | * A reference is kept to the callback for the lifetime of the loop 265 | * to prevent "Cannot destroy active lambda function" fatal error from 266 | * the event extension. 267 | */ 268 | private function createStreamCallback() 269 | { 270 | $read =& $this->readListeners; 271 | $write =& $this->writeListeners; 272 | $this->streamCallback = function ($stream, $flags) use (&$read, &$write) { 273 | $key = (int) $stream; 274 | 275 | if (Event::READ === (Event::READ & $flags) && isset($read[$key])) { 276 | \call_user_func($read[$key], $stream); 277 | } 278 | 279 | if (Event::WRITE === (Event::WRITE & $flags) && isset($write[$key])) { 280 | \call_user_func($write[$key], $stream); 281 | } 282 | }; 283 | } 284 | 285 | // --------------------- react event loop end 286 | 287 | /** 288 | * Event base. 289 | * @var object 290 | */ 291 | protected $_eventBase = null; 292 | 293 | /** 294 | * All listeners for read/write event. 295 | * @var array 296 | */ 297 | protected $_allEvents = array(); 298 | 299 | /** 300 | * Event listeners of signal. 301 | * @var array 302 | */ 303 | protected $_eventSignal = array(); 304 | 305 | /** 306 | * All timer event listeners. 307 | * [func, args, event, flag, time_interval] 308 | * @var array 309 | */ 310 | protected $_eventTimer = array(); 311 | 312 | /** 313 | * Timer id. 314 | * @var int 315 | */ 316 | protected static $_timerId = 1; 317 | 318 | /** 319 | * @see EventInterface::add() 320 | */ 321 | public function add($fd, $flag, $func, $args=array()) 322 | { 323 | if (\class_exists('\\\\Event', false)) { 324 | $class_name = '\\\\Event'; 325 | } else { 326 | $class_name = '\Event'; 327 | } 328 | switch ($flag) { 329 | case self::EV_SIGNAL: 330 | 331 | $fd_key = (int)$fd; 332 | $event = $class_name::signal($this->_eventBase, $fd, $func); 333 | if (!$event||!$event->add()) { 334 | return false; 335 | } 336 | $this->_eventSignal[$fd_key] = $event; 337 | return true; 338 | 339 | case self::EV_TIMER: 340 | case self::EV_TIMER_ONCE: 341 | 342 | $param = array($func, (array)$args, $flag, $fd, self::$_timerId); 343 | $event = new $class_name($this->_eventBase, -1, $class_name::TIMEOUT|$class_name::PERSIST, array($this, "timerCallback"), $param); 344 | if (!$event||!$event->addTimer($fd)) { 345 | return false; 346 | } 347 | $this->_eventTimer[self::$_timerId] = $event; 348 | return self::$_timerId++; 349 | 350 | default : 351 | $fd_key = (int)$fd; 352 | $real_flag = $flag === self::EV_READ ? $class_name::READ | $class_name::PERSIST : $class_name::WRITE | $class_name::PERSIST; 353 | $event = new $class_name($this->_eventBase, $fd, $real_flag, $func, $fd); 354 | if (!$event||!$event->add()) { 355 | return false; 356 | } 357 | $this->_allEvents[$fd_key][$flag] = $event; 358 | return true; 359 | } 360 | } 361 | 362 | /** 363 | * @see \Workerman\Events\EventInterface::del() 364 | */ 365 | public function del($fd, $flag) 366 | { 367 | switch ($flag) { 368 | 369 | case self::EV_READ: 370 | case self::EV_WRITE: 371 | 372 | $fd_key = (int)$fd; 373 | if (isset($this->_allEvents[$fd_key][$flag])) { 374 | $this->_allEvents[$fd_key][$flag]->del(); 375 | unset($this->_allEvents[$fd_key][$flag]); 376 | } 377 | if (empty($this->_allEvents[$fd_key])) { 378 | unset($this->_allEvents[$fd_key]); 379 | } 380 | break; 381 | 382 | case self::EV_SIGNAL: 383 | $fd_key = (int)$fd; 384 | if (isset($this->_eventSignal[$fd_key])) { 385 | $this->_eventSignal[$fd_key]->del(); 386 | unset($this->_eventSignal[$fd_key]); 387 | } 388 | break; 389 | 390 | case self::EV_TIMER: 391 | case self::EV_TIMER_ONCE: 392 | if (isset($this->_eventTimer[$fd])) { 393 | $this->_eventTimer[$fd]->del(); 394 | unset($this->_eventTimer[$fd]); 395 | } 396 | break; 397 | } 398 | return true; 399 | } 400 | 401 | /** 402 | * Timer callback. 403 | * @param null $fd 404 | * @param int $what 405 | * @param int $timer_id 406 | */ 407 | public function timerCallback($fd, $what, $param) 408 | { 409 | $timer_id = $param[4]; 410 | 411 | if ($param[2] === self::EV_TIMER_ONCE) { 412 | $this->_eventTimer[$timer_id]->del(); 413 | unset($this->_eventTimer[$timer_id]); 414 | } 415 | 416 | try { 417 | \call_user_func_array($param[0], $param[1]); 418 | } catch (\Exception $e) { 419 | \Workerman\Worker::log($e); 420 | exit(250); 421 | } catch (\Error $e) { 422 | Worker::log($e); 423 | exit(250); 424 | } 425 | } 426 | 427 | /** 428 | * @see \Workerman\Events\EventInterface::clearAllTimer() 429 | * @return void 430 | */ 431 | public function clearAllTimer() 432 | { 433 | foreach ($this->_eventTimer as $event) { 434 | $event->del(); 435 | } 436 | $this->_eventTimer = array(); 437 | } 438 | 439 | 440 | /** 441 | * @see EventInterface::loop() 442 | */ 443 | public function loop() 444 | { 445 | $this->_eventBase->loop(); 446 | } 447 | 448 | /** 449 | * Destroy loop. 450 | * 451 | * @return void 452 | */ 453 | public function destroy() 454 | { 455 | foreach ($this->_eventSignal as $event) { 456 | $event->del(); 457 | } 458 | } 459 | 460 | /** 461 | * Get timer count. 462 | * 463 | * @return integer 464 | */ 465 | public function getTimerCount() 466 | { 467 | return \count($this->_eventTimer); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/Connection/CoTcpConnection.php: -------------------------------------------------------------------------------- 1 | _coroutineId = $coId; 41 | parent::__construct($socket, $remote_address); 42 | } 43 | 44 | /** 45 | * @param CoTcpConnection $connection 46 | */ 47 | public function newConnect($connection) 48 | { 49 | // Try to emit onConnect callback. 50 | if ($this->onConnect) { 51 | try { 52 | $gen = \call_user_func($this->onConnect, $this); 53 | if ($gen instanceof \Generator) { 54 | $coId = Coworker::genCoId(); 55 | self::$_connectionSocketToCoId[(string) $connection->getSocket()] = $coId; 56 | CoWorker::setCoIdBySocket($connection->getSocket(), $coId); 57 | self::addCoroutine($gen, $coId); 58 | CoWorker::addCoroutine($gen, $coId); 59 | // run coroutine 60 | $yieldRe = $gen->current(); 61 | } 62 | } catch (\Exception $e) { 63 | static::log($e); 64 | exit(250); 65 | } catch (\Error $e) { 66 | static::log($e); 67 | exit(250); 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 发来的消息, 74 | * 75 | * @param CoTcpConnection $connection 76 | * @param string $data 77 | */ 78 | protected function _onMessage(CoTcpConnection $connection, $data) 79 | { 80 | $coId = self::getCoIdBySocket($connection->getSocket()); 81 | // CoWorker::refreshCoIdBySocket($connection->getSocket()); 82 | if (-1 !== $coId && isset(self::$_coroutines[$coId])) { 83 | CoWorker::$_currentCoId = $coId; 84 | self::coSend($coId, $connection->getSocket(), $data); 85 | } elseif ($this->onResponse[$coId]) { 86 | try { 87 | // Decode request buffer before Emitting onMessage callback. 88 | $gen = \call_user_func($this->onResponse[$coId], $connection, $data); 89 | if ($gen instanceof \Generator) { 90 | $coId = Coworker::genCoId(); 91 | self::addCoroutine($gen, $coId); 92 | CoWorker::addCoroutine($gen, $coId); 93 | // run coroutine 94 | $gen->current(); 95 | } 96 | } catch (\Exception $e) { 97 | Worker::log($e); 98 | exit(250); 99 | } catch (\Error $e) { 100 | Worker::log($e); 101 | exit(250); 102 | } 103 | } elseif ($this->onMessage) { 104 | try { 105 | // Decode request buffer before Emitting onMessage callback. 106 | $gen = \call_user_func($this->onMessage, $connection, $data); 107 | if ($gen instanceof \Generator) { 108 | $coId = Coworker::genCoId(); 109 | self::addCoroutine($gen, $coId); 110 | CoWorker::addCoroutine($gen, $coId); 111 | // run coroutine 112 | $gen->current(); 113 | } 114 | } catch (\Exception $e) { 115 | Worker::log($e); 116 | exit(250); 117 | } catch (\Error $e) { 118 | Worker::log($e); 119 | exit(250); 120 | } 121 | } 122 | return; 123 | } 124 | 125 | /** 126 | * 关闭链接 127 | * 128 | * @param CoTcpConnection $connection 129 | */ 130 | protected function _onClose($connection) 131 | { 132 | $coId = self::getCoIdBySocket($connection->getSocket()); 133 | // self::coThrow($coId, null, new ConnectionCloseException('the connection closed!')); 134 | // self::removeGeneratorBySocket($connection->getSocket()); 135 | if ($this->onClose) { 136 | try { 137 | $gen = \call_user_func($this->onClose, $connection); 138 | if ($gen instanceof \Generator) { 139 | $coId = Coworker::genCoId(); 140 | self::addCoroutine($gen, $coId); 141 | CoWorker::addCoroutine($gen, $coId); 142 | // run coroutine 143 | $gen->current(); 144 | } 145 | } catch (\Exception $e) { 146 | Worker::log($e); 147 | exit(250); 148 | } catch (\Error $e) { 149 | Worker::log($e); 150 | exit(250); 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * 报错了 157 | * 158 | * @param CoTcpConnection $connection 159 | * @param integer $code 160 | * @param string $msg 161 | */ 162 | protected function _onError($connection, $code, $msg) 163 | { 164 | $coId = self::getCoIdBySocket($connection->getSocket()); 165 | self::coThrow($coId, null, new ConnectionErrorException($msg)); 166 | if ($this->onError) { 167 | try { 168 | $gen = \call_user_func($this->onError, $connection, $code, $msg); 169 | if ($gen instanceof \Generator) { 170 | $coId = Coworker::genCoId(); 171 | self::addCoroutine($gen, $coId); 172 | CoWorker::addCoroutine($gen, $coId); 173 | // run coroutine 174 | $gen->current(); 175 | } 176 | } catch (\Exception $e) { 177 | Worker::log($e); 178 | exit(250); 179 | } catch (\Error $e) { 180 | Worker::log($e); 181 | exit(250); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * @param integer $len 188 | * @return \Generator|mixed 189 | */ 190 | public function readAsync($len) 191 | { 192 | $coId = CoWorker::getCurrentCoId(); 193 | $this->onResponse[$coId] = function (CoTcpConnection $connection, $data) use ($coId, $len) { 194 | CoWorker::$_currentCoId = $coId; 195 | CoTcpConnection::coSend($coId, $connection->getSocket(), $data); 196 | }; 197 | 198 | $data = yield; 199 | $this->onResponse[$coId] = null; 200 | return $data['data']; 201 | } 202 | 203 | /** 204 | * Sends data on the connection. 205 | * 206 | * @param mixed $send_buffer 207 | * @param bool $raw 208 | * @return bool|null 209 | */ 210 | public function sendAsync($send_buffer, $raw = false) 211 | { 212 | if (false) { 213 | yield; 214 | } 215 | if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { 216 | return false; 217 | } 218 | 219 | // Try to call protocol::encode($send_buffer) before sending. 220 | if (false === $raw && $this->protocol !== null) { 221 | $parser = $this->protocol; 222 | $send_buffer = $parser::encode($send_buffer, $this); 223 | if ($send_buffer === '') { 224 | return; 225 | } 226 | } 227 | 228 | if ($this->_status !== self::STATUS_ESTABLISHED || 229 | ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) 230 | ) { 231 | if ($this->_sendBuffer && $this->bufferIsFull()) { 232 | ++self::$statistics['send_fail']; 233 | return false; 234 | } 235 | $this->_sendBuffer .= $send_buffer; 236 | $this->checkBufferWillFull(); 237 | return; 238 | } 239 | 240 | // Attempt to send data directly. 241 | if ($this->_sendBuffer === '') { 242 | if ($this->transport === 'ssl') { 243 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 244 | $this->_sendBuffer = $send_buffer; 245 | $this->checkBufferWillFull(); 246 | return; 247 | } 248 | $len = 0; 249 | try { 250 | $len = @\fwrite($this->_socket, $send_buffer); 251 | } catch (\Exception $e) { 252 | Worker::log($e); 253 | } catch (\Error $e) { 254 | Worker::log($e); 255 | } 256 | // send successful. 257 | if ($len === \strlen($send_buffer)) { 258 | $this->bytesWritten += $len; 259 | return true; 260 | } 261 | // Send only part of the data. 262 | if ($len > 0) { 263 | $this->_sendBuffer = \substr($send_buffer, $len); 264 | $this->bytesWritten += $len; 265 | } else { 266 | // Connection closed? 267 | if (!\is_resource($this->_socket) || \feof($this->_socket)) { 268 | ++self::$statistics['send_fail']; 269 | /* 270 | if ($this->onError) { 271 | try { 272 | \call_user_func($this->onError, $this, \WORKERMAN_SEND_FAIL, 'client closed'); 273 | } catch (\Exception $e) { 274 | Worker::log($e); 275 | exit(250); 276 | } catch (\Error $e) { 277 | Worker::log($e); 278 | exit(250); 279 | } 280 | } */ 281 | $this->_onError($this, \WORKERMAN_SEND_FAIL, 'client closed'); 282 | $this->destroy(); 283 | return false; 284 | } 285 | $this->_sendBuffer = $send_buffer; 286 | } 287 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 288 | // Check if the send buffer will be full. 289 | $this->checkBufferWillFull(); 290 | return; 291 | } 292 | 293 | if ($this->bufferIsFull()) { 294 | ++self::$statistics['send_fail']; 295 | return false; 296 | } 297 | 298 | $this->_sendBuffer .= $send_buffer; 299 | // Check if the send buffer is full. 300 | $this->checkBufferWillFull(); 301 | } 302 | 303 | /** 304 | * Base read handler. 305 | * 306 | * @param resource $socket 307 | * @param bool $check_eof 308 | * @return void 309 | */ 310 | public function baseRead($socket, $check_eof = true) 311 | { 312 | // SSL handshake. 313 | if ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) { 314 | if ($this->doSslHandshake($socket)) { 315 | $this->_sslHandshakeCompleted = true; 316 | if ($this->_sendBuffer) { 317 | Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 318 | } 319 | } else { 320 | return; 321 | } 322 | } 323 | 324 | $buffer = ''; 325 | try { 326 | $buffer = @\fread($socket, self::READ_BUFFER_SIZE); 327 | } catch (\Exception $e) { 328 | } catch (\Error $e) { 329 | } 330 | 331 | // Check connection closed. 332 | if ($buffer === '' || $buffer === false) { 333 | if ($check_eof && (\feof($socket) || !\is_resource($socket) || $buffer === false)) { 334 | $this->destroy(); 335 | return; 336 | } 337 | } else { 338 | $this->bytesRead += \strlen($buffer); 339 | $this->_recvBuffer .= $buffer; 340 | } 341 | 342 | // If the application layer protocol has been set up. 343 | if ($this->protocol !== null) { 344 | $parser = $this->protocol; 345 | while ($this->_recvBuffer !== '' && !$this->_isPaused) { 346 | // The current packet length is known. 347 | if ($this->_currentPackageLength) { 348 | // Data is not enough for a package. 349 | if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { 350 | break; 351 | } 352 | } else { 353 | // Get current package length. 354 | try { 355 | $this->_currentPackageLength = $parser::input($this->_recvBuffer, $this); 356 | } catch (\Exception $e) { 357 | } catch (\Error $e) { 358 | } 359 | // The packet length is unknown. 360 | if ($this->_currentPackageLength === 0) { 361 | break; 362 | } elseif ($this->_currentPackageLength > 0 && $this->_currentPackageLength <= $this->maxPackageSize) { 363 | // Data is not enough for a package. 364 | if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { 365 | break; 366 | } 367 | } // Wrong package. 368 | else { 369 | Worker::safeEcho('Error package. package_length=' . \var_export($this->_currentPackageLength, true)); 370 | $this->destroy(); 371 | return; 372 | } 373 | } 374 | 375 | // The data is enough for a packet. 376 | ++self::$statistics['total_request']; 377 | // The current packet length is equal to the length of the buffer. 378 | if (\strlen($this->_recvBuffer) === $this->_currentPackageLength) { 379 | $one_request_buffer = $this->_recvBuffer; 380 | $this->_recvBuffer = ''; 381 | } else { 382 | // Get a full package from the buffer. 383 | $one_request_buffer = \substr($this->_recvBuffer, 0, $this->_currentPackageLength); 384 | // Remove the current package from the receive buffer. 385 | $this->_recvBuffer = \substr($this->_recvBuffer, $this->_currentPackageLength); 386 | } 387 | // Reset the current packet length to 0. 388 | $this->_currentPackageLength = 0; 389 | /* 390 | if (!$this->onMessage) { 391 | continue; 392 | } 393 | try { 394 | // Decode request buffer before Emitting onMessage callback. 395 | $gen = \call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this)); 396 | if ($gen instanceof \Generator) { 397 | $coId = Coworker::genCoId(); 398 | $this->_coroutines[$coId] = $gen; 399 | // run coroutine 400 | $gen->current(); 401 | } 402 | } catch (\Exception $e) { 403 | Worker::log($e); 404 | exit(250); 405 | } catch (\Error $e) { 406 | Worker::log($e); 407 | exit(250); 408 | } 409 | */ 410 | $this->_onMessage($this, $parser::decode($one_request_buffer, $this)); 411 | } 412 | return; 413 | } 414 | 415 | if ($this->_recvBuffer === '' || $this->_isPaused) { 416 | return; 417 | } 418 | 419 | // Applications protocol is not set. 420 | ++self::$statistics['total_request']; 421 | /* 422 | if (!$this->onMessage) { 423 | $this->_recvBuffer = ''; 424 | return; 425 | } 426 | try { 427 | \call_user_func($this->onMessage, $this, $this->_recvBuffer); 428 | } catch (\Exception $e) { 429 | Worker::log($e); 430 | exit(250); 431 | } catch (\Error $e) { 432 | Worker::log($e); 433 | exit(250); 434 | } 435 | */ 436 | $this->_onMessage($this, $this->_recvBuffer); 437 | // Clean receive buffer. 438 | $this->_recvBuffer = ''; 439 | } 440 | 441 | 442 | /** 443 | * Destroy connection. 444 | * 445 | * @return void 446 | */ 447 | public function destroy() 448 | { 449 | // Avoid repeated calls. 450 | if ($this->_status === self::STATUS_CLOSED) { 451 | return; 452 | } 453 | // Remove event listener. 454 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); 455 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); 456 | 457 | // Close socket. 458 | try { 459 | @\fclose($this->_socket); 460 | } catch (\Exception $e) { 461 | } catch (\Error $e) { 462 | } 463 | 464 | $this->_status = self::STATUS_CLOSED; 465 | // Try to emit onClose callback. 466 | /* 467 | if ($this->onClose) { 468 | try { 469 | \call_user_func($this->onClose, $this); 470 | } catch (\Exception $e) { 471 | Worker::log($e); 472 | exit(250); 473 | } catch (\Error $e) { 474 | Worker::log($e); 475 | exit(250); 476 | } 477 | }*/ 478 | $this->_onClose($this); 479 | // Try to emit protocol::onClose 480 | if ($this->protocol && \method_exists($this->protocol, 'onClose')) { 481 | try { 482 | \call_user_func(array($this->protocol, 'onClose'), $this); 483 | } catch (\Exception $e) { 484 | Worker::log($e); 485 | exit(250); 486 | } catch (\Error $e) { 487 | Worker::log($e); 488 | exit(250); 489 | } 490 | } 491 | $this->_sendBuffer = $this->_recvBuffer = ''; 492 | $this->_currentPackageLength = 0; 493 | $this->_isPaused = $this->_sslHandshakeCompleted = false; 494 | if ($this->_status === self::STATUS_CLOSED) { 495 | // Cleaning up the callback to avoid memory leaks. 496 | $this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = null; 497 | // Remove from worker->connections. 498 | if ($this->worker) { 499 | unset($this->worker->connections[$this->_id]); 500 | } 501 | unset(static::$connections[$this->_id]); 502 | } 503 | } 504 | 505 | /** 506 | * Destruct. 507 | * 508 | * @return void 509 | */ 510 | public function __destruct() 511 | { 512 | // 关闭相关协程 513 | // parent::__destruct(); 514 | } 515 | 516 | } 517 | -------------------------------------------------------------------------------- /src/Events/CoStreamSelect.php: -------------------------------------------------------------------------------- 1 | futureTickQueue = new FutureTickQueue(); 32 | $this->timers = new Timers(); 33 | $this->pcntl = \function_exists('pcntl_signal') && \function_exists('pcntl_signal_dispatch'); 34 | $this->pcntlPoll = $this->pcntl && !\function_exists('pcntl_async_signals'); 35 | $this->signals = new SignalsHandler(); 36 | 37 | // prefer async signals if available (PHP 7.1+) or fall back to dispatching on each tick 38 | if ($this->pcntl && !$this->pcntlPoll) { 39 | \pcntl_async_signals(true); 40 | } 41 | 42 | // Init SplPriorityQueue. 43 | $this->_scheduler = new \SplPriorityQueue(); 44 | $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 45 | } 46 | 47 | /** 48 | * Construct. 49 | */ 50 | // public function __construct() 51 | // { 52 | // // Init SplPriorityQueue. 53 | // $this->_scheduler = new \SplPriorityQueue(); 54 | // $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 55 | // } 56 | 57 | public function addReadStream($stream, $listener) 58 | { 59 | $key = (int) $stream; 60 | 61 | if (!isset($this->readStreams[$key])) { 62 | $this->readStreams[$key] = $stream; 63 | $this->readListeners[$key] = $listener; 64 | } 65 | } 66 | 67 | public function addWriteStream($stream, $listener) 68 | { 69 | $key = (int) $stream; 70 | 71 | if (!isset($this->writeStreams[$key])) { 72 | $this->writeStreams[$key] = $stream; 73 | $this->writeListeners[$key] = $listener; 74 | } 75 | } 76 | 77 | public function removeReadStream($stream) 78 | { 79 | $key = (int) $stream; 80 | 81 | unset( 82 | $this->readStreams[$key], 83 | $this->readListeners[$key] 84 | ); 85 | } 86 | 87 | public function removeWriteStream($stream) 88 | { 89 | $key = (int) $stream; 90 | 91 | unset( 92 | $this->writeStreams[$key], 93 | $this->writeListeners[$key] 94 | ); 95 | } 96 | 97 | public function addTimer($interval, $callback) 98 | { 99 | $timer = new Timer($interval, $callback, false); 100 | 101 | $this->timers->add($timer); 102 | 103 | return $timer; 104 | } 105 | 106 | public function addPeriodicTimer($interval, $callback) 107 | { 108 | $timer = new Timer($interval, $callback, true); 109 | 110 | $this->timers->add($timer); 111 | 112 | return $timer; 113 | } 114 | 115 | public function cancelTimer(TimerInterface $timer) 116 | { 117 | $this->timers->cancel($timer); 118 | } 119 | 120 | public function futureTick($listener) 121 | { 122 | $this->futureTickQueue->add($listener); 123 | } 124 | 125 | public function addSignal($signal, $listener) 126 | { 127 | if ($this->pcntl === false) { 128 | throw new \BadMethodCallException('Event loop feature "signals" isn\'t supported by the "StreamSelectLoop"'); 129 | } 130 | 131 | $first = $this->signals->count($signal) === 0; 132 | $this->signals->add($signal, $listener); 133 | 134 | if ($first) { 135 | \pcntl_signal($signal, array($this->signals, 'call')); 136 | } 137 | } 138 | 139 | public function removeSignal($signal, $listener) 140 | { 141 | if (!$this->signals->count($signal)) { 142 | return; 143 | } 144 | 145 | $this->signals->remove($signal, $listener); 146 | 147 | if ($this->signals->count($signal) === 0) { 148 | \pcntl_signal($signal, \SIG_DFL); 149 | } 150 | } 151 | 152 | public function run() 153 | { 154 | $this->running = true; 155 | 156 | while ($this->running) { 157 | $this->futureTickQueue->tick(); 158 | 159 | $this->timers->tick(); 160 | 161 | // Future-tick queue has pending callbacks ... 162 | if (!$this->running || !$this->futureTickQueue->isEmpty()) { 163 | $timeout = 0; 164 | 165 | // There is a pending timer, only block until it is due ... 166 | } elseif ($scheduledAt = $this->timers->getFirst()) { 167 | $timeout = $scheduledAt - $this->timers->getTime(); 168 | if ($timeout < 0) { 169 | $timeout = 0; 170 | } else { 171 | // Convert float seconds to int microseconds. 172 | // Ensure we do not exceed maximum integer size, which may 173 | // cause the loop to tick once every ~35min on 32bit systems. 174 | $timeout *= self::MICROSECONDS_PER_SECOND; 175 | $timeout = $timeout > \PHP_INT_MAX ? \PHP_INT_MAX : (int)$timeout; 176 | } 177 | 178 | // The only possible event is stream or signal activity, so wait forever ... 179 | } elseif ($this->readStreams || $this->writeStreams || !$this->signals->isEmpty()) { 180 | $timeout = null; 181 | 182 | // There's nothing left to do ... 183 | } else { 184 | break; 185 | } 186 | 187 | $this->waitForStreamActivity($timeout); 188 | } 189 | } 190 | 191 | public function stop() 192 | { 193 | $this->running = false; 194 | } 195 | 196 | /** 197 | * Wait/check for stream activity, or until the next timer is due. 198 | * 199 | * @param integer|null $timeout Activity timeout in microseconds, or null to wait forever. 200 | */ 201 | private function waitForStreamActivity($timeout) 202 | { 203 | $read = $this->readStreams; 204 | $write = $this->writeStreams; 205 | 206 | $available = $this->streamSelect($read, $write, $timeout); 207 | if ($this->pcntlPoll) { 208 | \pcntl_signal_dispatch(); 209 | } 210 | if (false === $available) { 211 | // if a system call has been interrupted, 212 | // we cannot rely on it's outcome 213 | return; 214 | } 215 | 216 | foreach ($read as $stream) { 217 | $key = (int) $stream; 218 | 219 | if (isset($this->readListeners[$key])) { 220 | \call_user_func($this->readListeners[$key], $stream); 221 | } 222 | } 223 | 224 | foreach ($write as $stream) { 225 | $key = (int) $stream; 226 | 227 | if (isset($this->writeListeners[$key])) { 228 | \call_user_func($this->writeListeners[$key], $stream); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * Emulate a stream_select() implementation that does not break when passed 235 | * empty stream arrays. 236 | * 237 | * @param array $read An array of read streams to select upon. 238 | * @param array $write An array of write streams to select upon. 239 | * @param int|null $timeout Activity timeout in microseconds, or null to wait forever. 240 | * 241 | * @return int|false The total number of streams that are ready for read/write. 242 | * Can return false if stream_select() is interrupted by a signal. 243 | */ 244 | private function streamSelect(array &$read, array &$write, $timeout) 245 | { 246 | if ($read || $write) { 247 | // We do not usually use or expose the `exceptfds` parameter passed to the underlying `select`. 248 | // However, Windows does not report failed connection attempts in `writefds` passed to `select` like most other platforms. 249 | // Instead, it uses `writefds` only for successful connection attempts and `exceptfds` for failed connection attempts. 250 | // We work around this by adding all sockets that look like a pending connection attempt to `exceptfds` automatically on Windows and merge it back later. 251 | // This ensures the public API matches other loop implementations across all platforms (see also test suite or rather test matrix). 252 | // Lacking better APIs, every write-only socket that has not yet read any data is assumed to be in a pending connection attempt state. 253 | // @link https://docs.microsoft.com/de-de/windows/win32/api/winsock2/nf-winsock2-select 254 | $except = null; 255 | if (\DIRECTORY_SEPARATOR === '\\') { 256 | $except = array(); 257 | foreach ($write as $key => $socket) { 258 | if (!isset($read[$key]) && @\ftell($socket) === 0) { 259 | $except[$key] = $socket; 260 | } 261 | } 262 | } 263 | 264 | // suppress warnings that occur, when stream_select is interrupted by a signal 265 | $ret = @\stream_select($read, $write, $except, $timeout === null ? null : 0, $timeout); 266 | 267 | if ($except) { 268 | $write = \array_merge($write, $except); 269 | } 270 | return $ret; 271 | } 272 | 273 | if ($timeout > 0) { 274 | \usleep($timeout); 275 | } elseif ($timeout === null) { 276 | // wait forever (we only reach this if we're only awaiting signals) 277 | // this may be interrupted and return earlier when a signal is received 278 | \sleep(PHP_INT_MAX); 279 | } 280 | 281 | return 0; 282 | } 283 | 284 | /** 285 | * All listeners for read/write event. 286 | * 287 | * @var array 288 | */ 289 | public $_allEvents = array(); 290 | 291 | /** 292 | * Event listeners of signal. 293 | * 294 | * @var array 295 | */ 296 | public $_signalEvents = array(); 297 | 298 | /** 299 | * Fds waiting for read event. 300 | * 301 | * @var array 302 | */ 303 | protected $_readFds = array(); 304 | 305 | /** 306 | * Fds waiting for write event. 307 | * 308 | * @var array 309 | */ 310 | protected $_writeFds = array(); 311 | 312 | /** 313 | * Fds waiting for except event. 314 | * 315 | * @var array 316 | */ 317 | protected $_exceptFds = array(); 318 | 319 | /** 320 | * Timer scheduler. 321 | * {['data':timer_id, 'priority':run_timestamp], ..} 322 | * 323 | * @var \SplPriorityQueue 324 | */ 325 | protected $_scheduler = null; 326 | 327 | /** 328 | * All timer event listeners. 329 | * [[func, args, flag, timer_interval], ..] 330 | * 331 | * @var array 332 | */ 333 | protected $_eventTimer = array(); 334 | 335 | /** 336 | * Timer id. 337 | * 338 | * @var int 339 | */ 340 | protected $_timerId = 1; 341 | 342 | /** 343 | * Select timeout. 344 | * 345 | * @var int 346 | */ 347 | protected $_selectTimeout = 100000000; 348 | 349 | /** 350 | * Paired socket channels 351 | * 352 | * @var array 353 | */ 354 | protected $channel = array(); 355 | 356 | 357 | 358 | /** 359 | * {@inheritdoc} 360 | */ 361 | public function add($fd, $flag, $func, $args = array()) 362 | { 363 | switch ($flag) { 364 | case self::EV_READ: 365 | case self::EV_WRITE: 366 | $count = $flag === self::EV_READ ? \count($this->_readFds) : \count($this->_writeFds); 367 | if ($count >= 1024) { 368 | echo "Warning: system call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.\n"; 369 | } else if (\DIRECTORY_SEPARATOR !== '/' && $count >= 256) { 370 | echo "Warning: system call select exceeded the maximum number of connections 256.\n"; 371 | } 372 | $fd_key = (int)$fd; 373 | $this->_allEvents[$fd_key][$flag] = array($func, $fd); 374 | if ($flag === self::EV_READ) { 375 | $this->_readFds[$fd_key] = $fd; 376 | } else { 377 | $this->_writeFds[$fd_key] = $fd; 378 | } 379 | break; 380 | case self::EV_EXCEPT: 381 | $fd_key = (int)$fd; 382 | $this->_allEvents[$fd_key][$flag] = array($func, $fd); 383 | $this->_exceptFds[$fd_key] = $fd; 384 | break; 385 | case self::EV_SIGNAL: 386 | // Windows not support signal. 387 | if(\DIRECTORY_SEPARATOR !== '/') { 388 | return false; 389 | } 390 | $fd_key = (int)$fd; 391 | $this->_signalEvents[$fd_key][$flag] = array($func, $fd); 392 | \pcntl_signal($fd, array($this, 'signalHandler')); 393 | break; 394 | case self::EV_TIMER: 395 | case self::EV_TIMER_ONCE: 396 | $timer_id = $this->_timerId++; 397 | $run_time = \microtime(true) + $fd; 398 | $this->_scheduler->insert($timer_id, -$run_time); 399 | $this->_eventTimer[$timer_id] = array($func, (array)$args, $flag, $fd); 400 | $select_timeout = ($run_time - \microtime(true)) * 1000000; 401 | if( $this->_selectTimeout > $select_timeout ){ 402 | $this->_selectTimeout = $select_timeout; 403 | } 404 | return $timer_id; 405 | } 406 | 407 | return true; 408 | } 409 | 410 | /** 411 | * Signal handler. 412 | * 413 | * @param int $signal 414 | */ 415 | public function signalHandler($signal) 416 | { 417 | \call_user_func_array($this->_signalEvents[$signal][self::EV_SIGNAL][0], array($signal)); 418 | } 419 | 420 | /** 421 | * {@inheritdoc} 422 | */ 423 | public function del($fd, $flag) 424 | { 425 | $fd_key = (int)$fd; 426 | switch ($flag) { 427 | case self::EV_READ: 428 | unset($this->_allEvents[$fd_key][$flag], $this->_readFds[$fd_key]); 429 | if (empty($this->_allEvents[$fd_key])) { 430 | unset($this->_allEvents[$fd_key]); 431 | } 432 | return true; 433 | case self::EV_WRITE: 434 | unset($this->_allEvents[$fd_key][$flag], $this->_writeFds[$fd_key]); 435 | if (empty($this->_allEvents[$fd_key])) { 436 | unset($this->_allEvents[$fd_key]); 437 | } 438 | return true; 439 | case self::EV_EXCEPT: 440 | unset($this->_allEvents[$fd_key][$flag], $this->_exceptFds[$fd_key]); 441 | if(empty($this->_allEvents[$fd_key])) 442 | { 443 | unset($this->_allEvents[$fd_key]); 444 | } 445 | return true; 446 | case self::EV_SIGNAL: 447 | if(\DIRECTORY_SEPARATOR !== '/') { 448 | return false; 449 | } 450 | unset($this->_signalEvents[$fd_key]); 451 | \pcntl_signal($fd, SIG_IGN); 452 | break; 453 | case self::EV_TIMER: 454 | case self::EV_TIMER_ONCE; 455 | unset($this->_eventTimer[$fd_key]); 456 | return true; 457 | } 458 | return false; 459 | } 460 | 461 | /** 462 | * Tick for timer. 463 | * 464 | * @return void 465 | */ 466 | protected function tick() 467 | { 468 | while (!$this->_scheduler->isEmpty()) { 469 | $scheduler_data = $this->_scheduler->top(); 470 | $timer_id = $scheduler_data['data']; 471 | $next_run_time = -$scheduler_data['priority']; 472 | $time_now = \microtime(true); 473 | $this->_selectTimeout = ($next_run_time - $time_now) * 1000000; 474 | if ($this->_selectTimeout <= 0) { 475 | $this->_scheduler->extract(); 476 | 477 | if (!isset($this->_eventTimer[$timer_id])) { 478 | continue; 479 | } 480 | 481 | // [func, args, flag, timer_interval] 482 | $task_data = $this->_eventTimer[$timer_id]; 483 | if ($task_data[2] === self::EV_TIMER) { 484 | $next_run_time = $time_now + $task_data[3]; 485 | $this->_scheduler->insert($timer_id, -$next_run_time); 486 | } 487 | \call_user_func_array($task_data[0], $task_data[1]); 488 | if (isset($this->_eventTimer[$timer_id]) && $task_data[2] === self::EV_TIMER_ONCE) { 489 | $this->del($timer_id, self::EV_TIMER_ONCE); 490 | } 491 | continue; 492 | } 493 | return; 494 | } 495 | $this->_selectTimeout = 100000000; 496 | } 497 | 498 | /** 499 | * {@inheritdoc} 500 | */ 501 | public function clearAllTimer() 502 | { 503 | $this->_scheduler = new \SplPriorityQueue(); 504 | $this->_scheduler->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 505 | $this->_eventTimer = array(); 506 | } 507 | 508 | /** 509 | * {@inheritdoc} 510 | */ 511 | public function loop() 512 | { 513 | while (1) { 514 | if(\DIRECTORY_SEPARATOR === '/') { 515 | // Calls signal handlers for pending signals 516 | \pcntl_signal_dispatch(); 517 | } 518 | 519 | $read = $this->_readFds; 520 | $write = $this->_writeFds; 521 | $except = $this->_exceptFds; 522 | 523 | if ($read || $write || $except) { 524 | // Waiting read/write/signal/timeout events. 525 | set_error_handler(function(){}); 526 | $ret = stream_select($read, $write, $except, 0, $this->_selectTimeout); 527 | restore_error_handler(); 528 | } else { 529 | usleep($this->_selectTimeout); 530 | $ret = false; 531 | } 532 | 533 | 534 | if (!$this->_scheduler->isEmpty()) { 535 | $this->tick(); 536 | } 537 | 538 | if (!$ret) { 539 | continue; 540 | } 541 | 542 | if ($read) { 543 | foreach ($read as $fd) { 544 | $fd_key = (int)$fd; 545 | if (isset($this->_allEvents[$fd_key][self::EV_READ])) { 546 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_READ][0], 547 | array($this->_allEvents[$fd_key][self::EV_READ][1])); 548 | } 549 | } 550 | } 551 | 552 | if ($write) { 553 | foreach ($write as $fd) { 554 | $fd_key = (int)$fd; 555 | if (isset($this->_allEvents[$fd_key][self::EV_WRITE])) { 556 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_WRITE][0], 557 | array($this->_allEvents[$fd_key][self::EV_WRITE][1])); 558 | } 559 | } 560 | } 561 | 562 | if($except) { 563 | foreach($except as $fd) { 564 | $fd_key = (int) $fd; 565 | if(isset($this->_allEvents[$fd_key][self::EV_EXCEPT])) { 566 | \call_user_func_array($this->_allEvents[$fd_key][self::EV_EXCEPT][0], 567 | array($this->_allEvents[$fd_key][self::EV_EXCEPT][1])); 568 | } 569 | } 570 | } 571 | } 572 | } 573 | 574 | /** 575 | * Destroy loop. 576 | * 577 | * @return void 578 | */ 579 | public function destroy() 580 | { 581 | 582 | } 583 | 584 | /** 585 | * Get timer count. 586 | * 587 | * @return integer 588 | */ 589 | public function getTimerCount() 590 | { 591 | return \count($this->_eventTimer); 592 | } 593 | 594 | } 595 | -------------------------------------------------------------------------------- /src/Connection/CoTcpClient.php: -------------------------------------------------------------------------------- 1 | getSocket(); 29 | $coId = self::getCoIdBySocket($socket); 30 | // $coId = CoWorker::getCoIdBySocket($socket); 31 | CoWorker::$_currentCoId = $coId; 32 | CoTcpConnection::coSend($coId, $socket, $success); 33 | if ($this->onConnect) { 34 | try { 35 | $gen = \call_user_func($this->onConnect, $client); 36 | if ($gen instanceof \Generator) { 37 | $coId = Coworker::genCoId(); 38 | self::addCoroutine($gen, $coId); 39 | CoWorker::addCoroutine($gen, $coId); 40 | // run coroutine 41 | $gen->current(); 42 | } 43 | } catch (\Exception $e) { 44 | Worker::log($e); 45 | exit(250); 46 | } catch (\Error $e) { 47 | Worker::log($e); 48 | exit(250); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * @param CoTcpClient $connection 55 | * @param $data 56 | */ 57 | protected function _onMessage($connection, $data) 58 | { 59 | $socket = $connection->getSocket(); 60 | $coId = self::getCoIdBySocket($socket); 61 | // $coId = CoWorker::getCoIdBySocket($socket); 62 | CoWorker::$_currentCoId = $coId; 63 | CoTcpConnection::coSend($coId, $socket, $data); 64 | if (!$this->onMessage) { 65 | return; 66 | } 67 | try { 68 | // Decode request buffer before Emitting onMessage callback. 69 | $gen = \call_user_func($this->onMessage, $connection, $data); 70 | if ($gen instanceof \Generator) { 71 | $coId = Coworker::genCoId(); 72 | self::addCoroutine($gen, $coId); 73 | CoWorker::addCoroutine($gen, $coId); 74 | // run coroutine 75 | $gen->current(); 76 | } 77 | } catch (\Exception $e) { 78 | Worker::log($e); 79 | exit(250); 80 | } catch (\Error $e) { 81 | Worker::log($e); 82 | exit(250); 83 | } 84 | } 85 | 86 | /** 87 | * @param CoTcpClient $connection 88 | */ 89 | protected function _onClose($connection) 90 | { 91 | // self::removeGeneratorBySocket($connection->getSocket()); 92 | // CoWorker::removeCoroutine(self::getCoIdBySocket($connection->getSocket())); 93 | if ($this->onClose) { 94 | try { 95 | $gen = \call_user_func($this->onClose, $connection); 96 | if ($gen instanceof \Generator) { 97 | $coId = Coworker::genCoId(); 98 | self::addCoroutine($gen, $coId); 99 | CoWorker::addCoroutine($gen, $coId); 100 | // run coroutine 101 | $gen->current(); 102 | } 103 | } catch (\Exception $e) { 104 | Worker::log($e); 105 | exit(250); 106 | } catch (\Error $e) { 107 | Worker::log($e); 108 | exit(250); 109 | } 110 | } 111 | } 112 | 113 | protected function _onError($connection, $code, $msg) 114 | { 115 | if ($this->onError) { 116 | try { 117 | $gen = \call_user_func($this->onError, $connection, $code, $msg); 118 | if ($gen instanceof \Generator) { 119 | $coId = Coworker::genCoId(); 120 | self::addCoroutine($gen, $coId); 121 | CoWorker::addCoroutine($gen, $coId); 122 | // run coroutine 123 | $gen->current(); 124 | } 125 | } catch (\Exception $e) { 126 | Worker::log($e); 127 | exit(250); 128 | } catch (\Error $e) { 129 | Worker::log($e); 130 | exit(250); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Sends data on the connection. 137 | * 138 | * @param mixed $send_buffer 139 | * @param bool $raw 140 | * @return bool|null 141 | */ 142 | public function sendAsync($send_buffer, $raw = false) 143 | { 144 | if ($this->_status === self::STATUS_CLOSING || $this->_status === self::STATUS_CLOSED) { 145 | return false; 146 | } 147 | 148 | // Try to call protocol::encode($send_buffer) before sending. 149 | if (false === $raw && $this->protocol !== null) { 150 | $parser = $this->protocol; 151 | $send_buffer = $parser::encode($send_buffer, $this); 152 | if ($send_buffer === '') { 153 | return; 154 | } 155 | } 156 | 157 | if ($this->_status !== self::STATUS_ESTABLISHED || 158 | ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) 159 | ) { 160 | if ($this->_sendBuffer && $this->bufferIsFull()) { 161 | ++self::$statistics['send_fail']; 162 | return false; 163 | } 164 | $this->_sendBuffer .= $send_buffer; 165 | $this->checkBufferWillFull(); 166 | return; 167 | } 168 | 169 | // Attempt to send data directly. 170 | if ($this->_sendBuffer === '') { 171 | if ($this->transport === 'ssl') { 172 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 173 | $this->_sendBuffer = $send_buffer; 174 | $this->checkBufferWillFull(); 175 | return; 176 | } 177 | $len = 0; 178 | try { 179 | $len = @\fwrite($this->_socket, $send_buffer); 180 | } catch (\Exception $e) { 181 | Worker::log($e); 182 | } catch (\Error $e) { 183 | Worker::log($e); 184 | } 185 | // send successful. 186 | if ($len === \strlen($send_buffer)) { 187 | $this->bytesWritten += $len; 188 | $coId = CoWorker::getCurrentCoId(); 189 | self::setCoIdBySocket($this->_socket, $coId); 190 | CoWorker::setCoIdBySocket($this->_socket, $coId); 191 | $re = yield $this->_socket; 192 | return $re['data']; 193 | } 194 | // Send only part of the data. 195 | if ($len > 0) { 196 | $this->_sendBuffer = \substr($send_buffer, $len); 197 | $this->bytesWritten += $len; 198 | } else { 199 | // Connection closed? 200 | if (!\is_resource($this->_socket) || \feof($this->_socket)) { 201 | ++self::$statistics['send_fail']; 202 | /* 203 | if ($this->onError) { 204 | try { 205 | \call_user_func($this->onError, $this, \WORKERMAN_SEND_FAIL, 'client closed'); 206 | } catch (\Exception $e) { 207 | Worker::log($e); 208 | exit(250); 209 | } catch (\Error $e) { 210 | Worker::log($e); 211 | exit(250); 212 | } 213 | } */ 214 | $this->_onError($this, \WORKERMAN_SEND_FAIL, 'client closed'); 215 | $this->destroy(); 216 | return false; 217 | } 218 | $this->_sendBuffer = $send_buffer; 219 | } 220 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 221 | // Check if the send buffer will be full. 222 | $this->checkBufferWillFull(); 223 | return; 224 | } 225 | 226 | if ($this->bufferIsFull()) { 227 | ++self::$statistics['send_fail']; 228 | return false; 229 | } 230 | 231 | $this->_sendBuffer .= $send_buffer; 232 | // Check if the send buffer is full. 233 | $this->checkBufferWillFull(); 234 | $re = (yield $this->_socket); 235 | return $re['data']; 236 | } 237 | 238 | /** 239 | * Check connection is successfully established or faild. 240 | * 241 | * @return void 242 | */ 243 | public function checkConnection() 244 | { 245 | // Remove EV_EXPECT for windows. 246 | if(\DIRECTORY_SEPARATOR === '\\') { 247 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_EXCEPT); 248 | } 249 | 250 | // Remove write listener. 251 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); 252 | 253 | if ($this->_status !== self::STATUS_CONNECTING) { 254 | return; 255 | } 256 | 257 | // Check socket state. 258 | if ($address = \stream_socket_get_name($this->_socket, true)) { 259 | // Nonblocking. 260 | \stream_set_blocking($this->_socket, false); 261 | // Compatible with hhvm 262 | if (\function_exists('stream_set_read_buffer')) { 263 | \stream_set_read_buffer($this->_socket, 0); 264 | } 265 | // Try to open keepalive for tcp and disable Nagle algorithm. 266 | if (\function_exists('socket_import_stream') && $this->transport === 'tcp') { 267 | $raw_socket = \socket_import_stream($this->_socket); 268 | \socket_set_option($raw_socket, \SOL_SOCKET, \SO_KEEPALIVE, 1); 269 | \socket_set_option($raw_socket, \SOL_TCP, \TCP_NODELAY, 1); 270 | } 271 | 272 | // SSL handshake. 273 | if ($this->transport === 'ssl') { 274 | $this->_sslHandshakeCompleted = $this->doSslHandshake($this->_socket); 275 | if ($this->_sslHandshakeCompleted === false) { 276 | return; 277 | } 278 | } else { 279 | // There are some data waiting to send. 280 | if ($this->_sendBuffer) { 281 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 282 | } 283 | } 284 | 285 | // Register a listener waiting read event. 286 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseReadAsync')); 287 | 288 | $this->_status = self::STATUS_ESTABLISHED; 289 | $this->_remoteAddress = $address; 290 | 291 | // Try to emit onConnect callback. 292 | /* 293 | if ($this->onConnect) { 294 | try { 295 | \call_user_func($this->onConnect, $this); 296 | } catch (\Exception $e) { 297 | Worker::log($e); 298 | exit(250); 299 | } catch (\Error $e) { 300 | Worker::log($e); 301 | exit(250); 302 | } 303 | } */ 304 | // Try to emit protocol::onConnect 305 | if (\method_exists($this->protocol, 'onConnect')) { 306 | try { 307 | \call_user_func(array($this->protocol, 'onConnect'), $this); 308 | } catch (\Exception $e) { 309 | Worker::log($e); 310 | exit(250); 311 | } catch (\Error $e) { 312 | Worker::log($e); 313 | exit(250); 314 | } 315 | } 316 | } else { 317 | // Connection failed. 318 | $this->emitError(\WORKERMAN_CONNECT_FAIL, 'connect ' . $this->_remoteAddress . ' fail after ' . round(\microtime(true) - $this->_connectStartTime, 4) . ' seconds'); 319 | if ($this->_status === self::STATUS_CLOSING) { 320 | $this->destroy(); 321 | } 322 | if ($this->_status === self::STATUS_CLOSED) { 323 | $this->onConnect = null; 324 | } 325 | } 326 | } 327 | 328 | /** 329 | * @param CoTcpConnection $parent 330 | */ 331 | public function checkConnectionAsync($parent) 332 | { 333 | // Remove EV_EXPECT for windows. 334 | if(\DIRECTORY_SEPARATOR === '\\') { 335 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_EXCEPT); 336 | } 337 | 338 | // Remove write listener. 339 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); 340 | 341 | if ($this->_status !== self::STATUS_CONNECTING) { 342 | return; 343 | } 344 | 345 | // Check socket state. 346 | if ($address = \stream_socket_get_name($this->_socket, true)) { 347 | // Nonblocking. 348 | \stream_set_blocking($this->_socket, false); 349 | // Compatible with hhvm 350 | if (\function_exists('stream_set_read_buffer')) { 351 | \stream_set_read_buffer($this->_socket, 0); 352 | } 353 | // Try to open keepalive for tcp and disable Nagle algorithm. 354 | if (\function_exists('socket_import_stream') && $this->transport === 'tcp') { 355 | $raw_socket = \socket_import_stream($this->_socket); 356 | \socket_set_option($raw_socket, \SOL_SOCKET, \SO_KEEPALIVE, 1); 357 | \socket_set_option($raw_socket, \SOL_TCP, \TCP_NODELAY, 1); 358 | } 359 | 360 | // SSL handshake. 361 | if ($this->transport === 'ssl') { 362 | $this->_sslHandshakeCompleted = $this->doSslHandshake($this->_socket); 363 | if ($this->_sslHandshakeCompleted === false) { 364 | return; 365 | } 366 | } else { 367 | // There are some data waiting to send. 368 | if ($this->_sendBuffer) { 369 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 370 | } 371 | } 372 | 373 | // Register a listener waiting read event. 374 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseReadAsync'), array($parent)); 375 | 376 | $this->_status = self::STATUS_ESTABLISHED; 377 | $this->_remoteAddress = $address; 378 | 379 | // Try to emit onConnect callback. 380 | /* 381 | if ($this->onConnect) { 382 | try { 383 | \call_user_func($this->onConnect, $this); 384 | } catch (\Exception $e) { 385 | Worker::log($e); 386 | exit(250); 387 | } catch (\Error $e) { 388 | Worker::log($e); 389 | exit(250); 390 | } 391 | } */ 392 | $this->_onConnect($this, true); 393 | // Try to emit protocol::onConnect 394 | if (\method_exists($this->protocol, 'onConnect')) { 395 | try { 396 | \call_user_func(array($this->protocol, 'onConnect'), $this); 397 | } catch (\Exception $e) { 398 | Worker::log($e); 399 | exit(250); 400 | } catch (\Error $e) { 401 | Worker::log($e); 402 | exit(250); 403 | } 404 | } 405 | } else { 406 | // Connection failed. 407 | $this->emitError(\WORKERMAN_CONNECT_FAIL, 'connect ' . $this->_remoteAddress . ' fail after ' . round(\microtime(true) - $this->_connectStartTime, 4) . ' seconds'); 408 | $this->_onConnect($this, false); 409 | if ($this->_status === self::STATUS_CLOSING) { 410 | $this->destroy(); 411 | } 412 | if ($this->_status === self::STATUS_CLOSED) { 413 | $this->onConnect = null; 414 | } 415 | } 416 | } 417 | /** 418 | * Do connect. 419 | * 420 | * @return void 421 | */ 422 | public function connect() 423 | { 424 | if ($this->_status !== self::STATUS_INITIAL && $this->_status !== self::STATUS_CLOSING && 425 | $this->_status !== self::STATUS_CLOSED) { 426 | return; 427 | } 428 | $this->_status = self::STATUS_CONNECTING; 429 | $this->_connectStartTime = \microtime(true); 430 | if ($this->transport !== 'unix') { 431 | if (!$this->_remotePort) { 432 | $this->_remotePort = $this->transport === 'ssl' ? 443 : 80; 433 | $this->_remoteAddress = $this->_remoteHost.':'.$this->_remotePort; 434 | } 435 | // Open socket connection asynchronously. 436 | if ($this->_contextOption) { 437 | $context = \stream_context_create($this->_contextOption); 438 | $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", 439 | $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT, $context); 440 | } else { 441 | $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", 442 | $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT); 443 | } 444 | } else { 445 | $this->_socket = \stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0, 446 | \STREAM_CLIENT_ASYNC_CONNECT); 447 | } 448 | // If failed attempt to emit onError callback. 449 | if (!$this->_socket || !\is_resource($this->_socket)) { 450 | $this->emitError(\WORKERMAN_CONNECT_FAIL, $errstr); 451 | if ($this->_status === self::STATUS_CLOSING) { 452 | $this->destroy(); 453 | } 454 | if ($this->_status === self::STATUS_CLOSED) { 455 | $this->onConnect = null; 456 | } 457 | return; 458 | } 459 | $coId = CoWorker::getCurrentCoId(); 460 | self::setCoIdBySocket($this->_socket, $coId); 461 | CoWorker::setCoIdBySocket($this->_socket, $coId); 462 | // Add socket to global event loop waiting connection is successfully established or faild. 463 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'checkConnection')); 464 | // For windows. 465 | if(\DIRECTORY_SEPARATOR === '\\') { 466 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_EXCEPT, array($this, 'checkConnection')); 467 | } 468 | } 469 | 470 | public function _connectAsync($parent) 471 | { 472 | if ($this->_status !== self::STATUS_INITIAL && $this->_status !== self::STATUS_CLOSING && 473 | $this->_status !== self::STATUS_CLOSED) { 474 | return; 475 | } 476 | $this->_status = self::STATUS_CONNECTING; 477 | $this->_connectStartTime = \microtime(true); 478 | if ($this->transport !== 'unix') { 479 | if (!$this->_remotePort) { 480 | $this->_remotePort = $this->transport === 'ssl' ? 443 : 80; 481 | $this->_remoteAddress = $this->_remoteHost.':'.$this->_remotePort; 482 | } 483 | // Open socket connection asynchronously. 484 | if ($this->_contextOption) { 485 | $context = \stream_context_create($this->_contextOption); 486 | $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", 487 | $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT, $context); 488 | } else { 489 | $this->_socket = \stream_socket_client("tcp://{$this->_remoteHost}:{$this->_remotePort}", 490 | $errno, $errstr, 0, \STREAM_CLIENT_ASYNC_CONNECT); 491 | } 492 | } else { 493 | $this->_socket = \stream_socket_client("{$this->transport}://{$this->_remoteAddress}", $errno, $errstr, 0, 494 | \STREAM_CLIENT_ASYNC_CONNECT); 495 | } 496 | // If failed attempt to emit onError callback. 497 | if (!$this->_socket || !\is_resource($this->_socket)) { 498 | $this->emitError(\WORKERMAN_CONNECT_FAIL, $errstr); 499 | if ($this->_status === self::STATUS_CLOSING) { 500 | $this->destroy(); 501 | } 502 | if ($this->_status === self::STATUS_CLOSED) { 503 | $this->onConnect = null; 504 | } 505 | return; 506 | } 507 | $coId = CoWorker::getCurrentCoId(); 508 | self::setCoIdBySocket($this->_socket, $coId); 509 | CoWorker::setCoIdBySocket($this->_socket, $coId); 510 | // Add socket to global event loop waiting connection is successfully established or faild. 511 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_WRITE, array($this, 'checkConnectionAsync'), array($parent)); 512 | // For windows. 513 | if(\DIRECTORY_SEPARATOR === '\\') { 514 | Worker::$globalEvent->add($this->_socket, EventInterface::EV_EXCEPT, array($this, 'checkConnectionAsync'), array($parent)); 515 | } 516 | $re = yield $this->_socket; 517 | return $re['data']; 518 | } 519 | 520 | /** 521 | * @param CoTcpConnection $parent 522 | * 523 | * @return \Generator|void|Promise 524 | */ 525 | public function connectAsync($parent) 526 | { 527 | return yield from $this->_connectAsync($parent); 528 | } 529 | 530 | /** 531 | * Base read handler. 532 | * 533 | * @param resource $socket 534 | * @param bool $check_eof 535 | * @return void 536 | */ 537 | public function baseReadAsync($socket, $check_eof = true) 538 | { 539 | // SSL handshake. 540 | if ($this->transport === 'ssl' && $this->_sslHandshakeCompleted !== true) { 541 | if ($this->doSslHandshake($socket)) { 542 | $this->_sslHandshakeCompleted = true; 543 | if ($this->_sendBuffer) { 544 | Worker::$globalEvent->add($socket, EventInterface::EV_WRITE, array($this, 'baseWrite')); 545 | } 546 | } else { 547 | return; 548 | } 549 | } 550 | 551 | $buffer = ''; 552 | try { 553 | $buffer = @\fread($socket, self::READ_BUFFER_SIZE); 554 | } catch (\Exception $e) { 555 | } catch (\Error $e) { 556 | } 557 | 558 | // Check connection closed. 559 | if ($buffer === '' || $buffer === false) { 560 | if ($check_eof && (\feof($socket) || !\is_resource($socket) || $buffer === false)) { 561 | $this->destroy(); 562 | return; 563 | } 564 | } else { 565 | $this->bytesRead += \strlen($buffer); 566 | $this->_recvBuffer .= $buffer; 567 | } 568 | 569 | // If the application layer protocol has been set up. 570 | if ($this->protocol !== null) { 571 | $parser = $this->protocol; 572 | while ($this->_recvBuffer !== '' && !$this->_isPaused) { 573 | // The current packet length is known. 574 | if ($this->_currentPackageLength) { 575 | // Data is not enough for a package. 576 | if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { 577 | break; 578 | } 579 | } else { 580 | // Get current package length. 581 | try { 582 | $this->_currentPackageLength = $parser::input($this->_recvBuffer, $this); 583 | } catch (\Exception $e) { 584 | } catch (\Error $e) { 585 | } 586 | // The packet length is unknown. 587 | if ($this->_currentPackageLength === 0) { 588 | break; 589 | } elseif ($this->_currentPackageLength > 0 && $this->_currentPackageLength <= $this->maxPackageSize) { 590 | // Data is not enough for a package. 591 | if ($this->_currentPackageLength > \strlen($this->_recvBuffer)) { 592 | break; 593 | } 594 | } // Wrong package. 595 | else { 596 | Worker::safeEcho('Error package. package_length=' . \var_export($this->_currentPackageLength, true)); 597 | $this->destroy(); 598 | return; 599 | } 600 | } 601 | 602 | // The data is enough for a packet. 603 | ++self::$statistics['total_request']; 604 | // The current packet length is equal to the length of the buffer. 605 | if (\strlen($this->_recvBuffer) === $this->_currentPackageLength) { 606 | $one_request_buffer = $this->_recvBuffer; 607 | $this->_recvBuffer = ''; 608 | } else { 609 | // Get a full package from the buffer. 610 | $one_request_buffer = \substr($this->_recvBuffer, 0, $this->_currentPackageLength); 611 | // Remove the current package from the receive buffer. 612 | $this->_recvBuffer = \substr($this->_recvBuffer, $this->_currentPackageLength); 613 | } 614 | // Reset the current packet length to 0. 615 | $this->_currentPackageLength = 0; 616 | /* 617 | if (!$this->onMessage) { 618 | continue; 619 | } 620 | try { 621 | // Decode request buffer before Emitting onMessage callback. 622 | $gen = \call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this)); 623 | if ($gen instanceof \Generator) { 624 | $coId = Coworker::genCoId(); 625 | self::$_coroutines[$coId] = $gen; 626 | // run coroutine 627 | $gen->current(); 628 | } 629 | } catch (\Exception $e) { 630 | Worker::log($e); 631 | exit(250); 632 | } catch (\Error $e) { 633 | Worker::log($e); 634 | exit(250); 635 | } 636 | */ 637 | $msg = $parser::decode($one_request_buffer, $this); 638 | $this->_onMessage($this, $msg); 639 | $coId = self::getCoIdBySocket($socket); 640 | // $coId = CoWorker::getCoIdBySocket($socket); 641 | // CoTcpConnection::coSend($coId, $socket, $msg); 642 | } 643 | return; 644 | } 645 | 646 | if ($this->_recvBuffer === '' || $this->_isPaused) { 647 | return; 648 | } 649 | 650 | // Applications protocol is not set. 651 | ++self::$statistics['total_request']; 652 | /* 653 | if (!$this->onMessage) { 654 | $this->_recvBuffer = ''; 655 | return; 656 | } 657 | try { 658 | \call_user_func($this->onMessage, $this, $this->_recvBuffer); 659 | } catch (\Exception $e) { 660 | Worker::log($e); 661 | exit(250); 662 | } catch (\Error $e) { 663 | Worker::log($e); 664 | exit(250); 665 | } 666 | */ 667 | $this->_onMessage($this, $this->_recvBuffer); 668 | // Clean receive buffer. 669 | $this->_recvBuffer = ''; 670 | } 671 | 672 | /** 673 | * Destroy connection. 674 | * 675 | * @return void 676 | */ 677 | public function destroy() 678 | { 679 | // Avoid repeated calls. 680 | if ($this->_status === self::STATUS_CLOSED) { 681 | return; 682 | } 683 | // Remove event listener. 684 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_READ); 685 | Worker::$globalEvent->del($this->_socket, EventInterface::EV_WRITE); 686 | 687 | // Close socket. 688 | try { 689 | @\fclose($this->_socket); 690 | } catch (\Exception $e) { 691 | } catch (\Error $e) { 692 | } 693 | 694 | $this->_status = self::STATUS_CLOSED; 695 | // Try to emit onClose callback. 696 | /* 697 | if ($this->onClose) { 698 | try { 699 | \call_user_func($this->onClose, $this); 700 | } catch (\Exception $e) { 701 | Worker::log($e); 702 | exit(250); 703 | } catch (\Error $e) { 704 | Worker::log($e); 705 | exit(250); 706 | } 707 | }*/ 708 | $this->_onClose($this); 709 | // Try to emit protocol::onClose 710 | if ($this->protocol && \method_exists($this->protocol, 'onClose')) { 711 | try { 712 | \call_user_func(array($this->protocol, 'onClose'), $this); 713 | } catch (\Exception $e) { 714 | Worker::log($e); 715 | exit(250); 716 | } catch (\Error $e) { 717 | Worker::log($e); 718 | exit(250); 719 | } 720 | } 721 | $this->_sendBuffer = $this->_recvBuffer = ''; 722 | $this->_currentPackageLength = 0; 723 | $this->_isPaused = $this->_sslHandshakeCompleted = false; 724 | if ($this->_status === self::STATUS_CLOSED) { 725 | // Cleaning up the callback to avoid memory leaks. 726 | $this->onMessage = $this->onClose = $this->onError = $this->onBufferFull = $this->onBufferDrain = null; 727 | // Remove from worker->connections. 728 | if ($this->worker) { 729 | unset($this->worker->connections[$this->_id]); 730 | } 731 | unset(static::$connections[$this->_id]); 732 | } 733 | } 734 | 735 | /** 736 | * Destruct. 737 | * 738 | * @return void 739 | */ 740 | public function __destruct() 741 | { 742 | parent::__destruct(); 743 | } 744 | 745 | /** 746 | * @throws \Exception 747 | */ 748 | public function __clone() 749 | { 750 | // TODO: Implement __clone() method. 751 | throw new \Exception('你就不能new一个么,非要clone!'); 752 | } 753 | 754 | } 755 | --------------------------------------------------------------------------------