使用Express+TypeScript编写后台服务

178 阅读9分钟

项目背景

最近接到一个比较简单的任务,需求如下: 1、从MQTT服务器订阅断电报警信息然后入库到SQLServer或者MySQL数据库中 2、从MQTT服务器订阅到站点报警(0断电,1来电)、GPS信息(经纬度)、设备信号,然后在内存中缓存每个站点的这三种信息,再加上最新通信时间(接收到订阅的消息的最新时间), 3、针对每个站点(SS打头的编码)和ClientID(设备编码),做一个HTTP GET请求接口,前端可以根据站点编码和设备编码请求该站点的数据,主要是为后期做站点在线、离线状态判断、断电告警来服务的。 程序简单的思维导图如下图所示: 程序思维导图 本来打算使用C++写的,考虑到C++写HTTP接口相对比较麻烦,还是采用Nodejs写比较方便,因为Nodejs对于MQTT、HTTP的支持比较友好,比较适合写这种简单的后台程序。 程序大概的流程是: 1、从MQTT服务器上订阅如下的三种主题消息: 订阅主题 (1). 报警, 0断电, 1来电 /alarmSing 0

消息主题和内容示例如下: /alarmSing/865650043997457=>0

(2). GPS信息 /lbsLocation lat=022.6315208&lng=114.0741963

消息主题和内容示例如下: /lbsLocation/865650043997457=> lat=022.6315208&lng=114.0741963

(3).设备信号 /csq 18 消息主题和内容示例如下: /csq/865650043997457=>27

需要在config.yaml文件中配置好MQTT服务器的配置信息,示例如下:

rxmqtt:
  host:    127.0.0.1
  port:     8099
  user:     poweralarm
  pwd: "poweralarm@123"
  id: "mqweb_20200826_nodejs_alarm"
  clean:    true

然后先连接MQTT服务器,设置订阅的主题并针对这三个主题分别写对应的回调处理函数。

2、在内存中维护一张站点信息的Map缓存数据结构,这里为了方便选择了TypeScript编写,

 stationInfos: Map<string, StationInfo>;

其中StationInfo是一个站点信息类

3、在接收到MQTT服务器推送的报警(/alarmSing)、GPS信息(/lbsLocation)、设备信号(/csq )这三种消息时,分别修改stationInfos这个Map缓存对象,并根据传递的DeviceId查询是否存在该站点,如果存在则更新设置对应的数据、最新通信时间、站点在线状态等。 4、编写http接口,根据站点编码集合站点信息Map缓存stationInfos返回对应的信息 5、当接收到站点断电消息时除了更新stationInfos缓存外,还需要将对应的断电报警信息入库。

数据库结构

目前数据库操作只涉及到两张表:站点和设备ID表Breakelectric以及断电报警记录表PowerCutHistory

MySQL数据表结构

DROP TABLE IF EXISTS `breakelectric`;
CREATE TABLE `breakelectric`  (
  `SStation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '站点编码',
  `DeviceId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '设备Id',
  `SStationName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '站点名称'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

DROP TABLE IF EXISTS `powercuthistory`;
CREATE TABLE `powercuthistory`  (
  `SStation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `DeviceId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `SDateTime` datetime(0) NULL DEFAULT NULL,
  `DevState` bit(1) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

SQLServer数据表结构

DROP TABLE [dbo].[Breakelectric]
GO
CREATE TABLE [dbo].[Breakelectric] (
[SStation] varchar(255) NOT NULL ,
[DeviceId] varchar(100) NULL ,
[SStationName] varchar(255) NOT NULL 
)

DROP TABLE [dbo].[PowerCutHistory]
GO
CREATE TABLE [dbo].[PowerCutHistory] (
[SStation] varchar(255) NULL ,
[DeviceId] varchar(100) NULL ,
[SDateTime] datetime NULL ,
[DevState] bit NULL 
)

几个关键的封装类

MQTT-TypeScript封装

为了简便,将MQTT客户端封装成一个类来使用,代码如下:

import mqtt = require('mqtt')
import moment = require('moment')

export interface MqttConnOpt extends mqtt.IClientOptions{}

export declare type OnMessageFunc = (topic: string, payload: Buffer) => void

declare class Topic {
  public topic: string;
  public qos: 0|1|2;
}

export class MQTT {
  mqclient: mqtt.MqttClient;
  brokerHost: string;
  brokerPort: number;
  subscribeTopics: Array<Topic>;
  subscribeCallbacks: Map<string, OnMessageFunc>;
  connOpt: MqttConnOpt;
  /**
   * 是否成功连接到MQTT broker
   */
  connected: boolean;

  constructor(host?: string | any, port?: number) {
    this.brokerHost = host;
    this.brokerPort = port;
    this.subscribeTopics = new Array<Topic>();
    this.subscribeCallbacks = new Map<string, OnMessageFunc>();
    this.connected = false;
  }

  /**
   * 订阅主题
   */
  public subscribe(topic: string, qos: 0|1|2) {
    this.subscribeTopics.push({topic: topic, qos: qos});
    if (this.is_connected()){
      this.mqclient.subscribe(topic, {qos: qos});
    }
  }

  /**
   * 设置消息数据回调函数
   */
  public set_message_callback(topicPatten: string, cb: OnMessageFunc) {
    this.subscribeCallbacks.set(topicPatten, cb);
  }

  /**
   * 是否已连接到服务器
   */
  public is_connected() {
    // return this.mqclient.connected == true;
    return this.connected == true;
  }

  /**
   * 连接到服务器
   */
  public connect(opts?: MqttConnOpt){
    // 打开重新订阅
    opts.resubscribe = false;
    this.connOpt = opts;
    this.mqclient = mqtt.connect(`mqtt://${this.brokerHost}:${this.brokerPort}`, opts);

    this.mqclient.on('connect', (connack)=>{
      console.log(`成功连接到服务器[${this.brokerHost}:${this.brokerPort}]`);
      this.connected = true;
      for (let index = 0; index < this.subscribeTopics.length; index++) {
        const element = this.subscribeTopics[index];
        this.mqclient.subscribe(element.topic, {qos: element.qos});
      }
    });

    this.mqclient.on('message', (topic: string, payload: Buffer)=>{
      console.log(`[${moment().format('YY-MM-DD HH:mm')}] ${this.brokerHost} ${topic}`)
      this.mqclient;
      this.subscribeCallbacks.forEach((val, key)=>{
        if (topic.indexOf(key) != -1){
          val(topic, payload);
        }
      });
    });

    this.mqclient.on('reconnect', ()=>{
      console.log("重新连接")
    });

    this.mqclient.on('error', (err: Error)=>{
      console.log(err)
    });
  }

  /**
   * 推送数据
   */
  public publish(topic: string, message: string, qos: 0|1|2) {
    this.mqclient.publish(topic, message, {qos: qos, retain: false})
  }

}

其中,需要注意的一点就是MQTT服务器有可能意外重启或者其他原因断开,这时需要断线重连。在C++、C#、Java等语言中可以开启一个断线重连监测线程,每隔一段时间监测与MQTT服务器的连接情况,如果断线则重新连接。

yaml文件配置类对象

为了方便这里采用yaml文件作为配置文件,之前使用C++时也常用xml、ini、yaml作为配置文件,Java SpringBoot也常用yml或yaml作为配置文件。 我的yaml配置文件如下图所示:

rxmqtt:
  host:    127.0.0.1
  port:     8099
  user:     poweralarm
  pwd: "poweralarm@123"
  id: "mqweb_20200826_nodejs_alarm"
  clean:    true
# dbsql:
#   host: 127.0.0.1
#   port: 1433
#   user: sa
#   pwd: "123456"
#   database: EMCHNVideoMonitor
dbsql:
  host: 127.0.0.1
  port: 3306
  user: root
  pwd: "123456"
  database: EMCHNVideoMonitor
redis: 
  host:     127.0.0.1
  port:     7001
  pwd: 123456
  index:     3
http: 3000
rpcUrl: 127.0.0.1:18885
enableMqtt: true
enableDB: true
enableRedis: true
enableWS: true
enableRPC: true
offlineTimeout: 90000
cacheInterval: 10000

针对上面的yaml配置文件,编写对应的yaml配置读取类,如下所示:

import YAML = require('yaml')
import fs = require('fs')

declare interface MqttConnOpt{
  host: string;
  port: number;
  user: string;
  pwd: string;
  clean: boolean;
  id: string;
}
declare interface DBConnOpt{
  host: string;
  port: number;
  user: string;
  pwd: string;
  database: string;
}
declare interface RedisConnOpt{
  host: string;
  port: number;
  pwd: string;
  db: number;
}

export {
  MqttConnOpt,
  DBConnOpt,
  RedisConnOpt,
  Config,
}


class Config {
  rxmqtt: MqttConnOpt;
  dbsql: DBConnOpt;
  redis: RedisConnOpt;
  /**
   * http 端口
   */
  http: number;
  /**
   * rpcUrl 服务器地址
   */
  rpcUrl: string;
  /**
   * 是否启用mqtt
   */
  enableMqtt: boolean;
  /**
   * 是否启用sqlServer或者mysql数据库
   */
  enableDB: boolean;
  /**
   * 是否启用redis
   */
  enableRedis: boolean;
  /**
   * 是否启用websocket
   */
  enableWS: boolean;
  /**
   * 是否启用RPC
   */
  enableRPC: boolean;
  /**
   * 离线超时时间, 毫秒
   */
  offlineTimeout: number;
  /**
   * 缓存存储间隔, 毫秒
   */
  cacheInterval: number;

  constructor(){
    try{
      let buffer = fs.readFileSync('config.yaml', 'utf8');
      let config = YAML.parse(buffer);
      this.rxmqtt = config['rxmqtt'];
      this.dbsql = config['dbsql'];
      this.redis = config['redis'];
      this.http = config['http'];
      this.rpcUrl = config['rpcUrl'];
      this.enableMqtt = config['enableMqtt'];
      this.enableDB = config['enableDB'];
      this.enableRedis = config['enableRedis'];
      this.enableWS = config['enableWS'];
      this.enableRPC = config['enableRPC'];
      this.offlineTimeout = config['offlineTimeout'];
      this.cacheInterval = config['cacheInterval'];
    }catch(err){
      console.log(err)
    }
  }

  /**
   * save
   */
  public save() {
    try{
      fs.writeFileSync('config.yaml', YAML.stringify(this))
    }catch(err){
      console.log(err)
    }
  }
}

其实使用yaml这个第三方库结合typescript读写yaml文件还是比较方便的。

数据操作类的封装

mysql操作类

nodejs中可以使用mariadb或者sequelize等库操作mysql数据库,这里使用mariadb这个库 MariaDBClient.ts

import mariadb = require('mariadb')
import { StationInfo } from './StationInfo'
import moment = require('moment')

// 定义数据查询回调接口
export declare type OnQueryInfoReqCallback = (err: Error, rc: Array<any>) => void
// 定义入库回调接口
export declare type OnRecordReqCallback = (err: Error, rc: boolean) => void

export class MariaDBClient {
  dbpool: mariadb.Pool;
  host: string;
  port: number;
  user: string;
  password: string;
  dbName: string;
  connected: boolean;

   // 站点信息 Map
   public stationInfos: Map<string, StationInfo>;

  constructor(username: string, password: string, dbName: string, host?: string | any, port?: number) {
    this.host = host;
    this.port = port;
    this.user = username;
    this.password = password;
    this.dbName = dbName;
    this.connected = false;
    this.stationInfos = new Map<string, StationInfo>();
    // 初始化mariadb数据库客户端
    this.initMariadb();
    // 加载站点信息到内存中
    this.getStationInfo();
  }

   /**
   * 初始化mariadb数据库客户端
   */
  public initMariadb() {
    this.dbpool = mariadb.createPool({
      host: this.host,
      port: this.port,
      user: this.user,
      password: this.password,
      database: this.dbName,
      connectionLimit: 10,
    });
  }

  /**
   * 是否已连接到MariaDB数据库
   */
  public is_connected() {
    return this.connected == true;
  }

   /**
   * 获取站点信息
   */
  public async getStationInfo() {
    let conn;
    try {
      conn = await this.dbpool.getConnection();
      const rows = await conn.query("SELECT SStation, DeviceId, SStationName from Breakelectric WHERE SStation != '' AND DeviceId != '';");
      for (let i = 0; i < rows.length; i++) {
        const it = rows[i];
        const SStation = it['SStation'];
        this.stationInfos.has
        if (!this.stationInfos.has(SStation)) {
          let si = new StationInfo();
          si.SStation = it['SStation'];
          si.DeviceId = it['DeviceId'];
          si.SStationName = it['SStationName'];
          console.log(`第${i + 1}个站点:站点编码:${si.SStation},设备Id: ${si.DeviceId},站点名称:${si.SStationName}`);

          this.stationInfos.set(SStation, si);
        } 
      }
    } catch (e) {
      console.error(e);
    } finally {
      if (conn) conn.release(); //release to pool
    }
  }

   /**
   * 
   * @param record 获取站点列表
   */
  public async getStationList(cb: OnQueryInfoReqCallback) {
    let conn;
    try {
      conn = await this.dbpool.getConnection();
      const rows = await conn.query("SELECT SStation, DeviceId, SStationName from Breakelectric WHERE SStation != '' AND DeviceId != '';");
      let stationList = new Array<any>();
      for (let i = 0; i < rows.length; i++) {
        const rowItem = rows[i];
        let iitem = {
          'SStation': rowItem['SStation'],
          'DeviceId': rowItem['DeviceId'],
          'SStationName': rowItem['SStationName']
        }
        stationList.push(iitem);
      }
      if (cb) cb(null, stationList);
    } catch (e) {
      console.error(e);
      if (cb) cb(e, null);
    } finally {
      if (conn) conn.release(); //release to pool
    }
  }

  /**
  * 增、删、改、查 CRUD操作 API
  */

  /**
   * 
   * @param record 插入断电报警记录
   * @param cb 
   */
  public async insertStationRecord(record: any) {
    if (record === null) {
      return;
    }
    let sql1 = "INSERT INTO `emchnvideomonitor`.`powercuthistory` (`SStation`, `DeviceId`, `SDateTime`, `DevState`)  VALUES";

    let conn: mariadb.PoolConnection;
    try {
      conn = await this.dbpool.getConnection();
      
      var sqlstr = sql1;

      let SStation = record.SStation;    // 站点名称
      let DeviceId = record.DeviceId;    // 设备Id
      let SDateTime = record.SDateTime;  // 时间
      let DevState = record.DevState;    // 状态(0停电,1来电)

      var it = `('${SStation}','${DeviceId}','${SDateTime}',${DevState})`;
      sqlstr += it;
      console.log(sqlstr);
      await conn.query(sqlstr);
      // if (cb) cb(null, true);
    } catch (e) {
      console.error('插入断电报警信息失败,',e);
      // if (cb) cb(e, false);
    } finally {
      if (conn) conn.release(); //release to pool
    }
  }
}

sqlserver操作类

nodejs中可以使用tedious、mmsql、sequelize等库操作sqlserver数据库,这里采用mssql封装sqlserver操作: MariaDBClient.ts

import mssql = require('mssql');

// 定义数据查询回调接口
export declare type OnQueryCallback = (err: Error, rc: any) => void
export declare type OnExecCallback = (err: Error, rc: boolean) => void

export class MSSQLDBClient {
  // 数据库连接字符串
  // 连接方式:"mssql://用户名:密码@ip地址:1433(默认端口号)/数据库名称"
  constr: string;

  constructor(username: string, password: string, host: string, port: number, dbName: string) {
    this.constr = `mssql://${username}:${password}@${host}:${port}/${dbName}`;
    mssql.connect(this.constr).then(function () {
      console.log('----------------');
      console.log('-数据库登录成功-');
      console.log('----------------');
    }).catch(function (err) {
      console.log(err);
    })
  }

  /**
   * 根据sql脚本查询数据库中的表
   * @param strSql SQL脚本
   * @param cb 查询结果的回调函数
   */
  public async query(strSql: string, cb: OnQueryCallback) {
    try {
      await mssql.connect(this.constr).then(function() {
        new mssql.Request().query(strSql).then(function(result) {
          // console.log(result);
          if (cb) cb(null, result);
        }).catch(function(err) {
          console.log(err);
          if (cb) cb(err, null);
        });
        // Stored Procedure
    }).catch(function(err) {
      console.log(err);
      if (cb) cb(err, null);
    })
    } catch (err) {
      console.log(err);
      if (cb) cb(err, null);
    } 
  }

  /**
   * 
   * @param strSql SQL脚本
   * @param cb 执行SQL脚本的回调
   */
  public async exec(strSql: string, cb: OnExecCallback) {
    await mssql.connect(this.constr, function () {
      mssql.query(strSql, function (err, data) {
        if (err) {
          if (cb) cb(err, false);
        } else {
          if (cb) cb(null, true);
          mssql.close();
        }
      });
    }
    );
  } 
}

主服务类 service.ts

import moment = require('moment')
import sql = require('mssql')
import { Config } from './config'
import { MQTT } from './mq'
import * as http from 'http'
import { StationInfo } from './StationInfo'
// const mssqlDBClient = require('./db');
// import { MSSQLDBClient } from './MSSQLDBClient'
import { MariaDBClient } from './MariaDBClient'


/**********************************************************************************************************
 * 1、从MQTT服务器订阅断电报警信息然后入库到SQLServer数据库中
 * 2、从MQTT服务器订阅到站点报警(0断电,1来电)、GPS信息、设备信号,然后在内存中分别缓存每个站点的这三种信息,再加上最新通信时间(接收到订阅的消息的最新时间),
 * 然后针对每个站点(SS打头的编码)和ClientID(设备编码),做一个HTTP GET请求接口,前端可以根据站点编码和设备编码请求该站点的数据,
 * 主要是为后期做站点在线、离线来服务的。
 */

export class Service {
  mqttList: Array<MQTT>;      // mqtt客户端列表
  stationInfos: Map<string, StationInfo>;
  config: Config;
  Server: http.Server;
  App: any;
  // mssqlDBClient: MSSQLDBClient;
  mySQLDBClient: MariaDBClient;

  // 构造函数
  constructor(app:any, server:http.Server) {
    this.Server = server;
    this.App = app;
    this.config = new Config();
    // 初始化配置
    // this.mssqlDBClient = new MSSQLDBClient(
    //   this.config.dbsql.user,
    //   this.config.dbsql.pwd,
    //   this.config.dbsql.host,
    //   this.config.dbsql.port,
    //   this.config.dbsql.database
    // );
     // 创建数据库客户端
     this.mySQLDBClient = new MariaDBClient(
      this.config.dbsql.user,
      this.config.dbsql.pwd,
      this.config.dbsql.database,
      this.config.dbsql.host,
      this.config.dbsql.port
    );
    
    this.mqttList = new Array<MQTT>();
    this.stationInfos = new Map<string, StationInfo>();
    // 建立客户端连接
    if (this.config.enableMqtt) {
      this.connectMqtt();
    }
    // 加载缓存数据到内存中
    this.LoadStations();
    // 定时检查站点是否在线
    // this.taskCheckStationOnline();
    // 定时存储站点数据缓存
    this.taskStoreStationData();
    // 定时重载站点信息
    this.timerLoadStationInfo();
    // 初始化http请求
    this.initApp();
  }

  /**
   * 定时加载站点信息
   */
  public timerLoadStationInfo() {
    setInterval(async ()=>{
      // this.getStationInfo();
      await this.mySQLDBClient.getStationInfo();
      this.stationInfos = this.mySQLDBClient.stationInfos;
    }, 120*1000);
  }

  /**
   * 加载缓存数据到内存中
   */
  public async LoadStations() {
    // 加载最后的缓存数据
    // await this.LoadRedisData();
    // 加载站点信息
    // await this.getStationInfo();
    await this.mySQLDBClient.getStationInfo();
    this.stationInfos = this.mySQLDBClient.stationInfos;
  }
 
  /**
   * 定时检查站点的状态
   */
  public taskCheckStationOnline() {
    setInterval(()=>{
      this.timerCheckOnline();
    }, this.config.offlineTimeout);
  }

  /**
   * 定时存储站点缓存数据
   */
  public taskStoreStationData() {
    setInterval(()=>{
      // this.timerStorStationData();
      // this.taskStorNewData();
    }, this.config.cacheInterval);
  }


  /**
   * 检查站点是否在线
   */
  public timerCheckOnline() {
    let stcodeIds = [];
    this.stationInfos.forEach((val, key) => {
      let previous_online = val.Online;
      val.checkOnline();
      // 如果先前在线,现在离线
      if (previous_online && !val.Online) {
        stcodeIds.push(val.SStation);
        // this.sendHeart2WSClient(val.toWebHeartString());
      }
    })
  }

  /**
   * 初始化http请求
   */
  public initApp() {
    if (!this.App) {
      return;
    }
    // 路由接口
    // 获取所有的站点编码和设备ID映射列表
    this.App.get('/api/getStationList', (req, res) => {
      let stationList = [];
      this.stationInfos.forEach((val, key) => {
        stationList.push({
          'SStation': val.SStation,
          'DeviceId': val.DeviceId,
          'SStationName': val.SStationName
         });
      });
      res.send({
        rc: true,
        data: stationList
      })
    });
    
    // 根据站点编码获取当前站点信息
    this.App.get('/api/getAlarmInfo/:stcode', (req, res) => {
      let { stcode } = req.params;
      if (this.stationInfos.has(stcode)) {
        let item = this.stationInfos.get(stcode);
        return res.send({
          rc: true,
          data: item
        })
      } else {
        res.send({
          rc: false,
          data: '站点编码不存在'
        })
      }
    })
    // 获取所有站点断电报警、设备信号、经纬度等信息
    this.App.get('/api/getAllStationInfos', (req, res) => {
      let stationList = [];
      this.stationInfos.forEach((val, key) => {
        stationList.push(val);
      })
      res.send({
        rc: true,
        data: stationList
      })
    })
  }

  /**
   * 获取站点信息
   */
  public async getStationInfo() {
    // 一、SQLServer
    // let strSql = "SELECT SStation, DeviceId, SStationName from Breakelectric WHERE SStation != '' AND DeviceId != '';";
    // this.mssqlDBClient.query(strSql, (err, result) => {
    //   if (result == null || result == '' || result.recordsets[0] == undefined
    //     || result.recordsets[0] == null) {
    //     return;
    //   }
    //   let resultArray = result.recordsets[0];
    //   if (resultArray != null && resultArray.length > 0) {
    //     for (let i = 0; i < resultArray.length; i++) {
    //       // this.stationList.push(resultArray[i]);
    //       let iitem = resultArray[i];
    //       console.log(`第${i+1}个站点,SStation:${iitem.SStation},DeviceId: ${iitem.DeviceId},SStationName: ${iitem.SStationName}`);
    //       // console.log(resultArray[i]);
    //       let stcode = iitem['SStation'];
    //       if (!this.stationInfos.has(stcode)) {
    //         this.stationInfos.set(stcode, new StationInfo());
    //       }
    //       var si = this.stationInfos.get(stcode);
    //       si.SStation = iitem['SStation'];
    //       si.DeviceId = iitem['DeviceId'];
    //       si.SStationName = iitem['SStationName'];
    //     }
    //     console.log(JSON.stringify(this.stationInfos));
    //   }
    // });
    // 二、MariaDB
    this.mySQLDBClient.getStationList((err, result) => {
      if (!err && result != null && result != []) {
        for (let i = 0; i < result.length; i++) {
          let iitem = result[i];
          let SStation = iitem['SStation'];
          if (!this.stationInfos.has(SStation)) {
            this.stationInfos.set(SStation, new StationInfo());
          } 
          var si = this.stationInfos.get(SStation);
          si.SStation = iitem['SStation'];
          si.DeviceId = iitem['DeviceId'];
          si.SStationName = iitem['SStationName'];
          console.log(`第${i + 1}个站点:站点编码:${si.SStation},设备Id: ${si.DeviceId},站点名称:${si.SStationName}`);
        }
      }
    })
  }

  /**
   * 连接MQTT服务器
   */
  public connectMqtt() {
    var it = new MQTT(this.config.rxmqtt.host, this.config.rxmqtt.port);
    // 订阅主题
    // 1. 报警, 0断电, 1来电
    // /alarmSing      0
    it.subscribe('/alarmSing', 0);
    // 2. GPS信息
    // /lbsLocation    lat=022.6315208&lng=114.0741963
    it.subscribe('/lbsLocation', 0);
    // 3.设备信号
    // /csq            18
    it.subscribe('/csq', 0);
    it.set_message_callback('/alarmSing', this.handleAlarmSing.bind(this));
    it.set_message_callback('/lbsLocation', this.handleGpsLocation.bind(this));
    it.set_message_callback('/csq', this.handleCsq.bind(this));
    it.connect({
      username: this.config.rxmqtt.user,
      password: this.config.rxmqtt.pwd,
      clientId: this.config.rxmqtt.id,
      clean: this.config.rxmqtt.clean,
    });
  
    this.mqttList.push(it);
  }

  /**
   * 断电报警数据处理函数
   */
  handleAlarmSing(topic: string, payload: Buffer) {
    console.log(`断电报警数据: ${topic}=>${payload.toString()}`);
    const topics = topic.split('/');
    // /alarmSing/867814045313299=>1
    console.log('DeviceId', topics[1]);
  
    if (topics[1] == 'alarmSing') {
      let deviceId = topics[2];
      console.log('设备Id: ', deviceId);
      let alarmDevState = parseInt(payload.toString());
      console.log('断电报警DevState: ', alarmDevState == 0 ? '停电' : '来电');
      // 根据DeviceId查询对应的站点编码SStation
      let stcode = '';
      this.stationInfos.forEach((val, key) => {
        if (val.DeviceId == deviceId) {
          stcode = key;
          // 更新该站点的通信时间以及断电报警信息
          let comTime = moment().format('YYYY-MM-DD HH:mm:ss');
          var si = this.stationInfos.get(stcode);
          let strStcode = si.SStation;
          si.alarmSing = alarmDevState;
          si.CommTime = comTime;
          si.Online = true;
          this.stationInfos.set(stcode, si);
          // 将断电报警信息做入库处理
          // this.powerCutAlarmStore({
          //   SStation: strStcode,
          //   DeviceId: deviceId,
          //   SDateTime: comTime,
          //   DevState: alarmDevState
          // });
          this.mySQLDBClient.insertStationRecord({
            SStation: strStcode,
            DeviceId: deviceId,
            SDateTime: comTime,
            DevState: alarmDevState
          })
         }
      });
    }
  }

  /**
  * GPS信息数据处理函数
  */
 handleGpsLocation (topic: string, payload: Buffer) {
    console.log(`GPS信息数据: ${topic}=>${payload.toString()}`);
    const topics = topic.split('/');
    // /lbsLocation/867814045313299=>lat=022.7219409&lng=114.0222168
    if (topics[1] == 'lbsLocation') {
      let deviceId = topics[2];
      console.log('设备Id: ', deviceId);
      let strPayload = payload.toString();
      let strLatitude = strPayload.substring(strPayload.indexOf("lat=")+4, strPayload.indexOf("&"));
      let latitude = parseFloat(strLatitude);
      console.log('latitude: ', latitude);

      let strLongitude = strPayload.substring(strPayload.indexOf("lng=")+4);
      let longitude = parseFloat(strLongitude.toString());
      console.log('longitude: ', longitude);

      // 根据DeviceId查询对应的站点编码SStation
      let stcode = '';
      this.stationInfos.forEach((val, key) => {
        if (val.DeviceId == deviceId) {
          stcode = key;
          // 更新该站点的通信时间以及经纬度信息
          let comTime = moment().format('YYYY-MM-DD HH:mm:ss');
          var si = this.stationInfos.get(stcode);
          si.Online = true;
          si.longitude = longitude;
          si.latitude = latitude;
          si.CommTime = comTime;
          this.stationInfos.set(stcode, si);
         }
      });
    }
  }

  /**
  * 设备信号数据处理函数
  */
 handleCsq(topic: string, payload: Buffer) {
    console.log(`设备信号数据: ${topic}=>${payload.toString()}`);
    const topics = topic.split('/');
    // /csq/867814045454838=>20
    if (topics[1] == 'csq') {
     let deviceId = topics[2];
     console.log('设备Id: ', deviceId);
     let csq = parseInt(payload.toString());
     console.log('设备信号: ', csq);
     // 根据DeviceId查询对应的站点编码SStation
     let stcode = '';
     this.stationInfos.forEach((val, key) => {
       if (val.DeviceId == deviceId) {
         stcode = key;
         // 更新该站点的通信时间以及csq信号值
         let comTime = moment().format('YYYY-MM-DD HH:mm:ss');
         var si = this.stationInfos.get(stcode);
         si.Online = true;
         si.csq = csq;
         si.CommTime = comTime;
         this.stationInfos.set(stcode, si);
        }
     });
   }
  }
  
  /**
   * 站点断电报警数据存储
   */
  public async powerCutAlarmStore(alarmRecord: any) {
    var SStation = alarmRecord.SStation;
    var DeviceId = alarmRecord.DeviceId;
    var SDateTime = alarmRecord.SDateTime;
    var DevState = alarmRecord.DevState;

    let strInsert = "INSERT INTO powercuthistory(SStation, DeviceId, SDateTime, DevState)  VALUES";
    strInsert += `('${SStation}','${DeviceId}','${SDateTime}',${DevState})`;
    // this.mssqlDBClient.exec(strInsert, (err, rc) => {
    //   if (err) {
    //     console.log('插入报警数据出错:', err);
    //   }
    // })
  }
}

app.js

这里为了简便,我直接使用express生成器生成了项目的基本框架,对应的app.js文件如下:

var createError = require('http-errors');
var express = require('express');
var app = express();
var path = require('path');
var logger = require('morgan');

// var indexRouter = require('./routes/index');
// var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

// app.use('/', indexRouter);
// app.use('/users', usersRouter);

module.exports = app;

bin/www

在bin/www文件中创建了service类的实例,然后读取config配置,并启动相关服务。注意:这里需要将app和server传入到service对象中,在service对象中编写http接口,这样就能保证http接口和站点信息缓存共享同一份数据了,如果将http接口写在app.js或者routes/api.js中,创建两个service对象,就不能保证站点信息缓存信息的数据同步了。

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('hnmqalarmstore:server');
var http = require('http');

var config_1 = require('../config');
var Service_1 = require('../service');

var config = new config_1.Config();

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || config.http);
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

// 服务对象
new Service_1.Service(app, server);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

使用到的一些第三方库

yaml、mssql、mariadb、mqtt、express等,对应的项目的package.json文件如下:

"name": "hnmqalarmstore", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "@js-joda/core": "^3.0.0", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "debug": "~2.6.9", "express": "^4.16.4", "express-session": "^1.17.1", "http-errors": "^1.8.0", "jade": "^1.11.0", "mariadb": "^2.4.2", "moment": "^2.27.0", "morgan": "^1.9.1", "mqtt": "^4.2.1", "mssql": "^6.2.1", "yaml": "^1.10.0" }, "devDependencies": { "nodemon": "^2.0.4" } }