NTP入门介绍

这篇文章除了代码部分,其他均为我从其他人文章处搬运过来的,只阐述我个人的阅读思路,读者如果看到写的不好的地方敬请谅解,可以从文章底部原文跳转查看!

概述

  • 网络时间协议,英文名称:Network Time Protocol(NTP) 是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,
    它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒), 且可介由加密确认的方式来防止恶毒的协议攻击。NTP的目的是在无序的Internet环境中提供精确和健壮的时间服务。

  • NTP 基于 UDP 报文进行传输, 使用的UDP端口号为 123.

  • 使用 NTP 的目的是对网络内所有具有时钟的设备进行时钟同步, 使网络内所有设备的时钟保持一致, 从而使设备能够提供基于统一时间的多种应用.

  • 对于运行 NTP 的本地系统, 既可以接收来自其他时钟源的同步, 又可以作为时钟源同步其他的时钟, 并且可以和其他设备相互同步.

工作原理

实现方式

  • 无线时钟: 服务器系统可以通过串口连接一个无线时钟. 无线时钟接收 GPS 的卫星发射的信号来决定当前时间. 无线时钟是一个非常精确的时间源, 但是需要花一定的费用.
  • 时间服务器: 还可以使用网络中 NTP 时间服务器, 通过这个服务器来同步网络中的系统的时钟.
  • 域网内的同步: 如果只是需要在本局域网内进行系统间的时钟同步, 那么就可以使用局域网中任何一个系统的时钟. 你需要选择局域网中的一个节点的时钟作”权威的”的时间源, 然后其它的节点就只需要与这个时间源进行时间同步即可. 使用这种方式,
    所有的节点都会使用一个公共的系统时钟, 但是不需要和局域网外的系统进行时钟同步. 如果一个系统在一个局域网的内部, 同时又不能使用无线时钟, 这种方式是最好的选择.

工作流程

  • 这里目前我只列举【NTP服务端与客户端的交互过程】这一工作模式,还有许多其他的工作模式, 请参考这边文章,东西写的是真的详细,我的基本都是从这里搬运过来的.
    NTP 协议简单分析

NTP服务端与客户端的交互过程

客户端和服务端都有一个时间轴,分别代表着各自系统的时间,当客户端想要同步服务端的时间时,客户端会构造一个NTP协议包发送到NTP服务端,客户端会记下此时发送的时间t0,经过一段网络延时传输后,服务器在t1时刻收到数据包,经过一段时间处理后在t2时刻向客户端返回数据包,
再经过一段网络延时传输后客户端在t3时刻收到NTP服务器数据包。
t0和t3是客户端时间系统的时间、t1和t2是NTP服务端时间系统的时间,它们是有区别的。 t0、t1、t2分别对应着server->cient NTP报文中的三个参数:

t0:origin timestamp(客户端发送数据包的时间)
t1: receive timestamp(服务器接收到数据包的时间)
t2: transmit timestamp(服务器返回数据包时间)
t3: client receive timestamp(客户端收到回复报文时本地的时间)。

延时和时间偏差计算

假设:客户端与服务端的时间系统的偏差定义为θ、网络的往/返延迟(单程延时)定义为δ
推导过程:

  1. 根据交互原理,可以列出方程组:
1
2
t0+θ+δ=t1
t2-θ+δ=t3 // 这里我也还没理解为啥是这样的? 如果看到这里的同学可以思考一下
  1. 求解方程组,得到以下结果:
1
2
θ=(t1-t0+t2-t3)/2  
δ=(t1-t0+t3-t2)/2

记忆时可以采用极限法,分别假设延时和偏差为0.

客户端时间校准

对于时间要求不那么精准设备,客户端可把服务端端的返回时间t2固化为本地时间。

但是作为一个标准的通信协议,必须计算上网络的传输延时,需要把t2+δ固化为本地时间。

以上client时间校准算法只为理解过程,不代表真实做法

NTP报文

NTP报文示例

其中192.10.10.189为NTP的server端,192.10.10.32为client端。

NTP报文格式

  • NTP 有两种不同类型的报文, 一种是 时钟同步报文, 一种是 控制报文.
  • 控制报文 仅用于需要网络管理的场合, 对于时钟同步功能不是必需的, 暂不做分析.
  • 时钟同步报文格式如下:

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

  • LI (Leap Indicator): 闰秒标识器, 长度为 2 Bits, 用来预警最近一分钟插入 1s 或者删除 1s.
LI Value 含义
00 0 无预告
01 1 醉经一分钟有 61s
10 2 最近一分钟有 59s
11 3 警告状态(时钟未同步)
  • VN (Version Number): 版本号, 长度为 3 Bits, 目前最新的版本是 4, 向下兼容指定于 RFC 1305 的版本 3.
  • Mode: 工作模式, 长度为 3 Bits.

点对点模式下, 客户端请求时设置此字段为 3, 服务器应答时设置此字段为 4. 广播模式下, 服务器应答设置此字段为 5.

Mode Value 含义
000 0 保留
001 1 主动对称模式
010 2 被动对称模式
011 3 客户端模式
100 4 服务器模式
101 5 广播或组播模式
110 6 NTP控制报文
111 7 预留给内部使用
  • Stratum: 系统时钟的层数, 长度为 8 Bits, 取值范围 1~16, 定义时钟的准确度. 层数为 1 的时钟准确度最高, 准确度从 1 到 16 依次递减, 阶层的上限为15, 层数为 16的时钟处于未同步状态,
    不能作为参考时钟.
    • NTP 获得 UTC 的时间来源可以是原子钟, 天文台, 卫星, 也可以从Internet上获取.
    • stratum-0 是高精度计时设备, 例如原子钟 (如铯, 铷), GPS时钟或其他无线电时钟. 它们生成非常精确的脉冲秒信号, 触发所连接计算机上的中断和时间戳. 也称为参考 (基准) 时钟.
    • stratum-1 是与 stratum-0 设备相连, 在几微秒误差内同步系统时钟的计算机.
    • 时间是按 NTP 服务器的等级传播. 按照距离外部 UTC 源的远近将所有服务器归入不同的 Stratum (层) 中. Stratum-1 在顶层, 有外部 UTC 接入, 而Stratum-2 则从 Stratum-1
      获取时间, Stratum-3 从 Stratum-2 获取时间, 以此类推, 但 Stratum 层的总数限制在15以内. 所有这些服务器在逻辑上形成阶梯式的架构并相互连接, 而 Stratum-1
      的时间服务器是整个系统的基础.
stratum 含义
0 未指定或者难以获得
1 主要参考(如: 无线电时钟,校正的原子时钟)
2~15 第二参考(Via NTP)
16 未同步状态, 不能作为参考时钟
  • Poll: 轮询间隔时间, 长度为 8 Bits, 两个连续NTP报文之间的时间间隔, 用 2 的幂来表示, 比如值为 6 表示最小间隔为 2^6 = 64s.
  • Precision: 系统时钟的精度, 长度为 8 Bits, 用 2 的幂来表示, 比如 50Hz(20ms)或者60Hz(16.67ms) 可以表示成值 -5 (2^-5 = 0.03125s = 31.25ms).
  • Root Delay: 本地到主参考时钟源的往返时间, 长度为 32 Bits, 有 15~16 位小数部分的无符号定点小数.
  • Root Dispersion: 系统时钟相对于主参考时钟的最大误差, 长度为 32 Bits, 有 15~16 位小数部分的无符号定点小数.
  • Reference Identifier: 参考时钟源的标识, 长度为 32 Bits.
  • Reference Timestamp: 系统时钟最后一次被设定或更新的时间, 长度为 64 Bits, 无符号定点数, 前 32 Bits 表示整数部分, 后 32 Bits 表示小数部分, 理论分辨率 2^−32s.
  • Originate Timestamp: NTP请求报文离开发送端时发送端的本地时间, 长度为 64 Bits.
  • Receive Timestamp: NTP请求报文到达接收端时接收端的本地时间, 长度为 64 Bits.
  • Transmit Timestamp: 应答报文离开应答者时应答者的本地时间, 长度为 64 Bits.
  • Authenticator(Optional): 验证信息, 长度为 96 Bits, (可选信息), 当实现了 NTP 认证模式时, 主要标识符和信息数字域就包括已定义的信息认证代码 (MAC) 信息.
说明

(这里是最容易被坑到的) 时间戳的记录以秒的形式从 1900-01-01 00:00:00 算起. NTP的时间精度在 WAN 为 数十毫秒, 在 LAN 为 亚毫秒级甚至更高, 在 Internet 上绝大部分能提供 1-50ms 的精确度, 取决于同步源和网络路径等特性. 比如: 当前时间为 1902-01-01 01:01:01, 与 1900 的参考时间相差为:
(3652246060+3600+60+1) = 63075661s = 0x03C2754D s. 转换成二进制: 0000 0011 1100 0010 0111 0101 0100 1101 XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX. 因为只有 32 Bits 表示秒数, 所以到了 2036 年数据就会溢出. 所以以 136 年为一个周期置 0 , 会用一些外部的方法来表示是相对于 1900 年还是 2036 年的时间. NTP 的未来版本可能将时间表示扩展到 128 Bits: 其中 64 Bits 表示秒, 64 Bits 表示秒的小数. 当前的 NTPv4 格式支持 “时代数字” (Era Number)和 “时代偏移” (Era Offset), 正确使用它们应该有助于解决日期翻转问题. 据Mills称: “64 Bits 秒的小数足以分辨光子以光速通过电子所需的时间. 64 Bits 的秒足以提供明确的时间表示, 直到宇宙变暗.”

java代码实现

这里的实现方式其实是有很多种,且并非java代码来实现的,但是由于我比较擅长java语言,所以这里就以java语言为示例来展示ntp的实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* NPT java Demo
*
* @auther Lengff
* @time 2021/11/13
* @fileName NTPDmo
*/
public class NTPDmo {
private static Logger log = LoggerFactory.getLogger(NTPDmo.class);

public static void main(String[] args) {
try {
byte[] bytes = new byte[48];
bytes[0] = getHeader();
InetAddress address = InetAddress.getByName("time.windows.com");
// 请求部分其实是有很多数据的,具体的请看参考请求报文说明,这里我们就只设置一个请求头部分即可
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, 123);
long t0 = System.currentTimeMillis();
DatagramSocket socket = new DatagramSocket();
socket.send(packet);
socket.receive(packet);
long t3 = System.currentTimeMillis();
byte[] data = packet.getData();
long t1 = getTimestamp(data, 32, 36);
long t2 = getTimestamp(data, 40, 44);
log.info("t0 ===> {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date(t0)));
log.info("t1 ===> {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date(t1)));
log.info("t2 ===> {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date(t2)));
log.info("t3 ===> {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date(t3)));
long diff = (t1 + t2 - t0 - t3) / 2;
long delay = (t1 - t2 - t0 + t3) / 2;
log.info("系统偏差为:{}ms", diff);
log.info("网络延迟为:{}ms", delay);
log.info("最终同步时间为 ===> {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date(System.currentTimeMillis() + diff)));
socket.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}

/**
* 获取请求报文头信息
*
* @return
*/
public static byte getHeader() {
// LI (Leap Indicator): 闰秒标识器, 长度为 2 Bits, 用来预警最近一分钟插入 1s 或者删除 1s.
int li = 3;
// VN (Version Number): 版本号, 长度为 3 Bits, 目前最新的版本是 4, 向下兼容指定于 RFC 1305 的版本 3.
int VN = 4;
// Mode: 工作模式, 长度为 3 Bits. 3:客户端模式 4: 服务端模式
int mode = 3;

int data = 0;
data += li << 6;
data += VN << 3;
data += mode;
return (byte) data;
}

/**
* 获取NTP返回数据中的时间戳
* 这里最容易遇到坑,大多数用户只是说明返回的时间戳是一个64bit的数据,但是并没有说明这64bit数据的格式是什么样的
* 这里简单描述一下就是 前面的32bit是时间戳的秒数(是用1900-01-01 00:00:00开始的秒数,但是我们的是1970年,所以需要减掉2208988800秒)
* 后面的32bit是更细一些的小数部分的数据,需要特殊处理一下,
* 具体请参考这篇博客(写的非常详细和全面): https://blog.srefan.com/2017/07/ntp-protocol/
*
* @param data NTP返回数据
* @param second 秒数部分起始下标
* @param fraction 小数部分起始下标
* @return 时间戳
*/
public static long getTimestamp(byte[] data, int second, int fraction) {
long secondsLong = Integer.toUnsignedLong(getInt(data, second));
long fractionLong = Integer.toUnsignedLong(getInt(data, fraction));
long time = (secondsLong - 2208988800L) * 1000;
// 这里为啥要这么做,我目前也不得而知,大概意思就是转成二进制处理,最后会得到一个小数
long milli = (fractionLong * 1000 + Integer.MIN_VALUE) / 0x100000000L;
log.info("获取的时间戳为:{}", time + milli);
return time + milli;
}

/**
* 从bytes中获取int数值
*
* @param data bytes
* @param index 起始下标
* @return int 整数
*/
public static int getInt(byte[] data, int index) {
int temp = 0;
for (int i = index; i < 4 + index; i++) {
temp += (data[i] & 0xff) << ((3 - i) * 8);
}
return temp;
}
}

参考文章

一开始看了很多篇都大差不差的文章,总是没办法完整的理解ntp协议的含义,后面google到一篇发现讲的非常的好,这里我不得不吐槽baidu这个垃圾了,同样的关键字google确实能查到更优质的内容。

特别感谢这篇

下面这些也很重要