配置定时任务定时任务是由时间触发的执行过程,是一种很常见的业务逻辑。Unix在早期版本中提供了定时任务调度模块Cron,至今仍在各种Linux系统上使用。cron的配置文件crontab格式全面但清晰,可以解决大部分场景下的定时任务配置问题。企业级服务器可以通过类似crontab的形式灵活配置各种定时任务逻辑。crontab的格式如下:#作业定义示例:#.----------------minute(0-59)#|.------------小时(0-23)#||.------------月中的第几天(1-31)#|||.--------月(1-12)或一月、二月、三月、四月...#||||.----星期几(0-6)(星期日=0或7)或星期日、星期一、星期二、星期三、星期四、星期五、星期六#||||#*****用户名要执行的命令本章将基于上一章完成的项目licg9999/nodejs-server-examples-10-log。通过类似crontab的方式,通过node-schedule配置定时任务,检测可能包含网络攻击信息的店铺,并通过nodemailer向管理员发送可疑店铺信息邮件。在项目根目录下执行node-schedule和nodemailer的安装命令:$yarnaddnode-schedulenodemailer#在本地安装node-schedule和nodemailer#...info直接依赖├─node-schedule@1.3.2└─nodemailer@6.4.11#...网络攻击检测现在实现了网络攻击信息的定时检测和告警逻辑。先补充服务层发布://src/services/shop.jsconst{Shop}=require('../models');classShopService{asyncinit(){}-asyncfind({id,pageIndex=0,pageSize=10,日志记录}){+asyncfind({id,pageIndex=0,pageSize=10,where,logging}){if(id){return[awaitShop.findByPk(id,{logging})];}returnawaitShop.findAll({offset:pageIndex*pageSize,limit:pageSize,+where,logging,});}//...}//...//src/services/mail.jsconst{promisify}=require('util');constnodemailer=require('nodemailer');const{mailerOptions}=require('../config');classMailService{邮件程序;asyncinit(){this.mailer=nodemailer.createTransport(mailerOptions);等待promisify(this.mailer.verify)();}asyncsendMail(params){returnawaitthis.mailer.sendMail({from:mailerOptions.auth.user,...params,});}}letservice;module.exports=async()=>{if(!service){service=newMailSer副();等待服务.init();}returnservice;};//src/config/index.jsconstmerge=require('lodash.merge');constlogger=require('../utils/logger');const{logging}=logger;constconfig={//默认配置default:{//...+mailerOptions:{+host:'smtp.126.com',+port:465,+secure:true,+logger:logger.child({type:'mail'}),+auth:{+user:process.env.MAILER_USER,+pass:process.env.MAILER_PASS,+},+},},//...};//...#.env.localGITHUB_CLIENT_ID='b8ada004c6d682426cfb'GITHUB_CLIENT_SECRET='0b13f2ab5651f33f879a535fc2b316c6c731a041'++MAILER_com.nse'+MAILER_com.nse='M2_6CAEJHSTBWNOKHRVL'注意由于应用节点可能不会停止1个,执行巡航检测时会使用分布式锁限制执行节点数,避免重复报警。这里使用数据库实现分布式锁:$#生成定时任务锁的模型文件和schema迁移文件$yarnsequelizemodel:generate--namescheduleLock--attributesname:string,counter:integer$#namesrc/models/schedulelock.js作为src/models/scheduleLock.js$mvsrc/models/schedulelock.jssrc/models/scheduleLock.js$treesrc/models#显示src/models目录内容结构src/models├──config│└──index.js├──index.js├──migrate│├──20200725045100-create-shop.js│├──20200727025727-create-session.js│└──20200801120113-create-schedule-lock.js├──scheduleLock.js├──种子│└──20200725050230-first-shop.js──商店└──.js调整src/models/scheduleLock.js与src/models/migrate/20200801120113-create-schedule-lock.js://src/models/scheduleLock.jsconst{Model}=require('sequelize');module.exports=(sequelize,DataTypes)=>{classscheduleLockextendsModel{/***定义关联的辅助方法。*此方法不是Sequelize生命周期的一部分。*`models/index`文件会自动调用这个方法。*/staticassociate(models){//在这里定义关联}}scheduleLock.init({name:DataTypes.STRING,counter:DataTypes.INTEGER,},{sequelize,modelName:'ScheduleLock',table名称:'schedule_lock',});返回scheduleLock;};//src/models/migrate/20200801120113-create-schedule-lock.jsmodule.exports={up:async(queryInterface,Sequelize)=>{awaitqueryInterface.createTable('schedule_lock',{id:{allowNull:false,autoIncrement:true,primaryKey:true,type:Sequelize.INTEGER,},name:{type:Sequelize.STRING,},counter:{type:Sequelize.INTEGER,},created_at:{allowNull:false,类型:Sequelize.DATE,},updated_at:{allowNull:false,type:Sequelize.DATE,},});},down:async(queryInterface,Sequelize)=>{awaitqueryInterface.dropTable('schedule_lock');},};然后编写检查逻辑:$mkdirsrc/schedules#新建src/schedules存放定时任务$treesrc-L1#显示src目录的内容结构src├──config├──controllers├──middlewares├──models├──molds├──schedules├──server.js├──services└──utils//src/schedules/inspectAttack.jsconst{basename}=require('path');constschedule=require('node-schedule');const{sequelize,ScheduleLock,Sequelize}=require('../models');constmailService=require('../services/mail');constshopService=require('../services/shop');constescapeHtmlInObject=require('../utils/escape-html-in-object');constlogger=require('../utils/logger');const{Op}=Sequelize;//当前任务的锁名constLOCK_NAME=basename(__dirname);//锁的最大占用时间constLOCK_TIMEOUT=15*60*1000;//分布式任务并发数constCONCURRENCY=1;//告警邮件发送对象constMAIL_RECEIVER='licg9999@126.com';classInspectAttack{mailService;店铺服务;asyncinit(){this.mailService=awaitmailService();this.shopService=awaitshopService();//每15分钟检查一次schedule.scheduleJob('*/15****',this.findAttackedShopInfoAndSendMail);}findAttackedShopInfoAndSendMail=async()=>{//lockconstlockUpT=awaitsequelize.transaction();尝试{const[lock]=awaitScheduleLock。findOrCreate({where:{name:LOCK_NAME},defaults:{name:LOCK_NAME,counter:0},transaction:lockUpT,});if(lock.counter>=CONCURRENCY){if(Date.now()-lock.updatedAt.valueOf()>LOCK_TIMEOUT){lock.counter--;awaitlock.save({transaction:lockUpT});}awaitlockUpT.commit();返回;}锁。计数器++;awaitlock.save({transaction:lockUpT});等待lockUpT.commit();}catch(err){logger.error(err);等待lockUpT.rollback();返回;}try{//寻找异常数据constshops=awaitthis.shopService.find({pageSize:100,where:{name:{[Op.or]:[{[Op.like]:'<%'},{[Op.like]:'%>'}]},},});//发送报警邮件if(shops.length){constsubject='安全警告,发现可疑店铺信息!';consthtml=`
${shops.map((shop)=>JSON.stringify(escapeHtmlInObject(shop),null,2)).join('\n')}`;awaitthis.mailService.sendMail({to:MAIL_RECEIVER,subject,html});}}catch{}//解锁constlockDownT=awaitsequelize.transaction();尝试{constlock=awaitScheduleLock.findOne({where:{name:LOCK_NAME},transaction:lockDownT,});if(lock.counter>0){lock.counter--;awaitlock.save({transaction:lockDownT});}等待lockDownT.commit();}catch{awaitlockDownT.rollback();}};}module.exports=async()=>{consts=newInspectAttack();等待s.init();};//src/schedules/index.jsconstinspectAttackSchedule=require('./inspectAttack');module.exports=asyncfunctioninitSchedules(){awaitinspectAttackSchedule();};//src/server.jsconstexpress=require('express');const{resolve}=rerequire('path');const{promisify}=require('util');constinitMiddlewares=require('./middlewares');constinitControllers=require('./controllers');+constinitSchedules=require('./schedules');constlogger=require('./utils/logger');constserver=express();constport=parseInt(process.env.PORT||'9000');constpublicDir=resolve('民众');constmoldsDir=resolve('src/moulds');asyncfunctionbootstrap(){server.use(awaitinitMiddlewares());server.use(express.static(publicDir));server.use('/模具',express.static(mouldsDir));server.use(awaitinitControllers());server.use(errorHandler);+awaitinitSchedules();等待promisify(server.listen.bind(server,port))();logger.info(`>Startedonport${port}`);}//...查看告警添加两条包含网络攻击的店铺信息后,分钟数为15的倍数时即可收到告警邮箱:本章源代码licg9999/nodejs-server-examples-11-schedule更多阅读从零开始构建Node.js企业Web服务器:静态服务从零开始构建Node.js企业Web服务器(一):界面与分层BuildNode.jsEnterpriseWebServerfromScratch(二):验证BuildNode.jsEnterpriseWebServerfromScratchb服务器(三):用中间件从头搭建一个Node.js企业级Web服务器(四):用异常处理从头搭建一个Node.js企业级Web服务器(五):搭建一个Node.js企业级-从头开始构建用于数据库访问的Node.js企业级Web服务器(6):从头开始构建用于会话的Node.js企业级Web服务器(7):从头开始构建用于身份验证和登录的Node.js企业级Web服务器(8):从零开始搭建一个Node.js企业级Web服务器(九):配置项从零开始搭建一个Node.js企业级Web服务器(十):日志从零开始搭建一个Node.js企业级Web服务器(十一):定时任务从头搭建一个Node.js企业级Web服务器(十二):远程调用从头搭建Node.js企业级Web服务器(十三):断点调试与性能分析搭建Node.js企业从零开始构建Node.js企业级Web服务器(十四):自动化测试从零开始构建Node.js企业级Web服务器(十五):总结与展望