21.11. SNTP: простой синхронизирующий сетевой протокол

21.11. SNTP: простой синхронизирующий сетевой протокол

Синхронизирующий сетевой протокол (Network Time Protocol, NTP) — это сложный протокол синхронизации часов в глобальной или локальной сети. Его точность часто может достигать миллисекунд. В RFC 1305 [76] этот протокол подробно описан, а в RFC 2030 [77] рассматривается протокол SNTP — упрощенная версия NTP, предназначенная для узлов, которым не требуется функциональность полной реализации NTP. Типичной является ситуация, когда несколько узлов в локальной сети синхронизируют свои часы через Интернет с другими узлами NTP, а затем распространяют полученное значение времени в локальной сети с использованием либо широковещательной, либо многоадресной передачи.

В этом разделе мы создадим клиент SNTP, который прослушивает широковещательные или групповые сообщения NTP на всех присоединенных сетях, а затем выводит разницу во времени между пакетом NTP и текущим истинным временем узла. Мы не пытаемся изменить это время, поскольку для этого необходимы права привилегированного пользователя.

Файл ntp.h, показанный в листинге 21.11, содержит некоторые из основных определений формата пакета NTP.

Листинг 21.11. Заголовок ntp.h: формат пакета NTP и определения

//ssntp/ntp.h

 1 #define JAN_1970 2208988800UL /* 1970 - 1900 в секундах */

 2 struct l_fixedpt { /* 64-разрядное число с фиксированной точкой */

 3  uint32_t int_part;

 4  uint32_t fraction;

 5 };

 6 struct s_fixedpt { /* 32-разрядное число с фиксированной точкой */

 7  u_short int_part;

 8  u_short fraction;

 9 };

10 struct ntpdata { /* заголовок NTP */

11  u_char status;

12  u_char stratum;

13  u_char ppoll;

14  int    precision:8;

15  struct s_fixedpt distance;

16  struct s_fixedpt dispersion;

17  uint32_t refid;

18  struct l_fixedpt reftime;

19  struct l_fixedpt org;

20  struct 1_fixedpt rec;

21  struct l_fixedpt xmt;

22 };

23 #define VERSION_MASK 0x38

24 #define MODE_MASK 0x07

25 #define MODE CLIENT 3

26 #define MODE_SERVER 4

27 #define MODE_BROADCAST 5

2-22 l_fixedpt задает 64-разрядные числа с фиксированной точкой, используемые NTP для отметок времени, a s_fixedpt — 32-разрядные значения с фиксированной точкой, также используемые NTP. Структура ntpdata представляет 48-байтовый формат пакета NTP.

В листинге 21.12 пpeдcтaвлeнa функция main.

Листинг 21.12. Функция main

//ssntp/main.c

 1 #include "sntp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  char buf[MAXLINE];

 7  ssize_t n;

 8  socklen_t salen, len;

 9  struct ifi_info *ifi;

10  struct sockaddr *mcastsa, *wild, *from;

11  struct timeval now;

12  if (argc != 2)

13   err_quit("usage: ssntp <Ipaddress>");

14  sockfd = Udp_client(argv[1], "ntp", (void**)&mcastsa, &salen);

15  wild = Malloc(salen);

16  memcpy(wild, mcastsa. salen); /* копируем семейство и порт */

17  sock_set_wild(wild, salen);

18  Bind(sockfd, wild, salen); /* связываем сокет с универсальным[3]

                                  адресом */

19 #ifdef MCAST

20  /* получаем список интерфейсов и обрабатываем каждый интерфейс */

21  for (ifi = Get_ifi_info(mcastsa->sa_family, 1); ifi != NULL;

22   ifi = ifi->ifi_next) {

23   if (ifi->ifi_flags & IFF_MULTICAST) {

24    Mcast_join(sockfd, mcastsa, salen, ifi->ififname, 0);

25    printf("joined %s on %s ",

26     Sock_ntop(mcastsa, salen), ifi->ifi_name);

27   }

28  }

29 #endif

30  from = Malloc(salen);

31  for (;;) {

32   len = salen;

33   n = Recvfrom(sockfd, buf, sizeof(buf), 0, from, &len);

34   Gettimeofday(&now, NULL);

35   sntp_proc(buf, n, &now);

36  }

37 }

Получение IP-адреса многоадресной передачи

12-14 При выполнении программы пользователь должен задать в качестве аргумента командной строки адрес многоадресной передачи, к которому он будет присоединяться. В случае IPv4 это будет 224.0.1.1 или имя ntp.mcast.net. В случае IPv6 это будет ff05::101 для области действия NTP, локальной в пределах сайта. Наша функция udp_client выделяет в памяти пространство для структуры адреса сокета корректного типа (либо IPv4, либо IPv6) и записывает адрес многоадресной передачи и порт в эту структуру. Если эта программа выполняется на узле, не поддерживающем многоадресную передачу, может быть задан любой IP-адрес, так как в этой структуре задействуются только семейство адресов и порт. Обратите внимание, что наша функция udp_client не связывает адрес с сокетом (то есть не вызывает функцию bind) — она лишь создает сокет и заполняет структуру адреса сокета.

Связывание универсального адреса с сокетом

15-18 Мы выделяем в памяти пространство для другой структуры адреса сокета и заполняем ее, копируя структуру, заполненную функцией udp_client. При этом задаются семейство адреса и порт. Мы вызываем нашу функцию sock_set_wild, чтобы присвоить IP-адресу универсальный адрес, а затем вызываем функцию bind.

Получение списка интерфейсов

20-22 Наша функция get_ifi_info возвращает информацию обо всех интерфейсах и адресах. Запрашиваемое нами семейство адреса берется из структуры адреса сокета, заполненной функцией udp_client на основе аргумента командной строки.

Присоединение к группе

23-27 Мы вызываем нашу функцию mcast_join, чтобы присоединиться к группе, заданной аргументом командной строки для каждого интерфейса, поддерживающего многоадресную передачу. Все эти присоединения происходят на одном сокете, который использует эта программа. Как отмечалось ранее, количество присоединений на одном сокете ограничено константой IP_MAX_MEMBERSHIPS (которая обычно имеет значение 20), но лишь немногие многоинтерфейсные узлы используют столько интерфейсов.

Чтение и обработка всех пакетов NTP

30-36 В памяти размещается другая структура адреса сокета для хранения адреса, возвращаемого функцией recvfrom, и программа входит в бесконечный цикл, считывая все пакеты NTP, которые получает узел, и вызывая нашу функцию sntp_proc (описывается далее) для обработки пакета. Поскольку сокет был связан с универсальным адресом и присоединение к группе произошло на всех интерфейсах, поддерживающих многоадресную передачу, сокет должен получить любой пакет NTP направленной, широковещательной или многоадресной передачи, получаемый узлом. Перед вызовом функции sntp_proc мы вызываем функцию gettimeofday, чтобы получить текущее время, потому что функция sntp_proc вычисляет разницу между временем пакета и текущим временем.

Наша функция sntp_proc, показанная в листинге 21.13, обрабатывает пакет NTP.

Листинг 21.13. Функция sntp_proc: обработка пакета NTР

//ssntp/sntp_proc.c

 1 #include "sntp.h"

 2 void

 3 sntp proc(char *buf, ssize_t n, struct timeval *nowptr)

 4 {

 5  int version, mode;

 6  uint32_t nsec, useci;

 7  double usecf;

 8  struct timeval diff;

 9  struct ntpdata *ntp;

10  if (n < (ssize_t)sizeof(struct ntpdata)) {

11   printf(" packet too small: %d bytes ", n);

12   return;

13  }

14  ntp = (struct ntpdata*)buf;

15  version = (ntp->status & VERSION_MASK) >> 3;

16  mode = ntp->status & MODE_MASK;

17  printf(" v%d, mode %d, strat %d, ", version, mode, ntp->stratum);

18  if (mode == MODE_CLIENT) {

19   printf("client ");

20   return;

21  }

22  nsec = ntohl(ntp->xmt.int_part) - JAN_1970;

23  useci = ntohl(ntp->xmt.fraction); /* 32-разрядная дробь */

24  usecf = useci; /* дробь в double */

25  usecf /= 4294967296.0; /* деление на 2**32 -> [0, 1.0) */

26  useci = usecf * 1000000.0; /* дробь в миллионную часть */

27  diff.tv_sec = nowptr->tv_sec - nsec;

28  if ((diff.tv_usec = nowptr->tv_usec - useci) < 0) {

29   diff.tv_usec += 1000000;

30   diff.tv_sec--;

31  }

32  useci = (diff.tv_sec * 1000000) + diff.tv_usec; /* diff в мс */

33  printf("clock difference = %d usec ", useci);

34 }

Ратификация пакета

10-21 Сначала мы проверяем размер пакета, затем выводим его версию, режим и слой (stratum) сервера. Если режимом является MODE_CLIENT, пакет является запросом клиента, а не ответом сервера, и мы игнорируем его.

Получение времени передачи из пакета NTP

22-34 В пакете NTP нас интересует поле xmt — отметка времени. Это 64-разрядное значение с фиксированной точкой, определяющее момент отправки пакета сервером. Поскольку отметки времени NTP отсчитывают секунды начиная с 1 января 1900 года, а отметки времени Unix — с 1 января 1970 года, сначала мы вычитаем JAN_1970 (число секунд в 70 годах) из целой части.

Дробная часть — это 32-разрядное целое без знака, которое может принимать значение от 0 до 4 294 967 295 включительно. Оно копируется из 32-разрядного целого (usecf) в переменную с плавающей точкой двойной точности (usecf) и делится на 4 294 967 296 (232). Результат больше либо равен 0.0 и меньше 1.0. Мы умножаем это число на 1 000 000 — число микросекунд в секунде, записывая результат в переменную useci как 32-разрядное целое без знака.

Число микросекунд лежит в интервале от 0 до 999 999 (см. упражнение 21.5). Мы преобразуем значение в микросекунды, поскольку отметка времени Unix, возвращаемая функцией gettimeofday, возвращается как два целых числа: число секунд и число микросекунд, прошедшее с 1 января 1970 года (UTC). Затем мы вычисляем и выводим разницу между истинным временем узла и истинным временем сервера NTP в микросекундах.

Один из факторов, не учитываемых нашей программой, — это задержка в сети между клиентом и сервером. Но мы считаем, что пакеты NTP обычно приходят как широковещательные или многоадресные пакеты в локальной сети, а в этом случае задержка в сети составит всего несколько миллисекунд.

Если мы запустим эту программу на узле macosx с сервером NTP на узле freebsd4, который с помощью многоадресной передачи отправляет пакеты NTP в сеть Ethernet каждые 64 с, то получим следующий результат:

macosx # ssntp 224.0.1.1

joined 224.0.1.1.123 on lo0

joined 224.0.1.1.123 on en1

v4, mode 5, strat 3, clock difference = 661 usec

v4, mode 5, strat 3, clock difference = -1789 usec

v4, mode 5, strat 3, clock difference = -2945 usec

v4, mode 5, strat 3, clock difference = -3689 usec

v4, mode 5, strat 3, clock difference = -5425 usec

v4, mode 5, strat 3, clock difference = -6700 usec

v4, mode 5, strat 3, clock difference = -8520 usec

Перед запуском нашей программы мы завершили на узле работу NTP-сервера, поэтому когда наша программа запускается, время очень близко к времени сервера. Мы видим, что этот узел отстал на 9181 мс за 384 с работы программы, то есть за 24 ч он отстанет на 2 с.

Данный текст является ознакомительным фрагментом.