13

I'm looking for a way to synchronize time between clients with a good precision (let's say 0.5 seconds at least).

I exclude using jsontime or exploiting timestamp in server response headers due to a poor precision (a second or maybe less).

UPDATE: It should work even with mobile connections. It's not unfrequent (e.g. here in Italy) that 3G connections itself have a round trip time around 0.5s, so algorithm has to be robust.

micred
  • 1,482
  • 1
  • 19
  • 19

2 Answers2

20

Resort to the good old ICMP Timestamp message scheme. It's fairly trivial to implement in JavaScript and PHP.

Here's an implementation of this scheme using JavaScript and PHP:

// browser.js

var request = new XMLHttpRequest();
request.onreadystatechange = readystatechangehandler;
request.open("POST", "http://www.example.com/sync.php", true);
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send("original=" + (new Date).getTime());

function readystatechangehandler() {
    var returned = (new Date).getTime();
    if (request.readyState === 4 && request.status === 200) {
        var timestamp = request.responseText.split('|');
        var original = + timestamp[0];
        var receive = + timestamp[1];
        var transmit = + timestamp[2];
        var sending = receive - original;
        var receiving = returned - transmit;
        var roundtrip = sending + receiving;
        var oneway = roundtrip / 2;
        var difference = sending - oneway; // this is what you want
        // so the server time will be client time + difference
    }
}

Now for the sync.php code:

<?php
    $receive = round(microtime(true) * 1000);
    echo $_POST["original"] . '|';
    echo $receive . '|';
    echo round(microtime(true) * 1000);
?>

I haven't tested the above code, but it should work.

Note: The following method will accurately calculate the time difference between the client and the server provided that actual time to send and receive messages is the same or approximately the same. Consider the following scenario:

  Time    Client   Server
-------- -------- --------
Original        0        2
Receive         3        5
Transmit        4        6
Returned        7        9
  1. As you can see, the client and server clocks are 2 units off sync. Hence when the client sends the timestamp request, it records the original time as 0.
  2. The server receives the request 3 units later, but records the receive time as 5 units because it's 2 units ahead.
  3. Then it transmits the timestamp reply one unit later and records the transmit time as 6 units.
  4. The client receives the reply after 3 units (i.e. at 9 units according to the server). However, since it's 2 units behind the server it records the returned time as 7 units.

Using this data, we can calculate:

Sending = Receive - Original = 5 - 0 = 5
Receiving = Returned - Transmit = 7 - 6 = 1
Roundtrip = Sending + Receiving = 5 + 1 = 6

As you can see from above, the sending and receiving times are calculated incorrectly depending upon how much the client and server are off sync. However, the roundtrip time will always be correct because we are first adding two units (receive + original), and then subtracting two units (returned - transmit).

If we assume that the oneway time is always half of the roundtrip time (i.e. the time to transmit is the time to receive, then we can easily calculate the time difference as follows):

Oneway = Roundtrip / 2 = 6 / 2 = 3
Difference = Sending - Oneway = 5 - 3 = 2

As you can see, we accurately calculated the time difference as 2 units. The equation for time difference is always sending - oneway time. However, the accuracy of this equation depends upon how accurately you calculate the oneway time. If the actual time to send and receive the messages is not equal or approximately equal, you'll need to find some other way to calculate the one way time. However, for your purposes this should suffice.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • How can I send ICMP packets from javascript? I've see only an ActiveX called ActiveSocket for sending ICMP packets but I can't use ActiveX.It shoud work on webkit. – micred Dec 12 '11 at 17:52
  • You don't send ICMP packets. You simply use the same method that ICMP uses to synchronize the time on the server and the client. Hope that clears your doubt. Cheers. ;) – Aadit M Shah Dec 12 '11 at 18:42
  • 1
    Your formatting in PHP looks incorrect: `microtime()` with no parameters returns a string of the form "0.uuuuuu ssssssssss". I'd rather `echo round(microtime(true)*1000)`. – Edgar Bonet Dec 12 '11 at 21:35
  • Ah, yes. Thank you for that correction. I usually use RingoJS for my back end. Hence I didn't bother testing my PHP code. I just copied it from the PHP manuals. I'll correct it right now. – Aadit M Shah Dec 13 '11 at 03:00
  • 1
    Another couple of corrections: if the PHP code expects POST, JavaScript should `request.open("POST", ...)`. It should also `request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");`, or PHP may get confused. Oh, and BTW, no need for the PHP code to provide two timestamps (receive and transmit), as it's unlikely that these will differ by more than one millisecond. No need for the original timestamp to do the round trip either: JavaScript can just remember it. Thus, the PHP code can just `echo round(microtime(true) * 1000);`, nothing more is needed. – Edgar Bonet Dec 13 '11 at 10:41
  • Thanks for pointing those out for me. I changed the JavaScript. I'm going to let the PHP code be as it is. Too lazy to change anything else. – Aadit M Shah Dec 13 '11 at 11:29
  • 2
    I think there will be 2 problems, correct me if I am wrong: 1) ICMP Timestamp use ICMP as transport protocol. This algorithm use TCP so I think that 3 way handshake of TCP can introduce significant and unpredictable delays (and oneway is not roundtrip / 2). Even a worse scenario if that POST involve a "CORS preflight" to obtain authorization for cross-domain ajax. Maybe exploiting HTTP persistent connection can be a viable solution to improve accuracy? (e.g. opening a dummy request to initially setup the connection and than using the proposed algorithm?) – micred Dec 14 '11 at 08:49
  • 2
    2) $receive in server side sync.php doesn't represent the time when the packet is received. It works only if the server has no load. In common situation the web-server puts the request in a queue and a pool of php processes take care of the request in a FIFO logic. In this implementation is impossible to discriminate the time when the packet was received from the time when the packet was processed by a php process. And how to recognize if a response is valid or invalid? (e.g. due to a burst of requests that has introduced latency?) – micred Dec 14 '11 at 08:52
  • 1
    @micred: If doing cross-domain AJAX (or even in-domain, actually), I would use GET rather than POST. As I commented previously, there is no need for the original timestamp to do a round trip, so POST has no benefit. Also, if the request goes to the same server that delivered the JavaScript, chances are this connection will be reused. – Edgar Bonet Dec 14 '11 at 21:36
  • 1
    @micred: As for your last question, the difference between the Original and Returned timestamps (roudtrip time) is an indication of the error bar. If it's too big, you can always send another request. Also, OP doesn't require millisecond precision, only somewhat better than 0.5 s, which is perfectly feasible over HTTP unless the server is severely jammed. – Edgar Bonet Dec 14 '11 at 21:37
7

If you just want to sync the timeclock on several computers, NTP themselves recommend setting up your own timeserver.

Server Endpoint

Set up an api endpoint on your server i.e.

http://localhost:3000/api/time

returns:

status: 200, 
body: { time: {{currentTime}} }

How this is done will depend on the backend language that you're using.

Client Code

Given such an endpoint, this JS snippet I threw together will

  1. Poll your server endpoint 10 times.
  2. Attempt to compensate for latency by removing half of the round-trip time.
  3. Report the average offset between the server and the client.

var offsets = [];
var counter = 0;
var maxTimes = 10;
var beforeTime = null;

// get average 
var mean = function(array) {
  var sum = 0;
  
  array.forEach(function (value) {
    sum += value;
  });
  
  return sum/array.length;
}

var getTimeDiff = function() {
  beforeTime = Date.now();
  $.ajax('/api/time', {
      type: 'GET',
      success: function(response) {
          var now, timeDiff, serverTime, offset;
          counter++;
          
          // Get offset
          now = Date.now();
          timeDiff = (now-beforeTime)/2;
          serverTime = response.data.time-timeDiff;
          offset = now-serverTime;
          
          console.log(offset);
          
          // Push to array
          offsets.push(offset)
          if (counter < maxTimes) {
            // Repeat
            getTimeDiff();
          } else {
            var averageOffset = mean(offsets);
            console.log("average offset:" + averageOffset);
          }
      }
  });
}

// populate 'offsets' array and return average offsets
getTimeDiff();

You can use this computed offset (just add it to local time), to determine a common "universal" time from each client's context.

FreePender
  • 4,770
  • 2
  • 18
  • 15
  • 2
    This is useful, thanks. It might also make sense to use median instead of mean to reject outliers. – vgru Nov 16 '15 at 15:03