├── example ├── wwwcounter.txt ├── taskManagerClient.php ├── www │ ├── fpm_yar_db.php │ └── fpm_yar_test.php ├── init.inc.php ├── service │ ├── Test.php │ └── Db.php ├── server_swoole.php ├── client1.php ├── benckmark │ ├── testSimple.php │ ├── mkTestData.php │ ├── testConcurrent.php │ └── lib.php ├── client_mul.php ├── server_plug.php ├── server.php ├── client_plug.php ├── client2.php ├── server1.php └── taskManagerServer.php ├── src ├── RuntimeException.php ├── encoder │ ├── EncoderInterface.php │ ├── EncoderPHP.php │ ├── EncoderJson.php │ └── EncoderMsgpack.php ├── log │ ├── LogInterface.php │ ├── File.php │ ├── TraitLog.php │ └── Log.php ├── event │ ├── InterfaceListen.php │ ├── InterfaceEventDispatcher.php │ └── TraitEventManager.php ├── Token.php ├── plug │ ├── LogSample.php │ ├── Admin.php │ └── ProfilerSample.php ├── Response.php ├── Request.php ├── Debug.php ├── Yar.php ├── Packer.php ├── Dispatcher.php ├── TaskManager.php ├── Server.php └── Protocol.php ├── tests ├── autoload.php ├── syar │ └── PackerTest.php ├── ClientTest.php └── client.php ├── .gitignore ├── composer.json └── Readme.md /example/wwwcounter.txt: -------------------------------------------------------------------------------- 1 | 2 -------------------------------------------------------------------------------- /example/taskManagerClient.php: -------------------------------------------------------------------------------- 1 | handle(); -------------------------------------------------------------------------------- /example/www/fpm_yar_test.php: -------------------------------------------------------------------------------- 1 | handle(); -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- 1 | addPsr4("sar\\", __DIR__ . '/syar'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Example user template template 3 | ### Example user template 4 | 5 | # IntelliJ project files 6 | .idea 7 | *.iml 8 | out 9 | gen 10 | vendor 11 | composer.lock 12 | tests/phpunit.phar -------------------------------------------------------------------------------- /src/encoder/EncoderInterface.php: -------------------------------------------------------------------------------- 1 | addPsr4('syar\\example\\service\\', __DIR__ . '/service'); -------------------------------------------------------------------------------- /src/log/LogInterface.php: -------------------------------------------------------------------------------- 1 | set([ 5 | 'http_parse_post' => false 6 | ]); 7 | 8 | $http->on('request', function ($request, $response) { 9 | echo $request->rawContent(); 10 | $response->end("
";
14 | var_dump($name);
15 | var_dump($age);
16 |
--------------------------------------------------------------------------------
/example/benckmark/testSimple.php:
--------------------------------------------------------------------------------
1 | [
10 | 'api' => '/test',
11 | 'method' => 'getAge',
12 | 'params' => []
13 | ],
14 | 'name' => [
15 | 'api' => '/test',
16 | 'method' => 'getName',
17 | 'params' => ['tester']
18 | ]
19 | ];
20 | $rs = $client->calls($requests);
21 |
22 | //
23 | echo "";
24 | var_dump($rs);
25 |
--------------------------------------------------------------------------------
/example/server_plug.php:
--------------------------------------------------------------------------------
1 | setLogger(new Log());
12 | $server->getProtocol()->getProcessor()->setNs($apiNs);
13 |
14 | // add plug
15 | $server->addPlug(new \syar\plug\Admin());
16 | $server->addPlug(new \syar\plug\LogSample(), false);
17 |
18 | // reg task for log
19 | $server->getTaskManager()->regTask('log', function($log){
20 | echo $log;
21 | });
22 |
23 | $server->run();
--------------------------------------------------------------------------------
/example/server.php:
--------------------------------------------------------------------------------
1 | setLogger(new Log());
11 | $service = new \syar\example\service\Test();
12 | $server->setDispatcher(function(\syar\Token $token, $isDocument) use ($service){
13 | if(!$isDocument){
14 | $method = $token->getMethod();
15 | $params = $token->getArgs();
16 | $value = call_user_func_array(array($service, $method), $params);
17 | } else {
18 | $value = "Yar api document
";
19 | }
20 | return $value;
21 | });
22 |
23 | $server->run(['max_request' => 10000]);
--------------------------------------------------------------------------------
/example/client_plug.php:
--------------------------------------------------------------------------------
1 | [
10 | 'api' => '/test',
11 | 'method' => 'getAge',
12 | 'params' => []
13 | ],
14 | 'name' => [
15 | 'api' => '/test',
16 | 'method' => 'getName',
17 | 'params' => ['tester']
18 | ]];
19 | $rs = $client->calls($requests);
20 |
21 | // status
22 | $status = file_get_contents('http://127.0.0.1:5604/admin/status');
23 | //$closed = file_get_contents('http://127.0.0.1:5604/admin/stop');
24 | //
25 | echo "";
26 | var_dump($rs);
27 | var_dump($status);
28 |
--------------------------------------------------------------------------------
/example/client2.php:
--------------------------------------------------------------------------------
1 | ";
28 | var_dump($data);
--------------------------------------------------------------------------------
/example/benckmark/mkTestData.php:
--------------------------------------------------------------------------------
1 | run($c, function(swoole_process $worker) use($step){
14 | echo "Worker {$worker->pid} start\n";
15 | $dbo = \syar\example\service\Db::getDb();
16 | for($x = 0; $x < $step; $x++){
17 | $name = 'title_' . ($x);
18 | $age = rand(10, 50);
19 | $sex = rand(1, 2);
20 | $sql = "insert tmp_1(title, sex, age) value ('{$name}', {$sex}, {$age})";
21 | $dbo->query($sql);
22 | }
23 | $worker->exit(0);
24 | });
25 | }
26 |
27 | // 50进程生成1万测试数据
28 | insertTestData(50, 10000);
--------------------------------------------------------------------------------
/example/benckmark/testConcurrent.php:
--------------------------------------------------------------------------------
1 | =5.4.16",
29 | "ext-swoole": ">=1.8.8",
30 | "ext-msgpack": "*"
31 | },
32 |
33 | "require-dev" : {
34 | },
35 |
36 | "config" : {
37 | "secure-http" : false
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/example/server1.php:
--------------------------------------------------------------------------------
1 | setLogger(new Log());
12 | $server->getProtocol()->getProcessor()->setNs($apiNs);
13 | $server->run([
14 | 'max_connection' => 20480,
15 | 'worker_num' => 48,
16 | 'task_worker_num' => 16,
17 | ]);
18 |
19 | /**
20 | * @see http://wiki.swoole.com/wiki/page/p-server/sysctl.html
21 |
22 | 内核参数调整
23 | ulimit -n 655350
24 |
25 | # time_wait
26 | sysctl net.ipv4.tcp_tw_recycle=1
27 | sysctl net.ipv4.tcp_tw_reuse=1
28 |
29 | # 进程间通信
30 | sysctl net.unix.max_dgram_qlen=100
31 |
32 | # socket buffer
33 | sysctl net.core.wmem_default=8388608
34 | sysctl net.core.rmem_default=8388608
35 | sysctl net.core.rmem_default=16777216
36 | sysctl net.core.wmem_default=16777216
37 |
38 | # 消息队列
39 | sysctl kernel.msgmnb=4203520
40 | sysctl kernel.msgmni=64
41 | sysctl kernel.msgmax=8192
42 | */
--------------------------------------------------------------------------------
/src/log/File.php:
--------------------------------------------------------------------------------
1 | file = $file;
16 | }
17 |
18 | /**
19 | * @param string $file
20 | */
21 | public function setFile($file) {
22 | $this->file = $file;
23 | }
24 |
25 | protected function getFile($type){
26 | return $this->file;
27 | }
28 |
29 | protected function dispose($message, $type = 'info'){
30 | file_put_contents(
31 | $this->getFile($type),
32 | $this->format($message, $type),
33 | FILE_APPEND
34 | );
35 | }
36 |
37 | /**
38 | * @return bool|int
39 | */
40 | public function logrotate(){
41 | if(file_exists($this->file)){
42 | $size = filesize($this->file);
43 | if($size > $this->maxSize){
44 | return file_put_contents($this->file, '');
45 | }
46 | }
47 | return true;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/event/TraitEventManager.php:
--------------------------------------------------------------------------------
1 | bind($this);
18 | }
19 |
20 | public function on($name, $callback){
21 | $this->__ECallback[$name][] = $callback;
22 | }
23 |
24 | public function off($name = null) {
25 | if(!$name){
26 | $this->__ECallback = [];
27 | }else{
28 | unset($this->__ECallback[$name]);
29 | }
30 | }
31 |
32 | public function trigger($event) {
33 | $args = func_get_args();
34 | array_shift($args);
35 |
36 | foreach($this->__ECallback[$event] as $callback){
37 | if(true === call_user_func_array($callback, $args)){
38 | break;
39 | }
40 | }
41 | }
42 |
43 | public function hasListener($name){
44 | return isset($this->__ECallback[$name]);
45 | }
46 | }
--------------------------------------------------------------------------------
/src/log/TraitLog.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
23 | return $this;
24 | }
25 |
26 | /**
27 | * @return LogInterface|Monolog
28 | */
29 | public function getLogger() {
30 | return isset($this->logger) ? $this->logger : null;
31 | }
32 |
33 | /**
34 | * @param $message
35 | * @param string $type
36 | * @param array $context
37 | */
38 | protected function log($message, $type = 'info', $context = []){
39 | if(!isset($this->logger)){
40 | return;
41 | }
42 |
43 | if($this->logger instanceof LogInterface) {
44 | $this->logger->log($message, $type);
45 | } else {
46 | $this->logger->log($type, $message, $context);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/example/service/Db.php:
--------------------------------------------------------------------------------
1 | true]);
27 | return self::$pdo;
28 | }
29 |
30 | public function getInfo($id){
31 | $sql = "select * from tmp_1 where id=" . intval($id);
32 | //echo $sql . "\n";
33 | $set = self::getDb()->query($sql);
34 | $info = $set->fetch(PDO::FETCH_ASSOC);
35 | return $info;
36 | }
37 |
38 | public function getList($start = 0, $limit = 10){
39 | $start = intval($start);
40 | $limit = intval($limit);
41 | $sql = "select * from tmp_1 where 1 limit {$limit} offset {$start}";
42 | $set = self::getDb()->query($sql);
43 | $list = $set->fetchAll(PDO::FETCH_ASSOC);
44 | return $list;
45 | }
46 | }
--------------------------------------------------------------------------------
/example/taskManagerServer.php:
--------------------------------------------------------------------------------
1 | set([
8 | 'task_worker_num' => 2,
9 | 'http_parse_post' => false,
10 | ]);
11 |
12 | $taskManager = new TaskManager($http);
13 |
14 | $taskManager->regTask('test', function($hello = 'hello world'){
15 | echo "run task test:\n";
16 | echo $hello . "\n";
17 | throw new Exception("has error");
18 | });
19 |
20 | $taskManager->regTask('send_mail', function($address = 'email@address', $content = "mail body"){
21 | echo "run task send_mail:\n";
22 | echo $address . "\n";
23 | echo $content . "\n";
24 | return true;
25 | });
26 |
27 | $http->on('request', function ($request, $response) use ($taskManager) {
28 | echo "\n";
29 | $taskManager->doTask('test', ['Hello world']);
30 | echo "---------\n";
31 | $taskManager->doTasks([
32 | ['test', "Hello world 1"],
33 | ['send_mail', ['test@address', 'mail for test']]
34 | ]);
35 | echo "---------\n";
36 | $taskManager->doTasksAsync([
37 | ['test', "Hello world 1"],
38 | ['send_mail', ['test@address', 'mail for test']]
39 | ], function($rs){
40 | var_dump($rs);
41 | });
42 | $response->end("Hello Swoole. #".rand(1000, 9999)."
");
43 | });
44 |
45 | $http->start();
--------------------------------------------------------------------------------
/src/Token.php:
--------------------------------------------------------------------------------
1 | class = $class;
24 | $this->method = $method;
25 | $this->args = $args;
26 | $this->options = $options;
27 | }
28 |
29 | /**
30 | * @return mixed
31 | */
32 | public function getClass(){
33 | return trim($this->class, '\\/ ');
34 | }
35 |
36 | /**
37 | * @return mixed
38 | */
39 | public function getMethod(){
40 | return $this->method;
41 | }
42 |
43 | /**
44 | * @return mixed
45 | */
46 | public function getArgs(){
47 | return $this->args;
48 | }
49 |
50 | /**
51 | * @param $key
52 | * @param null $def
53 | * @return mixed|array
54 | */
55 | public function getOption($key = null, $def = null){
56 | if(!$key){
57 | return $this->options;
58 | }
59 | return isset($this->options[$key]) ? $this->options[$key] : $def;
60 | }
61 |
62 | /**
63 | * /API_PATH?args
64 | * /?api=API_PATH
65 | * @return string
66 | */
67 | public function getApi(){
68 | $api = $this->class ?: $this->getOption('api');
69 | $api = str_replace('/', ".", $api);
70 | if(!$api){
71 | return '';
72 | }
73 |
74 | if($this->method){
75 | $api .= "." . $this->method;
76 | }
77 | return $api;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/plug/LogSample.php:
--------------------------------------------------------------------------------
1 | start = $this->_time();
26 | }
27 |
28 | protected $start;
29 |
30 | /**
31 | * @param mixed $rs
32 | * @param Token $token
33 | * @param Protocol $protocol
34 | */
35 | public function onRequest2($rs, $token, $protocol){
36 | $message = $token->getClass()
37 | . "({$token->getMethod()})" . "\t"
38 | . round($this->_time() - $this->start, 5) . "\t"
39 | . json_encode($token->getArgs());
40 | $log = date("Y-m-d H:i:s") . " " . $message . "\n";
41 |
42 | $taskManager = $protocol->server->getTaskManager();
43 | if($taskManager->has('log')){
44 | $taskManager->doTask('log', [$log]);
45 | } else {
46 | echo $log;
47 | }
48 | }
49 |
50 | /**
51 | * @param Dispatcher $em
52 | */
53 | function bind($em){
54 | $em->on($em::EVENT_REQUEST_BEFORE, array($this, 'onRequest1'));
55 | $em->on($em::EVENT_REQUEST_AFTER, array($this, 'onRequest2'));
56 | }
57 | }
--------------------------------------------------------------------------------
/tests/syar/PackerTest.php:
--------------------------------------------------------------------------------
1 | unpack($this->getData());
14 |
15 | $this->assertEquals($yar->packer['packName'], 'MSGPACK');
16 | $this->assertEquals($yar->getRequestMethod(), 'add');
17 | $this->assertEquals($yar->getRequestParams(), [1, 2]);
18 | }
19 |
20 | function testPack(){
21 | $packer = new Packer();
22 | $requestData = $this->getData();
23 | $yar = $packer->unpack($requestData);
24 |
25 | $rs = ['test'];
26 | $yar->setReturnValue($rs);
27 |
28 | $responseData = $this->getData(2, $rs);
29 | $packData = $packer->pack($yar);
30 |
31 | $this->assertEquals($packData, $responseData);
32 | }
33 |
34 | protected function getData($type = 1, $rs = []){
35 | $data = [
36 | 'i' => '3594717145',
37 | 'm' => 'add',
38 | 'p' => [1, 2],
39 | ];
40 | if($type == 2){
41 | $data = [
42 | 'r' => $rs,
43 | 'i' => $data['i'],
44 | 's' => 0
45 | ];
46 | }
47 |
48 | $header = [
49 | 'id' => 12903494,
50 | 'Version' => 0,
51 | 'MagicNum' => 0x80DFEC60,
52 | 'Reserved' => 0,
53 | 'Provider' => '',
54 | 'Token' => '',
55 | 'BodyLen' => 27,
56 | ];
57 | $data = msgpack_pack($data);
58 | $header['BodyLen'] = strlen($data) + 8;
59 | $header = pack('NnNNa32a32N',
60 | $header['id'],
61 | $header['Version'],
62 | $header['MagicNum'],
63 | $header['Reserved'],
64 | $header['Provider'],
65 | $header['Token'],
66 | $header['BodyLen']
67 | );
68 | return $header . "MSGPACK " . $data;
69 | }
70 | }
--------------------------------------------------------------------------------
/tests/ClientTest.php:
--------------------------------------------------------------------------------
1 | getUrl($api));
15 | }
16 |
17 | protected function getUrl($api = 'test'){
18 | return $url = "http://127.0.0.1:5604/{$api}";
19 | }
20 |
21 | function testRpc1(){
22 | $client = $this->getYarClient();
23 | $name = $client->getName("tester");
24 | $age = $client->getAge();
25 |
26 | $this->assertEquals($name, 'tester Hello');
27 | $this->assertEquals($age, 20);
28 | }
29 |
30 | function testConcurrentClient(){
31 | $url = $this->getUrl();
32 |
33 | $data = [];
34 | Yar_Concurrent_Client::call($url, "getName", ['tester'],
35 | function($rs) use (& $data){
36 | $data['name'] = $rs;
37 | }
38 | );
39 | Yar_Concurrent_Client::call($url, "getAge", [],
40 | function($rs) use (& $data){
41 | $data['age'] = $rs;
42 | }
43 | );
44 | Yar_Concurrent_Client::loop();
45 |
46 | $this->assertEquals($data['name'], 'tester Hello');
47 | $this->assertEquals($data['age'], 20);
48 | }
49 |
50 | function testBatch(){
51 | $client = $this->getYarClient('multiple');
52 | $requests = [
53 | 'age' => [
54 | 'api' => '/test',
55 | 'method' => 'getAge',
56 | 'params' => []
57 | ],
58 | 'name' => [
59 | 'api' => '/test',
60 | 'method' => 'getName',
61 | 'params' => ['tester']
62 | ]];
63 | $rs = $client->calls($requests);
64 | $this->assertEquals($rs['name'], 'tester Hello');
65 | $this->assertEquals($rs['age'], 20);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/plug/Admin.php:
--------------------------------------------------------------------------------
1 | getPath();
25 | if(strpos($path, $this->pathPrefix) !== 0){
26 | return false;
27 | }
28 |
29 | $server = $protocol->server->getSwooleServer();
30 | $cmd = str_replace($this->pathPrefix, '', $path);
31 | switch($cmd){
32 | case "status" :
33 | $rs = $server->stats();
34 | break;
35 | case "reload" :
36 | $rs = $server->reload();
37 | break;
38 | case "stop" :
39 | $rs = $server->shutdown();
40 | break;
41 | default :
42 | $rs = "invalid command";
43 | }
44 |
45 | $rs = var_export($rs, true);
46 | if($request->get('pretty')){
47 | $rs = "\n{$rs}";
48 | }
49 | $response->body = $rs;
50 | $response->send();
51 |
52 | return true;
53 | }
54 |
55 | /**
56 | * @param Protocol $em
57 | */
58 | function bind($em){
59 | $em->on($em::EVENT_REQUEST_BEFORE, array($this, 'onRequest'));
60 | }
61 | }
--------------------------------------------------------------------------------
/src/plug/ProfilerSample.php:
--------------------------------------------------------------------------------
1 | counter = self::$counter;
33 | $this->queue[self::$counter] = [
34 | 'start' => $this->_time(),
35 | 'api' => $request->getPath(),
36 | 'method' => $request->yar ? $request->yar->getRequestMethod() : '',
37 | ];
38 | $this->queueLen++;
39 | }
40 |
41 | function onRequest2($request){
42 | $index = $request->counter;
43 | $this->queue[$index]['time'] = $this->_time() - $this->queue[$index]['start'];
44 |
45 | if($this->queueLen >= $this->queueItemMax){
46 | var_dump($this->queue);
47 | $this->queueLen = 0;
48 | $this->queue = [];
49 | }
50 | }
51 |
52 | /**
53 | * @param Protocol $em
54 | */
55 | function bind($em){
56 | $em->on($em::EVENT_REQUEST_BEFORE, array($this, 'onRequest1'));
57 | $em->on($em::EVENT_RESPONSE_AFTER, array($this, 'onRequest2'));
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Response.php:
--------------------------------------------------------------------------------
1 | response = $response;
20 | }
21 |
22 | function __call($name, $arguments) {
23 | return call_user_func_array(array($this->response, $name), $arguments);
24 | }
25 |
26 | /**
27 | * 设置Http状态
28 | * @param $code
29 | */
30 | function setHttpStatus($code) {
31 | $this->response->status($code);
32 | }
33 |
34 | /**
35 | * 设置Http头信息
36 | * @param $key
37 | * @param $value
38 | */
39 | function setHeader($key, $value) {
40 | $this->response->header($key, $value);
41 | }
42 |
43 | function gzip($level = 1){
44 | $this->response->gzip($level);
45 | }
46 |
47 | public $body;
48 |
49 | /**
50 | * 添加http header
51 | * @param $header
52 | */
53 | function addHeaders(array $header) {
54 | foreach($header as $key => $value)
55 | $this->response->header($key, $value);
56 | }
57 |
58 | function noCache() {
59 | $this->response->header('CacheListener-Control',
60 | 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
61 | $this->response->header('Pragma','no-cache');
62 | }
63 |
64 | protected $isSend = false;
65 |
66 | function send(){
67 | $this->isSend = true;
68 | $this->response->end($this->body);
69 | }
70 |
71 | public function isSend(){
72 | return $this->isSend;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/log/Log.php:
--------------------------------------------------------------------------------
1 | log($message, $type);
32 | }
33 |
34 | /**
35 | * @param mixed $mask
36 | */
37 | public function setMask($mask) {
38 | $this->mask = $mask;
39 | }
40 |
41 | protected function format($message, $type){
42 | if(!$message){
43 | return "\n";
44 | }
45 | return "[{$type} " . microtime(true) . '] - ' . var_export($message, true) . PHP_EOL;
46 | }
47 |
48 | protected function getMask($type){
49 | $types = [
50 | 'info' => self::INFO,
51 | 'warning' => self::WARNING,
52 | 'error' => self::ERROR,
53 | 'debug' => self::DEBUG,
54 | ];
55 | return isset($types[$type]) ? $types[$type] : self::INFO;
56 | }
57 |
58 | public function log($message, $type = 'info'){
59 | if($this->mask & $this->getMask($type)){
60 | $this->dispose($message, $type);
61 | }
62 | return $this;
63 | }
64 |
65 | protected function dispose($message, $type = 'info'){
66 | echo $this->format($message, $type);
67 | }
68 |
69 | public function logrotate() {
70 |
71 | }
72 | }
--------------------------------------------------------------------------------
/src/Request.php:
--------------------------------------------------------------------------------
1 | request = $request;
27 | }
28 |
29 | function __call($name, $arguments) {
30 | return call_user_func_array(array($this->request, $name), $arguments);
31 | }
32 |
33 | function getHeader($key = null, $def = null){
34 | if(!$key){
35 | return $this->request->header;
36 | }
37 | return isset($this->request->header[$key]) ? $this->request->header[$key] : $def;
38 | }
39 |
40 | function getUri(){
41 | return $this->request->server['request_uri'];
42 | }
43 |
44 | function getPath(){
45 | return $this->request->server['path_info'];
46 | }
47 |
48 | function getIp() {
49 | return $this->request->server['remote_addr'];
50 | }
51 |
52 | function getMethod(){
53 | return $this->request->server['request_method'];
54 | }
55 |
56 | public function isPost(){
57 | return $this->request->server['request_method'] == 'POST';
58 | }
59 |
60 | function getPost(){
61 | return $this->request->post;
62 | }
63 |
64 | function getGet(){
65 | return isset($this->request->get) ? $this->request->get : [];
66 | }
67 |
68 | function get($key){
69 | return isset($this->request->get[$key]) ? $this->request->get[$key] : null;
70 | }
71 |
72 | function getYarMethod() {
73 | return $this->yar->getRequestMethod();
74 | }
75 |
76 | public function getYarParams(){
77 | return $this->yar->getRequestParams();
78 | }
79 | }
--------------------------------------------------------------------------------
/src/Debug.php:
--------------------------------------------------------------------------------
1 | $t) {
48 | if (!isset($t['file'])) {
49 | $t['file'] = 'unknown';
50 | } else {
51 | $t['file'] = self::getFilePath($t['file']);
52 | }
53 | if (!isset($t['line'])) {
54 | $t['line'] = 0;
55 | }
56 | if (!isset($t['function'])) {
57 | $t['function'] = 'unknown';
58 | }
59 | $log .= "#$i {$t['file']}({$t['line']}): ";
60 | if (isset($t['object']) && is_object($t['object'])){
61 | $log .= get_class($t['object']) . '->';
62 | }
63 | $log .= "{$t['function']}()\n";
64 | }
65 |
66 | if($start && count($log) > $start){
67 | $log = array_slice($log, $start);
68 | }
69 |
70 | return $log;
71 | }
72 |
73 | protected static function getFilePath($file){
74 | if(defined('PATH_ROOT')){
75 | return str_replace(PATH_ROOT, '', $file);
76 | } else {
77 | $root = preg_replace('/^(.+?)\/vender/', '$0', __FILE__);
78 | return str_replace($root, '', $file);
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/src/Yar.php:
--------------------------------------------------------------------------------
1 | '', //transaction id
36 | "m" => '', //the method which being called
37 | "p" => array(), //parameters
38 | )
39 | */
40 | public $request;
41 |
42 | /**
43 | * @var array
44 | * array(
45 | "i" => '',
46 | "s" => '', //status 0 == success
47 | "r" => '', //return value
48 | "o" => '', //output
49 | "e" => '', //error or exception
50 | )
51 | */
52 | public $response;
53 |
54 | function isError(){
55 | return isset($this->response['e']);
56 | }
57 |
58 | function getResponse(){
59 | if(isset($this->request)){
60 | $this->response['i'] = $this->request['i'];
61 | }
62 |
63 | if(!isset($this->response['s'])){
64 | $this->response['s'] = 0;
65 | }
66 |
67 | return $this->response;
68 | }
69 |
70 | function setReturnValue($value){
71 | $this->response['r'] = $value;
72 | }
73 |
74 | function getRequestMethod(){
75 | return $this->request['m'];
76 | }
77 |
78 | function getRequestParams(){
79 | return $this->request['p'];
80 | }
81 |
82 | /**
83 | * @param string $message
84 | * @param int $status
85 | * @param string $output
86 | */
87 | function setError($message, $status = 1, $output = ''){
88 | $this->response['s'] = $status;
89 | $this->response['e'] = $message;
90 | $this->response['o'] = $output;
91 | }
92 |
93 | function getError(){
94 | return $this->response['e'];
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/tests/client.php:
--------------------------------------------------------------------------------
1 | '3594717145',
46 | 'm' => 'getAge1',
47 | 'p' => [1, 2],
48 | ];
49 |
50 | $header = [
51 | 'id' => 12903494,
52 | 'Version' => 0,
53 | 'MagicNum' => 0x80DFEC60,
54 | 'Reserved' => 0,
55 | 'Provider' => '',
56 | 'Token' => '',
57 | 'BodyLen' => 27,
58 | ];
59 |
60 | $data = serialize($request);
61 | $header['BodyLen'] = strlen($data) + 8;
62 |
63 | $header = pack('NnNNa32a32N',
64 | $header['id'],
65 | $header['Version'],
66 | $header['MagicNum'],
67 | $header['Reserved'],
68 | $header['Provider'],
69 | $header['Token'],
70 | $header['BodyLen']
71 | );
72 |
73 | $data = $header . "PHP\x0\x0\x0\x0\x0" . $data;
74 |
75 | echo post($url, $data, [
76 | "User-Agent: PHP Yar Rpc-1.2.4",
77 | "Content-Type: application/x-www-form-urlencoded"
78 | ]);
79 | echo "\n";
--------------------------------------------------------------------------------
/src/Packer.php:
--------------------------------------------------------------------------------
1 | response['e'] = "Invalid request";
38 | return $yar;
39 | }
40 |
41 | $header = substr($data, 0, 82);
42 | $header = unpack(self::HEADER_STRUCT, $header);
43 |
44 | if(strlen($data) - 82 != $header['BodyLen']){
45 | $yar->response['e'] = "Invalid body";
46 | return $yar;
47 | }
48 |
49 | $packName = substr($data, 82, 8);
50 | $yar->packer['packData'] = $packName;
51 |
52 | $packName = $this->getPackName($packName);
53 | $yar->packer['packName'] = $packName;
54 |
55 | $encoder = $this->getEncoder($packName);
56 | $request = $encoder->decode(substr($data, 90));
57 |
58 | $yar->header = $header;
59 | $yar->request = $request;
60 | $yar->packer['encoder'] = $encoder;
61 | return $yar;
62 | }
63 |
64 | protected function getPackName($data){
65 | foreach(self::$packagers as $packer){
66 | if(strncasecmp($packer, $data, strlen($packer)) == 0){
67 | return $packer;
68 | }
69 | }
70 | return self::ENCODE_PHP;
71 | }
72 |
73 | /**
74 | * @param Yar $yar
75 | * @return string
76 | */
77 | function pack($yar){
78 | /** @var EncoderInterface $packer */
79 | $packer = $yar->packer['encoder'];
80 | $data = $packer->encode($yar->getResponse());
81 |
82 | $header =& $yar->header;
83 | $header['BodyLen'] = strlen($data) + 8;
84 |
85 | return pack(self::HEADER_PACK,
86 | $header['id'],
87 | $header['Version'],
88 | $header['MagicNum'],
89 | $header['Reserved'],
90 | $header['Provider'],
91 | $header['Token'],
92 | $header['BodyLen']
93 | )
94 | . $yar->packer['packData']
95 | . $data
96 | ;
97 | }
98 |
99 | /**
100 | * @param string $type
101 | * @return EncoderJson|EncoderMsgpack|EncoderPHP
102 | */
103 | protected function getEncoder($type = self::ENCODE_JSON){
104 | if(isset(self::$encoder[$type])){
105 | return self::$encoder[$type];
106 | }
107 |
108 | switch($type){
109 | case self::ENCODE_MSGPACK :
110 | $instance = new EncoderMsgpack();
111 | break;
112 |
113 | case self::ENCODE_JSON :
114 | $instance = new EncoderJson();
115 | break;
116 |
117 | default :
118 | $instance = new EncoderPHP();
119 | }
120 |
121 | self::$encoder[$type] = $instance;
122 | return $instance;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/Dispatcher.php:
--------------------------------------------------------------------------------
1 | ns = $ns;
27 | return $this;
28 | }
29 |
30 | /**
31 | * @param $api
32 | * @param $class
33 | */
34 | public function regClass($api, $class){
35 | $this->classMap[$api] = $class;
36 | return;
37 | }
38 |
39 | protected $instances = [];
40 | protected function getClass($api){
41 | $api = trim($api, '/');
42 | if(isset($this->instances[$api])){
43 | return $this->instances[$api];
44 | }
45 |
46 | if(isset($this->classMap[$api])){
47 | $class = $this->classMap[$api];
48 | } else {
49 | if(strpos($api, "/") !== false){
50 | $classPrefix = str_replace('/', '\\', dirname($api)) . "\\";
51 | $className = ucfirst(basename($api));
52 | } else {
53 | $classPrefix = '';
54 | $className = ucfirst($api);
55 | }
56 | $class = $this->ns . $classPrefix . $className;
57 | }
58 |
59 | if(is_string($class)){
60 | if(!class_exists($class)){
61 | throw(new Exception("Invalid class({$class})"));
62 | }
63 | $this->instances[$api] = new $class();
64 | } else {
65 | $this->instances[$api] = $class;
66 | }
67 |
68 | return $this->instances[$api];
69 | }
70 |
71 |
72 | public $canCache = false;
73 | private $caches;
74 |
75 | /**
76 | * @param Token $request [path, call_method, method_params, $_GET]
77 | * @param $protocol
78 | * @return mixed
79 | * @throws Exception
80 | */
81 | protected function process($request, $protocol){
82 | if($this->canCache){
83 | $cacheId = serialize([$request->getApi(), $request->getArgs()]);
84 | $cacheId = md5($cacheId);
85 | if(isset($this->caches[$cacheId])){
86 | return $this->caches[$cacheId];
87 | }
88 | }
89 |
90 | $class = $this->getClass($request->getClass());
91 | $method = $request->getMethod();
92 | if(!method_exists($class, $method)){
93 | throw(new Exception("Invalid method"));
94 | }
95 |
96 | if(method_exists($class, 'setProtocol')){
97 | $class->setProtocol($protocol);
98 | }
99 |
100 | $rs = call_user_func_array(array($class, $method), $request->getArgs());
101 | if(isset($cacheId)){
102 | $this->caches[$cacheId] = $rs;
103 | }
104 | return $rs;
105 | }
106 |
107 | protected function getDocument($request){
108 | return "Swoole yar document
";
109 | }
110 |
111 | /**
112 | * @param Token $token
113 | * @param $protocol
114 | * @param $isDocument
115 | * @return mixed|string
116 | * @throws Exception
117 | */
118 | public function __invoke($token, $isDocument, $protocol) {
119 | if(!$isDocument) {
120 | if($this->hasListener(self::EVENT_REQUEST_BEFORE)){
121 | $this->trigger(self::EVENT_REQUEST_BEFORE, $token, $protocol);
122 | }
123 |
124 | $rs = $this->process($token, $protocol);
125 |
126 | if($this->hasListener(self::EVENT_REQUEST_AFTER)){
127 | $this->trigger(self::EVENT_REQUEST_AFTER, $rs, $token, $protocol);
128 | }
129 | return $rs;
130 | } else {
131 | return $this->getDocument($token);
132 | }
133 | }
134 | }
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | ## 为何用swoole来实现 Yar server
2 | * 提升Yar服务效率
3 | * 提升Yar服务稳定性
4 | * 学习swoole, yar(在此感谢laruence,rango及swoole开发团队)
5 |
6 | ## Requirements
7 | 1. php5.4+
8 | 1. ext-swoole 1.8.8+
9 | 1. ext-msgpack 如果yar使用msgpack编码方式
10 |
11 | ## Installation
12 |
13 | ```
14 | composer require stcer/syar
15 | ```
16 |
17 | ## Example
18 | **服务端**
19 | example\server.php
20 |
21 | ```
22 | use syar\Server;
23 | use syar\log\File as FileLog;
24 | use syar\log\Log;
25 |
26 | $vendorPath = __Your vendor path__;
27 | /** @var \Composer\Autoload\ClassLoader $loader */
28 | $loader = include($vendorPath . "/autoload.php");
29 | $loader->addPsr4('syar\\example\\service\\', __DIR__ . '/service');
30 |
31 | $server = new Server('0.0.0.0', '5604');
32 | $server->setLogger(new Log());
33 | $service = new \syar\example\service\Test();
34 | $server->setDispatcher(function(\syar\Token $token, $isDocument) use ($service){
35 | if(!$isDocument){
36 | $method = $token->getMethod();
37 | $params = $token->getArgs();
38 | $value = call_user_func_array(array($service, $method), $params);
39 | } else {
40 | $value = "Yar api document";
41 | }
42 | return $value;
43 | });
44 |
45 | $server->run(['max_request' => 10000]);
46 |
47 | ```
48 |
49 | example/service/Test.php
50 |
51 | ```
52 | namespace syar\example\service;
53 |
54 | /**
55 | * Class Test
56 | * @package syar\example\service
57 | */
58 | class Test {
59 | public function getName($userName){
60 | return $userName . " Hello";
61 | }
62 |
63 | public function getAge(){
64 | return 20;
65 | }
66 | }
67 |
68 | ```
69 |
70 | 命令行启动server.php
71 |
72 | ```
73 | #php server.php
74 |
75 | ```
76 |
77 | **客户端**
78 | ```
79 | $url = "http://127.0.0.1:5604/test";
80 | $client = new Yar_client($url);
81 | $name = $client->getName("tester");
82 | $age = $client->getAge();
83 |
84 | //
85 | echo "\n";
86 | var_dump($name);
87 | var_dump($age);
88 |
89 | ```
90 |
91 |
92 |
93 | ## 简单性能测试(benchmark)
94 | 测试脚本 example/benchmark/testSimple.php,
95 | 测试环境(虚拟机)
96 |
97 | * cpu: i5 - 4460
98 | * mem: 4G
99 | * os: centos6.5
100 | * php: php7(fpm: 20进程, swoole: 18进程(8 worker + 10 task)
101 |
102 |
103 | 脚本一共完成44次接口调用:
104 |
105 | 1. 简单接口调用 2次
106 | 1. 数据库查询接口调用2次
107 | 1. 并发简单接口调用 20次
108 | 1. 并发数据库查询接口调用 20次
109 |
110 | ```
111 | function test($type, $times = 5, $limit = 5){
112 | $timer = new Timer();
113 | $benchmark = new Benchmark($type);
114 | $rs[] = $benchmark->simpleTest(); // 2
115 | $rs[] = $benchmark->dbTest($limit); // 2
116 | $rs[] = $benchmark->batchTest($times, $limit); // 20
117 | $rs[] = $benchmark->concurrentTest($times, $limit); // 20
118 | $stop = $timer->stop();
119 |
120 | // 44 calls, 22 db, 22 normal
121 | foreach($rs as $v){
122 | var_dump($v);
123 | }
124 |
125 | return $stop;
126 | }
127 |
128 | // start test
129 | $times['syar'] = test('syar');
130 | $times['fpm'] = test('fpm');
131 | var_dump($times);
132 |
133 | ---------------------------
134 | output:
135 |
136 | array(2) {
137 | ["syar"]=>
138 | float(0.01271)
139 | ["fpm"]=>
140 | float(0.08602)
141 | }
142 |
143 | ```
144 | 在当前测试环境下,在使用syar批量接口请求,fpm环境下的执行时间大概是syar下的3 -- 6倍左右,
145 |
146 | ### 简单压力测试
147 | 测试脚本 example/benchmark/testConcurrent.php, 50%接口随机查询数据库(10000条数据, 主要为测试接口通信性能)
148 |
149 | * syar 20个并发进程2.4w次接口调用, 用时2.6s秒左右, QPS 9300左右, 可能存在个别调用错误
150 | * fpm 20个并发进程2.4w次接口调用, 用时15s秒左右, QPS 1600左右, 并产生大量Timeout was reached错误
151 |
152 |
153 | ## 扩展特性
154 |
155 | ### 接口批量请求
156 | * 批量请求的接口,服务端使用多个任务进程并行执行
157 | * 请求地址 http://{your_server_address}/multiple
158 | * 调用方法名 function calls($requests);
159 | $requests参数格式 [请求1数组, 请求2数组, ...],
160 | 请求数据格式:['api' => ApiName, 'method' => MethodName, 'params' => []]
161 | * 单个接口执行错误, 服务端记录错误日志, 返回['code' => CODE, 'error' => ERROR MESSAGE]格式数组, 客户端自行处理
162 |
163 | 客户端请求示例:
164 | ```
165 | #example/client_mul.php
166 | $vendorPath = ...;
167 | $loader = include($vendorPath . "/autoload.php");
168 |
169 | $url = "http://127.0.0.1:5604/multiple";
170 | $client = new Yar_client($url);
171 |
172 | $calls = [
173 | 'age' => [
174 | 'api' => '/test',
175 | 'method' => 'getAge',
176 | 'params' => []
177 | ],
178 | 'name' => [
179 | 'api' => '/test',
180 | 'method' => 'getName',
181 | 'params' => [rand(1, 245301)]
182 | ]
183 | ];
184 | $rs = $client->calls($calls);
185 |
186 | var_dump($rs);
187 | ```
188 |
189 |
190 | ### Protocol插件与Dispatcher插件
191 |
192 | 应用示例参考 example/server_plug.php, client_plug.php
193 |
194 | Protocol触发事件:
195 |
196 | 1. Protocol::EVENT_REQUEST_BEFORE, 请求开始触发, 可以提前响应客户端, 中断正常解析流程
197 | 1. Protocol::EVENT_RESPONSE_AFTER, 请求结束触发, 可以适用请求结束之后的处理工作,比如写日志等
198 |
199 | Dispatcher触发事件:
200 |
201 | 1. Dispatcher::EVENT_REQUEST_BEFORE, Api接口执行前触发
202 | 1. Dispatcher::EVENT_REQUEST_AFTER, Api接口执行后触发
203 |
204 |
205 | ### 投递任务到task进程异步执行
206 |
207 | 应用示例参考 example/taskManagerServer.php
208 |
209 | * TaskMananger->regTask()
210 | * TaskMananger->doTask()
211 | * TaskMananger->doTasks()
212 | * TaskMananger->doTasksAsync()
213 |
214 | ## 已知问题
215 | 1. 未完成文档解析, 可使用自带的yar server显示文档
216 | 1. 由于代码是从私有框架独立出来,可能存在未知bug
--------------------------------------------------------------------------------
/src/TaskManager.php:
--------------------------------------------------------------------------------
1 | server = $server;
35 | $this->server->on("task", array($this, 'onTask'));
36 | $this->server->on("finish", array($this, 'onFinish'));
37 | }
38 |
39 | /**
40 | * @param string $id
41 | * @param callback $taskCallback
42 | * @return $this
43 | */
44 | function regTask($id, $taskCallback) {
45 | $this->callbacks[$id] = $taskCallback;
46 | return $this;
47 | }
48 |
49 | /**
50 | * @param $id
51 | * @return bool
52 | */
53 | function has($id){
54 | return isset($this->callbacks[$id]);
55 | }
56 |
57 | /**
58 | * 执行任务
59 | * @param $data
60 | * @return array|mixed
61 | */
62 | private function processTask($data) {
63 | if(!is_array($data)
64 | || !isset($data['id'])
65 | || !($id = $data['id'])
66 | || !isset($this->callbacks[$id])
67 | ){
68 | return $this->getErrorInfo("Invalid task id");
69 | }
70 |
71 | try{
72 | return call_user_func_array($this->callbacks[$id], $data['params']);
73 | } catch(Exception $e){
74 | return $this->getErrorInfo($e);
75 | }
76 | }
77 |
78 | /**
79 | * @param Exception|string $e
80 | * @return array
81 | */
82 | private function getErrorInfo($e){
83 | if(is_string($e)){
84 | $e = new Exception($e);
85 | }
86 | return [
87 | 'error' => $e->getMessage(),
88 | 'code' => $e->getCode(),
89 | 'trace' => $e->getTraceAsString()
90 | ];
91 | }
92 |
93 | /**
94 | * @param $id
95 | * @param array $params
96 | * @param callback $finishCallback
97 | * @param array $bindArgs
98 | * @return void
99 | * @throws Exception
100 | */
101 | function doTask($id, $params = [], $finishCallback = null, $bindArgs = []){
102 | if(!isset($this->callbacks[$id])){
103 | throw(new Exception("Invalid task id"));
104 | }
105 |
106 | $request = $this->getTaskArgs([$id, $params]);
107 | if($this->server->taskworker){
108 | // 在task进程, 任务同步执行
109 | $rs = $this->processTask($request);
110 | if($finishCallback){
111 | call_user_func($finishCallback, $rs, $bindArgs);
112 | }
113 | } else {
114 | $this->server->task($request, -1, function($serv, $task_id, $data) use($finishCallback, $bindArgs) {
115 | if($finishCallback){
116 | call_user_func($finishCallback, $data, $bindArgs);
117 | }
118 | });
119 | }
120 | }
121 |
122 | /**
123 | * @param $requests [ [id, [params]], ...]
124 | * @param $callback
125 | * @param array $bindParams
126 | * @param float $timeout
127 | * @return mixed
128 | */
129 | public function doTasks($requests, $callback = null, $bindParams = [], $timeout = 10.0){
130 | if($this->server->taskworker){
131 | // 在task进程, 任务同步执行
132 | $results = [];
133 | foreach($requests as $index => $request){
134 | $results[$index] = $this->processTask($this->getTaskArgs($request));
135 | }
136 | } else {
137 | $tasks = [];
138 | foreach($requests as $index => $request){
139 | $tasks[$index] = $this->getTaskArgs($request);
140 | }
141 | $results = $this->server->taskWaitMulti($tasks, $timeout);
142 | }
143 | if($callback){
144 | call_user_func($callback, $results, $bindParams);
145 | }
146 | return $results;
147 | }
148 |
149 | private function getTaskArgs($request){
150 | $params = isset($request[1])
151 | ? (is_array($request[1]) ? $request[1] : array($request[1]))
152 | : [];
153 | return [
154 | 'id' => $request[0],
155 | 'params' => $params
156 | ];
157 | }
158 |
159 | function onTask($serv, $task_id, $from_id, $data){
160 | return $this->processTask($data);
161 | }
162 |
163 | /**
164 | * @param $requests
165 | * @param $callback
166 | * @param array $bindParams
167 | */
168 | public function doTasksAsync($requests, $callback, $bindParams = []){
169 | // init request status
170 | static::$mulIndex++;
171 | static::$mulRunMap[static::$mulIndex] = [
172 | 'total' => count($requests),
173 | 'finish' => 0,
174 | 'start' => time(),
175 | 'callback' => $callback,
176 | 'bindParams' => $bindParams,
177 | 'rs' => [],
178 | ];
179 |
180 | foreach($requests as $index => $request){
181 | // to task
182 | $taskId = $this->server->task($this->getTaskArgs($request));
183 | $this->runMap[$taskId] = [
184 | 'isMul' => true,
185 | 'order' => $index,
186 | 'start' => time(),
187 | 'mulIndex' => static::$mulIndex,
188 | ];
189 | }
190 | }
191 |
192 | /**
193 | * @param $serv
194 | * @param $task_id
195 | * @param $data
196 | */
197 | function onFinish($serv, $task_id, $data){
198 | if(!isset($this->runMap[$task_id])){
199 | return;
200 | }
201 |
202 | $taskInfo = $this->runMap[$task_id];
203 | unset($this->runMap[$task_id]);
204 | if($taskInfo['isMul']){
205 | $status =& self::$mulRunMap[$taskInfo['mulIndex']];
206 | $status['finish']++;
207 |
208 | $rsOrder = $taskInfo['order'];
209 | $status['rs'][$rsOrder] = $data;
210 | //$isExpire = (time() - $taskInfo['start']) > $this->maxRunTimes;
211 | if($status['finish'] < $status['total']){
212 | return;
213 | }
214 |
215 | call_user_func(
216 | $status['callback'],
217 | $status['rs'],
218 | $status['bindParams']
219 | );
220 |
221 | unset($status);
222 | unset(self::$mulRunMap[$taskInfo['mulIndex']]);
223 | } else {
224 | if(isset($taskInfo['callback'])){
225 | call_user_func($taskInfo['callback'], $data, $taskInfo['bindParams']);
226 | }
227 | }
228 | }
229 | }
--------------------------------------------------------------------------------
/src/Server.php:
--------------------------------------------------------------------------------
1 | 10240,
42 | 'worker_num' => 8, //worker process num
43 | 'max_request' => 10000,
44 | 'task_worker_num' => 10,
45 | 'task_max_request' => 10000,
46 | 'backlog' => 128, //listen backlog
47 | 'open_tcp_keepalive' => 1,
48 | 'heartbeat_check_interval' => 5,
49 | 'heartbeat_idle_time' => 10,
50 | 'http_parse_post' => false,
51 | );
52 |
53 | function __construct($host, $port) {
54 | $this->host = $host;
55 | $this->port = $port;
56 | $this->sw = $this->createServer();
57 | }
58 |
59 | protected function createServer(){
60 | return new swoole_http_server($this->host, $this->port);
61 | }
62 |
63 | /**
64 | * @param $key
65 | * @param null $value
66 | * @return $this
67 | */
68 | function setOption($key, $value = null){
69 | if(is_array($key)){
70 | $this->setting = array_merge($this->setting, $key);
71 | } else {
72 | $this->setting[$key] = $value;
73 | }
74 | return $this;
75 | }
76 |
77 |
78 | function daemonize() {
79 | $this->setting['daemonize'] = 1;
80 | }
81 |
82 | /**
83 | * @param $protocol
84 | * @return $this
85 | * @throws \Exception
86 | */
87 | function setProtocol($protocol){
88 | $this->protocol = $protocol;
89 | return $this;
90 | }
91 |
92 | /**
93 | * @return Protocol
94 | * @throws \Exception
95 | */
96 | public function getProtocol() {
97 | if(!isset($this->protocol)){
98 | $this->protocol = new Protocol();
99 | }
100 | return $this->protocol;
101 | }
102 |
103 | /**
104 | * @return TaskManager
105 | */
106 | public function getTaskManager(){
107 | if(!isset($this->taskManager)){
108 | $this->taskManager = new TaskManager($this->sw);
109 | }
110 | return $this->taskManager;
111 | }
112 |
113 | /**
114 | * @return swoole_http_server|swoole_server
115 | */
116 | function getSwooleServer(){
117 | return $this->sw;
118 | }
119 |
120 | /**
121 | * @param array $setting
122 | */
123 | function run($setting = array()) {
124 | register_shutdown_function(array($this, 'handleFatal'));
125 |
126 | // set options
127 | $this->setOption($setting);
128 | $this->sw->set($this->setting);
129 |
130 | // bind method for swoole server
131 | $this->chkConfig();
132 | $this->bind();
133 |
134 | // start server
135 | $this->sw->start();
136 | }
137 |
138 | protected function chkConfig(){
139 | $protocol = $this->getProtocol();
140 | $protocol->server = $this;
141 | $protocol->chkConfig();
142 | }
143 |
144 | protected function bind(){
145 | $binds = [
146 | 'onServerStart' => 'ManagerStart',
147 | 'onServerStop' => 'ManagerStop',
148 | ];
149 | foreach($binds as $method => $evt){
150 | $this->sw->on($evt, array($this, $method));
151 | }
152 |
153 | $protocol = $this->getProtocol();
154 | $binds = [
155 | 'onServerStart' => 'ManagerStart',
156 | 'onServerStop' => 'ManagerStop',
157 |
158 | 'onWorkerStart' => 'WorkerStart',
159 | 'onWorkerStop' => 'WorkerStop',
160 |
161 | 'onConnect' => 'Connect',
162 | 'onReceive' => 'Receive',
163 | 'onClose' => 'Close',
164 | 'onRequest' => 'request',
165 | ];
166 | foreach($binds as $method => $evt){
167 | if(method_exists($protocol, $method)){
168 | $this->sw->on($evt, array($protocol, $method));
169 | }
170 | }
171 | }
172 |
173 | function onServerStart($serv){
174 | $this->log("Server start on {$this->host}:{$this->port}, pid {$serv->master_pid}");
175 | if (!empty($this->setting['pid_file'])){
176 | file_put_contents($this->setting['pid_file'], $serv->master_pid);
177 | }
178 | }
179 |
180 | function onServerStop(){
181 | $this->log("Server stop");
182 | if (!empty($this->setting['pid_file'])) {
183 | unlink($this->setting['pid_file']);
184 | }
185 | }
186 |
187 |
188 | private $currentRequest = [];
189 |
190 | /**
191 | * @param $req
192 | * @param Response $response
193 | */
194 | public function setCurrentRequest($req, $response){
195 | $this->currentRequest = [
196 | 'request' => $req,
197 | 'response' => $response,
198 | ];
199 | }
200 |
201 | /**
202 | * catch error
203 | */
204 | function handleFatal(){
205 | if($log = Debug::traceError()) {
206 | $this->log($log, "error");
207 | }
208 |
209 | if(isset($this->currentRequest['response'])){
210 | $this->currentRequest['response']->end("PHP Parse error:" . $log);
211 | }
212 | }
213 |
214 | /**
215 | * @param $callback
216 | */
217 | function setDispatcher($callback){
218 | $this->getProtocol()->setProcess($callback);
219 | }
220 |
221 | /**
222 | * @param $plug
223 | * @param $forProtocol
224 | * @return $this
225 | */
226 | public function addPlug($plug, $forProtocol = true){
227 | if($forProtocol){
228 | $dispatcher = $this->getProtocol();
229 | } else {
230 | $dispatcher = $this->getProtocol()->getProcessor();
231 | }
232 |
233 | if($dispatcher instanceof InterfaceEventDispatcher){
234 | $dispatcher->attaches($plug);
235 | }
236 | return $this;
237 | }
238 | }
--------------------------------------------------------------------------------
/example/benckmark/lib.php:
--------------------------------------------------------------------------------
1 | simpleTest(); // 2
20 | $rs[] = $benchmark->dbTest(); // 2
21 |
22 | $times = $times ?: ($batch ? 5 : 1);
23 | if($batch){
24 | $rs[] = $benchmark->batchTest($times); // 4 * $times
25 | } else {
26 | $rs[] = $benchmark->concurrentTest($times); // 4 * $times
27 | }
28 |
29 | $stop = $timer->stop();
30 |
31 | // 44 calls, 22 db, 22 normal
32 | if(IS_OUTPUT){
33 | echo "{$type} -----------------------------\n";
34 | foreach($rs as $v){
35 | var_dump($v);
36 | }
37 | }
38 |
39 | return $stop;
40 | }
41 |
42 | function ab($type = 'syar', $c = 1, $n = 1, $batch = false, $times = 1) {
43 | $pm = new SimpleProcessorManager();
44 | $timer = new Timer();
45 |
46 | $pm->run($c, function(swoole_process $worker) use($type, $n, $batch, $times){
47 | for($i = 0; $i < $n; $i++){
48 | echo "Worker {$worker->pid} for the {$i}th\n";
49 | test($type, $batch, $times);
50 | }
51 | $worker->exit(0);
52 | });
53 | return $timer->stop();
54 | }
55 |
56 | /**
57 | * Class Benchmark
58 | * @package syar\example\benckmark
59 | */
60 | class Benchmark {
61 |
62 | protected $type = 'syar';
63 | protected $url;
64 | protected $urlFpm = "http://syar7.x1.cn/fpm_yar_%s.php";
65 | protected $urlSyar = "http://127.0.0.1:5604/%s";
66 |
67 | /**
68 | * Benchmark constructor.
69 | * @param string $type
70 | */
71 | public function __construct($type){
72 | $this->type = $type;
73 | $this->url = $this->isFpm() ? $this->urlFpm : $this->urlSyar;
74 | }
75 |
76 | private function isFpm(){
77 | return $this->type == 'fpm';
78 | }
79 |
80 | /**
81 | * 1万表记录测试数据
82 | * @var int
83 | * @see file : mkTestData.php
84 | */
85 | private $maxTableId = 10000;
86 | // 列表查询取前2w记录随机
87 | private $maxTableStart = 5000;
88 |
89 | /**
90 | * @return int
91 | */
92 | private function getInfoId(){
93 | return rand(1, $this->maxTableId);
94 | }
95 |
96 | /**
97 | * @param int $limit
98 | * @return int
99 | */
100 | private function getStart($limit = 5){
101 | $start = rand($limit, $this->maxTableStart) - $limit;
102 | if($start < 0){
103 | $start = 0;
104 | }
105 | return $start;
106 | }
107 |
108 | /**
109 | * @param $api
110 | * @param bool $returnUrl
111 | * @return string|Yar_Client
112 | */
113 | function getClient($api, $returnUrl = false){
114 | $url = sprintf($this->url, $api);
115 | if($returnUrl){
116 | return $url;
117 | }
118 |
119 | $client = new Yar_Client($url);
120 | $client->setOpt(YAR_OPT_PACKAGER, 'msgpack');
121 | return $client;
122 | }
123 |
124 | function dbTest(){
125 | $client = $this->getClient('db');
126 | $info = $client->getInfo($this->getInfoId());
127 | $list = $client->getList($this->getStart(), $this->listLimit);
128 | return [$info, $list];
129 | }
130 |
131 | function simpleTest(){
132 | $client = $this->getClient('test');
133 | $name = $client->getName("tester");
134 | $age = $client->getAge();
135 | return [$name, $age];
136 | }
137 |
138 | /**
139 | * @param int $times
140 | * @return array
141 | */
142 | function concurrentTest($times = 1){
143 | $data = [];
144 |
145 | $url1 = $this->getClient('test', true);
146 | $url2 = $this->getClient('db', true);
147 | for($i = 0; $i < $times; $i++){
148 | $this->concurrentCall($data, $url1, 'getName', [rand(0, 245301)], 'name_' . $i);
149 | $this->concurrentCall($data, $url1, 'getAge', [], 'age_' . $i);
150 | $this->concurrentCall($data, $url2, 'getInfo', [$this->getInfoId()], 'info_' . $i);
151 | $this->concurrentCall($data, $url2, 'getList', [$this->getStart(), $this->listLimit], 'list_' . $i);
152 | }
153 | Yar_Concurrent_Client::loop();
154 | Yar_Concurrent_Client::reset();
155 | return $data;
156 | }
157 |
158 | private function concurrentCall(&$data, $url, $method, $args, $key){
159 | Yar_Concurrent_Client::call(
160 | $url, $method, $args,
161 | function($rs) use ($key, &$data){
162 | $data[$key] = $rs;
163 | }
164 | );
165 | }
166 |
167 | protected $listLimit = 2;
168 |
169 | /**
170 | * @param int $times
171 | * @return array
172 | */
173 | function batchTest($times = 1) {
174 | if($this->isFpm()){
175 | return $this->concurrentTest($times);
176 | }
177 | $requests = [];
178 | for($i = 0; $i < $times; $i++){
179 | $requests["age_{$i}"] = ['api' => 'test', 'method' => 'getAge', 'params' => []];
180 | $requests["name_{$i}"] = ['api' => 'test', 'method' => 'getName', 'params' => ['test']];
181 | $requests["info_{$i}"] = ['api' => 'db', 'method' => 'getInfo', 'params' => [$this->getInfoId()]];
182 | $requests["list_{$i}"] = ['api' => 'db', 'method' => 'getList', 'params' => [$this->getStart(), $this->listLimit]];
183 | }
184 | $client = $this->getClient('multiple');
185 | return $client->calls($requests);
186 | }
187 | }
188 |
189 | /**
190 | * Class Timer
191 | */
192 | class Timer{
193 | protected $startTime;
194 |
195 | function __construct($autoStart = true){
196 | if($autoStart){
197 | $this->start();
198 | }
199 | }
200 |
201 | function start(){
202 | $this->startTime = $this->_time();
203 | }
204 |
205 | function stop($echo = false, $str = ''){
206 | $time = $this->_time();
207 | $times = round($time - $this->startTime, 5);
208 | $this->startTime = $time;
209 |
210 | if($echo){
211 | echo $str . $times . "\n";
212 | }
213 | return $times;
214 | }
215 |
216 | protected function _time(){
217 | $mtime = microtime ();
218 | $mtime = explode (' ', $mtime);
219 | return $mtime[1] + $mtime[0];
220 | }
221 | }
222 |
223 |
224 | /**
225 | * Class SimpleProcessorManager
226 | * @package j\debug
227 | */
228 | class SimpleProcessorManager{
229 | protected $works = [];
230 | protected $workNums;
231 | protected function start($callback){
232 | for($i = 0; $i < $this->workNums; $i++) {
233 | $process = new swoole_process($callback, false, false);
234 | $pid = $process->start();
235 | $workers[$pid] = $process;
236 | }
237 | }
238 |
239 | function run($workNums, $callback){
240 | $this->workNums = $workNums;
241 | $this->start($callback);
242 | $this->close();
243 | }
244 |
245 | protected function close(){
246 | for($i = 0; $i < $this->workNums; $i++) {
247 | $ret = swoole_process::wait();
248 | $pid = $ret['pid'];
249 | echo "Worker Exit, PID=" . $pid . PHP_EOL;
250 | }
251 | }
252 | }
253 |
254 |
--------------------------------------------------------------------------------
/src/Protocol.php:
--------------------------------------------------------------------------------
1 | packer = new Packer();
46 | }
47 |
48 | /**
49 | * @throws RuntimeException
50 | */
51 | public function chkConfig(){
52 | if(!isset($this->server)){
53 | throw new RuntimeException("Set protocol's server first");
54 | }
55 |
56 | if(!is_callable($this->getProcessor())){
57 | throw new RuntimeException("Set protocol's processor first");
58 | }
59 |
60 | $taskManager = $this->server->getTaskManager();
61 | $taskManager->regTask('process', array($this, 'process'));
62 | }
63 |
64 | /**
65 | * @param $callback
66 | */
67 | function setProcess($callback){
68 | $this->processor = $callback;
69 | }
70 |
71 | /**
72 | * @return callable|Dispatcher
73 | */
74 | public function getProcessor(){
75 | if(!isset($this->processor)){
76 | $this->processor = new Dispatcher();
77 | }
78 | return $this->processor;
79 | }
80 |
81 | static $i = 1;
82 | /**
83 | * @param swoole_http_request $req
84 | * @param swoole_http_response $res
85 | */
86 | function onRequest(swoole_http_request $req, swoole_http_response $res) {
87 | $request = new Request($req);
88 | $response = new Response($res);
89 | $this->server->setCurrentRequest($request, $response);
90 |
91 | if($request->isPost()) {
92 | $request->yar = $this->packer->unpack($req->rawContent());
93 | }
94 |
95 | if($this->hasListener(self::EVENT_REQUEST_BEFORE)){
96 | $this->trigger(self::EVENT_REQUEST_BEFORE, $request, $response, $this);
97 | if($response->isSend()){
98 | return;
99 | }
100 | }
101 |
102 | if(isset($request->yar)) {
103 | if($request->yar->isError()){
104 | // 解包错误
105 | $this->response([], $request, $response);
106 | return;
107 | }
108 |
109 | if($this->isMulApi($request)){
110 | // 批量请求
111 | $this->mulRequest($request, $response);
112 | return;
113 | }
114 |
115 | $isDocument = false;
116 | $method = $request->getYarMethod();
117 | $params = $request->getYarParams();
118 | } else {
119 | $isDocument = true;
120 | $method = $params = '';
121 | }
122 |
123 | // process request
124 | $get = isset($req->get) ? $req->get : [];
125 | $token = new Token($request->getPath(), $method, $params, $get);
126 |
127 | $rs = $this->process($token, $isDocument);
128 | $this->response($rs, $request, $response);
129 | }
130 |
131 | /**
132 | * @param $request Request
133 | * @return bool
134 | */
135 | protected function isMulApi($request) {
136 | return
137 | $this->multipleApiPath == $request->getPath() &&
138 | $this->multipleApiMethod == $request->yar->getRequestMethod();
139 | }
140 |
141 | /**
142 | * @param $request Request
143 | * @param $response Response
144 | */
145 | protected function mulRequest($request, $response){
146 | $params = $request->yar->getRequestParams();
147 |
148 | // maybe a bug
149 | // $client->calls($calls);
150 | // $params = $calls
151 | $params = $params[0];
152 |
153 | if(!is_array($params) || count($params) == 0){
154 | $this->response([
155 | 'code' => 500,
156 | "error" => "Invalid request params for multiple request"
157 | ], $request, $response);
158 | return;
159 | }
160 |
161 | $requests = [];
162 | foreach($params as $key => $param){
163 | $token = new Token(
164 | $param['api'], $param['method'], $param['params'],
165 | isset($param['options']) ? $param['options'] : []
166 | );
167 | $requests[$key] = ['process', [$token]] ;
168 | }
169 |
170 | // init
171 | $this->server->getTaskManager()->doTasksAsync($requests, function($results) use($request, $response){
172 | $this->response(['code' => 200, 'rs' => $this->formatMulResults($results)], $request, $response);
173 | });
174 |
175 | // $results = $this->server->getTaskManager()->doTasks($requests);
176 | // $this->response(['code' => 200, 'rs' => $this->formatMulResults($results)], $request, $response);
177 | }
178 |
179 | protected function formatMulResults($results){
180 | $data = [];
181 | foreach($results as $key => $rs) {
182 | if($rs['code'] == 200){
183 | $data[$key] = $rs['rs'];
184 | } else {
185 | unset($rs['debug']);
186 | $data[$key] = $rs;
187 | }
188 | }
189 | return $data;
190 | }
191 |
192 | /**
193 | * @param $requestToken
194 | * @param $isDocument
195 | * @return array
196 | */
197 | public function process($requestToken, $isDocument = false) {
198 | try{
199 | return [
200 | 'code' => 200,
201 | 'rs' => call_user_func($this->processor, $requestToken, $isDocument, $this)
202 | ];
203 | } catch (\Exception $e) {
204 | $error = [
205 | 'code' => $e->getCode(),
206 | 'error' => $e->getMessage(),
207 | 'debug' => $e->getTraceAsString()
208 | ];
209 | $this->log(var_export($error), 'error');
210 | return $error;
211 | }
212 | }
213 |
214 | /**
215 | * @param string $data
216 | * @param Request $request
217 | * @param Response $response
218 | */
219 | protected function response($data, $request, $response){
220 | if($response->isSend()){
221 | return;
222 | }
223 |
224 | if($yar = $request->yar){
225 | if(!$yar->isError()){
226 | if($data['code'] == 200){
227 | $yar->setReturnValue($data['rs']);
228 | } else {
229 | $yar->setError($data['error']);
230 | }
231 | }
232 | $response->setHeader('Content-Type', 'application/octet-stream');
233 | $response->body = $this->packer->pack($request->yar);
234 | } else {
235 | $response->setHttpStatus(500);
236 | $response->body = $data['code'] == 200 ? $data['rs'] : $data['error'];
237 | }
238 |
239 | //压缩
240 | if ($this->gzip) {
241 | $response->gzip($this->gzip_level);
242 | }
243 |
244 | // 输出返回
245 | $response->send();
246 |
247 | if($this->hasListener(self::EVENT_RESPONSE_AFTER)){
248 | $this->trigger(self::EVENT_RESPONSE_AFTER, $request, $response, $data, $this);
249 | }
250 | }
251 | }
--------------------------------------------------------------------------------