Let's make a NTP Client in C

2016-04-26  

David Lettier  

The source file for this project is hosted on GitHub. There is also an identical version but in Python. The Python version is much shorter.

Network Time Protocol

typedef struct
{

  unsigned li   : 2;       // Only two bits. Leap indicator.
  unsigned vn   : 3;       // Only three bits. Version number of the protocol.
  unsigned mode : 3;       // Only three bits. Mode. Client will pick mode 3 for client.

  uint8_t stratum;         // Eight bits. Stratum level of the local clock.
  uint8_t poll;            // Eight bits. Maximum interval between successive messages.
  uint8_t precision;       // Eight bits. Precision of the local clock.

  uint32_t rootDelay;      // 32 bits. Total round trip delay time.
  uint32_t rootDispersion; // 32 bits. Max error aloud from primary clock source.
  uint32_t refId;          // 32 bits. Reference clock identifier.

  uint32_t refTm_s;        // 32 bits. Reference time-stamp seconds.
  uint32_t refTm_f;        // 32 bits. Reference time-stamp fraction of a second.

  uint32_t origTm_s;       // 32 bits. Originate time-stamp seconds.
  uint32_t origTm_f;       // 32 bits. Originate time-stamp fraction of a second.

  uint32_t rxTm_s;         // 32 bits. Received time-stamp seconds.
  uint32_t rxTm_f;         // 32 bits. Received time-stamp fraction of a second.

  uint32_t txTm_s;         // 32 bits and the most important field the client cares about. Transmit time-stamp seconds.
  uint32_t txTm_f;         // 32 bits. Transmit time-stamp fraction of a second.

} ntp_packet;              // Total: 384 bits or 48 bytes.

The NTP message consists of a 384 bit or 48 byte data structure containing 17 fields.

Populate our Message

// Create and zero out the packet. All 48 bytes worth.

ntp_packet packet = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

memset( &packet, 0, sizeof( ntp_packet ) );

// Set the first byte's bits to 00,011,011 for li = 0, vn = 3, and mode = 3. The rest will be left set to zero.

*( ( char * ) &packet + 0 ) = 0x1b; // Represents 27 in base 10 or 00011011 in base 2.

First we zero-out or clear out the memory of our structure and then fill it in with leap indicator zero, version number three, and mode 3. The rest we can leave blank and still get back the time from the server.

Setup our Socket and Server Data Structure

// Create a UDP socket, convert the host-name to an IP address, set the port number,
// connect to the server, send the packet, and then read in the return packet.

struct sockaddr_in serv_addr; // Server address data structure.
struct hostent *server;      // Server data structure.

sockfd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); // Create a UDP socket.

if ( sockfd < 0 )
  error( "ERROR opening socket" );

server = gethostbyname( host_name ); // Convert URL to IP.

if ( server == NULL )
  error( "ERROR, no such host" );

// Zero out the server address structure.

bzero( ( char* ) &serv_addr, sizeof( serv_addr ) );

serv_addr.sin_family = AF_INET;

// Copy the server's IP address to the server address structure.

bcopy( ( char* )server->h_addr, ( char* ) &serv_addr.sin_addr.s_addr, server->h_length );

// Convert the port number integer to network big-endian style and save it to the server address structure.

serv_addr.sin_port = htons( portno );

Before we can start communicating we have to setup our socket, server and server address structures. We will be using the User Datagram Protocol (versus TCP) for our socket since the server we are sending our message to is listening on port number 123 using UDP.

Send our Message to the Server


// Call up the server using its IP address and port number.

if ( connect( sockfd, ( struct sockaddr * ) &serv_addr, sizeof( serv_addr) ) < 0 )
  error( "ERROR connecting" );

// Send it the NTP packet it wants. If n == -1, it failed.

n = write( sockfd, ( char* ) &packet, sizeof( ntp_packet ) );

if ( n < 0 )
  error( "ERROR writing to socket" );

With our message payload, socket, server and address setup, we can now send our message to the server. To do this, we write our 48 byte struct to the socket.

Read in the Return Message

// Wait and receive the packet back from the server. If n == -1, it failed.

n = read( sockfd, ( char* ) &packet, sizeof( ntp_packet ) );

if ( n < 0 )
  error( "ERROR reading from socket" );

Now that our message is sent, we block or wait for the response by reading from the socket. The message we get back should be the same size as the message we sent. We will store the incoming message in packet just like we stored our outgoing message.

Parse the Return Message

// These two fields contain the time-stamp seconds as the packet left the NTP server.
// The number of seconds correspond to the seconds passed since 1900.
// ntohl() converts the bit/byte order from the network's to host's "endianness".

packet.txTm_s = ntohl( packet.txTm_s ); // Time-stamp seconds.
packet.txTm_f = ntohl( packet.txTm_f ); // Time-stamp fraction of a second.

// Extract the 32 bits that represent the time-stamp seconds (since NTP epoch) from when the packet left the server.
// Subtract 70 years worth of seconds from the seconds since 1900.
// This leaves the seconds since the UNIX epoch of 1970.
// (1900)------------------(1970)**************************************(Time Packet Left the Server)

time_t txTm = ( time_t ) ( packet.txTm_s - NTP_TIMESTAMP_DELTA );

The message we get back is in network order or big-endian form. Depending on the machine you run this on, ntohl will transform the bits from either big to little or big to big-endian. You can think of big or little-endian as reading from left to right or tfel ot thgir respectively.

With the data in the order we need it, we can now subtract the delta and cast the resulting number to a time-stamp number. Note that NTP_TIMESTAMP_DELTA = 2208988800ull which is the NTP time-stamp of 1 Jan 1970 or put another way 2,208,988,800 unsigned long long seconds.

While the Unix timescale is not shown directly in the table, the correspondence between the NTP and Unix timescales is determined only by the constant 2,208,988,800. This is the number of Gregorian seconds from the NTP prime epoch 0h, 1 January 1900 to the Unix prime epoch 0h, 1 January 1970.
// Print the time we got from the server, accounting for local timezone and conversion from UTC time.

printf( "Time: %s", ctime( ( const time_t* ) &txTm ) );

With the time-stamp in hand, we can now print it out in its more natural textual form.

~/ntpclient git:master ❯❯❯ ./a.out
Time: Tue Apr 26 02:22:46 2016

Wrap-up

Using the C programming language, we built a NTP client. This client can communicate with a remote NTP server using UDP on port 123. The message format was a 48 byte structure that we partially filled out and sent off to the server. The server replied with a same sized message and we parsed the response to extract the current time-stamp.