Monday, April 26, 2010

What time is it? Part 2

In Part 1, we implemented a server using the human-readable DAYTIME protocol.

One of the drawbacks of human-readable time is the challenge of parsing the text into an object that can be manipulated by computers. This is made harder when there is no clear specification for the format of the text (some conventions like RFC 822 exist, but the DAYTIME protocol leaves this detail unspecified).

Also in 1983, the TIME protocol (specified in RFC 868) was developed. It was intended to be used by machines (rather than humans) to retrieve the current time. As with the DAYTIME protocol, it supports both TCP and UDP. Below, we will implement a TIME server using UDP in Factor.

These are the vocabularies that we will be using:

USING: arrays destructors io.sockets kernel math pack system ;

The timestamp representation used as a payload is a 32-bit binary number of seconds since January 1, 1900. Most computers now use a 64-bit number of milliseconds since January 1, 1970 (referred to as the "epoch") to store the current time. Factor allows access to the current time as a number of microseconds since the epoch using the system-micros word.

We need to first create a word that will return the number of seconds since 1900. There were 2,208,988,800 seconds between January 1, 1900 and January 1, 1970. We will use that to adjust the current time:

CONSTANT: TIME1970 2208988800

: seconds-since-1900 ( -- n )
    system-micros 1000000 / >fixnum TIME1970 + ;

Jumping ahead slightly, we will create a word that receives a packet from a client (usually empty) and then sends the current time as a 32-bit binary number of seconds since 1900 back to the client. The io.sockets vocabulary provides some words for creating datagram servers (for UDP).

: serve-time ( datagram -- )
    [ receive nip ] keep
    [ seconds-since-1900 1array "i" pack ] 2dip
    send ;

According to the specification, the TIME server should run on port 37, but that typically requires administrative privileges, so we will run it on port 3700 for ease of testing. It is also best practices to bind to a specific network interface but, for now, we will listen for packets from any interface.

By running this code in the listener, we can test a datagram port that handles a single packet:

( scratchpad ) f 3700 <inet4> <datagram> [
               ] with-disposal

And then using netcat, we can send an empty UDP packet to the server, which should respond with the current time in binary (which is why it is hard to read):

$ nc -u 3700 ?X?

Putting this all together, we can create a word that will loop forever, serving the current time in this manner:

: time-server ( -- )
    f 3700 <inet4> <datagram> [
        [ dup serve-time t ] loop drop
    ] with-disposal ;

The resolution of this protocol is low (since it is number of seconds), limiting the use-cases it is capable of supporting. Over the years, much better solutions have become standardized.


Slava Pestov said...

Here is a better way of getting the seconds since 1900 without any magic numbers or low-level words:

now 1900 0 0 time- duration>seconds >integer

Slava Pestov said...

now 1900 0 0 <date> time- duration>seconds >integer .