Linux下C/C++ SNTP网络时间协议实现

对于许多应用程序,特别是在小型计算机和微控制器上,不需要NTP的最终性能。便开发了简单网络时间协议(SNTP),为功能较弱的计算机提供时钟同步,而这些计算机不需要NTP的复杂性。

而简单网络时间协议(SNTP)是一种用于同步计算机网络时钟的互联网协议,是网络时间协议(NTP)的一个子集。与NTP相比,需要更少的内存和处理能力。它用于精确时钟同步不是关键的应用程序。使用TCP/IP协议套件,UDP端口123。

SNTP协议套件

SNTP基于TCP/IP协议套件。它是一个应用层时间协议,是网络时间协议基础协议的一部分。SNTP与NTP一起使用用户数据报协议(UDP)进行通信。默认情况下,使用UDP端口123。

新SNTP安装的一个常见疏忽是,任何防火墙中的UDP端口都必须保持打开状态,以允许通信传输。如果客户端无法与服务器同步,请检查UDP端口123在防火墙配置中是否打开。

SNTP可以在IPv4和IPv6网络上运行,并由RFC 4330定义。

SNTP的工作原理

SNTP协议是基于定时信息分组交换的客户端-服务器协议。通常,客户端以单播模式操作。SNTP客户端以周期性的间隔(通常为每64秒一次)向NTP服务器发送一个引用其IP地址的请求。服务器通常与诸如GPS之类的硬件参考时钟同步。

在接收到来自服务器的响应时,客户端可以计算其内部时钟和服务器时钟之间的系统时间偏移。通过计算分组往返延迟来实现增强的同步。

如果需要,可以在广播或多播模式下配置SNTP。在这种情况下,服务器以周期性的间隔不断地广播时间戳数据包。客户端不断监听数据包,并在收到数据包后相应地调整系统时钟。

NTP和SNTP之间的区别

NTP开发了具有校准技术的复杂统计算法,旨在过滤微小的差异,以实现高度的时钟同步。精细的时间调整是通过调整主机系统时钟的“滴答”频率来实现的。NTP试图通过倾斜时间调整来无缝地进行时间校正,以避免阶跃变化。

冗余是通过连续监测多个时间参考来实现的。复杂的选择算法确定了最可靠和最稳定的。可以监控多个时间源,包括硬件时钟和网络时间源的混合,有助于增强鲁棒性。

NTP中还实现了许多安全功能。安全的客户端-服务器身份验证是通过实现对称密钥加密来实现的。这使得客户端能够确定接收到的时间戳的来源,从而减轻欺骗。

相比之下,SNTP采用了一种更简单的网络时钟同步方法。NTP算法的许多复杂性已经被去除或简化。

许多SNTP客户端不是倾斜时间,而是逐步调整时间。但是,步进时间可能会导致事件排序出现问题。例如,如果在生成两个事务之间后退一步,则它们将不会有正确的顺序。

SNTP也缺乏监控和过滤多个时间参考的能力。该协议通常只能被配置为从单个时间源进行操作。如果实现多个冗余时间源,这不是一个好的选择。为了简单起见,许多SNTP客户端没有实现安全身份验证技术,这可能会使系统容易受到恶意用户的攻击。

SNTP协议报文分析

下图描述了NTP和SNTP消息格式在消息中的IP和UDP标头之后。此格式为与RFC 1305中描述的NTP消息格式相同以下描述的引用标识符字段除外。对于SNTP客户端消息,其中大多数字段为零或已初始化具有预先指定的数据。

NTP消息格式:
Linux下C/C++ SNTP网络时间协议实现_第1张图片
Leap Indicator (LI):这是一个即将发生的两位代码警告,在当前时间的最后一分钟插入/删除的闰秒
值的定义如下:

 LI Meaning
 ---------------------------------------------
 0 no warning
 1 last minute has 61 seconds
 2 last minute has 59 seconds
 3 alarm condition (clock not synchronized)

Version Number (VN): 表示NTP/SNTP版本号,目前为4。如有必要进行区分,在IPv4、IPv6和OSI之间。

Mode:表示协议模式。这个值的定义如下:

 Mode Meaning
 ------------------------------------
 0 reserved
 1 symmetric active
 2 symmetric passive
 3 client
 4 server
 5 broadcast
 6 reserved for NTP control message
 7 reserved for private use

Stratum:表示层该字段仅在SNTP服务器消息中是重要的,其中,这些值定义如下:

 Stratum Meaning
 ----------------------------------------------
 0 kiss-o’-death message (see below)
 1 primary reference (e.g., synchronized by radio clock)
 2-15 secondary reference (synchronized by NTP or SNTP)
 16-255 reserved

Poll Interval: 用作二的指数,其中得到的值是最大间隔,在以秒为单位的连续消息之间。该领域意义重大,仅在SNTP服务器消息中,其中的值范围从4(16秒)到17(131072秒-约36小时)。

Precision:用作的指数二,其中得到的值是系统时钟的精度以秒为单位。此字段仅在服务器消息中有效,其中该值的范围从市电频率时钟的-6到市电频率的-20在一些工作站中发现微秒时钟。

Root Delay: 指示到主要参考源的总往返延迟,以秒为单位其中分数点在比特15和16之间。请注意变量可以同时具有正值和负值,具体取决于相对时间和频率偏移。该领域意义重大仅在服务器消息中,其中的值范围为负值几毫秒到几百的正值毫秒。

Root Dispersion:指示由于时钟频率容差引起的最大误差秒,小数点在比特15和16之间。此字段仅在服务器消息中有效,其中的值范围为零到几百毫秒。

Reference Identifier: 用于标识特定参考源。该领域仅在服务器消息,其中用于层0和1(主服务器),值为四个字符的ASCII字符串,左侧对齐并零填充到32位。对于IPv4辅助服务器,该值是同步源的32位IPv4地址。对于IPv6和OSI辅助服务器,该值为同步的IPv6或NSAP地址的MD5哈希

Reference Timestamp: 此字段是系统时钟最后的时间以64位时间戳格式设置或校正。

Originate Timestamp: 这是请求离开的时间服务器的客户端,采用64位时间戳格式。

Receive Timestamp: 这是请求到达的时间服务器或回复到达客户端,时间戳为64位。

Transmit Timestamp:这是请求离开的时间客户端或回复离开服务器,时间戳为64位。

SNTP消息结构:

typedef struct {
  uint8_t flags;
  /* 八位标志-跳跃指示器、版本号和模式 
   0 1 2 3 4 5 6 7
  +-+-+-+-+-+-+-+-+
  |LI | VN  |Mode |
  +-+-+-+-+-+-+-+-+
  */
  uint8_t stratum;         
  uint8_t pollInterval;     
  uint8_t precision;           
  uint32_t rootDelay;
  uint32_t rootDispersion;
  uint32_t refIdentifier;
  ntp_timestamp refTimestamp;
  ntp_timestamp orgTimestamp;
  ntp_timestamp recvTimestamp;
  ntp_timestamp transmitTimestamp;

} ntp_packet;

Wireshark 抓ntp包:

Linux下C/C++ SNTP网络时间协议实现_第2张图片

sntp 客户端与服务器代码C/C++实现

client:

创建并初始化NTP数据包,将传输时间戳设置为客户端一天中的时间,将数据包发送到NTP服务器。

void print_unix_time(struct timeval *tv);

void print_ntp_time(ntp_timestamp *ntp);

void print_ntp_packet(ntp_packet *p);

void convert_ntp_to_unix(ntp_timestamp *ntp, struct timeval *unix_time);

void convert_unix_to_ntp(struct timeval *unix_time, ntp_timestamp *ntp);

void host_to_network(ntp_packet *p);

void network_to_host(ntp_packet *p);
void set_client_request(ntp_packet *p);

void print_sntp_output(ntp_packet *p, double offset, double delay,
                       struct sockaddr_in their_addr, char *host);

void check_reply(ntp_packet *p, ntp_packet *r);

double ntp_to_double(ntp_timestamp *p);

double calculate_offset(ntp_packet *p, ntp_timestamp *t);

double calculate_delay(ntp_packet *p, ntp_timestamp *t);

int main(int argc, char *argv[]) 
{

...

  if (argc == 3) 
  {
    strcpy(host, argv[1]);
    portno = atoi(argv[2]);
  }
  else if (argc == 1) 
  {
    strcpy(host, SERVER);
    portno = PORT;
    printf("Server and port not specified.\n");
    printf("Using default server: %s\nUsing default port: %d", host, portno);
  }
  else
  {
    fprintf(stderr, "usage: %s hostname port\n", argv[0]);
    exit(1);
  }

  if((he = gethostbyname(host)) == NULL) 
  {
    perror("client gethostbyname");
    exit(1);
  }

  if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) 
  {
    perror("client socket");
    exit(1);
  }

  tv.tv_sec = 10;
  tv.tv_usec = 0;
  if((setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) < 0) 
  {
    printf("socket timeout error no: %d", errno);
  }

  printf("\nSending..\n");

  memset(&their_addr, 0, sizeof(their_addr));
  their_addr.sin_family = AF_INET;
  their_addr.sin_port = htons(portno);
  their_addr.sin_addr = *((struct in_addr *)he->h_addr);

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

  set_client_request(&packet);
...

  socklen_t addr_len = (socklen_t)sizeof(struct sockaddr);
  if((numbytes = recvfrom(sockfd, &recvBuf, sizeof(ntp_packet), 0,
  (struct sockaddr *) &their_addr, &addr_len)) == -1) 
  {
    perror("server recvfrom");
    exit(1);
  }


  network_to_host(&recvBuf);

  printf("\n\nReceived:\n");

  /* 数据包到达时创建的目标时间戳,用于偏移和延迟 */
  ntp_timestamp destTimestamp = getCurrentTimestamp();

  /* 执行基本检查以检查服务器答复的有效性 */
  check_reply(&packet, &recvBuf);

  /* 使用接收到的数据包和目的地时间戳计算偏移量和延迟 */
  double offset = calculate_offset(&recvBuf, &destTimestamp);
  double delay = calculate_delay(&recvBuf, &destTimestamp);

  /* 打印格式化输出 */
  print_sntp_output(&recvBuf, offset, delay, their_addr, host);

  print_ntp_packet(&recvBuf);

...
}

server:
接收客户端请求,正确设置服务器答复,向客户端发送回复。


...
int main(int argc, char *argv[]) 
{
...

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

  if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) 
  {
	  perror("server socket");
	  exit(1);
  }

  memset(&my_addr, 0, sizeof(my_addr)); 
  my_addr.sin_family = AF_INET;         
  my_addr.sin_port = htons(MYPORT);    
  my_addr.sin_addr.s_addr = INADDR_ANY;

  if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) 
  {
    perror("server bind");
    exit(1);
  }

  socklen_t addr_len = (socklen_t)sizeof(struct sockaddr);

  while(1)
  {
...

    set_server_reply(&packet);

    /* send packet */
    if((numbytes = sendto(sockfd, &packet, sizeof(ntp_packet), 0,
      (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) 
      {
        perror("client sendto");
        exit(1);
    }
    ...
  }

...
}

运行结果:

Linux下C/C++ SNTP网络时间协议实现_第3张图片
Linux下C/C++ SNTP网络时间协议实现_第4张图片

If you need the complete source code, please add the WeChat number (c17865354792)

总结

简单网络时间协议(SNTP)是为内存和处理能力有限的小型计算机和微控制器开发的。它为不需要全面NTP精度的应用程序提供时钟同步。NTP更适合于同步大型计算机集群,例如公司网络。其校准算法的稳健性有助于大型计算机系统可靠地保持精确的时间同步。

Welcome to follow WeChat official account【程序猿编码

参考:RFC 4330、RFC 768、RFC 5905

你可能感兴趣的:(C/C++,C/C++,linux,客户端与服务器,ntp,sntp)