occaecatidicta
Version:
326 lines (290 loc) • 9.7 kB
text/typescript
import {getLogger} from 'omelox-logger';
let logger = getLogger('omelox-rpc', 'mqtt2-mailbox');
import {EventEmitter} from 'events';
import {Tracer} from '../../util/tracer';
import * as utils from '../../util/utils';
import {Composer} from '../../util/composer';
import * as util from 'util';
import * as net from 'net';
import {Msg} from '../../util/coder';
import {IMailBox, MailBoxOpts, MailBoxPkg} from '../mailbox';
const DEFAULT_CALLBACK_TIMEOUT = 10 * 1000;
const DEFAULT_INTERVAL = 50;
const MSG_TYPE = 0;
const PING = 1;
const PONG = 2;
export class TCPMailBox extends EventEmitter implements IMailBox {
curId: number = 0;
id: string;
host: string;
port: number;
requests: { [key: string]: any } = {};
timeout: { [key: string]: any } = {};
queue: Array<any> = [];
bufferMsg: any;
interval: number;
timeoutValue: number;
connected: boolean = false;
closed: boolean = false;
serverId: string;
opts: any;
socket: any = null;
_interval: any;
composer: Composer;
ping: number;
pong: number;
timer: {ping?: NodeJS.Timer, pong?: NodeJS.Timer};
constructor(serverInfo: {id: string, host: string, port: number}, opts: MailBoxOpts) {
super();
this.opts = opts || <any>{};
this.id = serverInfo.id;
this.host = serverInfo.host;
this.port = serverInfo.port;
this.composer = new Composer({
maxLength: opts.pkgSize
});
this.bufferMsg = opts.bufferMsg;
this.interval = opts.interval || DEFAULT_INTERVAL;
this.timeoutValue = opts.timeout || DEFAULT_CALLBACK_TIMEOUT;
// Heartbeat ping interval.
this.ping = 'ping' in opts ? opts.ping : 25e3;
// Heartbeat pong response timeout.
this.pong = 'pong' in opts ? opts.pong : 10e3;
this.timer = {};
this.connected = false;
}
connect(tracer: Tracer, cb: (err?: Error) => void) {
tracer && tracer.info('client', __filename, 'connect', 'tcp-mailbox try to connect');
if (this.connected) {
utils.invokeCallback(cb, new Error('mailbox has already connected.'));
return;
}
this.socket = net.connect(<any>{
port: this.port,
host: this.host
}, (err: Error) => {
// success to connect
this.connected = true;
this.closed = false;
if (this.bufferMsg) {
// start flush interval
this._interval = setInterval(() => {
this.flush();
}, this.interval);
}
this.heartbeat();
utils.invokeCallback(cb, err);
});
this.composer.on('data', (data: Buffer) => {
if (data[0] === PONG) {
// incoming::pong
this.heartbeat();
return;
}
try {
const pkg = JSON.parse(data.toString('utf-8', 1));
if(pkg instanceof Array) {
this.processMsgs(pkg);
} else {
this.processMsg(pkg);
}
} catch(err) {
if(err) {
logger.error('[omelox-rpc] tcp mailbox process data error: %j', err.stack);
}
}
});
this.socket.on('data', (data: any) => {
this.composer.feed(data);
});
this.socket.on('error', (err: Error) => {
if (!this.connected) {
utils.invokeCallback(cb, err);
return;
}
// this.emit('error', err, this);
this.emit('close', this.id);
});
this.socket.on('end', () => {
this.emit('close', this.id);
});
// TODO: reconnect and heartbeat
}
/**
* close mailbox
*/
close() {
if (this.closed) {
return;
}
this.closed = true;
this.connected = false;
if (this._interval) {
clearInterval(this._interval);
this._interval = null;
}
if(Object.keys(this.timer).length) {
clearTimeout(this.timer['ping']);
this.timer['ping'] = null;
clearTimeout(this.timer['pong']);
this.timer['pong'] = null;
}
if (this.socket) {
this.socket.removeAllListeners();
this.composer.removeAllListeners();
this.socket.destroy();
this.socket = null;
}
}
/**
* send message to remote server
*
* @param msg {service:"", method:"", args:[]}
* @param opts {} attach info to send method
* @param cb declaration decided by remote interface
*/
send(tracer: Tracer, msg: Msg, opts: MailBoxOpts, cb: (tracer: Tracer, msg?: any, cb?: Function) => void | null) {
tracer && tracer.info('client', __filename, 'send', 'tcp-mailbox try to send');
if (!this.connected) {
utils.invokeCallback(cb, new Error('not init.'));
return;
}
if (this.closed) {
utils.invokeCallback(cb, new Error('mailbox alread closed.'));
return;
}
let id = 0;
if(cb) {
id = this.curId++ & 0xffffffff;
if(!id) {
id = this.curId++ & 0xffffffff;
}
this.requests[id] = cb;
this.setCbTimeout(id, tracer, cb);
}
let pkg: any;
if (tracer && tracer.isEnabled) {
pkg = {
traceId: tracer.id,
seqId: tracer.seq,
source: tracer.source,
remote: tracer.remote,
id: id,
msg: msg
};
} else {
pkg = {
id: id,
msg: msg
};
}
if (this.bufferMsg) {
this.enqueue(pkg);
} else {
this.socket.write(this.composer.compose(MSG_TYPE, JSON.stringify(pkg), id));
}
}
/*
* Send a new heartbeat over the connection to ensure that we're still
* connected and our internet connection didn't drop. We cannot use server side
* heartbeats for this unfortunately.
* @api private
*/
heartbeat() {
if(!this.ping) return;
if(this.timer['pong']) {
clearTimeout(this.timer['pong']);
this.timer['pong'] = null;
}
if(!this.timer['ping']) {
this.timer['ping'] = setTimeout(ping, this.ping);
}
const self = this;
/**
* Exterminate the connection as we've timed out.
*
* @api private
*/
function pong() {
if(self.timer['pong']) {
clearTimeout(self.timer['pong']);
self.timer['pong'] = null;
}
self.emit('close', self.id);
logger.warn('pong timeout');
}
/**
* We should send a ping message to the server.
*
* @api private
*/
function ping() {
if(self.timer['ping']) {
clearTimeout(self.timer['ping']);
self.timer['ping'] = null;
}
self.socket.write(self.composer.compose(PING));
self.timer['pong'] = setTimeout(pong, self.pong);
}
}
enqueue(msg: Msg) {
this.queue.push(msg);
}
flush() {
if (this.closed || !this.queue.length) {
return;
}
this.socket.write(this.composer.compose(MSG_TYPE, JSON.stringify(this.queue), this.queue[0].id));
this.queue = [];
}
processMsgs(pkgs: Array<MailBoxPkg>) {
for (let i = 0, l = pkgs.length; i < l; i++) {
this.processMsg(pkgs[i]);
}
}
processMsg(pkg: MailBoxPkg) {
this.clearCbTimeout(<any>pkg.id);
let cb = this.requests[pkg.id];
if (!cb) {
return;
}
delete this.requests[pkg.id];
let tracer = null;
if (this.opts.rpcDebugLog) {
tracer = new Tracer(this.opts.rpcLogger, this.opts.rpcDebugLog, this.opts.clientId, pkg.source, pkg.resp, pkg.id, pkg.seq);
}
cb(tracer, null, pkg.resp);
}
setCbTimeout(id: number, tracer: Tracer, cb: (tracer: Tracer, msg?: any, cb?: Function) => void) {
const timer = setTimeout(() => {
this.clearCbTimeout(id);
if (!!this.requests[id]) {
delete this.requests[id];
}
logger.error('rpc callback timeout, remote server host: %s, port: %s', this.host, this.port);
if(cb) {
cb(tracer, new Error('rpc callback timeout'));
}
}, this.timeoutValue);
this.timeout[id] = timer;
}
clearCbTimeout(id: number) {
if (!this.timeout[id]) {
logger.warn('timer not exists, id: %s', id);
return;
}
clearTimeout(this.timeout[id]);
delete this.timeout[id];
}
}
/**
* Factory method to create mailbox
*
* @param {Object} server remote server info {id:"", host:"", port:""}
* @param {Object} opts construct parameters
* opts.bufferMsg {Boolean} msg should be buffered or send immediately.
* opts.interval {Boolean} msg queue flush interval if bufferMsg is true. default is 50 ms
*/
export const create = function (server: { id: string, host: string, port: number }, opts: MailBoxOpts): IMailBox {
return new TCPMailBox(server, opts || <any>{});
};