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
This commit is contained in:
Mike Thomas
2011-10-18 17:59:14 +00:00
parent b4d516ea29
commit 1e109e44cd
2 changed files with 261 additions and 3 deletions
@@ -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;
}
}
@@ -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;
}
}