Created ServerCommunicator which does all I/O using the ConnectionManager's

polled socket system. This much more cleanly and efficiently integrates peer
(and other server to server) connections into the normal server I/O framework
and should also eliminate for good the annoying server hangs that result when
our old blocking client I/O threads got their pants wedged.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5510 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2008-11-07 00:27:07 +00:00
parent b81ccde75b
commit 89f02fc728
16 changed files with 777 additions and 594 deletions
@@ -57,6 +57,7 @@ import com.threerings.presents.net.EventNotification;
import com.threerings.presents.net.FailureResponse;
import com.threerings.presents.net.ForwardEventRequest;
import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.ObjectResponse;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
@@ -65,10 +66,8 @@ import com.threerings.presents.net.ThrottleUpdatedMessage;
import com.threerings.presents.net.UnsubscribeRequest;
import com.threerings.presents.net.UnsubscribeResponse;
import com.threerings.presents.net.UpdateThrottleMessage;
import com.threerings.presents.net.UpstreamMessage;
import com.threerings.presents.server.net.Connection;
import com.threerings.presents.server.net.ConnectionManager;
import com.threerings.presents.server.net.MessageHandler;
import static com.threerings.presents.Log.log;
@@ -84,7 +83,7 @@ import static com.threerings.presents.Log.log;
* from the conmgr thread and therefore also need not be synchronized.
*/
public class PresentsSession
implements MessageHandler, ClientResolutionListener
implements Connection.MessageHandler, ClientResolutionListener
{
/** Used by {@link PresentsSession#setUsername} to report success or failure. */
public static interface UserChangeListener
@@ -396,8 +395,8 @@ public class PresentsSession
endSession();
}
// from interface MessageHandler
public void handleMessage (UpstreamMessage message)
// from interface Connection.MessageHandler
public void handleMessage (Message message)
{
_messagesIn++; // count 'em up!
@@ -957,16 +956,16 @@ public class PresentsSession
}
/**
* Message dispatchers are used to dispatch each different type of upstream message. We can
* look the dispatcher up in a table and invoke it through an overloaded member which is faster
* (so we think) than doing a bunch of instanceofs.
* Message dispatchers are used to dispatch each different type of message. We can look the
* dispatcher up in a table and invoke it through an overloaded member which is faster (so we
* think) than doing a bunch of instanceofs.
*/
protected static interface MessageDispatcher
{
/**
* Dispatch the supplied message for the specified client.
*/
void dispatch (PresentsSession client, UpstreamMessage mge);
void dispatch (PresentsSession client, Message mge);
}
/**
@@ -974,7 +973,7 @@ public class PresentsSession
*/
protected static class SubscribeDispatcher implements MessageDispatcher
{
public void dispatch (PresentsSession client, UpstreamMessage msg)
public void dispatch (PresentsSession client, Message msg)
{
SubscribeRequest req = (SubscribeRequest)msg;
// log.info("Subscribing [client=" + client + ", oid=" + req.getOid() + "].");
@@ -989,7 +988,7 @@ public class PresentsSession
*/
protected static class UnsubscribeDispatcher implements MessageDispatcher
{
public void dispatch (PresentsSession client, UpstreamMessage msg)
public void dispatch (PresentsSession client, Message msg)
{
UnsubscribeRequest req = (UnsubscribeRequest)msg;
int oid = req.getOid();
@@ -1009,7 +1008,7 @@ public class PresentsSession
*/
protected static class ForwardEventDispatcher implements MessageDispatcher
{
public void dispatch (PresentsSession client, UpstreamMessage msg)
public void dispatch (PresentsSession client, Message msg)
{
ForwardEventRequest req = (ForwardEventRequest)msg;
DEvent fevt = req.getEvent();
@@ -1036,7 +1035,7 @@ public class PresentsSession
*/
protected static class PingDispatcher implements MessageDispatcher
{
public void dispatch (PresentsSession client, UpstreamMessage msg)
public void dispatch (PresentsSession client, Message msg)
{
// send a pong response using the transport with which the message was received
PingRequest req = (PingRequest)msg;
@@ -1049,7 +1048,7 @@ public class PresentsSession
*/
protected static class ThrottleUpdatedDispatcher implements MessageDispatcher
{
public void dispatch (final PresentsSession client, UpstreamMessage msg)
public void dispatch (final PresentsSession client, Message msg)
{
log.debug("Client ACKed throttle update", "client", client);
client.throttleUpdated();
@@ -1061,7 +1060,7 @@ public class PresentsSession
*/
protected static class LogoffDispatcher implements MessageDispatcher
{
public void dispatch (final PresentsSession client, UpstreamMessage msg)
public void dispatch (final PresentsSession client, Message msg)
{
log.debug("Client requested logoff", "client", client);
client.safeEndSession();
@@ -21,62 +21,37 @@
package com.threerings.presents.server.net;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import com.samskivert.util.StringUtil;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.UpstreamMessage;
import com.threerings.presents.net.Message;
import static com.threerings.presents.Log.log;
/**
* The authing connection manages the client connection until
* authentication has completed (for better or for worse).
* The authing connection manages the client connection until authentication has completed (for
* better or for worse).
*/
public class AuthingConnection extends Connection
implements MessageHandler
{
/**
* Creates a new authing connection object that will manage the
* authentication process for the suppled client socket.
*/
public AuthingConnection (ConnectionManager cmgr, SelectionKey selkey,
SocketChannel channel)
throws IOException
public AuthingConnection ()
{
super(cmgr, selkey, channel, System.currentTimeMillis());
// we are our own message handler
setMessageHandler(this);
setMessageHandler(new MessageHandler() {
public void handleMessage (Message msg) {
try {
// keep a handle on our auth request
_authreq = (AuthRequest)msg;
// post ourselves for processing by the authmgr
_cmgr.authenticateConnection(AuthingConnection.this);
} catch (ClassCastException cce) {
log.warning("Received non-authreq message during authentication process",
"conn", AuthingConnection.this, "msg", msg);
}
}
});
}
/**
* Called when a new message has arrived from the client.
*/
public void handleMessage (UpstreamMessage msg)
{
try {
// keep a handle on our auth request
_authreq = (AuthRequest)msg;
// post ourselves for processing by the authmgr
_cmgr.authenticateConnection(this);
} catch (ClassCastException cce) {
log.warning("Received non-authreq message during " +
"authentication process [conn=" + this +
", msg=" + msg + "].");
}
}
/**
* Returns a reference to the auth request currently being processed
* by this authing connection.
* Returns a reference to the auth request currently being processed.
*/
public AuthRequest getAuthRequest ()
{
@@ -84,8 +59,8 @@ public class AuthingConnection extends Connection
}
/**
* Returns the auth response delivered to the client (only valid after
* the auth request has been processed.
* Returns the auth response delivered to the client (only valid after the auth request has
* been processed.
*/
public AuthResponse getAuthResponse ()
{
@@ -93,9 +68,8 @@ public class AuthingConnection extends Connection
}
/**
* Stores a reference to the auth response delivered to this
* connection. This is called by the auth manager after delivering the
* auth response to the client.
* Stores a reference to the auth response delivered to this connection. This is called by the
* auth manager after delivering the auth response to the client.
*/
public void setAuthResponse (AuthResponse authrsp)
{
@@ -105,8 +79,7 @@ public class AuthingConnection extends Connection
@Override
public String toString ()
{
return "[mode=AUTHING, addr=" +
StringUtil.toString(_channel.socket().getInetAddress()) + "]";
return "[mode=AUTHING, addr=" + getInetAddress() + "]";
}
protected AuthRequest _authreq;
@@ -43,34 +43,39 @@ import com.threerings.io.ObjectOutputStream;
import com.threerings.io.UnreliableObjectInputStream;
import com.threerings.io.UnreliableObjectOutputStream;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.UpstreamMessage;
import com.threerings.presents.util.DatagramSequencer;
import static com.threerings.presents.Log.log;
/**
* The base connection class implements the net event handler interface and processes raw incoming
* network data into a stream of parsed <code>UpstreamMessage</code> objects. It also provides the
* means to send messages to the client and facilities for checking delinquency.
* network data into a stream of parsed {@link Message} objects. It also provides the means to send
* messages to the client and facilities for checking delinquency.
*/
public abstract class Connection implements NetEventHandler
public class Connection implements NetEventHandler
{
/** Used with {@link #setMessageHandler}. */
public static interface MessageHandler {
/** Called when a complete message has been parsed from incoming network data. */
void handleMessage (Message message);
}
/** The key used by the NIO code to track this connection. */
public SelectionKey selkey;
/**
* Constructs a connection object that is associated with the supplied socket.
* Initializes a connection object with a socket and related info.
*
* @param cmgr The connection manager with which this connection is associated.
* @param selkey the key used by the NIO code to track this connection.
* @param channel The socket channel from which we'll be reading messages.
* @param createStamp The time at which this connection was created.
*/
public Connection (ConnectionManager cmgr, SelectionKey selkey, SocketChannel channel,
long createStamp)
public void init (ConnectionManager cmgr, SocketChannel channel, long createStamp)
throws IOException
{
_cmgr = cmgr;
_selkey = selkey;
_channel = channel;
_lastEvent = createStamp;
_connectionId = ++_lastConnectionId;
@@ -104,14 +109,6 @@ public abstract class Connection implements NetEventHandler
return _connectionId;
}
/**
* Returns the selection key associated with our socket channel.
*/
public SelectionKey getSelectionKey ()
{
return _selkey;
}
/**
* Returns the non-blocking socket object used to construct this connection.
*/
@@ -178,10 +175,39 @@ public abstract class Connection implements NetEventHandler
}
/**
* Called when there is a failure reading or writing on this connection. We notify the
* Queues up a request to have this connection closed by the connection manager once all
* messages in its queue have been written to its target.
*/
public void asyncClose ()
{
_cmgr.postAsyncClose(this);
}
/**
* Posts a message for delivery to this connection. The message will be delivered by the conmgr
* thread as soon as it gets to it.
*/
public void postMessage (Message msg)
{
// pass this along to the connection manager
_cmgr.postMessage(this, msg);
}
/**
* Called when an outgoing socket experiences a connect failure. The connection manager will
* have cleaned up the partial registration needed during the connect process, so we are only
* responsible for closing our socket.
*/
public void connectFailure (IOException ioe)
{
closeSocket();
}
/**
* Called when there is a failure reading or writing to this connection. We notify the
* connection manager and close ourselves down.
*/
public void handleFailure (IOException ioe)
public void networkFailure (IOException ioe)
{
// if we're already closed, then something is seriously funny
if (isClosed()) {
@@ -196,6 +222,135 @@ public abstract class Connection implements NetEventHandler
closeSocket();
}
/**
* Processes a datagram sent to this connection.
*/
public void handleDatagram (InetSocketAddress source, ByteBuffer buf, long when)
{
// lazily create our various bits and bobs
if (_digest == null) {
try {
_digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
log.warning("Missing MD5 algorithm.");
return;
}
ByteBufferInputStream bin = new ByteBufferInputStream(buf);
_sequencer = new DatagramSequencer(
new UnreliableObjectInputStream(bin),
new UnreliableObjectOutputStream(_cmgr.getFlattener()));
}
// verify the hash
buf.position(12);
_digest.update(buf);
byte[] hash = _digest.digest(_datagramSecret);
buf.position(4);
for (int ii = 0; ii < 8; ii++) {
if (hash[ii] != buf.get()) {
log.warning("Datagram failed hash check [connectionId=" + _connectionId +
", source=" + source + "].");
return;
}
}
// update our target address
_datagramAddress = source;
// read the contents through the sequencer
try {
Message msg = _sequencer.readDatagram();
if (msg == null) {
return; // received out of order
}
msg.received = when;
_handler.handleMessage(msg);
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading datagram [error=" + cnfe + "].");
} catch (IOException ioe) {
log.warning("Error reading datagram [error=" + ioe + "].");
}
}
// from interface NetEventHandler
public int handleEvent (long when)
{
// make a note that we received an event as of this time
_lastEvent = when;
int bytesIn = 0;
try {
// we're lazy about creating our input streams because we may be inheriting them from
// our authing connection and we don't want to unnecessarily create them in that case
if (_fin == null) {
_fin = new FramedInputStream();
_oin = new ObjectInputStream(_fin);
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
// there may be more than one frame in the buffer, so we keep reading them until we run
// out of data
while (_fin.readFrame(_channel)) {
// make a note of how many bytes are in this frame (including the frame length
// bytes which aren't reported in available())
bytesIn = _fin.available() + 4;
// parse the message and pass it on
Message msg = (Message)_oin.readObject();
msg.received = when;
// Log.info("Read message " + msg + ".");
_handler.handleMessage(msg);
}
} catch (EOFException eofe) {
// close down the socket gracefully
close();
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading message from socket [channel=" +
StringUtil.safeToString(_channel) + ", error=" + cnfe + "].");
// deal with the failure
String errmsg = "Unable to decode incoming message.";
networkFailure((IOException) new IOException(errmsg).initCause(cnfe));
} catch (IOException ioe) {
// don't log a warning for the ever-popular "the client dropped the connection" failure
String msg = ioe.getMessage();
if (msg == null || msg.indexOf("reset by peer") == -1) {
log.warning("Error reading message from socket [channel=" +
StringUtil.safeToString(_channel) + ", error=" + ioe + "].");
}
// deal with the failure
networkFailure(ioe);
}
return bytesIn;
}
// documentation inherited from interface
public boolean checkIdle (long now)
{
long idleMillis = now - _lastEvent;
if (idleMillis < PingRequest.PING_INTERVAL + LATENCY_GRACE) {
return false;
}
if (isClosed()) {
return true;
}
log.info("Disconnecting non-communicative client [conn=" + this +
", idle=" + idleMillis + "ms].");
return true;
}
@Override // from Object
public String toString ()
{
return "[id=" + (hashCode() % 1000) + ", addr=" + getInetAddress() + "]";
}
/**
* Returns the object input stream associated with this connection. This should only be used
* by the connection manager.
@@ -270,147 +425,10 @@ public abstract class Connection implements NetEventHandler
}
// clear out our references to prevent repeat closings
_selkey = null;
_channel = null;
}
/**
* Called when our client socket has data available for reading.
*/
public int handleEvent (long when)
{
// make a note that we received an event as of this time
_lastEvent = when;
int bytesIn = 0;
try {
// we're lazy about creating our input streams because we may be inheriting them from
// our authing connection and we don't want to unnecessarily create them in that case
if (_fin == null) {
_fin = new FramedInputStream();
_oin = new ObjectInputStream(_fin);
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
// there may be more than one frame in the buffer, so we keep reading them until we run
// out of data
while (_fin.readFrame(_channel)) {
// make a note of how many bytes are in this frame (including the frame length
// bytes which aren't reported in available())
bytesIn = _fin.available() + 4;
// parse the message and pass it on
UpstreamMessage msg = (UpstreamMessage)_oin.readObject();
msg.received = when;
// Log.info("Read message " + msg + ".");
_handler.handleMessage(msg);
}
} catch (EOFException eofe) {
// close down the socket gracefully
close();
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading message from socket [channel=" +
StringUtil.safeToString(_channel) + ", error=" + cnfe + "].");
// deal with the failure
String errmsg = "Unable to decode incoming message.";
handleFailure((IOException) new IOException(errmsg).initCause(cnfe));
} catch (IOException ioe) {
// don't log a warning for the ever-popular "the client dropped the connection" failure
String msg = ioe.getMessage();
if (msg == null || msg.indexOf("reset by peer") == -1) {
log.warning("Error reading message from socket [channel=" +
StringUtil.safeToString(_channel) + ", error=" + ioe + "].");
}
// deal with the failure
handleFailure(ioe);
}
return bytesIn;
}
/**
* Processes a datagram sent to this connection.
*/
public void handleDatagram (InetSocketAddress source, ByteBuffer buf, long when)
{
// lazily create our various bits and bobs
if (_digest == null) {
try {
_digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
log.warning("Missing MD5 algorithm.");
return;
}
ByteBufferInputStream bin = new ByteBufferInputStream(buf);
_sequencer = new DatagramSequencer(
new UnreliableObjectInputStream(bin),
new UnreliableObjectOutputStream(_cmgr.getFlattener()));
}
// verify the hash
buf.position(12);
_digest.update(buf);
byte[] hash = _digest.digest(_datagramSecret);
buf.position(4);
for (int ii = 0; ii < 8; ii++) {
if (hash[ii] != buf.get()) {
log.warning("Datagram failed hash check [connectionId=" + _connectionId +
", source=" + source + "].");
return;
}
}
// update our target address
_datagramAddress = source;
// read the contents through the sequencer
try {
UpstreamMessage msg = (UpstreamMessage)_sequencer.readDatagram();
if (msg == null) {
return; // received out of order
}
msg.received = when;
_handler.handleMessage(msg);
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading datagram [error=" + cnfe + "].");
} catch (IOException ioe) {
log.warning("Error reading datagram [error=" + ioe + "].");
}
}
// documentation inherited from interface
public boolean checkIdle (long now)
{
long idleMillis = now - _lastEvent;
if (idleMillis < PingRequest.PING_INTERVAL + LATENCY_GRACE) {
return false;
}
if (isClosed()) {
return true;
}
log.info("Disconnecting non-communicative client [conn=" + this +
", idle=" + idleMillis + "ms].");
return true;
}
/**
* Posts a downstream message for delivery to this connection. The message will be delivered by
* the conmgr thread as soon as it gets to it.
*/
public void postMessage (DownstreamMessage msg)
{
// pass this along to the connection manager
_cmgr.postMessage(this, msg);
}
protected ConnectionManager _cmgr;
protected SelectionKey _selkey;
protected SocketChannel _channel;
protected long _lastEvent;
@@ -62,7 +62,7 @@ import com.threerings.presents.client.Client;
import com.threerings.presents.data.ConMgrStats;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.DownstreamMessage;
import com.threerings.presents.net.Message;
import com.threerings.presents.server.Authenticator;
import com.threerings.presents.server.ChainedAuthenticator;
import com.threerings.presents.server.DummyAuthenticator;
@@ -193,6 +193,74 @@ public class ConnectionManager extends LoopingThread
}
}
/**
* Opens an outgoing connection to the supplied address. The connection will be opened in a
* non-blocking manner and added to the connection manager's select set. Messages posted to the
* connection prior to it being actually connected to its destination will remain in the queue.
* If the connection fails those messages will be dropped.
*
* @param conn the connection to be initialized and opened. Callers may want to provide a
* {@link Connection} derived class so that they may intercept calldown methods.
* @param hostname the hostname of the server to which to connect.
* @param port the port on which to connect to the server.
*
* @exception IOException thrown if an error occurs opening a connection to the supplied
* server. When this method returns the connection may or may not be actually connected to the
* server. If the asynchronous connection attempt fails, the Connection will be notified via
* {@link Connection#networkFailure}.
*/
public void openOutgoingConnection (final Connection conn, String hostname, int port)
throws IOException
{
// create a non-blocking socket channel to use for this connection
final SocketChannel sockchan = SocketChannel.open();
sockchan.configureBlocking(false);
// create our connection instance
conn.init(this, sockchan, System.currentTimeMillis());
// and register our channel with the selector (if this fails, we abandon ship immediately)
conn.selkey = sockchan.register(_selector, SelectionKey.OP_CONNECT);
// start our connection process (now if we fail we need to clean things up)
try {
NetEventHandler handler;
if (sockchan.connect(new InetSocketAddress(hostname, port))) {
// it is possible even for a non-blocking socket to connect immediately, in which
// case we stick the connection in as its event handler immediately
handler = conn;
} else {
// otherwise we wire up a special event handler that will wait for our socket to
// finish the connection process and then wire things up fully
handler = new NetEventHandler() {
public int handleEvent (long when) {
try {
if (sockchan.finishConnect()) {
// great, we're ready to roll, wire up the connection
conn.selkey = sockchan.register(_selector, SelectionKey.OP_READ);
_handlers.put(conn.selkey, conn);
}
} catch (IOException ioe) {
_handlers.remove(conn.selkey);
_oflowqs.remove(conn);
conn.connectFailure(ioe);
}
return 0;
}
public boolean checkIdle (long now) {
return conn.checkIdle(now);
}
};
}
_handlers.put(conn.selkey, handler);
} catch (IOException ioe) {
log.warning("Failed to initiate connection for " + sockchan + ".", ioe);
conn.connectFailure(ioe); // nothing else to clean up
throw ioe;
}
}
/**
* Queues a connection up to be closed on the conmgr thread.
*/
@@ -201,25 +269,8 @@ public class ConnectionManager extends LoopingThread
_deathq.append(conn);
}
/**
* Performs the authentication process on the specified connection. This is called by {@link
* AuthingConnection} itself once it receives its auth request.
*/
public void authenticateConnection (AuthingConnection conn)
{
_author.authenticateConnection(_authInvoker, conn, new ResultListener<AuthingConnection>() {
public void requestCompleted (AuthingConnection conn) {
_authq.append(conn);
}
public void requestFailed (Exception cause) {
// this never happens
}
});
}
// documentation inherited from interface ReportManager.Reporter
public void appendReport (
StringBuilder report, long now, long sinceLast, boolean reset)
// from interface ReportManager.Reporter
public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset)
{
ConMgrStats stats = getStats();
int connects = stats.connects - _lastStats.connects;
@@ -263,6 +314,22 @@ public class ConnectionManager extends LoopingThread
return super.isRunning() || _omgr.isRunning();
}
/**
* Performs the authentication process on the specified connection. This is called by {@link
* AuthingConnection} itself once it receives its auth request.
*/
protected void authenticateConnection (AuthingConnection conn)
{
_author.authenticateConnection(_authInvoker, conn, new ResultListener<AuthingConnection>() {
public void requestCompleted (AuthingConnection conn) {
_authq.append(conn);
}
public void requestFailed (Exception cause) {
// this never happens
}
});
}
/**
* Notifies the connection observers of a connection event. Used internally.
*/
@@ -363,10 +430,6 @@ public class ConnectionManager extends LoopingThread
}
}
// we'll use these for sending messages to clients
_framer = new FramingOutputStream();
_flattener = new ByteArrayOutputStream();
// notify our startup listener, if we have one
if (_startlist != null) {
_startlist.requestCompleted(null);
@@ -461,21 +524,20 @@ public class ConnectionManager extends LoopingThread
try {
// construct a new running connection to handle this connections network traffic
// from here on out
SelectionKey selkey = conn.getSelectionKey();
RunningConnection rconn = new RunningConnection(
this, selkey, conn.getChannel(), iterStamp);
Connection rconn = new Connection();
rconn.init(this, conn.getChannel(), iterStamp);
rconn.selkey = conn.selkey;
// we need to keep using the same object input and output streams from the
// beginning of the session because they have context that needs to be preserved
rconn.inheritStreams(conn);
// replace the mapping in the handlers table from the old conn with the new one
_handlers.put(selkey, rconn);
_handlers.put(rconn.selkey, rconn);
// add a mapping for the connection id and set the datagram secret
_connections.put(rconn.getConnectionId(), rconn);
rconn.setDatagramSecret(
conn.getAuthRequest().getCredentials().getDatagramSecret());
rconn.setDatagramSecret(conn.getAuthRequest().getCredentials().getDatagramSecret());
// transfer any overflow queue for that connection
OverflowQueue oflowHandler = _oflowqs.remove(conn);
@@ -494,9 +556,10 @@ public class ConnectionManager extends LoopingThread
Set<SelectionKey> ready = null;
try {
// check for incoming network events
// log.debug("Selecting from " + StringUtil.toString(_selector.keys()) + " (" +
// SELECT_LOOP_TIME + ").");
// check for incoming network events
int ecount = _selector.select(SELECT_LOOP_TIME);
ready = _selector.selectedKeys();
if (ecount == 0) {
@@ -538,8 +601,7 @@ public class ConnectionManager extends LoopingThread
try {
handler = _handlers.get(selkey);
if (handler == null) {
log.warning("Received network event but have no registered handler " +
"[selkey=" + selkey + "].");
log.warning("Received network event for unknown handler", "key", selkey);
// request that this key be removed from our selection set, which normally
// happens automatically but for some reason didn't
selkey.cancel();
@@ -592,7 +654,7 @@ public class ConnectionManager extends LoopingThread
}
} catch (IOException ioe) {
oq.conn.handleFailure(ioe);
oq.conn.networkFailure(ioe);
}
}
}
@@ -641,10 +703,15 @@ public class ConnectionManager extends LoopingThread
return true;
}
// if this is an asynchronous close request, queue the connection up for death
if (data == ASYNC_CLOSE_REQUEST) {
closeConnection(conn);
return true;
}
// sanity check the message size
if (data.length > 1024 * 1024) {
log.warning("Refusing to write absurdly large message [conn=" + conn +
", size=" + data.length + "].");
log.warning("Refusing to write very large message", "conn", conn, "size", data.length);
return true;
}
@@ -664,26 +731,26 @@ public class ConnectionManager extends LoopingThread
_outbuf.put(data);
_outbuf.flip();
// if the connection to which we're writing is not yet ready, the whole message is
// "leftover", so we pass it to the partial write handler
SocketChannel sochan = conn.getChannel();
if (!sochan.isConnected()) {
pwh.handlePartialWrite(conn, _outbuf);
return false;
}
// then write the data to the socket
int wrote = conn.getChannel().write(_outbuf);
int wrote = sochan.write(_outbuf);
noteWrite(1, wrote);
// if we didn't write our entire message, deal with the leftover bytes
if (_outbuf.remaining() > 0) {
fully = false;
// log.info("Partial write [conn=" + conn +
// ", msg=" + StringUtil.shortClassName(outmsg) + ", wrote=" + wrote +
// ", size=" + buffer.limit() + "].");
pwh.handlePartialWrite(conn, _outbuf);
// } else if (wrote > 10000) {
// log.info("Big write [conn=" + conn +
// ", msg=" + StringUtil.shortClassName(outmsg) +
// ", wrote=" + wrote + "].");
}
} catch (IOException ioe) {
// instruct the connection to deal with its failure
conn.handleFailure(ioe);
conn.networkFailure(ioe); // instruct the connection to deal with its failure
} finally {
_outbuf.clear();
@@ -790,8 +857,10 @@ public class ConnectionManager extends LoopingThread
// connection and register it with our selection set
SelectableChannel selchan = channel;
selchan.configureBlocking(false);
SelectionKey selkey = selchan.register(_selector, SelectionKey.OP_READ);
_handlers.put(selkey, new AuthingConnection(this, selkey, channel));
AuthingConnection aconn = new AuthingConnection();
aconn.selkey = selchan.register(_selector, SelectionKey.OP_READ);
aconn.init(this, channel, System.currentTimeMillis());
_handlers.put(aconn.selkey, aconn);
synchronized (this) {
_stats.connects++;
}
@@ -863,11 +932,11 @@ public class ConnectionManager extends LoopingThread
* which happens when forwarding an event to a client and at the completion of authentication,
* both of which <em>must</em> happen only on the distributed object thread.
*/
void postMessage (Connection conn, DownstreamMessage msg)
void postMessage (Connection conn, Message msg)
{
if (!isRunning()) {
log.warning(
"Posting message to inactive connection manager", "msg", msg, new Exception());
log.warning("Posting message to inactive connection manager",
"msg", msg, new Exception());
}
// sanity check
@@ -901,22 +970,20 @@ public class ConnectionManager extends LoopingThread
ByteBuffer buffer = _framer.frameAndReturnBuffer();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
// log.info("Flattened " + msg + " into " + data.length + " bytes.");
// log.info("Flattened " + msg + " into " + data.length + " bytes.");
// and slap both on the queue
_outq.append(new Tuple<Connection, byte[]>(conn, data));
_outq.append(Tuple.create(conn, data));
} catch (Exception e) {
log.warning("Failure flattening message [conn=" + conn +
", msg=" + msg + "].", e);
log.warning("Failure flattening message", "conn", conn, "msg", msg, e);
}
}
/**
* Helper function for {@link #postMessage}; handles posting the message as a datagram.
*/
void postDatagram (Connection conn, DownstreamMessage msg)
void postDatagram (Connection conn, Message msg)
throws Exception
{
_flattener.reset();
@@ -929,7 +996,20 @@ public class ConnectionManager extends LoopingThread
byte[] data = _flattener.toByteArray();
// slap it on the queue
_dataq.append(new Tuple<Connection, byte[]>(conn, data));
_dataq.append(Tuple.create(conn, data));
}
/**
* Posts a fake message to this connection's outgoing message queue that will cause the
* connection to be closed when this message is reached. This is only used by outgoing
* connections to ensure that they finish sending their queued outgoing messages before closing
* their connection. Incoming connections tend only to be closed at the request of the client
* or in case of delinquincy. In neither circumstance do we need to flush the client's outgoing
* queue before closing.
*/
void postAsyncClose (Connection conn)
{
_outq.append(Tuple.create(conn, ASYNC_CLOSE_REQUEST));
}
/**
@@ -939,7 +1019,7 @@ public class ConnectionManager extends LoopingThread
{
// remove this connection from our mappings (it is automatically removed from the Selector
// when the socket is closed)
_handlers.remove(conn.getSelectionKey());
_handlers.remove(conn.selkey);
_connections.remove(conn.getConnectionId());
_oflowqs.remove(conn);
synchronized (this) {
@@ -957,7 +1037,7 @@ public class ConnectionManager extends LoopingThread
{
// remove this connection from our mappings (it is automatically removed from the Selector
// when the socket is closed)
_handlers.remove(conn.getSelectionKey());
_handlers.remove(conn.selkey);
_connections.remove(conn.getConnectionId());
_oflowqs.remove(conn);
@@ -1012,8 +1092,14 @@ public class ConnectionManager extends LoopingThread
{
// write any partial message if we have one
if (_partial != null) {
// if our outgoing channel is still not ready, then bail immediately
SocketChannel sochan = conn.getChannel();
if (!sochan.isConnected()) {
return false;
}
// write all we can of our partial buffer
int wrote = conn.getChannel().write(_partial);
int wrote = sochan.write(_partial);
noteWrite(0, wrote);
if (_partial.remaining() == 0) {
@@ -1095,8 +1181,8 @@ public class ConnectionManager extends LoopingThread
protected Queue<Tuple<Connection, byte[]>> _outq = new Queue<Tuple<Connection, byte[]>>();
protected Queue<Tuple<Connection, byte[]>> _dataq = new Queue<Tuple<Connection, byte[]>>();
protected FramingOutputStream _framer;
protected ByteArrayOutputStream _flattener;
protected FramingOutputStream _framer = new FramingOutputStream();
protected ByteArrayOutputStream _flattener = new ByteArrayOutputStream();
protected ByteBuffer _outbuf = ByteBuffer.allocateDirect(64 * 1024);
protected ByteBuffer _databuf = ByteBuffer.allocateDirect(Client.MAX_DATAGRAM_SIZE);
@@ -1135,4 +1221,7 @@ public class ConnectionManager extends LoopingThread
protected static final int CONNECTION_ESTABLISHED = 0;
protected static final int CONNECTION_FAILED = 1;
protected static final int CONNECTION_CLOSED = 2;
/** Used to denote asynchronous close requests. */
protected static final byte[] ASYNC_CLOSE_REQUEST = new byte[0];
}
@@ -1,37 +0,0 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.presents.server.net;
import com.threerings.presents.net.UpstreamMessage;
/**
* After the connection object has parsed an entire upstream message, it
* passes it on to its message handler.
*/
public interface MessageHandler
{
/**
* Called when a complete message has been parsed from incoming
* network data.
*/
void handleMessage (UpstreamMessage message);
}
@@ -1,61 +0,0 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.presents.server.net;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import com.threerings.presents.net.UpstreamMessage;
/**
* A running connection object represents a fully operational client
* connection to the server.
*/
public class RunningConnection extends Connection
{
/**
* Constructs a new running connection object to manage the supplied
* client socket.
*/
public RunningConnection (ConnectionManager cmgr, SelectionKey selkey,
SocketChannel channel, long createStamp)
throws IOException
{
super(cmgr, selkey, channel, createStamp);
}
/**
* Called when a new message has arrived from the client.
*/
public void handleMessage (UpstreamMessage msg)
{
}
@Override
public String toString ()
{
return "[mode=RUNNING, id=" + (hashCode() % 1000) +
", addr=" + getInetAddress() + "]";
}
}
@@ -0,0 +1,196 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2008 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.presents.server.net;
import java.io.IOException;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.ClientDObjectMgr;
import com.threerings.presents.client.ClientObserver;
import com.threerings.presents.client.Communicator;
import com.threerings.presents.client.ObserverOps;
import com.threerings.presents.client.SessionObserver;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.LogoffRequest;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.UpstreamMessage;
import static com.threerings.presents.Log.log;
/**
* Provides Client {@link Communicator} services using non-blocking I/O and the connection manager.
*/
public class ServerCommunicator extends Communicator
{
public ServerCommunicator (Client client, ConnectionManager conmgr, RootDObjectManager rootmgr)
{
super(client);
_conmgr = conmgr;
_rootmgr = rootmgr;
_omgr = new ClientDObjectMgr(this, client);
}
@Override // from Communicator
public void logon ()
{
// make sure things are copacetic
if (_conn != null) {
throw new RuntimeException("Communicator already started.");
}
// we assume server entities have no firewall issues and can connect on the first port
try {
Connection conn = new Connection() {
@Override public void connectFailure (IOException ioe) {
_logonError = ioe; // report this as a logon failure
super.connectFailure(ioe);
}
@Override public void networkFailure (final IOException ioe) {
notifyClientObservers(new ObserverOps.Client() {
protected void notify (ClientObserver obs) {
obs.clientConnectionFailed(_client, ioe);
}
});
super.networkFailure(ioe);
}
@Override protected void closeSocket () {
super.closeSocket();
shutdown();
}
};
conn.setMessageHandler(new Connection.MessageHandler() {
public void handleMessage (Message message) {
try {
// our first message will always be an auth response
gotAuthResponse((AuthResponse)message);
} catch (Exception e) {
_logonError = e;
shutdown();
}
}
});
_conmgr.openOutgoingConnection(conn, _client.getHostname(), _client.getPorts()[0]);
_conn = conn;
if (_loader != null) {
_conn.setClassLoader(_loader);
}
// now send our auth request
postMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
_client.getBootGroups()));
} catch (IOException ioe) {
_logonError = ioe;
shutdown();
}
}
@Override // from Communicator
public void logoff ()
{
if (_conn != null) {
_conn.postMessage(new LogoffRequest());
_conn.asyncClose();
_conn = null;
}
}
@Override // from Communicator
public void gotBootstrap ()
{
// nothing needed
}
@Override // from Communicator
public void postMessage (final UpstreamMessage msg)
{
// if we're not on the main dobjmgr thread, we need to get there
if (!_rootmgr.isDispatchThread()) {
_rootmgr.postRunnable(new Runnable() {
public void run () {
postMessage(msg);
}
});
return;
}
if (_conn == null) {
log.info("Dropping message for lack of connection.", "client", _client, "msg", msg);
return;
}
// pass this message along to our connection
_conn.postMessage(msg);
// we cheat a bit and claim that we "wrote" when we post our messages so that we don't have
// to modify the connection manager to call a method on Connection every time it writes a
// message from the queue
updateWriteStamp();
}
@Override // from Communicator
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_conn != null) {
_conn.setClassLoader(loader);
}
}
@Override // from Communicator
protected synchronized void logonSucceeded (AuthResponseData data)
{
super.logonSucceeded(data);
// now we can route all messages to the ClientDObjectMgr
_conn.setMessageHandler(new Connection.MessageHandler() {
public void handleMessage (Message message) {
_omgr.processMessage(message);
}
});
}
protected void shutdown ()
{
if (_logonError == null) {
// we were logged on successfully, so report didLogoff first
notifyClientObservers(new ObserverOps.Session() {
protected void notify (SessionObserver obs) {
obs.clientDidLogoff(_client);
}
});
}
clientCleanup(_logonError);
}
protected ConnectionManager _conmgr;
protected RootDObjectManager _rootmgr;
protected Connection _conn;
protected ClientDObjectMgr _omgr;
protected ClassLoader _loader;
protected Exception _logonError;
}