├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app.js ├── app └── extend │ └── application.js ├── lib ├── emqtt.js ├── loader.js └── msgMiddleware.js └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | *.swp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | - '9' 6 | install: 7 | - npm i npminstall && npminstall 8 | script: 9 | - npm run ci 10 | after_script: 11 | - npminstall codecov && codecov 12 | services: 13 | - redis-server 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-mqtt 2 | 3 | mqtt client based on mqtt for egg framework 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ npm i egg-emqtt --save 9 | ``` 10 | 11 | ## Configuration 12 | 13 | Change `${app_root}/config/plugin.js` to enable mqtt plugin: 14 | 15 | ```js 16 | exports.emqtt = { 17 | enable: true, 18 | package: 'egg-emqtt', 19 | }; 20 | ``` 21 | 22 | Configure mqtt information in `${app_root}/config/config.default.js`: 23 | 24 | **Single Client** 25 | 26 | ```javascript 27 | config.emqtt = { 28 | client: { 29 | host: 'mqtt://xxx.xxx.xxx.xxx', 30 | password: 'xxxxx', 31 | username: 'xxxxx', 32 | clientId: 'xxxxx', 33 | options: { 34 | keepalive: 60, 35 | protocolId: 'MQTT', 36 | protocolVersion: 4, 37 | clean: true, 38 | reconnectPeriod: 1000, 39 | connectTimeout: 30 * 1000, 40 | rejectUnauthorized: false, 41 | ... 42 | }, 43 | msgMiddleware: [ 'xxxx' ], 44 | }, 45 | } 46 | ``` 47 | 48 | **Multi Clients** 49 | 50 | ```javascript 51 | config.emqtt = { 52 | clients: { 53 | foo: { 54 | host: 'mqtt://xxx.xxx.xxx.xxx', 55 | password: 'xxxxx', 56 | username: 'xxxxx', 57 | clientId: 'xxxxx', 58 | options: { 59 | keepalive: 60, 60 | protocolId: 'MQTT', 61 | protocolVersion: 4, 62 | clean: true, 63 | reconnectPeriod: 1000, 64 | connectTimeout: 30 * 1000, 65 | rejectUnauthorized: false, 66 | ... 67 | }, 68 | msgMiddleware: [ 'xxxx' ], 69 | }, 70 | bar: { 71 | ... 72 | }, 73 | } 74 | } 75 | ``` 76 | 77 | See [mqtt API Documentation](https://github.com/mqttjs/MQTT.js) for all available options. 78 | 79 | ## Usage 80 | 81 | In controller, you can use `app.emqtt` to get the mqtt instance, check [mqtt](https://github.com/mqttjs/MQTT.js) to see how to use. 82 | 83 | ```js 84 | // app/router.js 85 | 86 | module.exports = app => { 87 | const { router } = app; 88 | router.get('/', app.emqtt.controller.home.index); 89 | 90 | // mqtt_client,subscribe topic: a 91 | app.emqtt.route('a', app.mqtt.controller.home.index); 92 | }; 93 | 94 | // app/mqtt/controller/home.js 95 | 96 | module.exports = app => { 97 | return class HomeController extends app.Controller { 98 | async index() { 99 | /** 100 | * ctx.req = { 101 | * topic: 'a', 102 | * msg: 'xxxxxxxxxxxx', 103 | * } 104 | */ 105 | // publish 106 | await this.app.emqtt.publish('topic1', 'msg123456', { qos: 0 }); 107 | } 108 | }; 109 | }; 110 | 111 | // app/mqtt/middleware/msg2json.js 112 | module.exports = () => { 113 | return async (ctx, next) => { 114 | try { 115 | ctx.logger.info(ctx.req.msg); 116 | ctx.req.message = JSON.parse(ctx.req.msg); 117 | } catch (err) { 118 | ctx.logger.error(err); 119 | } 120 | await next(); 121 | ctx.logger.info(`Response_Time: ${ctx.starttime ? Date.now() - ctx.starttime : 0}ms Topic:${ctx.req.topic} Msg: ${ctx.req.msg}`); 122 | }; 123 | }; 124 | 125 | ``` 126 | 127 | ### Multi Clients 128 | 129 | If your Configure with multi clients, you can use `app.emqtt.get(instanceName)` to get the specific mqtt instance and use it like above. 130 | 131 | ## Questions & Suggestions 132 | 133 | Please open an issue [here](https://github.com/eggjs/egg/issues). 134 | 135 | ## License 136 | 137 | [MIT](LICENSE) 138 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const emqtt = require('./lib/emqtt'); 4 | 5 | module.exports = app => { 6 | if (app.config.emqtt) emqtt(app); 7 | }; 8 | -------------------------------------------------------------------------------- /app/extend/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmqttSymbol = Symbol('EGG-EMQTT#EMQTT'); 4 | const debug = require('debug')('egg-emqtt:app:extend:application.js'); 5 | 6 | module.exports = { 7 | get mqtt() { 8 | if (!this[EmqttSymbol]) { 9 | debug('[egg-emqtt] create Emqtt instance!'); 10 | this[EmqttSymbol] = {}; 11 | } 12 | return this[EmqttSymbol]; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /lib/emqtt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const is = require('is-type-of'); 4 | const assert = require('assert'); 5 | const Buffer = require('buffer').Buffer; 6 | const http = require('http'); 7 | const mqtt = require('mqtt'); 8 | 9 | const loader = require('./loader'); 10 | const msgMiddleware = require('./msgMiddleware'); 11 | 12 | module.exports = app => { 13 | loader(app); 14 | app.addSingleton('emqtt', createOneClient); 15 | }; 16 | 17 | function createOneClient(config, app) { 18 | assert(is.string(config.host), 'config.host must be String!'); 19 | assert(is.string(config.username), 'config.username must be String!'); 20 | assert(is.string(config.password), 'config.password must be String!'); 21 | assert(is.string(config.clientId), 'config.clientId must be String!'); 22 | 23 | const mqttClient = mqtt.connect(config.host, { 24 | clientId: config.clientId, 25 | username: config.username, 26 | password: config.password, 27 | keepalive: 60, 28 | protocolId: 'MQTT', 29 | protocolVersion: 4, 30 | clean: true, 31 | reconnectPeriod: 1000, 32 | connectTimeout: 30 * 1000, 33 | rejectUnauthorized: false, 34 | ...config.options, 35 | }); 36 | 37 | mqttClient.on('connect', () => { 38 | app.coreLogger.info('[egg-emqtt] connected %s@%s:%s:%s/%s', config.host, config.username, config.password, config.clientId, config.options); 39 | }); 40 | mqttClient.on('error', error => { 41 | app.coreLogger.error('[egg-emqtt] error clientid:%s', config.clientId); 42 | app.coreLogger.error(error); 43 | }); 44 | mqttClient.on('offline', () => { 45 | app.coreLogger.error('[egg-emqtt] offline clientid:%s', config.clientId); 46 | }); 47 | mqttClient.on('reconnect', () => { 48 | app.coreLogger.error('[egg-emqtt] reconnect clientid:%s', config.clientId); 49 | }); 50 | 51 | mqttClient.route = (topic, handler) => { 52 | mqttClient.subscribe(topic); 53 | app.coreLogger.info('[egg-emqtt] subscribe clientid:%s topic:%s', config.clientId, topic); 54 | 55 | const msgMiddlewares = []; 56 | const msgMiddlewareConfig = config.msgMiddleware; 57 | if (msgMiddlewareConfig) { 58 | assert(is.array(msgMiddlewareConfig), 'config.msgMiddleware must be Array!'); 59 | for (const middleware of msgMiddlewareConfig) { 60 | assert(app.mqtt.middleware[middleware], `can't find middleware: ${middleware} !`); 61 | msgMiddlewares.push(app.mqtt.middleware[middleware]); 62 | } 63 | } 64 | const topicAuth = new RegExp('^' + topic.replace('$queue/', '').replace(/^\$share\/([A-Za-z0-9]+)\//, '').replace(/([\[\]\?\(\)\\\\$\^\*\.|])/g, '\\$1').replace(/\+/g, '[^/]+').replace(/\/#$/, '(\/.*)?') + '$'); // emqtt兼容,共享订阅 65 | mqttClient.on('message', (top, message) => { 66 | if (!topicAuth.test(top)) return; 67 | const msg = Buffer.from(message).toString('utf8'); 68 | 69 | const request = { topic: top, msg, socket: { remoteAddress: `topic:${topic} ` }, method: 'sub', userId: `${config.username}:${config.clientId}` }; 70 | msgMiddleware(app, request, msgMiddlewares, () => { 71 | const ctx = app.createContext(request, new http.ServerResponse(request)); 72 | ctx.method = request.method; 73 | ctx.url = top; 74 | ctx.userId = request.userId; 75 | ctx.starttime = Date.now(); 76 | handler.call(ctx) 77 | .catch(e => { 78 | e.message = '[egg-emqtt] controller execute error: ' + e.message; 79 | app.coreLogger.error(e); 80 | }); 81 | }); 82 | }); 83 | }; 84 | 85 | return mqttClient; 86 | } 87 | -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('egg-emqtt:lib:loader.js'); 4 | const path = require('path'); 5 | 6 | module.exports = app => { 7 | let dirs = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app', 'mqtt', 'middleware')); 8 | 9 | app.mqtt.middleware = app.mqtt.middleware || {}; 10 | new app.loader.FileLoader({ 11 | directory: dirs, 12 | target: app.mqtt.middleware, 13 | inject: app, 14 | }).load(); 15 | 16 | /* istanbul ignore next */ 17 | app.mqtt.__defineGetter__('middlewares', () => { 18 | app.deprecate('app.mqtt.middlewares has been deprecated, please use app.mqtt.middleware instead!'); 19 | return app.mqtt.middleware; 20 | }); 21 | 22 | debug('[egg-mqtt] app.mqtt.middleware:', app.mqtt.middleware); 23 | 24 | dirs = app.loader.getLoadUnits().map(unit => path.join(unit.path, 'app', 'mqtt', 'controller')); 25 | 26 | app.mqtt.controller = app.mqtt.controller || {}; 27 | app.loader.loadController({ 28 | directory: dirs, 29 | target: app.mqtt.controller, 30 | }); 31 | 32 | /* istanbul ignore next */ 33 | app.mqtt.__defineGetter__('controllers', () => { 34 | app.deprecate('app.mqtt.controllers has been deprecated, please use app.mqtt.controller instead!'); 35 | return app.mqtt.controller; 36 | }); 37 | debug('[egg-mqtt] app.mqtt.controller:', app.mqtt.controller); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/msgMiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const compose = require('koa-compose'); 5 | const eggUtil = require('egg-core').utils; 6 | 7 | module.exports = (app, request, packetMiddlewares, next) => { 8 | const ctx = app.createContext(request, new http.ServerResponse(request)); 9 | ctx.method = request.method; 10 | ctx.url = request.topic; 11 | ctx.userId = request.userId; 12 | ctx.starttime = Date.now(); 13 | 14 | packetMiddlewares = packetMiddlewares.map(mw => eggUtil.middleware(mw)); 15 | const composed = compose(packetMiddlewares); 16 | 17 | composed(ctx, async () => { 18 | next(); 19 | }) 20 | .catch(e => { 21 | next(e); 22 | app.coreLogger.error(e); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-emqtt", 3 | "version": "1.0.0", 4 | "description": "egg plugin for mqtt", 5 | "eggPlugin": { 6 | "name": "emqtt" 7 | }, 8 | "keywords": [ 9 | "egg", 10 | "eggPlugin", 11 | "egg-plugin", 12 | "mqtt", 13 | "websocket" 14 | ], 15 | "dependencies": { 16 | "debug": "^3.1.0", 17 | "is-type-of": "^1.2.0", 18 | "koa-compose": "^4.0.0", 19 | "mqtt": "^2.17.0" 20 | }, 21 | "devDependencies": { 22 | "autod": "^3.0.1", 23 | "egg": "^2.2.0", 24 | "egg-bin": "^4.3.7", 25 | "egg-ci": "^1.8.0", 26 | "egg-mock": "^3.14.0", 27 | "eslint": "^4.15.0", 28 | "eslint-config-egg": "^6.0.0", 29 | "pedding": "^1.1.0", 30 | "rimraf": "^2.6.2", 31 | "semver": "^5.4.1", 32 | "supertest": "^3.0.0", 33 | "uws": "^8.14.1", 34 | "webstorm-disable-index": "^1.2.0" 35 | }, 36 | "engines": { 37 | "node": ">=6.0.0" 38 | }, 39 | "scripts": { 40 | "test": "npm run lint -- --fix && npm run test-local", 41 | "test-local": "egg-bin test", 42 | "cov": "egg-bin cov", 43 | "lint": "eslint .", 44 | "autod": "autod" 45 | }, 46 | "files": [ 47 | "app", 48 | "lib", 49 | "config", 50 | "app.js" 51 | ], 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/luofeng1/egg-emqtt.git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/eggjs/egg/issues" 58 | }, 59 | "homepage": "https://github.com/luofeng1/egg-emqtt#readme", 60 | "author": "luofeng1 ", 61 | "license": "MIT" 62 | } --------------------------------------------------------------------------------