UNPKG

motoboat

Version:

a powerful web framework, build on module http and https.

401 lines (361 loc) 14.6 kB
/** * motoboat 2.2.1 * Copyright (c) [2019.08] BraveWang * This software is licensed under the MPL-2.0. * You can use this software according to the terms and conditions of the MPL-2.0. * See the MPL for more details: * https://www.mozilla.org/en-US/MPL/2.0/ */ 'use strict'; const fs = require('fs'); const cluster = require('cluster'); const os = require('os'); const {spawn} = require('child_process'); const bodyParser = require('./bodyparser'); const middleware1 = require('./middleware1'); const router = require('./router'); const helper = require('./helper'); const connfilter = require('./connfilter'); const http1 = require('./http1'); /** * @param {object} options 初始化选项,参考值如下: * - ignoreSlash,忽略末尾的/,默认为true * - debug 调试模式,默认为false * - maxConn 最大连接数,使用daemon接口,则每个进程都可以最多处理maxConn限制数量,0表示不限制。 * - deny {Array} IP字符串数组,表示要拒绝访问的IP。 * - maxIPRequest {number} 单个IP单元时间内最大访问次数。 * - peerTime {number} 单元时间,配合maxIPRequest,默认为1表示1秒钟清空一次。 * - maxIPCache {number} 最大IP缓存个数,配合限制IP访问次数使用,默认为15000。 * - allow {Array} 限制IP请求次数的白名单。 * - timeout {number} 超时。 * - cert {string} 启用HTTPS要使用的证书文件路径。 * - key {string} 启用HTTPS的密钥文件路径。 * - globalLog {bool} 启用全局日志。 * - bodyMaxSize {number} 表示POST/PUT提交表单的最大字节数,包括上传文件。 * - maxFiles {number} 最大上传文件数量,超过则不处理。 * - daemon {bool} 启用守护进程模式。 * - pidFile {string} 保存Master进程PID的文件路径。 * - logFile {string} * - errorLogFile {string} * - logType {string} 日志类型,支持stdio、file、ignore * - server {object} 服务器选项,参考http2.createSecureServer * - pageNotFound {string} 404页面数据 * - cors {string} 允许跨域的域名,*表示所有 * - optionsReturn {bool} 是否自动返回OPTIONS请求,默认为true。 * - parseBody {bool} 自动解析上传文件数据,默认为true。 * - useLimit {bool} 启用连接限制。 * - loadInfoFile {string} daemon为true,负载信息会输出到设置的文件,默认为./loadinfo.log */ var motoboat = function (options = {}) { if (! (this instanceof motoboat) ) {return new motoboat(options);} this.config = { //此配置表示POST/PUT提交表单的最大字节数,也是上传文件的最大限制, bodyMaxSize : 8000000, maxFiles : 15, daemon : false, //开启守护进程 /* 开启守护进程模式后,如果设置路径不为空字符串,则会把pid写入到此文件,可用于服务管理。 */ pidFile : '', logFile : './access.log', errorLogFile : './error.log', /* 日志类型:stdio 标准输入输出,可用于调试 ignore 没有 file 文件,此时会使用log_file以及error_log_file的路径 */ logType : 'ignore', //开启HTTPS https : false, //HTTPS密钥和证书的路径 key : '', cert : '', //设置服务器超时,毫秒单位,在具体的请求中,可以通过stream设置具体请求的超时时间。 timeout : 15000, debug : false, pageNotFound : 'page not found', //展示负载信息,必须使用daemon接口 showLoadInfo: true, //useMinMiddleware: false, ignoreSlash: true, parseBody: true, //useRouter: true, useLimit: false, globalLog: false, //启用全局日志 loadInfoFile: './loadinfo.log', }; this.limit = { maxConn : 1024, //限制最大连接数,如果设置为0表示不限制 deny : [], //拒绝请求的IP。 maxIPRequest : 0, //每秒单个IP可以进行请求次数的上限,0表示不限制。 peerTime : 1, //IP访问次数限制的时间单元,1表示每隔1秒钟检测一次。 maxIPCache : 50000, //存储IP最大个数,是req_ip_table的上限,否则于性能有损。 allow : [], //限制IP请求次数的白名单。 }; if (typeof options !== 'object') { options = {}; } for(var k in options) { switch (k) { case 'maxConn': if (typeof options.maxConn=='number' && parseInt(options.maxConn) >= 0) { this.limit.maxConn = options.maxConn; } break; case 'deny': this.limit.deny = options.deny; break; case 'maxIPRequest': if (parseInt(options.maxIPRequest) >= 0) { this.limit.maxIPRequest = parseInt(options.maxIPRequest); } break; case 'peerTime': if (parseInt(options.peerTime) > 0) { this.limit.peerTime = parseInt(options.peerTime); } break; case 'maxIPCache': if (parseInt(options.maxIPCache) >= 1024) { this.limit.maxIPCache = parseInt(options.maxIPCache); } break; case 'allow': this.limit.allow = options.allow; break; case 'showLoadInfo': case 'logType': case 'daemon': case 'maxFiles': case 'bodyMaxSize': case 'pageNotFound': case 'useMinMiddleware': case 'debug': case 'server': case 'timeout': case 'globalLog': case 'logFile': case 'errorLogFile': case 'ignoreSlash': case 'parseBody': case 'useLimit': case 'loadInfoFile': case 'pidFile': this.config[k] = options[k]; break; default:; } } if (options.key && options.cert) { try { fs.accessSync(options.cert, fs.constants.F_OK); fs.accessSync(options.cert, fs.constants.F_OK); this.config.cert = options.cert; this.config.key = options.key; this.config.https = true; } catch (err) { throw(err); } } /** * 记录当前的运行情况 */ this.rundata = { conn : 0, platform : os.platform(), }; this.helper = helper; this.bodyParser = new bodyParser({maxFiles: this.config.maxFiles}); this.router = new router(options); this.midware = new middleware1(options); //必须要封装起来,使用this.middleware调用,否则会导致this指向错误。 this.add = function (midcall, options = {}) { return this.midware.add(midcall, this.router.group(), options); }; this.use = function (midcall, options = {}) { return this.midware.addCache(midcall, options); } //用于挂载其他服务,比如数据库连接。 this.service = {}; this.httpServ = new http1({ config: this.config, events: this.eventTable, router: this.router, midware: this.midware, service: this.service }); }; /** * 绑定事件的暂存结构和方法 */ motoboat.prototype.eventTable = {}; motoboat.prototype.on = function(evt, callback) { this.eventTable[evt] = callback; }; /** * 根据配置情况确定运行HTTP/1.1还是HTTP/2 * @param {number} port 端口号 * @param {string} host IP地址,可以是IPv4或IPv6 * 0.0.0.0 对应使用IPv6则是:: */ motoboat.prototype.run = function(port = 2020, host = '0.0.0.0') { this.midware.addFromCache(this.router.group()); if (this.config.parseBody) { this.add(this.bodyParser.middleware()); } this.add(this.httpServ.requestMidware); this.midware.addFinal(this.router.group()); //必须放在最后,用于返回最终数据。 if (this.config.useLimit) { var connlimit = new connfilter(this.limit, this.rundata); this.on('connection', connlimit.callback); } return this.httpServ.run(port, host); }; /**保存进程负载情况 */ motoboat.prototype.loadInfo = []; /** * 通过loadInfo保存的数据计算并显示进程和系统的负载情况。 * 这个函数只能在Master进程中调用。 * @param {object} w 子进程发送的数据。 */ motoboat.prototype.showLoadInfo = function (w) { var total = Object.keys(cluster.workers).length; if (this.loadInfo.length >= total) { this.loadInfo.sort((a, b) => { if (a.pid < b.pid) { return -1; } else if (a.pid > b.pid) { return 1; } return 0; }); if (!this.config.daemon) { console.clear(); } var oavg = os.loadavg(); var oscpu = ` CPU Loadavg 1m: ${oavg[0].toFixed(2)} 5m: ${oavg[1].toFixed(2)} 15m: ${oavg[2].toFixed(2)}\n`; var cols = ' PID CPU MEM, HEAP, HEAPUSED CONN\n'; var tmp = ''; var t = ''; for(let i=0; i<this.loadInfo.length; i++) { tmp = (this.loadInfo[i].pid).toString() + ' '; tmp = tmp.substring(0, 10); t = this.loadInfo[i].cpu.user + this.loadInfo[i].cpu.system; t = (t/102400).toFixed(2); tmp += t + '% '; tmp = tmp.substring(0, 20); tmp += (this.loadInfo[i].mem.rss / (1024*1024)).toFixed(1) + ', '; tmp += (this.loadInfo[i].mem.heapTotal / (1024*1024)).toFixed(1) + ','; tmp += (this.loadInfo[i].mem.heapUsed / (1024*1024)).toFixed(1); tmp += 'M '; tmp = tmp.substring(0, 42); tmp += this.loadInfo[i].conn.toString(); cols += ` ${tmp}\n`; } cols += ` Master PID: ${process.pid}\n`; cols += ` Listen ${this.loadInfo[0].host}:${this.loadInfo[0].port}\n`; if (this.config.daemon) { try { fs.writeFileSync(this.config.loadInfoFile,oscpu+cols, {encoding:'utf8'}); } catch (err) { } } else { console.log(oscpu+cols); } this.loadInfo = [w]; } else { this.loadInfo.push(w); } }; /** * Master进程调用的函数,用于监听消息事件。 */ motoboat.prototype.daemonMessage = function () { var the = this; var logger = null; if (this.config.logType == 'file') { var out_log; var err_log; try { fs.accessSync(this.config.logFile, fs.constants.F_OK); out_log = fs.createWriteStream(this.config.logFile, {flags: 'a+'}); } catch (err) { console.log(err); } try { fs.accessSync(this.config.errorLogFile, fs.constants.F_OK); err_log = fs.createWriteStream(this.config.errorLogFile, {flags: 'a+'}); } catch (err){ console.log(err); } logger = new console.Console({stdout:out_log, stderr: err_log}); } else if (this.config.logType == 'stdio') { var opts = {stdout:process.stdout, stderr: process.stderr}; logger = new console.Console(opts); } cluster.on('message', (worker, msg, handle) => { try { switch(msg.type) { case 'log': if (!logger) break; msg.success ? logger.log(JSON.stringify(msg)) : logger.error(JSON.stringify(msg)); break; case 'load': the.showLoadInfo(msg); break; case 'eaddr': console.log('端口已被使用,请先停止正在运行的进程。'); process.exit(1); default:; } } catch (err) { if(the.config.debug) {console.log(err);} } }); }; /** * 这个函数是可以用于运维部署,此函数默认会根据CPU核数创建对应的子进程处理请求。 * @param {number} port 端口号 * @param {string} IP地址,IPv4或IPv6,如果检测为数字,则会把数字赋值给num。 * @param {number} num,要创建的子进程数量,0表示自动,这时候根据CPU核心数量创建。 */ motoboat.prototype.daemon = function(port = 2020, host = '0.0.0.0', num = 0) { if (typeof host === 'number') { num = host; host = '0.0.0.0'; } var the = this; if (process.argv.indexOf('--daemon--') > 0) { } else if (this.config.daemon) { var args = process.argv.slice(1); args.push('--daemon--'); const serv = spawn ( process.argv[0], args, {detached : true, stdio : ['ignore', 1, 2]} ); serv.unref(); return true; } if (cluster.isMaster) { if (num <= 0) { num = os.cpus().length; } if (this.config.daemon && this.config.pidFile.length == 0) { this.config.pidFile = './motoboat.pid'; } if (typeof this.config.pidFile === 'string' && this.config.pidFile.length > 0) { fs.writeFile(this.config.pidFile, process.pid, (err) => { if (err) { console.error(err); } }); } this.daemonMessage(); for(var i=0; i<num; i++) { cluster.fork(); } if (cluster.isMaster) { setInterval(() => { var num_dis = num - Object.keys(cluster.workers).length; for(var i=0; i<num_dis; i++) { cluster.fork(); } }, 2000); } } else if (cluster.isWorker) { this.run(port, host); if (this.config.showLoadInfo) { var cpuLast = {user: 0, system: 0}; var cpuTime = {}; setInterval(() => { cpuTime = process.cpuUsage(cpuLast); process.send({ type : 'load', pid : process.pid, cpu : cpuTime, mem : process.memoryUsage(), conn : the.rundata.conn, host : host, port : port }); cpuLast = process.cpuUsage(); }, 1024); } } }; module.exports = motoboat;