From 1e109e44cdffe57c53cd6ab4843c7bfc75295a2b Mon Sep 17 00:00:00 2001 From: Mike Thomas Date: Tue, 18 Oct 2011 17:59:14 +0000 Subject: [PATCH] Make the Actionscript Client able to use the same mechanism as the Java Client to adjust for clock difference between client & server. This allows us to pass an expiration time from the server, and the client can appropriately turn that into a countdown timer to the appropriately adjusted time. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6733 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../com/threerings/presents/client/Client.as | 81 +++++++- .../presents/client/DeltaCalculator.as | 183 ++++++++++++++++++ 2 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 src/main/as/com/threerings/presents/client/DeltaCalculator.as diff --git a/src/main/as/com/threerings/presents/client/Client.as b/src/main/as/com/threerings/presents/client/Client.as index 01732630e..181356af6 100644 --- a/src/main/as/com/threerings/presents/client/Client.as +++ b/src/main/as/com/threerings/presents/client/Client.as @@ -26,6 +26,7 @@ import flash.utils.Timer; import com.threerings.util.DelayUtil; import com.threerings.util.Log; +import com.threerings.util.Long; import com.threerings.util.Throttle; import com.threerings.presents.client.InvocationService_ConfirmListener; @@ -225,6 +226,26 @@ public class Client extends EventDispatcher return _bstrap; } + /** + * Converts a server time stamp to a value comparable to client clock readings. + */ + public function fromServerTime (stamp :Long) :Long + { + // when we calculated our time delta, we did it such that: C - S = dT, thus to convert + // server to client time we do: C = S + dT + return Long.fromNumber(stamp.toNumber() + _serverDelta); + } + + /** + * Converts a client clock reading to a value comparable to a server time stamp. + */ + public function toServerTime (stamp :Long) :Long + { + // when we calculated our time delta, we did it such that: C - S = dT, thus to convert + // server to client time we do: S = C - dT + return Long.fromNumber(stamp.toNumber() - _serverDelta); + } + public function isLoggedOn () :Boolean { return (_clobj != null); @@ -369,6 +390,10 @@ public class Client extends EventDispatcher _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(flash.utils.getTimer()); + // log.debug("TimeBaseService: " + requireService(TimeBaseService)); } @@ -383,8 +408,40 @@ public class Client extends EventDispatcher } var now :uint = flash.utils.getTimer(); - if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) { + 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 + var req :PingRequest = new PingRequest(); + _comm.postMessage(req); + _dcalc.sentPing(req); + } + + } else if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) { _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 function establishClockDelta (now :Number) :void + { + if (_comm != null) { + // create a new delta calculator and start the process + _dcalc = new DeltaCalculator(); + var req :PingRequest = new PingRequest(); + _comm.postMessage(req); + _dcalc.sentPing(req); + _lastSync = now; } } @@ -405,7 +462,6 @@ public class Client extends EventDispatcher { // keep our client object around _clobj = clobj; - // and start up our tick interval (which will send pings when necessary) if (_tickInterval == null) { _tickInterval = new Timer(5000); @@ -453,7 +509,13 @@ public class Client extends EventDispatcher */ internal function gotPong (pong :PongResponse) :void { - // TODO: compute time delta? + // 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(); + } } internal function setOutgoingMessageThrottle (messagesPerSec :int) :void @@ -536,6 +598,16 @@ public class Client extends EventDispatcher /** Manages invocation services. */ protected var _invdir :InvocationDirector = new InvocationDirector(); + /** The difference between the server clock and the client clock (estimated immediately after + * logging on). */ + protected var _serverDelta :Number; + + /** Used when establishing our clock delta between the client and server. */ + protected var _dcalc :DeltaCalculator; + + /** The last time at which we synced our clock with the server. */ + protected var _lastSync :Number; + /** Ticks. */ protected var _tickInterval :Timer; @@ -547,5 +619,8 @@ public class Client extends EventDispatcher /** Used to temporarily track our server switcher so that we can tell when we're logging off * whether or not we're switching servers or actually ending our session. */ protected var _switcher :ServerSwitcher; + + /** How often we recompute our time offset from the server. */ + protected static const CLOCK_SYNC_INTERVAL :Number = 600 * 1000; } } diff --git a/src/main/as/com/threerings/presents/client/DeltaCalculator.as b/src/main/as/com/threerings/presents/client/DeltaCalculator.as new file mode 100644 index 000000000..e37c42edb --- /dev/null +++ b/src/main/as/com/threerings/presents/client/DeltaCalculator.as @@ -0,0 +1,183 @@ +// +// $Id: DeltaCalculator.java 6407 2011-01-01 05:02:21Z dhoover $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved +// http://code.google.com/p/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 flash.utils.getTimer; + +import com.threerings.util.Arrays; +import com.threerings.util.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 +{ + private static const log :Log = Log.getLog(DeltaCalculator); + + /** + * Constructs a delta calculator which is used to calculate the time + * delta between the client and server, accounting reasonably well for + * the delay introduced by sending a timestamp over the network from + * the server to the client. + */ + public function DeltaCalculator () + { + _deltas = []; + for (var ii :int = 0; ii < CLOCK_SYNC_PING_COUNT; ii++) { + _deltas.push(0); + } + } + + /** + * Should we send another ping? + */ + public function shouldSendPing () :Boolean + { + return (_ping == null) && !isDone(); + } + + /** + * Must be called when a ping message is sent to the server. + */ + public function sentPing (ping :PingRequest) :void + { + _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 function gotPong (pong :PongResponse) :Boolean + { + if (_ping == null) { + // an errant pong that is likely being processed late after + // a new connection was opened. + return false; + } + // don't freak out if they keep calling gotPong() after we're done + if (_iter >= _deltas.length) { + return true; + } + + // make a note of when the ping message was sent and when the pong + // response was received (both in client time) + var send :Number = _ping.getPackStamp(); + var recv :Number = pong.getUnpackStamp(); + _ping = null; // clear out the saved sent ping + + // Factor to convert a realtime value (such as the server uses) to a number relative to + // when flash started up. + var realtimeToTimer :Number = getTimer() - ((new Date()).getTime()); + + // make a note of when the pong response was sent (in server time) + // and the processing delay incurred on the server + var server :Number = pong.getPackStamp().toNumber() + realtimeToTimer; + var delay :Number = pong.getProcessDelay(); + + // compute the network delay (round-trip time divided by two) + var nettime :Number = (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); + + log.debug("Calculated delta", "delay", delay, "nettime", nettime, "delta", _deltas[_iter], + "rtt", (recv-send)); + + return (++_iter >= CLOCK_SYNC_PING_COUNT); + } + + /** + * Returns the best estimate client/server time-delta. + */ + public function getTimeDelta () :Number + { + if (_iter == 0) { // no responses yet + return 0; + } + + // Return a median value as our estimate, rather than an average. + // Mdb writes: + // ----------- + // I used the median because that was more likely to result in a + // sensible value. + // + // Assuming there are two kinds of packets, one that goes and comes + // back without delay and provides an accurate time value, and one + // that gets delayed somewhere on the way there or the way back and + // provides an inaccurate time value. + // + // If no packets are delayed, both algorithms should be fine. If one + // packet is delayed the median will select the middle, non-delayed + // packet, whereas the average will skew everything a bit because + // of the delayed packet. If two packets are delayed, the median + // will be more skewed than the average because it will benefit + // from the one accurate packet and if all three packets are delayed + // both algorithms will be (approximately) equally inaccurate. + // + // I believe the chances are most likely that zero or one packets + // will be delayed, so I chose the median rather than the average. + // ----------- + + // copy the deltas array so that we don't alter things before + // all pongs have arrived + var deltasCopy :Array = Arrays.copyOf(_deltas); + + // sort the estimates and return one from the middle + deltasCopy.sort(Array.NUMERIC); + return deltasCopy[int(deltasCopy.length/2)]; + } + + /** + * Returns true if this calculator has enough data to compute a time + * delta estimate. Stick a fork in it! + */ + public function isDone () :Boolean + { + return (_iter >= CLOCK_SYNC_PING_COUNT); + } + + /** The number of ping/pong iterations we've made. */ + protected var _iter :int; + + /** Client/server time delta estimates. */ + protected var _deltas :Array; // of Number + + /** 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 var _ping :PingRequest; + + /** The number of times we PING during clock sync to try to smooth out + * network jiggling. */ + protected static const CLOCK_SYNC_PING_COUNT :int = 3; +} + +} \ No newline at end of file