├── .gitignore ├── m-service ├── .editorconfig ├── example ├── dir1 │ └── file1.js ├── dir2 │ └── file1.js ├── web.js ├── s2.js └── s1.js ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── src ├── m-service.ts ├── index.ts ├── logger.ts ├── util.ts ├── net.ts └── app.ts ├── tsconfig.json ├── README-center.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .idea 4 | lib 5 | -------------------------------------------------------------------------------- /m-service: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./lib/m-service.js'); 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /example/dir1/file1.js: -------------------------------------------------------------------------------- 1 | module.exports.f1 = (console, query, body)=>{ 2 | return {query, body, msg:'dir1 success'}; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /example/dir2/file1.js: -------------------------------------------------------------------------------- 1 | module.exports.f1 = (console, query, body)=>{ 2 | return {query, body, msg:'dir2 success'}; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /example/web.js: -------------------------------------------------------------------------------- 1 | let ms = require('../lib/index'); 2 | 3 | ms.createApp({ 4 | services:{ 5 | port: 5500, 6 | dir: __dirname, 7 | names:['dir1'], 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /example/s2.js: -------------------------------------------------------------------------------- 1 | let ms = require('../lib/index'); 2 | ms.createApp({ 3 | centers:"http://localhost:5000/api/center", //指定服务中心 4 | services:{ //启动服务 5 | port: 5503, 6 | dir: __dirname, 7 | names:['dir2'], 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.autoSave": "onFocusChange", 4 | "typescript.tsdk": "/usr/local/lib/node_modules/.typescript_npminstall/typescript/2.1.4/typescript/lib" 5 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": [], 8 | "showOutput": "silent", 9 | "problemMatcher": "$tsc" 10 | } -------------------------------------------------------------------------------- /src/m-service.ts: -------------------------------------------------------------------------------- 1 | import {globalInit} from './app'; 2 | import {loadJsonConf} from './util'; 3 | import * as ms from './app'; 4 | 5 | let file = process.argv[2]; 6 | if (!file) 7 | file = '/etc/m-service.json'; 8 | globalInit(); 9 | 10 | async function main() { 11 | let conf = await loadJsonConf(file); 12 | console.log('conf is: ', conf); 13 | ms.createApp(conf); 14 | } 15 | 16 | main(); 17 | -------------------------------------------------------------------------------- /example/s1.js: -------------------------------------------------------------------------------- 1 | let ms = require('../lib/index'); 2 | 3 | ms.createApp({ 4 | centers:"http://localhost:5000/api/center", //指定服务中心 5 | center:{ //启动center,用于服务发现 6 | port:5000, 7 | dataFile:'/var/log/m-service.json', 8 | }, 9 | proxy:{ //启动proxy,自动处理服务发现,失败重试 10 | port:4999, 11 | }, 12 | services:{ //启动服务 13 | port: 5500, 14 | dir: __dirname, 15 | names:['dir1'], 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | let ms = require('./app'); 2 | module.exports.createApp = ms.createApp; 3 | 4 | !module.parent && ms.createApp({ 5 | centers:"http://localhost:5000/api/center", 6 | center:{ 7 | port:5000, 8 | dataFile:'/var/log/m-service.json', 9 | }, 10 | proxy:{ 11 | port:4999, 12 | }, 13 | services:{ 14 | port: 5500, 15 | dir: __dirname, 16 | names:['dir1'], 17 | } 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "target": "es6", 6 | "module": "commonjs", 7 | "noImplicitAny": false, 8 | "removeComments": false, 9 | "rootDir":"src", 10 | "outDir":"lib", 11 | "declaration":true, 12 | "noLib":false, 13 | "sourceMap": true 14 | }, 15 | "exclude":[ 16 | "node_modules", 17 | "lib", 18 | ".idea", 19 | "dist", 20 | ".git" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /README-center.md: -------------------------------------------------------------------------------- 1 | micro-service 微服务管理 2 | ==== 3 | 4 | #三个角色 5 | 6 | * 微服务----每个实际的业务都是作为一个微服务运行 7 | * 服务中心----记录所有微服务的路由,每个微服务都会定期向服务中心注册自己的服务(成为心跳) 8 | * 服务代理----用户只需要与代理通信,将请求发给代理,代理会与服务中心即微服务通信,自动处理服务路由等问题 9 | 10 | #自动服务发现 11 | 12 | 每个微服务启动后,会定时向服务中心注册,报告自己的ip、端口、运行的服务。服务中心记录信息,并提供当前所有服务的查询。服务代理会定时与服务中心通信,获取最新的服务列表。当用户请求新服务时,代理能够把请求路由到最新的微服务上。整个过程不需要配置,不需要人工干预。 13 | 14 | #高可用 15 | 16 | 当一个微服务失败时,代理会发现自己的请求失败,他会修改自己的本地服务列表,剔除失败的微服务,然后请求还存活的微服务。服务中心定期清理服务列表数据,由于微服务宕机,因此不再有心跳,因此一小段时间后,服务中心就会将失败的微服务剔除,失败的服务不再对系统产生影响。并且在这个宕机的过程中,用户的请求并没有收到影响。 17 | 18 | #负载均衡 19 | 20 | 服务代理通过自己的本地服务列表找到一个服务名对应的所有微服务,使用round-robin算法将请求转发到微服务上,达到负载均衡。 21 | 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "args": [], 6 | "cwd": "${workspaceRoot}", 7 | "env": { "NODE_ENV": "dev" }, 8 | "console": "integratedTerminal", 9 | "name": "DEBUG", 10 | "outFiles": ["${workspaceRoot}/lib/**/*.js"], 11 | "preLaunchTask": "tsc", 12 | "program": "${workspaceRoot}/lib/s1.js", 13 | "request": "launch", 14 | "runtimeArgs": ["--nolazy"], 15 | "runtimeExecutable": null, 16 | "sourceMaps": true, 17 | "stopOnEntry": false, 18 | "type": "node" 19 | }, 20 | { 21 | "name": "Attach", 22 | "type": "node", 23 | "request": "attach", 24 | "port": 5858 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "m-service", 3 | "version": "1.0.17", 4 | "description": "micro service common", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "m-service": "./m-service" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "concurrent 'tsc -w' 'npm run dev'", 12 | "dev": "sleep 2 && supervisor -w . -- . center proxy" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "babel-polyfill": "^6.13.0", 18 | "bluebird": "^3.3.5", 19 | "body-parser": "^1.14.1", 20 | "connect-timeout": "^1.7.0", 21 | "cookie-parser": "^1.4.3", 22 | "date-format-lite": "^0.9.1", 23 | "express": "^4.13.3", 24 | "express-http-proxy": "^0.10.1", 25 | "md5": "^2.0.0", 26 | "morgan": "^1.6.1", 27 | "multer": "^1.1.0", 28 | "request": "^2.67.0", 29 | "sprintf": "^0.1.5", 30 | "string": "^3.3.1", 31 | "tracer": "^0.8.3", 32 | "underscore": "^1.8.3" 33 | }, 34 | "devDependencies": { 35 | "@types/express": "^4.0.34", 36 | "@types/node": "^6.0.52", 37 | "@types/underscore": "^1.7.36", 38 | "supertest": "^1.1.0" 39 | }, 40 | "files": [ 41 | "m-service", 42 | "lib/*.ts", 43 | "lib/*.js", 44 | "*.json" 45 | ] 46 | } -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | let fs:any = require('fs'); 2 | 3 | export function getLogger(request_id): Console { 4 | let logger = require('tracer').console({ 5 | format : "{{timestamp}} {{file}}:{{line}} {{request_id}} {{message}}", 6 | dateformat : "yyyy.mm.dd HH:MM:ss.l", 7 | preprocess:data=>{ 8 | data.request_id = ''+request_id; 9 | } 10 | }); 11 | logger.request_id = request_id; 12 | return logger; 13 | } 14 | 15 | export let logger:Console = getLogger('system'); 16 | export let nolog:any = {log:f=>f}; 17 | 18 | export function printable(body:any) { 19 | if (!body) return null; 20 | let s = typeof body == 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body); 21 | if (s.length > 5*1024) return ` ... ${s.length} bytes`; 22 | return s; 23 | } 24 | 25 | export async function errorPrintable(err) { 26 | let sources = ['']; 27 | let reg = /((src|dist)\/.*?):(.*?):(\d+)/; 28 | let matches = err.stack.match(reg); 29 | if (matches) { 30 | let file:string = matches[1]; 31 | let line = parseInt(matches[3]); 32 | let column = parseInt(matches[4]); 33 | let lns = (await fs.readFileAsync(file)).toString('utf8').split('\n', 1000000); 34 | for(let l=line-4; l{ 9 | cb(); 10 | setInterval(cb, interval) 11 | }, next); 12 | } 13 | 14 | export async function loadJsonConf(filename) { 15 | let fs = require('fs'); 16 | if (await fs.existsAsync(filename)) { 17 | let cont = await fs.readFileAsync(filename); 18 | try { 19 | if (cont) { 20 | return eval(`(${cont})`); 21 | } 22 | } catch (err) { 23 | console.log('config content is:', cont); 24 | throw new Error(`bad config file ${filename}`); 25 | } 26 | } 27 | return {}; 28 | } 29 | 30 | export function getTimeStamp(){ 31 | return Math.floor(new Date().getTime()/1000); 32 | } 33 | 34 | export function md5(content) { 35 | let md5 = crypto.createHash('md5'); 36 | md5.update(content); 37 | let r = md5.digest('hex'); 38 | console.log(`${content} calculated to ${r}`); 39 | return r; 40 | } 41 | 42 | export async function catchError(p):Promise { 43 | return new Promise(resolve=>{ 44 | p.then(r=>{ 45 | resolve({result:r}); 46 | }, e=>{ 47 | resolve({error:e}) 48 | }) 49 | }); 50 | } 51 | 52 | export function toIdMap(rows, field?) { 53 | let result = {}; 54 | field = field || 'id' 55 | _.each(rows, r => result[r[field]] = r); 56 | return result; 57 | } 58 | 59 | export function escape2Html(str) { 60 | let arrEntities={'lt':'<','gt':'>','nbsp':' ','amp':'&','quot':'"'}; 61 | str = str.replace(/&(lt|gt|nbsp|amp|quot);/ig, 62 | (all,t)=>{ 63 | return arrEntities[t]; 64 | }); 65 | return str; 66 | } 67 | 68 | export function parseQuery(url) { 69 | let query = url.split('?')[1] || ''; 70 | let vars = query.split('&'); 71 | let r = {}; 72 | for (let i = 0; i < vars.length; i++) { 73 | let pair = vars[i].split('='); 74 | if(!pair[0] || !pair[1]) continue; 75 | r[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 76 | } 77 | return r; 78 | } 79 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | micro-service a mirco service framework/一个简单的node微服务框架 2 | ==== 3 | 4 | #特点 5 | 6 | * 基于http协议----方便跨语言使用 7 | * [自动服务发现](README-center.md)----自动管理并发现新加入的服务 8 | * [微服务高可用](README-center.md)----自动剔除故障的微服务,并对失败的请求重试 9 | * [微服务负载均衡](README-center.md)----自动在多个微服务实例上做负载均衡 10 | * ELK日志集成(即将上线) 11 | 12 | #安装 13 | 14 | npm install m-service --save 15 | 16 | #使用 17 | 18 | ##编写服务处理函数 19 | 20 | ```javascript 21 | // dir1/file1.js 22 | // 使用传入的console参数输出可以自动在日志里带上request id,便于跟踪一个请求在所有微服务上的日志 23 | // 返回值如果是非null,则会把该值JSON.stringify后作为结果返回,若是promise,则等待promise的结果再返回 24 | module.exports.f1 = (console, query, body, req, res)=>{ 25 | return {query, body, msg:'success'}; 26 | } 27 | ``` 28 | 29 | ##普通web服务模式 30 | 31 | 按照普通的web方式的方式提供服务 32 | 33 | ```javascript 34 | // web.js 35 | let ms = require('m-service'); 36 | 37 | ms.createApp({ 38 | services:{ 39 | port: 5500, 40 | dir: __dirname, 41 | names:['dir1'], 42 | } 43 | }); 44 | 45 | //localhost:4000/api/dir1/file1/f1?p1=1&p2=2 46 | ``` 47 | 48 | ##微服务模式: 49 | 50 | 分三个角色 51 | 52 | * 服务中心----服务注册,服务发现 53 | * 服务代理----提供集成的web接口,用户使用统一的url访问所有微服务,屏蔽微服务内部的细节 54 | * 微服务----提供实际的处理服务,并将服务注册到服务中心 55 | 56 | ###启动三个服务角色 57 | ```javascript 58 | // s1.js 59 | let ms = require('m-service'); 60 | 61 | ms.createApp({ 62 | centers:"http://localhost:5000/api/center", //指定服务中心,多个请用;分隔 63 | center:{ //启动center,用于服务发现 64 | port:5000, 65 | dataFile:'/var/log/m-service.json', 66 | }, 67 | proxy:{ //启动proxy,自动处理服务发现,失败重试 68 | port:4999, 69 | }, 70 | services:{ //启动服务 71 | port: 5500, 72 | dir: __dirname, 73 | names:['dir1'], 74 | } 75 | }); 76 | 77 | //localhost:5500/api/dir1/file1/f1?p1=1&p2=2&直接访问微服务 78 | //localhost:4999/api/dir1/file1/f1?p1=1&p2=2&通过代理访问微服务 79 | //localhost:5000/api/center/register&查看在线服务 80 | ``` 81 | ###只启动微服务 82 | ```javascript 83 | // dir2/file2.js 84 | module.exports.f = (console, query, body)=>{ 85 | return {query, body, msg:'success'}; 86 | } 87 | 88 | // s2.js 89 | let ms = require('m-service'); 90 | ms.createApp({ 91 | centers:"http://localhost:5000/api/center", //指定服务中心 92 | services:{ //启动服务 93 | port: 5501, 94 | dir: __dirname, 95 | names:['dir2'], 96 | } 97 | }); 98 | 99 | ``` 100 | 101 | 现在可以访问代理直接访问所有微服务 102 | 103 | localhost:4999/api/dir1/file1/f1?p1=1&p2=2&&通过代理访问微服务 104 | localhost:4999/api/dir2/file1/f1?p1=1&p2=2&&通过代理访问微服务 105 | 106 | ##开发 107 | ``` 108 | git clone https://github.com/yedf/micro-service.git 109 | cd micro-service 110 | cnpm install 111 | sudo cnpm install -g typescript 112 | npm start #启动微服务的注册中心、代理、服务名称为dir1的微服务 113 | ``` 114 | 115 | 启动另一个微服务dir2 116 | 117 | cd example && node s2.js 118 | 119 | ##只启动服务中心或代理 120 | ``` 121 | // /etc/m-service.json 122 | { 123 | centers:"http://localhost:5000/api/center", //指定服务中心 124 | center:{ //启动center,用于服务发现 125 | port:5000, 126 | dataFile:'/var/log/m-service.json', //保存当前已注册服务,重启不失效 127 | }, 128 | proxy:{ //启动proxy,自动处理服务发现,失败重试 129 | port:4999, 130 | } 131 | } 132 | ``` 133 | cnpm i -g m-service 134 | 135 | m-service 136 | -------------------------------------------------------------------------------- /src/net.ts: -------------------------------------------------------------------------------- 1 | let request = require('request'); 2 | // request.debug = true; 3 | import {printable} from './logger'; 4 | import * as _ from 'underscore'; 5 | 6 | export async function get(console:Console, url, options?):Promise { 7 | return new Promise(function(resolve, reject) { 8 | console.log(`getting ${url}`, options||''); 9 | let opts = {url, headers:{'x-request-id':console['request_id']}}; 10 | request(_.extend(opts, options), function(err, response, body) { 11 | console.log(`get ${url} body: ${body.length>10*1024?'length:'+body.length : body}`); 12 | err && reject(err) || resolve(body); 13 | }); 14 | }); 15 | } 16 | 17 | export async function getJson(console:Console, url, options?) { 18 | return get(console, url, options).then(function(res:any) { 19 | let body = JSON.parse(res); 20 | if (body.errcode || body.code) return Promise.reject(body); 21 | return Promise.resolve(body); 22 | }); 23 | } 24 | 25 | export async function getBinary(console:Console, url, options?): Promise { 26 | return new Promise(function(resolve, reject) { 27 | let data = []; 28 | console.log(`getting raw ${url}`); 29 | let opts = {url, headers:{'x-request-id':console['request_id']}}; 30 | request(_.extend(opts, options)).on('response', (response)=>{ 31 | response.setEncoding('binary'); 32 | response.on('data', chunk=>{ 33 | data.push(chunk); 34 | }).on('end',()=> { 35 | let all = data.join(''); 36 | console.log(`get raw ${url} body length: ${all.length}`); 37 | resolve(all); 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | export async function put(console:Console, url, data, options?): Promise { 44 | return new Promise(function (resolve, reject) { 45 | console.log(`putting raw ${url} data length: ${data.length}`); 46 | request(_.extend({ 47 | url: url, 48 | method: 'PUT', 49 | body: data 50 | }, options), (err, response, body) => { 51 | console.log(`put ${url} body ${printable(data)} err ${err} return body ${printable(body)}`); 52 | err && reject(err) || resolve(body); 53 | }); 54 | }) 55 | } 56 | 57 | export async function putJson(console:Console, url, data, options?):Promise { 58 | return put(console, url, JSON.stringify(data), options).then(function(res:any) { 59 | return Promise.resolve(JSON.parse(res)); 60 | }); 61 | } 62 | 63 | export async function post(console:Console, url, data, options?):Promise { 64 | return new Promise(function(resolve, reject) { 65 | console.log(`posting raw ${url} data length: ${printable(data)}`); 66 | request(_.extend({ 67 | url: url, 68 | method: 'POST', 69 | headers:{'x-request-id':console['request_id']}, 70 | body: data 71 | }, options), (err, response, body) => { 72 | console.log(`post ${url} body ${printable(data)} err ${err} return body ${printable(body)}`); 73 | err && reject(err) || resolve(body); 74 | }); 75 | }); 76 | } 77 | 78 | export async function postJson(console:Console, url,data,options?):Promise { 79 | return post(console, url,data, _.extend({json:true},options)).then(function(res:any) { 80 | return Promise.resolve(res); 81 | }) 82 | } 83 | 84 | export async function postStream(console:Console, url, data, options?):Promise { 85 | return post(console, url,data, _.extend({headers: {'Content-Type': 'application/octet-stream'}},options)).then(function(res:any) { 86 | return Promise.resolve(res); 87 | }) 88 | } 89 | 90 | export async function postForm(console:Console, url, formData, options?):Promise { 91 | return new Promise((resolve,reject) => { 92 | console.log(`posting ${url} form: ${printable(formData)}`); 93 | request.post(_.extend({url:url,formData:formData,headers:{'x-request-id':console['request_id']}}, options), (err,response,body)=> { 94 | console.log(`post ${url} body ${printable(formData)} err ${err} body ${printable(body)}`); 95 | err && reject(err) || resolve(body); 96 | }) 97 | }); 98 | } 99 | 100 | export async function postFormUrlEncoded(console:Console, url, form, options?):Promise { 101 | return new Promise((resolve,reject) => { 102 | console.log(`posting ${url} form: ${printable(form)}`); 103 | request.post(_.extend({url:url,form:form,headers:{'x-request-id':console['request_id']}}, options), (err,response,body)=> { 104 | console.log(`post ${url} body ${printable(form)} err ${err} body ${printable(body)}`); 105 | err && reject(err) || resolve(body); 106 | }) 107 | }); 108 | } 109 | 110 | export async function postFormJson(console:Console, url, formData, options?) { 111 | let r:any = await postForm(console, url, formData, options); 112 | return JSON.parse(r); 113 | } 114 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface String { 3 | padStart(n?:number, char?:string):string; 4 | } 5 | interface Date { 6 | format(fmt?:string):string; 7 | } 8 | } 9 | 10 | import {logger as console, printable, errorPrintable, getLogger} from './logger'; 11 | import * as process from 'process'; 12 | import {Request,Response} from 'express'; 13 | import * as express from 'express'; //this load used 100ms 14 | let parser:any = require('body-parser'); 15 | import * as _ from 'underscore'; 16 | export let fs = require('fs'); 17 | import * as util from './util'; 18 | 19 | let globalInited = false; 20 | export function globalInit() { 21 | if (globalInited) return; 22 | globalInited = true; 23 | let begin = new Date().getTime(); 24 | require("date-format-lite"); 25 | Date['masks']['default'] = 'YYYY-MM-DD hh:mm:ss'; 26 | require('babel-polyfill'); 27 | let bluebird = require('bluebird'); 28 | if (!fs.statAsync) { 29 | bluebird.promisifyAll(fs); 30 | fs.existsAsync = path => { 31 | return new Promise(resolve=>{ 32 | fs.exists(path, r=>resolve(r)); 33 | }); 34 | }; 35 | } 36 | process.on('unhandledRejection', (reason, p) => { 37 | console.log('---------------------------unhandledRejection:', reason); 38 | }); 39 | console.log(`global inited. used: ${new Date().getTime()-begin} ms`); 40 | } 41 | 42 | export function createApp(conf):any { 43 | let app = express(); 44 | app.use(require('morgan')('dev')) 45 | .use(require('cookie-parser')()) 46 | .use(require('connect-timeout')('1s')) 47 | .use(parser.json({limit: '50mb'})) 48 | .use(parser.urlencoded({extended:true,limit: '50mb'})) 49 | .use(parser.text({type:'text/*',limit: '50mb'})) 50 | .use(parser.raw({limit: '50mb'})) 51 | ; 52 | console.log('app inited'); 53 | if (conf.port) { 54 | app.listen(conf.port, ()=>{ 55 | console.log(`listening to port: ${conf.port}`) 56 | }); 57 | } 58 | globalInit(); 59 | if (conf.center) { 60 | createGlobalCenter(app, conf); 61 | } 62 | if (conf.services) { 63 | createServices(app, conf); 64 | } 65 | if (conf.proxy) { 66 | createGlobalProxy(app, conf); 67 | } 68 | return app; 69 | } 70 | 71 | export function createServices(app, conf) { 72 | let {port, names, dir} = conf.services; 73 | let net = require('./net'); 74 | app.use(names.map(s=>`/api/${s}/`), (req, res)=>handleService(dir, req, res)); 75 | app.listen(port, ()=>{ 76 | console.log(`services listening on port: ${port}`); 77 | }); 78 | if (!conf.centers) 79 | return; 80 | let reg = ()=>conf.centers.split(';').map(h=>{ 81 | net.postJson(console, h, {services:names, port:port}); 82 | }); 83 | util.setInterval2(reg, 30000, 0); 84 | } 85 | 86 | export async function outputResult (console, res:Response, cont:any, status?:number) { 87 | status = status || 200; 88 | let req:Request = res['req']; 89 | let request_id = req['request-id']; 90 | if (cont.stack) cont = await errorPrintable(cont); 91 | if (status == 200) { 92 | res.header("Content-Type", "application/json; charset=utf-8"); 93 | cont = JSON.stringify(cont) + '\n'; 94 | } 95 | res.header('x-request-id', request_id); 96 | let einfo1 = `${req.method} ${req.originalUrl} ${JSON.stringify(req.query)} body: ` + printable(req.body); 97 | let einfo2 = ` status: ${status} result: ${printable(cont)}`; 98 | if(status!=200) { 99 | let hint = ` hint:[${request_id}]\n`; 100 | cont = typeof cont=='string'?hint+cont: (typeof cont == 'object' ? _.extend(cont, {hint}) : cont); 101 | einfo1 += hint; 102 | await fs.writeFile('/tmp/e.log', `${new Date().format()} ${einfo1} \n${new Date().format()} ${einfo2}\n\n`, {encoding: 'utf8',flag: 'a'}); 103 | } 104 | console.log(einfo1); 105 | console.log(einfo2); 106 | res.status(status).send(cont); 107 | } 108 | 109 | async function existModule(path) { 110 | if (await fs.existsAsync(path+'.js')) return true; 111 | if (await fs.existsAsync(path+'.ts')) return true; 112 | return false; 113 | } 114 | 115 | async function callFunc(console:Console, dir, req, res) { 116 | let ps = req.originalUrl.slice('/api/'.length).split('/'); 117 | let service = ps[0]; 118 | let file = ps.slice(0, ps.length-1).join('/'); 119 | let func = ps[ps.length-1].split('?')[0]; 120 | let index = `${dir}/${service}/index`; 121 | let js = `${dir}/${file}`; 122 | if (await existModule(index)) { 123 | let m4 = require(index); 124 | if (!m4.entry) { 125 | return Promise.reject([`can't find func entry in ${index}`, 404]); 126 | } 127 | return await m4.entry(console, req, res); 128 | } else if (await existModule(js)) { 129 | let md = require(js); 130 | if (!md[func]) { 131 | return Promise.reject([`can't find func: ${func} in file: ${js}`, 404]); 132 | } 133 | let r = md[func](console, req.query, req.body, req, res); 134 | if (r && r.then) { 135 | r = await r; 136 | } 137 | return r; 138 | } 139 | return Promise.reject([`can't find file ${js} or ${index}`, 404]); 140 | } 141 | 142 | export async function handleService(dir, req:Request, res:Response): Promise { 143 | return handleRequest(req, res, (console, req, res)=>callFunc(console, dir, req, res)); 144 | } 145 | 146 | export async function handleRequest(req:Request, res:Response, handler): Promise { 147 | let request_id = req.header('x-request-id') || '-'+randString(); 148 | req['request_id'] = request_id; 149 | let console = getLogger(request_id); 150 | let promise = handler(console, req, res); 151 | promise.then( 152 | r => r && outputResult(console, res, r, 200), 153 | err=> outputResult(console, res, err, 500) 154 | ); 155 | } 156 | 157 | export function randString(length=8) { 158 | return Math.random().toString(36).slice(2, length+2); 159 | } 160 | 161 | //following is for center and proxy 162 | 163 | export function createGlobalCenter(app, conf) { 164 | app.use('/api/center', (req, res) => handleRequest(req, res, register)); 165 | app.listen(conf.center.port, ()=>{ 166 | console.log(`center listening on port: ${conf.center.port}`); 167 | }); 168 | centerServices.setSaveFile(conf.center.dataFile); 169 | } 170 | 171 | export function createGlobalProxy(app, conf) { 172 | app.use('*', (req, res) => handleRequest(req, res, proxy)); 173 | app.listen(conf.proxy.port, ()=>{ 174 | console.log(`proxy listening on port: ${conf.proxy.port}`); 175 | }); 176 | if (!conf.center && conf.centers) { //如果此进程没有center,则需要进行心跳获取最新的service,否则数据直接共享 177 | async function heartBeat() { 178 | let host = conf.centers.split(';')[0]; 179 | let net = require('./net'); 180 | centerServices.services = await net.getJson(console, host); 181 | } 182 | 183 | util.setInterval2(heartBeat, 30000, 1000); 184 | } 185 | } 186 | 187 | class CenterServices { 188 | async setSaveFile(filename) { 189 | this.filename = filename; 190 | if (filename) { 191 | let r = await util.loadJsonConf(filename); 192 | this.services = r; 193 | console.log(`services loaded: ${JSON.stringify(this.services)}`); 194 | } 195 | } 196 | filename:string; 197 | services:any = {}; 198 | clearExpires = ()=>{ 199 | let now = new Date().getTime(); 200 | for (let service in this.services) { 201 | let hosts = this.services[service]; 202 | for (let host in hosts) { 203 | let info = hosts[host]; 204 | if (info.expire < now) { 205 | console.log(`clearing now: ${now}, info: `, JSON.stringify(info)); 206 | delete hosts[host]; 207 | } 208 | } 209 | if (_.isEmpty(hosts)) { 210 | delete this.services[service]; 211 | } 212 | } 213 | if (this.filename) 214 | fs.writeFileAsync(this.filename, JSON.stringify(this.services)); 215 | }; 216 | addHost = (service) => { 217 | let name = service.name; 218 | if (!this.services[name]) { 219 | this.services[name] = {}; 220 | } 221 | let hosts = this.services[name]; 222 | let expire = service.expire || new Date().getTime() + 50 * 60 * 1000; 223 | let key = `http://${service.ip}:${service.port}`; 224 | hosts[key] = _.extend({}, service, {key, expire, expireTime:new Date(expire).format()}); 225 | if (this.filename) 226 | fs.writeFileAsync(this.filename, JSON.stringify(this.services)); 227 | } 228 | deleteHost = (service, host)=>{ 229 | if (this.services[service]) { 230 | delete this.services[service][host]; 231 | } 232 | } 233 | } 234 | 235 | let centerServices = new CenterServices(); 236 | 237 | // 注册一个服务,返回所有已注册的服务,如果没有服务名,则不注册服务,只返回所有服务 238 | // 如果指定一个过期的expire,则删除服务 239 | export async function register(console:Console, req:Request) { 240 | let {service, port, expire, services, ip, priority} = _.extend({}, req.query, req.body); 241 | ip = ip || req.connection.remoteAddress; 242 | if (ip[0] == ':') 243 | ip = `[${ip}]`; 244 | services = services || []; 245 | if (service) { 246 | services.push(service); 247 | } 248 | services.map(m=>centerServices.addHost({name:m, ip, port, expire, priority})); 249 | centerServices.clearExpires(); 250 | return centerServices.services; 251 | } 252 | 253 | class CurrentServices { 254 | constructor(){ 255 | setInterval(()=>{this.services={}}, 30*1000); 256 | } 257 | services:any = {}; 258 | allocateService(service, failedAddr?) { // allocate an address diff from failedAddr; 259 | if (failedAddr) { 260 | if (this.services[service] == failedAddr) { 261 | delete this.services[service]; 262 | } 263 | centerServices.deleteHost(service, failedAddr); 264 | } 265 | if (this.services[service]) //上次的地址OK,返回 266 | return this.services[service]; 267 | let candidates = _.values(centerServices.services[service]||{}); 268 | if (!candidates.length) 269 | return null; 270 | candidates = candidates.sort((a,b)=>(b.priority||-1000)-(a.priority||-1000)); //优先级排序 271 | if (candidates[0].priority) 272 | candidates = candidates.filter(a=>a.priority=candidates[0].priority); //只取优先级最高的候选 273 | return candidates[Math.floor(Math.random()*candidates.length)].key; //选择一个随机的 274 | } 275 | } 276 | 277 | let currentServices = new CurrentServices(); 278 | // 注册一个服务,返回所有已注册的服务,如果没有服务名,则不注册服务,只返回所有服务 279 | // 如果指定一个过期的expire,则删除服务 280 | export async function proxy(console:Console, req:Request, res:Response) { 281 | let service = req.originalUrl.slice('/api/'.length).split('/')[0]; 282 | req.url = req.originalUrl; 283 | innerProxy(console, service, req, res, 3); 284 | } 285 | 286 | function innerProxy(console, service, req, res, retry, failedAddr?) { 287 | if (retry <= 0) { 288 | return res.status(502).send(`service ${service} proxy error retry: ${retry}`); 289 | } 290 | let dest = currentServices.allocateService(service, failedAddr); 291 | if (!dest) { 292 | return res.status(502).send(`service ${service} found no dest`); 293 | } 294 | console.log(`proxying request to ${dest}`); 295 | let eproxy = require('express-http-proxy'); 296 | eproxy(dest)(req, res, (err)=>{ 297 | console.log(`proxied request to ${dest} failed`, err); 298 | innerProxy(console, service, req, res, retry-1, dest); 299 | }); 300 | 301 | } 302 | --------------------------------------------------------------------------------