NTP协议详解及C语言实现

NTP协议详解

一、前言

    NTP 是一种通过网络在计算机之间进行时钟同步的协议,它工作在 OSI 模型的应用层,通过一系列原理与算法,实现以极小的误差,将所有网络中的计算机与 UTC 同步。由于时钟硬件精度的限制,离线的设备不总是能时刻与 UTC 同步,误差随着时间累积使计算机的本地时钟产生较大的偏差。此外,设备初次启动,启动前时钟仍处于默认状态,也需要与现在的时间同步。因此,通过互联网与可靠的时间源同步是必要的。通过这一协议,设备将寻找合适的同步源,将自身时钟与同步源同步,以保证依赖时间的应用能正常运行。

二、NTP工作原理

  1. 客户端发送一个NTP消息包给服务器,该消息包携带该包离开客户端时的时间戳。假设该时间戳为10:00:00am(T1)
  2. 当此NTP消息包到达服务器时,服务器加上自己的时间戳,假设该时间戳为11:00:01am(T2)
  3. 当次NTP消息包离开服务器时,路由器在加上该包离开自己的时间戳,假设该时间戳为11:00:02am(T3)
  4. 当客户端接收到该响应消息包是,加上一个新的时间戳,假设该时间戳为10:00:03am(T4)

至此,客户端已经拥有足够的信息来计算两个重要的参数:

NTP消息来回一个周期的实验Delay=(T4-T1)-(T3-T2);

客户端相对于服务端的时间差Offset=((T2-T1)-(T4-T3))/2

综上所述,客户端就能根据这些信息来设定自己的时钟,使之与服务器的时钟同步。

三、 NTP 的数据格式

NTP 定义了两种数据格式:时间戳(timestamp)和日期戳(datestamp)。它们分别提供不同范围的时间表示法。

时间戳长 64 位,前 32 位表示从时代起点(era epoch,目前起点为 1900/1/1 00:00:00)开始的秒数,后 32 位表示秒数的小数部分。这样的时间表示具有 232 皮秒(即 232×10-12 秒)的精度。但这种表示法只能表示有限范围内的时间,因此时间戳存在一个 136 年的周期,一个周期称为一个 NTP 的时代(era)。下一个时代的起点在 2036 年。

日期戳长 128 位,前 32 位表示时代编号(从 0 开始),后 96 位是一个时间戳,分别为 32 位的秒数和 64 位的小数部分。容易发现,日期戳能表示的时间范围更广,精度也更高,它的跨度长于宇宙的年龄,精度小于光通过一个原子的时间。

时间戳常用于通信时的数据包中,而日期戳一般只在实现内部使用。

四、NTP报文格式

NTP的协议主要在RFC1350协议文档中进行了描述。

(1)报文结构

+---------------+---------------+---------------+---------------+

|    Head1      |     Head2     |     Head3     |     Head4     |

+---------------+---------------+---------------+---------------+

 0 1 2 3 4 5 6 7

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|LI | VN  |Mode |    Stratum    |     Poll      |   Precision   |

+---------------+---------------+---------------+---------------+

|                          Root Delay                           |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|          Seconds              |       Fractional Seconds      |

+---------------------------------------------------------------+

|                       Root Dispersion                         |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|          Seconds              |       Fractional Seconds      |

+---------------+---------------+---------------+---------------+

|                     Reference Identifier                      |

+---------------+---------------+---------------+---------------+

|                   Reference Timestamp (64)                    |

+---------------+---------------+---------------+---------------+

|                   Originate Timestamp (64)                    |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|            Org1               |              Org2             |

+---------------------------------------------------------------+

|                    Receive Timestamp (64)                     |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|            Ref1               |              Ref2             |

+---------------------------------------------------------------+

|                    Transmit Timestamp (64)                    |

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

|            Xmit1              |              Xmit2            |

+---------------------------------------------------------------+

|                    Destination Timestamp (64)                 |

+---------------------------------------------------------------+

|            Dst1               |               Dst2            |

+---------------------------------------------------------------+

|                          Extension  Field                     |

+---------------------------------------------------------------+

|                          Extension  Field                     |

+---------------------------------------------------------------+

|                           Key Identifier                      |

+---------------------------------------------------------------+

|                                dgst                           |

+---------------------------------------------------------------+

|                    1                   2                   3  |

|0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1|

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

(2) 字段含义

NTP报文格式如上图所示,它的字段含义参考如下:

  • LI 闰秒标识器 ,占用2bits,值为“11”时表示告警状态,时钟未被同步;其他值时NTP本身不做处理

1:no warning 未告警

2:last minute of the day has 61 seconds 最后一分钟有61秒

3:last minute of the day has 59 seconds 最后一分钟有59秒

4:unknown (clock unsynchronized) 未知(时钟未同步)

  • VN 版本号 ,占用3bits,表示NTP的版本号,目前最新版本为4.
  • Mode 模式 ,占用3bits,表示NTP的工作模式;不同的值所表示的含义分区是:

0:reserved 未定义

1:symmetric active 表示主动对等模式

2:symmetric passive 表示被动对等模式

3:client 表示客户模式

4:server 表示服务器模式

5:broadcast 表示广播模式或组播模式

6:NTP control message 表示此报文NTP控制报文

7:reserved for private use 预留给内部使用

  • Stratum ,系统时钟的层数,占用8bits

0 :unspecified or invalid 未指定或无效

1 :primary server (e.g., equipped with a GPS receiver) 主服务器

2-15 :secondary server (via NTP) 辅助服务器

16 :unsynchronized 不同步

17-255:reserved 保留

  • Poll 轮询间隔 ,占用8bits,表示两个NTP报文之间的最大时间间隔,以log2秒为单位,默认最小和最大的轮询间隔为610
  • Precision 精度 ),占用8bits,表示系统时钟的精度,以log2秒为单位

  • Root Delay 根时延 ),占用8bits,表示在到参考时钟的总往返的时延
  • Root Dispersion 根离散 ),占用8bits,表示到参考时钟的总色散
  • Reference Identifier 参考时钟标识符),占用32bits,用来标识特殊的参考源
  • Reference Timestamp 参考时间戳 ),占用64bits,系统时钟最后一次被修改的本地时间
  • Origin Timestamp 原始时间戳 ),占用64bitsNTP请求报文离开发送端时发送端的本地时间
  • Receive Timestamp 接受时间戳 ),占用64bitsNTP请求报文到达接收端的本地时间
  • Transmit Timestamp 传送时间戳 ),占用64bitsNTP应答报文离开应答者时的应答者本地时间
  • Destination Timestamp 目的时间戳 ),NTP应答包从服务器到达客户端的时间,(注:该字段不在标题字段中,它在数据包到达时确定,并在数据包缓冲区数据包结构中体现。)
  • Extension Field 2 ( 扩展字段,可变 )
  • Key Identifier 密钥标识符 ):客户端和服务器用于指定128MD5哈希值
  • dgst Message Digest,消息摘要 ):NTP数据包头和扩展字段

五、NTP工作模式

(1)单播客户端/服务器模式

1) 客户模式:运行在客户模式的主机定期向服务器端发送报文,报文中的Mode字段被设置为3(客户模式),不管服务器端是否可达及服务器端的层数。运行在这种模式的主机,通常是网络内部的工作站,它可以依照对方的时钟进行同步,但不会修改对方的时钟。

·2)服务器模式:运行在服务器模式的主机接收并回应报文,报文中的Mode字段设置为4(服务器模式),运行在服务器模式的主机,通常是网络内部的时间服务器,它可以向客户端提供同步信息,但不会修改自己的时钟。

运行在客户模式的主机在重新 启动时和重新启动后定期向运行在服务器模式的主机发送NTP报文;服务器收到客户端的报文后,首先将报文的目的IP地址和目的端口号分别与其源IP地址和源端口号相互相互交换,在填写所需要的信息,然后把报文发送给客户端。服务器在客户端发送请求之间无需保留任何状态信息,客户端根据本地情况自由管理发送报文的时间间隔。

(2)对等体模式

对等体模式下,主动对等体和被动对等体可以互相同步,等级第(层数大)的对等体向等级高(层数小)的对等体同步,主动对等体和被动对等体之间首先交互Mode字段为3(客户模式)和4(服务器模式)的NTP报文。

1)主动对等体:运行在这一模式下的主机定期发送报文,报文中的Mode设置为1(主动对等体)。在不考虑它的对等体是否可达以及对等体的层数,运行在这一模式下的主机可以向对方提供同步信息,也可以依照对方的时间信息同步本地时钟。

2)被动对等体:运行在这一模式的主机接收并回应报文,报文中的Mode字段设置为2(被动对等体)。运行在被动对等体模式的主机可以向对方提供同步信息,也可以依照对方的时间信息同步本地时钟。

3)运行被动对等体模式的必备条件:本机接收的报文来自一个运行在主动对等体模式下的对等体,且该对等体的层数等于或低于本机并路由可达

注:被动对等体模式运行在同步子网中层次较低层上时:这种模式下,不需要预先知道对等体的特性,因为只有当本机收到NTP报文时才建立连接及相关的状态变量。

(3)广播模式

1)运行在广播模式下,周期性向广播地址“255.255.255.255”发送时钟同步报文,报文中的Mode字段设置为5(广播/组播模式)。不管它的对等体是否可达或层数为多少。运行在广播模式的主机通常是网络内运行高速广播介质的时间服务器,向所有对等体提供同步信息,但不会修改自己的时钟。

2)客户端侦听来自服务器的广播消息包。当接收到第一个广播消息包后,客户端与服务器交互Mode字段为3(客户模式)和4(服务器模式)的NTP报文,即客户端先启用一个短暂的服务器/客户端模式与远程服务器交换消息,以获得客户端与服务器间的网络延迟。之后恢复广播模式,继续侦听广播消息包的到来,根据导来的广播消息包对本地时钟再次进行同步。

广播模式应用在有多台工作站、不需要很高的准确度的高速网络。典型的情况是网络中的一台或多态时间服务器定期向工作站广播报文,广播报文在毫秒级的延迟基础上确定时间。

(4)组播模式

1)服务器端周期向组播地址发送时钟同步报文,报文中的Mode字段设置为5(广播/组播模式)。运行组播模式的主机通常是网络内运行高速广播戒指的时间服务器,向所有对等体提供同步信息,但不会修改自己的时钟。

2)客户端侦听来自服务器的组播消息包,当接收导第一个组播消息包后,当客户端接收到第一个组播报文后,客户端与服务器交互Mode字段为3(客户模式)和4(服务器模式)的NTP报文,即客户端先启用一个短暂的服务器/客户模式与远程服务器交换消息,以获得客户端与服务器间的网络延迟,之后,客户端恢复组播模式,继续侦听组播消息包的到来,根据到来的组播消息包对本地时钟进行同步。

3)组播模式适用于有大量客户端分布在网络中的情况。通过在网络中使用NTP组播模式,NTP服务器发送的组播消息包可以到达网络中所有的客户端,从而降低由于NTP报文过多而给网络的造成的压力。

(5)Kiss-o`-Death(KOD)

KOD报文为客户端提供状态报告和接入控制等信息。在服务器上是能KOD后,服务器回向客户端发送DENY和RATE kiss码。

1)当客户端接收到DENY kiss码,客户端将断开与服务器的所有连接,并停止向服务器发送报文。

2)当客户端接收到RATE kiss码,客户端将立即缩短与该服务器的轮询时间间隔,且以后每次接收到RATE kiss码,轮询时间间隔都会进一步缩短。

六、NTP的应用

(1)时间信息的传输协议

时间信息的传输都使用UDP接口。服务器端口默认为123。使用的协议见NTP协议。

(2)常用时间服务器:

time.windows.com

time.ustc.edu.cn

cn.pool.ntp.org

asia.pool.ntp.org

cn.ntp.org.cn

ntp.aliyun.com

time.asia.apple.com

time.windows.com

(3)报文示例

//Send

1B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E3 E8 08 9B FD 70 A8 9B

//Recevice

1c 02 00 ee 00 00 00 07 00 00 02 9b ca 26 40 4c e3 e6 b9 62 09 7e 56 47 e3 e8 08 9b fd 70 a8 9b e3 e6 b9 6e c3 7f be fd e3 e6 b9 6e c3 80 ba a5(4

(4)NTP 时间的表示

  • NTP时间与 UNIX 时间

NTP 标度与 Unix 标度只相差一个常数 2208988800,这是 1900/1/1 NTP 标度起点)与 1970/1/1 Unix 标度起点)之间的秒数差别

整数时间解析

报文

转十进制

NTPUNIX时间标

得时间

e3 e6 d9 e3

3823557091

3823557091-2208988800=1614568291

2021-03-01 11:11:31

  • NTP 时间的小数部分

因为SNTP时间戳是重要的数据和用来描述协议主要产品的,一个专门的时间戳格式已经建立。NTP用时间戳表示为一64 bits  unsigned定点数,以秒的形式从190011日的000算起。整数部分在前32位里,后32bitsseconds  Fraction)用以表示秒以下的部分。在Seconds  Fraction 部分,无意义的低位应该设置为0。这种格式把方便的多精度算法和变换用于UDP/TIME的表示(单位:秒),但使得转化为ICMP的时间戳消息表示法(单位:毫秒)的过程变得复杂了。它代表的精度是大约是200picoseconds,这应该足以满足最高的要求了

注:1 picoseconds皮秒)等于一万亿分之一(即10的负12次方)秒。

小数时间解析

报文

转十进制

转毫秒

得时间

18 37 0c dc

406260956

406260956*0.000000000200*1000

81MS

七、程序实现(MFC)

//---------------------------------------------------------------------------
#include <WinSock.h>
#include <TIME.H>
//#pragma comment (lib,"Ws2_32")

struct   NTP_Packet
{
    int      Control_Word;   
    int      root_delay;   
    int      root_dispersion;   
    int      reference_identifier;   
    __int64    reference_timestamp;   
    __int64 originate_timestamp;   
    __int64 receive_timestamp;   
    int      transmit_timestamp_seconds;   
    int      transmit_timestamp_fractions;   
};

/************************************************************************/
/* 函数说明:自动与时间服务器同步更新
/* 参数说明:无
/* 返 回 值:成功返回TRUE,失败返回FALSE
/************************************************************************/
BOOL UpdateSysTime()
{
    // 国内常用NTP服务器地址及IP
    // https://www.douban.com/note/171309770/
    WORD    wVersionRequested;
    WSADATA wsaData;
    
    // 初始化版本
    wVersionRequested = MAKEWORD( 1, 1 );
    if (0!=WSAStartup(wVersionRequested, &wsaData)) 
    {
        WSACleanup();
        return FALSE;
    }
    if (LOBYTE(wsaData.wVersion)!=1 || HIBYTE(wsaData.wVersion)!=1) 
    {
        WSACleanup( );
        return FALSE; 
    }
    
    // 这个IP是中国大陆时间同步服务器地址,可自行修改
    SOCKET soc=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
    struct sockaddr_in addrSrv;
    

    // time.ustc.edu.cn 202.38.64.7
    // ntp.aliyun.com  203.107.6.88
    addrSrv.sin_addr.S_un.S_addr=inet_addr("203.107.6.88");//time.ustc.edu.cn");
    addrSrv.sin_family=AF_INET;
    addrSrv.sin_port=htons(123);
    
    NTP_Packet NTP_Send,NTP_Recv; 
    NTP_Send.Control_Word   =   htonl(0x1B000000);   
    NTP_Send.root_delay        =   0;   
    NTP_Send.root_dispersion   =   0;   
    NTP_Send.reference_identifier    =   0;   
    NTP_Send.reference_timestamp    =   0;   
    NTP_Send.originate_timestamp    =   0;   
    NTP_Send.receive_timestamp        =   0;   
    NTP_Send.transmit_timestamp_seconds        =   0;   
    NTP_Send.transmit_timestamp_fractions   =   0; 
    
    if(SOCKET_ERROR==sendto(soc,(const char*)&NTP_Send,sizeof(NTP_Send),
        0,(struct sockaddr*)&addrSrv,sizeof(addrSrv)))
    {
        closesocket(soc);
        return FALSE;
    }
    int sockaddr_Size =sizeof(addrSrv);
    if(SOCKET_ERROR==recvfrom(soc,(char*)&NTP_Recv,sizeof(NTP_Recv),
        0,(struct sockaddr*)&addrSrv,&sockaddr_Size))
    {
        closesocket(soc);
        return FALSE;
    }
    closesocket(soc);
    WSACleanup();
    
    SYSTEMTIME    newtime;
    float        Splitseconds;
    struct        tm    *lpLocalTime;
    time_t        ntp_time;
    
    // 获取时间服务器的时间
    ntp_time    = ntohl(NTP_Recv.transmit_timestamp_seconds)-2208988800;
    lpLocalTime = localtime(&ntp_time);
    if(lpLocalTime == NULL)
    {
        return FALSE;
    }
    
    // 获取新的时间
    newtime.wYear      =lpLocalTime->tm_year+1900;
    newtime.wMonth     =lpLocalTime->tm_mon+1;
    newtime.wDayOfWeek =lpLocalTime->tm_wday;
    newtime.wDay       =lpLocalTime->tm_mday;
    newtime.wHour      =lpLocalTime->tm_hour;
    newtime.wMinute    =lpLocalTime->tm_min;
    newtime.wSecond    =lpLocalTime->tm_sec;
    
    // 设置时间精度
    Splitseconds=(float)ntohl(NTP_Recv.transmit_timestamp_fractions);
    Splitseconds=(float)0.000000000200 * Splitseconds;
    Splitseconds=(float)1000.0 * Splitseconds;
    newtime.wMilliseconds   =   (unsigned   short)Splitseconds;
    
    // 修改本机系统时间
    SetLocalTime(&newtime);
    return TRUE;
}


int main(int argc, char* argv[])
{
    UpdateSysTime();
    return 0;
}

  • 4
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
NTP(Network Time Protocol)是一种用于同步计算机时间的协议。在 Linux 中,可以使用 NTP 客户端来同步本地计算机的时钟。以下是一个简单的 C 语言程序,可以使用 NTP 客户端来获取网络时间并将其设置为系统时间: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netinet/ip.h> #include <netinet/udp.h> #include <time.h> #define NTP_PORT 123 #define NTP_PACKET_SIZE 48 #define NTP_TIMESTAMP_DELTA 2208988800ull typedef struct ntp_packet { uint8_t li_vn_mode; uint8_t stratum; uint8_t poll; uint8_t precision; uint32_t root_delay; uint32_t root_dispersion; uint32_t ref_id; uint64_t ref_timestamp; uint64_t orig_timestamp; uint64_t recv_timestamp; uint64_t transmit_timestamp; } ntp_packet; void error(char *msg) { perror(msg); exit(1); } uint64_t get_ntp_time() { int sockfd, n; struct sockaddr_in serv_addr; ntp_packet packet; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr("pool.ntp.org"); serv_addr.sin_port = htons(NTP_PORT); sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) error("ERROR opening socket"); memset(&packet, 0, sizeof(packet)); packet.li_vn_mode = (0x3 << 6) | (0x3 << 3) | 0x3; if (sendto(sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) error("ERROR in sendto"); memset(&packet, 0, sizeof(packet)); if ((n = recv(sockfd, &packet, sizeof(packet), 0)) < 0) error("ERROR in recv"); close(sockfd); uint64_t timestamp = ntohl(packet.transmit_timestamp >> 32); timestamp = (timestamp << 32) | ntohl(packet.transmit_timestamp & 0xFFFFFFFF); timestamp -= NTP_TIMESTAMP_DELTA; return timestamp; } int main() { time_t now; struct tm *tm; uint64_t ntp_time; time(&now); tm = localtime(&now); printf("Local time: %s", asctime(tm)); ntp_time = get_ntp_time(); tm = gmtime((time_t *)&ntp_time); printf("NTP time: %s", asctime(tm)); if (settimeofday((struct timeval *)&ntp_time, NULL) < 0) error("ERROR in settimeofday"); time(&now); tm = localtime(&now); printf("Local time after setting NTP time: %s", asctime(tm)); return 0; } ``` 在上述程序中,我们使用 `get_ntp_time()` 函数获取当前网络时间。该函数发送一个 NTP 数据包到 NTP 服务器,并等待服务器响应。在收到响应后,函数从响应中提取时间戳,并将其转换为本地时间戳。 然后,我们使用 `settimeofday()` 函数将获取的网络时间设置为系统时间。最后,我们打印出本地时间和设置后的时间。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值