├── .gitignore ├── Application.php ├── LICENSE ├── README.md ├── composer.json ├── example ├── mqtt-server └── mqtt │ ├── components │ └── Auth.php │ ├── config │ ├── bootstrap.php │ ├── main-local.php │ ├── main.php │ ├── params-local.php │ └── params.php │ ├── controllers │ ├── CommonController.php │ ├── NoticeController.php │ ├── ReportController.php │ └── RoomController.php │ └── runtime │ └── logs │ └── .gitkeep └── src ├── Controller.php ├── Mqtt.php ├── Redis.php └── Task.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .local -------------------------------------------------------------------------------- /Application.php: -------------------------------------------------------------------------------- 1 | params['listen']; 25 | $server = new Server('0.0.0.0', $port, SWOOLE_PROCESS); 26 | $server->set([ 27 | 'worker_num' => 2, 28 | 'task_worker_num' => 2, 29 | 'open_mqtt_protocol' => 1, 30 | 'task_ipc_mode' => 3, 31 | 'debug_mode' => 1, 32 | 'heartbeat_check_interval' => 60, 33 | 'heartbeat_idle_time' => 180, 34 | 'daemonize' => Yii::$app->params['daemonize'], 35 | 'log_file' => Yii::$app->getRuntimePath() . '/logs/app.log' 36 | ]); 37 | $server->on('Start', [$this, 'onStart']); 38 | $server->on('Task', [$this, 'onTask']); 39 | $server->on('Finish', [$this, 'onFinish']); 40 | $server->on('Connect', [$this, 'onConnect']); 41 | $server->on('Receive', [$this, 'onReceive']); 42 | $server->on('Close', [$this, 'onClose']); 43 | $server->on('WorkerStart', [$this, 'onWorkerStart']); 44 | //Mount redis on $server 45 | $server->redis = Redis::getRedis(); 46 | $this->server = $server; 47 | $this->server->start(); 48 | } 49 | 50 | public function onStart($server) 51 | { 52 | echo "Server Start {$server->master_pid}" . PHP_EOL; 53 | } 54 | 55 | public function onWorkerStart(Server $server, $id) 56 | { 57 | if ($id != 0) return; 58 | go(function () use ($server) { 59 | $redis = new \Swoole\Coroutine\Redis; 60 | $config = Yii::$app->params['redis']; 61 | $result = $redis->connect($config['host'], $config['port']); 62 | if (!$result) return; 63 | if(!empty($config['auth']) && !$redis->auth($config['auth'])) return; 64 | while (true) { 65 | //Redis pub/sub feature; Follow the task structure, Recommend use redis publish like this: redis->publish('async', 'send/sms/15600008888'). 66 | $result = $redis->subscribe(['async']); 67 | if ($result) 68 | $server->task(Task::async($result[2])); 69 | } 70 | }); 71 | } 72 | 73 | public function onConnect($server, $fd, $from_id) 74 | { 75 | } 76 | 77 | public function onReceive(Server $server, $fd, $from, $buffer) 78 | { 79 | go(function () use ($server, $fd, $buffer) { 80 | try { 81 | $m = new Mqtt($buffer); 82 | echo $m; 83 | if ($m->tp == Mqtt::TP_CONNECT) { 84 | if (Yii::$app->params['auth'] && Yii::$app->auth->judge($m->connectInfo) === false) 85 | $m->replyConack(0x05); 86 | else 87 | $server->task(Task::internal('common/connect/' . $fd, $m->connectInfo)); 88 | } 89 | if (!is_null($m->ack)) $server->send($fd, $m->ack); 90 | switch ($m->tp) { 91 | case Mqtt::TP_PUBLISH: 92 | return $server->task(Task::publish($fd, $m->getTopic(), $m->getPayload())); 93 | case Mqtt::TP_SUBSCRIBE: 94 | $server->task(Task::internal('common/redis/sadd', ['mqtt_sub_fds_set_#' . $m->topic, $fd])); 95 | $server->task(Task::internal('common/redis/sadd', ['mqtt_sub_topics_set_#' . $fd, $m->topic])); 96 | return $server->task(Task::subscribe($fd, $m->topic, $m->getReqqos())); 97 | case Mqtt::TP_UNSUBSCRIBE: 98 | return $server->task(Task::internal('common/unsub/' . $fd, $m->getTopic())); 99 | case Mqtt::TP_DISCONNECT: 100 | $server->close($fd); 101 | } 102 | } catch (\Exception $e) { 103 | var_dump($e->getMessage()); 104 | $server->close($fd); 105 | } 106 | }); 107 | } 108 | 109 | public function onClose($server, $fd, $from) 110 | { 111 | $server->task(Task::internal('common/close/' . $fd)); 112 | } 113 | 114 | public function onTask(Server $server, $worker_id, $task_id, $task) 115 | { 116 | try { 117 | $class = new \ReflectionClass(Yii::$app->controllerNamespace . '\\' . ucfirst($task->class) . 'Controller'); 118 | $method = 'action' . ucfirst($task->func); 119 | if ($class->hasMethod($method)) { 120 | $actor = $class->getMethod($method); 121 | return $actor->invokeArgs($class->newInstanceArgs([$server, $task->fd, $task->topic, $task->verb]), [$task->param, $task->body]); 122 | } 123 | throw new \Exception($method . ' Undefined'); 124 | } catch (\Exception $e) { 125 | var_dump($e->getMessage()); 126 | } 127 | } 128 | 129 | public function onFinish(Server $server, $task_id, $data) 130 | { 131 | echo 'Task finished #' . $task_id . ' #' . $data . PHP_EOL; 132 | } 133 | 134 | public function handleRequest($_) 135 | { 136 | } 137 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MQTT For Yii2 Base On Swoole 4 2 | ============================== 3 | MQTT server for Yii2 base on swoole 4, Resolve topic as a route reflect into controller/action/param, And support redis pub/sub to trigger async task from your web application 4 | 5 | Installation 6 | ------------ 7 | Install Yii2: [Yii2](https://www.yiiframework.com). 8 | 9 | Install swoole: [swoole](https://www.swoole.com), recommend version 4+. 10 | 11 | Other dependency: php-redis extension. 12 | 13 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 14 | 15 | Either run 16 | 17 | ``` 18 | php composer.phar require --prefer-dist immusen/yii2-swoole-mqtt "~1.0" 19 | ``` 20 | 21 | or add 22 | 23 | ``` 24 | "immusen/yii2-swoole-mqtt": "~1.0" 25 | ``` 26 | 27 | to the require section of your `composer.json` file. 28 | 29 | 30 | Test or Usage 31 | ------------- 32 | 33 | ``` 34 | # after installation, cd project root path, e.g. cd yii2-advanced-project/ 35 | mv vendor/immusen/yii2-swoole-mqtt/example/mqtt ./ 36 | mv vendor/immusen/yii2-swoole-mqtt/example/mqtt-server ./ 37 | chmod a+x ./mqtt-server 38 | # run: 39 | ./mqtt-server 40 | # config : 41 | cat ./mqtt/config/params.php 42 | 8721, 45 | 'daemonize' => 0, 46 | 'auth' => 1, // config auth class in ./main.php 47 | ]; 48 | # or coding in ./mqtt/controllers/ 49 | ``` 50 | 51 | Test client: MQTTLens, MQTT.fx 52 | 53 | Example: 54 | -------- 55 | Case A: Subscribe/Publish 56 | 57 | > 1, mqtt client subscribe topic: room/count/100011 58 | 59 | > 2.1, mqtt client publish: every time publish topic: room/join/100011, the subscribe side will get count+1, or publish topic: room/leave/100011 get count -1. 60 | 61 | > 2.2, redis client pulish: every time $redis->publish('async', 'room/join/100011'), the subscribe side will get count+1, or $redis->publish('async', 'room/leave/100011') get count -1. 62 | 63 | Case B: Publish(Notification Or Report) 64 | 65 | > mqtt client publish topic: report/coord/100111 and payload: e.g. 110.12345678,30.12345678,0,85 66 | 67 | Coding: 68 | ------ 69 | MQTT subscribe topic: "channel/count/100001" will handle at: 70 | ``` 71 | class ChannelController{ 72 | public function actionCount($channel_id){ 73 | echo "client {$this->fd} subscribed the count change of channel {$channel_id}"; 74 | } 75 | } 76 | ``` 77 | > //client 1 subscribed the count change of channel 100001 78 | 79 | 80 | MQTT Publish Topic: "channel/join/100001" with payload: "Foo" will handle at: 81 | ``` 82 | class ChannelController{ 83 | public function actionJoin($channel_id, $who){ 84 | echo "{$who} join in channel {$channel_id}"; 85 | #then broadcast update to all client who subscribed channel 100001 86 | #$this->publish($fds, $sub_topic, $count); 87 | } 88 | } 89 | ``` 90 | > // Foo join in channel 100001 91 | 92 | MQTT 93 | ---- 94 | 95 | About MQTT: [MQTT Version 3.1.1 Plus Errata 01](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) 96 | 97 | > Non-complete implementation of MQTT 3.1.1 in this project, Upgrading... 98 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immusen/yii2-swoole-mqtt", 3 | "description": "MQTT server for Yii2 base on swoole 4, Resolve topic as a route reflect into controller/action/param, And support redis pub/sub to trigger async task from your web application", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2","extension","swoole","mqtt","mqtt server","redis","IOT", "async", "pub sub"], 6 | "license": "Apache-2.0", 7 | "authors": [ 8 | { 9 | "name": "immusen", 10 | "email": "immusen@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "yiisoft/yii2": "~2.0.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "immusen\\mqtt\\": "" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/mqtt-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 23 | exit($exitCode); 24 | -------------------------------------------------------------------------------- /example/mqtt/components/Auth.php: -------------------------------------------------------------------------------- 1 | redis->hget('PASSPORT_HUB_HASH_#MQTT', $data['uid']); 22 | // if (!$legal_token || $legal_token !== $data['token']) return false; 23 | // return true; 24 | return time() % 2 ? true : false; 25 | } 26 | } -------------------------------------------------------------------------------- /example/mqtt/config/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'mqtt', 11 | 'basePath' => dirname(__DIR__), 12 | 'bootstrap' => ['log'], 13 | 'controllerNamespace' => 'mqtt\controllers', 14 | 'components' => [ 15 | 'auth' => ['class' => 'mqtt\components\Auth'], 16 | 'errorHandler' => ['class' => 'yii\console\ErrorHandler'], 17 | 'log' => [ 18 | 'targets' => [ 19 | [ 20 | 'class' => 'yii\log\FileTarget', 21 | 'levels' => ['error', 'warning'], 22 | ], 23 | ], 24 | ], 25 | ], 26 | 'params' => $params, 27 | ]; 28 | -------------------------------------------------------------------------------- /example/mqtt/config/params-local.php: -------------------------------------------------------------------------------- 1 | 8721, 4 | 'daemonize' => 0, 5 | 'auth' => 1, // config auth class in ./main.php 6 | 'worker_num' => 2, 7 | 'task_worker_num' => 2, 8 | 'redis' => [ 9 | 'host' => '127.0.0.1', 10 | 'port' => '6379', 11 | // 'auth' => 'passwd', 12 | 'pool_size' => 10, 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /example/mqtt/controllers/CommonController.php: -------------------------------------------------------------------------------- 1 | redis->hset('mqtt_online_hash_fd@uid', $uid, $fd); 21 | $this->redis->hset('mqtt_online_hash_client@fd', $fd, serialize(['u' => $uid, 'c' => $connect_info['client_id']])); 22 | return true; 23 | } 24 | 25 | public function actionClose($fd) 26 | { 27 | echo '#client closed: ', $fd, PHP_EOL; 28 | if ($client = $this->redis->hget('mqtt_online_hash_client@fd', $fd)) { 29 | $client_arr = @unserialize($client); 30 | $this->redis->hdel('mqtt_online_hash_fd@uid', $client_arr['u']); 31 | $this->redis->hdel('mqtt_online_hash_client@fd', $fd); 32 | $this->actionUnsub($fd); 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | public function actionUnsub($fd, $topic = '') 39 | { 40 | echo '#client unsub: ', $fd, PHP_EOL; 41 | if ($topic == '') 42 | $topics = $this->redis->smembers('mqtt_sub_topics_set_#' . $fd); 43 | else 44 | $topics = array($topic); 45 | if ($topics == false) return false; 46 | do { 47 | $topic = array_pop($topics); 48 | $this->redis->srem('mqtt_sub_fds_set_#' . $topic, $fd); 49 | $this->redis->srem('mqtt_sub_topics_set_#' . $fd, $topic); 50 | } while ($topics); 51 | return true; 52 | } 53 | 54 | /** 55 | * Control redis by mqtt or trigger by other caller as a async task 56 | * @param $verb 57 | * @param $param 58 | * @return mixed 59 | */ 60 | public function actionRedis($verb, $param) 61 | { 62 | // if ($this->verb !== Task::VERB_INTERNAL) return false; // only accept internal call 63 | return call_user_func_array([$this->redis, $verb], $param); 64 | } 65 | 66 | public function actionDefault($_) 67 | { 68 | // 69 | } 70 | } -------------------------------------------------------------------------------- /example/mqtt/controllers/NoticeController.php: -------------------------------------------------------------------------------- 1 | redis->get('mqtt_notice_offline_@' . $symbol); 26 | return $this->publish([$this->fd], $this->topic, $msg); 27 | } 28 | 29 | 30 | /** 31 | * Verb: publish 32 | * May come forom mqtt publish or redis-publish 33 | * @param string $symbol 34 | * @param $payload 35 | * @return bool 36 | */ 37 | public function actionSend($symbol = 'global', $payload) 38 | { 39 | //get fds demo 40 | $fds = $this->redis->smembers('mqtt_notice_fds_set_@' . $symbol); 41 | return $this->publish($fds, 'notice/sub/' . $symbol, $payload); 42 | } 43 | } -------------------------------------------------------------------------------- /example/mqtt/controllers/ReportController.php: -------------------------------------------------------------------------------- 1 | redis->hget('mqtt_record_hash_#room', $room_id); 30 | //reply current count 31 | $this->publish($this->fd, $this->topic, $count ?: 0); 32 | //or some history message... 33 | // $this->publish($this->fd, $this->topic, $history_chat_message); 34 | } 35 | 36 | /** 37 | * Verb: publish 38 | * Client who join a room, send a PUBLISH to server, with a topic e.g. room/join/100001, and submit user info into $payload about somebody who join 39 | * also support redis pub/sub, so you can trigger this method by Yii::$app->redis->publish('async', 'room/join/100001') in your Yii Web application 40 | * @param $room_id 41 | * @param $payload 42 | * @return bool 43 | */ 44 | public function actionJoin($room_id, $payload = '') 45 | { 46 | echo '# room ', $room_id, ' one person joined, #', $payload, PHP_EOL; 47 | $count = $this->redis->hincrby('mqtt_record_hash_#room', $room_id, 1); 48 | $sub_topic = 'room/sub/' . $room_id; 49 | //May need send json string as result to client in real scene. e.g. '{"type":"notice", "count": 100, "ext":"foo"}' 50 | return $this->publish($this->subFds($sub_topic), $sub_topic, $count); 51 | } 52 | 53 | /** 54 | * Verb: publish 55 | * some one leave room... similar with actionJoin 56 | * @param $room_id 57 | * @return bool 58 | */ 59 | public function actionLeave($room_id) 60 | { 61 | $count = $this->redis->hincrby('mqtt_record_hash_#room', $room_id, -1); 62 | $count = $count < 1 ? 0 : $count; 63 | $sub_topic = 'room/sub/' . $room_id; 64 | return $this->publish($this->subFds($sub_topic), $sub_topic, $count); 65 | } 66 | 67 | /** 68 | * Verb: publish 69 | * Message to room, with a topic e.g. room/msg/100001, and submit a pure or json string as payload. e.g. 'hello' or '{"type":"msg","from":"foo","content":"hello!"}' 70 | * also support publish by Yii::$app->redis->publish('async', 'room/msg/100001/{"type":"msg","from":"foo","content":"hello!"}') 71 | * @param $room_id 72 | * @param $payload 73 | * @return bool 74 | */ 75 | public function actionMsg($room_id, $payload = '') 76 | { 77 | echo '# Msg to ', $room_id, ' by '. $this->verb .' with concent: ' . $payload . PHP_EOL; 78 | $sub_topic = 'room/sub/' . $room_id; 79 | return $this->publish($this->subFds($sub_topic), $sub_topic, $payload); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /example/mqtt/runtime/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immusen/yii2-swoole-mqtt/dbd315b7a12c77193fd016968ec0fc10a7ef3028/example/mqtt/runtime/logs/.gitkeep -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | fd = $fd; 31 | $this->topic = $topic; 32 | $this->verb = $verb; 33 | $this->redis = $server->redis; 34 | $this->server = $server; 35 | } 36 | 37 | /** 38 | * Broadcast publish 39 | * @param $fds 40 | * @param $topic 41 | * @param $content 42 | * @return bool; 43 | */ 44 | public function publish($fds, $topic, $content, $qos = 0) 45 | { 46 | if (!is_array($fds)) $fds = array($fds); 47 | $msg = $this->buildBuffer($topic, $content, $qos); 48 | $result = 1; 49 | $offline = [$topic]; 50 | while ($fds) { 51 | $fd = (int)array_pop($fds); 52 | if ($this->server->exist($fd)) { 53 | $result &= $this->server->send($fd, $msg) ? 1 : 0; 54 | } else { 55 | $this->redis->srem('mqtt_sub_fds_set_#' . $topic, $fd); 56 | } 57 | } 58 | return !!$result; 59 | } 60 | 61 | public function subFds($key, $prefix = '') 62 | { 63 | $prefix = $prefix ?: 'mqtt_sub_fds_set_#'; 64 | $res = $this->redis->smembers($prefix . $key); 65 | if (!$res) return []; 66 | return $res; 67 | } 68 | 69 | public function getClientInfo() 70 | { 71 | $res = ['u' => '', 'c' => '']; 72 | $info = $this->redis->hget('mqtt_online_hash_client@fd', $this->fd); 73 | if ($info) 74 | $res = @unserialize($info); 75 | return $res; 76 | } 77 | 78 | private function buildBuffer($topic, $content, $qos = 0x00, $cmd = 0x30, $retain = 0) 79 | { 80 | $buffer = ""; 81 | $buffer .= $topic; 82 | if ($qos > 0) $buffer .= chr(rand(0, 0xff)) . chr(rand(0, 0xff)); 83 | $buffer .= $content; 84 | $head = " "; 85 | $head{0} = chr($cmd + ($qos * 2)); 86 | $head .= $this->setMsgLength(strlen($buffer) + 2); 87 | $package = $head . chr(0) . $this->setMsgLength(strlen($topic)) . $buffer; 88 | return $package; 89 | } 90 | 91 | private function setMsgLength($len) 92 | { 93 | $string = ""; 94 | do { 95 | $digit = $len % 128; 96 | $len = $len >> 7; 97 | if ($len > 0) 98 | $digit = ($digit | 0x80); 99 | $string .= chr($digit); 100 | } while ($len > 0); 101 | return $string; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Mqtt.php: -------------------------------------------------------------------------------- 1 | 'CONNECT', 92 | 2 => 'CONNACK', 93 | 3 => 'PUBLISH', 94 | 4 => 'PUBACK', 95 | 5 => 'PUBREC', 96 | 6 => 'PUBREL', 97 | 7 => 'PUBCOMP', 98 | 8 => 'SUBSCRIBE', 99 | 9 => 'SUBACK', 100 | 10 => 'UNSUBSCRIBE', 101 | 11 => 'UNSUBACK', 102 | 12 => 'PINGREQ', 103 | 13 => 'PINGRESP', 104 | 14 => 'DISCONNECT', 105 | ]; 106 | 107 | public function __construct($buffer) 108 | { 109 | $this->printStr($buffer); 110 | $this->buffer = $buffer; 111 | $this->decodeHeader(); 112 | $decoder = 'decode' . ucfirst(strtolower(static::$tp_map[$this->tp])); 113 | call_user_func([$this, $decoder]); 114 | } 115 | 116 | /** 117 | * decode header 118 | */ 119 | private function decodeHeader() 120 | { 121 | $fh = $this->bufPop(static::FL_FIXED); 122 | $byte = ord($fh); 123 | $this->tp = ($byte & 0xF0) >> 4; 124 | $this->dup = ($byte & 0x08) >> 3; 125 | $this->qos = ($byte & 0x06) >> 1; 126 | $this->retain = $byte & 0x01; 127 | $this->remain_len = $this->getRemainLen(); 128 | } 129 | 130 | /** 131 | * decode connect message 132 | */ 133 | private function decodeConnect() 134 | { 135 | $info['protocol'] = $this->bufPop(); 136 | $info['version'] = ord($this->bufPop(static::FL_FIXED)); 137 | $byte = ord($this->bufPop(static::FL_FIXED)); 138 | $info['auth'] = ($byte & 0x80) >> 7; 139 | $info['auth'] &= ($byte & 0x40) >> 6; 140 | $info['will_retain'] = ($byte & 0x20) >> 5; 141 | $info['will_qos'] = ($byte & 0x18) >> 3; 142 | $info['will_flag'] = ($byte & 0x04); 143 | $info['clean_session'] = ($byte & 0x02) >> 1; 144 | $keep_alive = $this->bufPop(0, 2); 145 | $info['keep_alive'] = 256 * ord($keep_alive[0]) + ord($keep_alive[1]); 146 | $info['client_id'] = $this->bufPop(); 147 | if ($info['auth']) { 148 | $info['username'] = $this->bufPop(); 149 | $info['password'] = $this->bufPop(); 150 | } 151 | $this->connect_info = $info; 152 | // if ($info['auth'] === 0) $this->replyConack(0x04); 153 | $this->replyConack(0x00); 154 | } 155 | 156 | /** 157 | * decode publish and reply puback/pubrec 158 | */ 159 | private function decodePublish() 160 | { 161 | $this->topic = $this->bufPop(); 162 | if ($this->qos > 0) { 163 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 164 | if ($this->qos === 1) $this->ack = chr(0x40) . chr(0x02) . $this->packet_id; //puback 165 | if ($this->qos === 2) $this->ack = chr(0x50) . chr(0x02) . $this->packet_id; //pubrec 166 | } 167 | $this->payload = $this->buffer; 168 | } 169 | 170 | /** 171 | * decode puback 172 | */ 173 | private function decodePuback() 174 | { 175 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 176 | } 177 | 178 | /** 179 | * decode pubrec and reply pubrel, 2nd packet of QoS 2 protocol exchange 180 | */ 181 | private function decodePubrec() 182 | { 183 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 184 | $this->ack = chr(0x62) . chr(0x02) . $this->packet_id; //pubrel 185 | } 186 | 187 | /** 188 | * decode pubrel and reply pubcomp, 3rd packet of QoS 2 protocol exchange 189 | * @throws \Exception 190 | */ 191 | private function decodePubrel() 192 | { 193 | if ($this->qos !== 1) throw new \Exception(' bad buffer #' . __METHOD__); 194 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 195 | $this->ack = chr(0x70) . chr(0x02) . $this->packet_id; //pubcomp 196 | } 197 | 198 | /** 199 | * decode pubcomp, 4th and final packet of the QoS 2 protocol exchange 200 | * @throws \Exception 201 | */ 202 | private function decodePubcomp() 203 | { 204 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 205 | } 206 | 207 | /** 208 | * decode subscribe, not support multiple topic 209 | * @throws \Exception 210 | */ 211 | private function decodeSubscribe() 212 | { 213 | if ($this->qos !== 1) throw new \Exception(' bad buffer #' . __METHOD__); 214 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 215 | $this->topic = $this->bufPop(); 216 | $this->req_qos = ord($this->bufPop(static::FL_FIXED)); 217 | $payload = chr($this->req_qos); 218 | $this->ack = chr(0x90) . ($payload === '' ? chr(0x02) : chr(0x02 + strlen($payload))) . $this->packet_id . $payload; //suback 219 | } 220 | 221 | /** 222 | * decode unsubscribe, not support multiple topic 223 | * @throws \Exception 224 | */ 225 | private function decodeUnsubscribe() 226 | { 227 | if ($this->qos !== 1) throw new \Exception(' bad buffer #' . __METHOD__); 228 | $this->packet_id = $this->bufPop(static::FL_FIXED, 2); 229 | $this->topic = $this->bufPop(); 230 | $this->ack = chr(0xB0) . chr(0x02) . $this->packet_id; //unsuback 231 | } 232 | 233 | private function decodePingreq() 234 | { 235 | $this->ack = chr(0xD0) . chr(0); 236 | } 237 | 238 | private function decodeDisconnect() 239 | { 240 | //Nothing here, Connect close in server 241 | } 242 | 243 | public function replyConack($flag = 0x00) 244 | { 245 | $this->ack = chr(0x20) . chr(0x02) . chr(0) . chr($flag); 246 | } 247 | 248 | private function bufPop($flag = 1, $len = 1) 249 | { 250 | if (1 === $flag) $len = 256 * ord($this->bufPop(0)) + ord($this->bufPop(0)); 251 | if (strlen($this->buffer) < $len) return ''; 252 | preg_match('/^([\x{00}-\x{ff}]{' . $len . '})([\x{00}-\x{ff}]*)$/s', $this->buffer, $matches); 253 | $this->buffer = $matches[2]; 254 | return $matches[1]; 255 | } 256 | 257 | public function getRemainLen() 258 | { 259 | $multiplier = 1; 260 | $value = 0; 261 | do { 262 | $encodedByte = ord($this->bufPop(static::FL_FIXED)); 263 | $value += ($encodedByte & 127) * $multiplier; 264 | if ($multiplier > 128 * 128 * 128) $value = -1; 265 | $multiplier *= 128; 266 | } while (($encodedByte & 128) != 0); 267 | return $value; 268 | } 269 | 270 | /** 271 | * @return int 272 | */ 273 | public function getTp() 274 | { 275 | return $this->tp; 276 | } 277 | 278 | /** 279 | * @return mixed 280 | */ 281 | public function getTopic() 282 | { 283 | return $this->topic; 284 | } 285 | 286 | /** 287 | * @return mixed 288 | */ 289 | public function getReqqos() 290 | { 291 | return $this->req_qos; 292 | } 293 | 294 | /** 295 | * @return mixed 296 | */ 297 | public function getPayload() 298 | { 299 | return $this->payload; 300 | } 301 | 302 | /** 303 | * @return mixed 304 | */ 305 | public function getConnectInfo() 306 | { 307 | return $this->connect_info; 308 | } 309 | 310 | /** 311 | * @return string 312 | */ 313 | public function getAck() 314 | { 315 | return $this->ack; 316 | } 317 | 318 | public function __get($name) 319 | { 320 | return call_user_func([$this, 'get' . ucfirst($name)]); 321 | } 322 | 323 | public function printStr($string) 324 | { 325 | $strlen = strlen($string); 326 | for ($j = 0; $j < $strlen; $j++) { 327 | $num = ord($string{$j}); 328 | if ($num > 31) 329 | $chr = $string{$j}; 330 | else 331 | $chr = " "; 332 | printf("%4d: %08b : 0x%02x : %s \n", $j, $num, $num, $chr); 333 | } 334 | } 335 | 336 | public function __toString() 337 | { 338 | return '#TP:' . $this->tp . ' #Topic:' . $this->topic . ' #Msg:' . $this->payload . PHP_EOL; 339 | } 340 | 341 | } -------------------------------------------------------------------------------- /src/Redis.php: -------------------------------------------------------------------------------- 1 | pool = []; 25 | $this->config = \Yii::$app->params['redis']; 26 | if (!isset($this->config['pool_size'])) 27 | $this->config['pool_size'] = 10; 28 | $this->openConnection($this->config['pool_size']); 29 | } 30 | 31 | public static function getRedis() 32 | { 33 | if (!self::$redis) 34 | self::$redis = new self(); 35 | return self::$redis; 36 | } 37 | 38 | public function openConnection($size = 1) 39 | { 40 | for ($i = 0; $i < $size; $i++) { 41 | $redis = $this->newConnection(); 42 | array_push($this->pool, $redis); 43 | } 44 | } 45 | 46 | public function newConnection() 47 | { 48 | //depend php-redis extention 49 | $redis = new \Redis(); 50 | $res = $redis->connect($this->config['host'], $this->config['port']); 51 | if ($res == false) 52 | throw new \Exception('failed to connect redis server.', 999); 53 | if (isset($this->config['auth']) && !$redis->auth($this->config['auth'])) 54 | throw new \Exception('Redis auth failed!'); 55 | return $redis; 56 | } 57 | 58 | public function getConnection() 59 | { 60 | $attempt = 10; 61 | do { 62 | $attempt--; 63 | $connection = array_shift($this->pool); 64 | if ($connection) break; 65 | else 66 | if ($attempt == 0) throw new \Exception('failed to increase redis pool', 999); 67 | echo '#increase redis pool' . PHP_EOL; 68 | $this->openConnection(); 69 | } while ($attempt); 70 | return $connection; 71 | } 72 | 73 | public function releaseConnection($connection) 74 | { 75 | array_push($this->pool, $connection); 76 | } 77 | 78 | public function __call($name, $arguments) 79 | { 80 | $redis = $this->getConnection(); 81 | try { 82 | $result = call_user_func_array([$redis, $name], $arguments); 83 | $this->releaseConnection($redis); 84 | } catch (\Exception $e) { 85 | var_dump($e->getMessage()); 86 | $result = false; 87 | } 88 | return $result; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/Task.php: -------------------------------------------------------------------------------- 1 | fd = $fd; 32 | $this->topic = $topic; 33 | $this->verb = $verb; 34 | $this->body = $payload; 35 | $this->resolve($topic); 36 | } 37 | 38 | /** 39 | * Mqtt Publish task 40 | * @param $fd 41 | * @param $topic 42 | * @param string $payload 43 | * @return static 44 | * @throws \Exception 45 | */ 46 | public static function publish($fd, $topic, $payload = '') 47 | { 48 | return new static($fd, $topic, $payload, 'publish'); 49 | } 50 | 51 | /** 52 | * Mqtt subscribe task 53 | * @param $fd 54 | * @param $topic 55 | * @return static 56 | * @throws \Exception 57 | */ 58 | public static function subscribe($fd, $topic, $req_qos) 59 | { 60 | return new static($fd, $topic, $req_qos, 'subscribe'); 61 | } 62 | 63 | /** 64 | * Redis subscribe task 65 | * 66 | * Can play like this: $redis->publish('supervisor', 'channel/play/100011'), 67 | * then the task will do something like mqtt publish 68 | * 69 | * @param $message 70 | * @return static 71 | */ 72 | public static function async($message) 73 | { 74 | return new static(0, $message, '', 'async'); 75 | } 76 | 77 | /** 78 | * internal job 79 | * @param $route 80 | * @param $param 81 | * @return static 82 | */ 83 | public static function internal($route, $param = '') 84 | { 85 | return new static(0, $route, $param, 'internal'); 86 | } 87 | 88 | private function resolve($topic) 89 | { 90 | if (preg_match('/(\w+)\/?(\w*)\/?(.*)/s', $topic, $routes)) { 91 | $this->class = $routes[1]; 92 | $this->func = isset($routes[2]) ? $routes[2] : 'default'; 93 | $this->param = isset($routes[3]) ? $routes[3] : ''; 94 | } 95 | //resolve async task from redis pub/sub, controller/action/param/payload 96 | if ($this->verb == 'async' && preg_match('/(\w+)\/(.*)/s', $this->param, $matches)) { 97 | $this->param = $matches[1]; 98 | $this->body = $matches[2]; 99 | } 100 | } 101 | 102 | public function __toString() 103 | { 104 | return '#Task# verb: ' . $this->verb . ' controller: ' . $this->class . ' action: ' . $this->func . ' param: ' . $this->param . ' payload: ' . $this->body . PHP_EOL; 105 | } 106 | } --------------------------------------------------------------------------------