NTP服务器
1、NTP服务器
NTP协议是网络时间协议(Network Time Protocol),它是用来同步网络中各个计算机的时间的协议。它的用途是把计算机的时钟同步到世界协调时UTC,其精度在局域网内可达0.1ms,在互联网上绝大多数的地方其精度可以达到1-50ms。它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击。时间按NTP服务器的等级传播。按照离外部UTC源的远近把所有服务器归入不同的Stratum(层)中。
NTP要提供准确的时间,就必须有准确的时间来源,那可以用格林尼治时间吗?答案是否定的。因为格林尼治时间是以地球自转为基础的时间计量系统,但是地球每天的自转是有些不规则的,而且正在缓慢加速,因此,格林尼治时间已经不再被作为标准时间使用。
新的标准时间,是由原子钟报时的国际标准时间UTC(Universal Time Coordinated,世界协调时)。所以NTP获得UTC的时间来源可以是原子钟、天文台、卫星,也可以从Internet上获取。
有了准确而可靠的的时间源,那这个时间如何传播呢?
计算机主机一般同多个时间服务器连接, 利用统计学的算法过滤来自不同服务器的时间,以选择最佳的路径和来源来校正主机时间。即使主机在长时间无法与某一时间服务器相联系的情况下,NTP服务依然有效运转。
为防止对时间服务器的恶意破坏,NTP使用了识别(Authentication)机制,检查来对时的信息是否是真正来自所宣称的服务器并检查资料的返回路径,以提供对抗干扰的保护机制。
2、UTC
协调世界时,即格林威治平太阳时间,又称世界统一时间、世界标准时间、国际协调时间。由于英文(CUT)和法文(TUC)的缩写不同,作为妥协,简称UTC。
协调世界时是以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。中国大陆采用ISO 8601-1988的《数据元和交换格式信息交换日期和时间表示法》(GB/T 7408-1994)称之为国际协调时间,代替原来的GB/T 7408-1994;中国台湾采用CNS 7648的《资料元及交换格式–资讯交换–日期及时间的表示法》,称之为世界统一时间。
这套时间系统被应用于许多互联网和万维网的标准中,例如,网络时间协议就是协调世界时在互联网中使用的一种方式。
GMT(Greenwich Mean Time,格林威治时间)是英国格林尼治当地时间,它和UTC是的时间是一样的,及GMT=UTC+0。
在军事中,协调世界时区会使用“Z”来表示。又由于Z在无线电联络中使用“Zulu”作代称,协调世界时也会被称为"Zulu time"。中国大陆、中国香港、中国澳门、中国台湾、蒙古国、新加坡、马来西亚、菲律宾、西澳大利亚州的时间与UTC的时差均为+8,也就是UTC+8。
3、NTP报文格式
NTP是从时间协议(Time Protocol)和ICMP时间戳报文(ICMP TimeStamp Message)演变而来,在准确性和健壮性方面进行了特殊的设计,理论上精度可达十亿分之一秒。
NTP协议应用于分布式时间服务器和客户端之间,实现客户端和服务器的时间同步,从而使网络内所有设备的时钟基本保持一致。
NTP协议是基于UDP进行传输的,使用端口号为123。
字段名 | 长度 | 含义 |
---|---|---|
LI(Leap Indicator) | 2比特 | 这是一个两位的代码,表示在NTP时间标尺中将要插入的下一跳情况。值为“11”时表示告警状态,时钟不能被同步。 |
VN(Version Number) | 3比特 | NTP的版本号。 |
Mode | 3比特 |
NTP的工作模式。不同值表示的含义如下: 0:reserved,保留。 1:symmetric active,主动对等体模式。 2:symmetric passive,被动对等体模式。 3:client,客户模式。 4:server,服务器模式。 5:broadcast,广播模式。 6:reserved for NTP control messages,NTP控制报文。 7:reserved for private use,内部使用预留。 |
Stratum | 8比特 | 时钟的层数,定义了时钟的准确度。层数为1的时钟准确度最高,从1到15依次递减。 |
Poll Interval | 8比特 | 轮询时间,即发送报文的最小间隔时间。 |
Precision | 8比特 | 时钟的精度。 |
Root Delay | 32比特 | 到主参考时钟的总往返延迟时间。 |
Root Dispersion | 32比特 | 本地时钟相对于主参考时钟的最大误差。 |
Reference Identifier | 32比特 | 标识特定参考时钟。 |
Reference Timestamp | 64比特 | 本地时钟最后一次被设定或更新的时间。如果值为0表示本地时钟从未被同步过。 |
Originate Timestamp | 64比特 | NTP报文离开源端时的本地时间。 |
Receive Timestamp | 64比特 | NTP报文到达目的端的本地时间。 |
Transmit Timestamp | 64比特 | 目的端应答报文离开服务器端的本地时间。 |
Authenticator | 96比特 | (可选)验证信息。 |
字段名 | 长度 | 含义 |
---|---|---|
0 | 2比特 | 保留位。NTP本身不做处理。 |
VN(Version Number) | 3比特 | NTP的版本号,目前值为3。 |
6 | 3比特 | 表明是控制报文。 |
REM | 3比特 |
R:0表示命令,1表示响应。 E:0表示发送正常响应,1表示发送错误响应。 M:0表示最后一个分片,1表示其他。 |
Op | 5比特 | 操作码,表明命令的类型。 |
Sequence | 16比特 | 发送或接受到报文的顺序号。 |
Status | 16比特 | 表明当前系统的状态。 |
Association ID | 16比特 | 连接标示。 |
Offset | 16比特 | 偏移量。 |
Count | 16比特 | 数据域的长度。 |
Data | 最大468比特 | 包括发送报文或接受报文中的数据信息。 |
Padding | 16比特 | 填充字段。 |
Authenticator | 96比特 | (可选)验证信息。 |
//NTP的端口号 NTP_PORT 123 //常见的NTP的IP地址 uint8_t ntp_server_ip[4]={103,11,143,248}; //新加坡 uint8_t ntp_server_ip[4]={133,243,238,243}; //日本东京 uint8_t ntp_server_ip[4]={85,199,214,100}; //英国 uint8_t ntp_server_ip[4] = {202,120,2,101}; //上海交通大学ntp服务器IP地址202.120.2.101 uint8_t ntp_server_ip[4] = {210,72,145,44}; //国家授时中心服务器IP地址 210,72,145,44 uint8_t ntp_server_ip[4] = {202,118,1,81}; //辽宁省沈阳市 教育网 uint8_t ntp_server_ip[4]={203,107,6,88}; //阿里云 uint8_t ntp_server_ip[4]={120,25,115,20}; //阿里云 uint8_t ntp_server_ip[4]={120,25,108,11}; //阿里云
//报文结构体 typedef struct NTP_MessageTypeDef { unsigned int leap : 2;/* leap indicator */ unsigned int version : 3;/* version number ntp版本号*/ unsigned int mode : 3;/* mode ntp工作模式*/ unsigned int stratum : 8;/* stratum 时钟层数*/ unsigned int poll : 8;/* poll interval 轮询时间*/ unsigned int precision : 8;/* precision 时钟的精度*/ unsigned int rootdelay; /* root delay 到主参考时钟的总往返延迟时间*/ unsigned int rootdisp; /* root dispersion 本地时钟相对于主参考时钟的最大误差*/ unsigned int refid; /* reference ID 标识特定参考时钟*/ unsigned long long reftime; /* reference time 本地时钟最后一次被设定或更新的时间*/ unsigned long long oritime; /* origin timestamp NTP报文离开源端时的本地时间*/ unsigned long long rectime; /* receive timestamp NTP报文到达目的端的本地时间*/ unsigned long long tratime; /* transmit timestamp 目的端应答报文离开服务器端的本地时间*/ unsigned int Authenticator[3]; /* Authenticator (可选)验证信息 */ } NTP_MessageTypeDef;
Leap Indicator(LI) :闰秒指示符,这是一个2位的代码,用于警示在当天的最后一分钟里插入或删除的闰秒。取值如下: 0:无预告 1:最近一分钟有61秒 2:最近一分钟有59秒 3:警告状态(时钟未同步) Version Number(VN) :版本号,这是一个3位的整数,用于表示NTP的版本。 Mode :模式,这是一个3位的整数,表示模式,值定义如下: 0:保留 1:对称主动 2:对称被动 3:客户端 4:服务器端 5:广播 6:为NTP控制控制消息 7:为自用保留 Stratum :本地时钟层级,这是一个八位无符号整数,表示本地时钟的层级,其值定义如下: 0:未定义或难以获得 1:主要参考(如无线电时钟钟,校正的院子时钟) 2-255:第二参考(通过NTP或SNTP) Poll :轮询间隔,这是一个8位有符号整数,用于表示连续消息之间的最大间隔,以最接近2的N次幂来表示。如值为6表示2^6=64。 Precision :本地时钟精度精度,这是一个8位有符号整数,用于表示本地时钟精度,以最接近2的N次幂来表示。 Root Delay :这是一个32位有符号定点数,表示主要参考源的总往返时延,以秒为单位。该变量可以为正值和负值,具体取决于时间精度和偏移。 Root Dispersion :这是一个32位有符号定点数,表示相对于主参考源的最大误差,以秒为单位,在15和16位之间。通常在该字段中出现的值范围为0到几百毫秒 Reference Identifier :这是一个标识特定参考源的32位位串。在NTP版本3或版本4层级0或层级1服务器的情况下,这是一个4字符ASCII字符串,左对齐并且以0填充到32位。在NTP版本3辅助服务器中,这是参考源的32位IPv4地址。 Reference Timestamp :这是以64位时间戳格式表示的上次设置或更正的本地时钟时间。 Original Timestamp :这是以64位时间戳格式表示的请求离开客户端的时间。 Receive Timestamp :这是以64位时间戳格式表示的请求到达服务器端的时间。 Transmit Timestamp :这是以64位时间戳格式表示的应答离开服务器端的时间。 Authentication :认证信息。
测试程序
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<winsock2.h> #pragma comment(lib,"ws2_32.lib") typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t; #define NTP_PORT 123 //NTP的端口 #define SECS_PERDAY 86400UL // 一天的秒数 day = 60*60*24 #define NTP_START_TIME_YEAR 1900 //微软的时间戳是从1900.1.1算起,而unix的时间戳是从1970.1.1算起 #define FOUR_YEAR_DAYS (366+365+365+365) //4年一个周期内的总天数(NTP_START_TIME_YEAR~2038不存在2100这类年份,故暂不优化)1900开始:(366+365+365+365),但是1900不是闰年,计算完后要加1天 1970开始:(365+365+366+365) #define BUFFER_SIZE 1024 //缓冲区大小 //NTP服务器报文 typedef struct NTP_MessageTypeDef { unsigned int leap : 2;/* leap indicator */ unsigned int version : 3;/* version number ntp版本号*/ unsigned int mode : 3;/* mode ntp工作模式*/ unsigned int stratum : 8;/* stratum 时钟层数*/ unsigned int poll : 8;/* poll interval 轮询时间*/ unsigned int precision : 8;/* precision 时钟的精度*/ unsigned int rootdelay; /* root delay 到主参考时钟的总往返延迟时间*/ unsigned int rootdisp; /* root dispersion 本地时钟相对于主参考时钟的最大误差*/ unsigned int refid; /* reference ID 标识特定参考时钟*/ unsigned long long reftime; /* reference time 本地时钟最后一次被设定或更新的时间*/ unsigned long long oritime; /* origin timestamp NTP报文离开源端时的本地时间*/ unsigned long long rectime; /* receive timestamp NTP报文到达目的端的本地时间*/ unsigned long long tratime; /* transmit timestamp 目的端应答报文离开服务器端的本地时间*/ unsigned int Authenticator[3]; /* Authenticator (可选)验证信息 */ } NTP_MessageTypeDef; typedef struct NTP_TimeTypeDef { uint16_t year; //年 uint8_t month; //月 uint8_t date; //日 uint8_t week; //星期 uint8_t hour; //小时 uint8_t minute; //分钟 uint8_t second; //秒钟 unsigned long long time_s; //秒时间 }NTP_TimeTypeDef;
#include "ntp.h" static uint8_t peace_month_days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//平年每个月的天数 static uint8_t leap_month_days[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //闰年每个月的天数 /************************************************************************************************ * @ 函 数 名:NTP_GenerateReqMessage * @ 功能说明:生成NTP服务器请求报文 * @ 参 数:p_Message_Buf:报文缓冲区 * @ 返 回 值:无 ************************************************************************************************/ static void NTP_GenerateReqMessage(uint8_t *p_Message_Buf) { NTP_MessageTypeDef NTP_MessageStruct; NTP_MessageStruct.leap = 0; NTP_MessageStruct.version = 4; NTP_MessageStruct.mode = 3; NTP_MessageStruct.stratum = 0; NTP_MessageStruct.poll = 0; NTP_MessageStruct.precision = 0; NTP_MessageStruct.rootdelay = 0; NTP_MessageStruct.rootdisp = 0; NTP_MessageStruct.refid = 0; NTP_MessageStruct.reftime = 0; NTP_MessageStruct.oritime = 0; NTP_MessageStruct.rectime = 0; NTP_MessageStruct.tratime = 1; NTP_MessageStruct.Authenticator[0] = 0; NTP_MessageStruct.Authenticator[1] = 0; NTP_MessageStruct.Authenticator[2] = 0; //memcpy((void *)p_Message_Buf, (void const*)(&NTP_MessageStruct), sizeof(NTP_MessageStruct)); uint8_t temp = 0; temp = (NTP_MessageStruct.leap << 6) + (NTP_MessageStruct.version << 3) + (NTP_MessageStruct.mode); p_Message_Buf[0] = 0x23; // 0x23 = NTP_MessageStruct.leap << 6 + NTP_MessageStruct.version << 3 + NTP_MessageStruct.mode return; } /************************************************************************************************ * @ 函 数 名:NTP_WeekCalculate * @ 功能说明:根据日期计算星期 * @ 参 数:p_NTP_TimeStruct:时间结构体 * @ 返 回 值:无 ************************************************************************************************/ static uint8_t NTP_WeekCalculate(NTP_TimeTypeDef *p_NTP_TimeStruct) { uint16_t year = 0; uint8_t month = 0; uint8_t date = 0; uint8_t week = 0; year = p_NTP_TimeStruct->year; month = p_NTP_TimeStruct->month; date = p_NTP_TimeStruct->date; if (month == 1 || month == 2) { month += 12; year--; } week = (date + 2 * month + 3 * (month + 1) / 5 + year + year / 4 - year / 100 + year / 400) % 7; return (week + 1); } /************************************************************************************************ * @ 函 数 名:NTP_Leapyearjudgment * @ 功能说明:判断是否是闰年 * @ 参 数:Year:年 * @ 返 回 值:0: 平年 1:闰年 ************************************************************************************************/ static uint8_t NTP_Leapyearjudgment(uint16_t Year) { uint8_t ret = 0; if (Year % 400 == 0)//能被400整除的就是闰年 { ret = 1; } else { if (Year % 4 == 0 && Year % 100 != 0)//不能被400整除,但是能被4整除并且不能被100整除是闰年 { ret = 1; } else//不能被400整除,但是不能被4整除或者能被100整除是平年 { ret = 0; } } return ret; } /************************************************************************************************ * @ 函 数 名:NTP_DateToTimeStamp * @ 功能说明:日期转换成时间戳 * @ 参 数:NTP_TimeStruct:时间结构体 * @ 返 回 值:时间戳 ************************************************************************************************/ uint32_t NTP_DateToTimeStamp(NTP_TimeTypeDef *p_NTP_TimeStruct) { uint16_t year_temp = NTP_START_TIME_YEAR, month_temp = 1; //时间戳的起始时间 uint32_t day_num = 0, second_num = 0; //保存当前时间到时间戳起始时间的天数 //1.计算从NTP_START_TIME_YEAR年到当前时间前一年的总天数 如当前时间为2021.6.15 12.30.30,就计算从NTP_START_TIME_YEAR年到2020年的总天数 while (year_temp < p_NTP_TimeStruct->year) { if (NTP_Leapyearjudgment(year_temp))//判断是否为闰年 { day_num += 366;//闰年366天 } else { day_num += 365;//平年365天 } year_temp++; } //2.计算当前年份的1月到前一个月的总天数 如当前时间为2021.6.15 12.30.30,就计算从2021年1月到5月的总天数 while (month_temp < (p_NTP_TimeStruct->month)) { if (NTP_Leapyearjudgment(p_NTP_TimeStruct->year)) //判断当前年是否为闰年 { day_num += leap_month_days[month_temp - 1];//闰年各个月的天数 } else { day_num += peace_month_days[month_temp - 1];//平年各个月的天数 } month_temp++; } //3.计算当前月份的1号到前一天的天数 如当前时间为2021.6.15 12.30.30,就计算从2021.6.1到2021.6.14的总天数为14天 day_num += (p_NTP_TimeStruct->date - 1); //4.计算当前时分秒 second_num = day_num * 24 * 60 * 60; //从1900/1/1 00:00:00到当前时间前一天的总秒数 second_num += p_NTP_TimeStruct->hour * 60 * 60; second_num += p_NTP_TimeStruct->minute * 60; second_num += p_NTP_TimeStruct->second; return second_num; } /************************************************************************************************ * @ 函 数 名:NTP_TimeStampToDate * @ 功能说明:时间戳转换成日期 * @ 参 数:Time_Stamp:时间戳 NTP_TimeStruct:时间结构体 * @ 返 回 值:无 ************************************************************************************************/ void NTP_TimeStampToDate(uint32_t Time_Stamp, NTP_TimeTypeDef *p_NTP_TimeStruct) { uint32_t totle_day_num = 0, totle_second_num = 0; uint16_t remain_day_of_year = 0; uint16_t year_temp = 0; uint8_t *p_str = NULL; totle_day_num = (Time_Stamp) / (24 * 60 * 60); //计算过去的总天数(注意加括号) totle_second_num = (Time_Stamp) % (24 * 60 * 60); //计算当天过去的秒数 memset((void *)p_NTP_TimeStruct, 0x00, sizeof(NTP_TimeTypeDef)); //1.先计算时分秒 HH:MM:SS p_NTP_TimeStruct->hour = totle_second_num / 3600; p_NTP_TimeStruct->minute = (totle_second_num % 3600) / 60; p_NTP_TimeStruct->second = (totle_second_num % 3600) % 60; //2.计算哪一年 p_NTP_TimeStruct->year = NTP_START_TIME_YEAR + (totle_day_num / FOUR_YEAR_DAYS) * 4; //4年为一个周期,先计算过了几个4年周期,比如当前为2021,计算出来就是2020,那么就是从NTP_START_TIME_YEAR~2019这120年 remain_day_of_year += totle_day_num % FOUR_YEAR_DAYS; //4年一个周期剩余的天数 if (NTP_START_TIME_YEAR == 1900) //因为1900~1903连续四年都是平年,所以如果是从1900年开始计算,计算完后得重新加1天 { remain_day_of_year++; } year_temp = NTP_Leapyearjudgment(p_NTP_TimeStruct->year) ? 366 : 365;//计算当前年的天数 while (remain_day_of_year >= year_temp) // 判断剩余的天数满不满一年的天数 { p_NTP_TimeStruct->year++; remain_day_of_year -= year_temp; year_temp = NTP_Leapyearjudgment(p_NTP_TimeStruct->year) ? 366 : 365; } //3.计算哪一月的哪一天 p_str = NTP_Leapyearjudgment(p_NTP_TimeStruct->year) ? leap_month_days : peace_month_days; remain_day_of_year++; // 这里开始计算具体日期,remain_day_of_year为 0 时其实是 1 号,所以这里要先 +1 while (remain_day_of_year > *(p_str + p_NTP_TimeStruct->month)) { remain_day_of_year -= *(p_str + p_NTP_TimeStruct->month); p_NTP_TimeStruct->month++; } p_NTP_TimeStruct->month++; p_NTP_TimeStruct->date = remain_day_of_year; p_NTP_TimeStruct->week = NTP_WeekCalculate(p_NTP_TimeStruct); } /************************************************************************************************ * @ 函 数 名:NTP_GetMessageTime * @ 功能说明:从NTP回复的报文中获取时间 * @ 参 数:p_Message_Buf:报文缓冲区 Time_Start_Addr:报文中时间的起始位置 p_NTP_TimeStruct:时间结构体 * @ 返 回 值: ************************************************************************************************/ static void NTP_GetMessageTime(uint8_t* p_Message_Buf, uint16_t Time_Start_Addr, NTP_TimeTypeDef *p_NTP_TimeStruct) { unsigned long long second = 0; for (uint8_t i = 0; i < 4; i++) { second = (second << 8) | p_Message_Buf[Time_Start_Addr + i]; } second += 8 * 3600; p_NTP_TimeStruct->time_s = second; } int main() { WSADATA WSAData; SOCKET client_socket; //客户端的Socket SOCKADDR_IN server_addr; //服务器的地址数据结构 SOCKADDR_IN sock; uint8_t send_buf[BUFFER_SIZE]; //发送数据的缓冲区 uint8_t rece_buf[BUFFER_SIZE]; //接受数据的缓冲区 NTP_TimeTypeDef NTP_TimeStruct; if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0) { printf("初始化失败!"); return -1; } //创建客户端的Socket client_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//创建客户端的Socket if (client_socket == INVALID_SOCKET) { printf("创建套接字失败!"); } /* 设置阻塞超时 */ struct timeval timeOut; timeOut.tv_sec = 5; //设置5s超时 timeOut.tv_usec = 0; if (setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeOut, sizeof(timeOut)) < 0) { printf("time out setting failed\n"); } //配置服务器的IP地址和端口号 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(123);//端口号为4567 server_addr.sin_addr.s_addr = inet_addr("120.25.108.11"); //127.0.0.1为本电脑IP地址 int len = sizeof(sock); while (1) { uint8_t message_buf[48] = { 0 }; memset(&rece_buf, 0, sizeof(rece_buf)); NTP_GenerateReqMessage(message_buf); sendto(client_socket, message_buf, sizeof(message_buf), 0, (SOCKADDR*)&server_addr, sizeof(SOCKADDR)); int recv_len = recvfrom(client_socket, rece_buf, sizeof(rece_buf), 0, (SOCKADDR*)&sock, &len); if (recv_len >= 48)//接收到服务器返回的数据 { for (int i = 0; i < recv_len; i++) { printf("%02x ", rece_buf[i]); } printf("\r\n"); printf("\r\n"); NTP_GetMessageTime(rece_buf, 40, &NTP_TimeStruct); //从回复的报文中获取时间,回复数据包中时间的首地址为40 NTP_TimeStampToDate(NTP_TimeStruct.time_s, &NTP_TimeStruct); //由UTC时间计算日期 unsigned int time_temp = NTP_DateToTimeStamp(&NTP_TimeStruct); printf("%04d-%02d-%02dT%02d:%02d:%02d", NTP_TimeStruct.year, NTP_TimeStruct.month, NTP_TimeStruct.date, NTP_TimeStruct.hour, NTP_TimeStruct.minute, NTP_TimeStruct.second); printf("\r\n"); printf("\r\n"); printf("%u", time_temp); printf("\r\n"); printf("\r\n"); } Sleep(5000); } closesocket(client_socket); WSACleanup(); return 0; }