Files
narya/src/java/com/threerings/presents/client/Communicator.java
T
Michael Bayne f0d7b8642b Need to ensure that we don't claim that the communicator is gone until
after both the reader and writer have exited (otherwise the reader could
exit before the writer which would result in the client chucking its
reference to the communicator which could subsequently bite us in the ass
if anyone tried to reference the client's distributed object manager when
the writer finally exited and triggered all of the clientDidLogoff
callbacks). Multithreaded programming is fun!


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1396 542714f4-19e9-0310-aa3c-eee0fc999fb1
2002-05-27 01:37:07 +00:00

502 lines
16 KiB
Java

//
// $Id: Communicator.java,v 1.19 2002/05/27 01:37:07 mdb Exp $
package com.threerings.presents.client;
import java.io.*;
import java.net.Socket;
import java.net.InetAddress;
import com.samskivert.util.LoopingThread;
import com.samskivert.util.Queue;
import com.threerings.presents.Log;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.io.*;
import com.threerings.presents.io.ObjectStreamException;
import com.threerings.presents.net.*;
/**
* The client performs all network I/O on separate threads (one for
* reading and one for writing). The communicator class encapsulates that
* functionality.
*
* <pre>
* 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
* </pre>
*/
public class Communicator
{
/**
* Creates a new communicator instance which is associated with the
* supplied client.
*/
public Communicator (Client client)
{
_client = client;
}
/**
* Returns the distributed object manager in effect for this session.
* This instance is only valid while the client is connected to the
* server.
*/
public DObjectManager getDObjectManager ()
{
return _omgr;
}
/**
* 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 (_socket == 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);
}
/**
* 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();
// 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 (_socket == null) {
return;
}
Log.debug("Connection failed: " + 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 (_socket == 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;
// let the client know when we finally go away
if (_writer == null) {
_client.communicatorDidExit();
}
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.");
// 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
try {
_socket.close();
} catch (IOException cle) {
Log.warning("Error closing failed socket: " + cle);
}
_socket = null;
// let the client observers know that we're logged off
_client.notifyObservers(Client.CLIENT_DID_LOGOFF, null);
// let the client know when we finally go away
if (_reader == null) {
_client.communicatorDidExit();
}
}
/**
* Writes the supplied message to the socket.
*/
protected void sendMessage (UpstreamMessage msg)
throws IOException
{
// first we flatten the message so that we can measure it's length
TypedObjectFactory.writeTo(_dout, msg);
// then write the framed message to actual output stream
_fout.writeFrameAndReset(_out);
}
/**
* 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(_in));
// then use the typed object factory to read and decode the
// proper downstream message instance
return (DownstreamMessage)TypedObjectFactory.readFrom(_din);
}
/**
* 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)
{
// Log.info("Process msg: " + 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 <code>Communicator</code> 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);
// let the observers know that we've failed
_client.notifyObservers(Client.CLIENT_FAILED_TO_LOGON, e);
// and terminate our communicator thread
shutdown();
}
}
protected void connect ()
throws IOException
{
// if we're already connected, we freak out
if (_socket != null) {
throw new IOException("Already connected.");
}
// look up the address of the target server
InetAddress host = InetAddress.getByName(_client.getHostname());
int port = _client.getPort();
// establish a socket connection to said server
Log.debug("Connecting to server [host=" + host +
", port=" + port + "].");
_socket = new Socket(host, port);
// get a handle on our input and output streams
_in = _socket.getInputStream();
_out = _socket.getOutputStream();
// our messages are framed (preceded by their length), so we
// use these helper streams to manage the framing
_fin = new FramedInputStream();
_din = new DataInputStream(_fin);
_fout = new FramingOutputStream();
_dout = new DataOutputStream(_fout);
}
protected void logon ()
throws IOException, LogonException
{
// construct an auth request and send it
AuthRequest req = new AuthRequest(_client.getCredentials());
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 (ObjectStreamException ose) {
Log.warning("Error decoding message: " + ose);
Log.logStackTrace(ose);
} 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 ()
{
// 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 <code>Communicator</code> 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
{
public short getType ()
{
return -1;
}
}
protected Client _client;
protected Reader _reader;
protected Writer _writer;
protected Socket _socket;
protected InputStream _in;
protected OutputStream _out;
protected Queue _msgq = new Queue();
/** We use this to frame our upstream messages. */
protected FramingOutputStream _fout;
protected DataOutputStream _dout;
/** We use this to frame our downstream messages. */
protected FramedInputStream _fin;
protected DataInputStream _din;
protected ClientDObjectMgr _omgr;
}