Files
narya/src/java/com/threerings/presents/client/Client.java
T
Michael Bayne c92a922678 The rabbit hole goes deeper. Now we can configure the client with a custom
classloader to use when unserializing objects off the network. Also fixed
the way custom classloaders were used as Class.forName(class, true,
loader) seems to be the proper way to go to have caching work and whatnot.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3268 542714f4-19e9-0310-aa3c-eee0fc999fb1
2004-12-10 01:23:44 +00:00

739 lines
24 KiB
Java

//
// $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 com.samskivert.util.Interval;
import com.samskivert.util.IntervalManager;
import com.samskivert.util.ObserverList;
import com.samskivert.util.RunAnywhere;
import com.threerings.presents.Log;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.net.AuthResponseData;
import com.threerings.presents.net.BootstrapData;
import com.threerings.presents.net.Credentials;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
/**
* Through the client object, a connection to the system is established
* and maintained. The client object maintains two separate threads (a
* reader and a writer) by which all network traffic is managed.
*/
public class Client
{
/** The default port on which the server listens for client
* connections. */
public static final int DEFAULT_SERVER_PORT = 4007;
/**
* This is used by the client to allow dobj event dispatching to take
* place along side the activities of the rest of the application
* (usually this means running dobj events on the AWT thread).
*/
public static interface Invoker
{
/**
* Requests that the supplied runnable be queued up for invocation
* on the main event dispatching thread of the application.
*/
public void invokeLater (Runnable run);
}
/**
* Constructs a client object with the supplied credentials and
* invoker. The creds will be used to authenticate with any server to
* which this client attempts to connect. The invoker is used to
* operate the distributed object event dispatch mechanism. To allow
* the dobj event dispatch to coexist with threads like the AWT
* thread, the client will request that the invoker queue up a
* runnable whenever there are distributed object events that need to
* be processed. The invoker can then queue that runnable up on the
* AWT thread if it is so inclined to make life simpler for the rest
* of the application.
*
* @param creds the credentials to use when logging on to the server.
* These can be null, but <code>setCredentials</code> must then be
* called before any call to <code>logon</code>.
* @param invoker an invoker that can be used to process incoming
* events.
*/
public Client (Credentials creds, Invoker invoker)
{
_creds = creds;
_invoker = invoker;
}
/**
* Registers the supplied observer with this client. While registered
* the observer will receive notifications of state changes within the
* client. The function will refuse to register an already registered
* observer.
*
* @see ClientObserver
* @see SessionObserver
*/
public void addClientObserver (SessionObserver observer)
{
synchronized (_observers) {
_observers.add(observer);
}
}
/**
* Unregisters the supplied observer. Upon return of this function,
* the observer will no longer receive notifications of state changes
* within the client.
*/
public void removeClientObserver (SessionObserver observer)
{
synchronized (_observers) {
_observers.remove(observer);
}
}
/**
* Configures the client to communicate with the server on the
* supplied hostname/port combination.
*
* @see #logon
*/
public void setServer (String hostname, int port)
{
_hostname = hostname;
_port = port;
}
/**
* Returns the invoker in use by this client. This can be used to
* queue up event dispatching stints.
*/
public Invoker getInvoker ()
{
return _invoker;
}
/**
* Returns the hostname of the server to which this client is
* currently configured to connect.
*/
public String getHostname ()
{
return _hostname;
}
/**
* Returns the port on which this client is currently configured to
* connect to the server.
*/
public int getPort ()
{
return _port;
}
/**
* Returns the credentials with which this client is currently
* configured to connect to the server.
*/
public Credentials getCredentials ()
{
return _creds;
}
/**
* Sets the credentials that will be used by this client to
* authenticate with the server. This should be done before any call
* to <code>logon</code>.
*/
public void setCredentials (Credentials creds)
{
_creds = creds;
}
/**
* Returns the version string configured for this client.
*/
public String getVersion ()
{
return _version;
}
/**
* Sets the version string reported to the server during
* authentication. Some server implementations may wish to refuse
* connections by old or invalid client versions.
*/
public void setVersion (String version)
{
_version = version;
}
/**
* Configures the client with a custom class loader which will be used
* when reading objects off of the network.
*/
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_comm != null) {
_comm.setClassLoader(loader);
}
}
/**
* Returns the data associated with our authentication response. Users
* of the Presents system may wish to communicate authentication
* related information to their client by extending and augmenting
* {@link AuthResponseData}.
*/
public AuthResponseData getAuthResponseData ()
{
return _authData;
}
/**
* Returns the distributed object manager associated with this
* session. This reference is only valid for the duration of the
* session and a new reference must be obtained if the client
* disconnects and reconnects to the server.
*
* @return the dobjmgr in effect or null if we have no established
* connection to the server.
*/
public DObjectManager getDObjectManager ()
{
return (_comm != null) ? _comm.getDObjectManager() : null;
}
/**
* Instructs the distributed object manager associated with this
* client to allow objects of the specified class to linger around the
* specified number of milliseconds after their last subscriber has
* been removed before the client finally removes its object proxy and
* flushes the object. Normally, objects are flushed immediately
* following the removal of their last subscriber.
*
* <p><em>Note:</em> the delay will be applied to derived classes as
* well as exact matches. <em>Note also:</em> this method cannot be
* called until after the client has established a connection with the
* server and the distributed object manager is available.
*/
public void registerFlushDelay (Class objclass, long delay)
{
ClientDObjectMgr omgr = (ClientDObjectMgr)getDObjectManager();
omgr.registerFlushDelay(objclass, delay);
}
/**
* Returns the oid of the client object associated with this session.
* It is only valid for the duration of the session.
*/
public int getClientOid ()
{
return _cloid;
}
/**
* Returns a reference to the client object associated with this
* session. It is only valid for the duration of the session.
*/
public ClientObject getClientObject ()
{
return _clobj;
}
/**
* Returns the invocation director associated with this session. This
* reference is only valid for the duration of the session.
*/
public InvocationDirector getInvocationDirector ()
{
return _invdir;
}
/**
* Returns the first bootstrap service that could be located that
* implements the supplied {@link InvocationService} derivation.
* <code>null</code> is returned if no such service could be found.
*/
public InvocationService getService (Class sclass)
{
if (_bstrap == null) {
return null;
}
int scount = _bstrap.services.size();
for (int ii = 0; ii < scount; ii++) {
InvocationService service = (InvocationService)
_bstrap.services.get(ii);
if (sclass.isInstance(service)) {
return service;
}
}
return null;
}
/**
* Like {@link #getService} except that a {@link RuntimeException} is
* thrown if the service is not available. Useful to avoid redundant
* error checking when you know that the shit will hit the fan if a
* particular invocation service is not available.
*/
public InvocationService requireService (Class sclass)
{
InvocationService isvc = getService(sclass);
if (isvc == null) {
throw new RuntimeException(
sclass.getName() + " isn't available. " +
"I can't bear to go on.");
}
return isvc;
}
/**
* Returns a reference to the bootstrap data provided to this client
* at logon time.
*/
public BootstrapData getBootstrapData ()
{
return _bstrap;
}
/**
* Converts a server time stamp to a value comparable to client clock
* readings.
*/
public long fromServerTime (long stamp)
{
// when we calcuated our time delta, we did it such that: C - S =
// dT, thus to convert server to client time we do: C = S + dT
return stamp + _serverDelta;
}
/**
* Converts a client clock reading to a value comparable to a server
* time stamp.
*/
public long toServerTime (long stamp)
{
// when we calcuated our time delta, we did it such that: C - S =
// dT, thus to convert server to client time we do: S = C - dT
return stamp - _serverDelta;
}
/**
* Returns true if we are logged on, false if we're not.
*/
public synchronized boolean isLoggedOn ()
{
// we're not "logged on" until we're fully done with the
// procedure, meaning we have a client object reference
return (_clobj != null);
}
/**
* Requests that this client connect and logon to the server with
* which it was previously configured.
*/
public synchronized void logon ()
{
// if we have a communicator reference, we're already logged on
if (_comm != null) {
return;
}
// otherwise create a new communicator instance and start it up.
// this will initiate the logon process
_comm = new Communicator(this);
_comm.setClassLoader(_loader);
_comm.logon();
// register an interval that we'll use to keep the clock synced
// and to send pings when appropriate
_piid = IntervalManager.register(new Interval() {
public void intervalExpired (int id, Object arg) {
if (id != _piid) {
IntervalManager.remove(id);
} else {
tick();
}
}
}, 5000L, null, true);
}
/**
* Requests that the client log off of the server to which it is
* connected.
*
* @param abortable If true, the client will call
* <code>clientWillDisconnect</code> on all of the client observers
* and abort the logoff process if any of them return false. If false,
* <code>clientWillDisconnect</code> will not be called at all.
*
* @return true if the logoff succeeded, false if it failed due to a
* disagreeable observer.
*/
public boolean logoff (boolean abortable)
{
// if we have no communicator, we're not logged on anyway
if (_comm == null) {
Log.warning("Ignoring request to logoff because we're not " +
"logged on.");
return true;
}
// if the request is abortable, let's run it past the observers
// before we act upon it
if (abortable && notifyObservers(CLIENT_WILL_LOGOFF, null)) {
return false;
}
// kill our tick interval
_piid = -1;
// ask the communicator to send a logoff message and disconnect
// from the server
_comm.logoff();
return true;
}
/**
* Called every five seconds; ensures that we ping the server if we
* haven't communicated in a long while and periodically resyncs the
* client and server clock deltas.
*/
protected void tick ()
{
// if we're not connected, skip it
if (_comm == null) {
return;
}
long now = RunAnywhere.currentTimeMillis();
if (_dcalc != null) {
// if our current calculator is done, clear it out
if (_dcalc.isDone()) {
Log.debug("Time offset from server: " + _serverDelta + "ms.");
_dcalc = null;
} else {
// otherwise, send another ping
PingRequest req = new PingRequest();
_comm.postMessage(req);
_dcalc.sentPing(req);
}
} else if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) {
// if we haven't sent anything over the network in a while, we
// ping the server to let it know that we're still alive
_comm.postMessage(new PingRequest());
} else if (now - _lastSync > CLOCK_SYNC_INTERVAL) {
// resync our clock with the server
establishClockDelta(now);
}
}
/**
* Called during initialization to initiate a sequence of ping/pong
* messages which will be used to determine (with "good enough"
* accuracy) the difference between the client clock and the server
* clock so that we can later interpret server timestamps.
*/
protected void establishClockDelta (long now)
{
// create a new delta calculator and start the process
_dcalc = new DeltaCalculator();
PingRequest req = new PingRequest();
_comm.postMessage(req);
_dcalc.sentPing(req);
_lastSync = now;
}
/**
* Called by the invocation director when it successfully subscribes
* to the client object immediately following logon.
*/
protected void gotClientObject (ClientObject clobj)
{
// keep this around
_clobj = clobj;
// let the client know that logon has now fully succeeded
notifyObservers(Client.CLIENT_DID_LOGON, null);
}
/**
* Called by the invocation director if it fails to subscribe to the
* client object after logon.
*/
protected void getClientObjectFailed (Exception cause)
{
// pass the buck onto the listeners
notifyObservers(Client.CLIENT_FAILED_TO_LOGON, cause);
}
/**
* Called by the invocation director when it discovers that the client
* object has changed.
*/
protected void clientObjectDidChange (ClientObject clobj)
{
_clobj = clobj;
_cloid = _clobj.getOid();
// report to our observers
notifyObservers(Client.CLIENT_OBJECT_CHANGED, null);
}
boolean notifyObservers (int code, Exception cause)
{
final Notifier noty = new Notifier(code, cause);
Runnable unit = new Runnable() {
public void run () {
synchronized (_observers) {
_observers.apply(noty);
}
}
};
// we need to run immediately if this is WILL_LOGOFF or if we have
// no invoker (which currently only happens in some really obscure
// circumstances where we're using a Client instance on the server
// so that we can sort of pretend to be a real client)
if (code == CLIENT_WILL_LOGOFF || _invoker == null) {
unit.run();
return noty.getRejected();
} else {
// otherwise we can queue this notification up with our
// invoker and ensure that it's run on the proper thread
_invoker.invokeLater(unit);
return false;
}
}
synchronized void communicatorDidExit ()
{
// we know that prior to the call to this method, the observers
// were notified with CLIENT_DID_LOGOFF; that may not have been
// invoked yet, so we don't want to clear out our communicator
// reference immediately; instead we queue up an invoker unit to
// do so to ensure that it won't happen until CLIENT_DID_LOGOFF
// was dispatched
_invoker.invokeLater(new Runnable() {
public void run () {
// clear out our references
_comm = null;
_clobj = null;
_cloid = -1;
// and let our invocation director know we're logged off
_invdir.cleanup();
}
});
}
/**
* Called by the omgr when a bootstrap notification arrives.
*/
void gotBootstrap (BootstrapData data)
{
Log.debug("Got bootstrap " + data + ".");
// keep this around for interested parties
_bstrap = data;
// extract bootstrap information
_cloid = data.clientOid;
// initialize our invocation director
_invdir.init(_comm.getDObjectManager(), _cloid, this);
// send a few pings to the server to establish the clock offset
// between this client and server standard time
establishClockDelta(System.currentTimeMillis());
// we can't quite call initialization completed at this point
// because we need for the invocation director to fully initialize
// (which requires a round trip to the server) before turning the
// client loose to do things like request invocation services
}
/**
* Called when we receive a pong packet. We may be in the process of
* calculating the client/server time differential, or we may have
* already done that at which point we ignore pongs.
*/
void gotPong (PongResponse pong)
{
// if we're not calculating our client/server time delta, then we
// don't need to do anything with the pong
if (_dcalc == null) {
return;
}
// we update the delta after every receipt so as to immediately
// obtain an estimate of the clock delta and then refine it as
// more packets come in
_dcalc.gotPong(pong);
_serverDelta = _dcalc.getTimeDelta();
}
/**
* Used to notify client observers of events.
*/
protected class Notifier
implements ObserverList.ObserverOp
{
public Notifier (int code, Exception cause)
{
_code = code;
_cause = cause;
_rejected = false;
}
public boolean getRejected ()
{
return _rejected;
}
public boolean apply (Object observer)
{
SessionObserver obs = (SessionObserver)observer;
switch (_code) {
case CLIENT_DID_LOGON:
obs.clientDidLogon(Client.this);
break;
case CLIENT_FAILED_TO_LOGON:
if (obs instanceof ClientObserver) {
((ClientObserver)obs).clientFailedToLogon(
Client.this, _cause);
}
break;
case CLIENT_OBJECT_CHANGED:
if (obs instanceof SessionObserver) {
((SessionObserver)obs).clientObjectDidChange(Client.this);
}
break;
case CLIENT_CONNECTION_FAILED:
if (obs instanceof ClientObserver) {
((ClientObserver)obs).clientConnectionFailed(
Client.this, _cause);
}
break;
case CLIENT_WILL_LOGOFF:
if (obs instanceof ClientObserver) {
if (!((ClientObserver)obs).clientWillLogoff(Client.this)) {
_rejected = true;
}
}
break;
case CLIENT_DID_LOGOFF:
obs.clientDidLogoff(Client.this);
break;
default:
throw new RuntimeException("Invalid code supplied to " +
"notifyObservers: " + _code);
}
return true;
}
protected int _code;
protected Exception _cause;
protected boolean _rejected;
}
/** The credentials we used to authenticate with the server. */
protected Credentials _creds;
/** The version string reported to the server at auth time. */
protected String _version = "";
/** An entity that gives us the ability to process events on the main
* client thread (which is also the AWT thread). */
protected Invoker _invoker;
/** The data associated with our authentication response. */
protected AuthResponseData _authData;
/** Our client distribted object id. */
protected int _cloid = -1;
/** Our client distributed object. */
protected ClientObject _clobj;
/** The game server host. */
protected String _hostname;
/** The port on which we connect to the game server. */
protected int _port;
/** Our list of client observers. */
protected ObserverList _observers =
new ObserverList(ObserverList.SAFE_IN_ORDER_NOTIFY);
/** The entity that manages our network communications. */
protected Communicator _comm;
/** A custom class loader used to load objects that come in over the
* network. */
protected ClassLoader _loader = getClass().getClassLoader();
/** General startup information provided by the server. */
protected BootstrapData _bstrap;
/** Manages invocation services. */
protected InvocationDirector _invdir = new InvocationDirector();
/** The difference between the server clock and the client clock
* (estimated immediately after logging on). */
protected long _serverDelta;
/** Used when establishing our clock delta between the client and
* server. */
protected DeltaCalculator _dcalc;
/** The last time at which we synced our clock with the server. */
protected long _lastSync;
/** Our tick interval id. */
protected int _piid = -1;
/** How often we recompute our time offset from the server. */
protected static final long CLOCK_SYNC_INTERVAL = 600 * 1000L;
// client observer codes
static final int CLIENT_DID_LOGON = 0;
static final int CLIENT_FAILED_TO_LOGON = 1;
static final int CLIENT_OBJECT_CHANGED = 2;
static final int CLIENT_CONNECTION_FAILED = 3;
static final int CLIENT_WILL_LOGOFF = 4;
static final int CLIENT_DID_LOGOFF = 5;
}