// // $Id$ // // Narya library - tools for developing networked games // Copyright (C) 2002-2004 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.client; import java.io.EOFException; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; import com.samskivert.swing.RuntimeAdjust; import com.samskivert.util.IntListUtil; import com.samskivert.util.Interval; import com.samskivert.util.LoopingThread; import com.samskivert.util.Queue; import com.samskivert.util.StringUtil; import com.threerings.io.FramedInputStream; import com.threerings.io.FramingOutputStream; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; import com.threerings.presents.Log; import com.threerings.presents.data.AuthCodes; import com.threerings.presents.dobj.DObjectManager; import com.threerings.presents.net.AuthRequest; import com.threerings.presents.net.AuthResponse; import com.threerings.presents.net.AuthResponseData; import com.threerings.presents.net.DownstreamMessage; import com.threerings.presents.net.LogoffRequest; import com.threerings.presents.net.UpstreamMessage; /** * The client performs all network I/O on separate threads (one for * reading and one for writing). The communicator class encapsulates that * functionality. * *
* Logon synopsis:
*
* Client.logon():
* - Calls Communicator.start()
* Communicator.start():
* - spawn Reader thread
* Reader.run():
* { - connect
* - authenticate
* } if either fail, notify observers of failed logon
* - start writer thread
* - notify observers that we're logged on
* - read loop
* Writer.run():
* - write loop
*
*/
public class Communicator
{
/**
* Creates a new communicator instance which is associated with the
* supplied client.
*/
public Communicator (Client client)
{
_client = client;
}
/**
* Logs on to the server and initiates our full-duplex message
* exchange.
*/
public void logon ()
{
// make sure things are copacetic
if (_reader != null) {
throw new RuntimeException("Communicator already started.");
}
// start up the reader thread. it will connect to the server and
// start up the writer thread if everything went successfully
_reader = new Reader();
_reader.start();
}
/**
* Delivers a logoff notification to the server and shuts down the
* network connection. Also causes all communication threads to
* terminate.
*/
public synchronized void logoff ()
{
// if our socket is already closed, we've already taken care of
// this business
if (_channel == null) {
return;
}
// post a logoff message
postMessage(new LogoffRequest());
// let our reader and writer know that it's time to go
if (_reader != null) {
// if logoff() is being called by the client as part of a
// normal shutdown, this will cause the reader thread to be
// interrupted and shutdown gracefully. if logoff is being
// called by the reader thread as a result of a failed socket,
// it won't interrupt itself as it is already shutting down
// gracefully. if the JVM is buggy and calling interrupt() on
// a thread that is blocked on a socket doesn't wake it up,
// then when we close() the socket a bit further down, we have
// another chance that the reader thread will wake up; this
// time slightly less gracefully because it will think there's
// a network error when in fact we're just shutting down, but
// at least it will cleanly exit
_reader.shutdown();
}
if (_writer != null) {
// shutting down the writer thread is simpler because we can
// post a termination message on the queue and be sure that it
// will receive it. when the writer thread has delivered our
// logoff request and exited, we will complete the logoff
// process by closing our socket and invoking the
// clientDidLogoff callback
_writer.shutdown();
}
}
/**
* Queues up the specified message for delivery upstream.
*/
public void postMessage (UpstreamMessage msg)
{
// simply append the message to the queue
_msgq.append(msg);
}
/**
* Configures this communicator with a custom class loader to be used
* when reading and writing objects over the network.
*/
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_oin != null) {
_oin.setClassLoader(loader);
}
}
/**
* Callback called by the reader when the authentication process
* completes successfully. Here we extract the bootstrap information
* for the client and start up the writer thread to manage the other
* half of our bi-directional message stream.
*/
protected synchronized void logonSucceeded (AuthResponseData data)
{
Log.debug("Logon succeeded: " + data);
// create our distributed object manager
_omgr = new ClientDObjectMgr(this, _client);
// create a new writer thread and start it up
if (_writer != null) {
throw new RuntimeException("Writer already started!?");
}
_writer = new Writer();
_writer.start();
// fill the auth data into the client's local field so that it can
// be requested by external entities
_client._authData = data;
// wait for the bootstrap notification before we claim that we're
// actually logged on
}
/**
* Callback called by the reader or writer thread when something goes
* awry with our socket connection to the server.
*/
protected synchronized void connectionFailed (IOException ioe)
{
// make sure the socket isn't already closed down (meaning we've
// already dealt with the failed connection)
if (_channel == null) {
return;
}
Log.info("Connection failed: " + ioe);
Log.logStackTrace(ioe);
// let the client know that things went south
_client.notifyObservers(Client.CLIENT_CONNECTION_FAILED, ioe);
// and request that we go through the motions of logging off
logoff();
}
/**
* Callback called by the reader if the server closes the other end of
* the connection.
*/
protected synchronized void connectionClosed ()
{
// make sure the socket isn't already closed down (meaning we've
// already dealt with the closed connection)
if (_channel == null) {
return;
}
Log.debug("Connection closed.");
// now do the whole logoff thing
logoff();
}
/**
* Callback called by the reader thread when it goes away.
*/
protected synchronized void readerDidExit ()
{
// clear out our reader reference
_reader = null;
if (_writer == null) {
// there's no writer during authentication, so we may be
// responsible for closing the socket channel
closeChannel();
// let the client know when we finally go away
_client.cleanup(_logonError);
}
Log.debug("Reader thread exited.");
}
/**
* Callback called by the writer thread when it goes away.
*/
protected synchronized void writerDidExit ()
{
// clear out our writer reference
_writer = null;
Log.debug("Writer thread exited.");
// let the client observers know that we're logged off
_client.notifyObservers(Client.CLIENT_DID_LOGOFF, null);
// now that the writer thread has gone away, we can safely close
// our socket and let the client know that the logoff process has
// completed
closeChannel();
// let the client know when we finally go away
if (_reader == null) {
_client.cleanup(_logonError);
}
}
/**
* Closes the socket channel that we have open to the server. Called
* by either {@link #readerDidExit} or {@link #writerDidExit}
* whichever is called last.
*/
protected void closeChannel ()
{
if (_channel != null) {
Log.debug("Closing socket channel.");
try {
_channel.close();
} catch (IOException ioe) {
Log.warning("Error closing failed socket: " + ioe);
}
_channel = null;
// clear these out because they are probably large and in charge
_oin = null;
_oout = null;
}
}
/**
* Writes the supplied message to the socket.
*/
protected void sendMessage (UpstreamMessage msg)
throws IOException
{
if (_logMessages.getValue()) {
Log.info("SEND " + msg);
}
// first we write the message so that we can measure it's length
_oout.writeObject(msg);
_oout.flush();
// then write the framed message to actual output stream
try {
ByteBuffer buffer = _fout.frameAndReturnBuffer();
if (buffer.limit() > 4096) {
String txt = StringUtil.truncate(
String.valueOf(msg), 80, "...");
Log.info("Whoa, writin' a big one [msg=" + txt +
", size=" + buffer.limit() + "].");
}
int wrote = _channel.write(buffer);
if (wrote != buffer.limit()) {
Log.warning("Aiya! Couldn't write entire message [msg=" + msg +
", size=" + buffer.limit() +
", wrote=" + wrote + "].");
// } else {
// Log.info("Wrote " + wrote + " bytes.");
}
} finally {
_fout.resetFrame();
}
// make a note of our most recent write time
updateWriteStamp();
}
/**
* Returns the time at which we last sent a packet to the server.
*/
protected synchronized long getLastWrite ()
{
return _lastWrite;
}
/**
* Makes a note of the time at which we last communicated with the
* server.
*/
protected synchronized void updateWriteStamp ()
{
_lastWrite = System.currentTimeMillis();
}
/**
* Reads a new message from the socket (blocking until a message has
* arrived).
*/
protected DownstreamMessage receiveMessage ()
throws IOException
{
// read in the next message frame (readFrame() can return false
// meaning it only read part of the frame from the network, in
// which case we simply call it again because we can't do anything
// until it has a whole frame; it will throw an exception if it
// hits EOF or if something goes awry)
while (!_fin.readFrame(_channel));
try {
DownstreamMessage msg = (DownstreamMessage)_oin.readObject();
if (_logMessages.getValue()) {
Log.info("RECEIVE " + msg);
}
return msg;
} catch (ClassNotFoundException cnfe) {
throw (IOException) new IOException(
"Unable to decode incoming message.").initCause(cnfe);
}
}
/**
* Callback called by the reader thread when it has parsed a new
* message from the socket and wishes to have it processed.
*/
protected void processMessage (DownstreamMessage msg)
{
// post this message to the dobjmgr queue
_omgr.processMessage(msg);
}
/**
* The reader encapsulates the authentication and message reading
* process. It calls back to the Communicator class to do
* things, but the general flow of the reader thread is encapsulated
* in this class.
*/
protected class Reader extends LoopingThread
{
protected void willStart ()
{
// first we connect and authenticate with the server
try {
// connect to the server
connect();
// then authenticate
logon();
} catch (Exception e) {
Log.debug("Logon failed: " + e);
// Log.logStackTrace(e);
// once we're shutdown we'll report this error
_logonError = e;
// terminate our communicator thread
shutdown();
}
}
protected void connect ()
throws IOException
{
// if we're already connected, we freak out
if (_channel != null) {
throw new IOException("Already connected.");
}
// look up the address of the target server
InetAddress host = InetAddress.getByName(_client.getHostname());
// obtain the list of available ports on which to attempt our
// client connection and determine our preferred port
String pportKey = _client.getHostname() + ".preferred_port";
int[] ports = _client.getPorts();
int pport = PresentsPrefs.config.getValue(pportKey, ports[0]);
int ppidx = Math.max(0, IntListUtil.indexOf(ports, pport));
// try connecting on each of the ports in succession
for (int ii = 0; ii < ports.length; ii++) {
int port = ports[(ii+ppidx)%ports.length];
int nextPort = ports[(ii+ppidx+1)%ports.length];
Log.info("Connecting [host=" + host + ", port=" + port + "].");
InetSocketAddress addr = new InetSocketAddress(host, port);
try {
_channel = SocketChannel.open(addr);
_prefPortInterval =
new PrefPortInterval(port, nextPort, pportKey);
_prefPortInterval.schedule(PREF_PORT_DELAY);
break;
} catch (IOException ioe) {
if (ioe instanceof ConnectException && ii < ports.length) {
_client.reportLogonTribulations(
new LogonException(
AuthCodes.TRYING_NEXT_PORT, true));
continue; // try the next port
}
throw ioe;
}
}
_channel.configureBlocking(true);
// our messages are framed (preceded by their length), so we
// use these helper streams to manage the framing
_fin = new FramedInputStream();
_fout = new FramingOutputStream();
// create our object input and output streams
_oin = new ObjectInputStream(_fin);
_oin.setClassLoader(_loader);
_oout = new ObjectOutputStream(_fout);
}
protected void logon ()
throws IOException, LogonException
{
// construct an auth request and send it
AuthRequest req = new AuthRequest(_client.getCredentials(),
_client.getVersion());
sendMessage(req);
// now wait for the auth response
Log.debug("Waiting for auth response.");
AuthResponse rsp = (AuthResponse)receiveMessage();
AuthResponseData data = rsp.getData();
Log.debug("Got auth response: " + data);
// if the auth request failed, we want to let the communicator
// know by throwing a logon exception
if (!data.code.equals(AuthResponseData.SUCCESS)) {
throw new LogonException(data.code);
}
// we're all clear. let the communicator know that we're in
logonSucceeded(data);
}
// now that we're authenticated, we manage the reading
// half of things by continuously reading messages from
// the socket and processing them
protected void iterate ()
{
DownstreamMessage msg = null;
try {
// read the next message from the socket
msg = receiveMessage();
// process the message
processMessage(msg);
} catch (InterruptedIOException iioe) {
// somebody set up us the bomb! we've been interrupted
// which means that we're being shut down, so we just
// report it and return from iterate() like a good monkey
Log.debug("Reader thread woken up in time to die.");
} catch (EOFException eofe) {
// let the communicator know that our connection was
// closed
connectionClosed();
// and shut ourselves down
shutdown();
} catch (IOException ioe) {
// let the communicator know that our connection failed
connectionFailed(ioe);
// and shut ourselves down
shutdown();
} catch (Exception e) {
Log.warning("Error processing message [msg=" + msg +
", error=" + e + "].");
}
}
protected void handleIterateFailure (Exception e)
{
Log.warning("Uncaught exception it reader thread.");
Log.logStackTrace(e);
}
protected void didShutdown ()
{
// If we haven't recorded a preferred port yet, instead do the
// failure action since we didn't stay connected long enough.
if (_prefPortInterval != null) {
_prefPortInterval.cancel();
_prefPortInterval.failed();
}
// let the communicator know when we finally go away
readerDidExit();
}
protected void kick ()
{
// we want to interrupt the reader thread as it may be blocked
// listening to the socket; this is only called if the reader
// thread doesn't shut itself down
// interrupt();
}
}
/**
* The writer encapsulates the message writing process. It calls back
* to the Communicator class to do things, but the
* general flow of the writer thread is encapsulated in this class.
*/
protected class Writer extends LoopingThread
{
protected void iterate ()
{
// fetch the next message from the queue
UpstreamMessage msg = (UpstreamMessage)_msgq.get();
// if this is a termination message, we're being
// requested to exit, so we want to bail now rather
// than continuing
if (msg instanceof TerminationMessage) {
return;
}
try {
// write the message out the socket
sendMessage(msg);
} catch (IOException ioe) {
// let the communicator know if we have any
// problems
connectionFailed(ioe);
// and bail
shutdown();
}
}
protected void handleIterateFailure (Exception e)
{
Log.warning("Uncaught exception it writer thread.");
Log.logStackTrace(e);
}
protected void didShutdown ()
{
writerDidExit();
}
protected void kick ()
{
// post a bogus message to the outgoing queue to ensure that
// the writer thread notices that it's time to go
postMessage(new TerminationMessage());
}
}
/** This is used to terminate the writer thread. */
protected static class TerminationMessage extends UpstreamMessage
{
}
protected class PrefPortInterval extends Interval
{
public PrefPortInterval (int thisPort, int nextPort, String key)
{
super();
_thisPort = thisPort;
_nextPort = nextPort;
_key = key;
}
public void expired () {
PresentsPrefs.config.setValue(_key, _thisPort);
_prefPortInterval = null;
}
public void failed () {
PresentsPrefs.config.setValue(_key, _nextPort);
_prefPortInterval = null;
}
protected String _key;
protected int _thisPort;
protected int _nextPort;
}
protected Client _client;
protected Reader _reader;
protected Writer _writer;
protected SocketChannel _channel;
protected Queue _msgq = new Queue();
protected long _lastWrite;
protected Exception _logonError;
/** We use this to frame our upstream messages. */
protected FramingOutputStream _fout;
protected ObjectOutputStream _oout;
/** We use this to frame our downstream messages. */
protected FramedInputStream _fin;
protected ObjectInputStream _oin;
protected ClientDObjectMgr _omgr;
protected ClassLoader _loader;
/** We use this interval to record the preferred port if it stays connected
* long enough. */
protected PrefPortInterval _prefPortInterval;
/** Time a port must remain connected before we mark it as preferred. */
protected static long PREF_PORT_DELAY = 5000L;
/** Used to control low-level message logging. */
protected static RuntimeAdjust.BooleanAdjust _logMessages =
new RuntimeAdjust.BooleanAdjust(
"Toggles whether or not all sent and received low-level " +
"network events are logged.", "narya.presents.log_events",
PresentsPrefs.config, false);
}