timesync
Version:
Time synchronization between peers
271 lines (183 loc) • 11.6 kB
Markdown
# timesync
Time synchronization between peers.
Usage scenarios:
- **master/slave**: Clients synchronize their time to that of a single server,
via either HTTP requests or WebSockets.
- **peer-to-peer**: Clients are connected in a (dynamic) peer-to-peer network
using WebRTC or WebSockets and must converge to a single, common time in the
network.
# Install
Install via npm:
```
npm install timesync
```
# Usage
A timesync client can basically connect to one server or multiple peers,
and will synchronize it's time. The synchronized time can be retrieved via
the method `now()`, and the client can subscribe to events like `'change'`
and `'sync'`.
```js
// create a timesync instance
var ts = timesync({
server: '...', // either a single server,
peers: [...] // or multiple peers
});
// get notified on changes in the offset
ts.on('change', function (offset) {
console.log('offset from system time:', offset, 'ms');
}
// get the synchronized time
console.log('now:', new Date(ts.now()));
```
# Example
Here a full usage example with express.js, showing both server and client side.
`timesync` has build-in support for requests over http and can be used with
express, a default http server, or other solutions. `timesync` can also be
used over other transports than http, for example using websockets or webrtc.
This is demonstrated in the [advanced examples](/examples/advanced).
More examples are available in the [/examples](/examples) folder.
Some of the examples use libraries like `express` or `socket.io`.
Before you can run these examples you will have to install these dependencies.
**server.js**
```js
var express = require('express');
var timesyncServer = require('timesync/server');
// create an express app
var port = 8081;
var app = express();
app.listen(port);
console.log('Server listening at http://localhost:' + port);
// serve static index.html
app.get('/', express.static(__dirname));
// handle timesync requests
app.use('/timesync', timesyncServer.requestHandler);
```
**index.html**
```html
<!DOCTYPE html>
<html>
<head>
<!-- note: for support on older browsers, you will need to load es5-shim and es6-shim -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.0.5/es5-shim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.23.0/es6-shim.min.js"></script>
<script src="/timesync/timesync.js"></script>
</head>
<script>
// create a timesync instance
var ts = timesync.create({
server: '/timesync',
interval: 10000
});
// get notified on changes in the offset
ts.on('change', function (offset) {
document.write('changed offset: ' + offset + ' ms<br>');
});
// get synchronized time
setInterval(function () {
var now = new Date(ts.now());
document.write('now: ' + now.toISOString() + ' ms<br>');
}, 1000);
</script>
</html>
```
# API
## Client
### Construction
An instance of timesync is created as:
```js
var ts = timesync(options);
```
#### Options
The following options are available:
Name | Type | Default | Description
---------- | ---------------------- | ---------- | ----------------------------------------
`delay` | `number` | `1000` | Delay in milliseconds between every request sent.
`interval` | `number` or `null` | `3600000` | Interval in milliseconds for running a synchronization. Defaults to 1 hour. Set to `null` to disable automatically running synchronizations (synchronize by calling `sync()`).
`now` | `function` | `Date.now` | Function returning the local system time.
`peers` | `string[]` or `string` | `[]` | Array or comma separated string with uri's or id's of the peers to synchronize with. Cannot be used in conjunction with option `server`.
`repeat` | `number` | `5` | Number of times to do a request to every peer.
`server` | `string` | none | Url of a single server in case of a master/slave configuration. Cannot be used in conjunction with option `peers`.
`timeout` | `number` | `10000` | Timeout in milliseconds for requests to fail.
### Methods
Name | Return type | Description
--------------------- | ----------- | ----------------------------------
`destroy()` | none | Destroy the timesync instance. Stops automatic synchronization. If timesync is currently executing a synchronization, this synchronization will be finished first.
`now()` | `number` | Get the synchronized time. Returns a timestamp. To create a `Date`, call `new Date(time.now())`.
`on(event, callback)` | `Object` | Register a callback handler for an event. Returns the timesync instance. See section [Events](#events) for more information.
`off(event [, callback])` | `Object` | Unregister a callback handler for an event. If no callback is provided, all callbacks of this event will be removed. Returns the timesync instance. See section [Events](#events) for more information.
`sync()` | none | Do a synchronization with all peers now.
To be able to send and receive messages from peers, `timesync` needs a transport. To hook up a transport like a websocket or http requests, one has to override the `send(id, data)` method of the `timesync` instance, and has to call `ts.receive(id, data)` on incoming messages.
Name | Return type | Description
----------------------------------- | ----------- | ----------------------------------
`send(to, data, timeout) : Promise` | none | Send a message to a peer. `to` is the id of the peer, and `data` a JSON object containing the message. Must return a Promise which resolves when the message has been sent, or rejects when sending failed or a timeout occurred.
`receive(from, data)` | none | Receive a message from a peer. `from` is the id of the sender, and `data` a JSON object containing the message.
`timesync` sends messages using the JSON-RPC protocol, as described in the section [Protocol](#protocol).
### Events
`timesync` emits events when starting and finishing a synchronization, and when the time offset changes. To listen for events:
```js
ts.on('change', function (offset) {
console.log('offset changed:', offset);
});
```
Available events:
Name | Description
---------| ----------
`change` | Emitted when the offset is changed. This can only happen during a synchronization. Callbacks are called with the new offset (a number) as argument.
`error` | Emitted when an error occurred. Callbacks are called with the error as argument.
`sync` | Emitted when a synchronization is started or finished. Callback are called with a value `'start'` or `'end'` as argument.
### Properties
Name | Type | Description
--------- | -------- | --------------------------------------------
`offset` | `number` | The offset from system time in milliseconds.
`options` | `Object` | An object holding all options of the timesync instance. One can safely adjust options like `peers` at any time. Not all options can be changed after construction, for example a changed `interval` value will not be applied.
## Server
`timesync` comes with a build in server to serve as a master for time synchronization. Clients can adjust their time to that of the server. The server basically just implements a POST request responding with its current time, and serves the static files `timesync.js` and `timesync.min.js` from the `/dist` folder. It's quite easy to implement this request handler yourself, as is demonstrated in the [advanced examples](/examples/advanced).
The protocol used by the server is described in the section [Protocol](#protocol).
### Load
The server can be loaded in node.js as:
```js
var timesyncServer = require('timesync/server');
```
### Methods
Name | Return type | Description
----------------------------- | ------------ | ----------------------------------
`createServer()` | `http.Server`| Create a new, dedicated http Server. This is just a shortcut for doing `http.createServer( timesyncServer.requestHandler )`.
`attachServer(server, [path])`| `http.Server`| Attach a request handler for time synchronization requests to an existing http Server. Argument `server` must be an instance of `http.Server`. Argument `path` is optional, and is `/timesync` by default.
### Properties
Name | Type | Description
----------------- | ---------- | --------------------------------------------
`requestHandler` | `function` | A default request handler, handling requests for the timesync server. Signature is `requestHandler(request, response)`. This handler can be used to attach to an expressjs server, or to create a plain http server by doing `http.createServer( timesyncServer.requestHandler )`.
# Protocol
`timesync` sends messages using the JSON-RPC protocol. A peer sends a message:
```json
{"jsonrpc": "2.0", "id": "12345", "method": "timesync"}
```
The receiving peer replies with the same id and its current time:
```json
{"jsonrpc": "2.0", "id": "12345", "result": 1423151204595}
```
The sending peer matches the returned message by id and uses the result to adjust it's offset.
# Algorithm
`timesync` uses a simple synchronization protocol aimed at the gaming industry, and extends this for peer-to-peer networks. The algorithm is described [here](https://web.archive.org/web/20160310125700/http://mine-control.com/zack/timesync/timesync.html):
> A simple algorithm with these properties is as follows:
>
> 1. Client stamps current local time on a "time request" packet and sends to server
> 2. Upon receipt by server, server stamps server-time and returns
> 3. Upon receipt by client, client subtracts current time from sent time and divides by two to compute latency. It subtracts current time from server time to determine client-server time delta and adds in the half-latency to get the correct clock delta. (So far this algorithm is very similar to SNTP)
> 4. The first result should immediately be used to update the clock since it will get the local clock into at least the right ballpark (at least the right timezone!)
> 5. The client repeats steps 1 through 3 five or more times, pausing a few seconds each time. Other traffic may be allowed in the interim, but should be minimized for best results
> 6. The results of the packet receipts are accumulated and sorted in lowest-latency to highest-latency order. The median latency is determined by picking the mid-point sample from this ordered list.
> 7. All samples above approximately 1 standard-deviation from the median are discarded and the remaining samples are averaged using an arithmetic mean.
This algorithm assumes multiple clients synchronizing with a single server. In case of multiple peers, `timesync` will take the average offset of all peers (excluding itself) as offset.
# Tutorials
- [Using the timesync library in Android applications](https://github.com/enmasseio/timesync/blob/master/docs/android-tutorial.md)
# Resources
- [A Stream-based Time Synchronization Technique For Networked Computer Games](https://web.archive.org/web/20160310125700/http://mine-control.com/zack/timesync/timesync.html)
- [Network Time Protocol](http://www.wikiwand.com/en/Network_Time_Protocol)
# Build
To build the library:
npm install
npm run build
This will generate the files `timesync.js` and `timesync.min.js` in the folder `/dist`.
To automatically build on changes, run:
npm run watch