From 2f1a67077a552fa713a6a8ca8138a8d48fe04319 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 28 May 2002 21:56:38 +0000 Subject: [PATCH] Added code to establish the time delta between the client and server clocks immediately following authentication. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1398 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../threerings/presents/client/Client.java | 156 +++++++++++++++--- .../presents/client/ClientDObjectMgr.java | 4 +- .../presents/client/DeltaCalculator.java | 91 ++++++++++ .../presents/net/BootstrapData.java | 5 +- .../threerings/presents/net/PingRequest.java | 54 +++++- .../threerings/presents/net/PongResponse.java | 103 +++++++++++- .../presents/server/PresentsClient.java | 5 +- 7 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 src/java/com/threerings/presents/client/DeltaCalculator.java diff --git a/src/java/com/threerings/presents/client/Client.java b/src/java/com/threerings/presents/client/Client.java index f1aa2511f..58e3dd0d9 100644 --- a/src/java/com/threerings/presents/client/Client.java +++ b/src/java/com/threerings/presents/client/Client.java @@ -1,5 +1,5 @@ // -// $Id: Client.java,v 1.22 2002/04/10 06:08:59 mdb Exp $ +// $Id: Client.java,v 1.23 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.client; @@ -13,6 +13,8 @@ import com.threerings.presents.data.ClientObject; import com.threerings.presents.dobj.DObjectManager; 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 @@ -202,6 +204,28 @@ public class Client 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 server tick stamp (which is the number of milliseconds + * since the server started up) to an absolute client timestamp. + */ + public long fromServerTicks (int ticks) + { + // we already have our adjusted server start stamp, so we just add + // the milliseconds since that time to get absolute time + return _serverStartStamp + ticks; + } + /** * Returns true if we are logged on, false if we're not. */ @@ -263,6 +287,50 @@ public class Client return true; } + /** + * 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 () + { + // create a new delta calculator and start the process + _dcalc = new DeltaCalculator(); + PingRequest req = new PingRequest(); + _comm.postMessage(req); + _dcalc.sentPing(req); + Log.info("Sent ping."); + } + + /** + * This is called when we've completed the process of pinging the + * server a few times to establish our clock delta. + */ + protected void clockDeltaEstablished () + { + // initialize our invocation director + ResultListener rl = new ResultListener() { + public void requestCompleted (Object result) { + // keep this around + _clobj = (ClientObject)result; + // let the client know that logon has now fully succeeded + notifyObservers(Client.CLIENT_DID_LOGON, null); + } + + public void requestFailed (Exception cause) { + // pass the buck onto the listeners + notifyObservers(Client.CLIENT_FAILED_TO_LOGON, cause); + } + }; + _invdir.init(_comm.getDObjectManager(), _cloid, _bstrap.invOid, rl); + + // 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 + } + boolean notifyObservers (int code, Exception cause) { boolean rejected = false; @@ -320,33 +388,65 @@ public class Client // extract bootstrap information _cloid = data.clientOid; - // initialize our invocation director - ResultListener rl = new ResultListener() { - public void requestCompleted (Object result) { - // keep this around - _clobj = (ClientObject)result; - // let the client know that logon has now fully succeeded - notifyObservers(Client.CLIENT_DID_LOGON, null); - } + // keep track of our server start time + _serverStartStamp = data.serverStartStamp; - public void requestFailed (Exception cause) { - // pass the buck onto the listeners - notifyObservers(Client.CLIENT_FAILED_TO_LOGON, cause); - } - }; - _invdir.init(_comm.getDObjectManager(), _cloid, data.invOid, rl); - - // 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 + // send a few pings to the server to establish the clock offset + // between this client and server standard time + establishClockDelta(); } + /** + * 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) + { + Log.info("Got 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; + } + + // if we are calculating, we'll either be sending another ping... + if (_dcalc.gotPong(pong)) { + PingRequest req = new PingRequest(); + _comm.postMessage(req); + _dcalc.sentPing(req); + + } else { + // ...or we're done so we can grab the time delta and finish + // our business + _serverDelta = _dcalc.getTimeDelta(); + // adjust our server start stamp into client time + _serverStartStamp += _serverDelta; + // free up our delta calculator + _dcalc = null; + // let the client continue with its initialization + clockDeltaEstablished(); + } + } + + /** The credentials we used to authenticate with the server. */ protected Credentials _creds; + + /** An entity that gives us the ability to process events on the main + * client thread (which is also the AWT thread). */ protected Invoker _invoker; + + /** Our client distribted object id. */ + protected int _cloid; + + /** 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. */ @@ -355,10 +455,24 @@ public class Client /** The entity that manages our network communications. */ protected Communicator _comm; + /** General startup information provided by the server. */ protected BootstrapData _bstrap; - protected int _cloid; + + /** 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; + + /** The time from which server ticks are computed (converted into + * client time. */ + protected long _serverStartStamp; + + /** Used when establishing our clock delta between the client and + * server. */ + protected DeltaCalculator _dcalc; + // client observer codes static final int CLIENT_DID_LOGON = 0; static final int CLIENT_FAILED_TO_LOGON = 1; diff --git a/src/java/com/threerings/presents/client/ClientDObjectMgr.java b/src/java/com/threerings/presents/client/ClientDObjectMgr.java index e52fbc097..620439b1c 100644 --- a/src/java/com/threerings/presents/client/ClientDObjectMgr.java +++ b/src/java/com/threerings/presents/client/ClientDObjectMgr.java @@ -1,5 +1,5 @@ // -// $Id: ClientDObjectMgr.java,v 1.13 2002/02/10 04:19:34 mdb Exp $ +// $Id: ClientDObjectMgr.java,v 1.14 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.client; @@ -134,7 +134,7 @@ public class ClientDObjectMgr notifyFailure(oid); } else if (obj instanceof PongResponse) { - Log.info("Got pong."); + _client.gotPong((PongResponse)obj); } else if (obj instanceof ObjectAction) { ObjectAction act = (ObjectAction)obj; diff --git a/src/java/com/threerings/presents/client/DeltaCalculator.java b/src/java/com/threerings/presents/client/DeltaCalculator.java new file mode 100644 index 000000000..08088c7f4 --- /dev/null +++ b/src/java/com/threerings/presents/client/DeltaCalculator.java @@ -0,0 +1,91 @@ +// +// $Id: DeltaCalculator.java,v 1.1 2002/05/28 21:56:38 mdb Exp $ + +package com.threerings.presents.client; + +import java.util.Arrays; +import com.samskivert.util.StringUtil; + +import com.threerings.presents.Log; +import com.threerings.presents.net.PingRequest; +import com.threerings.presents.net.PongResponse; + +/** + * Used to compute the client/server time delta, attempting to account for + * the network delay experienced when the server sends its current time to + * the client. + */ +public class DeltaCalculator +{ + /** + * Constructs a delta calculator which is used to calculate the time + * delta between the client and server, accounding reasonably well for + * the delay introduced by sending a timestamp over the network from + * the server to the client. + */ + public DeltaCalculator () + { + _deltas = new long[CLOCK_SYNC_PING_COUNT]; + } + + /** + * Must be called when a ping message is sent to the server. + */ + public void sentPing (PingRequest ping) + { + _ping = ping; + } + + /** + * Must be called when the pong response arrives back from the server. + * + * @return true if we've iterated sufficiently many times to establish + * a stable time delta estimate. + */ + public boolean gotPong (PongResponse pong) + { + // make a note of when the ping message was sent and when the pong + // response was received (both in client time) + long send = _ping.getPackStamp(), recv = pong.getUnpackStamp(); + + // make a note of when the pong response was sent (in server time) + // and the processing delay incurred on the server + long server = pong.getPackStamp(), delay = pong.getProcessDelay(); + + // compute the network delay (round-trip time divided by two) + long nettime = (recv - send - delay)/2; + + // the time delta is the client time when the pong was received + // minus the server's send time (plus network delay): dT = C - S + _deltas[_iter] = recv - (server + nettime); + + return (++_iter >= CLOCK_SYNC_PING_COUNT); + } + + /** + * Returns the best estimate client/server time-delta. + */ + public long getTimeDelta () + { + // sort the estimates and return one from the middle + Arrays.sort(_deltas); + Log.info("Returning time delta " + + "[deltas=" + StringUtil.toString(_deltas) + "]."); + return _deltas[_deltas.length/2]; + } + + /** The number of ping/pong iterations we've made. */ + protected int _iter; + + /** Client/server time delta estimates. */ + protected long[] _deltas; + + /** A reference to the most recently sent ping which we use to obtain + * the appropriate send stamp when we get the corresponding receive + * stamp. */ + protected PingRequest _ping; + + /** The number of times we PING during clock sync to try to smooth out + * network jiggling. */ + protected static final int CLOCK_SYNC_PING_COUNT = 3; +} diff --git a/src/java/com/threerings/presents/net/BootstrapData.java b/src/java/com/threerings/presents/net/BootstrapData.java index eb3f2f260..9b95f8f63 100644 --- a/src/java/com/threerings/presents/net/BootstrapData.java +++ b/src/java/com/threerings/presents/net/BootstrapData.java @@ -1,5 +1,5 @@ // -// $Id: BootstrapData.java,v 1.4 2002/02/04 01:47:20 mdb Exp $ +// $Id: BootstrapData.java,v 1.5 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.net; @@ -18,4 +18,7 @@ public class BootstrapData extends DObject /** The oid to which to send invocation requests. */ public int invOid; + + /** The time from which server ticks are incrementing. */ + public long serverStartStamp; } diff --git a/src/java/com/threerings/presents/net/PingRequest.java b/src/java/com/threerings/presents/net/PingRequest.java index 4c87a814d..20704675a 100644 --- a/src/java/com/threerings/presents/net/PingRequest.java +++ b/src/java/com/threerings/presents/net/PingRequest.java @@ -1,8 +1,12 @@ // -// $Id: PingRequest.java,v 1.5 2001/10/11 04:07:53 mdb Exp $ +// $Id: PingRequest.java,v 1.6 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.net; +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + public class PingRequest extends UpstreamMessage { /** The code for a ping request. */ @@ -16,6 +20,46 @@ public class PingRequest extends UpstreamMessage super(); } + /** + * Returns a timestamp that was obtained when this packet was encoded + * by the low-level networking code. + */ + public long getPackStamp () + { + return _packStamp; + } + + /** + * Returns a timestamp that was obtained when this packet was decoded + * by the low-level networking code. + */ + public long getUnpackStamp () + { + return _unpackStamp; + } + + // documentation inherited + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + + // grab a timestamp noting when we were encoded into a raw buffer + // for delivery over the network + _packStamp = System.currentTimeMillis(); + } + + // documentation inherited + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + + // grab a timestamp noting when we were decoded from a raw buffer + // after being received over the network + _unpackStamp = System.currentTimeMillis(); + } + public short getType () { return TYPE; @@ -25,4 +69,12 @@ public class PingRequest extends UpstreamMessage { return "[type=PING, msgid=" + messageId + "]"; } + + /** A time stamp obtained when we serialize this object. */ + protected long _packStamp; + + /** A time stamp obtained when we unserialize this object (the intent + * is to get a timestamp as close as possible to when the packet was + * received on the network). */ + protected long _unpackStamp; } diff --git a/src/java/com/threerings/presents/net/PongResponse.java b/src/java/com/threerings/presents/net/PongResponse.java index 2065a5a9e..34d53dfe2 100644 --- a/src/java/com/threerings/presents/net/PongResponse.java +++ b/src/java/com/threerings/presents/net/PongResponse.java @@ -1,8 +1,14 @@ // -// $Id: PongResponse.java,v 1.6 2001/10/11 04:07:53 mdb Exp $ +// $Id: PongResponse.java,v 1.7 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.net; +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.threerings.presents.Log; + public class PongResponse extends DownstreamMessage { /** The code for a pong response. */ @@ -16,6 +22,82 @@ public class PongResponse extends DownstreamMessage super(); } + /** + * Constructs a pong response which will use the supplied ping time to + * establish the end-to-end processing delay introduced by the server. + */ + public PongResponse (long pingStamp) + { + // save this for when we are serialized in preparation for + // delivery over the network + _pingStamp = pingStamp; + } + + /** + * Returns the time at which this packet was packed for delivery in + * the time frame of the server that sent the packet. + */ + public long getPackStamp () + { + return _packStamp; + } + + /** + * Returns the number of milliseconds that elapsed between the time + * that the ping which instigated this pong was read from the network + * and the time that this pong was written to the network. + */ + public int getProcessDelay () + { + return _processDelay; + } + + /** + * Returns a timestamp that was obtained when this packet was decoded + * by the low-level networking code. + */ + public long getUnpackStamp () + { + return _unpackStamp; + } + + // documentation inherited + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + + // make a note of the time at which we were packed + _packStamp = System.currentTimeMillis(); + out.writeLong(_packStamp); + + // the time spent between unpacking the ping and packing the pong + // is the processing delay + if (_pingStamp == 0L) { + Log.warning("Pong response written that was not constructed " + + "with a valid ping stamp [rsp=" + this + "]."); + _processDelay = 0; + } else { + _processDelay = (int)(_packStamp - _pingStamp); + } + out.writeInt(_processDelay); + } + + // documentation inherited + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + + // grab a timestamp noting when we were decoded from a raw buffer + // after being received over the network + _unpackStamp = System.currentTimeMillis(); + + // read in our time stamps + _packStamp = in.readLong(); + _processDelay = in.readInt(); + } + public short getType () { return TYPE; @@ -25,4 +107,23 @@ public class PongResponse extends DownstreamMessage { return "[type=PONG, msgid=" + messageId + "]"; } + + /** The ping unpack stamp provided at construct time to this pong + * response; only valid on the sending process, not the receiving + * process. */ + protected long _pingStamp; + + /** The timestamp obtained immediately before this packet was sent out + * over the network. */ + protected long _packStamp; + + /** The delay in milliseconds between the time that the ping request + * was read from the network and the time the pong response was + * written to the network. */ + protected int _processDelay; + + /** A time stamp obtained when we unserialize this object (the intent + * is to get a timestamp as close as possible to when the packet was + * received on the network). */ + protected long _unpackStamp; } diff --git a/src/java/com/threerings/presents/server/PresentsClient.java b/src/java/com/threerings/presents/server/PresentsClient.java index c7f1d4c4d..7d9d4c198 100644 --- a/src/java/com/threerings/presents/server/PresentsClient.java +++ b/src/java/com/threerings/presents/server/PresentsClient.java @@ -1,5 +1,5 @@ // -// $Id: PresentsClient.java,v 1.30 2002/04/26 02:32:27 mdb Exp $ +// $Id: PresentsClient.java,v 1.31 2002/05/28 21:56:38 mdb Exp $ package com.threerings.presents.server; @@ -561,7 +561,8 @@ public class PresentsClient // send a pong response Connection conn = client.getConnection(); if (conn != null) { - conn.postMessage(new PongResponse()); + PingRequest req = (PingRequest)msg; + conn.postMessage(new PongResponse(req.getUnpackStamp())); } else { Log.info("Dropped pong response [client=" + client + "]."); }