// // $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.util.HashSet; import com.samskivert.util.Interval; import com.samskivert.util.ObserverList; import com.samskivert.util.RunAnywhere; import com.samskivert.util.RunQueue; import com.threerings.presents.Log; import com.threerings.presents.data.ClientObject; import com.threerings.presents.data.InvocationCodes; 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 ports on which the server listens for client connections. */ public static final int[] DEFAULT_SERVER_PORTS = { 47624 }; /** * Constructs a client object with the supplied credentials and RunQueue. The creds will be * used to authenticate with any server to which this client attempts to connect. The RunQueue * 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 * RunQueue queue up a runnable whenever there are distributed object events that need to be * processed. The RunQueue 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 * setCredentials must then be called before any call to logon. * @param runQueue a RunQueue that can be used to process incoming events. */ public Client (Credentials creds, RunQueue runQueue) { _creds = creds; _runQueue = runQueue; } /** * 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); } } /** * Sets whether or not this client is operating in standalone mode. */ public void setStandalone (boolean standalone) { _standalone = standalone; } /** * Checks whether or not this client is operating in a standalone mode. */ public boolean isStandalone () { return _standalone; } /** * Configures the client to communicate with the server on the supplied hostname and set of * ports (which will be tried in succession). * * @see #logon */ public void setServer (String hostname, int[] ports) { _hostname = hostname; _ports = ports; } /** * Returns the RunQueue in use by this client. This can be used to queue up event dispatching * stints. */ public RunQueue getRunQueue () { return _runQueue; } /** * 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[] getPorts () { return _ports; } /** * 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 logon. */ 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 _omgr; } /** * 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. * *

Note: the delay will be applied to derived classes as well as exact * matches. Note also: 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; } /** * Marks this client as interested in the specified bootstrap services group. Any services * registered as bootstrap services with the supplied group name will be included in this * clients bootstrap services set. This must be called before {@link #logon}. */ public void addServiceGroup (String group) { _bootGroups.add(group); } /** * Returns the first bootstrap service that could be located that implements the supplied * {@link InvocationService} derivation. null 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 in active communication (we may not yet be logged on, but we could be * trying to log on). */ public synchronized boolean isActive () { // if we have a communicator, we're doing something return (_comm != null); } /** * 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. * * @return false if we're already logged on, true if a logon attempt was initiated. */ public synchronized boolean logon () { // if we have a communicator reference, we're already logged on if (_comm != null) { return false; } // notify our observers notifyObservers(CLIENT_WILL_LOGON, null); // 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 if (_tickInterval == null) { _tickInterval = new Interval() { public void expired () { tick(); } }; _tickInterval.schedule(5000L, true); } return true; } /** * Requests that the client log off of the server to which it is connected. * * @param abortable If true, the client will call clientWillDisconnect on all of * the client observers and abort the logoff process if any of them return false. If false, * clientWillDisconnect 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 _tickInterval.cancel(); _tickInterval = null; // ask the communicator to send a logoff message and disconnect from the server _comm.logoff(); return true; } /** * For standalone mode, this notifies observers that the client has logged off and cleans up. */ public void standaloneLogoff () { notifyObservers(CLIENT_DID_LOGOFF, null); cleanup(null); } /** * Called by the {@link ClientDObjectMgr} when our bootstrap notification arrives. If the * client and server are being run in "merged" mode in a single JVM, this is how the client is * configured with the server's distributed object manager and provided with bootstrap data. */ public void gotBootstrap (BootstrapData data, DObjectManager omgr) { Log.debug("Got bootstrap " + data + "."); // keep these around for interested parties _bstrap = data; _omgr = omgr; // extract bootstrap information _cloid = data.clientOid; // initialize our invocation director _invdir.init(omgr, _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 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 if (_dcalc.shouldSendPing()) { // 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) { if (_comm != null) { // 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 {@link Communicator} if it is experiencing trouble logging on but is still * trying fallback strategies. */ protected void reportLogonTribulations (final LogonException cause) { _runQueue.postRunnable(new Runnable() { public void run () { notifyObservers(CLIENT_FAILED_TO_LOGON, cause); } }); } /** * 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_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_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_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 RunQueue (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 || _runQueue == null) { unit.run(); return noty.getRejected(); } else { // otherwise we can queue this notification up with our RunQueue and ensure that it's // run on the proper thread _runQueue.postRunnable(unit); return false; } } synchronized void cleanup (final Exception logonError) { // 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 a runnable unit to do so to // ensure that it won't happen until CLIENT_DID_LOGOFF was dispatched _runQueue.postRunnable(new Runnable() { public void run () { // clear out our references _comm = null; _omgr = null; _clobj = null; _cloid = -1; _standalone = false; // and let our invocation director know we're logged off _invdir.cleanup(); // if we were cleaned up due to a failure to logon, we can report the logon error // now that the communicator is cleaned up; this allows a logon failure listener to // immediately try another logon (hopefully with something changed like the server // or port) if (logonError != null) { notifyObservers(CLIENT_FAILED_TO_LOGON, logonError); } else { notifyObservers(CLIENT_DID_CLEAR, null); } } }); } /** * 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 currently calculating our delta, then we can throw away the pong if (_dcalc != null) { // 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 (SessionObserver obs) { switch (_code) { case CLIENT_WILL_LOGON: obs.clientWillLogon(Client.this); break; 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; case CLIENT_DID_CLEAR: if (obs instanceof ClientObserver) { ((ClientObserver)obs).clientDidClear(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. */ protected RunQueue _runQueue; /** The distributed object manager we're using during this session. */ protected DObjectManager _omgr; /** 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; /** Whether or not this client is operating in a standalone mode. */ protected boolean _standalone; /** The game server host. */ protected String _hostname; /** The ports on which we connect to the game server. */ protected int[] _ports; /** 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; /** The set of bootstrap service groups this client cares about. */ protected HashSet _bootGroups = new HashSet(); { _bootGroups.add(InvocationCodes.GLOBAL_GROUP); } /** 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 Interval _tickInterval; /** 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_WILL_LOGON = 0; static final int CLIENT_DID_LOGON = 1; static final int CLIENT_FAILED_TO_LOGON = 2; static final int CLIENT_OBJECT_CHANGED = 3; static final int CLIENT_CONNECTION_FAILED = 4; static final int CLIENT_WILL_LOGOFF = 5; static final int CLIENT_DID_LOGOFF = 6; static final int CLIENT_DID_CLEAR = 7; }