scalra
Version:
node.js framework to prototype and scale rapidly
259 lines (199 loc) • 7.69 kB
JavaScript
//
// socket.js
//
// a socket module that handles setting up a new socket connection
//
// history:
// 2013-09-13 extracted from frontier.js
/*
public methods:
SocketHandler(connHandler, eventHandler)
// setup a new socket whether (in/out)
setup (socket)
// handle incoming messages
process (socket, data)
*/
//
// local reference
//
var l_name = 'SR.Socket';
// setup a usable socket
// TODO: current implemention is too complicated --> simplify it
// TODO: is passing eventHandler necessary? (right now it's used by connector / listener separately)
var SocketHandler = function (connHandler, eventHandler) {
// connection handler (handles 'addConnection' and 'removeConnection' actions
if (!connHandler || typeof connHandler.addConnection !== 'function' || typeof connHandler.removeConnection !== 'function') {
LOG.error('connHandler not specified or missing addConnection/removeConnection functions', l_name);
} else {
this.connHandler = connHandler;
}
// event handler (dispatch incoming message to the correct event handler
if (!eventHandler || typeof eventHandler.dispatcher !== 'function') {
LOG.error('eventHandler not specified or missing dispatcher', l_name);
} else {
this.eventHandler = eventHandler;
}
// to allow methods be accessed within functions
var that = this;
//-----------------------------------------
// parameters:
// socket: 'object' a socket retunred by node.js
// data: 'string' data received for this transmission
// process incoming data
this.process = function (socket, data) {
// if no data to be delivered or socket
if (socket === undefined || data === undefined || typeof data.length !== 'number')
return;
var parsed = undefined;
var buf = socket.recv_buf + data;
// try to parse first, if failed then buffer it
try {
parsed = JSON.parse(buf);
} catch (e) {
LOG.sys(socket.host + ':' + socket.port + ' JSON.parse() error, save it to buffer...' + e, l_name);
socket.recv_buf += data;
parsed = undefined;
}
// we have something valid to process
if (parsed) {
var conn = SR.Conn.getConnObject(socket.connID);
// NOTE: object instead of event/update name is passed
var event = SR.EventManager.unpack(parsed, conn, conn.connID);
SR.EventManager.checkin(event, this.eventHandler.dispatcher);
// clear buffer after successful parse
socket.recv_buf = '';
}
};
// setup a usable socket (can be either a server-received incoming connection, or a outgoing client connection)
this.setup = function (socket) {
// start from here step 3
// NOTE: this part is called when connection is made
LOG.sys('setup new socket...', l_name);
// whether this socket can be connected
if (socket.connected === false)
return;
socket.setEncoding('UTF8');
socket.setKeepAlive(true, 20 * 1000); // 20 seconds keepalive
socket.setNoDelay(true);
// indicates whether socket buffer is full (will set to 'true' when full)
// if full, all sending events on this socket will pause until socket is not full
socket.isFull = false;
// buffer for partially received message
socket.recv_buf = '';
// store remote host info
// NOTE: remoteAddress & remotePort may become invalid when socket closes,
// so need to keep record here...
// NOTE: right now these info are kept mainly to display which remote host is closed/disconnected
if (socket.hasOwnProperty('host') === false) {
socket.host = socket.remoteAddress;
socket.port = socket.remotePort;
}
/*
var socket = conn.connector;
try {
// continue to next connection is send is success
if (socket.send(data + '\n') === true) {
continue;
}
}
catch (e) {
LOG.error('socket.send exception-' + e, l_name);
// print to show the message
LOG.error('data: ' + data, l_name);
}
*/
// record a new connection while attching connID to socket
if (that.connHandler) {
LOG.sys('notify for new socket connection...', l_name);
// NOTE: use addConnection for *any* persistant connections (websocket/long-polling)?
// TODO: need to check if addConnection can take function as parameter (instead of socket object)
//that.connHandler.addConnection(socket, 'socket', {host: socket.host, port: socket.port});
var conn = that.connHandler.addConnection(function (res_obj, data) {
// do not process for empty objects
if (!res_obj || Object.keys(res_obj).length === 0)
return false;
try {
// continue to next connection is send is success
if (socket.send(data + '\n') === true)
return true;
else
return false;
} catch (e) {
LOG.error('socket.send exception-' + e, l_name);
// print to show the message
LOG.error('data: ' + data, l_name);
return false;
}
}, 'socket', {host: socket.host, port: socket.port});
// record connection info (add a 'connID' property to socket)
socket.connID = conn.connID;
}
// set the socket as ready to send
socket.connected = true;
// when socket becomes empty again, continue sending
socket.on('drain', function () {
socket.isFull = false;
socket.resume();
});
// NOTE: 'data' event might come BEFORE 'connect' event is triggerred
socket.on('data', function (data) {
that.process(socket, data);
});
socket.on('end', function () {
LOG.sys('connection end for: ' + socket.host + ':' + socket.port, l_name);
});
// handle connection error or close
var disconnect_handler = function (err) {
// print error
if (err) {
LOG.error('socket returns error: ', l_name);
LOG.error(err, l_name);
}
// prevent doing close connection more than once
if (socket.hasOwnProperty('connected') === false || socket.connected === false)
return;
socket.connected = false;
// NOTE: we allow disconnect to go through, to allow remote shutdown
var hostinfo = socket.host + ':' + socket.port;
LOG.sys('close connection for: ' + hostinfo, l_name);
// notify callback for deletion
if (that.connHandler) {
that.connHandler.removeConnection(socket,
function () {
LOG.warn('connection closed for ' + hostinfo, l_name);
}
);
}
};
socket.on('close', disconnect_handler);
socket.on('error', disconnect_handler);
// attach error-catching socket write
socket.send = that.send;
return socket;
};
// error catching version of send socket message
// NOTE: 'this' variable should access both a socket instance's variables and also the variables from the Socket.prototype
// verify this?
this.send = function (msg) {
// NOTE: all original 'socket' variable have been changed to 'this'
if (this.connected !== true)
return false;
LOG.sys('sending length: ' + msg.length, l_name);
LOG.sys(msg, l_name);
if (msg.startsWith('{"_cid"')) {
LOG.stack();
return false;
}
// send data to socket, append '\n' at end to indicate message end
if (this.write(msg) === false) {
// socket has slowed, lower the priority of this socket
this.isFull = true;
this.pause();
LOG.warn('socket buffer full. remote host: ' + this.host + ':' + this.port, l_name);
return false;
}
return true;
};
}; // end SocketHandler
// export socket object to outside
exports.icSocket = SocketHandler;