// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved // http://code.google.com/p/narya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.nio.conman; import java.util.ArrayList; import java.util.Map; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.name.Named; import com.samskivert.util.IntMap; import com.samskivert.util.IntMaps; import com.samskivert.util.Lifecycle; import com.samskivert.util.LoopingThread; import com.samskivert.util.Queue; import com.samskivert.util.Tuple; import com.threerings.presents.data.ConMgrStats; import com.threerings.presents.server.ReportManager; import com.threerings.nio.SelectorIterable; import static com.threerings.NaryaLog.log; /** * Manages socket connections. It creates connection objects for each socket connection, but those * connection objects interact closely with the connection manager because network I/O is done via * a poll()-like mechanism rather than via threads.
*
* ConnectionManager doesn't directly accept TCP connections; it expects
* {@link ServerSocketChannelAcceptor} or an external entity to do so and call its
* {@link #handleAcceptedSocket} method
*/
public abstract class ConnectionManager extends LoopingThread
implements Lifecycle.ShutdownComponent, ReportManager.Reporter
{
/**
* Creates a connection manager instance.
*/
public ConnectionManager (Lifecycle cycle, ReportManager repmgr)
throws IOException
{
super("ConnectionManager");
cycle.addComponent(this);
repmgr.registerReporter(this);
_selector = Selector.open();
}
/**
* Instructs us to execute the specified runnable when the connection manager thread exits.
* Note: this will be executed on the connection manager thread, so don't do anything
* dangerous. Only one action may be specified and it may be cleared by calling this method
* with null.
*/
public void setShutdownAction (Runnable onExit)
{
_onExit = onExit;
}
/**
* Returns our current runtime statistics. Note: don't call this method too
* frequently as it is synchronized and will contend with the network I/O thread.
*/
public synchronized ConMgrStats getStats ()
{
// fill in our snapshot values
_stats.connectionCount = _connections.size();
_stats.handlerCount = _handlers.size();
_stats.deathQueueSize = _deathq.size();
_stats.outQueueSize = _outq.size();
if (_oflowqs.size() > 0) {
_stats.overQueueSize = 0;
for (OverflowQueue oq : _oflowqs.values()) {
_stats.overQueueSize += oq.size();
}
}
return _stats.clone();
}
/**
* Registers ops on chan on this manager's selector and hooks
* netEventHandler up to receive events whenever the selection occurs.
*/
public SelectionKey register (SelectableChannel chan, int ops, NetEventHandler netEventHandler)
throws IOException
{
SelectionKey key = chan.register(_selector, ops);
_handlers.put(key, netEventHandler);
return key;
}
/**
* Introduces a new active socket into Presents from off the ConnectionManager thread. If
* Presents is embedded in another framework that handles socket acceptance, this will be
* called by its socket acceptor to get the socket into Presents to start authorization.
*/
public void transferAcceptedSocket (SocketChannel channel)
{
_acceptedq.append(channel);
}
/**
* Queues a connection up to be closed on the conmgr thread.
*/
public void closeConnection (Connection conn)
{
_deathq.append(conn);
}
// from interface ReportManager.Reporter
public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset)
{
ConMgrStats stats = getStats();
long eventCount = stats.eventCount - _lastStats.eventCount;
int connects = stats.connects - _lastStats.connects;
int disconnects = stats.disconnects - _lastStats.disconnects;
int closes = stats.closes - _lastStats.closes;
long bytesIn = stats.bytesIn - _lastStats.bytesIn;
long bytesOut = stats.bytesOut - _lastStats.bytesOut;
long msgsIn = stats.msgsIn - _lastStats.msgsIn;
long msgsOut = stats.msgsOut - _lastStats.msgsOut;
if (reset) {
_lastStats = stats;
}
// make sure we don't div0 if this method somehow gets called twice in
// the same millisecond
sinceLast = Math.max(sinceLast, 1L);
report.append("* presents.net.ConnectionManager:\n");
report.append("- Network connections: ");
report.append(stats.connectionCount).append(" connections, ");
report.append(stats.handlerCount).append(" handlers\n");
report.append("- Network activity: ");
report.append(eventCount).append(" events, ");
report.append(connects).append(" connects, ");
report.append(disconnects).append(" disconnects, ");
report.append(closes).append(" closes\n");
report.append("- Network input: ");
report.append(bytesIn).append(" bytes, ");
report.append(msgsIn).append(" msgs, ");
report.append(msgsIn*1000/sinceLast).append(" mps, ");
long avgIn = (msgsIn == 0) ? 0 : (bytesIn/msgsIn);
report.append(avgIn).append(" avg size, ");
report.append(bytesIn*1000/sinceLast).append(" bps\n");
report.append("- Network output: ");
report.append(bytesOut).append(" bytes, ");
report.append(msgsOut).append(" msgs, ");
report.append(msgsOut*1000/sinceLast).append(" mps, ");
long avgOut = (msgsOut == 0) ? 0 : (bytesOut/msgsOut);
report.append(avgOut).append(" avg size, ");
report.append(bytesOut*1000/sinceLast).append(" bps\n");
}
@Override // from LoopingThread
protected void willStart ()
{
super.willStart();
_selectorSelector = new SelectorIterable(
_selector, _selectLoopTime, new SelectorIterable.SelectFailureHandler() {
public void handleSelectFailure (Exception e) {
log.error("One of our selectors crapped out completely. " +
"Shutting down the connection manager.", e);
shutdown();
}
});
}
@Override // from LoopingThread
protected void iterate ()
{
// performs the select loop; this is the body of the conmgr thread
final long iterStamp = System.currentTimeMillis();
// note whether or not we're generating a debug report
boolean generateDebugReport = (iterStamp - _lastDebugStamp > DEBUG_REPORT_INTERVAL);
if (DEBUG_REPORT && generateDebugReport) {
_lastDebugStamp = iterStamp;
}
// close any connections that have been queued up to die
Connection dconn;
while ((dconn = _deathq.getNonBlocking()) != null) {
// it's possible that we caught an EOF trying to read from this connection even after
// it was queued up for death, so let's avoid trying to close it twice
if (!dconn.isClosed()) {
dconn.close();
}
}
// close connections that have had no network traffic for too long
for (NetEventHandler handler : _handlers.values()) {
if (handler.checkIdle(iterStamp)) {
// this will queue the connection for closure on our next tick
handler.becameIdle();
}
}
// send any messages that are waiting on the outgoing overflow and message queues
sendOutgoingMessages(iterStamp);
// we may be in the middle of shutting down (in which case super.isRunning() is false but
// isRunning() is true); this is because we stick around until the dobject manager is
// totally done so that we can send shutdown-related events out to our clients; during
// those last moments we don't want to accept new connections or read any incoming messages
if (super.isRunning()) {
handleIncoming(iterStamp);
}
if (DEBUG_REPORT && generateDebugReport) {
log.info("CONMGR status " + getStats());
}
}
protected void handleIncoming(long iterStamp) {
SocketChannel accepted;
while ((accepted = _acceptedq.getNonBlocking()) != null) {
handleAcceptedSocket(accepted);
}
// listen for and process incoming network events
processIncomingEvents(iterStamp);
}
/**
* Adds a connection for the given socket to the managed set.
*/
protected abstract void handleAcceptedSocket (SocketChannel channel);
protected void handleAcceptedSocket (SocketChannel channel, Connection conn)
{
try {
// create a new authing connection object to manage the authentication of this client
// connection and register it with our selection set
channel.configureBlocking(false);
conn.init(this, channel, System.currentTimeMillis());
conn.selkey = register(channel, SelectionKey.OP_READ, conn);
_handlers.put(conn.selkey, conn);
synchronized (this) {
_stats.connects++;
}
} catch (IOException ioe) {
// no need to generate a warning because this happens in the normal course of events
log.info("Failure accepting new connection: " + ioe);
// make sure we don't leak a socket if something went awry
try {
channel.socket().close();
} catch (IOException ioe2) {
log.warning("Failed closing aborted connection: " + ioe2);
}
}
}
/**
* Checks for any network events on our set of sockets and passes those events down to their
* associated {@link NetEventHandler}s for processing.
*/
protected void processIncomingEvents (long iterStamp)
{
// process those events
long bytesIn = 0, msgsIn = 0, eventCount = 0;
for (SelectionKey selkey : _selectorSelector) {
eventCount++;
NetEventHandler handler = null;
try {
handler = _handlers.get(selkey);
if (handler == null) {
log.warning("Received network event for unknown handler",
"key", selkey, "ops", selkey.readyOps());
// request that this key be removed from our selection set, which normally
// happens automatically but for some reason didn't
selkey.cancel();
continue;
}
// log.info("Got event", "selkey", selkey, "handler", handler);
int got = handler.handleEvent(iterStamp);
if (got != 0) {
bytesIn += got;
// we know that the handlers only report having read bytes when they have a
// whole message, so we can count thusly
msgsIn++;
}
} catch (Exception e) {
log.warning("Error processing network data: " + handler + ".", e);
// if you freak out here, you go straight in the can
if (handler != null && handler instanceof Connection) {
closeConnection((Connection)handler);
}
}
}
synchronized (this) {
// update our stats
_stats.eventCount += eventCount;
_stats.bytesIn += bytesIn;
_stats.msgsIn += msgsIn;
}
}
/**
* Writes all queued overflow and normal messages to their respective sockets. Connections that
* already have established overflow queues will have their messages appended to their overflow
* queue instead so that they are delivered in the proper order.
*/
protected void sendOutgoingMessages (long iterStamp)
{
// first attempt to send any messages waiting on the overflow queues
if (_oflowqs.size() > 0) {
// do this on a snapshot as a network failure writing oflow queue messages will result
// in the queue being removed from _oflowqs via the connectionFailed() code path
for (OverflowQueue oq : _oflowqs.values().toArray(new OverflowQueue[_oflowqs.size()])) {
try {
// try writing the messages in this overflow queue
if (oq.writeOverflowMessages(iterStamp)) {
// if they were all written, we can remove it
_oflowqs.remove(oq.conn);
}
} catch (IOException ioe) {
oq.conn.networkFailure(ioe);
}
}
}
// then send any new messages
Tuple