occaecatidicta
Version:
288 lines (255 loc) • 9.28 kB
text/typescript
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 * as Coder from '../../util/coder';
import {AcceptorOpts, IAcceptor, AcceptorCallback} from '../acceptor';
import { getLogger, Logger } from 'omelox-logger';
let logger = getLogger('omelox-rpc', 'tcp-acceptor');
export interface AcceptorPkg {
source: string;
remote: string;
id: string & number;
seq: number;
msg: string;
}
const MSG_TYPE = 0;
const PING = 1;
const PONG = 2;
const RES_TYPE = 3;
const DEFAULT_INTERVAL = 50;
export class TCPAcceptor extends EventEmitter implements IAcceptor {
bufferMsg: any;
interval: number; // flush interval in ms
pkgSize: number;
_interval: any; // interval object
server: any;
rpcLogger: any;
rpcDebugLog: any;
sockets: { [key: string]: net.Socket | any } = {};
msgQueues: { [key: string]: any } = {};
cb: (tracer: any, msg?: any, cb?: Function) => void;
inited: boolean = false;
closed: boolean;
ping: number;
timer: {[key: number]: NodeJS.Timer};
socketId: number;
constructor(opts: AcceptorOpts, cb: AcceptorCallback) {
super();
this.bufferMsg = opts.bufferMsg;
this.interval = opts.interval || DEFAULT_INTERVAL; // flush interval in ms
this.pkgSize = opts.pkgSize;
this.rpcLogger = opts.rpcLogger;
this.rpcDebugLog = opts.rpcDebugLog;
this._interval = null; // interval object
// Heartbeat ping interval.
this.ping = 'ping' in opts ? opts.ping : 25e3;
// ping timer for each client connection
this.timer = {};
this.server = null;
this.sockets = {};
this.msgQueues = {};
this.cb = cb;
this.socketId = 0;
}
listen(port: string | number) {
// check status
if (this.inited) {
throw new Error('already inited.');
}
this.inited = true;
this.server = net.createServer();
this.server.listen(port);
this.server.on('error', (err: Error) => {
logger.error('rpc tcp server is error: %j', err.stack);
this.emit('error', err, this);
});
this.server.on('connection', (socket: net.Socket | any) => {
socket.id = this.socketId++;
this.sockets[socket.id] = socket;
socket.composer = new Composer({
maxLength: this.pkgSize
});
this.timer[socket.id] = null;
this.heartbeat(socket.id);
socket.on('data', (data: Buffer) => {
socket.composer.feed(data);
});
socket.composer.on('data', (data: Buffer) => {
this.heartbeat(socket.id);
if(data[0] === PING) {
// incoming::ping
socket.write(socket.composer.compose(PONG));
return;
}
try {
const pkg = JSON.parse(data.toString('utf-8', 1));
// let id = null;
//
if(pkg instanceof Array) {
this.processMsgs(socket, pkg);
} else {
this.processMsg(socket, pkg);
}
} catch(err) { // json parse exception
if(err) {
socket.composer.reset();
logger.error('tcp-acceptor socket.on(data) error', socket.remoteAddress, data.length, data, err);
}
}
});
socket.on('error', (err: Error) => {
logger.error('tcp socket error: %j address:%s', err, socket.remoteAddress);
});
socket.on('close', () => {
logger.error('tcp socket close: %s', socket.id);
delete this.sockets[socket.id];
delete this.msgQueues[socket.id];
if(this.timer[socket.id]) {
clearTimeout(this.timer[socket.id]);
}
delete this.timer[socket.id];
});
});
if (this.bufferMsg) {
this._interval = setInterval(() => {
this.flush();
}, this.interval);
}
}
/**
* 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(socketId: number) {
if(!this.ping) return;
if(this.timer[socketId]) {
this.sockets[socketId].heartbeat = true;
return;
}
/**
* Exterminate the connection as we've timed out.
*/
function ping(self: TCPAcceptor, socketId: number) {
// if pkg come, modify heartbeat flag, return;
if(self.sockets[socketId].heartbeat) {
self.sockets[socketId].heartbeat = false;
return;
}
// if no pkg come
// remove listener on socket,close socket
if(self.timer[socketId]) {
clearInterval(self.timer[socketId]);
self.timer[socketId] = null;
}
let remoteAddress = self.sockets[socketId].remoteAddress;
self.sockets[socketId].composer.removeAllListeners();
self.sockets[socketId].removeAllListeners();
self.sockets[socketId].destroy();
delete self.sockets[socketId];
delete self.msgQueues[socketId];
logger.warn('ping timeout with socket id: %s address:%s', socketId, remoteAddress);
}
this.timer[socketId] = setInterval(ping.bind(null, this, socketId), this.ping + 5e3) as any;
logger.info('wait ping with socket id: %s' , socketId);
}
close() {
if (!!this.closed) {
return;
}
this.closed = true;
if (this._interval) {
clearInterval(this._interval);
this._interval = null;
}
try {
this.server.close();
} catch (err) {
logger.error('rpc server close error: %j', err.stack);
}
this.emit('closed');
}
cloneError(origin: { msg: any, stack: any }) {
// copy the stack infos for Error instance json result is empty
let res = {
msg: origin.msg,
stack: origin.stack
};
return res;
}
respCallback(socket: net.Socket | any, pkg: AcceptorPkg, tracer: Tracer, ...args: any []) {
for(let i = 0, l = args.length; i < l; i++) {
if(args[i] instanceof Error) {
args[i] = this.cloneError(args[i]);
}
}
let resp;
if(tracer && tracer.isEnabled) {
resp = {traceId: tracer.id, seqId: tracer.seq, source: tracer.source, id: pkg.id, resp: args};
}
else {
resp = {id: pkg.id, resp: args};
}
if(this.bufferMsg) {
this.enqueue(socket, resp);
} else {
socket.write(socket.composer.compose(RES_TYPE, JSON.stringify(resp), null));
}
}
processMsg(socket: any, pkg: AcceptorPkg) {
let tracer: Tracer = null;
if (this.rpcDebugLog) {
tracer = new Tracer(this.rpcLogger, this.rpcDebugLog, pkg.remote, pkg.source, pkg.msg, pkg.id, pkg.seq);
tracer.info('server', __filename, 'processMsg', 'tcp-acceptor receive message and try to process message');
}
this.cb(tracer, pkg.msg, pkg.id ? this.respCallback.bind(this, socket, pkg, tracer) : null);
}
processMsgs(socket: any, pkgs: Array<AcceptorPkg>) {
for (let i = 0, l = pkgs.length; i < l; i++) {
this.processMsg(socket, pkgs[i]);
}
}
enqueue(socket: any, msg: {[key: string]: any}) {
let queue = this.msgQueues[socket.id];
if (!queue) {
queue = this.msgQueues[socket.id] = [];
}
queue.push(msg);
}
flush() {
let sockets = this.sockets,
queues = this.msgQueues,
queue, socket;
for (let socketId in queues) {
socket = sockets[socketId];
if (!socket) {
// clear pending messages if the socket not exist any more
delete queues[socketId];
continue;
}
queue = queues[socketId];
if (!queue.length) {
continue;
}
socket.write(socket.composer.compose(RES_TYPE, JSON.stringify(queue)));
queues[socketId] = [];
}
}
}
/**
* create acceptor
*
* @param opts init params
* @param cb(tracer, msg, cb) callback function that would be invoked when new message arrives
*/
export function create(opts: AcceptorOpts, cb: (tracer: Tracer, msg?: any, cb?: Function) => void) {
return new TCPAcceptor(opts || <any>{}, cb);
}
process.on('SIGINT', function () {
process.exit();
});