Rolf B: Nachtrag: Zeitdifferenz

Beitrag lesen

Hallo Jo,

eigentlich müsstest Du doch mit dem SNTP-Algorithmus klarkommen können. Ich habe ihn mal um einen gleitenden Durchschnitt der Zeitdifferenzen angereichert...

class SyncedTime {
   constructor() {
      this.delta = 0;
   }
   now() { 
      return Date.now() + this.delta;
   }
   static synchronize(timeService, syncDelay) {
      let deltaList = [];
      let avgLength = 3;
      let tickCount = 0;
      let syncObject = new SyncedTime();
      
      return new Promise(doSync);

      function doSync(resolve, reject) {
         let syncStart = Date.now();
         timeService()
         .then(servertime => {
            // Client-/Server Delta für diesen Aufruf berechnen.
            let syncEnd = Date.now();
            let ipDelay = (syncEnd - syncStart) / 2;
            changeDelta(servertime + ipDelay - syncEnd);

            // Nächsten Zyklus einleiten - in der Init-Phase mehrmals schnell hintereinander un
            // Initialisierungseffekte zu eliminieren.
            if (tickCount < avgLength) {
               setTimeout(() => doSync(resolve,reject), 0);
               tickCount++;
            }
            else {
               // Nur einmal resolven. Alle Folgeaufrufe mit resolve/reject=null durchführen.
               if (resolve) resolve(syncObject);
               setTimeout(() => doSync(null,null), syncDelay);
            }
         });
      }
      function changeDelta(delta) {
         console.log("Apply new delta " + delta);
         // Gleitenden Durchschnitt der letzten 3 Deltas berechnen
         deltaList.push(delta);
         if (deltaList.length > avgLength) deltaList.shift();
         syncObject.delta = deltaList.reduce((x,y)=>x+y, 0)/deltaList.length;
      }
   }
} 

Aufzurufen mit

SyncedTime
   .synchronize(timeService, 10000)
   .then(myProgramWithServerTime);

timeService ist eine Funktion, die die Serverzeit holt und ein Promise liefert, das zur zurückgelieferten Serverzeit resolved. Wenn Du das nicht hast oder haben willst, bau es so um dass die Funktion einen Callback erwartet, dem die Serverzeit übergeben wird. Läuft auf's selbe hinaus, aber Promises sind schicker :)

Auf diese Weise geht es im then-Handler weiter, sobald syncTime eingeschwungen ist (d.h. einmal mehr synchronisiert hat als die Länge des gleitenden Durchschnitts), und er bekommt ein SyncedTime Objekt übergeben, das sich automatisch mit dem Server synchronisiert und über die now-Methode die synchronisierte Zeit liefert.

Die SyncedTime Klasse muss gar nicht so oft synchronisieren, man wird davon ausgehen können dass Client- und Server-Uhr in bspw. 5 Minuten nur um wenige Mikrosekunden auseinander laufen. Die 10 Sekunden aus meinem Aufrufbeispiel sind vermutlich viel zu viel.

Damit bekommst Du dann eine relativ genaue Annäherung der Clientzeit an die Serverzeit hin. Eine (hypothetische) Funktion zf(t), die zum Zeitpunkt t den aktuellen Zeitfehler angibt, dürfte im Betrag unter 100ms bleiben, vermutlich weniger.

Wichtig ist nur, dass der Server die Time-Requests nicht queued, sondern sofort verarbeitet. Bei einem stark belasteten Server kann es zum Queueing kommen, wenn zu viele Requeste eintreffen. Die Laufzeitbalance von Request und Response ist dann schief. Es kann sinnvoll sein, für die Time Requests einen separaten Node laufen zu lassen.

Das Problem beginnt, wenn Du anfängst, auf Sekunden zu runden. Aus dem stets nahe bei 0 liegenden Fehlerwert wird dann auf einmal eine Rechteck-Kurve, die meistens auf 0 liegt, aber gelegentlich auf 1 oder -1 springt. Das Integral von zf und gerundetem zf dürfte über die Zeit gleich sein, aber wenn dich die heftigen Ausschläge stören, müsste man fragen: Musst Du runden?

Rolf

--
sumpsi - posui - clusi