// // $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; import java.net.InetAddress; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.io.IOException; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.samskivert.util.IntMap; import com.samskivert.util.IntMaps; import com.samskivert.util.ResultListener; import com.samskivert.util.Throttle; import com.threerings.util.Name; import com.threerings.presents.annotation.AnyThread; import com.threerings.presents.annotation.EventThread; import com.threerings.presents.client.Client; import com.threerings.presents.data.ClientObject; import com.threerings.presents.dobj.DEvent; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.ObjectAccessException; import com.threerings.presents.dobj.ProxySubscriber; import com.threerings.presents.net.AuthRequest; import com.threerings.presents.net.BootstrapData; import com.threerings.presents.net.BootstrapNotification; import com.threerings.presents.net.Credentials; import com.threerings.presents.net.DownstreamMessage; 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; import com.threerings.presents.net.SubscribeRequest; import com.threerings.presents.net.ThrottleUpdatedMessage; import com.threerings.presents.net.TransmitDatagramsRequest; import com.threerings.presents.net.UnsubscribeRequest; import com.threerings.presents.net.UnsubscribeResponse; import com.threerings.presents.net.UpdateThrottleMessage; import com.threerings.presents.server.net.Connection; import com.threerings.presents.server.net.ConnectionManager; import static com.threerings.presents.Log.log; /** * Represents a client session in the server. It is associated with a connection instance (while * the client is connected) and acts as the intermediary for the remote client in terms of passing * along events forwarded by the client, ensuring that subscriptions are maintained on behalf of * the client and that events are forwarded to the client. * *
A note on synchronization: the client object is structured so that its
* Note: this means that a hacked client can refuse to ACK message rate reductions
* and continue to use the most generous rate ever assigned to it. Don't increase the throttle
* beyond the default for untrusted clients. This mechanism exists so that trusted clients can
* have their throttle relaxed in a robust manner which will not result in disconnects if the
* client happens to be at or near the throttle limit when the throttle is reduced.
*/
@EventThread
public void setIncomingMessageThrottle (int messagesPerSec)
{
synchronized(_pendingThrottles) {
_pendingThrottles.add(messagesPerSec);
}
postMessage(new UpdateThrottleMessage(messagesPerSec));
// when we get a ThrottleUpdatedMessage from the client, we'll apply the new throttle
}
/**
* Danger: this method is not for general consumption. This changes the username of
* the client, but should only be done very early in a user's session, when you know that no
* one has mapped the user based on their username or has in any other way made use of their
* username in a way that will break. However, it should not be done too early in the
* session. The client must be fully resolved.
*
* It exists to support systems wherein a user logs in with an account username and then
* chooses a "screen name" by which they will play (often from a small set of available
* "characters" available per account). This will take care of remapping the username to client
* object mappings that were made by the Presents services when the user logs on, but anything
* else that has had its grubby mits on the username will be left to its own devices, hence the
* care that must be exercised when using this method.
*
* @param ucl an entity that will (optionally) be notified when the username conversion process
* is complete.
*/
public void setUsername (Name username, final UserChangeListener ucl)
{
ClientResolutionListener clr = new ClientResolutionListener() {
public void clientResolved (final Name username, final ClientObject clobj) {
// if they old client object is gone by now, they ended their session while we were
// switching, so freak out
if (_clobj == null) {
log.warning("Client disappeared before we could complete the switch to a " +
"new client object", "ousername", _username, "nusername", username);
_clmgr.releaseClientObject(username);
Exception error = new Exception("Early withdrawal");
resolutionFailed(username, error);
return;
}
// call down to any derived classes
clientObjectWillChange(_clobj, clobj);
// let the caller know that we've got some new business
if (ucl != null) {
ucl.changeReported(clobj, new ResultListener Note: This function will be called on the dobjmgr thread which means that object
* manipulations are OK, but client instance manipulations must done carefully.
*/
@EventThread
protected void sessionWillStart ()
{
// configure a specific access controller for the client object
_clobj.setAccessController(PresentsObjectAccess.CLIENT);
}
/**
* Called when the client resumes a session (after having disconnected and reconnected). After
* this method is executed, the bootstrap information will be sent to the client which will
* trigger the resumption of the session. Derived classes that override this method should be
* sure to call Note: This function will be called on the dobjmgr thread which means that object
* manipulations are OK, but client instance manipulations must done carefully.
*/
@EventThread
protected void sessionWillResume ()
{
}
/**
* Called when the client session ends (either because the client logged off or because the
* server forcibly terminated the session). Derived classes that override this method should
* be sure to call Note: This function will be called on the dobjmgr thread which means that object
* manipulations are OK, but client instance manipulations must done carefully.
*/
@EventThread
protected void sessionDidEnd ()
{
// clear out our subscriptions so that we don't get a complaint about inability to forward
// the object destroyed event we're about to generate
clearSubscrips(false);
}
/**
* Called to inform derived classes when the client has subscribed to a distributed object.
*/
@EventThread
protected void subscribedToObject (DObject object)
{
}
/**
* Called to inform derived classes when the client has unsubscribed from a distributed object.
*/
@EventThread
protected void unsubscribedFromObject (DObject object)
{
}
/**
* This is called once we have a handle on the client distributed object. It sends a bootstrap
* notification to the client with all the information it will need to interact with the
* server.
*/
protected void sendBootstrap ()
{
// log.info("Sending bootstrap " + this + ".");
// create and populate our bootstrap data
BootstrapData data = createBootstrapData();
populateBootstrapData(data);
// create a send bootstrap notification
postMessage(new BootstrapNotification(data));
}
/**
* Derived client classes can override this member to create derived bootstrap data classes
* that contain extra bootstrap information, if desired.
*/
protected BootstrapData createBootstrapData ()
{
return new BootstrapData();
}
/**
* Derived client classes can override this member to populate the bootstrap data with
* additional information. They should be sure to call Note: This function will be called on the dobjmgr thread which means that object
* manipulations are OK, but client instance manipulations must be done carefully.
*/
protected void populateBootstrapData (BootstrapData data)
{
// give them the connection id
Connection conn = getConnection();
if (conn != null) {
data.connectionId = conn.getConnectionId();
} else {
log.warning("Connection disappeared before we could send bootstrap response.",
"client", this);
return; // stop here as we're just going to throw away this bootstrap
}
// and the client object id
data.clientOid = _clobj.getOid();
// fill in the list of bootstrap services
if (_areq.getBootGroups() == null) {
log.warning("Client provided no invocation service boot groups? " + this);
data.services = Lists.newArrayList();
} else {
data.services = _invmgr.getBootstrapServices(_areq.getBootGroups());
}
}
/**
* Called by the connection manager when this client's connection is unmapped. That may be
* because of a connection failure (in which case this call will be followed up by a call to
* Subscriber implementation (which is called from the dobjmgr thread) can proceed
* without synchronization. This does not overlap with its other client duties which are called
* from the conmgr thread and therefore also need not be synchronized.
*/
public class PresentsSession
implements Connection.MessageHandler, ClientResolutionListener
{
/** Used by {@link PresentsSession#setUsername} to report success or failure. */
public static interface UserChangeListener
{
/** Called when the new client object has been resolved and the new client object reported
* to the client, but the old one has not yet been destroyed. Any events delivered on this
* callback to the old client object will be delivered.
*
* @param rl when this method is finished with its business and the old client object can
* be destroyed, the result listener should be called. */
void changeReported (ClientObject newObji, ResultListenersuper.sessionWillStart.
*
* super.sessionWillResume.
*
* super.sessionDidEnd.
*
* super.populateBootstrapData
* before doing their own populating, however.
*
* connectionFailed) or it may be because of an orderly closing of the
* connection. In either case, the client can deal with its lack of a connection in this
* method. This is invoked by the conmgr thread and should behave accordingly.
*/
protected void wasUnmapped ()
{
// clear out our connection reference
setConnection(null);
// if we are being closed after the omgr has shutdown, then just stop here; the whole world
// is about to come to a screeching halt anyway
if (_omgr.isRunning()) {
// clear out our subscriptions: we need to do this on the dobjmgr thread. it is
// important that we do this *after* we clear out our connection reference. once the
// connection ref is null, no more subscriptions will be processed (even those that
// were queued up before the connection went away)
_omgr.postRunnable(new Runnable() {
public void run () {
sessionConnectionClosed();
}
});
}
}
/**
* Called on the dobjmgr thread when the connection associated with this session has been
* closed and unmapped. If the user logged off before closing their connection, this will be
* preceded by a call to {@link #sessionDidEnd}.
*/
@EventThread
protected void sessionConnectionClosed ()
{
// clear out our dobj subscriptions in case they weren't cleared by a call to sessionDidEnd
clearSubscrips(false);
}
/**
* Called by the connection manager when this client's connection fails. This is invoked on the
* conmgr thread and should behave accordingly.
*/
protected void connectionFailed (IOException fault)
{
// nothing to do here, the client manager already complained about the failed connection
}
/**
* Sets our connection reference in a thread safe way. Also establishes the back reference to
* us as the connection's message handler.
*/
protected synchronized void setConnection (Connection conn)
{
// if our connection is being cleared out, record the amount of time we were connected to
// our total connected time
long now = System.currentTimeMillis();
if (_conn != null && conn == null) {
_connectTime += ((now - _networkStamp) / 1000);
}
// keep a handle to the new connection
_conn = conn;
// tell the connection to pass messages on to us (if we're setting a connection rather than
// clearing one out)
if (_conn != null) {
_conn.setMessageHandler(this);
// configure any active custom class loader
if (_loader != null) {
_conn.setClassLoader(_loader);
}
}
// make a note that our network status changed
_networkStamp = now;
}
/**
* The connection instance must be accessed via this member function because it is read from
* both the dobjmgr and conmgr threads and is modified by the conmgr thread.
*
* @return The connection instance associated with this client or null if the client is not
* currently connected.
*/
protected synchronized Connection getConnection ()
{
return _conn;
}
/**
* Callable from non-dobjmgr thread, this queues up a runnable on the dobjmgr thread to post
* the supplied message to this client.
*/
protected final void safePostMessage (final DownstreamMessage msg)
{
_omgr.postRunnable(new Runnable() {
public void run () {
postMessage(msg);
}
});
}
/** Queues a message for delivery to the client. */
protected boolean postMessage (DownstreamMessage msg)
{
Connection conn = getConnection();
if (conn != null) {
conn.postMessage(msg);
_messagesOut++; // count 'em up!
return true;
}
// don't log dropped messages unless we're dropping a lot of them (meaning something is
// still queueing messages up for this dead client even though it shouldn't be)
if (++_messagesDropped % 50 == 0) {
log.warning("Dropping many messages?", "client", this, "count", _messagesDropped);
}
// make darned sure we don't have any remaining subscriptions
if (_subscrips.size() > 0) {
// log.warning("Clearing stale subscriptions", "client", this,
// "subscrips", _subscrips.size());
clearSubscrips(_messagesDropped > 10);
}
return false;
}
/**
* Notifies this client that its throttle was updated.
*/
protected void throttleUpdated ()
{
int messagesPerSec;
synchronized(_pendingThrottles) {
if (_pendingThrottles.size() == 0) {
log.warning("Received throttleUpdated but have no pending throttles",
"client", this);
return;
}
messagesPerSec = _pendingThrottles.remove(0);
}
log.info("Applying updated throttle", "client", this, "msgsPerSec", messagesPerSec);
// We set our hard throttle over a 10 second period instead of a 1 second period to
// account for periods of network congestion that might cause otherwise properly
// throttled messages to bunch up while they're "on the wire"; we also add a one
// message buffer so that if the client is right up against the limit, we don't end
// up quibbling over a couple of milliseconds
_throttle.reinit(10*messagesPerSec+1, 10*1000L);
}
@Override
public String toString ()
{
StringBuilder buf = new StringBuilder("[");
toString(buf);
return buf.append("]").toString();
}
protected String who ()
{
return (_username == null) ? "null" :
(_username.getClass().getSimpleName() + "(" + _username + ")");
}
/**
* Returns the duration (in milliseconds) after which a disconnected session will be terminated
* and flushed.
*/
protected long getFlushTime ()
{
return DEFAULT_FLUSH_TIME;
}
/**
* Derived classes override this to augment stringification.
*/
protected void toString (StringBuilder buf)
{
buf.append("who=").append(who());
buf.append(", conn=").append(getConnection());
buf.append(", in=").append(_messagesIn);
buf.append(", out=").append(_messagesOut);
}
/**
* Creates a properly initialized inner-class proxy subscriber.
*/
protected ClientProxy createProxySubscriber ()
{
return new ClientProxy();
}
/** Used to track information about an object subscription. */
protected class ClientProxy implements ProxySubscriber
{
public DObject object;
public void unsubscribe ()
{
object.removeSubscriber(this);
unsubscribedFromObject(object);
}
// from interface ProxySubscriber
public void objectAvailable (DObject dobj)
{
if (postMessage(new ObjectResponse